mirror of
https://codeberg.org/polarisfm/youtube-dl
synced 2024-11-30 20:27:54 +01:00
Merge commit '5d90a8a'
merge upstream youtube-dl into local mod
This commit is contained in:
commit
e2e16b8d42
7
.github/ISSUE_TEMPLATE.md
vendored
7
.github/ISSUE_TEMPLATE.md
vendored
@ -6,12 +6,13 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Make sure you are using the *latest* version: run `youtube-dl --version` and ensure your version is *2017.11.15*. If it's not, read [this FAQ entry](https://github.com/rg3/youtube-dl/blob/master/README.md#how-do-i-update-youtube-dl) and update. Issues with outdated version will be rejected.
|
### Make sure you are using the *latest* version: run `youtube-dl --version` and ensure your version is *2018.10.05*. If it's not, read [this FAQ entry](https://github.com/rg3/youtube-dl/blob/master/README.md#how-do-i-update-youtube-dl) and update. Issues with outdated version will be rejected.
|
||||||
- [ ] I've **verified** and **I assure** that I'm running youtube-dl **2017.11.15**
|
- [ ] I've **verified** and **I assure** that I'm running youtube-dl **2018.10.05**
|
||||||
|
|
||||||
### Before submitting an *issue* make sure you have:
|
### Before submitting an *issue* make sure you have:
|
||||||
- [ ] At least skimmed through the [README](https://github.com/rg3/youtube-dl/blob/master/README.md), **most notably** the [FAQ](https://github.com/rg3/youtube-dl#faq) and [BUGS](https://github.com/rg3/youtube-dl#bugs) sections
|
- [ ] At least skimmed through the [README](https://github.com/rg3/youtube-dl/blob/master/README.md), **most notably** the [FAQ](https://github.com/rg3/youtube-dl#faq) and [BUGS](https://github.com/rg3/youtube-dl#bugs) sections
|
||||||
- [ ] [Searched](https://github.com/rg3/youtube-dl/search?type=Issues) the bugtracker for similar issues including closed ones
|
- [ ] [Searched](https://github.com/rg3/youtube-dl/search?type=Issues) the bugtracker for similar issues including closed ones
|
||||||
|
- [ ] Checked that provided video/audio/playlist URLs (if any) are alive and playable in a browser
|
||||||
|
|
||||||
### What is the purpose of your *issue*?
|
### What is the purpose of your *issue*?
|
||||||
- [ ] Bug report (encountered problems with youtube-dl)
|
- [ ] Bug report (encountered problems with youtube-dl)
|
||||||
@ -35,7 +36,7 @@ Add the `-v` flag to **your command line** you run youtube-dl with (`youtube-dl
|
|||||||
[debug] User config: []
|
[debug] User config: []
|
||||||
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
[debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj']
|
||||||
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
[debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251
|
||||||
[debug] youtube-dl version 2017.11.15
|
[debug] youtube-dl version 2018.10.05
|
||||||
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
[debug] Python version 2.7.11 - Windows-2003Server-5.2.3790-SP2
|
||||||
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
[debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4
|
||||||
[debug] Proxy map: {}
|
[debug] Proxy map: {}
|
||||||
|
1
.github/ISSUE_TEMPLATE_tmpl.md
vendored
1
.github/ISSUE_TEMPLATE_tmpl.md
vendored
@ -12,6 +12,7 @@
|
|||||||
### Before submitting an *issue* make sure you have:
|
### Before submitting an *issue* make sure you have:
|
||||||
- [ ] At least skimmed through the [README](https://github.com/rg3/youtube-dl/blob/master/README.md), **most notably** the [FAQ](https://github.com/rg3/youtube-dl#faq) and [BUGS](https://github.com/rg3/youtube-dl#bugs) sections
|
- [ ] At least skimmed through the [README](https://github.com/rg3/youtube-dl/blob/master/README.md), **most notably** the [FAQ](https://github.com/rg3/youtube-dl#faq) and [BUGS](https://github.com/rg3/youtube-dl#bugs) sections
|
||||||
- [ ] [Searched](https://github.com/rg3/youtube-dl/search?type=Issues) the bugtracker for similar issues including closed ones
|
- [ ] [Searched](https://github.com/rg3/youtube-dl/search?type=Issues) the bugtracker for similar issues including closed ones
|
||||||
|
- [ ] Checked that provided video/audio/playlist URLs (if any) are alive and playable in a browser
|
||||||
|
|
||||||
### What is the purpose of your *issue*?
|
### What is the purpose of your *issue*?
|
||||||
- [ ] Bug report (encountered problems with youtube-dl)
|
- [ ] Bug report (encountered problems with youtube-dl)
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -47,3 +47,7 @@ youtube-dl.zsh
|
|||||||
*.iml
|
*.iml
|
||||||
|
|
||||||
tmp/
|
tmp/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# VS Code related files
|
||||||
|
.vscode
|
||||||
|
13
.travis.yml
13
.travis.yml
@ -7,16 +7,21 @@ python:
|
|||||||
- "3.4"
|
- "3.4"
|
||||||
- "3.5"
|
- "3.5"
|
||||||
- "3.6"
|
- "3.6"
|
||||||
|
- "pypy"
|
||||||
|
- "pypy3"
|
||||||
sudo: false
|
sudo: false
|
||||||
env:
|
env:
|
||||||
- YTDL_TEST_SET=core
|
- YTDL_TEST_SET=core
|
||||||
- YTDL_TEST_SET=download
|
- YTDL_TEST_SET=download
|
||||||
matrix:
|
matrix:
|
||||||
|
include:
|
||||||
|
- env: JYTHON=true; YTDL_TEST_SET=core
|
||||||
|
- env: JYTHON=true; YTDL_TEST_SET=download
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- env: YTDL_TEST_SET=download
|
- env: YTDL_TEST_SET=download
|
||||||
|
- env: JYTHON=true; YTDL_TEST_SET=core
|
||||||
|
- env: JYTHON=true; YTDL_TEST_SET=download
|
||||||
|
before_install:
|
||||||
|
- if [ "$JYTHON" == "true" ]; then ./devscripts/install_jython.sh; export PATH="$HOME/jython/bin:$PATH"; fi
|
||||||
script: ./devscripts/run_tests.sh
|
script: ./devscripts/run_tests.sh
|
||||||
notifications:
|
|
||||||
email:
|
|
||||||
- filippo.valsorda@gmail.com
|
|
||||||
- yasoob.khld@gmail.com
|
|
||||||
|
15
AUTHORS
15
AUTHORS
@ -231,3 +231,18 @@ John Dong
|
|||||||
Tatsuyuki Ishi
|
Tatsuyuki Ishi
|
||||||
Daniel Weber
|
Daniel Weber
|
||||||
Kay Bouché
|
Kay Bouché
|
||||||
|
Yang Hongbo
|
||||||
|
Lei Wang
|
||||||
|
Petr Novák
|
||||||
|
Leonardo Taccari
|
||||||
|
Martin Weinelt
|
||||||
|
Surya Oktafendri
|
||||||
|
TingPing
|
||||||
|
Alexandre Macabies
|
||||||
|
Bastian de Groot
|
||||||
|
Niklas Haas
|
||||||
|
András Veres-Szentkirályi
|
||||||
|
Enes Solak
|
||||||
|
Nathan Rossi
|
||||||
|
Thomas van der Berg
|
||||||
|
Luca Cherubin
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
include README.md
|
include README.md
|
||||||
include test/*.py
|
include LICENSE
|
||||||
include test/*.json
|
include AUTHORS
|
||||||
|
include ChangeLog
|
||||||
include youtube-dl.bash-completion
|
include youtube-dl.bash-completion
|
||||||
include youtube-dl.fish
|
include youtube-dl.fish
|
||||||
include youtube-dl.1
|
include youtube-dl.1
|
||||||
recursive-include docs Makefile conf.py *.rst
|
recursive-include docs Makefile conf.py *.rst
|
||||||
|
recursive-include test *
|
||||||
|
25
Makefile
25
Makefile
@ -14,6 +14,9 @@ PYTHON ?= /usr/bin/env python
|
|||||||
# set SYSCONFDIR to /etc if PREFIX=/usr or PREFIX=/usr/local
|
# set SYSCONFDIR to /etc if PREFIX=/usr or PREFIX=/usr/local
|
||||||
SYSCONFDIR = $(shell if [ $(PREFIX) = /usr -o $(PREFIX) = /usr/local ]; then echo /etc; else echo $(PREFIX)/etc; fi)
|
SYSCONFDIR = $(shell if [ $(PREFIX) = /usr -o $(PREFIX) = /usr/local ]; then echo /etc; else echo $(PREFIX)/etc; fi)
|
||||||
|
|
||||||
|
# set markdown input format to "markdown-smart" for pandoc version 2 and to "markdown" for pandoc prior to version 2
|
||||||
|
MARKDOWN = $(shell if [ `pandoc -v | head -n1 | cut -d" " -f2 | head -c1` = "2" ]; then echo markdown-smart; else echo markdown; fi)
|
||||||
|
|
||||||
install: youtube-dl youtube-dl.1 youtube-dl.bash-completion youtube-dl.zsh youtube-dl.fish
|
install: youtube-dl youtube-dl.1 youtube-dl.bash-completion youtube-dl.zsh youtube-dl.fish
|
||||||
install -d $(DESTDIR)$(BINDIR)
|
install -d $(DESTDIR)$(BINDIR)
|
||||||
install -m 755 youtube-dl $(DESTDIR)$(BINDIR)
|
install -m 755 youtube-dl $(DESTDIR)$(BINDIR)
|
||||||
@ -36,8 +39,17 @@ test:
|
|||||||
|
|
||||||
ot: offlinetest
|
ot: offlinetest
|
||||||
|
|
||||||
|
# Keep this list in sync with devscripts/run_tests.sh
|
||||||
offlinetest: codetest
|
offlinetest: codetest
|
||||||
$(PYTHON) -m nose --verbose test --exclude test_download.py --exclude test_age_restriction.py --exclude test_subtitles.py --exclude test_write_annotations.py --exclude test_youtube_lists.py --exclude test_iqiyi_sdk_interpreter.py --exclude test_socks.py
|
$(PYTHON) -m nose --verbose test \
|
||||||
|
--exclude test_age_restriction.py \
|
||||||
|
--exclude test_download.py \
|
||||||
|
--exclude test_iqiyi_sdk_interpreter.py \
|
||||||
|
--exclude test_socks.py \
|
||||||
|
--exclude test_subtitles.py \
|
||||||
|
--exclude test_write_annotations.py \
|
||||||
|
--exclude test_youtube_lists.py \
|
||||||
|
--exclude test_youtube_signature.py
|
||||||
|
|
||||||
tar: youtube-dl.tar.gz
|
tar: youtube-dl.tar.gz
|
||||||
|
|
||||||
@ -73,11 +85,11 @@ supportedsites:
|
|||||||
$(PYTHON) devscripts/make_supportedsites.py docs/supportedsites.md
|
$(PYTHON) devscripts/make_supportedsites.py docs/supportedsites.md
|
||||||
|
|
||||||
README.txt: README.md
|
README.txt: README.md
|
||||||
pandoc -f markdown -t plain README.md -o README.txt
|
pandoc -f $(MARKDOWN) -t plain README.md -o README.txt
|
||||||
|
|
||||||
youtube-dl.1: README.md
|
youtube-dl.1: README.md
|
||||||
$(PYTHON) devscripts/prepare_manpage.py youtube-dl.1.temp.md
|
$(PYTHON) devscripts/prepare_manpage.py youtube-dl.1.temp.md
|
||||||
pandoc -s -f markdown -t man youtube-dl.1.temp.md -o youtube-dl.1
|
pandoc -s -f $(MARKDOWN) -t man youtube-dl.1.temp.md -o youtube-dl.1
|
||||||
rm -f youtube-dl.1.temp.md
|
rm -f youtube-dl.1.temp.md
|
||||||
|
|
||||||
youtube-dl.bash-completion: youtube_dl/*.py youtube_dl/*/*.py devscripts/bash-completion.in
|
youtube-dl.bash-completion: youtube_dl/*.py youtube_dl/*/*.py devscripts/bash-completion.in
|
||||||
@ -101,7 +113,7 @@ _EXTRACTOR_FILES = $(shell find youtube_dl/extractor -iname '*.py' -and -not -in
|
|||||||
youtube_dl/extractor/lazy_extractors.py: devscripts/make_lazy_extractors.py devscripts/lazy_load_template.py $(_EXTRACTOR_FILES)
|
youtube_dl/extractor/lazy_extractors.py: devscripts/make_lazy_extractors.py devscripts/lazy_load_template.py $(_EXTRACTOR_FILES)
|
||||||
$(PYTHON) devscripts/make_lazy_extractors.py $@
|
$(PYTHON) devscripts/make_lazy_extractors.py $@
|
||||||
|
|
||||||
youtube-dl.tar.gz: youtube-dl README.md README.txt youtube-dl.1 youtube-dl.bash-completion youtube-dl.zsh youtube-dl.fish ChangeLog
|
youtube-dl.tar.gz: youtube-dl README.md README.txt youtube-dl.1 youtube-dl.bash-completion youtube-dl.zsh youtube-dl.fish ChangeLog AUTHORS
|
||||||
@tar -czf youtube-dl.tar.gz --transform "s|^|youtube-dl/|" --owner 0 --group 0 \
|
@tar -czf youtube-dl.tar.gz --transform "s|^|youtube-dl/|" --owner 0 --group 0 \
|
||||||
--exclude '*.DS_Store' \
|
--exclude '*.DS_Store' \
|
||||||
--exclude '*.kate-swp' \
|
--exclude '*.kate-swp' \
|
||||||
@ -110,11 +122,10 @@ youtube-dl.tar.gz: youtube-dl README.md README.txt youtube-dl.1 youtube-dl.bash-
|
|||||||
--exclude '*~' \
|
--exclude '*~' \
|
||||||
--exclude '__pycache__' \
|
--exclude '__pycache__' \
|
||||||
--exclude '.git' \
|
--exclude '.git' \
|
||||||
--exclude 'testdata' \
|
|
||||||
--exclude 'docs/_build' \
|
--exclude 'docs/_build' \
|
||||||
-- \
|
-- \
|
||||||
bin devscripts test youtube_dl docs \
|
bin devscripts test youtube_dl docs \
|
||||||
ChangeLog LICENSE README.md README.txt \
|
ChangeLog AUTHORS LICENSE README.md README.txt \
|
||||||
Makefile MANIFEST.in youtube-dl.1 youtube-dl.bash-completion \
|
Makefile MANIFEST.in youtube-dl.1 youtube-dl.bash-completion \
|
||||||
youtube-dl.zsh youtube-dl.fish setup.py \
|
youtube-dl.zsh youtube-dl.fish setup.py setup.cfg \
|
||||||
youtube-dl
|
youtube-dl
|
||||||
|
48
README.md
48
README.md
@ -17,7 +17,7 @@ youtube-dl - download videos from youtube.com or other video platforms
|
|||||||
|
|
||||||
# INSTALLATION
|
# INSTALLATION
|
||||||
|
|
||||||
To install it right away for all UNIX users (Linux, OS X, etc.), type:
|
To install it right away for all UNIX users (Linux, macOS, etc.), type:
|
||||||
|
|
||||||
sudo curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl
|
sudo curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl
|
||||||
sudo chmod a+rx /usr/local/bin/youtube-dl
|
sudo chmod a+rx /usr/local/bin/youtube-dl
|
||||||
@ -35,7 +35,7 @@ You can also use pip:
|
|||||||
|
|
||||||
This command will update youtube-dl if you have already installed it. See the [pypi page](https://pypi.python.org/pypi/youtube_dl) for more information.
|
This command will update youtube-dl if you have already installed it. See the [pypi page](https://pypi.python.org/pypi/youtube_dl) for more information.
|
||||||
|
|
||||||
OS X users can install youtube-dl with [Homebrew](https://brew.sh/):
|
macOS users can install youtube-dl with [Homebrew](https://brew.sh/):
|
||||||
|
|
||||||
brew install youtube-dl
|
brew install youtube-dl
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ Or with [MacPorts](https://www.macports.org/):
|
|||||||
Alternatively, refer to the [developer instructions](#developer-instructions) for how to check out and work with the git repository. For further options, including PGP signatures, see the [youtube-dl Download Page](https://rg3.github.io/youtube-dl/download.html).
|
Alternatively, refer to the [developer instructions](#developer-instructions) for how to check out and work with the git repository. For further options, including PGP signatures, see the [youtube-dl Download Page](https://rg3.github.io/youtube-dl/download.html).
|
||||||
|
|
||||||
# DESCRIPTION
|
# DESCRIPTION
|
||||||
**youtube-dl** is a command-line program to download videos from YouTube.com and a few more sites. It requires the Python interpreter, version 2.6, 2.7, or 3.2+, and it is not platform specific. It should work on your Unix box, on Windows or on Mac OS X. It is released to the public domain, which means you can modify it, redistribute it or use it however you like.
|
**youtube-dl** is a command-line program to download videos from YouTube.com and a few more sites. It requires the Python interpreter, version 2.6, 2.7, or 3.2+, and it is not platform specific. It should work on your Unix box, on Windows or on macOS. It is released to the public domain, which means you can modify it, redistribute it or use it however you like.
|
||||||
|
|
||||||
youtube-dl [OPTIONS] URL [URL...]
|
youtube-dl [OPTIONS] URL [URL...]
|
||||||
|
|
||||||
@ -93,8 +93,8 @@ Alternatively, refer to the [developer instructions](#developer-instructions) fo
|
|||||||
|
|
||||||
## Network Options:
|
## Network Options:
|
||||||
--proxy URL Use the specified HTTP/HTTPS/SOCKS proxy.
|
--proxy URL Use the specified HTTP/HTTPS/SOCKS proxy.
|
||||||
To enable experimental SOCKS proxy, specify
|
To enable SOCKS proxy, specify a proper
|
||||||
a proper scheme. For example
|
scheme. For example
|
||||||
socks5://127.0.0.1:1080/. Pass in an empty
|
socks5://127.0.0.1:1080/. Pass in an empty
|
||||||
string (--proxy "") for direct connection
|
string (--proxy "") for direct connection
|
||||||
--socket-timeout SECONDS Time to wait before giving up, in seconds
|
--socket-timeout SECONDS Time to wait before giving up, in seconds
|
||||||
@ -106,16 +106,18 @@ Alternatively, refer to the [developer instructions](#developer-instructions) fo
|
|||||||
--geo-verification-proxy URL Use this proxy to verify the IP address for
|
--geo-verification-proxy URL Use this proxy to verify the IP address for
|
||||||
some geo-restricted sites. The default
|
some geo-restricted sites. The default
|
||||||
proxy specified by --proxy (or none, if the
|
proxy specified by --proxy (or none, if the
|
||||||
options is not present) is used for the
|
option is not present) is used for the
|
||||||
actual downloading.
|
actual downloading.
|
||||||
--geo-bypass Bypass geographic restriction via faking
|
--geo-bypass Bypass geographic restriction via faking
|
||||||
X-Forwarded-For HTTP header (experimental)
|
X-Forwarded-For HTTP header
|
||||||
--no-geo-bypass Do not bypass geographic restriction via
|
--no-geo-bypass Do not bypass geographic restriction via
|
||||||
faking X-Forwarded-For HTTP header
|
faking X-Forwarded-For HTTP header
|
||||||
(experimental)
|
|
||||||
--geo-bypass-country CODE Force bypass geographic restriction with
|
--geo-bypass-country CODE Force bypass geographic restriction with
|
||||||
explicitly provided two-letter ISO 3166-2
|
explicitly provided two-letter ISO 3166-2
|
||||||
country code (experimental)
|
country code
|
||||||
|
--geo-bypass-ip-block IP_BLOCK Force bypass geographic restriction with
|
||||||
|
explicitly provided IP block in CIDR
|
||||||
|
notation
|
||||||
|
|
||||||
## Video Selection:
|
## Video Selection:
|
||||||
--playlist-start NUMBER Playlist video to start at (default is 1)
|
--playlist-start NUMBER Playlist video to start at (default is 1)
|
||||||
@ -198,10 +200,15 @@ Alternatively, refer to the [developer instructions](#developer-instructions) fo
|
|||||||
size. By default, the buffer size is
|
size. By default, the buffer size is
|
||||||
automatically resized from an initial value
|
automatically resized from an initial value
|
||||||
of SIZE.
|
of SIZE.
|
||||||
|
--http-chunk-size SIZE Size of a chunk for chunk-based HTTP
|
||||||
|
downloading (e.g. 10485760 or 10M) (default
|
||||||
|
is disabled). May be useful for bypassing
|
||||||
|
bandwidth throttling imposed by a webserver
|
||||||
|
(experimental)
|
||||||
--playlist-reverse Download playlist videos in reverse order
|
--playlist-reverse Download playlist videos in reverse order
|
||||||
--playlist-random Download playlist videos in random order
|
--playlist-random Download playlist videos in random order
|
||||||
--xattr-set-filesize Set file xattribute ytdl.filesize with
|
--xattr-set-filesize Set file xattribute ytdl.filesize with
|
||||||
expected file size (experimental)
|
expected file size
|
||||||
--hls-prefer-native Use the native HLS downloader instead of
|
--hls-prefer-native Use the native HLS downloader instead of
|
||||||
ffmpeg
|
ffmpeg
|
||||||
--hls-prefer-ffmpeg Use ffmpeg instead of the native HLS
|
--hls-prefer-ffmpeg Use ffmpeg instead of the native HLS
|
||||||
@ -218,7 +225,9 @@ Alternatively, refer to the [developer instructions](#developer-instructions) fo
|
|||||||
|
|
||||||
## Filesystem Options:
|
## Filesystem Options:
|
||||||
-a, --batch-file FILE File containing URLs to download ('-' for
|
-a, --batch-file FILE File containing URLs to download ('-' for
|
||||||
stdin)
|
stdin), one URL per line. Lines starting
|
||||||
|
with '#', ';' or ']' are considered as
|
||||||
|
comments and ignored.
|
||||||
--id Use only video ID in file name
|
--id Use only video ID in file name
|
||||||
-o, --output TEMPLATE Output filename template, see the "OUTPUT
|
-o, --output TEMPLATE Output filename template, see the "OUTPUT
|
||||||
TEMPLATE" for all the info
|
TEMPLATE" for all the info
|
||||||
@ -418,9 +427,9 @@ Alternatively, refer to the [developer instructions](#developer-instructions) fo
|
|||||||
default; fix file if we can, warn
|
default; fix file if we can, warn
|
||||||
otherwise)
|
otherwise)
|
||||||
--prefer-avconv Prefer avconv over ffmpeg for running the
|
--prefer-avconv Prefer avconv over ffmpeg for running the
|
||||||
postprocessors (default)
|
|
||||||
--prefer-ffmpeg Prefer ffmpeg over avconv for running the
|
|
||||||
postprocessors
|
postprocessors
|
||||||
|
--prefer-ffmpeg Prefer ffmpeg over avconv for running the
|
||||||
|
postprocessors (default)
|
||||||
--ffmpeg-location PATH Location of the ffmpeg/avconv binary;
|
--ffmpeg-location PATH Location of the ffmpeg/avconv binary;
|
||||||
either the path to the binary or its
|
either the path to the binary or its
|
||||||
containing directory.
|
containing directory.
|
||||||
@ -433,7 +442,7 @@ Alternatively, refer to the [developer instructions](#developer-instructions) fo
|
|||||||
|
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
|
|
||||||
You can configure youtube-dl by placing any supported command line option to a configuration file. On Linux and OS X, the system wide configuration file is located at `/etc/youtube-dl.conf` and the user wide configuration file at `~/.config/youtube-dl/config`. On Windows, the user wide configuration file locations are `%APPDATA%\youtube-dl\config.txt` or `C:\Users\<user name>\youtube-dl.conf`. Note that by default configuration file may not exist so you may need to create it yourself.
|
You can configure youtube-dl by placing any supported command line option to a configuration file. On Linux and macOS, the system wide configuration file is located at `/etc/youtube-dl.conf` and the user wide configuration file at `~/.config/youtube-dl/config`. On Windows, the user wide configuration file locations are `%APPDATA%\youtube-dl\config.txt` or `C:\Users\<user name>\youtube-dl.conf`. Note that by default configuration file may not exist so you may need to create it yourself.
|
||||||
|
|
||||||
For example, with the following configuration file youtube-dl will always extract the audio, not copy the mtime, use a proxy and save all videos under `Movies` directory in your home directory:
|
For example, with the following configuration file youtube-dl will always extract the audio, not copy the mtime, use a proxy and save all videos under `Movies` directory in your home directory:
|
||||||
```
|
```
|
||||||
@ -502,6 +511,8 @@ The basic usage is not to set any template arguments when downloading a single f
|
|||||||
- `timestamp` (numeric): UNIX timestamp of the moment the video became available
|
- `timestamp` (numeric): UNIX timestamp of the moment the video became available
|
||||||
- `upload_date` (string): Video upload date (YYYYMMDD)
|
- `upload_date` (string): Video upload date (YYYYMMDD)
|
||||||
- `uploader_id` (string): Nickname or id of the video uploader
|
- `uploader_id` (string): Nickname or id of the video uploader
|
||||||
|
- `channel` (string): Full name of the channel the video is uploaded on
|
||||||
|
- `channel_id` (string): Id of the channel
|
||||||
- `location` (string): Physical location where the video was filmed
|
- `location` (string): Physical location where the video was filmed
|
||||||
- `duration` (numeric): Length of the video in seconds
|
- `duration` (numeric): Length of the video in seconds
|
||||||
- `view_count` (numeric): How many users have watched the video on the platform
|
- `view_count` (numeric): How many users have watched the video on the platform
|
||||||
@ -511,6 +522,9 @@ The basic usage is not to set any template arguments when downloading a single f
|
|||||||
- `average_rating` (numeric): Average rating give by users, the scale used depends on the webpage
|
- `average_rating` (numeric): Average rating give by users, the scale used depends on the webpage
|
||||||
- `comment_count` (numeric): Number of comments on the video
|
- `comment_count` (numeric): Number of comments on the video
|
||||||
- `age_limit` (numeric): Age restriction for the video (years)
|
- `age_limit` (numeric): Age restriction for the video (years)
|
||||||
|
- `is_live` (boolean): Whether this video is a live stream or a fixed-length video
|
||||||
|
- `start_time` (numeric): Time in seconds where the reproduction should start, as specified in the URL
|
||||||
|
- `end_time` (numeric): Time in seconds where the reproduction should end, as specified in the URL
|
||||||
- `format` (string): A human-readable description of the format
|
- `format` (string): A human-readable description of the format
|
||||||
- `format_id` (string): Format code specified by `--format`
|
- `format_id` (string): Format code specified by `--format`
|
||||||
- `format_note` (string): Additional info about the format
|
- `format_note` (string): Additional info about the format
|
||||||
@ -536,6 +550,8 @@ The basic usage is not to set any template arguments when downloading a single f
|
|||||||
- `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according to the total length of the playlist
|
- `playlist_index` (numeric): Index of the video in the playlist padded with leading zeros according to the total length of the playlist
|
||||||
- `playlist_id` (string): Playlist identifier
|
- `playlist_id` (string): Playlist identifier
|
||||||
- `playlist_title` (string): Playlist title
|
- `playlist_title` (string): Playlist title
|
||||||
|
- `playlist_uploader` (string): Full name of the playlist uploader
|
||||||
|
- `playlist_uploader_id` (string): Nickname or id of the playlist uploader
|
||||||
|
|
||||||
Available for the video that belongs to some logical chapter or section:
|
Available for the video that belongs to some logical chapter or section:
|
||||||
|
|
||||||
@ -856,9 +872,9 @@ Either prepend `https://www.youtube.com/watch?v=` or separate the ID from the op
|
|||||||
|
|
||||||
Use the `--cookies` option, for example `--cookies /path/to/cookies/file.txt`.
|
Use the `--cookies` option, for example `--cookies /path/to/cookies/file.txt`.
|
||||||
|
|
||||||
In order to extract cookies from browser use any conforming browser extension for exporting cookies. For example, [cookies.txt](https://chrome.google.com/webstore/detail/cookiestxt/njabckikapfpffapmjgojcnbfjonfjfg) (for Chrome) or [Export Cookies](https://addons.mozilla.org/en-US/firefox/addon/export-cookies/) (for Firefox).
|
In order to extract cookies from browser use any conforming browser extension for exporting cookies. For example, [cookies.txt](https://chrome.google.com/webstore/detail/cookiestxt/njabckikapfpffapmjgojcnbfjonfjfg) (for Chrome) or [cookies.txt](https://addons.mozilla.org/en-US/firefox/addon/cookies-txt/) (for Firefox).
|
||||||
|
|
||||||
Note that the cookies file must be in Mozilla/Netscape format and the first line of the cookies file must be either `# HTTP Cookie File` or `# Netscape HTTP Cookie File`. Make sure you have correct [newline format](https://en.wikipedia.org/wiki/Newline) in the cookies file and convert newlines if necessary to correspond with your OS, namely `CRLF` (`\r\n`) for Windows and `LF` (`\n`) for Unix and Unix-like systems (Linux, Mac OS, etc.). `HTTP Error 400: Bad Request` when using `--cookies` is a good sign of invalid newline format.
|
Note that the cookies file must be in Mozilla/Netscape format and the first line of the cookies file must be either `# HTTP Cookie File` or `# Netscape HTTP Cookie File`. Make sure you have correct [newline format](https://en.wikipedia.org/wiki/Newline) in the cookies file and convert newlines if necessary to correspond with your OS, namely `CRLF` (`\r\n`) for Windows and `LF` (`\n`) for Unix and Unix-like systems (Linux, macOS, etc.). `HTTP Error 400: Bad Request` when using `--cookies` is a good sign of invalid newline format.
|
||||||
|
|
||||||
Passing cookies to youtube-dl is a good way to workaround login when a particular extractor does not implement it explicitly. Another use case is working around [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA) some websites require you to solve in particular cases in order to get access (e.g. YouTube, CloudFlare).
|
Passing cookies to youtube-dl is a good way to workaround login when a particular extractor does not implement it explicitly. Another use case is working around [CAPTCHA](https://en.wikipedia.org/wiki/CAPTCHA) some websites require you to solve in particular cases in order to get access (e.g. YouTube, CloudFlare).
|
||||||
|
|
||||||
|
@ -1,27 +1,22 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import urllib.request
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
versions_info = json.load(open('update/versions.json'))
|
versions_info = json.load(open('update/versions.json'))
|
||||||
version = versions_info['latest']
|
version = versions_info['latest']
|
||||||
URL = versions_info['versions'][version]['bin'][0]
|
version_dict = versions_info['versions'][version]
|
||||||
|
|
||||||
data = urllib.request.urlopen(URL).read()
|
|
||||||
|
|
||||||
# Read template page
|
# Read template page
|
||||||
with open('download.html.in', 'r', encoding='utf-8') as tmplf:
|
with open('download.html.in', 'r', encoding='utf-8') as tmplf:
|
||||||
template = tmplf.read()
|
template = tmplf.read()
|
||||||
|
|
||||||
sha256sum = hashlib.sha256(data).hexdigest()
|
|
||||||
template = template.replace('@PROGRAM_VERSION@', version)
|
template = template.replace('@PROGRAM_VERSION@', version)
|
||||||
template = template.replace('@PROGRAM_URL@', URL)
|
template = template.replace('@PROGRAM_URL@', version_dict['bin'][0])
|
||||||
template = template.replace('@PROGRAM_SHA256SUM@', sha256sum)
|
template = template.replace('@PROGRAM_SHA256SUM@', version_dict['bin'][1])
|
||||||
template = template.replace('@EXE_URL@', versions_info['versions'][version]['exe'][0])
|
template = template.replace('@EXE_URL@', version_dict['exe'][0])
|
||||||
template = template.replace('@EXE_SHA256SUM@', versions_info['versions'][version]['exe'][1])
|
template = template.replace('@EXE_SHA256SUM@', version_dict['exe'][1])
|
||||||
template = template.replace('@TAR_URL@', versions_info['versions'][version]['tar'][0])
|
template = template.replace('@TAR_URL@', version_dict['tar'][0])
|
||||||
template = template.replace('@TAR_SHA256SUM@', versions_info['versions'][version]['tar'][1])
|
template = template.replace('@TAR_SHA256SUM@', version_dict['tar'][1])
|
||||||
with open('download.html', 'w', encoding='utf-8') as dlf:
|
with open('download.html', 'w', encoding='utf-8') as dlf:
|
||||||
dlf.write(template)
|
dlf.write(template)
|
||||||
|
@ -13,7 +13,7 @@ year = str(datetime.datetime.now().year)
|
|||||||
for fn in glob.glob('*.html*'):
|
for fn in glob.glob('*.html*'):
|
||||||
with io.open(fn, encoding='utf-8') as f:
|
with io.open(fn, encoding='utf-8') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
newc = re.sub(r'(?P<copyright>Copyright © 2006-)(?P<year>[0-9]{4})', 'Copyright © 2006-' + year, content)
|
newc = re.sub(r'(?P<copyright>Copyright © 2011-)(?P<year>[0-9]{4})', 'Copyright © 2011-' + year, content)
|
||||||
if content != newc:
|
if content != newc:
|
||||||
tmpFn = fn + '.part'
|
tmpFn = fn + '.part'
|
||||||
with io.open(tmpFn, 'wt', encoding='utf-8') as outf:
|
with io.open(tmpFn, 'wt', encoding='utf-8') as outf:
|
||||||
|
5
devscripts/install_jython.sh
Executable file
5
devscripts/install_jython.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
wget http://central.maven.org/maven2/org/python/jython-installer/2.7.1/jython-installer-2.7.1.jar
|
||||||
|
java -jar jython-installer-2.7.1.jar -s -d "$HOME/jython"
|
||||||
|
$HOME/jython/bin/jython -m pip install nose
|
@ -1,6 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
DOWNLOAD_TESTS="age_restriction|download|subtitles|write_annotations|iqiyi_sdk_interpreter|youtube_lists"
|
# Keep this list in sync with the `offlinetest` target in Makefile
|
||||||
|
DOWNLOAD_TESTS="age_restriction|download|iqiyi_sdk_interpreter|socks|subtitles|write_annotations|youtube_lists|youtube_signature"
|
||||||
|
|
||||||
test_set=""
|
test_set=""
|
||||||
multiprocess_args=""
|
multiprocess_args=""
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
- **1up.com**
|
- **1up.com**
|
||||||
- **20min**
|
- **20min**
|
||||||
- **220.ro**
|
- **220.ro**
|
||||||
|
- **23video**
|
||||||
- **24video**
|
- **24video**
|
||||||
- **3qsdn**: 3Q SDN
|
- **3qsdn**: 3Q SDN
|
||||||
- **3sat**
|
- **3sat**
|
||||||
@ -10,10 +11,10 @@
|
|||||||
- **56.com**
|
- **56.com**
|
||||||
- **5min**
|
- **5min**
|
||||||
- **6play**
|
- **6play**
|
||||||
|
- **7plus**
|
||||||
- **8tracks**
|
- **8tracks**
|
||||||
- **91porn**
|
- **91porn**
|
||||||
- **9c9media**
|
- **9c9media**
|
||||||
- **9c9media:stack**
|
|
||||||
- **9gag**
|
- **9gag**
|
||||||
- **9now.com.au**
|
- **9now.com.au**
|
||||||
- **abc.net.au**
|
- **abc.net.au**
|
||||||
@ -46,6 +47,7 @@
|
|||||||
- **anitube.se**
|
- **anitube.se**
|
||||||
- **Anvato**
|
- **Anvato**
|
||||||
- **AnySex**
|
- **AnySex**
|
||||||
|
- **APA**
|
||||||
- **Aparat**
|
- **Aparat**
|
||||||
- **AppleConnect**
|
- **AppleConnect**
|
||||||
- **AppleDaily**: 臺灣蘋果日報
|
- **AppleDaily**: 臺灣蘋果日報
|
||||||
@ -54,6 +56,7 @@
|
|||||||
- **archive.org**: archive.org videos
|
- **archive.org**: archive.org videos
|
||||||
- **ARD**
|
- **ARD**
|
||||||
- **ARD:mediathek**
|
- **ARD:mediathek**
|
||||||
|
- **ARDBetaMediathek**
|
||||||
- **Arkena**
|
- **Arkena**
|
||||||
- **arte.tv**
|
- **arte.tv**
|
||||||
- **arte.tv:+7**
|
- **arte.tv:+7**
|
||||||
@ -95,9 +98,11 @@
|
|||||||
- **bbc.co.uk:article**: BBC articles
|
- **bbc.co.uk:article**: BBC articles
|
||||||
- **bbc.co.uk:iplayer:playlist**
|
- **bbc.co.uk:iplayer:playlist**
|
||||||
- **bbc.co.uk:playlist**
|
- **bbc.co.uk:playlist**
|
||||||
|
- **BBVTV**
|
||||||
- **Beatport**
|
- **Beatport**
|
||||||
- **Beeg**
|
- **Beeg**
|
||||||
- **BehindKink**
|
- **BehindKink**
|
||||||
|
- **Bellator**
|
||||||
- **BellMedia**
|
- **BellMedia**
|
||||||
- **Bet**
|
- **Bet**
|
||||||
- **Bigflix**
|
- **Bigflix**
|
||||||
@ -105,6 +110,8 @@
|
|||||||
- **BiliBili**
|
- **BiliBili**
|
||||||
- **BioBioChileTV**
|
- **BioBioChileTV**
|
||||||
- **BIQLE**
|
- **BIQLE**
|
||||||
|
- **BitChute**
|
||||||
|
- **BitChuteChannel**
|
||||||
- **BleacherReport**
|
- **BleacherReport**
|
||||||
- **BleacherReportCMS**
|
- **BleacherReportCMS**
|
||||||
- **blinkx**
|
- **blinkx**
|
||||||
@ -112,27 +119,31 @@
|
|||||||
- **BokeCC**
|
- **BokeCC**
|
||||||
- **BostonGlobe**
|
- **BostonGlobe**
|
||||||
- **Bpb**: Bundeszentrale für politische Bildung
|
- **Bpb**: Bundeszentrale für politische Bildung
|
||||||
- **BR**: Bayerischer Rundfunk Mediathek
|
- **BR**: Bayerischer Rundfunk
|
||||||
- **BravoTV**
|
- **BravoTV**
|
||||||
- **Break**
|
- **Break**
|
||||||
- **brightcove:legacy**
|
- **brightcove:legacy**
|
||||||
- **brightcove:new**
|
- **brightcove:new**
|
||||||
|
- **BRMediathek**: Bayerischer Rundfunk Mediathek
|
||||||
- **bt:article**: Bergens Tidende Articles
|
- **bt:article**: Bergens Tidende Articles
|
||||||
- **bt:vestlendingen**: Bergens Tidende - Vestlendingen
|
- **bt:vestlendingen**: Bergens Tidende - Vestlendingen
|
||||||
|
- **BusinessInsider**
|
||||||
- **BuzzFeed**
|
- **BuzzFeed**
|
||||||
- **BYUtv**
|
- **BYUtv**
|
||||||
- **BYUtvEvent**
|
|
||||||
- **Camdemy**
|
- **Camdemy**
|
||||||
- **CamdemyFolder**
|
- **CamdemyFolder**
|
||||||
|
- **CamModels**
|
||||||
|
- **CamTube**
|
||||||
- **CamWithHer**
|
- **CamWithHer**
|
||||||
- **canalc2.tv**
|
- **canalc2.tv**
|
||||||
- **Canalplus**: canalplus.fr, piwiplus.fr and d8.tv
|
- **Canalplus**: mycanal.fr and piwiplus.fr
|
||||||
- **Canvas**
|
- **Canvas**
|
||||||
- **CanvasEen**: canvas.be and een.be
|
- **CanvasEen**: canvas.be and een.be
|
||||||
- **CarambaTV**
|
- **CarambaTV**
|
||||||
- **CarambaTVPage**
|
- **CarambaTVPage**
|
||||||
- **CartoonNetwork**
|
- **CartoonNetwork**
|
||||||
- **cbc.ca**
|
- **cbc.ca**
|
||||||
|
- **cbc.ca:olympics**
|
||||||
- **cbc.ca:player**
|
- **cbc.ca:player**
|
||||||
- **cbc.ca:watch**
|
- **cbc.ca:watch**
|
||||||
- **cbc.ca:watch:video**
|
- **cbc.ca:watch:video**
|
||||||
@ -160,6 +171,7 @@
|
|||||||
- **ClipRs**
|
- **ClipRs**
|
||||||
- **Clipsyndicate**
|
- **Clipsyndicate**
|
||||||
- **CloserToTruth**
|
- **CloserToTruth**
|
||||||
|
- **CloudflareStream**
|
||||||
- **cloudtime**: CloudTime
|
- **cloudtime**: CloudTime
|
||||||
- **Cloudy**
|
- **Cloudy**
|
||||||
- **Clubic**
|
- **Clubic**
|
||||||
@ -169,7 +181,6 @@
|
|||||||
- **CNN**
|
- **CNN**
|
||||||
- **CNNArticle**
|
- **CNNArticle**
|
||||||
- **CNNBlogs**
|
- **CNNBlogs**
|
||||||
- **CollegeRama**
|
|
||||||
- **ComCarCoff**
|
- **ComCarCoff**
|
||||||
- **ComedyCentral**
|
- **ComedyCentral**
|
||||||
- **ComedyCentralFullEpisodes**
|
- **ComedyCentralFullEpisodes**
|
||||||
@ -182,13 +193,13 @@
|
|||||||
- **Crackle**
|
- **Crackle**
|
||||||
- **Criterion**
|
- **Criterion**
|
||||||
- **CrooksAndLiars**
|
- **CrooksAndLiars**
|
||||||
- **Crunchyroll**
|
- **crunchyroll**
|
||||||
- **crunchyroll:playlist**
|
- **crunchyroll:playlist**
|
||||||
- **CSNNE**
|
- **CSNNE**
|
||||||
- **CSpan**: C-SPAN
|
- **CSpan**: C-SPAN
|
||||||
- **CtsNews**: 華視新聞
|
- **CtsNews**: 華視新聞
|
||||||
- **CTVNews**
|
- **CTVNews**
|
||||||
- **culturebox.francetvinfo.fr**
|
- **Culturebox**
|
||||||
- **CultureUnplugged**
|
- **CultureUnplugged**
|
||||||
- **curiositystream**
|
- **curiositystream**
|
||||||
- **curiositystream:collection**
|
- **curiositystream:collection**
|
||||||
@ -197,9 +208,8 @@
|
|||||||
- **dailymotion**
|
- **dailymotion**
|
||||||
- **dailymotion:playlist**
|
- **dailymotion:playlist**
|
||||||
- **dailymotion:user**
|
- **dailymotion:user**
|
||||||
- **DailymotionCloud**
|
- **DaisukiMotto**
|
||||||
- **Daisuki**
|
- **DaisukiMottoPlaylist**
|
||||||
- **DaisukiPlaylist**
|
|
||||||
- **daum.net**
|
- **daum.net**
|
||||||
- **daum.net:clip**
|
- **daum.net:clip**
|
||||||
- **daum.net:playlist**
|
- **daum.net:playlist**
|
||||||
@ -210,6 +220,7 @@
|
|||||||
- **defense.gouv.fr**
|
- **defense.gouv.fr**
|
||||||
- **democracynow**
|
- **democracynow**
|
||||||
- **DHM**: Filmarchiv - Deutsches Historisches Museum
|
- **DHM**: Filmarchiv - Deutsches Historisches Museum
|
||||||
|
- **Digg**
|
||||||
- **DigitallySpeaking**
|
- **DigitallySpeaking**
|
||||||
- **Digiteka**
|
- **Digiteka**
|
||||||
- **Discovery**
|
- **Discovery**
|
||||||
@ -230,6 +241,7 @@
|
|||||||
- **DrTuber**
|
- **DrTuber**
|
||||||
- **drtv**
|
- **drtv**
|
||||||
- **drtv:live**
|
- **drtv:live**
|
||||||
|
- **DTube**
|
||||||
- **Dumpert**
|
- **Dumpert**
|
||||||
- **dvtv**: http://video.aktualne.cz/
|
- **dvtv**: http://video.aktualne.cz/
|
||||||
- **dw**
|
- **dw**
|
||||||
@ -240,10 +252,12 @@
|
|||||||
- **egghead:course**: egghead.io course
|
- **egghead:course**: egghead.io course
|
||||||
- **egghead:lesson**: egghead.io lesson
|
- **egghead:lesson**: egghead.io lesson
|
||||||
- **eHow**
|
- **eHow**
|
||||||
|
- **EinsUndEinsTV**
|
||||||
- **Einthusan**
|
- **Einthusan**
|
||||||
- **eitb.tv**
|
- **eitb.tv**
|
||||||
- **EllenTV**
|
- **EllenTube**
|
||||||
- **EllenTV:clips**
|
- **EllenTubePlaylist**
|
||||||
|
- **EllenTubeVideo**
|
||||||
- **ElPais**: El País
|
- **ElPais**: El País
|
||||||
- **Embedly**
|
- **Embedly**
|
||||||
- **EMPFlix**
|
- **EMPFlix**
|
||||||
@ -254,10 +268,11 @@
|
|||||||
- **ESPN**
|
- **ESPN**
|
||||||
- **ESPNArticle**
|
- **ESPNArticle**
|
||||||
- **EsriVideo**
|
- **EsriVideo**
|
||||||
- **ETOnline**
|
|
||||||
- **Europa**
|
- **Europa**
|
||||||
- **EveryonesMixtape**
|
- **EveryonesMixtape**
|
||||||
|
- **EWETV**
|
||||||
- **ExpoTV**
|
- **ExpoTV**
|
||||||
|
- **Expressen**
|
||||||
- **ExtremeTube**
|
- **ExtremeTube**
|
||||||
- **EyedoTV**
|
- **EyedoTV**
|
||||||
- **facebook**
|
- **facebook**
|
||||||
@ -266,10 +281,10 @@
|
|||||||
- **fc2**
|
- **fc2**
|
||||||
- **fc2:embed**
|
- **fc2:embed**
|
||||||
- **Fczenit**
|
- **Fczenit**
|
||||||
- **fernsehkritik.tv**
|
|
||||||
- **filmon**
|
- **filmon**
|
||||||
- **filmon:channel**
|
- **filmon:channel**
|
||||||
- **Firstpost**
|
- **Filmweb**
|
||||||
|
- **FiveThirtyEight**
|
||||||
- **FiveTV**
|
- **FiveTV**
|
||||||
- **Flickr**
|
- **Flickr**
|
||||||
- **Flipagram**
|
- **Flipagram**
|
||||||
@ -281,19 +296,24 @@
|
|||||||
- **Foxgay**
|
- **Foxgay**
|
||||||
- **foxnews**: Fox News and Fox Business Video
|
- **foxnews**: Fox News and Fox Business Video
|
||||||
- **foxnews:article**
|
- **foxnews:article**
|
||||||
- **foxnews:insider**
|
|
||||||
- **FoxSports**
|
- **FoxSports**
|
||||||
- **france2.fr:generation-quoi**
|
- **france2.fr:generation-what**
|
||||||
- **FranceCulture**
|
- **FranceCulture**
|
||||||
- **FranceInter**
|
- **FranceInter**
|
||||||
- **FranceTV**
|
- **FranceTV**
|
||||||
- **FranceTVEmbed**
|
- **FranceTVEmbed**
|
||||||
- **francetvinfo.fr**
|
- **francetvinfo.fr**
|
||||||
|
- **FranceTVJeunesse**
|
||||||
|
- **FranceTVSite**
|
||||||
- **Freesound**
|
- **Freesound**
|
||||||
- **freespeech.org**
|
- **freespeech.org**
|
||||||
- **FreshLive**
|
- **FreshLive**
|
||||||
|
- **FrontendMasters**
|
||||||
|
- **FrontendMastersCourse**
|
||||||
|
- **FrontendMastersLesson**
|
||||||
- **Funimation**
|
- **Funimation**
|
||||||
- **Funk**
|
- **FunkChannel**
|
||||||
|
- **FunkMix**
|
||||||
- **FunnyOrDie**
|
- **FunnyOrDie**
|
||||||
- **Fusion**
|
- **Fusion**
|
||||||
- **Fux**
|
- **Fux**
|
||||||
@ -301,7 +321,6 @@
|
|||||||
- **GameInformer**
|
- **GameInformer**
|
||||||
- **GameOne**
|
- **GameOne**
|
||||||
- **gameone:playlist**
|
- **gameone:playlist**
|
||||||
- **Gamersyde**
|
|
||||||
- **GameSpot**
|
- **GameSpot**
|
||||||
- **GameStar**
|
- **GameStar**
|
||||||
- **Gaskrank**
|
- **Gaskrank**
|
||||||
@ -311,6 +330,7 @@
|
|||||||
- **Gfycat**
|
- **Gfycat**
|
||||||
- **GiantBomb**
|
- **GiantBomb**
|
||||||
- **Giga**
|
- **Giga**
|
||||||
|
- **GlattvisionTV**
|
||||||
- **Glide**: Glide mobile video messages (glide.me)
|
- **Glide**: Glide mobile video messages (glide.me)
|
||||||
- **Globo**
|
- **Globo**
|
||||||
- **GloboArticle**
|
- **GloboArticle**
|
||||||
@ -332,6 +352,7 @@
|
|||||||
- **HentaiStigma**
|
- **HentaiStigma**
|
||||||
- **hetklokhuis**
|
- **hetklokhuis**
|
||||||
- **hgtv.com:show**
|
- **hgtv.com:show**
|
||||||
|
- **HiDive**
|
||||||
- **HistoricFilms**
|
- **HistoricFilms**
|
||||||
- **history:topic**: History.com Topic
|
- **history:topic**: History.com Topic
|
||||||
- **hitbox**
|
- **hitbox**
|
||||||
@ -339,7 +360,7 @@
|
|||||||
- **HitRecord**
|
- **HitRecord**
|
||||||
- **HornBunny**
|
- **HornBunny**
|
||||||
- **HotNewHipHop**
|
- **HotNewHipHop**
|
||||||
- **HotStar**
|
- **hotstar**
|
||||||
- **hotstar:playlist**
|
- **hotstar:playlist**
|
||||||
- **Howcast**
|
- **Howcast**
|
||||||
- **HowStuffWorks**
|
- **HowStuffWorks**
|
||||||
@ -356,17 +377,18 @@
|
|||||||
- **ImgurAlbum**
|
- **ImgurAlbum**
|
||||||
- **Ina**
|
- **Ina**
|
||||||
- **Inc**
|
- **Inc**
|
||||||
- **Indavideo**
|
|
||||||
- **IndavideoEmbed**
|
- **IndavideoEmbed**
|
||||||
- **InfoQ**
|
- **InfoQ**
|
||||||
- **Instagram**
|
- **Instagram**
|
||||||
- **instagram:user**: Instagram user profile
|
- **instagram:user**: Instagram user profile
|
||||||
|
- **Internazionale**
|
||||||
- **InternetVideoArchive**
|
- **InternetVideoArchive**
|
||||||
- **IPrima**
|
- **IPrima**
|
||||||
- **iqiyi**: 爱奇艺
|
- **iqiyi**: 爱奇艺
|
||||||
- **Ir90Tv**
|
- **Ir90Tv**
|
||||||
- **ITTF**
|
- **ITTF**
|
||||||
- **ITV**
|
- **ITV**
|
||||||
|
- **ITVBTCC**
|
||||||
- **ivi**: ivi.ru
|
- **ivi**: ivi.ru
|
||||||
- **ivi:compilation**: ivi.ru compilations
|
- **ivi:compilation**: ivi.ru compilations
|
||||||
- **ivideon**: Ivideon TV
|
- **ivideon**: Ivideon TV
|
||||||
@ -381,7 +403,6 @@
|
|||||||
- **JWPlatform**
|
- **JWPlatform**
|
||||||
- **Kakao**
|
- **Kakao**
|
||||||
- **Kaltura**
|
- **Kaltura**
|
||||||
- **Kamcord**
|
|
||||||
- **KanalPlay**: Kanal 5/9/11 Play
|
- **KanalPlay**: Kanal 5/9/11 Play
|
||||||
- **Kankan**
|
- **Kankan**
|
||||||
- **Karaoketv**
|
- **Karaoketv**
|
||||||
@ -391,6 +412,7 @@
|
|||||||
- **Ketnet**
|
- **Ketnet**
|
||||||
- **KhanAcademy**
|
- **KhanAcademy**
|
||||||
- **KickStarter**
|
- **KickStarter**
|
||||||
|
- **KinoPoisk**
|
||||||
- **KonserthusetPlay**
|
- **KonserthusetPlay**
|
||||||
- **kontrtube**: KontrTube.ru - Труба зовёт
|
- **kontrtube**: KontrTube.ru - Труба зовёт
|
||||||
- **KrasView**: Красвью
|
- **KrasView**: Красвью
|
||||||
@ -413,6 +435,7 @@
|
|||||||
- **Lecture2Go**
|
- **Lecture2Go**
|
||||||
- **LEGO**
|
- **LEGO**
|
||||||
- **Lemonde**
|
- **Lemonde**
|
||||||
|
- **Lenta**
|
||||||
- **LePlaylist**
|
- **LePlaylist**
|
||||||
- **LetvCloud**: 乐视云
|
- **LetvCloud**: 乐视云
|
||||||
- **Libsyn**
|
- **Libsyn**
|
||||||
@ -421,6 +444,7 @@
|
|||||||
- **limelight**
|
- **limelight**
|
||||||
- **limelight:channel**
|
- **limelight:channel**
|
||||||
- **limelight:channel_list**
|
- **limelight:channel_list**
|
||||||
|
- **LineTV**
|
||||||
- **LiTV**
|
- **LiTV**
|
||||||
- **LiveLeak**
|
- **LiveLeak**
|
||||||
- **LiveLeakEmbed**
|
- **LiveLeakEmbed**
|
||||||
@ -436,16 +460,21 @@
|
|||||||
- **m6**
|
- **m6**
|
||||||
- **macgamestore**: MacGameStore trailers
|
- **macgamestore**: MacGameStore trailers
|
||||||
- **mailru**: Видео@Mail.Ru
|
- **mailru**: Видео@Mail.Ru
|
||||||
- **MakersChannel**
|
- **mailru:music**: Музыка@Mail.Ru
|
||||||
|
- **mailru:music:search**: Музыка@Mail.Ru
|
||||||
- **MakerTV**
|
- **MakerTV**
|
||||||
- **mangomolo:live**
|
- **mangomolo:live**
|
||||||
- **mangomolo:video**
|
- **mangomolo:video**
|
||||||
- **ManyVids**
|
- **ManyVids**
|
||||||
|
- **Markiza**
|
||||||
|
- **MarkizaPage**
|
||||||
|
- **massengeschmack.tv**
|
||||||
- **MatchTV**
|
- **MatchTV**
|
||||||
- **MDR**: MDR.DE and KiKA
|
- **MDR**: MDR.DE and KiKA
|
||||||
- **media.ccc.de**
|
- **media.ccc.de**
|
||||||
- **Medialaan**
|
- **Medialaan**
|
||||||
- **Mediaset**
|
- **Mediaset**
|
||||||
|
- **Mediasite**
|
||||||
- **Medici**
|
- **Medici**
|
||||||
- **megaphone.fm**: megaphone.fm embedded players
|
- **megaphone.fm**: megaphone.fm embedded players
|
||||||
- **Meipai**: 美拍
|
- **Meipai**: 美拍
|
||||||
@ -469,12 +498,13 @@
|
|||||||
- **Mixer:vod**
|
- **Mixer:vod**
|
||||||
- **MLB**
|
- **MLB**
|
||||||
- **Mnet**
|
- **Mnet**
|
||||||
|
- **MNetTV**
|
||||||
- **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net
|
- **MoeVideo**: LetitBit video services: moevideo.net, playreplay.net and videochart.net
|
||||||
- **Mofosex**
|
- **Mofosex**
|
||||||
- **Mojvideo**
|
- **Mojvideo**
|
||||||
- **Moniker**: allmyvideos.net and vidspot.net
|
|
||||||
- **Morningstar**: morningstar.com
|
- **Morningstar**: morningstar.com
|
||||||
- **Motherless**
|
- **Motherless**
|
||||||
|
- **MotherlessGroup**
|
||||||
- **Motorsport**: motorsport.com
|
- **Motorsport**: motorsport.com
|
||||||
- **MovieClips**
|
- **MovieClips**
|
||||||
- **MovieFap**
|
- **MovieFap**
|
||||||
@ -493,11 +523,14 @@
|
|||||||
- **mva:course**: Microsoft Virtual Academy courses
|
- **mva:course**: Microsoft Virtual Academy courses
|
||||||
- **Mwave**
|
- **Mwave**
|
||||||
- **MwaveMeetGreet**
|
- **MwaveMeetGreet**
|
||||||
|
- **MyChannels**
|
||||||
- **MySpace**
|
- **MySpace**
|
||||||
- **MySpace:album**
|
- **MySpace:album**
|
||||||
- **MySpass**
|
- **MySpass**
|
||||||
- **Myvi**
|
- **Myvi**
|
||||||
- **MyVidster**
|
- **MyVidster**
|
||||||
|
- **MyviEmbed**
|
||||||
|
- **MyVisionTV**
|
||||||
- **n-tv.de**
|
- **n-tv.de**
|
||||||
- **natgeo**
|
- **natgeo**
|
||||||
- **natgeo:episodeguide**
|
- **natgeo:episodeguide**
|
||||||
@ -506,8 +539,10 @@
|
|||||||
- **NBA**
|
- **NBA**
|
||||||
- **NBC**
|
- **NBC**
|
||||||
- **NBCNews**
|
- **NBCNews**
|
||||||
- **NBCOlympics**
|
- **nbcolympics**
|
||||||
|
- **nbcolympics:stream**
|
||||||
- **NBCSports**
|
- **NBCSports**
|
||||||
|
- **NBCSportsStream**
|
||||||
- **NBCSportsVPlayer**
|
- **NBCSportsVPlayer**
|
||||||
- **ndr**: NDR.de - Norddeutscher Rundfunk
|
- **ndr**: NDR.de - Norddeutscher Rundfunk
|
||||||
- **ndr:embed**
|
- **ndr:embed**
|
||||||
@ -521,6 +556,7 @@
|
|||||||
- **netease:program**: 网易云音乐 - 电台节目
|
- **netease:program**: 网易云音乐 - 电台节目
|
||||||
- **netease:singer**: 网易云音乐 - 歌手
|
- **netease:singer**: 网易云音乐 - 歌手
|
||||||
- **netease:song**: 网易云音乐
|
- **netease:song**: 网易云音乐
|
||||||
|
- **NetPlus**
|
||||||
- **Netzkino**
|
- **Netzkino**
|
||||||
- **Newgrounds**
|
- **Newgrounds**
|
||||||
- **NewgroundsPlaylist**
|
- **NewgroundsPlaylist**
|
||||||
@ -534,11 +570,9 @@
|
|||||||
- **nfl.com**
|
- **nfl.com**
|
||||||
- **NhkVod**
|
- **NhkVod**
|
||||||
- **nhl.com**
|
- **nhl.com**
|
||||||
- **nhl.com:news**: NHL news
|
|
||||||
- **nhl.com:videocenter**
|
|
||||||
- **nhl.com:videocenter:category**: NHL videocenter category
|
|
||||||
- **nick.com**
|
- **nick.com**
|
||||||
- **nick.de**
|
- **nick.de**
|
||||||
|
- **nickelodeon:br**
|
||||||
- **nickelodeonru**
|
- **nickelodeonru**
|
||||||
- **nicknight**
|
- **nicknight**
|
||||||
- **niconico**: ニコニコ動画
|
- **niconico**: ニコニコ動画
|
||||||
@ -554,11 +588,10 @@
|
|||||||
- **Normalboots**
|
- **Normalboots**
|
||||||
- **NosVideo**
|
- **NosVideo**
|
||||||
- **Nova**: TN.cz, Prásk.tv, Nova.cz, Novaplus.cz, FANDA.tv, Krásná.cz and Doma.cz
|
- **Nova**: TN.cz, Prásk.tv, Nova.cz, Novaplus.cz, FANDA.tv, Krásná.cz and Doma.cz
|
||||||
|
- **NovaEmbed**
|
||||||
- **nowness**
|
- **nowness**
|
||||||
- **nowness:playlist**
|
- **nowness:playlist**
|
||||||
- **nowness:series**
|
- **nowness:series**
|
||||||
- **NowTV** (Currently broken)
|
|
||||||
- **NowTVList**
|
|
||||||
- **nowvideo**: NowVideo
|
- **nowvideo**: NowVideo
|
||||||
- **Noz**
|
- **Noz**
|
||||||
- **npo**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
- **npo**: npo.nl, ntr.nl, omroepwnl.nl, zapp.nl and npo3.nl
|
||||||
@ -571,7 +604,9 @@
|
|||||||
- **NRKSkole**: NRK Skole
|
- **NRKSkole**: NRK Skole
|
||||||
- **NRKTV**: NRK TV and NRK Radio
|
- **NRKTV**: NRK TV and NRK Radio
|
||||||
- **NRKTVDirekte**: NRK TV Direkte and NRK Radio Direkte
|
- **NRKTVDirekte**: NRK TV Direkte and NRK Radio Direkte
|
||||||
|
- **NRKTVEpisode**
|
||||||
- **NRKTVEpisodes**
|
- **NRKTVEpisodes**
|
||||||
|
- **NRKTVSeason**
|
||||||
- **NRKTVSeries**
|
- **NRKTVSeries**
|
||||||
- **ntv.ru**
|
- **ntv.ru**
|
||||||
- **Nuvid**
|
- **Nuvid**
|
||||||
@ -598,21 +633,27 @@
|
|||||||
- **orf:iptv**: iptv.ORF.at
|
- **orf:iptv**: iptv.ORF.at
|
||||||
- **orf:oe1**: Radio Österreich 1
|
- **orf:oe1**: Radio Österreich 1
|
||||||
- **orf:tvthek**: ORF TVthek
|
- **orf:tvthek**: ORF TVthek
|
||||||
|
- **OsnatelTV**
|
||||||
- **PacktPub**
|
- **PacktPub**
|
||||||
- **PacktPubCourse**
|
- **PacktPubCourse**
|
||||||
- **PandaTV**: 熊猫TV
|
- **PandaTV**: 熊猫TV
|
||||||
- **pandora.tv**: 판도라TV
|
- **pandora.tv**: 판도라TV
|
||||||
|
- **ParamountNetwork**
|
||||||
- **parliamentlive.tv**: UK parliament videos
|
- **parliamentlive.tv**: UK parliament videos
|
||||||
- **Patreon**
|
- **Patreon**
|
||||||
- **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC)
|
- **pbs**: Public Broadcasting Service (PBS) and member stations: PBS: Public Broadcasting Service, APT - Alabama Public Television (WBIQ), GPB/Georgia Public Broadcasting (WGTV), Mississippi Public Broadcasting (WMPN), Nashville Public Television (WNPT), WFSU-TV (WFSU), WSRE (WSRE), WTCI (WTCI), WPBA/Channel 30 (WPBA), Alaska Public Media (KAKM), Arizona PBS (KAET), KNME-TV/Channel 5 (KNME), Vegas PBS (KLVX), AETN/ARKANSAS ETV NETWORK (KETS), KET (WKLE), WKNO/Channel 10 (WKNO), LPB/LOUISIANA PUBLIC BROADCASTING (WLPB), OETA (KETA), Ozarks Public Television (KOZK), WSIU Public Broadcasting (WSIU), KEET TV (KEET), KIXE/Channel 9 (KIXE), KPBS San Diego (KPBS), KQED (KQED), KVIE Public Television (KVIE), PBS SoCal/KOCE (KOCE), ValleyPBS (KVPT), CONNECTICUT PUBLIC TELEVISION (WEDH), KNPB Channel 5 (KNPB), SOPTV (KSYS), Rocky Mountain PBS (KRMA), KENW-TV3 (KENW), KUED Channel 7 (KUED), Wyoming PBS (KCWC), Colorado Public Television / KBDI 12 (KBDI), KBYU-TV (KBYU), Thirteen/WNET New York (WNET), WGBH/Channel 2 (WGBH), WGBY (WGBY), NJTV Public Media NJ (WNJT), WLIW21 (WLIW), mpt/Maryland Public Television (WMPB), WETA Television and Radio (WETA), WHYY (WHYY), PBS 39 (WLVT), WVPT - Your Source for PBS and More! (WVPT), Howard University Television (WHUT), WEDU PBS (WEDU), WGCU Public Media (WGCU), WPBT2 (WPBT), WUCF TV (WUCF), WUFT/Channel 5 (WUFT), WXEL/Channel 42 (WXEL), WLRN/Channel 17 (WLRN), WUSF Public Broadcasting (WUSF), ETV (WRLK), UNC-TV (WUNC), PBS Hawaii - Oceanic Cable Channel 10 (KHET), Idaho Public Television (KAID), KSPS (KSPS), OPB (KOPB), KWSU/Channel 10 & KTNW/Channel 31 (KWSU), WILL-TV (WILL), Network Knowledge - WSEC/Springfield (WSEC), WTTW11 (WTTW), Iowa Public Television/IPTV (KDIN), Nine Network (KETC), PBS39 Fort Wayne (WFWA), WFYI Indianapolis (WFYI), Milwaukee Public Television (WMVS), WNIN (WNIN), WNIT Public Television (WNIT), WPT (WPNE), WVUT/Channel 22 (WVUT), WEIU/Channel 51 (WEIU), WQPT-TV (WQPT), WYCC PBS Chicago (WYCC), WIPB-TV (WIPB), WTIU (WTIU), CET (WCET), ThinkTVNetwork (WPTD), WBGU-TV (WBGU), WGVU TV (WGVU), NET1 (KUON), Pioneer Public Television (KWCM), SDPB Television (KUSD), TPT (KTCA), KSMQ (KSMQ), KPTS/Channel 8 (KPTS), KTWU/Channel 11 (KTWU), East Tennessee PBS (WSJK), WCTE-TV (WCTE), WLJT, Channel 11 (WLJT), WOSU TV (WOSU), WOUB/WOUC (WOUB), WVPB (WVPB), WKYU-PBS (WKYU), KERA 13 (KERA), MPBN (WCBB), Mountain Lake PBS (WCFE), NHPTV (WENH), Vermont PBS (WETK), witf (WITF), WQED Multimedia (WQED), WMHT Educational Telecommunications (WMHT), Q-TV (WDCQ), WTVS Detroit Public TV (WTVS), CMU Public Television (WCMU), WKAR-TV (WKAR), WNMU-TV Public TV 13 (WNMU), WDSE - WRPT (WDSE), WGTE TV (WGTE), Lakeland Public Television (KAWE), KMOS-TV - Channels 6.1, 6.2 and 6.3 (KMOS), MontanaPBS (KUSM), KRWG/Channel 22 (KRWG), KACV (KACV), KCOS/Channel 13 (KCOS), WCNY/Channel 24 (WCNY), WNED (WNED), WPBS (WPBS), WSKG Public TV (WSKG), WXXI (WXXI), WPSU (WPSU), WVIA Public Media Studios (WVIA), WTVI (WTVI), Western Reserve PBS (WNEO), WVIZ/PBS ideastream (WVIZ), KCTS 9 (KCTS), Basin PBS (KPBT), KUHT / Channel 8 (KUHT), KLRN (KLRN), KLRU (KLRU), WTJX Channel 12 (WTJX), WCVE PBS (WCVE), KBTC Public Television (KBTC)
|
||||||
- **pcmag**
|
- **pcmag**
|
||||||
- **PearVideo**
|
- **PearVideo**
|
||||||
|
- **PeerTube**
|
||||||
- **People**
|
- **People**
|
||||||
|
- **PerformGroup**
|
||||||
- **periscope**: Periscope
|
- **periscope**: Periscope
|
||||||
- **periscope:user**: Periscope user videos
|
- **periscope:user**: Periscope user videos
|
||||||
- **PhilharmonieDeParis**: Philharmonie de Paris
|
- **PhilharmonieDeParis**: Philharmonie de Paris
|
||||||
- **phoenix.de**
|
- **phoenix.de**
|
||||||
- **Photobucket**
|
- **Photobucket**
|
||||||
|
- **Picarto**
|
||||||
|
- **PicartoVod**
|
||||||
- **Piksel**
|
- **Piksel**
|
||||||
- **Pinkbike**
|
- **Pinkbike**
|
||||||
- **Pladform**
|
- **Pladform**
|
||||||
@ -644,6 +685,8 @@
|
|||||||
- **PrimeShareTV**
|
- **PrimeShareTV**
|
||||||
- **PromptFile**
|
- **PromptFile**
|
||||||
- **prosiebensat1**: ProSiebenSat.1 Digital
|
- **prosiebensat1**: ProSiebenSat.1 Digital
|
||||||
|
- **puhutv**
|
||||||
|
- **puhutv:serie**
|
||||||
- **Puls4**
|
- **Puls4**
|
||||||
- **Pyvideo**
|
- **Pyvideo**
|
||||||
- **qqmusic**: QQ音乐
|
- **qqmusic**: QQ音乐
|
||||||
@ -651,6 +694,9 @@
|
|||||||
- **qqmusic:playlist**: QQ音乐 - 歌单
|
- **qqmusic:playlist**: QQ音乐 - 歌单
|
||||||
- **qqmusic:singer**: QQ音乐 - 歌手
|
- **qqmusic:singer**: QQ音乐 - 歌手
|
||||||
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
- **qqmusic:toplist**: QQ音乐 - 排行榜
|
||||||
|
- **QuantumTV**
|
||||||
|
- **Quickline**
|
||||||
|
- **QuicklineLive**
|
||||||
- **R7**
|
- **R7**
|
||||||
- **R7Article**
|
- **R7Article**
|
||||||
- **radio.de**
|
- **radio.de**
|
||||||
@ -662,6 +708,9 @@
|
|||||||
- **Rai**
|
- **Rai**
|
||||||
- **RaiPlay**
|
- **RaiPlay**
|
||||||
- **RaiPlayLive**
|
- **RaiPlayLive**
|
||||||
|
- **RaiPlayPlaylist**
|
||||||
|
- **RayWenderlich**
|
||||||
|
- **RayWenderlichCourse**
|
||||||
- **RBMARadio**
|
- **RBMARadio**
|
||||||
- **RDS**: RDS.ca
|
- **RDS**: RDS.ca
|
||||||
- **RedBullTV**
|
- **RedBullTV**
|
||||||
@ -677,7 +726,6 @@
|
|||||||
- **revision**
|
- **revision**
|
||||||
- **revision3:embed**
|
- **revision3:embed**
|
||||||
- **RICE**
|
- **RICE**
|
||||||
- **RingTV**
|
|
||||||
- **RMCDecouverte**
|
- **RMCDecouverte**
|
||||||
- **RockstarGames**
|
- **RockstarGames**
|
||||||
- **RoosterTeeth**
|
- **RoosterTeeth**
|
||||||
@ -698,6 +746,7 @@
|
|||||||
- **rtve.es:live**: RTVE.es live streams
|
- **rtve.es:live**: RTVE.es live streams
|
||||||
- **rtve.es:television**
|
- **rtve.es:television**
|
||||||
- **RTVNH**
|
- **RTVNH**
|
||||||
|
- **RTVS**
|
||||||
- **Rudo**
|
- **Rudo**
|
||||||
- **RUHD**
|
- **RUHD**
|
||||||
- **RulePorn**
|
- **RulePorn**
|
||||||
@ -713,7 +762,7 @@
|
|||||||
- **safari**: safaribooksonline.com online video
|
- **safari**: safaribooksonline.com online video
|
||||||
- **safari:api**
|
- **safari:api**
|
||||||
- **safari:course**: safaribooksonline.com online courses
|
- **safari:course**: safaribooksonline.com online courses
|
||||||
- **Sandia**: Sandia National Laboratories
|
- **SAKTV**
|
||||||
- **Sapo**: SAPO Vídeos
|
- **Sapo**: SAPO Vídeos
|
||||||
- **savefrom.net**
|
- **savefrom.net**
|
||||||
- **SBS**: sbs.com.au
|
- **SBS**: sbs.com.au
|
||||||
@ -728,7 +777,10 @@
|
|||||||
- **ServingSys**
|
- **ServingSys**
|
||||||
- **Servus**
|
- **Servus**
|
||||||
- **Sexu**
|
- **Sexu**
|
||||||
|
- **SeznamZpravy**
|
||||||
|
- **SeznamZpravyArticle**
|
||||||
- **Shahid**
|
- **Shahid**
|
||||||
|
- **ShahidShow**
|
||||||
- **Shared**: shared.sx
|
- **Shared**: shared.sx
|
||||||
- **ShowRoomLive**
|
- **ShowRoomLive**
|
||||||
- **Sina**
|
- **Sina**
|
||||||
@ -764,11 +816,11 @@
|
|||||||
- **Spiegel**
|
- **Spiegel**
|
||||||
- **Spiegel:Article**: Articles on spiegel.de
|
- **Spiegel:Article**: Articles on spiegel.de
|
||||||
- **Spiegeltv**
|
- **Spiegeltv**
|
||||||
- **Spike**
|
- **sport.francetvinfo.fr**
|
||||||
- **Sport5**
|
- **Sport5**
|
||||||
- **SportBoxEmbed**
|
- **SportBoxEmbed**
|
||||||
- **SportDeutschland**
|
- **SportDeutschland**
|
||||||
- **Sportschau**
|
- **SpringboardPlatform**
|
||||||
- **Sprout**
|
- **Sprout**
|
||||||
- **sr:mediathek**: Saarländischer Rundfunk
|
- **sr:mediathek**: Saarländischer Rundfunk
|
||||||
- **SRGSSR**
|
- **SRGSSR**
|
||||||
@ -781,9 +833,12 @@
|
|||||||
- **streamcloud.eu**
|
- **streamcloud.eu**
|
||||||
- **StreamCZ**
|
- **StreamCZ**
|
||||||
- **StreetVoice**
|
- **StreetVoice**
|
||||||
|
- **StretchInternet**
|
||||||
- **SunPorno**
|
- **SunPorno**
|
||||||
- **SVT**
|
- **SVT**
|
||||||
|
- **SVTPage**
|
||||||
- **SVTPlay**: SVT Play and Öppet arkiv
|
- **SVTPlay**: SVT Play and Öppet arkiv
|
||||||
|
- **SVTSeries**
|
||||||
- **SWRMediathek**
|
- **SWRMediathek**
|
||||||
- **Syfy**
|
- **Syfy**
|
||||||
- **SztvHu**
|
- **SztvHu**
|
||||||
@ -792,7 +847,7 @@
|
|||||||
- **tagesschau:player**
|
- **tagesschau:player**
|
||||||
- **Tass**
|
- **Tass**
|
||||||
- **TastyTrade**
|
- **TastyTrade**
|
||||||
- **TBS** (Currently broken)
|
- **TBS**
|
||||||
- **TDSLifeway**
|
- **TDSLifeway**
|
||||||
- **teachertube**: teachertube.com videos
|
- **teachertube**: teachertube.com videos
|
||||||
- **teachertube:user:collection**: teachertube.com user and collection videos
|
- **teachertube:user:collection**: teachertube.com user and collection videos
|
||||||
@ -802,13 +857,17 @@
|
|||||||
- **techtv.mit.edu**
|
- **techtv.mit.edu**
|
||||||
- **ted**
|
- **ted**
|
||||||
- **Tele13**
|
- **Tele13**
|
||||||
|
- **Tele5**
|
||||||
- **TeleBruxelles**
|
- **TeleBruxelles**
|
||||||
- **Telecinco**: telecinco.es, cuatro.com and mediaset.es
|
- **Telecinco**: telecinco.es, cuatro.com and mediaset.es
|
||||||
- **Telegraaf**
|
- **Telegraaf**
|
||||||
- **TeleMB**
|
- **TeleMB**
|
||||||
- **TeleQuebec**
|
- **TeleQuebec**
|
||||||
|
- **TeleQuebecEmission**
|
||||||
|
- **TeleQuebecLive**
|
||||||
- **TeleTask**
|
- **TeleTask**
|
||||||
- **Telewebion**
|
- **Telewebion**
|
||||||
|
- **TennisTV**
|
||||||
- **TF1**
|
- **TF1**
|
||||||
- **TFO**
|
- **TFO**
|
||||||
- **TheIntercept**
|
- **TheIntercept**
|
||||||
@ -816,7 +875,6 @@
|
|||||||
- **ThePlatform**
|
- **ThePlatform**
|
||||||
- **ThePlatformFeed**
|
- **ThePlatformFeed**
|
||||||
- **TheScene**
|
- **TheScene**
|
||||||
- **TheSixtyOne**
|
|
||||||
- **TheStar**
|
- **TheStar**
|
||||||
- **TheSun**
|
- **TheSun**
|
||||||
- **TheWeatherChannel**
|
- **TheWeatherChannel**
|
||||||
@ -862,11 +920,16 @@
|
|||||||
- **tvigle**: Интернет-телевидение Tvigle.ru
|
- **tvigle**: Интернет-телевидение Tvigle.ru
|
||||||
- **tvland.com**
|
- **tvland.com**
|
||||||
- **TVN24**
|
- **TVN24**
|
||||||
|
- **TVNet**
|
||||||
- **TVNoe**
|
- **TVNoe**
|
||||||
|
- **TVNow**
|
||||||
|
- **TVNowList**
|
||||||
|
- **TVNowShow**
|
||||||
- **tvp**: Telewizja Polska
|
- **tvp**: Telewizja Polska
|
||||||
- **tvp:embed**: Telewizja Polska
|
- **tvp:embed**: Telewizja Polska
|
||||||
- **tvp:series**
|
- **tvp:series**
|
||||||
- **TVPlayer**
|
- **TVPlayer**
|
||||||
|
- **TVPlayHome**
|
||||||
- **Tweakers**
|
- **Tweakers**
|
||||||
- **twitch:chapter**
|
- **twitch:chapter**
|
||||||
- **twitch:clips**
|
- **twitch:clips**
|
||||||
@ -884,7 +947,9 @@
|
|||||||
- **udemy**
|
- **udemy**
|
||||||
- **udemy:course**
|
- **udemy:course**
|
||||||
- **UDNEmbed**: 聯合影音
|
- **UDNEmbed**: 聯合影音
|
||||||
|
- **UFCTV**
|
||||||
- **UKTVPlay**
|
- **UKTVPlay**
|
||||||
|
- **umg:de**: Universal Music Deutschland
|
||||||
- **Unistra**
|
- **Unistra**
|
||||||
- **Unity**
|
- **Unity**
|
||||||
- **uol.com.br**
|
- **uol.com.br**
|
||||||
@ -914,7 +979,6 @@
|
|||||||
- **vice**
|
- **vice**
|
||||||
- **vice:article**
|
- **vice:article**
|
||||||
- **vice:show**
|
- **vice:show**
|
||||||
- **Viceland**
|
|
||||||
- **Vidbit**
|
- **Vidbit**
|
||||||
- **Viddler**
|
- **Viddler**
|
||||||
- **Videa**
|
- **Videa**
|
||||||
@ -930,6 +994,7 @@
|
|||||||
- **VideoPress**
|
- **VideoPress**
|
||||||
- **videoweed**: VideoWeed
|
- **videoweed**: VideoWeed
|
||||||
- **Vidio**
|
- **Vidio**
|
||||||
|
- **VidLii**
|
||||||
- **vidme**
|
- **vidme**
|
||||||
- **vidme:user**
|
- **vidme:user**
|
||||||
- **vidme:user:likes**
|
- **vidme:user:likes**
|
||||||
@ -954,6 +1019,7 @@
|
|||||||
- **Vimple**: Vimple - one-click video hosting
|
- **Vimple**: Vimple - one-click video hosting
|
||||||
- **Vine**
|
- **Vine**
|
||||||
- **vine:user**
|
- **vine:user**
|
||||||
|
- **Viqeo**
|
||||||
- **Viu**
|
- **Viu**
|
||||||
- **viu:ott**
|
- **viu:ott**
|
||||||
- **viu:playlist**
|
- **viu:playlist**
|
||||||
@ -979,12 +1045,14 @@
|
|||||||
- **vrv**
|
- **vrv**
|
||||||
- **vrv:series**
|
- **vrv:series**
|
||||||
- **VShare**
|
- **VShare**
|
||||||
|
- **VTXTV**
|
||||||
- **vube**: Vube.com
|
- **vube**: Vube.com
|
||||||
- **VuClip**
|
- **VuClip**
|
||||||
- **VVVVID**
|
- **VVVVID**
|
||||||
- **VyboryMos**
|
- **VyboryMos**
|
||||||
- **Vzaar**
|
- **Vzaar**
|
||||||
- **Walla**
|
- **Walla**
|
||||||
|
- **WalyTV**
|
||||||
- **washingtonpost**
|
- **washingtonpost**
|
||||||
- **washingtonpost:article**
|
- **washingtonpost:article**
|
||||||
- **wat.tv**
|
- **wat.tv**
|
||||||
@ -992,10 +1060,14 @@
|
|||||||
- **WatchIndianPorn**: Watch Indian Porn
|
- **WatchIndianPorn**: Watch Indian Porn
|
||||||
- **WDR**
|
- **WDR**
|
||||||
- **wdr:mobile**
|
- **wdr:mobile**
|
||||||
|
- **WDRElefant**
|
||||||
|
- **WDRPage**
|
||||||
- **Webcaster**
|
- **Webcaster**
|
||||||
- **WebcasterFeed**
|
- **WebcasterFeed**
|
||||||
- **WebOfStories**
|
- **WebOfStories**
|
||||||
- **WebOfStoriesPlaylist**
|
- **WebOfStoriesPlaylist**
|
||||||
|
- **Weibo**
|
||||||
|
- **WeiboMobile**
|
||||||
- **WeiqiTV**: WQTV
|
- **WeiqiTV**: WQTV
|
||||||
- **wholecloud**: WholeCloud
|
- **wholecloud**: WholeCloud
|
||||||
- **Wimp**
|
- **Wimp**
|
||||||
@ -1015,6 +1087,8 @@
|
|||||||
- **xiami:artist**: 虾米音乐 - 歌手
|
- **xiami:artist**: 虾米音乐 - 歌手
|
||||||
- **xiami:collection**: 虾米音乐 - 精选集
|
- **xiami:collection**: 虾米音乐 - 精选集
|
||||||
- **xiami:song**: 虾米音乐
|
- **xiami:song**: 虾米音乐
|
||||||
|
- **ximalaya**: 喜马拉雅FM
|
||||||
|
- **ximalaya:album**: 喜马拉雅FM 专辑
|
||||||
- **XMinus**
|
- **XMinus**
|
||||||
- **XNXX**
|
- **XNXX**
|
||||||
- **Xstream**
|
- **Xstream**
|
||||||
@ -1028,6 +1102,7 @@
|
|||||||
- **yandexmusic:album**: Яндекс.Музыка - Альбом
|
- **yandexmusic:album**: Яндекс.Музыка - Альбом
|
||||||
- **yandexmusic:playlist**: Яндекс.Музыка - Плейлист
|
- **yandexmusic:playlist**: Яндекс.Музыка - Плейлист
|
||||||
- **yandexmusic:track**: Яндекс.Музыка - Трек
|
- **yandexmusic:track**: Яндекс.Музыка - Трек
|
||||||
|
- **YapFiles**
|
||||||
- **YesJapan**
|
- **YesJapan**
|
||||||
- **yinyuetai:video**: 音悦Tai
|
- **yinyuetai:video**: 音悦Tai
|
||||||
- **Ynet**
|
- **Ynet**
|
||||||
@ -1038,6 +1113,7 @@
|
|||||||
- **YouNowLive**
|
- **YouNowLive**
|
||||||
- **YouNowMoment**
|
- **YouNowMoment**
|
||||||
- **YouPorn**
|
- **YouPorn**
|
||||||
|
- **YourPorn**
|
||||||
- **YourUpload**
|
- **YourUpload**
|
||||||
- **youtube**: YouTube.com
|
- **youtube**: YouTube.com
|
||||||
- **youtube:channel**: YouTube.com channels
|
- **youtube:channel**: YouTube.com channels
|
||||||
@ -1056,6 +1132,8 @@
|
|||||||
- **youtube:watchlater**: Youtube watch later list, ":ytwatchlater" for short (requires authentication)
|
- **youtube:watchlater**: Youtube watch later list, ":ytwatchlater" for short (requires authentication)
|
||||||
- **Zapiks**
|
- **Zapiks**
|
||||||
- **Zaq1**
|
- **Zaq1**
|
||||||
|
- **Zattoo**
|
||||||
|
- **ZattooLive**
|
||||||
- **ZDF**
|
- **ZDF**
|
||||||
- **ZDFChannel**
|
- **ZDFChannel**
|
||||||
- **zingmp3**: mp3.zing.vn
|
- **zingmp3**: mp3.zing.vn
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
universal = True
|
universal = True
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
exclude = youtube_dl/extractor/__init__.py,devscripts/buildserver.py,devscripts/lazy_load_template.py,devscripts/make_issue_template.py,setup.py,build,.git
|
exclude = youtube_dl/extractor/__init__.py,devscripts/buildserver.py,devscripts/lazy_load_template.py,devscripts/make_issue_template.py,setup.py,build,.git,venv
|
||||||
ignore = E402,E501,E731
|
ignore = E402,E501,E731,E741
|
||||||
|
1
setup.py
1
setup.py
@ -109,6 +109,7 @@ setup(
|
|||||||
author_email='ytdl@yt-dl.org',
|
author_email='ytdl@yt-dl.org',
|
||||||
maintainer='Sergey M.',
|
maintainer='Sergey M.',
|
||||||
maintainer_email='dstftw@gmail.com',
|
maintainer_email='dstftw@gmail.com',
|
||||||
|
license='Unlicense',
|
||||||
packages=[
|
packages=[
|
||||||
'youtube_dl',
|
'youtube_dl',
|
||||||
'youtube_dl.extractor', 'youtube_dl.downloader',
|
'youtube_dl.extractor', 'youtube_dl.downloader',
|
||||||
|
@ -493,9 +493,20 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
|
|||||||
_TEST_CASES = [
|
_TEST_CASES = [
|
||||||
(
|
(
|
||||||
# https://github.com/rg3/youtube-dl/issues/13919
|
# https://github.com/rg3/youtube-dl/issues/13919
|
||||||
|
# Also tests duplicate representation ids, see
|
||||||
|
# https://github.com/rg3/youtube-dl/issues/15111
|
||||||
'float_duration',
|
'float_duration',
|
||||||
'http://unknown/manifest.mpd',
|
'http://unknown/manifest.mpd',
|
||||||
[{
|
[{
|
||||||
|
'manifest_url': 'http://unknown/manifest.mpd',
|
||||||
|
'ext': 'm4a',
|
||||||
|
'format_id': '318597',
|
||||||
|
'format_note': 'DASH audio',
|
||||||
|
'protocol': 'http_dash_segments',
|
||||||
|
'acodec': 'mp4a.40.2',
|
||||||
|
'vcodec': 'none',
|
||||||
|
'tbr': 61.587,
|
||||||
|
}, {
|
||||||
'manifest_url': 'http://unknown/manifest.mpd',
|
'manifest_url': 'http://unknown/manifest.mpd',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'format_id': '318597',
|
'format_id': '318597',
|
||||||
@ -562,7 +573,89 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
|
|||||||
'width': 1920,
|
'width': 1920,
|
||||||
'height': 1080,
|
'height': 1080,
|
||||||
}]
|
}]
|
||||||
),
|
), (
|
||||||
|
# https://github.com/rg3/youtube-dl/pull/14844
|
||||||
|
'urls_only',
|
||||||
|
'http://unknown/manifest.mpd',
|
||||||
|
[{
|
||||||
|
'manifest_url': 'http://unknown/manifest.mpd',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'format_id': 'h264_aac_144p_m4s',
|
||||||
|
'format_note': 'DASH video',
|
||||||
|
'protocol': 'http_dash_segments',
|
||||||
|
'acodec': 'mp4a.40.2',
|
||||||
|
'vcodec': 'avc3.42c01e',
|
||||||
|
'tbr': 200,
|
||||||
|
'width': 256,
|
||||||
|
'height': 144,
|
||||||
|
}, {
|
||||||
|
'manifest_url': 'http://unknown/manifest.mpd',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'format_id': 'h264_aac_240p_m4s',
|
||||||
|
'format_note': 'DASH video',
|
||||||
|
'protocol': 'http_dash_segments',
|
||||||
|
'acodec': 'mp4a.40.2',
|
||||||
|
'vcodec': 'avc3.42c01e',
|
||||||
|
'tbr': 400,
|
||||||
|
'width': 424,
|
||||||
|
'height': 240,
|
||||||
|
}, {
|
||||||
|
'manifest_url': 'http://unknown/manifest.mpd',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'format_id': 'h264_aac_360p_m4s',
|
||||||
|
'format_note': 'DASH video',
|
||||||
|
'protocol': 'http_dash_segments',
|
||||||
|
'acodec': 'mp4a.40.2',
|
||||||
|
'vcodec': 'avc3.42c01e',
|
||||||
|
'tbr': 800,
|
||||||
|
'width': 640,
|
||||||
|
'height': 360,
|
||||||
|
}, {
|
||||||
|
'manifest_url': 'http://unknown/manifest.mpd',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'format_id': 'h264_aac_480p_m4s',
|
||||||
|
'format_note': 'DASH video',
|
||||||
|
'protocol': 'http_dash_segments',
|
||||||
|
'acodec': 'mp4a.40.2',
|
||||||
|
'vcodec': 'avc3.42c01e',
|
||||||
|
'tbr': 1200,
|
||||||
|
'width': 856,
|
||||||
|
'height': 480,
|
||||||
|
}, {
|
||||||
|
'manifest_url': 'http://unknown/manifest.mpd',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'format_id': 'h264_aac_576p_m4s',
|
||||||
|
'format_note': 'DASH video',
|
||||||
|
'protocol': 'http_dash_segments',
|
||||||
|
'acodec': 'mp4a.40.2',
|
||||||
|
'vcodec': 'avc3.42c01e',
|
||||||
|
'tbr': 1600,
|
||||||
|
'width': 1024,
|
||||||
|
'height': 576,
|
||||||
|
}, {
|
||||||
|
'manifest_url': 'http://unknown/manifest.mpd',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'format_id': 'h264_aac_720p_m4s',
|
||||||
|
'format_note': 'DASH video',
|
||||||
|
'protocol': 'http_dash_segments',
|
||||||
|
'acodec': 'mp4a.40.2',
|
||||||
|
'vcodec': 'avc3.42c01e',
|
||||||
|
'tbr': 2400,
|
||||||
|
'width': 1280,
|
||||||
|
'height': 720,
|
||||||
|
}, {
|
||||||
|
'manifest_url': 'http://unknown/manifest.mpd',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'format_id': 'h264_aac_1080p_m4s',
|
||||||
|
'format_note': 'DASH video',
|
||||||
|
'protocol': 'http_dash_segments',
|
||||||
|
'acodec': 'mp4a.40.2',
|
||||||
|
'vcodec': 'avc3.42c01e',
|
||||||
|
'tbr': 4400,
|
||||||
|
'width': 1920,
|
||||||
|
'height': 1080,
|
||||||
|
}]
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
for mpd_file, mpd_url, expected_formats in _TEST_CASES:
|
for mpd_file, mpd_url, expected_formats in _TEST_CASES:
|
||||||
@ -601,5 +694,55 @@ jwplayer("mediaplayer").setup({"abouttext":"Visit Indie DB","aboutlink":"http:\/
|
|||||||
self.ie._sort_formats(formats)
|
self.ie._sort_formats(formats)
|
||||||
expect_value(self, formats, expected_formats, None)
|
expect_value(self, formats, expected_formats, None)
|
||||||
|
|
||||||
|
def test_parse_xspf(self):
|
||||||
|
_TEST_CASES = [
|
||||||
|
(
|
||||||
|
'foo_xspf',
|
||||||
|
'https://example.org/src/foo_xspf.xspf',
|
||||||
|
[{
|
||||||
|
'id': 'foo_xspf',
|
||||||
|
'title': 'Pandemonium',
|
||||||
|
'description': 'Visit http://bigbrother404.bandcamp.com',
|
||||||
|
'duration': 202.416,
|
||||||
|
'formats': [{
|
||||||
|
'manifest_url': 'https://example.org/src/foo_xspf.xspf',
|
||||||
|
'url': 'https://example.org/src/cd1/track%201.mp3',
|
||||||
|
}],
|
||||||
|
}, {
|
||||||
|
'id': 'foo_xspf',
|
||||||
|
'title': 'Final Cartridge (Nichico Twelve Remix)',
|
||||||
|
'description': 'Visit http://bigbrother404.bandcamp.com',
|
||||||
|
'duration': 255.857,
|
||||||
|
'formats': [{
|
||||||
|
'manifest_url': 'https://example.org/src/foo_xspf.xspf',
|
||||||
|
'url': 'https://example.org/%E3%83%88%E3%83%A9%E3%83%83%E3%82%AF%E3%80%80%EF%BC%92.mp3',
|
||||||
|
}],
|
||||||
|
}, {
|
||||||
|
'id': 'foo_xspf',
|
||||||
|
'title': 'Rebuilding Nightingale',
|
||||||
|
'description': 'Visit http://bigbrother404.bandcamp.com',
|
||||||
|
'duration': 287.915,
|
||||||
|
'formats': [{
|
||||||
|
'manifest_url': 'https://example.org/src/foo_xspf.xspf',
|
||||||
|
'url': 'https://example.org/src/track3.mp3',
|
||||||
|
}, {
|
||||||
|
'manifest_url': 'https://example.org/src/foo_xspf.xspf',
|
||||||
|
'url': 'https://example.com/track3.mp3',
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
for xspf_file, xspf_url, expected_entries in _TEST_CASES:
|
||||||
|
with io.open('./test/testdata/xspf/%s.xspf' % xspf_file,
|
||||||
|
mode='r', encoding='utf-8') as f:
|
||||||
|
entries = self.ie._parse_xspf(
|
||||||
|
compat_etree_fromstring(f.read().encode('utf-8')),
|
||||||
|
xspf_file, xspf_url=xspf_url, xspf_base_url=xspf_url)
|
||||||
|
expect_value(self, entries, expected_entries, None)
|
||||||
|
for i in range(len(entries)):
|
||||||
|
expect_dict(self, entries[i], expected_entries[i])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -466,11 +466,11 @@ class TestFormatSelection(unittest.TestCase):
|
|||||||
ydl = YDL({'simulate': True})
|
ydl = YDL({'simulate': True})
|
||||||
self.assertEqual(ydl._default_format_spec({}), 'bestvideo+bestaudio/best')
|
self.assertEqual(ydl._default_format_spec({}), 'bestvideo+bestaudio/best')
|
||||||
|
|
||||||
ydl = YDL({'is_live': True})
|
ydl = YDL({})
|
||||||
self.assertEqual(ydl._default_format_spec({}), 'best/bestvideo+bestaudio')
|
self.assertEqual(ydl._default_format_spec({'is_live': True}), 'best/bestvideo+bestaudio')
|
||||||
|
|
||||||
ydl = YDL({'simulate': True, 'is_live': True})
|
ydl = YDL({'simulate': True})
|
||||||
self.assertEqual(ydl._default_format_spec({}), 'bestvideo+bestaudio/best')
|
self.assertEqual(ydl._default_format_spec({'is_live': True}), 'bestvideo+bestaudio/best')
|
||||||
|
|
||||||
ydl = YDL({'outtmpl': '-'})
|
ydl = YDL({'outtmpl': '-'})
|
||||||
self.assertEqual(ydl._default_format_spec({}), 'best/bestvideo+bestaudio')
|
self.assertEqual(ydl._default_format_spec({}), 'best/bestvideo+bestaudio')
|
||||||
|
@ -92,8 +92,8 @@ class TestDownload(unittest.TestCase):
|
|||||||
def generator(test_case, tname):
|
def generator(test_case, tname):
|
||||||
|
|
||||||
def test_template(self):
|
def test_template(self):
|
||||||
ie = youtube_dl.extractor.get_info_extractor(test_case['name'])
|
ie = youtube_dl.extractor.get_info_extractor(test_case['name'])()
|
||||||
other_ies = [get_info_extractor(ie_key) for ie_key in test_case.get('add_ie', [])]
|
other_ies = [get_info_extractor(ie_key)() for ie_key in test_case.get('add_ie', [])]
|
||||||
is_playlist = any(k.startswith('playlist') for k in test_case)
|
is_playlist = any(k.startswith('playlist') for k in test_case)
|
||||||
test_cases = test_case.get(
|
test_cases = test_case.get(
|
||||||
'playlist', [] if is_playlist else [test_case])
|
'playlist', [] if is_playlist else [test_case])
|
||||||
|
125
test/test_downloader_http.py
Normal file
125
test/test_downloader_http.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
# Allow direct execution
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from test.helper import try_rm
|
||||||
|
from youtube_dl import YoutubeDL
|
||||||
|
from youtube_dl.compat import compat_http_server
|
||||||
|
from youtube_dl.downloader.http import HttpFD
|
||||||
|
from youtube_dl.utils import encodeFilename
|
||||||
|
import ssl
|
||||||
|
import threading
|
||||||
|
|
||||||
|
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def http_server_port(httpd):
|
||||||
|
if os.name == 'java' and isinstance(httpd.socket, ssl.SSLSocket):
|
||||||
|
# In Jython SSLSocket is not a subclass of socket.socket
|
||||||
|
sock = httpd.socket.sock
|
||||||
|
else:
|
||||||
|
sock = httpd.socket
|
||||||
|
return sock.getsockname()[1]
|
||||||
|
|
||||||
|
|
||||||
|
TEST_SIZE = 10 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPTestRequestHandler(compat_http_server.BaseHTTPRequestHandler):
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_content_range(self, total=None):
|
||||||
|
range_header = self.headers.get('Range')
|
||||||
|
start = end = None
|
||||||
|
if range_header:
|
||||||
|
mobj = re.search(r'^bytes=(\d+)-(\d+)', range_header)
|
||||||
|
if mobj:
|
||||||
|
start = int(mobj.group(1))
|
||||||
|
end = int(mobj.group(2))
|
||||||
|
valid_range = start is not None and end is not None
|
||||||
|
if valid_range:
|
||||||
|
content_range = 'bytes %d-%d' % (start, end)
|
||||||
|
if total:
|
||||||
|
content_range += '/%d' % total
|
||||||
|
self.send_header('Content-Range', content_range)
|
||||||
|
return (end - start + 1) if valid_range else total
|
||||||
|
|
||||||
|
def serve(self, range=True, content_length=True):
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', 'video/mp4')
|
||||||
|
size = TEST_SIZE
|
||||||
|
if range:
|
||||||
|
size = self.send_content_range(TEST_SIZE)
|
||||||
|
if content_length:
|
||||||
|
self.send_header('Content-Length', size)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(b'#' * size)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == '/regular':
|
||||||
|
self.serve()
|
||||||
|
elif self.path == '/no-content-length':
|
||||||
|
self.serve(content_length=False)
|
||||||
|
elif self.path == '/no-range':
|
||||||
|
self.serve(range=False)
|
||||||
|
elif self.path == '/no-range-no-content-length':
|
||||||
|
self.serve(range=False, content_length=False)
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
|
||||||
|
class FakeLogger(object):
|
||||||
|
def debug(self, msg):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def warning(self, msg):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def error(self, msg):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestHttpFD(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.httpd = compat_http_server.HTTPServer(
|
||||||
|
('127.0.0.1', 0), HTTPTestRequestHandler)
|
||||||
|
self.port = http_server_port(self.httpd)
|
||||||
|
self.server_thread = threading.Thread(target=self.httpd.serve_forever)
|
||||||
|
self.server_thread.daemon = True
|
||||||
|
self.server_thread.start()
|
||||||
|
|
||||||
|
def download(self, params, ep):
|
||||||
|
params['logger'] = FakeLogger()
|
||||||
|
ydl = YoutubeDL(params)
|
||||||
|
downloader = HttpFD(ydl, params)
|
||||||
|
filename = 'testfile.mp4'
|
||||||
|
try_rm(encodeFilename(filename))
|
||||||
|
self.assertTrue(downloader.real_download(filename, {
|
||||||
|
'url': 'http://127.0.0.1:%d/%s' % (self.port, ep),
|
||||||
|
}))
|
||||||
|
self.assertEqual(os.path.getsize(encodeFilename(filename)), TEST_SIZE)
|
||||||
|
try_rm(encodeFilename(filename))
|
||||||
|
|
||||||
|
def download_all(self, params):
|
||||||
|
for ep in ('regular', 'no-content-length', 'no-range', 'no-range-no-content-length'):
|
||||||
|
self.download(params, ep)
|
||||||
|
|
||||||
|
def test_regular(self):
|
||||||
|
self.download_all({})
|
||||||
|
|
||||||
|
def test_chunked(self):
|
||||||
|
self.download_all({
|
||||||
|
'http_chunk_size': 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
@ -47,7 +47,7 @@ class HTTPTestRequestHandler(compat_http_server.BaseHTTPRequestHandler):
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
return
|
return
|
||||||
|
|
||||||
new_url = 'http://localhost:%d/中文.html' % http_server_port(self.server)
|
new_url = 'http://127.0.0.1:%d/中文.html' % http_server_port(self.server)
|
||||||
self.send_response(302)
|
self.send_response(302)
|
||||||
self.send_header(b'Location', new_url.encode('utf-8'))
|
self.send_header(b'Location', new_url.encode('utf-8'))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@ -74,7 +74,7 @@ class FakeLogger(object):
|
|||||||
class TestHTTP(unittest.TestCase):
|
class TestHTTP(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.httpd = compat_http_server.HTTPServer(
|
self.httpd = compat_http_server.HTTPServer(
|
||||||
('localhost', 0), HTTPTestRequestHandler)
|
('127.0.0.1', 0), HTTPTestRequestHandler)
|
||||||
self.port = http_server_port(self.httpd)
|
self.port = http_server_port(self.httpd)
|
||||||
self.server_thread = threading.Thread(target=self.httpd.serve_forever)
|
self.server_thread = threading.Thread(target=self.httpd.serve_forever)
|
||||||
self.server_thread.daemon = True
|
self.server_thread.daemon = True
|
||||||
@ -86,15 +86,15 @@ class TestHTTP(unittest.TestCase):
|
|||||||
return
|
return
|
||||||
|
|
||||||
ydl = YoutubeDL({'logger': FakeLogger()})
|
ydl = YoutubeDL({'logger': FakeLogger()})
|
||||||
r = ydl.extract_info('http://localhost:%d/302' % self.port)
|
r = ydl.extract_info('http://127.0.0.1:%d/302' % self.port)
|
||||||
self.assertEqual(r['entries'][0]['url'], 'http://localhost:%d/vid.mp4' % self.port)
|
self.assertEqual(r['entries'][0]['url'], 'http://127.0.0.1:%d/vid.mp4' % self.port)
|
||||||
|
|
||||||
|
|
||||||
class TestHTTPS(unittest.TestCase):
|
class TestHTTPS(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
certfn = os.path.join(TEST_DIR, 'testcert.pem')
|
certfn = os.path.join(TEST_DIR, 'testcert.pem')
|
||||||
self.httpd = compat_http_server.HTTPServer(
|
self.httpd = compat_http_server.HTTPServer(
|
||||||
('localhost', 0), HTTPTestRequestHandler)
|
('127.0.0.1', 0), HTTPTestRequestHandler)
|
||||||
self.httpd.socket = ssl.wrap_socket(
|
self.httpd.socket = ssl.wrap_socket(
|
||||||
self.httpd.socket, certfile=certfn, server_side=True)
|
self.httpd.socket, certfile=certfn, server_side=True)
|
||||||
self.port = http_server_port(self.httpd)
|
self.port = http_server_port(self.httpd)
|
||||||
@ -107,11 +107,11 @@ class TestHTTPS(unittest.TestCase):
|
|||||||
ydl = YoutubeDL({'logger': FakeLogger()})
|
ydl = YoutubeDL({'logger': FakeLogger()})
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
Exception,
|
Exception,
|
||||||
ydl.extract_info, 'https://localhost:%d/video.html' % self.port)
|
ydl.extract_info, 'https://127.0.0.1:%d/video.html' % self.port)
|
||||||
|
|
||||||
ydl = YoutubeDL({'logger': FakeLogger(), 'nocheckcertificate': True})
|
ydl = YoutubeDL({'logger': FakeLogger(), 'nocheckcertificate': True})
|
||||||
r = ydl.extract_info('https://localhost:%d/video.html' % self.port)
|
r = ydl.extract_info('https://127.0.0.1:%d/video.html' % self.port)
|
||||||
self.assertEqual(r['entries'][0]['url'], 'https://localhost:%d/vid.mp4' % self.port)
|
self.assertEqual(r['entries'][0]['url'], 'https://127.0.0.1:%d/vid.mp4' % self.port)
|
||||||
|
|
||||||
|
|
||||||
def _build_proxy_handler(name):
|
def _build_proxy_handler(name):
|
||||||
@ -132,23 +132,23 @@ def _build_proxy_handler(name):
|
|||||||
class TestProxy(unittest.TestCase):
|
class TestProxy(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.proxy = compat_http_server.HTTPServer(
|
self.proxy = compat_http_server.HTTPServer(
|
||||||
('localhost', 0), _build_proxy_handler('normal'))
|
('127.0.0.1', 0), _build_proxy_handler('normal'))
|
||||||
self.port = http_server_port(self.proxy)
|
self.port = http_server_port(self.proxy)
|
||||||
self.proxy_thread = threading.Thread(target=self.proxy.serve_forever)
|
self.proxy_thread = threading.Thread(target=self.proxy.serve_forever)
|
||||||
self.proxy_thread.daemon = True
|
self.proxy_thread.daemon = True
|
||||||
self.proxy_thread.start()
|
self.proxy_thread.start()
|
||||||
|
|
||||||
self.geo_proxy = compat_http_server.HTTPServer(
|
self.geo_proxy = compat_http_server.HTTPServer(
|
||||||
('localhost', 0), _build_proxy_handler('geo'))
|
('127.0.0.1', 0), _build_proxy_handler('geo'))
|
||||||
self.geo_port = http_server_port(self.geo_proxy)
|
self.geo_port = http_server_port(self.geo_proxy)
|
||||||
self.geo_proxy_thread = threading.Thread(target=self.geo_proxy.serve_forever)
|
self.geo_proxy_thread = threading.Thread(target=self.geo_proxy.serve_forever)
|
||||||
self.geo_proxy_thread.daemon = True
|
self.geo_proxy_thread.daemon = True
|
||||||
self.geo_proxy_thread.start()
|
self.geo_proxy_thread.start()
|
||||||
|
|
||||||
def test_proxy(self):
|
def test_proxy(self):
|
||||||
geo_proxy = 'localhost:{0}'.format(self.geo_port)
|
geo_proxy = '127.0.0.1:{0}'.format(self.geo_port)
|
||||||
ydl = YoutubeDL({
|
ydl = YoutubeDL({
|
||||||
'proxy': 'localhost:{0}'.format(self.port),
|
'proxy': '127.0.0.1:{0}'.format(self.port),
|
||||||
'geo_verification_proxy': geo_proxy,
|
'geo_verification_proxy': geo_proxy,
|
||||||
})
|
})
|
||||||
url = 'http://foo.com/bar'
|
url = 'http://foo.com/bar'
|
||||||
@ -162,7 +162,7 @@ class TestProxy(unittest.TestCase):
|
|||||||
|
|
||||||
def test_proxy_with_idn(self):
|
def test_proxy_with_idn(self):
|
||||||
ydl = YoutubeDL({
|
ydl = YoutubeDL({
|
||||||
'proxy': 'localhost:{0}'.format(self.port),
|
'proxy': '127.0.0.1:{0}'.format(self.port),
|
||||||
})
|
})
|
||||||
url = 'http://中文.tw/'
|
url = 'http://中文.tw/'
|
||||||
response = ydl.urlopen(url).read().decode('utf-8')
|
response = ydl.urlopen(url).read().decode('utf-8')
|
||||||
|
@ -232,7 +232,7 @@ class TestNPOSubtitles(BaseTestSubtitles):
|
|||||||
|
|
||||||
|
|
||||||
class TestMTVSubtitles(BaseTestSubtitles):
|
class TestMTVSubtitles(BaseTestSubtitles):
|
||||||
url = 'http://www.cc.com/video-clips/kllhuv/stand-up-greg-fitzsimmons--uncensored---too-good-of-a-mother'
|
url = 'http://www.cc.com/video-clips/p63lk0/adam-devine-s-house-party-chasing-white-swans'
|
||||||
IE = ComedyCentralIE
|
IE = ComedyCentralIE
|
||||||
|
|
||||||
def getInfoDict(self):
|
def getInfoDict(self):
|
||||||
@ -243,7 +243,7 @@ class TestMTVSubtitles(BaseTestSubtitles):
|
|||||||
self.DL.params['allsubtitles'] = True
|
self.DL.params['allsubtitles'] = True
|
||||||
subtitles = self.getSubtitles()
|
subtitles = self.getSubtitles()
|
||||||
self.assertEqual(set(subtitles.keys()), set(['en']))
|
self.assertEqual(set(subtitles.keys()), set(['en']))
|
||||||
self.assertEqual(md5(subtitles['en']), 'b9f6ca22a6acf597ec76f61749765e65')
|
self.assertEqual(md5(subtitles['en']), '78206b8d8a0cfa9da64dc026eea48961')
|
||||||
|
|
||||||
|
|
||||||
class TestNRKSubtitles(BaseTestSubtitles):
|
class TestNRKSubtitles(BaseTestSubtitles):
|
||||||
|
@ -42,6 +42,7 @@ from youtube_dl.utils import (
|
|||||||
is_html,
|
is_html,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
limit_length,
|
limit_length,
|
||||||
|
merge_dicts,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
month_by_name,
|
month_by_name,
|
||||||
multipart_encode,
|
multipart_encode,
|
||||||
@ -53,10 +54,12 @@ from youtube_dl.utils import (
|
|||||||
parse_filesize,
|
parse_filesize,
|
||||||
parse_count,
|
parse_count,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
|
parse_resolution,
|
||||||
pkcs1pad,
|
pkcs1pad,
|
||||||
read_batch_urls,
|
read_batch_urls,
|
||||||
sanitize_filename,
|
sanitize_filename,
|
||||||
sanitize_path,
|
sanitize_path,
|
||||||
|
sanitize_url,
|
||||||
expand_path,
|
expand_path,
|
||||||
prepend_extension,
|
prepend_extension,
|
||||||
replace_extension,
|
replace_extension,
|
||||||
@ -75,6 +78,7 @@ from youtube_dl.utils import (
|
|||||||
uppercase_escape,
|
uppercase_escape,
|
||||||
lowercase_escape,
|
lowercase_escape,
|
||||||
url_basename,
|
url_basename,
|
||||||
|
url_or_none,
|
||||||
base_url,
|
base_url,
|
||||||
urljoin,
|
urljoin,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
@ -219,6 +223,12 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(sanitize_path('./abc'), 'abc')
|
self.assertEqual(sanitize_path('./abc'), 'abc')
|
||||||
self.assertEqual(sanitize_path('./../abc'), '..\\abc')
|
self.assertEqual(sanitize_path('./../abc'), '..\\abc')
|
||||||
|
|
||||||
|
def test_sanitize_url(self):
|
||||||
|
self.assertEqual(sanitize_url('//foo.bar'), 'http://foo.bar')
|
||||||
|
self.assertEqual(sanitize_url('httpss://foo.bar'), 'https://foo.bar')
|
||||||
|
self.assertEqual(sanitize_url('rmtps://foo.bar'), 'rtmps://foo.bar')
|
||||||
|
self.assertEqual(sanitize_url('https://foo.bar'), 'https://foo.bar')
|
||||||
|
|
||||||
def test_expand_path(self):
|
def test_expand_path(self):
|
||||||
def env(var):
|
def env(var):
|
||||||
return '%{0}%'.format(var) if sys.platform == 'win32' else '${0}'.format(var)
|
return '%{0}%'.format(var) if sys.platform == 'win32' else '${0}'.format(var)
|
||||||
@ -343,6 +353,8 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(unified_timestamp('Feb 7, 2016 at 6:35 pm'), 1454870100)
|
self.assertEqual(unified_timestamp('Feb 7, 2016 at 6:35 pm'), 1454870100)
|
||||||
self.assertEqual(unified_timestamp('2017-03-30T17:52:41Q'), 1490896361)
|
self.assertEqual(unified_timestamp('2017-03-30T17:52:41Q'), 1490896361)
|
||||||
self.assertEqual(unified_timestamp('Sep 11, 2013 | 5:49 AM'), 1378878540)
|
self.assertEqual(unified_timestamp('Sep 11, 2013 | 5:49 AM'), 1378878540)
|
||||||
|
self.assertEqual(unified_timestamp('December 15, 2017 at 7:49 am'), 1513324140)
|
||||||
|
self.assertEqual(unified_timestamp('2018-03-14T08:32:43.1493874+00:00'), 1521016363)
|
||||||
|
|
||||||
def test_determine_ext(self):
|
def test_determine_ext(self):
|
||||||
self.assertEqual(determine_ext('http://example.com/foo/bar.mp4/?download'), 'mp4')
|
self.assertEqual(determine_ext('http://example.com/foo/bar.mp4/?download'), 'mp4')
|
||||||
@ -350,6 +362,7 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(determine_ext('http://example.com/foo/bar.nonext/?download', None), None)
|
self.assertEqual(determine_ext('http://example.com/foo/bar.nonext/?download', None), None)
|
||||||
self.assertEqual(determine_ext('http://example.com/foo/bar/mp4?download', None), None)
|
self.assertEqual(determine_ext('http://example.com/foo/bar/mp4?download', None), None)
|
||||||
self.assertEqual(determine_ext('http://example.com/foo/bar.m3u8//?download'), 'm3u8')
|
self.assertEqual(determine_ext('http://example.com/foo/bar.m3u8//?download'), 'm3u8')
|
||||||
|
self.assertEqual(determine_ext('foobar', None), None)
|
||||||
|
|
||||||
def test_find_xpath_attr(self):
|
def test_find_xpath_attr(self):
|
||||||
testxml = '''<root>
|
testxml = '''<root>
|
||||||
@ -495,6 +508,16 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(urljoin('http://foo.de/', ['foobar']), None)
|
self.assertEqual(urljoin('http://foo.de/', ['foobar']), None)
|
||||||
self.assertEqual(urljoin('http://foo.de/a/b/c.txt', '.././../d.txt'), 'http://foo.de/d.txt')
|
self.assertEqual(urljoin('http://foo.de/a/b/c.txt', '.././../d.txt'), 'http://foo.de/d.txt')
|
||||||
|
|
||||||
|
def test_url_or_none(self):
|
||||||
|
self.assertEqual(url_or_none(None), None)
|
||||||
|
self.assertEqual(url_or_none(''), None)
|
||||||
|
self.assertEqual(url_or_none('foo'), None)
|
||||||
|
self.assertEqual(url_or_none('http://foo.de'), 'http://foo.de')
|
||||||
|
self.assertEqual(url_or_none('https://foo.de'), 'https://foo.de')
|
||||||
|
self.assertEqual(url_or_none('http$://foo.de'), None)
|
||||||
|
self.assertEqual(url_or_none('http://foo.de'), 'http://foo.de')
|
||||||
|
self.assertEqual(url_or_none('//foo.de'), '//foo.de')
|
||||||
|
|
||||||
def test_parse_age_limit(self):
|
def test_parse_age_limit(self):
|
||||||
self.assertEqual(parse_age_limit(None), None)
|
self.assertEqual(parse_age_limit(None), None)
|
||||||
self.assertEqual(parse_age_limit(False), None)
|
self.assertEqual(parse_age_limit(False), None)
|
||||||
@ -508,6 +531,8 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(parse_age_limit('PG-13'), 13)
|
self.assertEqual(parse_age_limit('PG-13'), 13)
|
||||||
self.assertEqual(parse_age_limit('TV-14'), 14)
|
self.assertEqual(parse_age_limit('TV-14'), 14)
|
||||||
self.assertEqual(parse_age_limit('TV-MA'), 17)
|
self.assertEqual(parse_age_limit('TV-MA'), 17)
|
||||||
|
self.assertEqual(parse_age_limit('TV14'), 14)
|
||||||
|
self.assertEqual(parse_age_limit('TV_G'), 0)
|
||||||
|
|
||||||
def test_parse_duration(self):
|
def test_parse_duration(self):
|
||||||
self.assertEqual(parse_duration(None), None)
|
self.assertEqual(parse_duration(None), None)
|
||||||
@ -659,6 +684,17 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(dict_get(d, ('b', 'c', key, )), None)
|
self.assertEqual(dict_get(d, ('b', 'c', key, )), None)
|
||||||
self.assertEqual(dict_get(d, ('b', 'c', key, ), skip_false_values=False), false_value)
|
self.assertEqual(dict_get(d, ('b', 'c', key, ), skip_false_values=False), false_value)
|
||||||
|
|
||||||
|
def test_merge_dicts(self):
|
||||||
|
self.assertEqual(merge_dicts({'a': 1}, {'b': 2}), {'a': 1, 'b': 2})
|
||||||
|
self.assertEqual(merge_dicts({'a': 1}, {'a': 2}), {'a': 1})
|
||||||
|
self.assertEqual(merge_dicts({'a': 1}, {'a': None}), {'a': 1})
|
||||||
|
self.assertEqual(merge_dicts({'a': 1}, {'a': ''}), {'a': 1})
|
||||||
|
self.assertEqual(merge_dicts({'a': 1}, {}), {'a': 1})
|
||||||
|
self.assertEqual(merge_dicts({'a': None}, {'a': 1}), {'a': 1})
|
||||||
|
self.assertEqual(merge_dicts({'a': ''}, {'a': 1}), {'a': ''})
|
||||||
|
self.assertEqual(merge_dicts({'a': ''}, {'a': 'abc'}), {'a': 'abc'})
|
||||||
|
self.assertEqual(merge_dicts({'a': None}, {'a': ''}, {'a': 'abc'}), {'a': 'abc'})
|
||||||
|
|
||||||
def test_encode_compat_str(self):
|
def test_encode_compat_str(self):
|
||||||
self.assertEqual(encode_compat_str(b'\xd1\x82\xd0\xb5\xd1\x81\xd1\x82', 'utf-8'), 'тест')
|
self.assertEqual(encode_compat_str(b'\xd1\x82\xd0\xb5\xd1\x81\xd1\x82', 'utf-8'), 'тест')
|
||||||
self.assertEqual(encode_compat_str('тест', 'utf-8'), 'тест')
|
self.assertEqual(encode_compat_str('тест', 'utf-8'), 'тест')
|
||||||
@ -692,6 +728,10 @@ class TestUtil(unittest.TestCase):
|
|||||||
d = json.loads(stripped)
|
d = json.loads(stripped)
|
||||||
self.assertEqual(d, {'status': 'success'})
|
self.assertEqual(d, {'status': 'success'})
|
||||||
|
|
||||||
|
stripped = strip_jsonp('({"status": "success"});')
|
||||||
|
d = json.loads(stripped)
|
||||||
|
self.assertEqual(d, {'status': 'success'})
|
||||||
|
|
||||||
def test_uppercase_escape(self):
|
def test_uppercase_escape(self):
|
||||||
self.assertEqual(uppercase_escape('aä'), 'aä')
|
self.assertEqual(uppercase_escape('aä'), 'aä')
|
||||||
self.assertEqual(uppercase_escape('\\U0001d550'), '𝕐')
|
self.assertEqual(uppercase_escape('\\U0001d550'), '𝕐')
|
||||||
@ -745,6 +785,10 @@ class TestUtil(unittest.TestCase):
|
|||||||
'vcodec': 'h264',
|
'vcodec': 'h264',
|
||||||
'acodec': 'aac',
|
'acodec': 'aac',
|
||||||
})
|
})
|
||||||
|
self.assertEqual(parse_codecs('av01.0.05M.08'), {
|
||||||
|
'vcodec': 'av01.0.05M.08',
|
||||||
|
'acodec': 'none',
|
||||||
|
})
|
||||||
|
|
||||||
def test_escape_rfc3986(self):
|
def test_escape_rfc3986(self):
|
||||||
reserved = "!*'();:@&=+$,/?#[]"
|
reserved = "!*'();:@&=+$,/?#[]"
|
||||||
@ -813,6 +857,9 @@ class TestUtil(unittest.TestCase):
|
|||||||
inp = '''{"duration": "00:01:07"}'''
|
inp = '''{"duration": "00:01:07"}'''
|
||||||
self.assertEqual(js_to_json(inp), '''{"duration": "00:01:07"}''')
|
self.assertEqual(js_to_json(inp), '''{"duration": "00:01:07"}''')
|
||||||
|
|
||||||
|
inp = '''{segments: [{"offset":-3.885780586188048e-16,"duration":39.75000000000001}]}'''
|
||||||
|
self.assertEqual(js_to_json(inp), '''{"segments": [{"offset":-3.885780586188048e-16,"duration":39.75000000000001}]}''')
|
||||||
|
|
||||||
def test_js_to_json_edgecases(self):
|
def test_js_to_json_edgecases(self):
|
||||||
on = js_to_json("{abc_def:'1\\'\\\\2\\\\\\'3\"4'}")
|
on = js_to_json("{abc_def:'1\\'\\\\2\\\\\\'3\"4'}")
|
||||||
self.assertEqual(json.loads(on), {"abc_def": "1'\\2\\'3\"4"})
|
self.assertEqual(json.loads(on), {"abc_def": "1'\\2\\'3\"4"})
|
||||||
@ -884,6 +931,13 @@ class TestUtil(unittest.TestCase):
|
|||||||
on = js_to_json('{/*comment\n*/42/*comment\n*/:/*comment\n*/42/*comment\n*/}')
|
on = js_to_json('{/*comment\n*/42/*comment\n*/:/*comment\n*/42/*comment\n*/}')
|
||||||
self.assertEqual(json.loads(on), {'42': 42})
|
self.assertEqual(json.loads(on), {'42': 42})
|
||||||
|
|
||||||
|
on = js_to_json('{42:4.2e1}')
|
||||||
|
self.assertEqual(json.loads(on), {'42': 42.0})
|
||||||
|
|
||||||
|
def test_js_to_json_malformed(self):
|
||||||
|
self.assertEqual(js_to_json('42a1'), '42"a1"')
|
||||||
|
self.assertEqual(js_to_json('42a-1'), '42"a"-1')
|
||||||
|
|
||||||
def test_extract_attributes(self):
|
def test_extract_attributes(self):
|
||||||
self.assertEqual(extract_attributes('<e x="y">'), {'x': 'y'})
|
self.assertEqual(extract_attributes('<e x="y">'), {'x': 'y'})
|
||||||
self.assertEqual(extract_attributes("<e x='y'>"), {'x': 'y'})
|
self.assertEqual(extract_attributes("<e x='y'>"), {'x': 'y'})
|
||||||
@ -964,6 +1018,16 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(parse_count('1.1kk '), 1100000)
|
self.assertEqual(parse_count('1.1kk '), 1100000)
|
||||||
self.assertEqual(parse_count('1.1kk views'), 1100000)
|
self.assertEqual(parse_count('1.1kk views'), 1100000)
|
||||||
|
|
||||||
|
def test_parse_resolution(self):
|
||||||
|
self.assertEqual(parse_resolution(None), {})
|
||||||
|
self.assertEqual(parse_resolution(''), {})
|
||||||
|
self.assertEqual(parse_resolution('1920x1080'), {'width': 1920, 'height': 1080})
|
||||||
|
self.assertEqual(parse_resolution('1920×1080'), {'width': 1920, 'height': 1080})
|
||||||
|
self.assertEqual(parse_resolution('1920 x 1080'), {'width': 1920, 'height': 1080})
|
||||||
|
self.assertEqual(parse_resolution('720p'), {'height': 720})
|
||||||
|
self.assertEqual(parse_resolution('4k'), {'height': 2160})
|
||||||
|
self.assertEqual(parse_resolution('8K'), {'height': 4320})
|
||||||
|
|
||||||
def test_version_tuple(self):
|
def test_version_tuple(self):
|
||||||
self.assertEqual(version_tuple('1'), (1,))
|
self.assertEqual(version_tuple('1'), (1,))
|
||||||
self.assertEqual(version_tuple('10.23.344'), (10, 23, 344))
|
self.assertEqual(version_tuple('10.23.344'), (10, 23, 344))
|
||||||
@ -1042,6 +1106,18 @@ ffmpeg version 2.4.4 Copyright (c) 2000-2014 the FFmpeg ...'''), '2.4.4')
|
|||||||
self.assertFalse(match_str(
|
self.assertFalse(match_str(
|
||||||
'like_count > 100 & dislike_count <? 50 & description',
|
'like_count > 100 & dislike_count <? 50 & description',
|
||||||
{'like_count': 190, 'dislike_count': 10}))
|
{'like_count': 190, 'dislike_count': 10}))
|
||||||
|
self.assertTrue(match_str('is_live', {'is_live': True}))
|
||||||
|
self.assertFalse(match_str('is_live', {'is_live': False}))
|
||||||
|
self.assertFalse(match_str('is_live', {'is_live': None}))
|
||||||
|
self.assertFalse(match_str('is_live', {}))
|
||||||
|
self.assertFalse(match_str('!is_live', {'is_live': True}))
|
||||||
|
self.assertTrue(match_str('!is_live', {'is_live': False}))
|
||||||
|
self.assertTrue(match_str('!is_live', {'is_live': None}))
|
||||||
|
self.assertTrue(match_str('!is_live', {}))
|
||||||
|
self.assertTrue(match_str('title', {'title': 'abc'}))
|
||||||
|
self.assertTrue(match_str('title', {'title': ''}))
|
||||||
|
self.assertFalse(match_str('!title', {'title': 'abc'}))
|
||||||
|
self.assertFalse(match_str('!title', {'title': ''}))
|
||||||
|
|
||||||
def test_parse_dfxp_time_expr(self):
|
def test_parse_dfxp_time_expr(self):
|
||||||
self.assertEqual(parse_dfxp_time_expr(None), None)
|
self.assertEqual(parse_dfxp_time_expr(None), None)
|
||||||
|
@ -61,7 +61,7 @@ class TestYoutubeLists(unittest.TestCase):
|
|||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
dl.params['extract_flat'] = True
|
dl.params['extract_flat'] = True
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubePlaylistIE(dl)
|
||||||
result = ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')
|
result = ie.extract('https://www.youtube.com/playlist?list=PL-KKIb8rvtMSrAO9YFbeM6UQrAqoFTUWv')
|
||||||
self.assertIsPlaylist(result)
|
self.assertIsPlaylist(result)
|
||||||
for entry in result['entries']:
|
for entry in result['entries']:
|
||||||
self.assertTrue(entry.get('title'))
|
self.assertTrue(entry.get('title'))
|
||||||
|
218
test/testdata/mpd/urls_only.mpd
vendored
Normal file
218
test/testdata/mpd/urls_only.mpd
vendored
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<MPD maxSegmentDuration="PT0H0M10.000S" mediaPresentationDuration="PT0H4M1.728S" minBufferTime="PT1.500S" profiles="urn:mpeg:dash:profile:isoff-main:2011" type="static" xmlns="urn:mpeg:dash:schema:mpd:2011">
|
||||||
|
<Period duration="PT0H4M1.728S">
|
||||||
|
<AdaptationSet bitstreamSwitching="true" lang="und" maxHeight="1080" maxWidth="1920" par="16:9" segmentAlignment="true">
|
||||||
|
<ContentComponent contentType="video" id="1"/>
|
||||||
|
<Representation audioSamplingRate="44100" bandwidth="200000" codecs="avc3.42c01e,mp4a.40.2" frameRate="25" height="144" id="h264_aac_144p_m4s" mimeType="video/mp4" sar="1:1" startWithSAP="1" width="256">
|
||||||
|
<SegmentList duration="10000" timescale="1000">
|
||||||
|
<Initialization sourceURL="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/init/432f65a0.mp4"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/0/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/1/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/2/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/3/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/4/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/5/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/6/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/7/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/8/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/9/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/10/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/11/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/12/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/13/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/14/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/15/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/16/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/17/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/18/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/19/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/20/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/21/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/22/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/23/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_144p_m4s/24/432f65a0.m4s"/>
|
||||||
|
</SegmentList>
|
||||||
|
</Representation>
|
||||||
|
<Representation audioSamplingRate="44100" bandwidth="400000" codecs="avc3.42c01e,mp4a.40.2" frameRate="25" height="240" id="h264_aac_240p_m4s" mimeType="video/mp4" sar="160:159" startWithSAP="1" width="424">
|
||||||
|
<SegmentList duration="10000" timescale="1000">
|
||||||
|
<Initialization sourceURL="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/init/432f65a0.mp4"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/0/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/1/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/2/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/3/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/4/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/5/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/6/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/7/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/8/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/9/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/10/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/11/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/12/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/13/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/14/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/15/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/16/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/17/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/18/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/19/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/20/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/21/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/22/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/23/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_240p_m4s/24/432f65a0.m4s"/>
|
||||||
|
</SegmentList>
|
||||||
|
</Representation>
|
||||||
|
<Representation audioSamplingRate="44100" bandwidth="800000" codecs="avc3.42c01e,mp4a.40.2" frameRate="25" height="360" id="h264_aac_360p_m4s" mimeType="video/mp4" sar="1:1" startWithSAP="1" width="640">
|
||||||
|
<SegmentList duration="10000" timescale="1000">
|
||||||
|
<Initialization sourceURL="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/init/432f65a0.mp4"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/0/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/1/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/2/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/3/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/4/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/5/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/6/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/7/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/8/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/9/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/10/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/11/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/12/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/13/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/14/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/15/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/16/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/17/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/18/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/19/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/20/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/21/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/22/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/23/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_360p_m4s/24/432f65a0.m4s"/>
|
||||||
|
</SegmentList>
|
||||||
|
</Representation>
|
||||||
|
<Representation audioSamplingRate="44100" bandwidth="1200000" codecs="avc3.42c01e,mp4a.40.2" frameRate="25" height="480" id="h264_aac_480p_m4s" mimeType="video/mp4" sar="320:321" startWithSAP="1" width="856">
|
||||||
|
<SegmentList duration="10000" timescale="1000">
|
||||||
|
<Initialization sourceURL="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/init/432f65a0.mp4"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/0/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/1/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/2/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/3/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/4/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/5/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/6/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/7/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/8/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/9/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/10/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/11/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/12/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/13/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/14/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/15/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/16/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/17/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/18/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/19/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/20/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/21/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/22/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/23/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_480p_m4s/24/432f65a0.m4s"/>
|
||||||
|
</SegmentList>
|
||||||
|
</Representation>
|
||||||
|
<Representation audioSamplingRate="44100" bandwidth="1600000" codecs="avc3.42c01e,mp4a.40.2" frameRate="25" height="576" id="h264_aac_576p_m4s" mimeType="video/mp4" sar="1:1" startWithSAP="1" width="1024">
|
||||||
|
<SegmentList duration="10000" timescale="1000">
|
||||||
|
<Initialization sourceURL="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/init/432f65a0.mp4"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/0/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/1/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/2/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/3/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/4/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/5/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/6/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/7/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/8/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/9/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/10/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/11/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/12/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/13/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/14/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/15/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/16/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/17/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/18/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/19/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/20/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/21/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/22/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/23/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_576p_m4s/24/432f65a0.m4s"/>
|
||||||
|
</SegmentList>
|
||||||
|
</Representation>
|
||||||
|
<Representation audioSamplingRate="44100" bandwidth="2400000" codecs="avc3.42c01e,mp4a.40.2" frameRate="25" height="720" id="h264_aac_720p_m4s" mimeType="video/mp4" sar="1:1" startWithSAP="1" width="1280">
|
||||||
|
<SegmentList duration="10000" timescale="1000">
|
||||||
|
<Initialization sourceURL="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/init/432f65a0.mp4"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/0/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/1/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/2/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/3/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/4/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/5/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/6/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/7/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/8/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/9/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/10/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/11/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/12/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/13/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/14/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/15/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/16/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/17/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/18/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/19/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/20/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/21/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/22/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/23/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_720p_m4s/24/432f65a0.m4s"/>
|
||||||
|
</SegmentList>
|
||||||
|
</Representation>
|
||||||
|
<Representation audioSamplingRate="44100" bandwidth="4400000" codecs="avc3.42c01e,mp4a.40.2" frameRate="25" height="1080" id="h264_aac_1080p_m4s" mimeType="video/mp4" sar="1:1" startWithSAP="1" width="1920">
|
||||||
|
<SegmentList duration="10000" timescale="1000">
|
||||||
|
<Initialization sourceURL="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/init/432f65a0.mp4"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/0/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/1/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/2/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/3/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/4/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/5/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/6/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/7/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/8/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/9/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/10/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/11/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/12/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/13/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/14/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/15/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/16/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/17/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/18/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/19/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/20/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/21/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/22/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/23/432f65a0.m4s"/>
|
||||||
|
<SegmentURL media="../vd_5999c902ea707c67d8e267a9_1503250723/h264_aac_1080p_m4s/24/432f65a0.m4s"/>
|
||||||
|
</SegmentList>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
34
test/testdata/xspf/foo_xspf.xspf
vendored
Normal file
34
test/testdata/xspf/foo_xspf.xspf
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<playlist version="1" xmlns="http://xspf.org/ns/0/">
|
||||||
|
<date>2018-03-09T18:01:43Z</date>
|
||||||
|
<trackList>
|
||||||
|
<track>
|
||||||
|
<location>cd1/track%201.mp3</location>
|
||||||
|
<title>Pandemonium</title>
|
||||||
|
<creator>Foilverb</creator>
|
||||||
|
<annotation>Visit http://bigbrother404.bandcamp.com</annotation>
|
||||||
|
<album>Pandemonium EP</album>
|
||||||
|
<trackNum>1</trackNum>
|
||||||
|
<duration>202416</duration>
|
||||||
|
</track>
|
||||||
|
<track>
|
||||||
|
<location>../%E3%83%88%E3%83%A9%E3%83%83%E3%82%AF%E3%80%80%EF%BC%92.mp3</location>
|
||||||
|
<title>Final Cartridge (Nichico Twelve Remix)</title>
|
||||||
|
<annotation>Visit http://bigbrother404.bandcamp.com</annotation>
|
||||||
|
<creator>Foilverb</creator>
|
||||||
|
<album>Pandemonium EP</album>
|
||||||
|
<trackNum>2</trackNum>
|
||||||
|
<duration>255857</duration>
|
||||||
|
</track>
|
||||||
|
<track>
|
||||||
|
<location>track3.mp3</location>
|
||||||
|
<location>https://example.com/track3.mp3</location>
|
||||||
|
<title>Rebuilding Nightingale</title>
|
||||||
|
<annotation>Visit http://bigbrother404.bandcamp.com</annotation>
|
||||||
|
<creator>Foilverb</creator>
|
||||||
|
<album>Pandemonium EP</album>
|
||||||
|
<trackNum>3</trackNum>
|
||||||
|
<duration>287915</duration>
|
||||||
|
</track>
|
||||||
|
</trackList>
|
||||||
|
</playlist>
|
@ -211,7 +211,7 @@ class YoutubeDL(object):
|
|||||||
At the moment, this is only supported by YouTube.
|
At the moment, this is only supported by YouTube.
|
||||||
proxy: URL of the proxy server to use
|
proxy: URL of the proxy server to use
|
||||||
geo_verification_proxy: URL of the proxy to use for IP address verification
|
geo_verification_proxy: URL of the proxy to use for IP address verification
|
||||||
on geo-restricted sites. (Experimental)
|
on geo-restricted sites.
|
||||||
socket_timeout: Time to wait for unresponsive hosts, in seconds
|
socket_timeout: Time to wait for unresponsive hosts, in seconds
|
||||||
bidi_workaround: Work around buggy terminals without bidirectional text
|
bidi_workaround: Work around buggy terminals without bidirectional text
|
||||||
support, using fridibi
|
support, using fridibi
|
||||||
@ -259,7 +259,7 @@ class YoutubeDL(object):
|
|||||||
- "warn": only emit a warning
|
- "warn": only emit a warning
|
||||||
- "detect_or_warn": check whether we can do anything
|
- "detect_or_warn": check whether we can do anything
|
||||||
about it, warn otherwise (default)
|
about it, warn otherwise (default)
|
||||||
source_address: (Experimental) Client-side IP address to bind to.
|
source_address: Client-side IP address to bind to.
|
||||||
call_home: Boolean, true iff we are allowed to contact the
|
call_home: Boolean, true iff we are allowed to contact the
|
||||||
youtube-dl servers for debugging.
|
youtube-dl servers for debugging.
|
||||||
sleep_interval: Number of seconds to sleep before each download when
|
sleep_interval: Number of seconds to sleep before each download when
|
||||||
@ -281,11 +281,14 @@ class YoutubeDL(object):
|
|||||||
match_filter_func in utils.py is one example for this.
|
match_filter_func in utils.py is one example for this.
|
||||||
no_color: Do not emit color codes in output.
|
no_color: Do not emit color codes in output.
|
||||||
geo_bypass: Bypass geographic restriction via faking X-Forwarded-For
|
geo_bypass: Bypass geographic restriction via faking X-Forwarded-For
|
||||||
HTTP header (experimental)
|
HTTP header
|
||||||
geo_bypass_country:
|
geo_bypass_country:
|
||||||
Two-letter ISO 3166-2 country code that will be used for
|
Two-letter ISO 3166-2 country code that will be used for
|
||||||
explicit geographic restriction bypassing via faking
|
explicit geographic restriction bypassing via faking
|
||||||
X-Forwarded-For HTTP header (experimental)
|
X-Forwarded-For HTTP header
|
||||||
|
geo_bypass_ip_block:
|
||||||
|
IP range in CIDR notation that will be used similarly to
|
||||||
|
geo_bypass_country
|
||||||
|
|
||||||
The following options determine which downloader is picked:
|
The following options determine which downloader is picked:
|
||||||
external_downloader: Executable of the external downloader to call.
|
external_downloader: Executable of the external downloader to call.
|
||||||
@ -298,11 +301,12 @@ class YoutubeDL(object):
|
|||||||
the downloader (see youtube_dl/downloader/common.py):
|
the downloader (see youtube_dl/downloader/common.py):
|
||||||
nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
|
nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
|
||||||
noresizebuffer, retries, continuedl, noprogress, consoletitle,
|
noresizebuffer, retries, continuedl, noprogress, consoletitle,
|
||||||
xattr_set_filesize, external_downloader_args, hls_use_mpegts.
|
xattr_set_filesize, external_downloader_args, hls_use_mpegts,
|
||||||
|
http_chunk_size.
|
||||||
|
|
||||||
The following options are used by the post processors:
|
The following options are used by the post processors:
|
||||||
prefer_ffmpeg: If True, use ffmpeg instead of avconv if both are available,
|
prefer_ffmpeg: If False, use avconv instead of ffmpeg if both are available,
|
||||||
otherwise prefer avconv.
|
otherwise prefer ffmpeg.
|
||||||
postprocessor_args: A list of additional command-line arguments for the
|
postprocessor_args: A list of additional command-line arguments for the
|
||||||
postprocessor.
|
postprocessor.
|
||||||
|
|
||||||
@ -531,6 +535,8 @@ class YoutubeDL(object):
|
|||||||
def save_console_title(self):
|
def save_console_title(self):
|
||||||
if not self.params.get('consoletitle', False):
|
if not self.params.get('consoletitle', False):
|
||||||
return
|
return
|
||||||
|
if self.params.get('simulate', False):
|
||||||
|
return
|
||||||
if compat_os_name != 'nt' and 'TERM' in os.environ:
|
if compat_os_name != 'nt' and 'TERM' in os.environ:
|
||||||
# Save the title on stack
|
# Save the title on stack
|
||||||
self._write_string('\033[22;0t', self._screen_file)
|
self._write_string('\033[22;0t', self._screen_file)
|
||||||
@ -538,6 +544,8 @@ class YoutubeDL(object):
|
|||||||
def restore_console_title(self):
|
def restore_console_title(self):
|
||||||
if not self.params.get('consoletitle', False):
|
if not self.params.get('consoletitle', False):
|
||||||
return
|
return
|
||||||
|
if self.params.get('simulate', False):
|
||||||
|
return
|
||||||
if compat_os_name != 'nt' and 'TERM' in os.environ:
|
if compat_os_name != 'nt' and 'TERM' in os.environ:
|
||||||
# Restore the title from stack
|
# Restore the title from stack
|
||||||
self._write_string('\033[23;0t', self._screen_file)
|
self._write_string('\033[23;0t', self._screen_file)
|
||||||
@ -975,6 +983,8 @@ class YoutubeDL(object):
|
|||||||
'playlist': playlist,
|
'playlist': playlist,
|
||||||
'playlist_id': ie_result.get('id'),
|
'playlist_id': ie_result.get('id'),
|
||||||
'playlist_title': ie_result.get('title'),
|
'playlist_title': ie_result.get('title'),
|
||||||
|
'playlist_uploader': ie_result.get('uploader'),
|
||||||
|
'playlist_uploader_id': ie_result.get('uploader_id'),
|
||||||
'playlist_index': i + playliststart,
|
'playlist_index': i + playliststart,
|
||||||
'extractor': ie_result['extractor'],
|
'extractor': ie_result['extractor'],
|
||||||
'webpage_url': ie_result['webpage_url'],
|
'webpage_url': ie_result['webpage_url'],
|
||||||
@ -1030,7 +1040,7 @@ class YoutubeDL(object):
|
|||||||
'!=': operator.ne,
|
'!=': operator.ne,
|
||||||
}
|
}
|
||||||
operator_rex = re.compile(r'''(?x)\s*
|
operator_rex = re.compile(r'''(?x)\s*
|
||||||
(?P<key>width|height|tbr|abr|vbr|asr|filesize|fps)
|
(?P<key>width|height|tbr|abr|vbr|asr|filesize|filesize_approx|fps)
|
||||||
\s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
|
\s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
|
||||||
(?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)
|
(?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)
|
||||||
$
|
$
|
||||||
@ -1472,23 +1482,28 @@ class YoutubeDL(object):
|
|||||||
if info_dict.get('%s_number' % field) is not None and not info_dict.get(field):
|
if info_dict.get('%s_number' % field) is not None and not info_dict.get(field):
|
||||||
info_dict[field] = '%s %d' % (field.capitalize(), info_dict['%s_number' % field])
|
info_dict[field] = '%s %d' % (field.capitalize(), info_dict['%s_number' % field])
|
||||||
|
|
||||||
subtitles = info_dict.get('subtitles')
|
for cc_kind in ('subtitles', 'automatic_captions'):
|
||||||
if subtitles:
|
cc = info_dict.get(cc_kind)
|
||||||
for _, subtitle in subtitles.items():
|
if cc:
|
||||||
|
for _, subtitle in cc.items():
|
||||||
for subtitle_format in subtitle:
|
for subtitle_format in subtitle:
|
||||||
if subtitle_format.get('url'):
|
if subtitle_format.get('url'):
|
||||||
subtitle_format['url'] = sanitize_url(subtitle_format['url'])
|
subtitle_format['url'] = sanitize_url(subtitle_format['url'])
|
||||||
if subtitle_format.get('ext') is None:
|
if subtitle_format.get('ext') is None:
|
||||||
subtitle_format['ext'] = determine_ext(subtitle_format['url']).lower()
|
subtitle_format['ext'] = determine_ext(subtitle_format['url']).lower()
|
||||||
|
|
||||||
|
automatic_captions = info_dict.get('automatic_captions')
|
||||||
|
subtitles = info_dict.get('subtitles')
|
||||||
|
|
||||||
if self.params.get('listsubtitles', False):
|
if self.params.get('listsubtitles', False):
|
||||||
if 'automatic_captions' in info_dict:
|
if 'automatic_captions' in info_dict:
|
||||||
self.list_subtitles(info_dict['id'], info_dict.get('automatic_captions'), 'automatic captions')
|
self.list_subtitles(
|
||||||
|
info_dict['id'], automatic_captions, 'automatic captions')
|
||||||
self.list_subtitles(info_dict['id'], subtitles, 'subtitles')
|
self.list_subtitles(info_dict['id'], subtitles, 'subtitles')
|
||||||
return
|
return
|
||||||
|
|
||||||
info_dict['requested_subtitles'] = self.process_subtitles(
|
info_dict['requested_subtitles'] = self.process_subtitles(
|
||||||
info_dict['id'], subtitles,
|
info_dict['id'], subtitles, automatic_captions)
|
||||||
info_dict.get('automatic_captions'))
|
|
||||||
|
|
||||||
# We now pick which formats have to be downloaded
|
# We now pick which formats have to be downloaded
|
||||||
if info_dict.get('formats') is None:
|
if info_dict.get('formats') is None:
|
||||||
@ -1846,7 +1861,7 @@ class YoutubeDL(object):
|
|||||||
def compatible_formats(formats):
|
def compatible_formats(formats):
|
||||||
video, audio = formats
|
video, audio = formats
|
||||||
# Check extension
|
# Check extension
|
||||||
video_ext, audio_ext = audio.get('ext'), video.get('ext')
|
video_ext, audio_ext = video.get('ext'), audio.get('ext')
|
||||||
if video_ext and audio_ext:
|
if video_ext and audio_ext:
|
||||||
COMPATIBLE_EXTS = (
|
COMPATIBLE_EXTS = (
|
||||||
('mp3', 'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'ismv', 'isma'),
|
('mp3', 'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'ismv', 'isma'),
|
||||||
@ -2231,8 +2246,16 @@ class YoutubeDL(object):
|
|||||||
sys.exc_clear()
|
sys.exc_clear()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self._write_string('[debug] Python version %s - %s\n' % (
|
|
||||||
platform.python_version(), platform_name()))
|
def python_implementation():
|
||||||
|
impl_name = platform.python_implementation()
|
||||||
|
if impl_name == 'PyPy' and hasattr(sys, 'pypy_version_info'):
|
||||||
|
return impl_name + ' version %d.%d.%d' % sys.pypy_version_info[:3]
|
||||||
|
return impl_name
|
||||||
|
|
||||||
|
self._write_string('[debug] Python version %s (%s) - %s\n' % (
|
||||||
|
platform.python_version(), python_implementation(),
|
||||||
|
platform_name()))
|
||||||
|
|
||||||
exe_versions = FFmpegPostProcessor.get_versions(self)
|
exe_versions = FFmpegPostProcessor.get_versions(self)
|
||||||
exe_versions['rtmpdump'] = rtmpdump_version()
|
exe_versions['rtmpdump'] = rtmpdump_version()
|
||||||
|
@ -191,6 +191,11 @@ def _real_main(argv=None):
|
|||||||
if numeric_buffersize is None:
|
if numeric_buffersize is None:
|
||||||
parser.error('invalid buffer size specified')
|
parser.error('invalid buffer size specified')
|
||||||
opts.buffersize = numeric_buffersize
|
opts.buffersize = numeric_buffersize
|
||||||
|
if opts.http_chunk_size is not None:
|
||||||
|
numeric_chunksize = FileDownloader.parse_bytes(opts.http_chunk_size)
|
||||||
|
if not numeric_chunksize:
|
||||||
|
parser.error('invalid http chunk size specified')
|
||||||
|
opts.http_chunk_size = numeric_chunksize
|
||||||
if opts.playliststart <= 0:
|
if opts.playliststart <= 0:
|
||||||
raise ValueError('Playlist start must be positive')
|
raise ValueError('Playlist start must be positive')
|
||||||
if opts.playlistend not in (-1, None) and opts.playlistend < opts.playliststart:
|
if opts.playlistend not in (-1, None) and opts.playlistend < opts.playliststart:
|
||||||
@ -346,6 +351,7 @@ def _real_main(argv=None):
|
|||||||
'keep_fragments': opts.keep_fragments,
|
'keep_fragments': opts.keep_fragments,
|
||||||
'buffersize': opts.buffersize,
|
'buffersize': opts.buffersize,
|
||||||
'noresizebuffer': opts.noresizebuffer,
|
'noresizebuffer': opts.noresizebuffer,
|
||||||
|
'http_chunk_size': opts.http_chunk_size,
|
||||||
'continuedl': opts.continue_dl,
|
'continuedl': opts.continue_dl,
|
||||||
'noprogress': opts.noprogress,
|
'noprogress': opts.noprogress,
|
||||||
'progress_with_newline': opts.progress_with_newline,
|
'progress_with_newline': opts.progress_with_newline,
|
||||||
@ -424,6 +430,7 @@ def _real_main(argv=None):
|
|||||||
'config_location': opts.config_location,
|
'config_location': opts.config_location,
|
||||||
'geo_bypass': opts.geo_bypass,
|
'geo_bypass': opts.geo_bypass,
|
||||||
'geo_bypass_country': opts.geo_bypass_country,
|
'geo_bypass_country': opts.geo_bypass_country,
|
||||||
|
'geo_bypass_ip_block': opts.geo_bypass_ip_block,
|
||||||
# just for deprecation check
|
# just for deprecation check
|
||||||
'autonumber': opts.autonumber if opts.autonumber is True else None,
|
'autonumber': opts.autonumber if opts.autonumber is True else None,
|
||||||
'usetitle': opts.usetitle if opts.usetitle is True else None,
|
'usetitle': opts.usetitle if opts.usetitle is True else None,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
|
from .compat import compat_b64decode
|
||||||
from .utils import bytes_to_intlist, intlist_to_bytes
|
from .utils import bytes_to_intlist, intlist_to_bytes
|
||||||
|
|
||||||
BLOCK_SIZE_BYTES = 16
|
BLOCK_SIZE_BYTES = 16
|
||||||
@ -180,7 +180,7 @@ def aes_decrypt_text(data, password, key_size_bytes):
|
|||||||
"""
|
"""
|
||||||
NONCE_LENGTH_BYTES = 8
|
NONCE_LENGTH_BYTES = 8
|
||||||
|
|
||||||
data = bytes_to_intlist(base64.b64decode(data.encode('utf-8')))
|
data = bytes_to_intlist(compat_b64decode(data))
|
||||||
password = bytes_to_intlist(password.encode('utf-8'))
|
password = bytes_to_intlist(password.encode('utf-8'))
|
||||||
|
|
||||||
key = password[:key_size_bytes] + [0] * (key_size_bytes - len(password))
|
key = password[:key_size_bytes] + [0] * (key_size_bytes - len(password))
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import collections
|
import collections
|
||||||
|
import ctypes
|
||||||
import email
|
import email
|
||||||
import getpass
|
import getpass
|
||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
@ -2784,6 +2787,12 @@ except NameError: # Python 3
|
|||||||
compat_numeric_types = (int, float, complex)
|
compat_numeric_types = (int, float, complex)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
compat_integer_types = (int, long)
|
||||||
|
except NameError: # Python 3
|
||||||
|
compat_integer_types = (int, )
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (2, 7):
|
if sys.version_info < (2, 7):
|
||||||
def compat_socket_create_connection(address, timeout, source_address=None):
|
def compat_socket_create_connection(address, timeout, source_address=None):
|
||||||
host, port = address
|
host, port = address
|
||||||
@ -2894,9 +2903,24 @@ except TypeError:
|
|||||||
if isinstance(spec, compat_str):
|
if isinstance(spec, compat_str):
|
||||||
spec = spec.encode('ascii')
|
spec = spec.encode('ascii')
|
||||||
return struct.unpack(spec, *args)
|
return struct.unpack(spec, *args)
|
||||||
|
|
||||||
|
class compat_Struct(struct.Struct):
|
||||||
|
def __init__(self, fmt):
|
||||||
|
if isinstance(fmt, compat_str):
|
||||||
|
fmt = fmt.encode('ascii')
|
||||||
|
super(compat_Struct, self).__init__(fmt)
|
||||||
else:
|
else:
|
||||||
compat_struct_pack = struct.pack
|
compat_struct_pack = struct.pack
|
||||||
compat_struct_unpack = struct.unpack
|
compat_struct_unpack = struct.unpack
|
||||||
|
if platform.python_implementation() == 'IronPython' and sys.version_info < (2, 7, 8):
|
||||||
|
class compat_Struct(struct.Struct):
|
||||||
|
def unpack(self, string):
|
||||||
|
if not isinstance(string, buffer): # noqa: F821
|
||||||
|
string = buffer(string) # noqa: F821
|
||||||
|
return super(compat_Struct, self).unpack(string)
|
||||||
|
else:
|
||||||
|
compat_Struct = struct.Struct
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from future_builtins import zip as compat_zip
|
from future_builtins import zip as compat_zip
|
||||||
@ -2906,14 +2930,45 @@ except ImportError: # not 2.6+ or is 3.x
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
compat_zip = zip
|
compat_zip = zip
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info < (3, 3):
|
||||||
|
def compat_b64decode(s, *args, **kwargs):
|
||||||
|
if isinstance(s, compat_str):
|
||||||
|
s = s.encode('ascii')
|
||||||
|
return base64.b64decode(s, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
compat_b64decode = base64.b64decode
|
||||||
|
|
||||||
|
|
||||||
|
if platform.python_implementation() == 'PyPy' and sys.pypy_version_info < (5, 4, 0):
|
||||||
|
# PyPy2 prior to version 5.4.0 expects byte strings as Windows function
|
||||||
|
# names, see the original PyPy issue [1] and the youtube-dl one [2].
|
||||||
|
# 1. https://bitbucket.org/pypy/pypy/issues/2360/windows-ctypescdll-typeerror-function-name
|
||||||
|
# 2. https://github.com/rg3/youtube-dl/pull/4392
|
||||||
|
def compat_ctypes_WINFUNCTYPE(*args, **kwargs):
|
||||||
|
real = ctypes.WINFUNCTYPE(*args, **kwargs)
|
||||||
|
|
||||||
|
def resf(tpl, *args, **kwargs):
|
||||||
|
funcname, dll = tpl
|
||||||
|
return real((str(funcname), dll), *args, **kwargs)
|
||||||
|
|
||||||
|
return resf
|
||||||
|
else:
|
||||||
|
def compat_ctypes_WINFUNCTYPE(*args, **kwargs):
|
||||||
|
return ctypes.WINFUNCTYPE(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'compat_HTMLParseError',
|
'compat_HTMLParseError',
|
||||||
'compat_HTMLParser',
|
'compat_HTMLParser',
|
||||||
'compat_HTTPError',
|
'compat_HTTPError',
|
||||||
|
'compat_Struct',
|
||||||
|
'compat_b64decode',
|
||||||
'compat_basestring',
|
'compat_basestring',
|
||||||
'compat_chr',
|
'compat_chr',
|
||||||
'compat_cookiejar',
|
'compat_cookiejar',
|
||||||
'compat_cookies',
|
'compat_cookies',
|
||||||
|
'compat_ctypes_WINFUNCTYPE',
|
||||||
'compat_etree_fromstring',
|
'compat_etree_fromstring',
|
||||||
'compat_etree_register_namespace',
|
'compat_etree_register_namespace',
|
||||||
'compat_expanduser',
|
'compat_expanduser',
|
||||||
@ -2925,6 +2980,7 @@ __all__ = [
|
|||||||
'compat_http_client',
|
'compat_http_client',
|
||||||
'compat_http_server',
|
'compat_http_server',
|
||||||
'compat_input',
|
'compat_input',
|
||||||
|
'compat_integer_types',
|
||||||
'compat_itertools_count',
|
'compat_itertools_count',
|
||||||
'compat_kwargs',
|
'compat_kwargs',
|
||||||
'compat_numeric_types',
|
'compat_numeric_types',
|
||||||
|
@ -45,10 +45,12 @@ class FileDownloader(object):
|
|||||||
min_filesize: Skip files smaller than this size
|
min_filesize: Skip files smaller than this size
|
||||||
max_filesize: Skip files larger than this size
|
max_filesize: Skip files larger than this size
|
||||||
xattr_set_filesize: Set ytdl.filesize user xattribute with expected size.
|
xattr_set_filesize: Set ytdl.filesize user xattribute with expected size.
|
||||||
(experimental)
|
|
||||||
external_downloader_args: A list of additional command-line arguments for the
|
external_downloader_args: A list of additional command-line arguments for the
|
||||||
external downloader.
|
external downloader.
|
||||||
hls_use_mpegts: Use the mpegts container for HLS videos.
|
hls_use_mpegts: Use the mpegts container for HLS videos.
|
||||||
|
http_chunk_size: Size of a chunk for chunk-based HTTP downloading. May be
|
||||||
|
useful for bypassing bandwidth throttling imposed by
|
||||||
|
a webserver (experimental)
|
||||||
|
|
||||||
Subclasses of this one must re-define the real_download method.
|
Subclasses of this one must re-define the real_download method.
|
||||||
"""
|
"""
|
||||||
@ -246,12 +248,13 @@ class FileDownloader(object):
|
|||||||
if self.params.get('noprogress', False):
|
if self.params.get('noprogress', False):
|
||||||
self.to_screen('[download] Download completed')
|
self.to_screen('[download] Download completed')
|
||||||
else:
|
else:
|
||||||
|
msg_template = '100%%'
|
||||||
|
if s.get('total_bytes') is not None:
|
||||||
s['_total_bytes_str'] = format_bytes(s['total_bytes'])
|
s['_total_bytes_str'] = format_bytes(s['total_bytes'])
|
||||||
|
msg_template += ' of %(_total_bytes_str)s'
|
||||||
if s.get('elapsed') is not None:
|
if s.get('elapsed') is not None:
|
||||||
s['_elapsed_str'] = self.format_seconds(s['elapsed'])
|
s['_elapsed_str'] = self.format_seconds(s['elapsed'])
|
||||||
msg_template = '100%% of %(_total_bytes_str)s in %(_elapsed_str)s'
|
msg_template += ' in %(_elapsed_str)s'
|
||||||
else:
|
|
||||||
msg_template = '100%% of %(_total_bytes_str)s'
|
|
||||||
self._report_progress_status(
|
self._report_progress_status(
|
||||||
msg_template % s, is_last_line=True)
|
msg_template % s, is_last_line=True)
|
||||||
|
|
||||||
|
@ -2,7 +2,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
from ..compat import compat_urllib_error
|
from ..compat import compat_urllib_error
|
||||||
from ..utils import urljoin
|
from ..utils import (
|
||||||
|
DownloadError,
|
||||||
|
urljoin,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DashSegmentsFD(FragmentFD):
|
class DashSegmentsFD(FragmentFD):
|
||||||
@ -57,6 +60,14 @@ class DashSegmentsFD(FragmentFD):
|
|||||||
count += 1
|
count += 1
|
||||||
if count <= fragment_retries:
|
if count <= fragment_retries:
|
||||||
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
||||||
|
except DownloadError:
|
||||||
|
# Don't retry fragment if error occurred during HTTP downloading
|
||||||
|
# itself since it has own retry settings
|
||||||
|
if not fatal:
|
||||||
|
self.report_skip_fragment(frag_index)
|
||||||
|
break
|
||||||
|
raise
|
||||||
|
|
||||||
if count > fragment_retries:
|
if count > fragment_retries:
|
||||||
if not fatal:
|
if not fatal:
|
||||||
self.report_skip_fragment(frag_index)
|
self.report_skip_fragment(frag_index)
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import re
|
import time
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
@ -30,6 +31,7 @@ class ExternalFD(FileDownloader):
|
|||||||
tmpfilename = self.temp_name(filename)
|
tmpfilename = self.temp_name(filename)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
started = time.time()
|
||||||
retval = self._call_downloader(tmpfilename, info_dict)
|
retval = self._call_downloader(tmpfilename, info_dict)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
if not info_dict.get('is_live'):
|
if not info_dict.get('is_live'):
|
||||||
@ -41,15 +43,20 @@ class ExternalFD(FileDownloader):
|
|||||||
self.to_screen('[%s] Interrupted by user' % self.get_basename())
|
self.to_screen('[%s] Interrupted by user' % self.get_basename())
|
||||||
|
|
||||||
if retval == 0:
|
if retval == 0:
|
||||||
|
status = {
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'finished',
|
||||||
|
'elapsed': time.time() - started,
|
||||||
|
}
|
||||||
|
if filename != '-':
|
||||||
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
||||||
self.to_screen('\r[%s] Downloaded %s bytes' % (self.get_basename(), fsize))
|
self.to_screen('\r[%s] Downloaded %s bytes' % (self.get_basename(), fsize))
|
||||||
self.try_rename(tmpfilename, filename)
|
self.try_rename(tmpfilename, filename)
|
||||||
self._hook_progress({
|
status.update({
|
||||||
'downloaded_bytes': fsize,
|
'downloaded_bytes': fsize,
|
||||||
'total_bytes': fsize,
|
'total_bytes': fsize,
|
||||||
'filename': filename,
|
|
||||||
'status': 'finished',
|
|
||||||
})
|
})
|
||||||
|
self._hook_progress(status)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.to_stderr('\n')
|
self.to_stderr('\n')
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from __future__ import division, unicode_literals
|
from __future__ import division, unicode_literals
|
||||||
|
|
||||||
import base64
|
|
||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
|
compat_b64decode,
|
||||||
compat_etree_fromstring,
|
compat_etree_fromstring,
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
compat_urllib_error,
|
compat_urllib_error,
|
||||||
@ -312,7 +312,7 @@ class F4mFD(FragmentFD):
|
|||||||
boot_info = self._get_bootstrap_from_url(bootstrap_url)
|
boot_info = self._get_bootstrap_from_url(bootstrap_url)
|
||||||
else:
|
else:
|
||||||
bootstrap_url = None
|
bootstrap_url = None
|
||||||
bootstrap = base64.b64decode(node.text.encode('ascii'))
|
bootstrap = compat_b64decode(node.text)
|
||||||
boot_info = read_bootstrap_info(bootstrap)
|
boot_info = read_bootstrap_info(bootstrap)
|
||||||
return boot_info, bootstrap_url
|
return boot_info, bootstrap_url
|
||||||
|
|
||||||
@ -349,7 +349,7 @@ class F4mFD(FragmentFD):
|
|||||||
live = boot_info['live']
|
live = boot_info['live']
|
||||||
metadata_node = media.find(_add_ns('metadata'))
|
metadata_node = media.find(_add_ns('metadata'))
|
||||||
if metadata_node is not None:
|
if metadata_node is not None:
|
||||||
metadata = base64.b64decode(metadata_node.text.encode('ascii'))
|
metadata = compat_b64decode(metadata_node.text)
|
||||||
else:
|
else:
|
||||||
metadata = None
|
metadata = None
|
||||||
|
|
||||||
|
@ -74,8 +74,13 @@ class FragmentFD(FileDownloader):
|
|||||||
return not ctx['live'] and not ctx['tmpfilename'] == '-'
|
return not ctx['live'] and not ctx['tmpfilename'] == '-'
|
||||||
|
|
||||||
def _read_ytdl_file(self, ctx):
|
def _read_ytdl_file(self, ctx):
|
||||||
|
assert 'ytdl_corrupt' not in ctx
|
||||||
stream, _ = sanitize_open(self.ytdl_filename(ctx['filename']), 'r')
|
stream, _ = sanitize_open(self.ytdl_filename(ctx['filename']), 'r')
|
||||||
|
try:
|
||||||
ctx['fragment_index'] = json.loads(stream.read())['downloader']['current_fragment']['index']
|
ctx['fragment_index'] = json.loads(stream.read())['downloader']['current_fragment']['index']
|
||||||
|
except Exception:
|
||||||
|
ctx['ytdl_corrupt'] = True
|
||||||
|
finally:
|
||||||
stream.close()
|
stream.close()
|
||||||
|
|
||||||
def _write_ytdl_file(self, ctx):
|
def _write_ytdl_file(self, ctx):
|
||||||
@ -107,11 +112,12 @@ class FragmentFD(FileDownloader):
|
|||||||
def _append_fragment(self, ctx, frag_content):
|
def _append_fragment(self, ctx, frag_content):
|
||||||
try:
|
try:
|
||||||
ctx['dest_stream'].write(frag_content)
|
ctx['dest_stream'].write(frag_content)
|
||||||
|
ctx['dest_stream'].flush()
|
||||||
finally:
|
finally:
|
||||||
if self.__do_ytdl_file(ctx):
|
if self.__do_ytdl_file(ctx):
|
||||||
self._write_ytdl_file(ctx)
|
self._write_ytdl_file(ctx)
|
||||||
if not self.params.get('keep_fragments', False):
|
if not self.params.get('keep_fragments', False):
|
||||||
os.remove(ctx['fragment_filename_sanitized'])
|
os.remove(encodeFilename(ctx['fragment_filename_sanitized']))
|
||||||
del ctx['fragment_filename_sanitized']
|
del ctx['fragment_filename_sanitized']
|
||||||
|
|
||||||
def _prepare_frag_download(self, ctx):
|
def _prepare_frag_download(self, ctx):
|
||||||
@ -157,11 +163,17 @@ class FragmentFD(FileDownloader):
|
|||||||
if self.__do_ytdl_file(ctx):
|
if self.__do_ytdl_file(ctx):
|
||||||
if os.path.isfile(encodeFilename(self.ytdl_filename(ctx['filename']))):
|
if os.path.isfile(encodeFilename(self.ytdl_filename(ctx['filename']))):
|
||||||
self._read_ytdl_file(ctx)
|
self._read_ytdl_file(ctx)
|
||||||
if ctx['fragment_index'] > 0 and resume_len == 0:
|
is_corrupt = ctx.get('ytdl_corrupt') is True
|
||||||
|
is_inconsistent = ctx['fragment_index'] > 0 and resume_len == 0
|
||||||
|
if is_corrupt or is_inconsistent:
|
||||||
|
message = (
|
||||||
|
'.ytdl file is corrupt' if is_corrupt else
|
||||||
|
'Inconsistent state of incomplete fragment download')
|
||||||
self.report_warning(
|
self.report_warning(
|
||||||
'Inconsistent state of incomplete fragment download. '
|
'%s. Restarting from the beginning...' % message)
|
||||||
'Restarting from the beginning...')
|
|
||||||
ctx['fragment_index'] = resume_len = 0
|
ctx['fragment_index'] = resume_len = 0
|
||||||
|
if 'ytdl_corrupt' in ctx:
|
||||||
|
del ctx['ytdl_corrupt']
|
||||||
self._write_ytdl_file(ctx)
|
self._write_ytdl_file(ctx)
|
||||||
else:
|
else:
|
||||||
self._write_ytdl_file(ctx)
|
self._write_ytdl_file(ctx)
|
||||||
@ -240,12 +252,16 @@ class FragmentFD(FileDownloader):
|
|||||||
if os.path.isfile(ytdl_filename):
|
if os.path.isfile(ytdl_filename):
|
||||||
os.remove(ytdl_filename)
|
os.remove(ytdl_filename)
|
||||||
elapsed = time.time() - ctx['started']
|
elapsed = time.time() - ctx['started']
|
||||||
|
|
||||||
|
if ctx['tmpfilename'] == '-':
|
||||||
|
downloaded_bytes = ctx['complete_frags_downloaded_bytes']
|
||||||
|
else:
|
||||||
self.try_rename(ctx['tmpfilename'], ctx['filename'])
|
self.try_rename(ctx['tmpfilename'], ctx['filename'])
|
||||||
fsize = os.path.getsize(encodeFilename(ctx['filename']))
|
downloaded_bytes = os.path.getsize(encodeFilename(ctx['filename']))
|
||||||
|
|
||||||
self._hook_progress({
|
self._hook_progress({
|
||||||
'downloaded_bytes': fsize,
|
'downloaded_bytes': downloaded_bytes,
|
||||||
'total_bytes': fsize,
|
'total_bytes': downloaded_bytes,
|
||||||
'filename': ctx['filename'],
|
'filename': ctx['filename'],
|
||||||
'status': 'finished',
|
'status': 'finished',
|
||||||
'elapsed': elapsed,
|
'elapsed': elapsed,
|
||||||
|
@ -75,8 +75,9 @@ class HlsFD(FragmentFD):
|
|||||||
fd.add_progress_hook(ph)
|
fd.add_progress_hook(ph)
|
||||||
return fd.real_download(filename, info_dict)
|
return fd.real_download(filename, info_dict)
|
||||||
|
|
||||||
def anvato_ad(s):
|
def is_ad_fragment(s):
|
||||||
return s.startswith('#ANVATO-SEGMENT-INFO') and 'type=ad' in s
|
return (s.startswith('#ANVATO-SEGMENT-INFO') and 'type=ad' in s or
|
||||||
|
s.startswith('#UPLYNK-SEGMENT') and s.endswith(',ad'))
|
||||||
|
|
||||||
media_frags = 0
|
media_frags = 0
|
||||||
ad_frags = 0
|
ad_frags = 0
|
||||||
@ -86,7 +87,7 @@ class HlsFD(FragmentFD):
|
|||||||
if not line:
|
if not line:
|
||||||
continue
|
continue
|
||||||
if line.startswith('#'):
|
if line.startswith('#'):
|
||||||
if anvato_ad(line):
|
if is_ad_fragment(line):
|
||||||
ad_frags += 1
|
ad_frags += 1
|
||||||
ad_frag_next = True
|
ad_frag_next = True
|
||||||
continue
|
continue
|
||||||
@ -163,7 +164,8 @@ class HlsFD(FragmentFD):
|
|||||||
return False
|
return False
|
||||||
if decrypt_info['METHOD'] == 'AES-128':
|
if decrypt_info['METHOD'] == 'AES-128':
|
||||||
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', media_sequence)
|
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', media_sequence)
|
||||||
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(decrypt_info['URI']).read()
|
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
||||||
|
self._prepare_url(info_dict, decrypt_info['URI'])).read()
|
||||||
frag_content = AES.new(
|
frag_content = AES.new(
|
||||||
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
|
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
|
||||||
self._append_fragment(ctx, frag_content)
|
self._append_fragment(ctx, frag_content)
|
||||||
@ -194,7 +196,7 @@ class HlsFD(FragmentFD):
|
|||||||
'start': sub_range_start,
|
'start': sub_range_start,
|
||||||
'end': sub_range_start + int(splitted_byte_range[0]),
|
'end': sub_range_start + int(splitted_byte_range[0]),
|
||||||
}
|
}
|
||||||
elif anvato_ad(line):
|
elif is_ad_fragment(line):
|
||||||
ad_frag_next = True
|
ad_frag_next = True
|
||||||
|
|
||||||
self._finish_frag_download(ctx)
|
self._finish_frag_download(ctx)
|
||||||
|
@ -4,13 +4,18 @@ import errno
|
|||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import FileDownloader
|
from .common import FileDownloader
|
||||||
from ..compat import compat_urllib_error
|
from ..compat import (
|
||||||
|
compat_str,
|
||||||
|
compat_urllib_error,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ContentTooShortError,
|
ContentTooShortError,
|
||||||
encodeFilename,
|
encodeFilename,
|
||||||
|
int_or_none,
|
||||||
sanitize_open,
|
sanitize_open,
|
||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
write_xattr,
|
write_xattr,
|
||||||
@ -38,21 +43,26 @@ class HttpFD(FileDownloader):
|
|||||||
add_headers = info_dict.get('http_headers')
|
add_headers = info_dict.get('http_headers')
|
||||||
if add_headers:
|
if add_headers:
|
||||||
headers.update(add_headers)
|
headers.update(add_headers)
|
||||||
basic_request = sanitized_Request(url, None, headers)
|
|
||||||
request = sanitized_Request(url, None, headers)
|
|
||||||
|
|
||||||
is_test = self.params.get('test', False)
|
is_test = self.params.get('test', False)
|
||||||
|
chunk_size = self._TEST_FILE_SIZE if is_test else (
|
||||||
if is_test:
|
info_dict.get('downloader_options', {}).get('http_chunk_size') or
|
||||||
request.add_header('Range', 'bytes=0-%s' % str(self._TEST_FILE_SIZE - 1))
|
self.params.get('http_chunk_size') or 0)
|
||||||
|
|
||||||
ctx.open_mode = 'wb'
|
ctx.open_mode = 'wb'
|
||||||
ctx.resume_len = 0
|
ctx.resume_len = 0
|
||||||
|
ctx.data_len = None
|
||||||
|
ctx.block_size = self.params.get('buffersize', 1024)
|
||||||
|
ctx.start_time = time.time()
|
||||||
|
ctx.chunk_size = None
|
||||||
|
|
||||||
if self.params.get('continuedl', True):
|
if self.params.get('continuedl', True):
|
||||||
# Establish possible resume length
|
# Establish possible resume length
|
||||||
if os.path.isfile(encodeFilename(ctx.tmpfilename)):
|
if os.path.isfile(encodeFilename(ctx.tmpfilename)):
|
||||||
ctx.resume_len = os.path.getsize(encodeFilename(ctx.tmpfilename))
|
ctx.resume_len = os.path.getsize(
|
||||||
|
encodeFilename(ctx.tmpfilename))
|
||||||
|
|
||||||
|
ctx.is_resume = ctx.resume_len > 0
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
retries = self.params.get('retries', 0)
|
retries = self.params.get('retries', 0)
|
||||||
@ -64,11 +74,36 @@ class HttpFD(FileDownloader):
|
|||||||
def __init__(self, source_error):
|
def __init__(self, source_error):
|
||||||
self.source_error = source_error
|
self.source_error = source_error
|
||||||
|
|
||||||
|
class NextFragment(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_range(req, start, end):
|
||||||
|
range_header = 'bytes=%d-' % start
|
||||||
|
if end:
|
||||||
|
range_header += compat_str(end)
|
||||||
|
req.add_header('Range', range_header)
|
||||||
|
|
||||||
def establish_connection():
|
def establish_connection():
|
||||||
if ctx.resume_len != 0:
|
ctx.chunk_size = (random.randint(int(chunk_size * 0.95), chunk_size)
|
||||||
|
if not is_test and chunk_size else chunk_size)
|
||||||
|
if ctx.resume_len > 0:
|
||||||
|
range_start = ctx.resume_len
|
||||||
|
if ctx.is_resume:
|
||||||
self.report_resuming_byte(ctx.resume_len)
|
self.report_resuming_byte(ctx.resume_len)
|
||||||
request.add_header('Range', 'bytes=%d-' % ctx.resume_len)
|
|
||||||
ctx.open_mode = 'ab'
|
ctx.open_mode = 'ab'
|
||||||
|
elif ctx.chunk_size > 0:
|
||||||
|
range_start = 0
|
||||||
|
else:
|
||||||
|
range_start = None
|
||||||
|
ctx.is_resume = False
|
||||||
|
range_end = range_start + ctx.chunk_size - 1 if ctx.chunk_size else None
|
||||||
|
if range_end and ctx.data_len is not None and range_end >= ctx.data_len:
|
||||||
|
range_end = ctx.data_len - 1
|
||||||
|
has_range = range_start is not None
|
||||||
|
ctx.has_range = has_range
|
||||||
|
request = sanitized_Request(url, None, headers)
|
||||||
|
if has_range:
|
||||||
|
set_range(request, range_start, range_end)
|
||||||
# Establish connection
|
# Establish connection
|
||||||
try:
|
try:
|
||||||
ctx.data = self.ydl.urlopen(request)
|
ctx.data = self.ydl.urlopen(request)
|
||||||
@ -77,12 +112,24 @@ class HttpFD(FileDownloader):
|
|||||||
# that don't support resuming and serve a whole file with no Content-Range
|
# that don't support resuming and serve a whole file with no Content-Range
|
||||||
# set in response despite of requested Range (see
|
# set in response despite of requested Range (see
|
||||||
# https://github.com/rg3/youtube-dl/issues/6057#issuecomment-126129799)
|
# https://github.com/rg3/youtube-dl/issues/6057#issuecomment-126129799)
|
||||||
if ctx.resume_len > 0:
|
if has_range:
|
||||||
content_range = ctx.data.headers.get('Content-Range')
|
content_range = ctx.data.headers.get('Content-Range')
|
||||||
if content_range:
|
if content_range:
|
||||||
content_range_m = re.search(r'bytes (\d+)-', content_range)
|
content_range_m = re.search(r'bytes (\d+)-(\d+)?(?:/(\d+))?', content_range)
|
||||||
# Content-Range is present and matches requested Range, resume is possible
|
# Content-Range is present and matches requested Range, resume is possible
|
||||||
if content_range_m and ctx.resume_len == int(content_range_m.group(1)):
|
if content_range_m:
|
||||||
|
if range_start == int(content_range_m.group(1)):
|
||||||
|
content_range_end = int_or_none(content_range_m.group(2))
|
||||||
|
content_len = int_or_none(content_range_m.group(3))
|
||||||
|
accept_content_len = (
|
||||||
|
# Non-chunked download
|
||||||
|
not ctx.chunk_size or
|
||||||
|
# Chunked download and requested piece or
|
||||||
|
# its part is promised to be served
|
||||||
|
content_range_end == range_end or
|
||||||
|
content_len < range_end)
|
||||||
|
if accept_content_len:
|
||||||
|
ctx.data_len = content_len
|
||||||
return
|
return
|
||||||
# Content-Range is either not present or invalid. Assuming remote webserver is
|
# Content-Range is either not present or invalid. Assuming remote webserver is
|
||||||
# trying to send the whole file, resume is not possible, so wiping the local file
|
# trying to send the whole file, resume is not possible, so wiping the local file
|
||||||
@ -90,16 +137,15 @@ class HttpFD(FileDownloader):
|
|||||||
self.report_unable_to_resume()
|
self.report_unable_to_resume()
|
||||||
ctx.resume_len = 0
|
ctx.resume_len = 0
|
||||||
ctx.open_mode = 'wb'
|
ctx.open_mode = 'wb'
|
||||||
|
ctx.data_len = int_or_none(ctx.data.info().get('Content-length', None))
|
||||||
return
|
return
|
||||||
except (compat_urllib_error.HTTPError, ) as err:
|
except (compat_urllib_error.HTTPError, ) as err:
|
||||||
if (err.code < 500 or err.code >= 600) and err.code != 416:
|
if err.code == 416:
|
||||||
# Unexpected HTTP error
|
|
||||||
raise
|
|
||||||
elif err.code == 416:
|
|
||||||
# Unable to resume (requested range not satisfiable)
|
# Unable to resume (requested range not satisfiable)
|
||||||
try:
|
try:
|
||||||
# Open the connection again without the range header
|
# Open the connection again without the range header
|
||||||
ctx.data = self.ydl.urlopen(basic_request)
|
ctx.data = self.ydl.urlopen(
|
||||||
|
sanitized_Request(url, None, headers))
|
||||||
content_length = ctx.data.info()['Content-Length']
|
content_length = ctx.data.info()['Content-Length']
|
||||||
except (compat_urllib_error.HTTPError, ) as err:
|
except (compat_urllib_error.HTTPError, ) as err:
|
||||||
if err.code < 500 or err.code >= 600:
|
if err.code < 500 or err.code >= 600:
|
||||||
@ -130,6 +176,9 @@ class HttpFD(FileDownloader):
|
|||||||
ctx.resume_len = 0
|
ctx.resume_len = 0
|
||||||
ctx.open_mode = 'wb'
|
ctx.open_mode = 'wb'
|
||||||
return
|
return
|
||||||
|
elif err.code < 500 or err.code >= 600:
|
||||||
|
# Unexpected HTTP error
|
||||||
|
raise
|
||||||
raise RetryDownload(err)
|
raise RetryDownload(err)
|
||||||
except socket.error as err:
|
except socket.error as err:
|
||||||
if err.errno != errno.ECONNRESET:
|
if err.errno != errno.ECONNRESET:
|
||||||
@ -160,7 +209,7 @@ class HttpFD(FileDownloader):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
byte_counter = 0 + ctx.resume_len
|
byte_counter = 0 + ctx.resume_len
|
||||||
block_size = self.params.get('buffersize', 1024)
|
block_size = ctx.block_size
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
# measure time over whole while-loop, so slow_down() and best_block_size() work together properly
|
# measure time over whole while-loop, so slow_down() and best_block_size() work together properly
|
||||||
@ -168,10 +217,11 @@ class HttpFD(FileDownloader):
|
|||||||
before = start # start measuring
|
before = start # start measuring
|
||||||
|
|
||||||
def retry(e):
|
def retry(e):
|
||||||
if ctx.tmpfilename != '-':
|
to_stdout = ctx.tmpfilename == '-'
|
||||||
|
if not to_stdout:
|
||||||
ctx.stream.close()
|
ctx.stream.close()
|
||||||
ctx.stream = None
|
ctx.stream = None
|
||||||
ctx.resume_len = os.path.getsize(encodeFilename(ctx.tmpfilename))
|
ctx.resume_len = byte_counter if to_stdout else os.path.getsize(encodeFilename(ctx.tmpfilename))
|
||||||
raise RetryDownload(e)
|
raise RetryDownload(e)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@ -233,25 +283,30 @@ class HttpFD(FileDownloader):
|
|||||||
|
|
||||||
# Progress message
|
# Progress message
|
||||||
speed = self.calc_speed(start, now, byte_counter - ctx.resume_len)
|
speed = self.calc_speed(start, now, byte_counter - ctx.resume_len)
|
||||||
if data_len is None:
|
if ctx.data_len is None:
|
||||||
eta = None
|
eta = None
|
||||||
else:
|
else:
|
||||||
eta = self.calc_eta(start, time.time(), data_len - ctx.resume_len, byte_counter - ctx.resume_len)
|
eta = self.calc_eta(start, time.time(), ctx.data_len - ctx.resume_len, byte_counter - ctx.resume_len)
|
||||||
|
|
||||||
self._hook_progress({
|
self._hook_progress({
|
||||||
'status': 'downloading',
|
'status': 'downloading',
|
||||||
'downloaded_bytes': byte_counter,
|
'downloaded_bytes': byte_counter,
|
||||||
'total_bytes': data_len,
|
'total_bytes': ctx.data_len,
|
||||||
'tmpfilename': ctx.tmpfilename,
|
'tmpfilename': ctx.tmpfilename,
|
||||||
'filename': ctx.filename,
|
'filename': ctx.filename,
|
||||||
'eta': eta,
|
'eta': eta,
|
||||||
'speed': speed,
|
'speed': speed,
|
||||||
'elapsed': now - start,
|
'elapsed': now - ctx.start_time,
|
||||||
})
|
})
|
||||||
|
|
||||||
if is_test and byte_counter == data_len:
|
if is_test and byte_counter == data_len:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if not is_test and ctx.chunk_size and ctx.data_len is not None and byte_counter < ctx.data_len:
|
||||||
|
ctx.resume_len = byte_counter
|
||||||
|
# ctx.block_size = block_size
|
||||||
|
raise NextFragment()
|
||||||
|
|
||||||
if ctx.stream is None:
|
if ctx.stream is None:
|
||||||
self.to_stderr('\n')
|
self.to_stderr('\n')
|
||||||
self.report_error('Did not get any data blocks')
|
self.report_error('Did not get any data blocks')
|
||||||
@ -276,7 +331,7 @@ class HttpFD(FileDownloader):
|
|||||||
'total_bytes': byte_counter,
|
'total_bytes': byte_counter,
|
||||||
'filename': ctx.filename,
|
'filename': ctx.filename,
|
||||||
'status': 'finished',
|
'status': 'finished',
|
||||||
'elapsed': time.time() - start,
|
'elapsed': time.time() - ctx.start_time,
|
||||||
})
|
})
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -284,13 +339,14 @@ class HttpFD(FileDownloader):
|
|||||||
while count <= retries:
|
while count <= retries:
|
||||||
try:
|
try:
|
||||||
establish_connection()
|
establish_connection()
|
||||||
download()
|
return download()
|
||||||
return True
|
|
||||||
except RetryDownload as e:
|
except RetryDownload as e:
|
||||||
count += 1
|
count += 1
|
||||||
if count <= retries:
|
if count <= retries:
|
||||||
self.report_retry(e.source_error, count, retries)
|
self.report_retry(e.source_error, count, retries)
|
||||||
continue
|
continue
|
||||||
|
except NextFragment:
|
||||||
|
continue
|
||||||
except SucceedDownload:
|
except SucceedDownload:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -1,25 +1,27 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import struct
|
|
||||||
import binascii
|
import binascii
|
||||||
import io
|
import io
|
||||||
|
|
||||||
from .fragment import FragmentFD
|
from .fragment import FragmentFD
|
||||||
from ..compat import compat_urllib_error
|
from ..compat import (
|
||||||
|
compat_Struct,
|
||||||
|
compat_urllib_error,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
u8 = struct.Struct(b'>B')
|
u8 = compat_Struct('>B')
|
||||||
u88 = struct.Struct(b'>Bx')
|
u88 = compat_Struct('>Bx')
|
||||||
u16 = struct.Struct(b'>H')
|
u16 = compat_Struct('>H')
|
||||||
u1616 = struct.Struct(b'>Hxx')
|
u1616 = compat_Struct('>Hxx')
|
||||||
u32 = struct.Struct(b'>I')
|
u32 = compat_Struct('>I')
|
||||||
u64 = struct.Struct(b'>Q')
|
u64 = compat_Struct('>Q')
|
||||||
|
|
||||||
s88 = struct.Struct(b'>bx')
|
s88 = compat_Struct('>bx')
|
||||||
s16 = struct.Struct(b'>h')
|
s16 = compat_Struct('>h')
|
||||||
s1616 = struct.Struct(b'>hxx')
|
s1616 = compat_Struct('>hxx')
|
||||||
s32 = struct.Struct(b'>i')
|
s32 = compat_Struct('>i')
|
||||||
|
|
||||||
unity_matrix = (s32.pack(0x10000) + s32.pack(0) * 3) * 2 + s32.pack(0x40000000)
|
unity_matrix = (s32.pack(0x10000) + s32.pack(0) * 3) * 2 + s32.pack(0x40000000)
|
||||||
|
|
||||||
@ -139,7 +141,7 @@ def write_piff_header(stream, params):
|
|||||||
sample_entry_payload += u16.pack(0x18) # depth
|
sample_entry_payload += u16.pack(0x18) # depth
|
||||||
sample_entry_payload += s16.pack(-1) # pre defined
|
sample_entry_payload += s16.pack(-1) # pre defined
|
||||||
|
|
||||||
codec_private_data = binascii.unhexlify(params['codec_private_data'])
|
codec_private_data = binascii.unhexlify(params['codec_private_data'].encode('utf-8'))
|
||||||
if fourcc in ('H264', 'AVC1'):
|
if fourcc in ('H264', 'AVC1'):
|
||||||
sps, pps = codec_private_data.split(u32.pack(1))[1:]
|
sps, pps = codec_private_data.split(u32.pack(1))[1:]
|
||||||
avcc_payload = u8.pack(1) # configuration version
|
avcc_payload = u8.pack(1) # configuration version
|
||||||
|
@ -29,6 +29,7 @@ class RtmpFD(FileDownloader):
|
|||||||
proc = subprocess.Popen(args, stderr=subprocess.PIPE)
|
proc = subprocess.Popen(args, stderr=subprocess.PIPE)
|
||||||
cursor_in_new_line = True
|
cursor_in_new_line = True
|
||||||
proc_stderr_closed = False
|
proc_stderr_closed = False
|
||||||
|
try:
|
||||||
while not proc_stderr_closed:
|
while not proc_stderr_closed:
|
||||||
# read line from stderr
|
# read line from stderr
|
||||||
line = ''
|
line = ''
|
||||||
@ -88,6 +89,7 @@ class RtmpFD(FileDownloader):
|
|||||||
self.to_screen('')
|
self.to_screen('')
|
||||||
cursor_in_new_line = True
|
cursor_in_new_line = True
|
||||||
self.to_screen('[rtmpdump] ' + line)
|
self.to_screen('[rtmpdump] ' + line)
|
||||||
|
finally:
|
||||||
proc.wait()
|
proc.wait()
|
||||||
if not cursor_in_new_line:
|
if not cursor_in_new_line:
|
||||||
self.to_screen('')
|
self.to_screen('')
|
||||||
@ -163,7 +165,15 @@ class RtmpFD(FileDownloader):
|
|||||||
RD_INCOMPLETE = 2
|
RD_INCOMPLETE = 2
|
||||||
RD_NO_CONNECT = 3
|
RD_NO_CONNECT = 3
|
||||||
|
|
||||||
|
started = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
retval = run_rtmpdump(args)
|
retval = run_rtmpdump(args)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
if not info_dict.get('is_live'):
|
||||||
|
raise
|
||||||
|
retval = RD_SUCCESS
|
||||||
|
self.to_screen('\n[rtmpdump] Interrupted by user')
|
||||||
|
|
||||||
if retval == RD_NO_CONNECT:
|
if retval == RD_NO_CONNECT:
|
||||||
self.report_error('[rtmpdump] Could not connect to RTMP server.')
|
self.report_error('[rtmpdump] Could not connect to RTMP server.')
|
||||||
@ -171,7 +181,7 @@ class RtmpFD(FileDownloader):
|
|||||||
|
|
||||||
while retval in (RD_INCOMPLETE, RD_FAILED) and not test and not live:
|
while retval in (RD_INCOMPLETE, RD_FAILED) and not test and not live:
|
||||||
prevsize = os.path.getsize(encodeFilename(tmpfilename))
|
prevsize = os.path.getsize(encodeFilename(tmpfilename))
|
||||||
self.to_screen('[rtmpdump] %s bytes' % prevsize)
|
self.to_screen('[rtmpdump] Downloaded %s bytes' % prevsize)
|
||||||
time.sleep(5.0) # This seems to be needed
|
time.sleep(5.0) # This seems to be needed
|
||||||
args = basic_args + ['--resume']
|
args = basic_args + ['--resume']
|
||||||
if retval == RD_FAILED:
|
if retval == RD_FAILED:
|
||||||
@ -188,13 +198,14 @@ class RtmpFD(FileDownloader):
|
|||||||
break
|
break
|
||||||
if retval == RD_SUCCESS or (test and retval == RD_INCOMPLETE):
|
if retval == RD_SUCCESS or (test and retval == RD_INCOMPLETE):
|
||||||
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
||||||
self.to_screen('[rtmpdump] %s bytes' % fsize)
|
self.to_screen('[rtmpdump] Downloaded %s bytes' % fsize)
|
||||||
self.try_rename(tmpfilename, filename)
|
self.try_rename(tmpfilename, filename)
|
||||||
self._hook_progress({
|
self._hook_progress({
|
||||||
'downloaded_bytes': fsize,
|
'downloaded_bytes': fsize,
|
||||||
'total_bytes': fsize,
|
'total_bytes': fsize,
|
||||||
'filename': filename,
|
'filename': filename,
|
||||||
'status': 'finished',
|
'status': 'finished',
|
||||||
|
'elapsed': time.time() - started,
|
||||||
})
|
})
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_str
|
from ..compat import compat_str
|
||||||
@ -10,6 +13,8 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
try_get,
|
try_get,
|
||||||
|
unescapeHTML,
|
||||||
|
update_url_query,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -100,46 +105,59 @@ class ABCIE(InfoExtractor):
|
|||||||
|
|
||||||
class ABCIViewIE(InfoExtractor):
|
class ABCIViewIE(InfoExtractor):
|
||||||
IE_NAME = 'abc.net.au:iview'
|
IE_NAME = 'abc.net.au:iview'
|
||||||
_VALID_URL = r'https?://iview\.abc\.net\.au/programs/[^/]+/(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://iview\.abc\.net\.au/(?:[^/]+/)*video/(?P<id>[^/?#]+)'
|
||||||
|
_GEO_COUNTRIES = ['AU']
|
||||||
|
|
||||||
# ABC iview programs are normally available for 14 days only.
|
# ABC iview programs are normally available for 14 days only.
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://iview.abc.net.au/programs/diaries-of-a-broken-mind/ZX9735A001S00',
|
'url': 'https://iview.abc.net.au/show/ben-and-hollys-little-kingdom/series/0/video/ZX9371A050S00',
|
||||||
'md5': 'cde42d728b3b7c2b32b1b94b4a548afc',
|
'md5': 'cde42d728b3b7c2b32b1b94b4a548afc',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'ZX9735A001S00',
|
'id': 'ZX9371A050S00',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Diaries Of A Broken Mind',
|
'title': "Gaston's Birthday",
|
||||||
'description': 'md5:7de3903874b7a1be279fe6b68718fc9e',
|
'series': "Ben And Holly's Little Kingdom",
|
||||||
'upload_date': '20161010',
|
'description': 'md5:f9de914d02f226968f598ac76f105bcf',
|
||||||
'uploader_id': 'abc2',
|
'upload_date': '20180604',
|
||||||
'timestamp': 1476064920,
|
'uploader_id': 'abc4kids',
|
||||||
|
'timestamp': 1528140219,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
'skip': 'Video gone',
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, video_id)
|
video_params = self._download_json(
|
||||||
video_params = self._parse_json(self._search_regex(
|
'https://iview.abc.net.au/api/programs/' + video_id, video_id)
|
||||||
r'videoParams\s*=\s*({.+?});', webpage, 'video params'), video_id)
|
title = unescapeHTML(video_params.get('title') or video_params['seriesTitle'])
|
||||||
title = video_params.get('title') or video_params['seriesTitle']
|
stream = next(s for s in video_params['playlist'] if s.get('type') in ('program', 'livestream'))
|
||||||
stream = next(s for s in video_params['playlist'] if s.get('type') == 'program')
|
|
||||||
|
|
||||||
format_urls = [
|
house_number = video_params.get('episodeHouseNumber') or video_id
|
||||||
try_get(stream, lambda x: x['hds-unmetered'], compat_str)]
|
path = '/auth/hls/sign?ts={0}&hn={1}&d=android-tablet'.format(
|
||||||
|
int(time.time()), house_number)
|
||||||
|
sig = hmac.new(
|
||||||
|
b'android.content.res.Resources',
|
||||||
|
path.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||||
|
token = self._download_webpage(
|
||||||
|
'http://iview.abc.net.au{0}&sig={1}'.format(path, sig), video_id)
|
||||||
|
|
||||||
# May have higher quality video
|
def tokenize_url(url, token):
|
||||||
|
return update_url_query(url, {
|
||||||
|
'hdnea': token,
|
||||||
|
})
|
||||||
|
|
||||||
|
for sd in ('sd', 'sd-low'):
|
||||||
sd_url = try_get(
|
sd_url = try_get(
|
||||||
stream, lambda x: x['streams']['hds']['sd'], compat_str)
|
stream, lambda x: x['streams']['hls'][sd], compat_str)
|
||||||
if sd_url:
|
if not sd_url:
|
||||||
format_urls.append(sd_url.replace('metered', 'um'))
|
continue
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
formats = []
|
tokenize_url(sd_url, token), video_id, 'mp4',
|
||||||
for format_url in format_urls:
|
entry_protocol='m3u8_native', m3u8_id='hls', fatal=False)
|
||||||
if format_url:
|
if formats:
|
||||||
formats.extend(
|
break
|
||||||
self._extract_akamai_formats(format_url, video_id))
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
@ -150,18 +168,26 @@ class ABCIViewIE(InfoExtractor):
|
|||||||
'ext': 'vtt',
|
'ext': 'vtt',
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
is_live = video_params.get('livestream') == '1'
|
||||||
|
if is_live:
|
||||||
|
title = self._live_title(title)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': self._html_search_meta(['og:description', 'twitter:description'], webpage),
|
'description': video_params.get('description'),
|
||||||
'thumbnail': self._html_search_meta(['og:image', 'twitter:image:src'], webpage),
|
'thumbnail': video_params.get('thumbnail'),
|
||||||
'duration': int_or_none(video_params.get('eventDuration')),
|
'duration': int_or_none(video_params.get('eventDuration')),
|
||||||
'timestamp': parse_iso8601(video_params.get('pubDate'), ' '),
|
'timestamp': parse_iso8601(video_params.get('pubDate'), ' '),
|
||||||
'series': video_params.get('seriesTitle'),
|
'series': unescapeHTML(video_params.get('seriesTitle')),
|
||||||
'series_id': video_params.get('seriesHouseNumber') or video_id[:7],
|
'series_id': video_params.get('seriesHouseNumber') or video_id[:7],
|
||||||
'episode_number': int_or_none(self._html_search_meta('episodeNumber', webpage, default=None)),
|
'season_number': int_or_none(self._search_regex(
|
||||||
'episode': self._html_search_meta('episode_title', webpage, default=None),
|
r'\bSeries\s+(\d+)\b', title, 'season number', default=None)),
|
||||||
|
'episode_number': int_or_none(self._search_regex(
|
||||||
|
r'\bEp\s+(\d+)\b', title, 'episode number', default=None)),
|
||||||
|
'episode_id': house_number,
|
||||||
'uploader_id': video_params.get('channel'),
|
'uploader_id': video_params.get('channel'),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
|
'is_live': is_live,
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ class AbcNewsIE(InfoExtractor):
|
|||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://abcnews.go.com/Blotter/News/dramatic-video-rare-death-job-america/story?id=10498713#.UIhwosWHLjY',
|
'url': 'http://abcnews.go.com/Blotter/News/dramatic-video-rare-death-job-america/story?id=10498713#.UIhwosWHLjY',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '10498713',
|
'id': '10505354',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'display_id': 'dramatic-video-rare-death-job-america',
|
'display_id': 'dramatic-video-rare-death-job-america',
|
||||||
'title': 'Occupational Hazards',
|
'title': 'Occupational Hazards',
|
||||||
@ -79,7 +79,7 @@ class AbcNewsIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://abcnews.go.com/Entertainment/justin-timberlake-performs-stop-feeling-eurovision-2016/story?id=39125818',
|
'url': 'http://abcnews.go.com/Entertainment/justin-timberlake-performs-stop-feeling-eurovision-2016/story?id=39125818',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '39125818',
|
'id': '38897857',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'display_id': 'justin-timberlake-performs-stop-feeling-eurovision-2016',
|
'display_id': 'justin-timberlake-performs-stop-feeling-eurovision-2016',
|
||||||
'title': 'Justin Timberlake Drops Hints For Secret Single',
|
'title': 'Justin Timberlake Drops Hints For Secret Single',
|
||||||
|
@ -7,8 +7,10 @@ import functools
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_str
|
from ..compat import compat_str
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
try_get,
|
||||||
|
unified_timestamp,
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,39 +26,58 @@ class ACastIE(InfoExtractor):
|
|||||||
'id': '57de3baa-4bb0-487e-9418-2692c1277a34',
|
'id': '57de3baa-4bb0-487e-9418-2692c1277a34',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': '"Where Are You?": Taipei 101, Taiwan',
|
'title': '"Where Are You?": Taipei 101, Taiwan',
|
||||||
|
'description': 'md5:a0b4ef3634e63866b542e5b1199a1a0e',
|
||||||
'timestamp': 1196172000,
|
'timestamp': 1196172000,
|
||||||
'upload_date': '20071127',
|
'upload_date': '20071127',
|
||||||
'description': 'md5:a0b4ef3634e63866b542e5b1199a1a0e',
|
|
||||||
'duration': 211,
|
'duration': 211,
|
||||||
|
'creator': 'Concierge',
|
||||||
|
'series': 'Condé Nast Traveler Podcast',
|
||||||
|
'episode': '"Where Are You?": Taipei 101, Taiwan',
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
# test with multiple blings
|
# test with multiple blings
|
||||||
'url': 'https://www.acast.com/sparpodcast/2.raggarmordet-rosterurdetforflutna',
|
'url': 'https://www.acast.com/sparpodcast/2.raggarmordet-rosterurdetforflutna',
|
||||||
'md5': '55c0097badd7095f494c99a172f86501',
|
'md5': 'a02393c74f3bdb1801c3ec2695577ce0',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2a92b283-1a75-4ad8-8396-499c641de0d9',
|
'id': '2a92b283-1a75-4ad8-8396-499c641de0d9',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': '2. Raggarmordet - Röster ur det förflutna',
|
'title': '2. Raggarmordet - Röster ur det förflutna',
|
||||||
|
'description': 'md5:4f81f6d8cf2e12ee21a321d8bca32db4',
|
||||||
'timestamp': 1477346700,
|
'timestamp': 1477346700,
|
||||||
'upload_date': '20161024',
|
'upload_date': '20161024',
|
||||||
'description': 'md5:4f81f6d8cf2e12ee21a321d8bca32db4',
|
'duration': 2766.602563,
|
||||||
'duration': 2797,
|
'creator': 'Anton Berg & Martin Johnson',
|
||||||
|
'series': 'Spår',
|
||||||
|
'episode': '2. Raggarmordet - Röster ur det förflutna',
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
channel, display_id = re.match(self._VALID_URL, url).groups()
|
channel, display_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
s = self._download_json(
|
||||||
|
'https://play-api.acast.com/stitch/%s/%s' % (channel, display_id),
|
||||||
|
display_id)['result']
|
||||||
|
media_url = s['url']
|
||||||
cast_data = self._download_json(
|
cast_data = self._download_json(
|
||||||
'https://embed.acast.com/api/acasts/%s/%s' % (channel, display_id), display_id)
|
'https://play-api.acast.com/splash/%s/%s' % (channel, display_id),
|
||||||
|
display_id)['result']
|
||||||
|
e = cast_data['episode']
|
||||||
|
title = e['name']
|
||||||
return {
|
return {
|
||||||
'id': compat_str(cast_data['id']),
|
'id': compat_str(e['id']),
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'url': [b['audio'] for b in cast_data['blings'] if b['type'] == 'BlingAudio'][0],
|
'url': media_url,
|
||||||
'title': cast_data['name'],
|
'title': title,
|
||||||
'description': cast_data.get('description'),
|
'description': e.get('description') or e.get('summary'),
|
||||||
'thumbnail': cast_data.get('image'),
|
'thumbnail': e.get('image'),
|
||||||
'timestamp': parse_iso8601(cast_data.get('publishingDate')),
|
'timestamp': unified_timestamp(e.get('publishingDate')),
|
||||||
'duration': int_or_none(cast_data.get('duration')),
|
'duration': float_or_none(s.get('duration') or e.get('duration')),
|
||||||
|
'filesize': int_or_none(e.get('contentLength')),
|
||||||
|
'creator': try_get(cast_data, lambda x: x['show']['author'], compat_str),
|
||||||
|
'series': try_get(cast_data, lambda x: x['show']['name'], compat_str),
|
||||||
|
'season_number': int_or_none(e.get('seasonNumber')),
|
||||||
|
'episode': title,
|
||||||
|
'episode_number': int_or_none(e.get('episodeNumber')),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,17 +2,25 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import binascii
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..aes import aes_cbc_decrypt
|
from ..aes import aes_cbc_decrypt
|
||||||
from ..compat import compat_ord
|
from ..compat import (
|
||||||
|
compat_b64decode,
|
||||||
|
compat_ord,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
bytes_to_intlist,
|
bytes_to_intlist,
|
||||||
|
bytes_to_long,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
intlist_to_bytes,
|
intlist_to_bytes,
|
||||||
|
long_to_bytes,
|
||||||
|
pkcs1pad,
|
||||||
srt_subtitles_timecode,
|
srt_subtitles_timecode,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
urljoin,
|
urljoin,
|
||||||
@ -33,6 +41,7 @@ class ADNIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_BASE_URL = 'http://animedigitalnetwork.fr'
|
_BASE_URL = 'http://animedigitalnetwork.fr'
|
||||||
|
_RSA_KEY = (0xc35ae1e4356b65a73b551493da94b8cb443491c0aa092a357a5aee57ffc14dda85326f42d716e539a34542a0d3f363adf16c5ec222d713d5997194030ee2e4f0d1fb328c01a81cf6868c090d50de8e169c6b13d1675b9eeed1cbc51e1fffca9b38af07f37abd790924cd3bee59d0257cfda4fe5f3f0534877e21ce5821447d1b, 65537)
|
||||||
|
|
||||||
def _get_subtitles(self, sub_path, video_id):
|
def _get_subtitles(self, sub_path, video_id):
|
||||||
if not sub_path:
|
if not sub_path:
|
||||||
@ -40,17 +49,15 @@ class ADNIE(InfoExtractor):
|
|||||||
|
|
||||||
enc_subtitles = self._download_webpage(
|
enc_subtitles = self._download_webpage(
|
||||||
urljoin(self._BASE_URL, sub_path),
|
urljoin(self._BASE_URL, sub_path),
|
||||||
video_id, fatal=False, headers={
|
video_id, fatal=False)
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0',
|
|
||||||
})
|
|
||||||
if not enc_subtitles:
|
if not enc_subtitles:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# http://animedigitalnetwork.fr/components/com_vodvideo/videojs/adn-vjs.min.js
|
# http://animedigitalnetwork.fr/components/com_vodvideo/videojs/adn-vjs.min.js
|
||||||
dec_subtitles = intlist_to_bytes(aes_cbc_decrypt(
|
dec_subtitles = intlist_to_bytes(aes_cbc_decrypt(
|
||||||
bytes_to_intlist(base64.b64decode(enc_subtitles[24:])),
|
bytes_to_intlist(compat_b64decode(enc_subtitles[24:])),
|
||||||
bytes_to_intlist(b'\x1b\xe0\x29\x61\x38\x94\x24\x00\x12\xbd\xc5\x80\xac\xce\xbe\xb0'),
|
bytes_to_intlist(binascii.unhexlify(self._K + '9032ad7083106400')),
|
||||||
bytes_to_intlist(base64.b64decode(enc_subtitles[:24]))
|
bytes_to_intlist(compat_b64decode(enc_subtitles[:24]))
|
||||||
))
|
))
|
||||||
subtitles_json = self._parse_json(
|
subtitles_json = self._parse_json(
|
||||||
dec_subtitles[:-compat_ord(dec_subtitles[-1])].decode(),
|
dec_subtitles[:-compat_ord(dec_subtitles[-1])].decode(),
|
||||||
@ -105,15 +112,31 @@ class ADNIE(InfoExtractor):
|
|||||||
|
|
||||||
options = player_config.get('options') or {}
|
options = player_config.get('options') or {}
|
||||||
metas = options.get('metas') or {}
|
metas = options.get('metas') or {}
|
||||||
title = metas.get('title') or video_info['title']
|
|
||||||
links = player_config.get('links') or {}
|
links = player_config.get('links') or {}
|
||||||
|
sub_path = player_config.get('subtitles')
|
||||||
error = None
|
error = None
|
||||||
if not links:
|
if not links:
|
||||||
links_url = player_config['linksurl']
|
links_url = player_config.get('linksurl') or options['videoUrl']
|
||||||
links_data = self._download_json(urljoin(
|
token = options['token']
|
||||||
self._BASE_URL, links_url), video_id)
|
self._K = ''.join([random.choice('0123456789abcdef') for _ in range(16)])
|
||||||
|
message = bytes_to_intlist(json.dumps({
|
||||||
|
'k': self._K,
|
||||||
|
'e': 60,
|
||||||
|
't': token,
|
||||||
|
}))
|
||||||
|
padded_message = intlist_to_bytes(pkcs1pad(message, 128))
|
||||||
|
n, e = self._RSA_KEY
|
||||||
|
encrypted_message = long_to_bytes(pow(bytes_to_long(padded_message), e, n))
|
||||||
|
authorization = base64.b64encode(encrypted_message).decode()
|
||||||
|
links_data = self._download_json(
|
||||||
|
urljoin(self._BASE_URL, links_url), video_id, headers={
|
||||||
|
'Authorization': 'Bearer ' + authorization,
|
||||||
|
})
|
||||||
links = links_data.get('links') or {}
|
links = links_data.get('links') or {}
|
||||||
|
metas = metas or links_data.get('meta') or {}
|
||||||
|
sub_path = (sub_path or links_data.get('subtitles')) + '&token=' + token
|
||||||
error = links_data.get('error')
|
error = links_data.get('error')
|
||||||
|
title = metas.get('title') or video_info['title']
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for format_id, qualities in links.items():
|
for format_id, qualities in links.items():
|
||||||
@ -144,7 +167,7 @@ class ADNIE(InfoExtractor):
|
|||||||
'description': strip_or_none(metas.get('summary') or video_info.get('resume')),
|
'description': strip_or_none(metas.get('summary') or video_info.get('resume')),
|
||||||
'thumbnail': video_info.get('image'),
|
'thumbnail': video_info.get('image'),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': self.extract_subtitles(player_config.get('subtitles'), video_id),
|
'subtitles': self.extract_subtitles(sub_path, video_id),
|
||||||
'episode': metas.get('subtitle') or video_info.get('videoTitle'),
|
'episode': metas.get('subtitle') or video_info.get('videoTitle'),
|
||||||
'series': video_info.get('playlistTitle'),
|
'series': video_info.get('playlistTitle'),
|
||||||
}
|
}
|
||||||
|
@ -1325,8 +1325,8 @@ class AdobePassIE(InfoExtractor):
|
|||||||
_DOWNLOADING_LOGIN_PAGE = 'Downloading Provider Login Page'
|
_DOWNLOADING_LOGIN_PAGE = 'Downloading Provider Login Page'
|
||||||
|
|
||||||
def _download_webpage_handle(self, *args, **kwargs):
|
def _download_webpage_handle(self, *args, **kwargs):
|
||||||
headers = kwargs.get('headers', {})
|
headers = self.geo_verification_headers()
|
||||||
headers.update(self.geo_verification_headers())
|
headers.update(kwargs.get('headers', {}))
|
||||||
kwargs['headers'] = headers
|
kwargs['headers'] = headers
|
||||||
return super(AdobePassIE, self)._download_webpage_handle(
|
return super(AdobePassIE, self)._download_webpage_handle(
|
||||||
*args, **compat_kwargs(kwargs))
|
*args, **compat_kwargs(kwargs))
|
||||||
|
@ -7,6 +7,7 @@ from .turner import TurnerBaseIE
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
strip_or_none,
|
strip_or_none,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -98,7 +99,7 @@ class AdultSwimIE(TurnerBaseIE):
|
|||||||
if not video_id:
|
if not video_id:
|
||||||
entries = []
|
entries = []
|
||||||
for episode in video_data.get('archiveEpisodes', []):
|
for episode in video_data.get('archiveEpisodes', []):
|
||||||
episode_url = episode.get('url')
|
episode_url = url_or_none(episode.get('url'))
|
||||||
if not episode_url:
|
if not episode_url:
|
||||||
continue
|
continue
|
||||||
entries.append(self.url_result(
|
entries.append(self.url_result(
|
||||||
|
@ -122,7 +122,8 @@ class AENetworksIE(AENetworksBaseIE):
|
|||||||
|
|
||||||
query = {
|
query = {
|
||||||
'mbr': 'true',
|
'mbr': 'true',
|
||||||
'assetTypes': 'high_video_s3'
|
'assetTypes': 'high_video_ak',
|
||||||
|
'switch': 'hls_high_ak',
|
||||||
}
|
}
|
||||||
video_id = self._html_search_meta('aetn:VideoID', webpage)
|
video_id = self._html_search_meta('aetn:VideoID', webpage)
|
||||||
media_url = self._search_regex(
|
media_url = self._search_regex(
|
||||||
|
@ -9,6 +9,8 @@ from ..utils import (
|
|||||||
determine_ext,
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
url_or_none,
|
||||||
|
urlencode_postdata,
|
||||||
xpath_text,
|
xpath_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,6 +30,7 @@ class AfreecaTVIE(InfoExtractor):
|
|||||||
)
|
)
|
||||||
(?P<id>\d+)
|
(?P<id>\d+)
|
||||||
'''
|
'''
|
||||||
|
_NETRC_MACHINE = 'afreecatv'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://live.afreecatv.com:8079/app/index.cgi?szType=read_ucc_bbs&szBjId=dailyapril&nStationNo=16711924&nBbsNo=18605867&nTitleNo=36164052&szSkin=',
|
'url': 'http://live.afreecatv.com:8079/app/index.cgi?szType=read_ucc_bbs&szBjId=dailyapril&nStationNo=16711924&nBbsNo=18605867&nTitleNo=36164052&szSkin=',
|
||||||
'md5': 'f72c89fe7ecc14c1b5ce506c4996046e',
|
'md5': 'f72c89fe7ecc14c1b5ce506c4996046e',
|
||||||
@ -139,22 +142,22 @@ class AfreecaTVIE(InfoExtractor):
|
|||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# adult video
|
# PARTIAL_ADULT
|
||||||
'url': 'http://vod.afreecatv.com/PLAYER/STATION/26542731',
|
'url': 'http://vod.afreecatv.com/PLAYER/STATION/32028439',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '20171001_F1AE1711_196617479_1',
|
'id': '20180327_27901457_202289533_1',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '[생]서아 초심 찾기 방송 (part 1)',
|
'title': '[생]빨개요♥ (part 1)',
|
||||||
'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
|
'thumbnail': 're:^https?://(?:video|st)img.afreecatv.com/.*$',
|
||||||
'uploader': 'BJ서아',
|
'uploader': '[SA]서아',
|
||||||
'uploader_id': 'bjdyrksu',
|
'uploader_id': 'bjdyrksu',
|
||||||
'upload_date': '20171001',
|
'upload_date': '20180327',
|
||||||
'duration': 3600,
|
'duration': 3601,
|
||||||
'age_limit': 18,
|
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'expected_warnings': ['adult content'],
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.afreecatv.com/player/Player.swf?szType=szBjId=djleegoon&nStationNo=11273158&nBbsNo=13161095&nTitleNo=36327652',
|
'url': 'http://www.afreecatv.com/player/Player.swf?szType=szBjId=djleegoon&nStationNo=11273158&nBbsNo=13161095&nTitleNo=36327652',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@ -172,25 +175,107 @@ class AfreecaTVIE(InfoExtractor):
|
|||||||
video_key['part'] = int(m.group('part'))
|
video_key['part'] = int(m.group('part'))
|
||||||
return video_key
|
return video_key
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
self._login()
|
||||||
|
|
||||||
|
def _login(self):
|
||||||
|
username, password = self._get_login_info()
|
||||||
|
if username is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
login_form = {
|
||||||
|
'szWork': 'login',
|
||||||
|
'szType': 'json',
|
||||||
|
'szUid': username,
|
||||||
|
'szPassword': password,
|
||||||
|
'isSaveId': 'false',
|
||||||
|
'szScriptVar': 'oLoginRet',
|
||||||
|
'szAction': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self._download_json(
|
||||||
|
'https://login.afreecatv.com/app/LoginAction.php', None,
|
||||||
|
'Logging in', data=urlencode_postdata(login_form))
|
||||||
|
|
||||||
|
_ERRORS = {
|
||||||
|
-4: 'Your account has been suspended due to a violation of our terms and policies.',
|
||||||
|
-5: 'https://member.afreecatv.com/app/user_delete_progress.php',
|
||||||
|
-6: 'https://login.afreecatv.com/membership/changeMember.php',
|
||||||
|
-8: "Hello! AfreecaTV here.\nThe username you have entered belongs to \n an account that requires a legal guardian's consent. \nIf you wish to use our services without restriction, \nplease make sure to go through the necessary verification process.",
|
||||||
|
-9: 'https://member.afreecatv.com/app/pop_login_block.php',
|
||||||
|
-11: 'https://login.afreecatv.com/afreeca/second_login.php',
|
||||||
|
-12: 'https://member.afreecatv.com/app/user_security.php',
|
||||||
|
0: 'The username does not exist or you have entered the wrong password.',
|
||||||
|
-1: 'The username does not exist or you have entered the wrong password.',
|
||||||
|
-3: 'You have entered your username/password incorrectly.',
|
||||||
|
-7: 'You cannot use your Global AfreecaTV account to access Korean AfreecaTV.',
|
||||||
|
-10: 'Sorry for the inconvenience. \nYour account has been blocked due to an unauthorized access. \nPlease contact our Help Center for assistance.',
|
||||||
|
-32008: 'You have failed to log in. Please contact our Help Center.',
|
||||||
|
}
|
||||||
|
|
||||||
|
result = int_or_none(response.get('RESULT'))
|
||||||
|
if result != 1:
|
||||||
|
error = _ERRORS.get(result, 'You have failed to log in.')
|
||||||
|
raise ExtractorError(
|
||||||
|
'Unable to login: %s said: %s' % (self.IE_NAME, error),
|
||||||
|
expected=True)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
if re.search(r'alert\(["\']This video has been deleted', webpage):
|
||||||
|
raise ExtractorError(
|
||||||
|
'Video %s has been deleted' % video_id, expected=True)
|
||||||
|
|
||||||
|
station_id = self._search_regex(
|
||||||
|
r'nStationNo\s*=\s*(\d+)', webpage, 'station')
|
||||||
|
bbs_id = self._search_regex(
|
||||||
|
r'nBbsNo\s*=\s*(\d+)', webpage, 'bbs')
|
||||||
|
video_id = self._search_regex(
|
||||||
|
r'nTitleNo\s*=\s*(\d+)', webpage, 'title', default=video_id)
|
||||||
|
|
||||||
|
partial_view = False
|
||||||
|
for _ in range(2):
|
||||||
|
query = {
|
||||||
|
'nTitleNo': video_id,
|
||||||
|
'nStationNo': station_id,
|
||||||
|
'nBbsNo': bbs_id,
|
||||||
|
}
|
||||||
|
if partial_view:
|
||||||
|
query['partialView'] = 'SKIP_ADULT'
|
||||||
video_xml = self._download_xml(
|
video_xml = self._download_xml(
|
||||||
'http://afbbs.afreecatv.com:8080/api/video/get_video_info.php',
|
'http://afbbs.afreecatv.com:8080/api/video/get_video_info.php',
|
||||||
video_id, query={
|
video_id, 'Downloading video info XML%s'
|
||||||
'nTitleNo': video_id,
|
% (' (skipping adult)' if partial_view else ''),
|
||||||
'partialView': 'SKIP_ADULT',
|
video_id, headers={
|
||||||
})
|
'Referer': url,
|
||||||
|
}, query=query)
|
||||||
|
|
||||||
flag = xpath_text(video_xml, './track/flag', 'flag', default=None)
|
flag = xpath_text(video_xml, './track/flag', 'flag', default=None)
|
||||||
if flag and flag != 'SUCCEED':
|
if flag and flag == 'SUCCEED':
|
||||||
|
break
|
||||||
|
if flag == 'PARTIAL_ADULT':
|
||||||
|
self._downloader.report_warning(
|
||||||
|
'In accordance with local laws and regulations, underage users are restricted from watching adult content. '
|
||||||
|
'Only content suitable for all ages will be downloaded. '
|
||||||
|
'Provide account credentials if you wish to download restricted content.')
|
||||||
|
partial_view = True
|
||||||
|
continue
|
||||||
|
elif flag == 'ADULT':
|
||||||
|
error = 'Only users older than 19 are able to watch this video. Provide account credentials to download this content.'
|
||||||
|
else:
|
||||||
|
error = flag
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
'%s said: %s' % (self.IE_NAME, flag), expected=True)
|
'%s said: %s' % (self.IE_NAME, error), expected=True)
|
||||||
|
else:
|
||||||
|
raise ExtractorError('Unable to download video info')
|
||||||
|
|
||||||
video_element = video_xml.findall(compat_xpath('./track/video'))[1]
|
video_element = video_xml.findall(compat_xpath('./track/video'))[-1]
|
||||||
if video_element is None or video_element.text is None:
|
if video_element is None or video_element.text is None:
|
||||||
raise ExtractorError('Specified AfreecaTV video does not exist',
|
raise ExtractorError(
|
||||||
expected=True)
|
'Video %s video does not exist' % video_id, expected=True)
|
||||||
|
|
||||||
video_url = video_element.text.strip()
|
video_url = video_element.text.strip()
|
||||||
|
|
||||||
@ -220,7 +305,7 @@ class AfreecaTVIE(InfoExtractor):
|
|||||||
file_elements = video_element.findall(compat_xpath('./file'))
|
file_elements = video_element.findall(compat_xpath('./file'))
|
||||||
one = len(file_elements) == 1
|
one = len(file_elements) == 1
|
||||||
for file_num, file_element in enumerate(file_elements, start=1):
|
for file_num, file_element in enumerate(file_elements, start=1):
|
||||||
file_url = file_element.text
|
file_url = url_or_none(file_element.text)
|
||||||
if not file_url:
|
if not file_url:
|
||||||
continue
|
continue
|
||||||
key = file_element.get('key', '')
|
key = file_element.get('key', '')
|
||||||
@ -228,10 +313,19 @@ class AfreecaTVIE(InfoExtractor):
|
|||||||
r'^(\d{8})_', key, 'upload date', default=None)
|
r'^(\d{8})_', key, 'upload date', default=None)
|
||||||
file_duration = int_or_none(file_element.get('duration'))
|
file_duration = int_or_none(file_element.get('duration'))
|
||||||
format_id = key if key else '%s_%s' % (video_id, file_num)
|
format_id = key if key else '%s_%s' % (video_id, file_num)
|
||||||
|
if determine_ext(file_url) == 'm3u8':
|
||||||
formats = self._extract_m3u8_formats(
|
formats = self._extract_m3u8_formats(
|
||||||
file_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
file_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
m3u8_id='hls',
|
m3u8_id='hls',
|
||||||
note='Downloading part %d m3u8 information' % file_num)
|
note='Downloading part %d m3u8 information' % file_num)
|
||||||
|
else:
|
||||||
|
formats = [{
|
||||||
|
'url': file_url,
|
||||||
|
'format_id': 'http',
|
||||||
|
}]
|
||||||
|
if not formats:
|
||||||
|
continue
|
||||||
|
self._sort_formats(formats)
|
||||||
file_info = common_entry.copy()
|
file_info = common_entry.copy()
|
||||||
file_info.update({
|
file_info.update({
|
||||||
'id': format_id,
|
'id': format_id,
|
||||||
|
@ -11,7 +11,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
class AMCNetworksIE(ThePlatformIE):
|
class AMCNetworksIE(ThePlatformIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:amc|bbcamerica|ifc|wetv)\.com/(?:movies|shows(?:/[^/]+)+)/(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?(?:amc|bbcamerica|ifc|(?:we|sundance)tv)\.com/(?:movies|shows(?:/[^/]+)+)/(?P<id>[^/?#]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.ifc.com/shows/maron/season-04/episode-01/step-1',
|
'url': 'http://www.ifc.com/shows/maron/season-04/episode-01/step-1',
|
||||||
'md5': '',
|
'md5': '',
|
||||||
@ -51,6 +51,9 @@ class AMCNetworksIE(ThePlatformIE):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://www.wetv.com/shows/la-hair/videos/season-05/episode-09-episode-9-2/episode-9-sneak-peek-3',
|
'url': 'http://www.wetv.com/shows/la-hair/videos/season-05/episode-09-episode-9-2/episode-9-sneak-peek-3',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.sundancetv.com/shows/riviera/full-episodes/season-1/episode-01-episode-1',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
0
youtube_dl/extractor/americastestkitchen.py
Executable file → Normal file
0
youtube_dl/extractor/americastestkitchen.py
Executable file → Normal file
@ -3,11 +3,12 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
|
||||||
parse_iso8601,
|
|
||||||
mimetype2ext,
|
|
||||||
determine_ext,
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
int_or_none,
|
||||||
|
mimetype2ext,
|
||||||
|
parse_iso8601,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ class AMPIE(InfoExtractor):
|
|||||||
media_thumbnail = [media_thumbnail]
|
media_thumbnail = [media_thumbnail]
|
||||||
for thumbnail_data in media_thumbnail:
|
for thumbnail_data in media_thumbnail:
|
||||||
thumbnail = thumbnail_data.get('@attributes', {})
|
thumbnail = thumbnail_data.get('@attributes', {})
|
||||||
thumbnail_url = thumbnail.get('url')
|
thumbnail_url = url_or_none(thumbnail.get('url'))
|
||||||
if not thumbnail_url:
|
if not thumbnail_url:
|
||||||
continue
|
continue
|
||||||
thumbnails.append({
|
thumbnails.append({
|
||||||
@ -51,7 +52,7 @@ class AMPIE(InfoExtractor):
|
|||||||
media_subtitle = [media_subtitle]
|
media_subtitle = [media_subtitle]
|
||||||
for subtitle_data in media_subtitle:
|
for subtitle_data in media_subtitle:
|
||||||
subtitle = subtitle_data.get('@attributes', {})
|
subtitle = subtitle_data.get('@attributes', {})
|
||||||
subtitle_href = subtitle.get('href')
|
subtitle_href = url_or_none(subtitle.get('href'))
|
||||||
if not subtitle_href:
|
if not subtitle_href:
|
||||||
continue
|
continue
|
||||||
subtitles.setdefault(subtitle.get('lang') or 'en', []).append({
|
subtitles.setdefault(subtitle.get('lang') or 'en', []).append({
|
||||||
@ -65,7 +66,7 @@ class AMPIE(InfoExtractor):
|
|||||||
media_content = [media_content]
|
media_content = [media_content]
|
||||||
for media_data in media_content:
|
for media_data in media_content:
|
||||||
media = media_data.get('@attributes', {})
|
media = media_data.get('@attributes', {})
|
||||||
media_url = media.get('url')
|
media_url = url_or_none(media.get('url'))
|
||||||
if not media_url:
|
if not media_url:
|
||||||
continue
|
continue
|
||||||
ext = mimetype2ext(media.get('type')) or determine_ext(media_url)
|
ext = mimetype2ext(media.get('type')) or determine_ext(media_url)
|
||||||
@ -79,7 +80,7 @@ class AMPIE(InfoExtractor):
|
|||||||
else:
|
else:
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': media_data.get('media-category', {}).get('@attributes', {}).get('label'),
|
'format_id': media_data.get('media-category', {}).get('@attributes', {}).get('label'),
|
||||||
'url': media['url'],
|
'url': media_url,
|
||||||
'tbr': int_or_none(media.get('bitrate')),
|
'tbr': int_or_none(media.get('bitrate')),
|
||||||
'filesize': int_or_none(media.get('fileSize')),
|
'filesize': int_or_none(media.get('fileSize')),
|
||||||
'ext': ext,
|
'ext': ext,
|
||||||
|
@ -8,6 +8,7 @@ from ..utils import (
|
|||||||
determine_ext,
|
determine_ext,
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
url_or_none,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
@ -52,7 +53,7 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
}]
|
}]
|
||||||
|
|
||||||
def _login(self):
|
def _login(self):
|
||||||
(username, password) = self._get_login_info()
|
username, password = self._get_login_info()
|
||||||
if username is None:
|
if username is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -85,8 +86,8 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
|
|
||||||
if all(p not in response for p in ('>Logout<', 'href="/users/sign_out"')):
|
if all(p not in response for p in ('>Logout<', 'href="/users/sign_out"')):
|
||||||
error = self._search_regex(
|
error = self._search_regex(
|
||||||
r'<p class="alert alert-danger">(.+?)</p>',
|
r'<p[^>]+\bclass=(["\'])(?:(?!\1).)*\balert\b(?:(?!\1).)*\1[^>]*>(?P<error>.+?)</p>',
|
||||||
response, 'error', default=None)
|
response, 'error', default=None, group='error')
|
||||||
if error:
|
if error:
|
||||||
raise ExtractorError('Unable to login: %s' % error, expected=True)
|
raise ExtractorError('Unable to login: %s' % error, expected=True)
|
||||||
raise ExtractorError('Unable to log in')
|
raise ExtractorError('Unable to log in')
|
||||||
@ -165,7 +166,7 @@ class AnimeOnDemandIE(InfoExtractor):
|
|||||||
}, fatal=False)
|
}, fatal=False)
|
||||||
if not playlist:
|
if not playlist:
|
||||||
continue
|
continue
|
||||||
stream_url = playlist.get('streamurl')
|
stream_url = url_or_none(playlist.get('streamurl'))
|
||||||
if stream_url:
|
if stream_url:
|
||||||
rtmp = re.search(
|
rtmp = re.search(
|
||||||
r'^(?P<url>rtmpe?://(?P<host>[^/]+)/(?P<app>.+/))(?P<playpath>mp[34]:.+)',
|
r'^(?P<url>rtmpe?://(?P<host>[^/]+)/(?P<app>.+/))(?P<playpath>mp[34]:.+)',
|
||||||
|
@ -134,9 +134,33 @@ class AnvatoIE(InfoExtractor):
|
|||||||
'telemundo': 'anvato_mcp_telemundo_web_prod_c5278d51ad46fda4b6ca3d0ea44a7846a054f582'
|
'telemundo': 'anvato_mcp_telemundo_web_prod_c5278d51ad46fda4b6ca3d0ea44a7846a054f582'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_API_KEY = '3hwbSuqqT690uxjNYBktSQpa5ZrpYYR0Iofx7NcJHyA'
|
||||||
|
|
||||||
_ANVP_RE = r'<script[^>]+\bdata-anvp\s*=\s*(["\'])(?P<anvp>(?:(?!\1).)+)\1'
|
_ANVP_RE = r'<script[^>]+\bdata-anvp\s*=\s*(["\'])(?P<anvp>(?:(?!\1).)+)\1'
|
||||||
_AUTH_KEY = b'\x31\xc2\x42\x84\x9e\x73\xa0\xce'
|
_AUTH_KEY = b'\x31\xc2\x42\x84\x9e\x73\xa0\xce'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
# from https://www.boston25news.com/news/watch-humpback-whale-breaches-right-next-to-fishing-boat-near-nh/817484874
|
||||||
|
'url': 'anvato:8v9BEynrwx8EFLYpgfOWcG1qJqyXKlRM:4465496',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4465496',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'VIDEO: Humpback whale breaches right next to NH boat',
|
||||||
|
'description': 'VIDEO: Humpback whale breaches right next to NH boat. Footage courtesy: Zach Fahey.',
|
||||||
|
'duration': 22,
|
||||||
|
'timestamp': 1534855680,
|
||||||
|
'upload_date': '20180821',
|
||||||
|
'uploader': 'ANV',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# from https://sanfrancisco.cbslocal.com/2016/06/17/source-oakland-cop-on-leave-for-having-girlfriend-help-with-police-reports/
|
||||||
|
'url': 'anvato:DVzl9QRzox3ZZsP9bNu5Li3X7obQOnqP:3417601',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(AnvatoIE, self).__init__(*args, **kwargs)
|
super(AnvatoIE, self).__init__(*args, **kwargs)
|
||||||
self.__server_time = None
|
self.__server_time = None
|
||||||
@ -169,7 +193,8 @@ class AnvatoIE(InfoExtractor):
|
|||||||
'api': {
|
'api': {
|
||||||
'anvrid': anvrid,
|
'anvrid': anvrid,
|
||||||
'anvstk': md5_text('%s|%s|%d|%s' % (
|
'anvstk': md5_text('%s|%s|%d|%s' % (
|
||||||
access_key, anvrid, server_time, self._ANVACK_TABLE[access_key])),
|
access_key, anvrid, server_time,
|
||||||
|
self._ANVACK_TABLE.get(access_key, self._API_KEY))),
|
||||||
'anvts': server_time,
|
'anvts': server_time,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -277,10 +302,13 @@ class AnvatoIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
url, smuggled_data = unsmuggle_url(url, {})
|
url, smuggled_data = unsmuggle_url(url, {})
|
||||||
self._initialize_geo_bypass(smuggled_data.get('geo_countries'))
|
self._initialize_geo_bypass({
|
||||||
|
'countries': smuggled_data.get('geo_countries'),
|
||||||
|
})
|
||||||
|
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
access_key, video_id = mobj.group('access_key_or_mcp', 'id')
|
access_key, video_id = mobj.group('access_key_or_mcp', 'id')
|
||||||
if access_key not in self._ANVACK_TABLE:
|
if access_key not in self._ANVACK_TABLE:
|
||||||
access_key = self._MCP_TO_ACCESS_KEY_TABLE[access_key]
|
access_key = self._MCP_TO_ACCESS_KEY_TABLE.get(
|
||||||
|
access_key) or access_key
|
||||||
return self._get_anvato_videos(access_key, video_id)
|
return self._get_anvato_videos(access_key, video_id)
|
||||||
|
@ -7,6 +7,7 @@ from .common import InfoExtractor
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -77,7 +78,7 @@ class AolIE(InfoExtractor):
|
|||||||
formats.extend(self._extract_m3u8_formats(
|
formats.extend(self._extract_m3u8_formats(
|
||||||
m3u8_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
|
m3u8_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
|
||||||
for rendition in video_data.get('renditions', []):
|
for rendition in video_data.get('renditions', []):
|
||||||
video_url = rendition.get('url')
|
video_url = url_or_none(rendition.get('url'))
|
||||||
if not video_url:
|
if not video_url:
|
||||||
continue
|
continue
|
||||||
ext = rendition.get('format')
|
ext = rendition.get('format')
|
||||||
|
94
youtube_dl/extractor/apa.py
Normal file
94
youtube_dl/extractor/apa.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
|
js_to_json,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class APAIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://[^/]+\.apa\.at/embed/(?P<id>[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12})'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://uvp.apa.at/embed/293f6d17-692a-44e3-9fd5-7b178f3a1029',
|
||||||
|
'md5': '2b12292faeb0a7d930c778c7a5b4759b',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'jjv85FdZ',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '"Blau ist mysteriös": Die Blue Man Group im Interview',
|
||||||
|
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'duration': 254,
|
||||||
|
'timestamp': 1519211149,
|
||||||
|
'upload_date': '20180221',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://uvp-apapublisher.sf.apa.at/embed/2f94e9e6-d945-4db2-9548-f9a41ebf7b78',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://uvp-rma.sf.apa.at/embed/70404cca-2f47-4855-bbb8-20b1fae58f76',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://uvp-kleinezeitung.sf.apa.at/embed/f1c44979-dba2-4ebf-b021-e4cf2cac3c81',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_urls(webpage):
|
||||||
|
return [
|
||||||
|
mobj.group('url')
|
||||||
|
for mobj in re.finditer(
|
||||||
|
r'<iframe[^>]+\bsrc=(["\'])(?P<url>(?:https?:)?//[^/]+\.apa\.at/embed/[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}.*?)\1',
|
||||||
|
webpage)]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
jwplatform_id = self._search_regex(
|
||||||
|
r'media[iI]d\s*:\s*["\'](?P<id>[a-zA-Z0-9]{8})', webpage,
|
||||||
|
'jwplatform id', default=None)
|
||||||
|
|
||||||
|
if jwplatform_id:
|
||||||
|
return self.url_result(
|
||||||
|
'jwplatform:' + jwplatform_id, ie='JWPlatform',
|
||||||
|
video_id=video_id)
|
||||||
|
|
||||||
|
sources = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'sources\s*=\s*(\[.+?\])\s*;', webpage, 'sources'),
|
||||||
|
video_id, transform_source=js_to_json)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for source in sources:
|
||||||
|
if not isinstance(source, dict):
|
||||||
|
continue
|
||||||
|
source_url = url_or_none(source.get('file'))
|
||||||
|
if not source_url:
|
||||||
|
continue
|
||||||
|
ext = determine_ext(source_url)
|
||||||
|
if ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
source_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
|
m3u8_id='hls', fatal=False))
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'url': source_url,
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
thumbnail = self._search_regex(
|
||||||
|
r'image\s*:\s*(["\'])(?P<url>(?:(?!\1).)+)\1', webpage,
|
||||||
|
'thumbnail', fatal=False, group='url')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': video_id,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
@ -5,6 +5,7 @@ from .common import InfoExtractor
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ class AparatIE(InfoExtractor):
|
|||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for item in file_list[0]:
|
for item in file_list[0]:
|
||||||
file_url = item.get('file')
|
file_url = url_or_none(item.get('file'))
|
||||||
if not file_url:
|
if not file_url:
|
||||||
continue
|
continue
|
||||||
ext = mimetype2ext(item.get('type'))
|
ext = mimetype2ext(item.get('type'))
|
||||||
|
@ -41,7 +41,7 @@ class ArchiveOrgIE(InfoExtractor):
|
|||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
'http://archive.org/embed/' + video_id, video_id)
|
'http://archive.org/embed/' + video_id, video_id)
|
||||||
jwplayer_playlist = self._parse_json(self._search_regex(
|
jwplayer_playlist = self._parse_json(self._search_regex(
|
||||||
r"(?s)Play\('[^']+'\s*,\s*(\[.+\])\s*,\s*{.*?}\);",
|
r"(?s)Play\('[^']+'\s*,\s*(\[.+\])\s*,\s*{.*?}\)",
|
||||||
webpage, 'jwplayer playlist'), video_id)
|
webpage, 'jwplayer playlist'), video_id)
|
||||||
info = self._parse_jwplayer_data(
|
info = self._parse_jwplayer_data(
|
||||||
{'playlist': jwplayer_playlist}, video_id, base_url=url)
|
{'playlist': jwplayer_playlist}, video_id, base_url=url)
|
||||||
|
@ -14,66 +14,43 @@ from ..utils import (
|
|||||||
unified_strdate,
|
unified_strdate,
|
||||||
xpath_text,
|
xpath_text,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
from ..compat import compat_etree_fromstring
|
from ..compat import compat_etree_fromstring
|
||||||
|
|
||||||
|
|
||||||
class ARDMediathekIE(InfoExtractor):
|
class ARDMediathekIE(InfoExtractor):
|
||||||
IE_NAME = 'ARD:mediathek'
|
IE_NAME = 'ARD:mediathek'
|
||||||
_VALID_URL = r'^https?://(?:(?:www\.)?ardmediathek\.de|mediathek\.(?:daserste|rbb-online)\.de)/(?:.*/)(?P<video_id>[0-9]+|[^0-9][^/\?]+)[^/\?]*(?:\?.*)?'
|
_VALID_URL = r'^https?://(?:(?:www\.)?ardmediathek\.de|mediathek\.(?:daserste|rbb-online)\.de|one\.ard\.de)/(?:.*/)(?P<video_id>[0-9]+|[^0-9][^/\?]+)[^/\?]*(?:\?.*)?'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.ardmediathek.de/tv/Dokumentation-und-Reportage/Ich-liebe-das-Leben-trotzdem/rbb-Fernsehen/Video?documentId=29582122&bcastId=3822114',
|
# available till 26.07.2022
|
||||||
|
'url': 'http://www.ardmediathek.de/tv/S%C3%9CDLICHT/Was-ist-die-Kunst-der-Zukunft-liebe-Ann/BR-Fernsehen/Video?bcastId=34633636&documentId=44726822',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '29582122',
|
'id': '44726822',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Ich liebe das Leben trotzdem',
|
'title': 'Was ist die Kunst der Zukunft, liebe Anna McCarthy?',
|
||||||
'description': 'md5:45e4c225c72b27993314b31a84a5261c',
|
'description': 'md5:4ada28b3e3b5df01647310e41f3a62f5',
|
||||||
'duration': 4557,
|
'duration': 1740,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
}
|
||||||
'skip': 'HTTP Error 404: Not Found',
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.ardmediathek.de/tv/Tatort/Tatort-Scheinwelten-H%C3%B6rfassung-Video/Das-Erste/Video?documentId=29522730&bcastId=602916',
|
'url': 'https://one.ard.de/tv/Mord-mit-Aussicht/Mord-mit-Aussicht-6-39-T%C3%B6dliche-Nach/ONE/Video?bcastId=46384294&documentId=55586872',
|
||||||
'md5': 'f4d98b10759ac06c0072bbcd1f0b9e3e',
|
'only_matching': True,
|
||||||
'info_dict': {
|
|
||||||
'id': '29522730',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Tatort: Scheinwelten - Hörfassung (Video tgl. ab 20 Uhr)',
|
|
||||||
'description': 'md5:196392e79876d0ac94c94e8cdb2875f1',
|
|
||||||
'duration': 5252,
|
|
||||||
},
|
|
||||||
'skip': 'HTTP Error 404: Not Found',
|
|
||||||
}, {
|
}, {
|
||||||
# audio
|
# audio
|
||||||
'url': 'http://www.ardmediathek.de/tv/WDR-H%C3%B6rspiel-Speicher/Tod-eines-Fu%C3%9Fballers/WDR-3/Audio-Podcast?documentId=28488308&bcastId=23074086',
|
'url': 'http://www.ardmediathek.de/tv/WDR-H%C3%B6rspiel-Speicher/Tod-eines-Fu%C3%9Fballers/WDR-3/Audio-Podcast?documentId=28488308&bcastId=23074086',
|
||||||
'md5': '219d94d8980b4f538c7fcb0865eb7f2c',
|
'only_matching': True,
|
||||||
'info_dict': {
|
|
||||||
'id': '28488308',
|
|
||||||
'ext': 'mp3',
|
|
||||||
'title': 'Tod eines Fußballers',
|
|
||||||
'description': 'md5:f6e39f3461f0e1f54bfa48c8875c86ef',
|
|
||||||
'duration': 3240,
|
|
||||||
},
|
|
||||||
'skip': 'HTTP Error 404: Not Found',
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://mediathek.daserste.de/sendungen_a-z/328454_anne-will/22429276_vertrauen-ist-gut-spionieren-ist-besser-geht',
|
'url': 'http://mediathek.daserste.de/sendungen_a-z/328454_anne-will/22429276_vertrauen-ist-gut-spionieren-ist-besser-geht',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}, {
|
}, {
|
||||||
# audio
|
# audio
|
||||||
'url': 'http://mediathek.rbb-online.de/radio/Hörspiel/Vor-dem-Fest/kulturradio/Audio?documentId=30796318&topRessort=radio&bcastId=9839158',
|
'url': 'http://mediathek.rbb-online.de/radio/Hörspiel/Vor-dem-Fest/kulturradio/Audio?documentId=30796318&topRessort=radio&bcastId=9839158',
|
||||||
'md5': '4e8f00631aac0395fee17368ac0e9867',
|
'only_matching': True,
|
||||||
'info_dict': {
|
|
||||||
'id': '30796318',
|
|
||||||
'ext': 'mp3',
|
|
||||||
'title': 'Vor dem Fest',
|
|
||||||
'description': 'md5:c0c1c8048514deaed2a73b3a60eecacb',
|
|
||||||
'duration': 3287,
|
|
||||||
},
|
|
||||||
'skip': 'Video is no longer available',
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _extract_media_info(self, media_info_url, webpage, video_id):
|
def _extract_media_info(self, media_info_url, webpage, video_id):
|
||||||
@ -126,6 +103,8 @@ class ARDMediathekIE(InfoExtractor):
|
|||||||
quality = stream.get('_quality')
|
quality = stream.get('_quality')
|
||||||
server = stream.get('_server')
|
server = stream.get('_server')
|
||||||
for stream_url in stream_urls:
|
for stream_url in stream_urls:
|
||||||
|
if not url_or_none(stream_url):
|
||||||
|
continue
|
||||||
ext = determine_ext(stream_url)
|
ext = determine_ext(stream_url)
|
||||||
if quality != 'auto' and ext in ('f4m', 'm3u8'):
|
if quality != 'auto' and ext in ('f4m', 'm3u8'):
|
||||||
continue
|
continue
|
||||||
@ -146,13 +125,11 @@ class ARDMediathekIE(InfoExtractor):
|
|||||||
'play_path': stream_url,
|
'play_path': stream_url,
|
||||||
'format_id': 'a%s-rtmp-%s' % (num, quality),
|
'format_id': 'a%s-rtmp-%s' % (num, quality),
|
||||||
}
|
}
|
||||||
elif stream_url.startswith('http'):
|
else:
|
||||||
f = {
|
f = {
|
||||||
'url': stream_url,
|
'url': stream_url,
|
||||||
'format_id': 'a%s-%s-%s' % (num, ext, quality)
|
'format_id': 'a%s-%s-%s' % (num, ext, quality)
|
||||||
}
|
}
|
||||||
else:
|
|
||||||
continue
|
|
||||||
m = re.search(r'_(?P<width>\d+)x(?P<height>\d+)\.mp4$', stream_url)
|
m = re.search(r'_(?P<width>\d+)x(?P<height>\d+)\.mp4$', stream_url)
|
||||||
if m:
|
if m:
|
||||||
f.update({
|
f.update({
|
||||||
@ -251,20 +228,23 @@ class ARDMediathekIE(InfoExtractor):
|
|||||||
|
|
||||||
class ARDIE(InfoExtractor):
|
class ARDIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?P<mainurl>https?://(www\.)?daserste\.de/[^?#]+/videos/(?P<display_id>[^/?#]+)-(?P<id>[0-9]+))\.html'
|
_VALID_URL = r'(?P<mainurl>https?://(www\.)?daserste\.de/[^?#]+/videos/(?P<display_id>[^/?#]+)-(?P<id>[0-9]+))\.html'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'http://www.daserste.de/information/reportage-dokumentation/dokus/videos/die-story-im-ersten-mission-unter-falscher-flagge-100.html',
|
# available till 14.02.2019
|
||||||
'md5': 'd216c3a86493f9322545e045ddc3eb35',
|
'url': 'http://www.daserste.de/information/talk/maischberger/videos/das-groko-drama-zerlegen-sich-die-volksparteien-video-102.html',
|
||||||
|
'md5': '8e4ec85f31be7c7fc08a26cdbc5a1f49',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'display_id': 'die-story-im-ersten-mission-unter-falscher-flagge',
|
'display_id': 'das-groko-drama-zerlegen-sich-die-volksparteien-video',
|
||||||
'id': '100',
|
'id': '102',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'duration': 2600,
|
'duration': 4435.0,
|
||||||
'title': 'Die Story im Ersten: Mission unter falscher Flagge',
|
'title': 'Das GroKo-Drama: Zerlegen sich die Volksparteien?',
|
||||||
'upload_date': '20140804',
|
'upload_date': '20180214',
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
},
|
},
|
||||||
'skip': 'HTTP Error 404: Not Found',
|
}, {
|
||||||
}
|
'url': 'http://www.daserste.de/information/reportage-dokumentation/dokus/videos/die-story-im-ersten-mission-unter-falscher-flagge-100.html',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
@ -305,3 +285,76 @@ class ARDIE(InfoExtractor):
|
|||||||
'upload_date': upload_date,
|
'upload_date': upload_date,
|
||||||
'thumbnail': thumbnail,
|
'thumbnail': thumbnail,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ARDBetaMediathekIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https://beta\.ardmediathek\.de/[a-z]+/player/(?P<video_id>[a-zA-Z0-9]+)/(?P<display_id>[^/?#]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://beta.ardmediathek.de/ard/player/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE/die-robuste-roswita',
|
||||||
|
'md5': '2d02d996156ea3c397cfc5036b5d7f8f',
|
||||||
|
'info_dict': {
|
||||||
|
'display_id': 'die-robuste-roswita',
|
||||||
|
'id': 'Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
||||||
|
'title': 'Tatort: Die robuste Roswita',
|
||||||
|
'description': r're:^Der Mord.*trüber ist als die Ilm.',
|
||||||
|
'duration': 5316,
|
||||||
|
'thumbnail': 'https://img.ardmediathek.de/standard/00/55/43/59/34/-1774185891/16x9/960?mandant=ard',
|
||||||
|
'upload_date': '20180826',
|
||||||
|
'ext': 'mp4',
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
video_id = mobj.group('video_id')
|
||||||
|
display_id = mobj.group('display_id')
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
data_json = self._search_regex(r'window\.__APOLLO_STATE__\s*=\s*(\{.*);\n', webpage, 'json')
|
||||||
|
data = self._parse_json(data_json, display_id)
|
||||||
|
|
||||||
|
res = {
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
}
|
||||||
|
formats = []
|
||||||
|
for widget in data.values():
|
||||||
|
if widget.get('_geoblocked'):
|
||||||
|
raise ExtractorError('This video is not available due to geoblocking', expected=True)
|
||||||
|
|
||||||
|
if '_duration' in widget:
|
||||||
|
res['duration'] = widget['_duration']
|
||||||
|
if 'clipTitle' in widget:
|
||||||
|
res['title'] = widget['clipTitle']
|
||||||
|
if '_previewImage' in widget:
|
||||||
|
res['thumbnail'] = widget['_previewImage']
|
||||||
|
if 'broadcastedOn' in widget:
|
||||||
|
res['upload_date'] = unified_strdate(widget['broadcastedOn'])
|
||||||
|
if 'synopsis' in widget:
|
||||||
|
res['description'] = widget['synopsis']
|
||||||
|
if '_subtitleUrl' in widget:
|
||||||
|
res['subtitles'] = {'de': [{
|
||||||
|
'ext': 'ttml',
|
||||||
|
'url': widget['_subtitleUrl'],
|
||||||
|
}]}
|
||||||
|
if '_quality' in widget:
|
||||||
|
format_url = widget['_stream']['json'][0]
|
||||||
|
|
||||||
|
if format_url.endswith('.f4m'):
|
||||||
|
formats.extend(self._extract_f4m_formats(
|
||||||
|
format_url + '?hdcore=3.11.0',
|
||||||
|
video_id, f4m_id='hds', fatal=False))
|
||||||
|
elif format_url.endswith('m3u8'):
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
format_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
|
||||||
|
else:
|
||||||
|
formats.append({
|
||||||
|
'format_id': 'http-' + widget['_quality'],
|
||||||
|
'url': format_url,
|
||||||
|
'preference': 10, # Plain HTTP, that's nice
|
||||||
|
})
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
res['formats'] = formats
|
||||||
|
|
||||||
|
return res
|
||||||
|
@ -8,7 +8,6 @@ from .kaltura import KalturaIE
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
remove_end,
|
remove_end,
|
||||||
urlencode_postdata,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -34,19 +33,40 @@ class AsianCrushIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
data = self._download_json(
|
webpage = self._download_webpage(url, video_id)
|
||||||
'https://www.asiancrush.com/wp-admin/admin-ajax.php', video_id,
|
|
||||||
data=urlencode_postdata({
|
|
||||||
'postid': video_id,
|
|
||||||
'action': 'get_channel_kaltura_vars',
|
|
||||||
}))
|
|
||||||
|
|
||||||
entry_id = data['entry_id']
|
entry_id, partner_id, title = [None] * 3
|
||||||
|
|
||||||
|
vars = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'iEmbedVars\s*=\s*({.+?})', webpage, 'embed vars',
|
||||||
|
default='{}'), video_id, fatal=False)
|
||||||
|
if vars:
|
||||||
|
entry_id = vars.get('entry_id')
|
||||||
|
partner_id = vars.get('partner_id')
|
||||||
|
title = vars.get('vid_label')
|
||||||
|
|
||||||
|
if not entry_id:
|
||||||
|
entry_id = self._search_regex(
|
||||||
|
r'\bentry_id["\']\s*:\s*["\'](\d+)', webpage, 'entry id')
|
||||||
|
|
||||||
|
player = self._download_webpage(
|
||||||
|
'https://api.asiancrush.com/embeddedVideoPlayer', video_id,
|
||||||
|
query={'id': entry_id})
|
||||||
|
|
||||||
|
kaltura_id = self._search_regex(
|
||||||
|
r'entry_id["\']\s*:\s*(["\'])(?P<id>(?:(?!\1).)+)\1', player,
|
||||||
|
'kaltura id', group='id')
|
||||||
|
|
||||||
|
if not partner_id:
|
||||||
|
partner_id = self._search_regex(
|
||||||
|
r'/p(?:artner_id)?/(\d+)', player, 'partner id',
|
||||||
|
default='513551')
|
||||||
|
|
||||||
return self.url_result(
|
return self.url_result(
|
||||||
'kaltura:%s:%s' % (data['partner_id'], entry_id),
|
'kaltura:%s:%s' % (partner_id, kaltura_id),
|
||||||
ie=KalturaIE.ie_key(), video_id=entry_id,
|
ie=KalturaIE.ie_key(), video_id=kaltura_id,
|
||||||
video_title=data.get('vid_label'))
|
video_title=title)
|
||||||
|
|
||||||
|
|
||||||
class AsianCrushPlaylistIE(InfoExtractor):
|
class AsianCrushPlaylistIE(InfoExtractor):
|
||||||
|
@ -74,7 +74,7 @@ class AtresPlayerIE(InfoExtractor):
|
|||||||
self._login()
|
self._login()
|
||||||
|
|
||||||
def _login(self):
|
def _login(self):
|
||||||
(username, password) = self._get_login_info()
|
username, password = self._get_login_info()
|
||||||
if username is None:
|
if username is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -5,13 +5,12 @@ from .common import InfoExtractor
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
sanitized_Request,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AudiMediaIE(InfoExtractor):
|
class AudiMediaIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?audi-mediacenter\.com/(?:en|de)/audimediatv/(?P<id>[^/?#]+)'
|
_VALID_URL = r'https?://(?:www\.)?audi-mediacenter\.com/(?:en|de)/audimediatv/(?:video/)?(?P<id>[^/?#]+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'https://www.audi-mediacenter.com/en/audimediatv/60-seconds-of-audi-sport-104-2015-wec-bahrain-rookie-test-1467',
|
'url': 'https://www.audi-mediacenter.com/en/audimediatv/60-seconds-of-audi-sport-104-2015-wec-bahrain-rookie-test-1467',
|
||||||
'md5': '79a8b71c46d49042609795ab59779b66',
|
'md5': '79a8b71c46d49042609795ab59779b66',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -24,41 +23,46 @@ class AudiMediaIE(InfoExtractor):
|
|||||||
'duration': 74022,
|
'duration': 74022,
|
||||||
'view_count': int,
|
'view_count': int,
|
||||||
}
|
}
|
||||||
}
|
}, {
|
||||||
# extracted from https://audimedia.tv/assets/embed/embedded-player.js (dataSourceAuthToken)
|
'url': 'https://www.audi-mediacenter.com/en/audimediatv/video/60-seconds-of-audi-sport-104-2015-wec-bahrain-rookie-test-2991',
|
||||||
_AUTH_TOKEN = 'e25b42847dba18c6c8816d5d8ce94c326e06823ebf0859ed164b3ba169be97f2'
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
raw_payload = self._search_regex([
|
raw_payload = self._search_regex([
|
||||||
r'class="amtv-embed"[^>]+id="([^"]+)"',
|
r'class="amtv-embed"[^>]+id="([0-9a-z-]+)"',
|
||||||
r'class=\\"amtv-embed\\"[^>]+id=\\"([^"]+)\\"',
|
r'id="([0-9a-z-]+)"[^>]+class="amtv-embed"',
|
||||||
|
r'class=\\"amtv-embed\\"[^>]+id=\\"([0-9a-z-]+)\\"',
|
||||||
|
r'id=\\"([0-9a-z-]+)\\"[^>]+class=\\"amtv-embed\\"',
|
||||||
|
r'id=(?:\\)?"(amtve-[a-z]-\d+-[a-z]{2})',
|
||||||
], webpage, 'raw payload')
|
], webpage, 'raw payload')
|
||||||
_, stage_mode, video_id, lang = raw_payload.split('-')
|
_, stage_mode, video_id, _ = raw_payload.split('-')
|
||||||
|
|
||||||
# TODO: handle s and e stage_mode (live streams and ended live streams)
|
# TODO: handle s and e stage_mode (live streams and ended live streams)
|
||||||
if stage_mode not in ('s', 'e'):
|
if stage_mode not in ('s', 'e'):
|
||||||
request = sanitized_Request(
|
video_data = self._download_json(
|
||||||
'https://audimedia.tv/api/video/v1/videos/%s?embed[]=video_versions&embed[]=thumbnail_image&where[content_language_iso]=%s' % (video_id, lang),
|
'https://www.audimedia.tv/api/video/v1/videos/' + video_id,
|
||||||
headers={'X-Auth-Token': self._AUTH_TOKEN})
|
video_id, query={
|
||||||
json_data = self._download_json(request, video_id)['results']
|
'embed[]': ['video_versions', 'thumbnail_image'],
|
||||||
|
})['results']
|
||||||
formats = []
|
formats = []
|
||||||
|
|
||||||
stream_url_hls = json_data.get('stream_url_hls')
|
stream_url_hls = video_data.get('stream_url_hls')
|
||||||
if stream_url_hls:
|
if stream_url_hls:
|
||||||
formats.extend(self._extract_m3u8_formats(
|
formats.extend(self._extract_m3u8_formats(
|
||||||
stream_url_hls, video_id, 'mp4',
|
stream_url_hls, video_id, 'mp4',
|
||||||
entry_protocol='m3u8_native', m3u8_id='hls', fatal=False))
|
entry_protocol='m3u8_native', m3u8_id='hls', fatal=False))
|
||||||
|
|
||||||
stream_url_hds = json_data.get('stream_url_hds')
|
stream_url_hds = video_data.get('stream_url_hds')
|
||||||
if stream_url_hds:
|
if stream_url_hds:
|
||||||
formats.extend(self._extract_f4m_formats(
|
formats.extend(self._extract_f4m_formats(
|
||||||
stream_url_hds + '?hdcore=3.4.0',
|
stream_url_hds + '?hdcore=3.4.0',
|
||||||
video_id, f4m_id='hds', fatal=False))
|
video_id, f4m_id='hds', fatal=False))
|
||||||
|
|
||||||
for video_version in json_data.get('video_versions'):
|
for video_version in video_data.get('video_versions', []):
|
||||||
video_version_url = video_version.get('download_url') or video_version.get('stream_url')
|
video_version_url = video_version.get('download_url') or video_version.get('stream_url')
|
||||||
if not video_version_url:
|
if not video_version_url:
|
||||||
continue
|
continue
|
||||||
@ -79,11 +83,11 @@ class AudiMediaIE(InfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': json_data['title'],
|
'title': video_data['title'],
|
||||||
'description': json_data.get('subtitle'),
|
'description': video_data.get('subtitle'),
|
||||||
'thumbnail': json_data.get('thumbnail_image', {}).get('file'),
|
'thumbnail': video_data.get('thumbnail_image', {}).get('file'),
|
||||||
'timestamp': parse_iso8601(json_data.get('publication_date')),
|
'timestamp': parse_iso8601(video_data.get('publication_date')),
|
||||||
'duration': int_or_none(json_data.get('duration')),
|
'duration': int_or_none(video_data.get('duration')),
|
||||||
'view_count': int_or_none(json_data.get('view_count')),
|
'view_count': int_or_none(video_data.get('view_count')),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ class AudiomackIE(InfoExtractor):
|
|||||||
return {'_type': 'url', 'url': api_response['url'], 'ie_key': 'Soundcloud'}
|
return {'_type': 'url', 'url': api_response['url'], 'ie_key': 'Soundcloud'}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': api_response.get('id', album_url_tag),
|
'id': compat_str(api_response.get('id', album_url_tag)),
|
||||||
'uploader': api_response.get('artist'),
|
'uploader': api_response.get('artist'),
|
||||||
'title': api_response.get('title'),
|
'title': api_response.get('title'),
|
||||||
'url': api_response['url'],
|
'url': api_response['url'],
|
||||||
|
78
youtube_dl/extractor/aws.py
Normal file
78
youtube_dl/extractor/aws.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_urllib_parse_urlencode
|
||||||
|
|
||||||
|
|
||||||
|
class AWSIE(InfoExtractor):
|
||||||
|
_AWS_ALGORITHM = 'AWS4-HMAC-SHA256'
|
||||||
|
_AWS_REGION = 'us-east-1'
|
||||||
|
|
||||||
|
def _aws_execute_api(self, aws_dict, video_id, query=None):
|
||||||
|
query = query or {}
|
||||||
|
amz_date = datetime.datetime.utcnow().strftime('%Y%m%dT%H%M%SZ')
|
||||||
|
date = amz_date[:8]
|
||||||
|
headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Host': self._AWS_PROXY_HOST,
|
||||||
|
'X-Amz-Date': amz_date,
|
||||||
|
'X-Api-Key': self._AWS_API_KEY
|
||||||
|
}
|
||||||
|
session_token = aws_dict.get('session_token')
|
||||||
|
if session_token:
|
||||||
|
headers['X-Amz-Security-Token'] = session_token
|
||||||
|
|
||||||
|
def aws_hash(s):
|
||||||
|
return hashlib.sha256(s.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
# Task 1: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||||
|
canonical_querystring = compat_urllib_parse_urlencode(query)
|
||||||
|
canonical_headers = ''
|
||||||
|
for header_name, header_value in sorted(headers.items()):
|
||||||
|
canonical_headers += '%s:%s\n' % (header_name.lower(), header_value)
|
||||||
|
signed_headers = ';'.join([header.lower() for header in sorted(headers.keys())])
|
||||||
|
canonical_request = '\n'.join([
|
||||||
|
'GET',
|
||||||
|
aws_dict['uri'],
|
||||||
|
canonical_querystring,
|
||||||
|
canonical_headers,
|
||||||
|
signed_headers,
|
||||||
|
aws_hash('')
|
||||||
|
])
|
||||||
|
|
||||||
|
# Task 2: http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
||||||
|
credential_scope_list = [date, self._AWS_REGION, 'execute-api', 'aws4_request']
|
||||||
|
credential_scope = '/'.join(credential_scope_list)
|
||||||
|
string_to_sign = '\n'.join([self._AWS_ALGORITHM, amz_date, credential_scope, aws_hash(canonical_request)])
|
||||||
|
|
||||||
|
# Task 3: http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
|
||||||
|
def aws_hmac(key, msg):
|
||||||
|
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256)
|
||||||
|
|
||||||
|
def aws_hmac_digest(key, msg):
|
||||||
|
return aws_hmac(key, msg).digest()
|
||||||
|
|
||||||
|
def aws_hmac_hexdigest(key, msg):
|
||||||
|
return aws_hmac(key, msg).hexdigest()
|
||||||
|
|
||||||
|
k_signing = ('AWS4' + aws_dict['secret_key']).encode('utf-8')
|
||||||
|
for value in credential_scope_list:
|
||||||
|
k_signing = aws_hmac_digest(k_signing, value)
|
||||||
|
|
||||||
|
signature = aws_hmac_hexdigest(k_signing, string_to_sign)
|
||||||
|
|
||||||
|
# Task 4: http://docs.aws.amazon.com/general/latest/gr/sigv4-add-signature-to-request.html
|
||||||
|
headers['Authorization'] = ', '.join([
|
||||||
|
'%s Credential=%s/%s' % (self._AWS_ALGORITHM, aws_dict['access_key'], credential_scope),
|
||||||
|
'SignedHeaders=%s' % signed_headers,
|
||||||
|
'Signature=%s' % signature,
|
||||||
|
])
|
||||||
|
|
||||||
|
return self._download_json(
|
||||||
|
'https://%s%s%s' % (self._AWS_PROXY_HOST, aws_dict['uri'], '?' + canonical_querystring if canonical_querystring else ''),
|
||||||
|
video_id, headers=headers)
|
@ -44,7 +44,7 @@ class BambuserIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _login(self):
|
def _login(self):
|
||||||
(username, password) = self._get_login_info()
|
username, password = self._get_login_info()
|
||||||
if username is None:
|
if username is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@ -16,14 +15,18 @@ from ..utils import (
|
|||||||
int_or_none,
|
int_or_none,
|
||||||
KNOWN_EXTENSIONS,
|
KNOWN_EXTENSIONS,
|
||||||
parse_filesize,
|
parse_filesize,
|
||||||
|
str_or_none,
|
||||||
|
try_get,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
update_url_query,
|
update_url_query,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
|
unified_timestamp,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BandcampIE(InfoExtractor):
|
class BandcampIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://.*?\.bandcamp\.com/track/(?P<title>[^/?#&]+)'
|
_VALID_URL = r'https?://[^/]+\.bandcamp\.com/track/(?P<title>[^/?#&]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://youtube-dl.bandcamp.com/track/youtube-dl-test-song',
|
'url': 'http://youtube-dl.bandcamp.com/track/youtube-dl-test-song',
|
||||||
'md5': 'c557841d5e50261777a6585648adf439',
|
'md5': 'c557841d5e50261777a6585648adf439',
|
||||||
@ -35,13 +38,44 @@ class BandcampIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
'_skip': 'There is a limit of 200 free downloads / month for the test song'
|
'_skip': 'There is a limit of 200 free downloads / month for the test song'
|
||||||
}, {
|
}, {
|
||||||
|
# free download
|
||||||
'url': 'http://benprunty.bandcamp.com/track/lanius-battle',
|
'url': 'http://benprunty.bandcamp.com/track/lanius-battle',
|
||||||
'md5': '0369ace6b939f0927e62c67a1a8d9fa7',
|
'md5': '853e35bf34aa1d6fe2615ae612564b36',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2650410135',
|
'id': '2650410135',
|
||||||
'ext': 'aiff',
|
'ext': 'aiff',
|
||||||
'title': 'Ben Prunty - Lanius (Battle)',
|
'title': 'Ben Prunty - Lanius (Battle)',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'uploader': 'Ben Prunty',
|
'uploader': 'Ben Prunty',
|
||||||
|
'timestamp': 1396508491,
|
||||||
|
'upload_date': '20140403',
|
||||||
|
'release_date': '20140403',
|
||||||
|
'duration': 260.877,
|
||||||
|
'track': 'Lanius (Battle)',
|
||||||
|
'track_number': 1,
|
||||||
|
'track_id': '2650410135',
|
||||||
|
'artist': 'Ben Prunty',
|
||||||
|
'album': 'FTL: Advanced Edition Soundtrack',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# no free download, mp3 128
|
||||||
|
'url': 'https://relapsealumni.bandcamp.com/track/hail-to-fire',
|
||||||
|
'md5': 'fec12ff55e804bb7f7ebeb77a800c8b7',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '2584466013',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'Mastodon - Hail to Fire',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'uploader': 'Mastodon',
|
||||||
|
'timestamp': 1322005399,
|
||||||
|
'upload_date': '20111122',
|
||||||
|
'release_date': '20040207',
|
||||||
|
'duration': 120.79,
|
||||||
|
'track': 'Hail to Fire',
|
||||||
|
'track_number': 5,
|
||||||
|
'track_id': '2584466013',
|
||||||
|
'artist': 'Mastodon',
|
||||||
|
'album': 'Call of the Mastodon',
|
||||||
},
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@ -50,19 +84,23 @@ class BandcampIE(InfoExtractor):
|
|||||||
title = mobj.group('title')
|
title = mobj.group('title')
|
||||||
webpage = self._download_webpage(url, title)
|
webpage = self._download_webpage(url, title)
|
||||||
thumbnail = self._html_search_meta('og:image', webpage, default=None)
|
thumbnail = self._html_search_meta('og:image', webpage, default=None)
|
||||||
m_download = re.search(r'freeDownloadPage: "(.*?)"', webpage)
|
|
||||||
if not m_download:
|
|
||||||
m_trackinfo = re.search(r'trackinfo: (.+),\s*?\n', webpage)
|
|
||||||
if m_trackinfo:
|
|
||||||
json_code = m_trackinfo.group(1)
|
|
||||||
data = json.loads(json_code)[0]
|
|
||||||
track_id = compat_str(data['id'])
|
|
||||||
|
|
||||||
if not data.get('file'):
|
track_id = None
|
||||||
raise ExtractorError('Not streamable', video_id=track_id, expected=True)
|
track = None
|
||||||
|
track_number = None
|
||||||
|
duration = None
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for format_id, format_url in data['file'].items():
|
track_info = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'trackinfo\s*:\s*\[\s*({.+?})\s*\]\s*,\s*?\n',
|
||||||
|
webpage, 'track info', default='{}'), title)
|
||||||
|
if track_info:
|
||||||
|
file_ = track_info.get('file')
|
||||||
|
if isinstance(file_, dict):
|
||||||
|
for format_id, format_url in file_.items():
|
||||||
|
if not url_or_none(format_url):
|
||||||
|
continue
|
||||||
ext, abr_str = format_id.split('-', 1)
|
ext, abr_str = format_id.split('-', 1)
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
@ -72,48 +110,60 @@ class BandcampIE(InfoExtractor):
|
|||||||
'acodec': ext,
|
'acodec': ext,
|
||||||
'abr': int_or_none(abr_str),
|
'abr': int_or_none(abr_str),
|
||||||
})
|
})
|
||||||
|
track = track_info.get('title')
|
||||||
|
track_id = str_or_none(track_info.get('track_id') or track_info.get('id'))
|
||||||
|
track_number = int_or_none(track_info.get('track_num'))
|
||||||
|
duration = float_or_none(track_info.get('duration'))
|
||||||
|
|
||||||
self._sort_formats(formats)
|
def extract(key):
|
||||||
|
return self._search_regex(
|
||||||
|
r'\b%s\s*["\']?\s*:\s*(["\'])(?P<value>(?:(?!\1).)+)\1' % key,
|
||||||
|
webpage, key, default=None, group='value')
|
||||||
|
|
||||||
return {
|
artist = extract('artist')
|
||||||
'id': track_id,
|
album = extract('album_title')
|
||||||
'title': data['title'],
|
timestamp = unified_timestamp(
|
||||||
'thumbnail': thumbnail,
|
extract('publish_date') or extract('album_publish_date'))
|
||||||
'formats': formats,
|
release_date = unified_strdate(extract('album_release_date'))
|
||||||
'duration': float_or_none(data.get('duration')),
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
raise ExtractorError('No free songs found')
|
|
||||||
|
|
||||||
download_link = m_download.group(1)
|
download_link = self._search_regex(
|
||||||
video_id = self._search_regex(
|
r'freeDownloadPage\s*:\s*(["\'])(?P<url>(?:(?!\1).)+)\1', webpage,
|
||||||
|
'download link', default=None, group='url')
|
||||||
|
if download_link:
|
||||||
|
track_id = self._search_regex(
|
||||||
r'(?ms)var TralbumData = .*?[{,]\s*id: (?P<id>\d+),?$',
|
r'(?ms)var TralbumData = .*?[{,]\s*id: (?P<id>\d+),?$',
|
||||||
webpage, 'video id')
|
webpage, 'track id')
|
||||||
|
|
||||||
download_webpage = self._download_webpage(
|
download_webpage = self._download_webpage(
|
||||||
download_link, video_id, 'Downloading free downloads page')
|
download_link, track_id, 'Downloading free downloads page')
|
||||||
|
|
||||||
blob = self._parse_json(
|
blob = self._parse_json(
|
||||||
self._search_regex(
|
self._search_regex(
|
||||||
r'data-blob=(["\'])(?P<blob>{.+?})\1', download_webpage,
|
r'data-blob=(["\'])(?P<blob>{.+?})\1', download_webpage,
|
||||||
'blob', group='blob'),
|
'blob', group='blob'),
|
||||||
video_id, transform_source=unescapeHTML)
|
track_id, transform_source=unescapeHTML)
|
||||||
|
|
||||||
info = blob['digital_items'][0]
|
|
||||||
|
|
||||||
downloads = info['downloads']
|
|
||||||
track = info['title']
|
|
||||||
|
|
||||||
|
info = try_get(
|
||||||
|
blob, (lambda x: x['digital_items'][0],
|
||||||
|
lambda x: x['download_items'][0]), dict)
|
||||||
|
if info:
|
||||||
|
downloads = info.get('downloads')
|
||||||
|
if isinstance(downloads, dict):
|
||||||
|
if not track:
|
||||||
|
track = info.get('title')
|
||||||
|
if not artist:
|
||||||
artist = info.get('artist')
|
artist = info.get('artist')
|
||||||
title = '%s - %s' % (artist, track) if artist else track
|
if not thumbnail:
|
||||||
|
thumbnail = info.get('thumb_url')
|
||||||
|
|
||||||
download_formats = {}
|
download_formats = {}
|
||||||
|
download_formats_list = blob.get('download_formats')
|
||||||
|
if isinstance(download_formats_list, list):
|
||||||
for f in blob['download_formats']:
|
for f in blob['download_formats']:
|
||||||
name, ext = f.get('name'), f.get('file_extension')
|
name, ext = f.get('name'), f.get('file_extension')
|
||||||
if all(isinstance(x, compat_str) for x in (name, ext)):
|
if all(isinstance(x, compat_str) for x in (name, ext)):
|
||||||
download_formats[name] = ext.strip('.')
|
download_formats[name] = ext.strip('.')
|
||||||
|
|
||||||
formats = []
|
|
||||||
for format_id, f in downloads.items():
|
for format_id, f in downloads.items():
|
||||||
format_url = f.get('url')
|
format_url = f.get('url')
|
||||||
if not format_url:
|
if not format_url:
|
||||||
@ -126,13 +176,13 @@ class BandcampIE(InfoExtractor):
|
|||||||
})
|
})
|
||||||
format_id = f.get('encoding_name') or format_id
|
format_id = f.get('encoding_name') or format_id
|
||||||
stat = self._download_json(
|
stat = self._download_json(
|
||||||
stat_url, video_id, 'Downloading %s JSON' % format_id,
|
stat_url, track_id, 'Downloading %s JSON' % format_id,
|
||||||
transform_source=lambda s: s[s.index('{'):s.rindex('}') + 1],
|
transform_source=lambda s: s[s.index('{'):s.rindex('}') + 1],
|
||||||
fatal=False)
|
fatal=False)
|
||||||
if not stat:
|
if not stat:
|
||||||
continue
|
continue
|
||||||
retry_url = stat.get('retry_url')
|
retry_url = url_or_none(stat.get('retry_url'))
|
||||||
if not isinstance(retry_url, compat_str):
|
if not retry_url:
|
||||||
continue
|
continue
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': self._proto_relative_url(retry_url, 'http:'),
|
'url': self._proto_relative_url(retry_url, 'http:'),
|
||||||
@ -142,15 +192,28 @@ class BandcampIE(InfoExtractor):
|
|||||||
'filesize': parse_filesize(f.get('size_mb')),
|
'filesize': parse_filesize(f.get('size_mb')),
|
||||||
'vcodec': 'none',
|
'vcodec': 'none',
|
||||||
})
|
})
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
title = '%s - %s' % (artist, track) if artist else track
|
||||||
|
|
||||||
|
if not duration:
|
||||||
|
duration = float_or_none(self._html_search_meta(
|
||||||
|
'duration', webpage, default=None))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': track_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'thumbnail': info.get('thumb_url') or thumbnail,
|
'thumbnail': thumbnail,
|
||||||
'uploader': info.get('artist'),
|
'uploader': artist,
|
||||||
'artist': artist,
|
'timestamp': timestamp,
|
||||||
|
'release_date': release_date,
|
||||||
|
'duration': duration,
|
||||||
'track': track,
|
'track': track,
|
||||||
|
'track_number': track_number,
|
||||||
|
'track_id': track_id,
|
||||||
|
'artist': artist,
|
||||||
|
'album': album,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,7 +369,7 @@ class BandcampWeeklyIE(InfoExtractor):
|
|||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for format_id, format_url in show['audio_stream'].items():
|
for format_id, format_url in show['audio_stream'].items():
|
||||||
if not isinstance(format_url, compat_str):
|
if not url_or_none(format_url):
|
||||||
continue
|
continue
|
||||||
for known_ext in KNOWN_EXTENSIONS:
|
for known_ext in KNOWN_EXTENSIONS:
|
||||||
if known_ext in format_id:
|
if known_ext in format_id:
|
||||||
|
@ -12,6 +12,7 @@ from ..utils import (
|
|||||||
float_or_none,
|
float_or_none,
|
||||||
get_element_by_class,
|
get_element_by_class,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
js_to_json,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
try_get,
|
try_get,
|
||||||
@ -20,7 +21,6 @@ from ..utils import (
|
|||||||
urljoin,
|
urljoin,
|
||||||
)
|
)
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
compat_etree_fromstring,
|
|
||||||
compat_HTTPError,
|
compat_HTTPError,
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
)
|
)
|
||||||
@ -29,7 +29,7 @@ from ..compat import (
|
|||||||
class BBCCoUkIE(InfoExtractor):
|
class BBCCoUkIE(InfoExtractor):
|
||||||
IE_NAME = 'bbc.co.uk'
|
IE_NAME = 'bbc.co.uk'
|
||||||
IE_DESC = 'BBC iPlayer'
|
IE_DESC = 'BBC iPlayer'
|
||||||
_ID_REGEX = r'[pbw][\da-z]{7}'
|
_ID_REGEX = r'(?:[pbm][\da-z]{7}|w[\da-z]{7,14})'
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
https?://
|
https?://
|
||||||
(?:www\.)?bbc\.co\.uk/
|
(?:www\.)?bbc\.co\.uk/
|
||||||
@ -236,6 +236,12 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://www.bbc.co.uk/programmes/w3csv1y9',
|
'url': 'http://www.bbc.co.uk/programmes/w3csv1y9',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bbc.co.uk/programmes/m00005xn',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bbc.co.uk/programmes/w172w4dww1jqt5s',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_USP_RE = r'/([^/]+?)\.ism(?:\.hlsv2\.ism)?/[^/]+\.m3u8'
|
_USP_RE = r'/([^/]+?)\.ism(?:\.hlsv2\.ism)?/[^/]+\.m3u8'
|
||||||
@ -333,14 +339,9 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
self._raise_extractor_error(last_exception)
|
self._raise_extractor_error(last_exception)
|
||||||
|
|
||||||
def _download_media_selector_url(self, url, programme_id=None):
|
def _download_media_selector_url(self, url, programme_id=None):
|
||||||
try:
|
|
||||||
media_selection = self._download_xml(
|
media_selection = self._download_xml(
|
||||||
url, programme_id, 'Downloading media selection XML')
|
url, programme_id, 'Downloading media selection XML',
|
||||||
except ExtractorError as ee:
|
expected_status=(403, 404))
|
||||||
if isinstance(ee.cause, compat_HTTPError) and ee.cause.code in (403, 404):
|
|
||||||
media_selection = compat_etree_fromstring(ee.cause.read().decode('utf-8'))
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
return self._process_media_selector(media_selection, programme_id)
|
return self._process_media_selector(media_selection, programme_id)
|
||||||
|
|
||||||
def _process_media_selector(self, media_selection, programme_id):
|
def _process_media_selector(self, media_selection, programme_id):
|
||||||
@ -386,7 +387,7 @@ class BBCCoUkIE(InfoExtractor):
|
|||||||
m3u8_id=format_id, fatal=False))
|
m3u8_id=format_id, fatal=False))
|
||||||
if re.search(self._USP_RE, href):
|
if re.search(self._USP_RE, href):
|
||||||
usp_formats = self._extract_m3u8_formats(
|
usp_formats = self._extract_m3u8_formats(
|
||||||
re.sub(self._USP_RE, r'/\1\.ism/\1\.m3u8', href),
|
re.sub(self._USP_RE, r'/\1.ism/\1.m3u8', href),
|
||||||
programme_id, ext='mp4', entry_protocol='m3u8_native',
|
programme_id, ext='mp4', entry_protocol='m3u8_native',
|
||||||
m3u8_id=format_id, fatal=False)
|
m3u8_id=format_id, fatal=False)
|
||||||
for f in usp_formats:
|
for f in usp_formats:
|
||||||
@ -772,6 +773,28 @@ class BBCIE(BBCCoUkIE):
|
|||||||
# single video article embedded with data-media-vpid
|
# single video article embedded with data-media-vpid
|
||||||
'url': 'http://www.bbc.co.uk/sport/rowing/35908187',
|
'url': 'http://www.bbc.co.uk/sport/rowing/35908187',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bbc.co.uk/bbcthree/clip/73d0bbd0-abc3-4cea-b3c0-cdae21905eb1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'p06556y7',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Transfers: Cristiano Ronaldo to Man Utd, Arsenal to spend?',
|
||||||
|
'description': 'md5:4b7dfd063d5a789a1512e99662be3ddd',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
# window.__PRELOADED_STATE__
|
||||||
|
'url': 'https://www.bbc.co.uk/radio/play/b0b9z4yl',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b0b9z4vz',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Prom 6: An American in Paris and Turangalila',
|
||||||
|
'description': 'md5:51cf7d6f5c8553f197e58203bc78dff8',
|
||||||
|
'uploader': 'Radio 3',
|
||||||
|
'uploader_id': 'bbc_radio_three',
|
||||||
|
},
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -994,6 +1017,66 @@ class BBCIE(BBCCoUkIE):
|
|||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
preload_state = self._parse_json(self._search_regex(
|
||||||
|
r'window\.__PRELOADED_STATE__\s*=\s*({.+?});', webpage,
|
||||||
|
'preload state', default='{}'), playlist_id, fatal=False)
|
||||||
|
if preload_state:
|
||||||
|
current_programme = preload_state.get('programmes', {}).get('current') or {}
|
||||||
|
programme_id = current_programme.get('id')
|
||||||
|
if current_programme and programme_id and current_programme.get('type') == 'playable_item':
|
||||||
|
title = current_programme.get('titles', {}).get('tertiary') or playlist_title
|
||||||
|
formats, subtitles = self._download_media_selector(programme_id)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
synopses = current_programme.get('synopses') or {}
|
||||||
|
network = current_programme.get('network') or {}
|
||||||
|
duration = int_or_none(
|
||||||
|
current_programme.get('duration', {}).get('value'))
|
||||||
|
thumbnail = None
|
||||||
|
image_url = current_programme.get('image_url')
|
||||||
|
if image_url:
|
||||||
|
thumbnail = image_url.replace('{recipe}', '1920x1920')
|
||||||
|
return {
|
||||||
|
'id': programme_id,
|
||||||
|
'title': title,
|
||||||
|
'description': dict_get(synopses, ('long', 'medium', 'short')),
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'duration': duration,
|
||||||
|
'uploader': network.get('short_title'),
|
||||||
|
'uploader_id': network.get('id'),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
}
|
||||||
|
|
||||||
|
bbc3_config = self._parse_json(
|
||||||
|
self._search_regex(
|
||||||
|
r'(?s)bbcthreeConfig\s*=\s*({.+?})\s*;\s*<', webpage,
|
||||||
|
'bbcthree config', default='{}'),
|
||||||
|
playlist_id, transform_source=js_to_json, fatal=False)
|
||||||
|
if bbc3_config:
|
||||||
|
bbc3_playlist = try_get(
|
||||||
|
bbc3_config, lambda x: x['payload']['content']['bbcMedia']['playlist'],
|
||||||
|
dict)
|
||||||
|
if bbc3_playlist:
|
||||||
|
playlist_title = bbc3_playlist.get('title') or playlist_title
|
||||||
|
thumbnail = bbc3_playlist.get('holdingImageURL')
|
||||||
|
entries = []
|
||||||
|
for bbc3_item in bbc3_playlist['items']:
|
||||||
|
programme_id = bbc3_item.get('versionID')
|
||||||
|
if not programme_id:
|
||||||
|
continue
|
||||||
|
formats, subtitles = self._download_media_selector(programme_id)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
entries.append({
|
||||||
|
'id': programme_id,
|
||||||
|
'title': playlist_title,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
})
|
||||||
|
return self.playlist_result(
|
||||||
|
entries, playlist_id, playlist_title, playlist_description)
|
||||||
|
|
||||||
def extract_all(pattern):
|
def extract_all(pattern):
|
||||||
return list(filter(None, map(
|
return list(filter(None, map(
|
||||||
lambda s: self._parse_json(s, playlist_id, fatal=False),
|
lambda s: self._parse_json(s, playlist_id, fatal=False),
|
||||||
|
@ -12,7 +12,7 @@ class BellMediaIE(InfoExtractor):
|
|||||||
(?:
|
(?:
|
||||||
ctv|
|
ctv|
|
||||||
tsn|
|
tsn|
|
||||||
bnn|
|
bnn(?:bloomberg)?|
|
||||||
thecomedynetwork|
|
thecomedynetwork|
|
||||||
discovery|
|
discovery|
|
||||||
discoveryvelocity|
|
discoveryvelocity|
|
||||||
@ -27,17 +27,16 @@ class BellMediaIE(InfoExtractor):
|
|||||||
much\.com
|
much\.com
|
||||||
)/.*?(?:\bvid(?:eoid)?=|-vid|~|%7E|/(?:episode)?)(?P<id>[0-9]{6,})'''
|
)/.*?(?:\bvid(?:eoid)?=|-vid|~|%7E|/(?:episode)?)(?P<id>[0-9]{6,})'''
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.ctv.ca/video/player?vid=706966',
|
'url': 'https://www.bnnbloomberg.ca/video/david-cockfield-s-top-picks~1403070',
|
||||||
'md5': 'ff2ebbeae0aa2dcc32a830c3fd69b7b0',
|
'md5': '36d3ef559cfe8af8efe15922cd3ce950',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '706966',
|
'id': '1403070',
|
||||||
'ext': 'mp4',
|
'ext': 'flv',
|
||||||
'title': 'Larry Day and Richard Jutras on the TIFF red carpet of \'Stonewall\'',
|
'title': 'David Cockfield\'s Top Picks',
|
||||||
'description': 'etalk catches up with Larry Day and Richard Jutras on the TIFF red carpet of "Stonewall”.',
|
'description': 'md5:810f7f8c6a83ad5b48677c3f8e5bb2c3',
|
||||||
'upload_date': '20150919',
|
'upload_date': '20180525',
|
||||||
'timestamp': 1442624700,
|
'timestamp': 1527288600,
|
||||||
},
|
},
|
||||||
'expected_warnings': ['HTTP Error 404'],
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.thecomedynetwork.ca/video/player?vid=923582',
|
'url': 'http://www.thecomedynetwork.ca/video/player?vid=923582',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
@ -70,6 +69,7 @@ class BellMediaIE(InfoExtractor):
|
|||||||
'investigationdiscovery': 'invdisc',
|
'investigationdiscovery': 'invdisc',
|
||||||
'animalplanet': 'aniplan',
|
'animalplanet': 'aniplan',
|
||||||
'etalk': 'ctv',
|
'etalk': 'ctv',
|
||||||
|
'bnnbloomberg': 'bnn',
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_urllib_parse_unquote
|
from ..compat import (
|
||||||
|
compat_b64decode,
|
||||||
|
compat_urllib_parse_unquote,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BigflixIE(InfoExtractor):
|
class BigflixIE(InfoExtractor):
|
||||||
@ -39,8 +41,8 @@ class BigflixIE(InfoExtractor):
|
|||||||
webpage, 'title')
|
webpage, 'title')
|
||||||
|
|
||||||
def decode_url(quoted_b64_url):
|
def decode_url(quoted_b64_url):
|
||||||
return base64.b64decode(compat_urllib_parse_unquote(
|
return compat_b64decode(compat_urllib_parse_unquote(
|
||||||
quoted_b64_url).encode('ascii')).decode('utf-8')
|
quoted_b64_url)).decode('utf-8')
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for height, encoded_url in re.findall(
|
for height, encoded_url in re.findall(
|
||||||
|
@ -27,14 +27,14 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.bilibili.tv/video/av1074402/',
|
'url': 'http://www.bilibili.tv/video/av1074402/',
|
||||||
'md5': '9fa226fe2b8a9a4d5a69b4c6a183417e',
|
'md5': '5f7d29e1a2872f3df0cf76b1f87d3788',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1074402',
|
'id': '1074402',
|
||||||
'ext': 'mp4',
|
'ext': 'flv',
|
||||||
'title': '【金坷垃】金泡沫',
|
'title': '【金坷垃】金泡沫',
|
||||||
'description': 'md5:ce18c2a2d2193f0df2917d270f2e5923',
|
'description': 'md5:ce18c2a2d2193f0df2917d270f2e5923',
|
||||||
'duration': 308.315,
|
'duration': 308.067,
|
||||||
'timestamp': 1398012660,
|
'timestamp': 1398012678,
|
||||||
'upload_date': '20140420',
|
'upload_date': '20140420',
|
||||||
'thumbnail': r're:^https?://.+\.jpg',
|
'thumbnail': r're:^https?://.+\.jpg',
|
||||||
'uploader': '菊子桑',
|
'uploader': '菊子桑',
|
||||||
@ -59,17 +59,38 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
'url': 'http://www.bilibili.com/video/av8903802/',
|
'url': 'http://www.bilibili.com/video/av8903802/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '8903802',
|
'id': '8903802',
|
||||||
'ext': 'mp4',
|
|
||||||
'title': '阿滴英文|英文歌分享#6 "Closer',
|
'title': '阿滴英文|英文歌分享#6 "Closer',
|
||||||
'description': '滴妹今天唱Closer給你聽! 有史以来,被推最多次也是最久的歌曲,其实歌词跟我原本想像差蛮多的,不过还是好听! 微博@阿滴英文',
|
'description': '滴妹今天唱Closer給你聽! 有史以来,被推最多次也是最久的歌曲,其实歌词跟我原本想像差蛮多的,不过还是好听! 微博@阿滴英文',
|
||||||
|
},
|
||||||
|
'playlist': [{
|
||||||
|
'info_dict': {
|
||||||
|
'id': '8903802_part1',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': '阿滴英文|英文歌分享#6 "Closer',
|
||||||
|
'description': 'md5:3b1b9e25b78da4ef87e9b548b88ee76a',
|
||||||
'uploader': '阿滴英文',
|
'uploader': '阿滴英文',
|
||||||
'uploader_id': '65880958',
|
'uploader_id': '65880958',
|
||||||
'timestamp': 1488382620,
|
'timestamp': 1488382634,
|
||||||
'upload_date': '20170301',
|
'upload_date': '20170301',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
'skip_download': True, # Test metadata only
|
'skip_download': True, # Test metadata only
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'info_dict': {
|
||||||
|
'id': '8903802_part2',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': '阿滴英文|英文歌分享#6 "Closer',
|
||||||
|
'description': 'md5:3b1b9e25b78da4ef87e9b548b88ee76a',
|
||||||
|
'uploader': '阿滴英文',
|
||||||
|
'uploader_id': '65880958',
|
||||||
|
'timestamp': 1488382634,
|
||||||
|
'upload_date': '20170301',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True, # Test metadata only
|
||||||
|
},
|
||||||
|
}]
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_APP_KEY = '84956560bc028eb7'
|
_APP_KEY = '84956560bc028eb7'
|
||||||
@ -92,8 +113,12 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
if 'anime/' not in url:
|
if 'anime/' not in url:
|
||||||
cid = compat_parse_qs(self._search_regex(
|
cid = self._search_regex(
|
||||||
|
r'\bcid(?:["\']:|=)(\d+)', webpage, 'cid',
|
||||||
|
default=None
|
||||||
|
) or compat_parse_qs(self._search_regex(
|
||||||
[r'EmbedPlayer\([^)]+,\s*"([^"]+)"\)',
|
[r'EmbedPlayer\([^)]+,\s*"([^"]+)"\)',
|
||||||
|
r'EmbedPlayer\([^)]+,\s*\\"([^"]+)\\"\)',
|
||||||
r'<iframe[^>]+src="https://secure\.bilibili\.com/secure,([^"]+)"'],
|
r'<iframe[^>]+src="https://secure\.bilibili\.com/secure,([^"]+)"'],
|
||||||
webpage, 'player parameters'))['cid'][0]
|
webpage, 'player parameters'))['cid'][0]
|
||||||
else:
|
else:
|
||||||
@ -102,6 +127,7 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
video_id, anime_id, compat_urlparse.urljoin(url, '//bangumi.bilibili.com/anime/%s' % anime_id)))
|
video_id, anime_id, compat_urlparse.urljoin(url, '//bangumi.bilibili.com/anime/%s' % anime_id)))
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
|
'Referer': url
|
||||||
}
|
}
|
||||||
headers.update(self.geo_verification_headers())
|
headers.update(self.geo_verification_headers())
|
||||||
|
|
||||||
@ -113,19 +139,31 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
self._report_error(js)
|
self._report_error(js)
|
||||||
cid = js['result']['cid']
|
cid = js['result']['cid']
|
||||||
|
|
||||||
payload = 'appkey=%s&cid=%s&otype=json&quality=2&type=mp4' % (self._APP_KEY, cid)
|
headers = {
|
||||||
|
'Referer': url
|
||||||
|
}
|
||||||
|
headers.update(self.geo_verification_headers())
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
RENDITIONS = ('qn=80&quality=80&type=', 'quality=2&type=mp4')
|
||||||
|
for num, rendition in enumerate(RENDITIONS, start=1):
|
||||||
|
payload = 'appkey=%s&cid=%s&otype=json&%s' % (self._APP_KEY, cid, rendition)
|
||||||
sign = hashlib.md5((payload + self._BILIBILI_KEY).encode('utf-8')).hexdigest()
|
sign = hashlib.md5((payload + self._BILIBILI_KEY).encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
video_info = self._download_json(
|
video_info = self._download_json(
|
||||||
'http://interface.bilibili.com/playurl?%s&sign=%s' % (payload, sign),
|
'http://interface.bilibili.com/v2/playurl?%s&sign=%s' % (payload, sign),
|
||||||
video_id, note='Downloading video info page',
|
video_id, note='Downloading video info page',
|
||||||
headers=self.geo_verification_headers())
|
headers=headers, fatal=num == len(RENDITIONS))
|
||||||
|
|
||||||
|
if not video_info:
|
||||||
|
continue
|
||||||
|
|
||||||
if 'durl' not in video_info:
|
if 'durl' not in video_info:
|
||||||
|
if num < len(RENDITIONS):
|
||||||
|
continue
|
||||||
self._report_error(video_info)
|
self._report_error(video_info)
|
||||||
|
|
||||||
entries = []
|
|
||||||
|
|
||||||
for idx, durl in enumerate(video_info['durl']):
|
for idx, durl in enumerate(video_info['durl']):
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': durl['url'],
|
'url': durl['url'],
|
||||||
@ -150,11 +188,17 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
'duration': float_or_none(durl.get('length'), 1000),
|
'duration': float_or_none(durl.get('length'), 1000),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
})
|
})
|
||||||
|
break
|
||||||
|
|
||||||
title = self._html_search_regex('<h1[^>]*>([^<]+)</h1>', webpage, 'title')
|
title = self._html_search_regex(
|
||||||
|
('<h1[^>]+\btitle=(["\'])(?P<title>(?:(?!\1).)+)\1',
|
||||||
|
'(?s)<h1[^>]*>(?P<title>.+?)</h1>'), webpage, 'title',
|
||||||
|
group='title')
|
||||||
description = self._html_search_meta('description', webpage)
|
description = self._html_search_meta('description', webpage)
|
||||||
timestamp = unified_timestamp(self._html_search_regex(
|
timestamp = unified_timestamp(self._html_search_regex(
|
||||||
r'<time[^>]+datetime="([^"]+)"', webpage, 'upload time', default=None))
|
r'<time[^>]+datetime="([^"]+)"', webpage, 'upload time',
|
||||||
|
default=None) or self._html_search_meta(
|
||||||
|
'uploadDate', webpage, 'timestamp', default=None))
|
||||||
thumbnail = self._html_search_meta(['og:image', 'thumbnailUrl'], webpage)
|
thumbnail = self._html_search_meta(['og:image', 'thumbnailUrl'], webpage)
|
||||||
|
|
||||||
# TODO 'view_count' requires deobfuscating Javascript
|
# TODO 'view_count' requires deobfuscating Javascript
|
||||||
@ -168,13 +212,16 @@ class BiliBiliIE(InfoExtractor):
|
|||||||
}
|
}
|
||||||
|
|
||||||
uploader_mobj = re.search(
|
uploader_mobj = re.search(
|
||||||
r'<a[^>]+href="(?:https?:)?//space\.bilibili\.com/(?P<id>\d+)"[^>]+title="(?P<name>[^"]+)"',
|
r'<a[^>]+href="(?:https?:)?//space\.bilibili\.com/(?P<id>\d+)"[^>]*>(?P<name>[^<]+)',
|
||||||
webpage)
|
webpage)
|
||||||
if uploader_mobj:
|
if uploader_mobj:
|
||||||
info.update({
|
info.update({
|
||||||
'uploader': uploader_mobj.group('name'),
|
'uploader': uploader_mobj.group('name'),
|
||||||
'uploader_id': uploader_mobj.group('id'),
|
'uploader_id': uploader_mobj.group('id'),
|
||||||
})
|
})
|
||||||
|
if not info.get('uploader'):
|
||||||
|
info['uploader'] = self._html_search_meta(
|
||||||
|
'author', webpage, 'uploader', default=None)
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
entry.update(info)
|
entry.update(info)
|
||||||
|
120
youtube_dl/extractor/bitchute.py
Normal file
120
youtube_dl/extractor/bitchute.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import urlencode_postdata
|
||||||
|
|
||||||
|
|
||||||
|
class BitChuteIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?bitchute\.com/(?:video|embed|torrent/[^/]+)/(?P<id>[^/?#&]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.bitchute.com/video/szoMrox2JEI/',
|
||||||
|
'md5': '66c4a70e6bfc40dcb6be3eb1d74939eb',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'szoMrox2JEI',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Fuck bitches get money',
|
||||||
|
'description': 'md5:3f21f6fb5b1d17c3dee9cf6b5fe60b3a',
|
||||||
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
|
'uploader': 'Victoria X Rave',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bitchute.com/embed/lbb5G1hjPhw/',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.bitchute.com/torrent/Zee5BE49045h/szoMrox2JEI.webtorrent',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
'https://www.bitchute.com/video/%s' % video_id, video_id, headers={
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.57 Safari/537.36',
|
||||||
|
})
|
||||||
|
|
||||||
|
title = self._search_regex(
|
||||||
|
(r'<[^>]+\bid=["\']video-title[^>]+>([^<]+)', r'<title>([^<]+)'),
|
||||||
|
webpage, 'title', default=None) or self._html_search_meta(
|
||||||
|
'description', webpage, 'title',
|
||||||
|
default=None) or self._og_search_description(webpage)
|
||||||
|
|
||||||
|
formats = [
|
||||||
|
{'url': mobj.group('url')}
|
||||||
|
for mobj in re.finditer(
|
||||||
|
r'addWebSeed\s*\(\s*(["\'])(?P<url>(?:(?!\1).)+)\1', webpage)]
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
description = self._html_search_regex(
|
||||||
|
r'(?s)<div\b[^>]+\bclass=["\']full hidden[^>]+>(.+?)</div>',
|
||||||
|
webpage, 'description', fatal=False)
|
||||||
|
thumbnail = self._og_search_thumbnail(
|
||||||
|
webpage, default=None) or self._html_search_meta(
|
||||||
|
'twitter:image:src', webpage, 'thumbnail')
|
||||||
|
uploader = self._html_search_regex(
|
||||||
|
r'(?s)<p\b[^>]+\bclass=["\']video-author[^>]+>(.+?)</p>', webpage,
|
||||||
|
'uploader', fatal=False)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'description': description,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'uploader': uploader,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class BitChuteChannelIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?bitchute\.com/channel/(?P<id>[^/?#&]+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'https://www.bitchute.com/channel/victoriaxrave/',
|
||||||
|
'playlist_mincount': 185,
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'victoriaxrave',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_TOKEN = 'zyG6tQcGPE5swyAEFLqKUwMuMMuF6IO2DZ6ZDQjGfsL0e4dcTLwqkTTul05Jdve7'
|
||||||
|
|
||||||
|
def _entries(self, channel_id):
|
||||||
|
channel_url = 'https://www.bitchute.com/channel/%s/' % channel_id
|
||||||
|
offset = 0
|
||||||
|
for page_num in itertools.count(1):
|
||||||
|
data = self._download_json(
|
||||||
|
'%sextend/' % channel_url, channel_id,
|
||||||
|
'Downloading channel page %d' % page_num,
|
||||||
|
data=urlencode_postdata({
|
||||||
|
'csrfmiddlewaretoken': self._TOKEN,
|
||||||
|
'name': '',
|
||||||
|
'offset': offset,
|
||||||
|
}), headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||||
|
'Referer': channel_url,
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Cookie': 'csrftoken=%s' % self._TOKEN,
|
||||||
|
})
|
||||||
|
if data.get('success') is False:
|
||||||
|
break
|
||||||
|
html = data.get('html')
|
||||||
|
if not html:
|
||||||
|
break
|
||||||
|
video_ids = re.findall(
|
||||||
|
r'class=["\']channel-videos-image-container[^>]+>\s*<a\b[^>]+\bhref=["\']/video/([^"\'/]+)',
|
||||||
|
html)
|
||||||
|
if not video_ids:
|
||||||
|
break
|
||||||
|
offset += len(video_ids)
|
||||||
|
for video_id in video_ids:
|
||||||
|
yield self.url_result(
|
||||||
|
'https://www.bitchute.com/video/%s' % video_id,
|
||||||
|
ie=BitChuteIE.ie_key(), video_id=video_id)
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
channel_id = self._match_id(url)
|
||||||
|
return self.playlist_result(
|
||||||
|
self._entries(channel_id), playlist_id=channel_id)
|
@ -1,20 +1,23 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
|
parse_iso8601,
|
||||||
xpath_element,
|
xpath_element,
|
||||||
xpath_text,
|
xpath_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BRIE(InfoExtractor):
|
class BRIE(InfoExtractor):
|
||||||
IE_DESC = 'Bayerischer Rundfunk Mediathek'
|
IE_DESC = 'Bayerischer Rundfunk'
|
||||||
_VALID_URL = r'(?P<base_url>https?://(?:www\.)?br(?:-klassik)?\.de)/(?:[a-z0-9\-_]+/)+(?P<id>[a-z0-9\-_]+)\.html'
|
_VALID_URL = r'(?P<base_url>https?://(?:www\.)?br(?:-klassik)?\.de)/(?:[a-z0-9\-_]+/)+(?P<id>[a-z0-9\-_]+)\.html'
|
||||||
|
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
@ -123,10 +126,10 @@ class BRIE(InfoExtractor):
|
|||||||
for asset in assets.findall('asset'):
|
for asset in assets.findall('asset'):
|
||||||
format_url = xpath_text(asset, ['downloadUrl', 'url'])
|
format_url = xpath_text(asset, ['downloadUrl', 'url'])
|
||||||
asset_type = asset.get('type')
|
asset_type = asset.get('type')
|
||||||
if asset_type == 'HDS':
|
if asset_type.startswith('HDS'):
|
||||||
formats.extend(self._extract_f4m_formats(
|
formats.extend(self._extract_f4m_formats(
|
||||||
format_url + '?hdcore=3.2.0', media_id, f4m_id='hds', fatal=False))
|
format_url + '?hdcore=3.2.0', media_id, f4m_id='hds', fatal=False))
|
||||||
elif asset_type == 'HLS':
|
elif asset_type.startswith('HLS'):
|
||||||
formats.extend(self._extract_m3u8_formats(
|
formats.extend(self._extract_m3u8_formats(
|
||||||
format_url, media_id, 'mp4', 'm3u8_native', m3u8_id='hds', fatal=False))
|
format_url, media_id, 'mp4', 'm3u8_native', m3u8_id='hds', fatal=False))
|
||||||
else:
|
else:
|
||||||
@ -169,3 +172,140 @@ class BRIE(InfoExtractor):
|
|||||||
} for variant in variants.findall('variant') if xpath_text(variant, 'url')]
|
} for variant in variants.findall('variant') if xpath_text(variant, 'url')]
|
||||||
thumbnails.sort(key=lambda x: x['width'] * x['height'], reverse=True)
|
thumbnails.sort(key=lambda x: x['width'] * x['height'], reverse=True)
|
||||||
return thumbnails
|
return thumbnails
|
||||||
|
|
||||||
|
|
||||||
|
class BRMediathekIE(InfoExtractor):
|
||||||
|
IE_DESC = 'Bayerischer Rundfunk Mediathek'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?br\.de/mediathek/video/[^/?&#]*?-(?P<id>av:[0-9a-f]{24})'
|
||||||
|
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.br.de/mediathek/video/gesundheit-die-sendung-vom-28112017-av:5a1e6a6e8fce6d001871cc8e',
|
||||||
|
'md5': 'fdc3d485835966d1622587d08ba632ec',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'av:5a1e6a6e8fce6d001871cc8e',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Die Sendung vom 28.11.2017',
|
||||||
|
'description': 'md5:6000cdca5912ab2277e5b7339f201ccc',
|
||||||
|
'timestamp': 1511942766,
|
||||||
|
'upload_date': '20171129',
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
clip_id = self._match_id(url)
|
||||||
|
|
||||||
|
clip = self._download_json(
|
||||||
|
'https://proxy-base.master.mango.express/graphql',
|
||||||
|
clip_id, data=json.dumps({
|
||||||
|
"query": """{
|
||||||
|
viewer {
|
||||||
|
clip(id: "%s") {
|
||||||
|
title
|
||||||
|
description
|
||||||
|
duration
|
||||||
|
createdAt
|
||||||
|
ageRestriction
|
||||||
|
videoFiles {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
publicLocation
|
||||||
|
fileSize
|
||||||
|
videoProfile {
|
||||||
|
width
|
||||||
|
height
|
||||||
|
bitrate
|
||||||
|
encoding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
captionFiles {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
publicLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
teaserImages {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
imageFiles {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
publicLocation
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}""" % clip_id}).encode(), headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
})['data']['viewer']['clip']
|
||||||
|
title = clip['title']
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for edge in clip.get('videoFiles', {}).get('edges', []):
|
||||||
|
node = edge.get('node', {})
|
||||||
|
n_url = node.get('publicLocation')
|
||||||
|
if not n_url:
|
||||||
|
continue
|
||||||
|
ext = determine_ext(n_url)
|
||||||
|
if ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
n_url, clip_id, 'mp4', 'm3u8_native',
|
||||||
|
m3u8_id='hls', fatal=False))
|
||||||
|
else:
|
||||||
|
video_profile = node.get('videoProfile', {})
|
||||||
|
tbr = int_or_none(video_profile.get('bitrate'))
|
||||||
|
format_id = 'http'
|
||||||
|
if tbr:
|
||||||
|
format_id += '-%d' % tbr
|
||||||
|
formats.append({
|
||||||
|
'format_id': format_id,
|
||||||
|
'url': n_url,
|
||||||
|
'width': int_or_none(video_profile.get('width')),
|
||||||
|
'height': int_or_none(video_profile.get('height')),
|
||||||
|
'tbr': tbr,
|
||||||
|
'filesize': int_or_none(node.get('fileSize')),
|
||||||
|
})
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
for edge in clip.get('captionFiles', {}).get('edges', []):
|
||||||
|
node = edge.get('node', {})
|
||||||
|
n_url = node.get('publicLocation')
|
||||||
|
if not n_url:
|
||||||
|
continue
|
||||||
|
subtitles.setdefault('de', []).append({
|
||||||
|
'url': n_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
thumbnails = []
|
||||||
|
for edge in clip.get('teaserImages', {}).get('edges', []):
|
||||||
|
for image_edge in edge.get('node', {}).get('imageFiles', {}).get('edges', []):
|
||||||
|
node = image_edge.get('node', {})
|
||||||
|
n_url = node.get('publicLocation')
|
||||||
|
if not n_url:
|
||||||
|
continue
|
||||||
|
thumbnails.append({
|
||||||
|
'url': n_url,
|
||||||
|
'width': int_or_none(node.get('width')),
|
||||||
|
'height': int_or_none(node.get('height')),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': clip_id,
|
||||||
|
'title': title,
|
||||||
|
'description': clip.get('description'),
|
||||||
|
'duration': int_or_none(clip.get('duration')),
|
||||||
|
'timestamp': parse_iso8601(clip.get('createdAt')),
|
||||||
|
'age_limit': int_or_none(clip.get('ageRestriction')),
|
||||||
|
'formats': formats,
|
||||||
|
'subtitles': subtitles,
|
||||||
|
'thumbnails': thumbnails,
|
||||||
|
}
|
||||||
|
@ -3,15 +3,15 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_str
|
from .youtube import YoutubeIE
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_age_limit,
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BreakIE(InfoExtractor):
|
class BreakIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?P<site>break|screenjunkies)\.com/video/(?P<display_id>[^/]+?)(?:-(?P<id>\d+))?(?:[/?#&]|$)'
|
_VALID_URL = r'https?://(?:www\.)?break\.com/video/(?P<display_id>[^/]+?)(?:-(?P<id>\d+))?(?:[/?#&]|$)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.break.com/video/when-girls-act-like-guys-2468056',
|
'url': 'http://www.break.com/video/when-girls-act-like-guys-2468056',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -19,125 +19,73 @@ class BreakIE(InfoExtractor):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'When Girls Act Like D-Bags',
|
'title': 'When Girls Act Like D-Bags',
|
||||||
'age_limit': 13,
|
'age_limit': 13,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
# youtube embed
|
||||||
|
'url': 'http://www.break.com/video/someone-forgot-boat-brakes-work',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'RrrDLdeL2HQ',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Whale Watching Boat Crashing Into San Diego Dock',
|
||||||
|
'description': 'md5:afc1b2772f0a8468be51dd80eb021069',
|
||||||
|
'upload_date': '20160331',
|
||||||
|
'uploader': 'Steve Holden',
|
||||||
|
'uploader_id': 'sdholden07',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
'url': 'http://www.screenjunkies.com/video/best-quentin-tarantino-movie-2841915',
|
|
||||||
'md5': '5c2b686bec3d43de42bde9ec047536b0',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '2841915',
|
|
||||||
'display_id': 'best-quentin-tarantino-movie',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Best Quentin Tarantino Movie',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg',
|
|
||||||
'duration': 3671,
|
|
||||||
'age_limit': 13,
|
|
||||||
'tags': list,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.screenjunkies.com/video/honest-trailers-the-dark-knight',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '2348808',
|
|
||||||
'display_id': 'honest-trailers-the-dark-knight',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Honest Trailers - The Dark Knight',
|
|
||||||
'thumbnail': r're:^https?://.*\.(?:jpg|png)',
|
|
||||||
'age_limit': 10,
|
|
||||||
'tags': list,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
# requires subscription but worked around
|
|
||||||
'url': 'http://www.screenjunkies.com/video/knocking-dead-ep-1-the-show-so-far-3003285',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '3003285',
|
|
||||||
'display_id': 'knocking-dead-ep-1-the-show-so-far',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'State of The Dead Recap: Knocking Dead Pilot',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg',
|
|
||||||
'duration': 3307,
|
|
||||||
'age_limit': 13,
|
|
||||||
'tags': list,
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://www.break.com/video/ugc/baby-flex-2773063',
|
'url': 'http://www.break.com/video/ugc/baby-flex-2773063',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_DEFAULT_BITRATES = (48, 150, 320, 496, 864, 2240, 3264)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
site, display_id, video_id = re.match(self._VALID_URL, url).groups()
|
display_id, video_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
|
||||||
if not video_id:
|
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
video_id = self._search_regex(
|
|
||||||
(r'src=["\']/embed/(\d+)', r'data-video-content-id=["\'](\d+)'),
|
|
||||||
webpage, 'video id')
|
|
||||||
|
|
||||||
webpage = self._download_webpage(
|
youtube_url = YoutubeIE._extract_url(webpage)
|
||||||
'http://www.%s.com/embed/%s' % (site, video_id),
|
if youtube_url:
|
||||||
display_id, 'Downloading video embed page')
|
return self.url_result(youtube_url, ie=YoutubeIE.ie_key())
|
||||||
embed_vars = self._parse_json(
|
|
||||||
|
content = self._parse_json(
|
||||||
self._search_regex(
|
self._search_regex(
|
||||||
r'(?s)embedVars\s*=\s*({.+?})\s*</script>', webpage, 'embed vars'),
|
r'(?s)content["\']\s*:\s*(\[.+?\])\s*[,\n]', webpage,
|
||||||
|
'content'),
|
||||||
display_id)
|
display_id)
|
||||||
|
|
||||||
youtube_id = embed_vars.get('youtubeId')
|
|
||||||
if youtube_id:
|
|
||||||
return self.url_result(youtube_id, 'Youtube')
|
|
||||||
|
|
||||||
title = embed_vars['contentName']
|
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
bitrates = []
|
for video in content:
|
||||||
for f in embed_vars.get('media', []):
|
video_url = url_or_none(video.get('url'))
|
||||||
if not f.get('uri') or f.get('mediaPurpose') != 'play':
|
if not video_url:
|
||||||
continue
|
continue
|
||||||
bitrate = int_or_none(f.get('bitRate'))
|
bitrate = int_or_none(self._search_regex(
|
||||||
if bitrate:
|
r'(\d+)_kbps', video_url, 'tbr', default=None))
|
||||||
bitrates.append(bitrate)
|
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': f['uri'],
|
'url': video_url,
|
||||||
'format_id': 'http-%d' % bitrate if bitrate else 'http',
|
'format_id': 'http-%d' % bitrate if bitrate else 'http',
|
||||||
'width': int_or_none(f.get('width')),
|
|
||||||
'height': int_or_none(f.get('height')),
|
|
||||||
'tbr': bitrate,
|
'tbr': bitrate,
|
||||||
'format': 'mp4',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if not bitrates:
|
|
||||||
# When subscriptionLevel > 0, i.e. plus subscription is required
|
|
||||||
# media list will be empty. However, hds and hls uris are still
|
|
||||||
# available. We can grab them assuming bitrates to be default.
|
|
||||||
bitrates = self._DEFAULT_BITRATES
|
|
||||||
|
|
||||||
auth_token = embed_vars.get('AuthToken')
|
|
||||||
|
|
||||||
def construct_manifest_url(base_url, ext):
|
|
||||||
pieces = [base_url]
|
|
||||||
pieces.extend([compat_str(b) for b in bitrates])
|
|
||||||
pieces.append('_kbps.mp4.%s?%s' % (ext, auth_token))
|
|
||||||
return ','.join(pieces)
|
|
||||||
|
|
||||||
if bitrates and auth_token:
|
|
||||||
hds_url = embed_vars.get('hdsUri')
|
|
||||||
if hds_url:
|
|
||||||
formats.extend(self._extract_f4m_formats(
|
|
||||||
construct_manifest_url(hds_url, 'f4m'),
|
|
||||||
display_id, f4m_id='hds', fatal=False))
|
|
||||||
hls_url = embed_vars.get('hlsUri')
|
|
||||||
if hls_url:
|
|
||||||
formats.extend(self._extract_m3u8_formats(
|
|
||||||
construct_manifest_url(hls_url, 'm3u8'),
|
|
||||||
display_id, 'mp4', entry_protocol='m3u8_native', m3u8_id='hls', fatal=False))
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
title = self._search_regex(
|
||||||
|
(r'title["\']\s*:\s*(["\'])(?P<value>(?:(?!\1).)+)\1',
|
||||||
|
r'<h1[^>]*>(?P<value>[^<]+)'), webpage, 'title', group='value')
|
||||||
|
|
||||||
|
def get(key, name):
|
||||||
|
return int_or_none(self._search_regex(
|
||||||
|
r'%s["\']\s*:\s*["\'](\d+)' % key, webpage, name,
|
||||||
|
default=None))
|
||||||
|
|
||||||
|
age_limit = get('ratings', 'age limit')
|
||||||
|
video_id = video_id or get('pid', 'video id') or display_id
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'thumbnail': embed_vars.get('thumbUri'),
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
'duration': int_or_none(embed_vars.get('videoLengthInSeconds')) or None,
|
'age_limit': age_limit,
|
||||||
'age_limit': parse_age_limit(embed_vars.get('audienceRating')),
|
|
||||||
'tags': embed_vars.get('tags', '').split(','),
|
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
@ -464,7 +464,7 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
'timestamp': 1441391203,
|
'timestamp': 1441391203,
|
||||||
'upload_date': '20150904',
|
'upload_date': '20150904',
|
||||||
'uploader_id': '929656772001',
|
'uploader_id': '929656772001',
|
||||||
'formats': 'mincount:22',
|
'formats': 'mincount:20',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# with rtmp streams
|
# with rtmp streams
|
||||||
@ -478,7 +478,7 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
'timestamp': 1433556729,
|
'timestamp': 1433556729,
|
||||||
'upload_date': '20150606',
|
'upload_date': '20150606',
|
||||||
'uploader_id': '4036320279001',
|
'uploader_id': '4036320279001',
|
||||||
'formats': 'mincount:41',
|
'formats': 'mincount:39',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
@ -564,59 +564,7 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
|
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _parse_brightcove_metadata(self, json_data, video_id, headers={}):
|
||||||
url, smuggled_data = unsmuggle_url(url, {})
|
|
||||||
self._initialize_geo_bypass(smuggled_data.get('geo_countries'))
|
|
||||||
|
|
||||||
account_id, player_id, embed, video_id = re.match(self._VALID_URL, url).groups()
|
|
||||||
|
|
||||||
webpage = self._download_webpage(
|
|
||||||
'http://players.brightcove.net/%s/%s_%s/index.min.js'
|
|
||||||
% (account_id, player_id, embed), video_id)
|
|
||||||
|
|
||||||
policy_key = None
|
|
||||||
|
|
||||||
catalog = self._search_regex(
|
|
||||||
r'catalog\(({.+?})\);', webpage, 'catalog', default=None)
|
|
||||||
if catalog:
|
|
||||||
catalog = self._parse_json(
|
|
||||||
js_to_json(catalog), video_id, fatal=False)
|
|
||||||
if catalog:
|
|
||||||
policy_key = catalog.get('policyKey')
|
|
||||||
|
|
||||||
if not policy_key:
|
|
||||||
policy_key = self._search_regex(
|
|
||||||
r'policyKey\s*:\s*(["\'])(?P<pk>.+?)\1',
|
|
||||||
webpage, 'policy key', group='pk')
|
|
||||||
|
|
||||||
api_url = 'https://edge.api.brightcove.com/playback/v1/accounts/%s/videos/%s' % (account_id, video_id)
|
|
||||||
try:
|
|
||||||
json_data = self._download_json(api_url, video_id, headers={
|
|
||||||
'Accept': 'application/json;pk=%s' % policy_key
|
|
||||||
})
|
|
||||||
except ExtractorError as e:
|
|
||||||
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
|
|
||||||
json_data = self._parse_json(e.cause.read().decode(), video_id)[0]
|
|
||||||
message = json_data.get('message') or json_data['error_code']
|
|
||||||
if json_data.get('error_subcode') == 'CLIENT_GEO':
|
|
||||||
self.raise_geo_restricted(msg=message)
|
|
||||||
raise ExtractorError(message, expected=True)
|
|
||||||
raise
|
|
||||||
|
|
||||||
errors = json_data.get('errors')
|
|
||||||
if errors and errors[0].get('error_subcode') == 'TVE_AUTH':
|
|
||||||
custom_fields = json_data['custom_fields']
|
|
||||||
tve_token = self._extract_mvpd_auth(
|
|
||||||
smuggled_data['source_url'], video_id,
|
|
||||||
custom_fields['bcadobepassrequestorid'],
|
|
||||||
custom_fields['bcadobepassresourceid'])
|
|
||||||
json_data = self._download_json(
|
|
||||||
api_url, video_id, headers={
|
|
||||||
'Accept': 'application/json;pk=%s' % policy_key
|
|
||||||
}, query={
|
|
||||||
'tveToken': tve_token,
|
|
||||||
})
|
|
||||||
|
|
||||||
title = json_data['name'].strip()
|
title = json_data['name'].strip()
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
@ -624,7 +572,8 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
container = source.get('container')
|
container = source.get('container')
|
||||||
ext = mimetype2ext(source.get('type'))
|
ext = mimetype2ext(source.get('type'))
|
||||||
src = source.get('src')
|
src = source.get('src')
|
||||||
if ext == 'ism' or container == 'WVM':
|
# https://support.brightcove.com/playback-api-video-fields-reference#key_systems_object
|
||||||
|
if ext == 'ism' or container == 'WVM' or source.get('key_systems'):
|
||||||
continue
|
continue
|
||||||
elif ext == 'm3u8' or container == 'M2TS':
|
elif ext == 'm3u8' or container == 'M2TS':
|
||||||
if not src:
|
if not src:
|
||||||
@ -681,7 +630,16 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
'format_id': build_format_id('rtmp'),
|
'format_id': build_format_id('rtmp'),
|
||||||
})
|
})
|
||||||
formats.append(f)
|
formats.append(f)
|
||||||
|
if not formats:
|
||||||
|
# for sonyliv.com DRM protected videos
|
||||||
|
s3_source_url = json_data.get('custom_fields', {}).get('s3sourceurl')
|
||||||
|
if s3_source_url:
|
||||||
|
formats.append({
|
||||||
|
'url': s3_source_url,
|
||||||
|
'format_id': 'source',
|
||||||
|
})
|
||||||
|
|
||||||
|
errors = json_data.get('errors')
|
||||||
if not formats and errors:
|
if not formats and errors:
|
||||||
error = errors[0]
|
error = errors[0]
|
||||||
raise ExtractorError(
|
raise ExtractorError(
|
||||||
@ -689,6 +647,9 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
for f in formats:
|
||||||
|
f.setdefault('http_headers', {}).update(headers)
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
for text_track in json_data.get('text_tracks', []):
|
for text_track in json_data.get('text_tracks', []):
|
||||||
if text_track.get('src'):
|
if text_track.get('src'):
|
||||||
@ -708,9 +669,75 @@ class BrightcoveNewIE(AdobePassIE):
|
|||||||
'thumbnail': json_data.get('thumbnail') or json_data.get('poster'),
|
'thumbnail': json_data.get('thumbnail') or json_data.get('poster'),
|
||||||
'duration': duration,
|
'duration': duration,
|
||||||
'timestamp': parse_iso8601(json_data.get('published_at')),
|
'timestamp': parse_iso8601(json_data.get('published_at')),
|
||||||
'uploader_id': account_id,
|
'uploader_id': json_data.get('account_id'),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
'tags': json_data.get('tags', []),
|
'tags': json_data.get('tags', []),
|
||||||
'is_live': is_live,
|
'is_live': is_live,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
url, smuggled_data = unsmuggle_url(url, {})
|
||||||
|
self._initialize_geo_bypass({
|
||||||
|
'countries': smuggled_data.get('geo_countries'),
|
||||||
|
'ip_blocks': smuggled_data.get('geo_ip_blocks'),
|
||||||
|
})
|
||||||
|
|
||||||
|
account_id, player_id, embed, video_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
'http://players.brightcove.net/%s/%s_%s/index.min.js'
|
||||||
|
% (account_id, player_id, embed), video_id)
|
||||||
|
|
||||||
|
policy_key = None
|
||||||
|
|
||||||
|
catalog = self._search_regex(
|
||||||
|
r'catalog\(({.+?})\);', webpage, 'catalog', default=None)
|
||||||
|
if catalog:
|
||||||
|
catalog = self._parse_json(
|
||||||
|
js_to_json(catalog), video_id, fatal=False)
|
||||||
|
if catalog:
|
||||||
|
policy_key = catalog.get('policyKey')
|
||||||
|
|
||||||
|
if not policy_key:
|
||||||
|
policy_key = self._search_regex(
|
||||||
|
r'policyKey\s*:\s*(["\'])(?P<pk>.+?)\1',
|
||||||
|
webpage, 'policy key', group='pk')
|
||||||
|
|
||||||
|
api_url = 'https://edge.api.brightcove.com/playback/v1/accounts/%s/videos/%s' % (account_id, video_id)
|
||||||
|
headers = {
|
||||||
|
'Accept': 'application/json;pk=%s' % policy_key,
|
||||||
|
}
|
||||||
|
referrer = smuggled_data.get('referrer')
|
||||||
|
if referrer:
|
||||||
|
headers.update({
|
||||||
|
'Referer': referrer,
|
||||||
|
'Origin': re.search(r'https?://[^/]+', referrer).group(0),
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
json_data = self._download_json(api_url, video_id, headers=headers)
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 403:
|
||||||
|
json_data = self._parse_json(e.cause.read().decode(), video_id)[0]
|
||||||
|
message = json_data.get('message') or json_data['error_code']
|
||||||
|
if json_data.get('error_subcode') == 'CLIENT_GEO':
|
||||||
|
self.raise_geo_restricted(msg=message)
|
||||||
|
raise ExtractorError(message, expected=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
errors = json_data.get('errors')
|
||||||
|
if errors and errors[0].get('error_subcode') == 'TVE_AUTH':
|
||||||
|
custom_fields = json_data['custom_fields']
|
||||||
|
tve_token = self._extract_mvpd_auth(
|
||||||
|
smuggled_data['source_url'], video_id,
|
||||||
|
custom_fields['bcadobepassrequestorid'],
|
||||||
|
custom_fields['bcadobepassresourceid'])
|
||||||
|
json_data = self._download_json(
|
||||||
|
api_url, video_id, headers={
|
||||||
|
'Accept': 'application/json;pk=%s' % policy_key
|
||||||
|
}, query={
|
||||||
|
'tveToken': tve_token,
|
||||||
|
})
|
||||||
|
|
||||||
|
return self._parse_brightcove_metadata(
|
||||||
|
json_data, video_id, headers=headers)
|
||||||
|
42
youtube_dl/extractor/businessinsider.py
Normal file
42
youtube_dl/extractor/businessinsider.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from .jwplatform import JWPlatformIE
|
||||||
|
|
||||||
|
|
||||||
|
class BusinessInsiderIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:[^/]+\.)?businessinsider\.(?:com|nl)/(?:[^/]+/)*(?P<id>[^/?#&]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'http://uk.businessinsider.com/how-much-radiation-youre-exposed-to-in-everyday-life-2016-6',
|
||||||
|
'md5': 'ca237a53a8eb20b6dc5bd60564d4ab3e',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'hZRllCfw',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': "Here's how much radiation you're exposed to in everyday life",
|
||||||
|
'description': 'md5:9a0d6e2c279948aadaa5e84d6d9b99bd',
|
||||||
|
'upload_date': '20170709',
|
||||||
|
'timestamp': 1499606400,
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.businessinsider.nl/5-scientifically-proven-things-make-you-less-attractive-2017-7/',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.businessinsider.com/excel-index-match-vlookup-video-how-to-2015-2?IR=T',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
jwplatform_id = self._search_regex(
|
||||||
|
(r'data-media-id=["\']([a-zA-Z0-9]{8})',
|
||||||
|
r'id=["\']jwplayer_([a-zA-Z0-9]{8})',
|
||||||
|
r'id["\']?\s*:\s*["\']?([a-zA-Z0-9]{8})'),
|
||||||
|
webpage, 'jwplatform id')
|
||||||
|
return self.url_result(
|
||||||
|
'jwplatform:%s' % jwplatform_id, ie=JWPlatformIE.ie_key(),
|
||||||
|
video_id=video_id)
|
@ -3,20 +3,19 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import ExtractorError
|
|
||||||
|
|
||||||
|
|
||||||
class BYUtvIE(InfoExtractor):
|
class BYUtvIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?byutv\.org/watch/(?!event/)(?P<id>[0-9a-f-]+)(?:/(?P<display_id>[^/?#&]+))?'
|
_VALID_URL = r'https?://(?:www\.)?byutv\.org/(?:watch|player)/(?!event/)(?P<id>[0-9a-f-]+)(?:/(?P<display_id>[^/?#&]+))?'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.byutv.org/watch/6587b9a3-89d2-42a6-a7f7-fd2f81840a7d/studio-c-season-5-episode-5',
|
'url': 'http://www.byutv.org/watch/6587b9a3-89d2-42a6-a7f7-fd2f81840a7d/studio-c-season-5-episode-5',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '6587b9a3-89d2-42a6-a7f7-fd2f81840a7d',
|
'id': 'ZvanRocTpW-G5_yZFeltTAMv6jxOU9KH',
|
||||||
'display_id': 'studio-c-season-5-episode-5',
|
'display_id': 'studio-c-season-5-episode-5',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Season 5 Episode 5',
|
'title': 'Season 5 Episode 5',
|
||||||
'description': 'md5:e07269172baff037f8e8bf9956bc9747',
|
'description': 'md5:1d31dc18ef4f075b28f6a65937d22c65',
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
'thumbnail': r're:^https?://.*',
|
||||||
'duration': 1486.486,
|
'duration': 1486.486,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
@ -26,6 +25,9 @@ class BYUtvIE(InfoExtractor):
|
|||||||
}, {
|
}, {
|
||||||
'url': 'http://www.byutv.org/watch/6587b9a3-89d2-42a6-a7f7-fd2f81840a7d',
|
'url': 'http://www.byutv.org/watch/6587b9a3-89d2-42a6-a7f7-fd2f81840a7d',
|
||||||
'only_matching': True,
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.byutv.org/player/27741493-dc83-40b0-8420-e7ae38a2ae98/byu-football-toledo-vs-byu-93016?listid=4fe0fee5-0d3c-4a29-b725-e4948627f472&listindex=0&q=toledo',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@ -33,16 +35,16 @@ class BYUtvIE(InfoExtractor):
|
|||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
display_id = mobj.group('display_id') or video_id
|
display_id = mobj.group('display_id') or video_id
|
||||||
|
|
||||||
webpage = self._download_webpage(url, display_id)
|
ep = self._download_json(
|
||||||
episode_code = self._search_regex(
|
'https://api.byutv.org/api3/catalog/getvideosforcontent', video_id,
|
||||||
r'(?s)episode:(.*?\}),\s*\n', webpage, 'episode information')
|
query={
|
||||||
|
'contentid': video_id,
|
||||||
ep = self._parse_json(
|
'channel': 'byutv',
|
||||||
episode_code, display_id, transform_source=lambda s:
|
'x-byutv-context': 'web$US',
|
||||||
re.sub(r'(\n\s+)([a-zA-Z]+):\s+\'(.*?)\'', r'\1"\2": "\3"', s))
|
}, headers={
|
||||||
|
'x-byutv-context': 'web$US',
|
||||||
if ep['providerType'] != 'Ooyala':
|
'x-byutv-platformkey': 'xsaaw9c7y5',
|
||||||
raise ExtractorError('Unsupported provider %s' % ep['provider'])
|
})['ooyalaVOD']
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'_type': 'url_transparent',
|
'_type': 'url_transparent',
|
||||||
@ -50,44 +52,7 @@ class BYUtvIE(InfoExtractor):
|
|||||||
'url': 'ooyala:%s' % ep['providerId'],
|
'url': 'ooyala:%s' % ep['providerId'],
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
'title': ep['title'],
|
'title': ep.get('title'),
|
||||||
'description': ep.get('description'),
|
'description': ep.get('description'),
|
||||||
'thumbnail': ep.get('imageThumbnail'),
|
'thumbnail': ep.get('imageThumbnail'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BYUtvEventIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?byutv\.org/watch/event/(?P<id>[0-9a-f-]+)'
|
|
||||||
_TEST = {
|
|
||||||
'url': 'http://www.byutv.org/watch/event/29941b9b-8bf6-48d2-aebf-7a87add9e34b',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '29941b9b-8bf6-48d2-aebf-7a87add9e34b',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Toledo vs. BYU (9/30/16)',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'add_ie': ['Ooyala'],
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
|
||||||
|
|
||||||
ooyala_id = self._search_regex(
|
|
||||||
r'providerId\s*:\s*(["\'])(?P<id>(?:(?!\1).)+)\1',
|
|
||||||
webpage, 'ooyala id', group='id')
|
|
||||||
|
|
||||||
title = self._search_regex(
|
|
||||||
r'class=["\']description["\'][^>]*>\s*<h1>([^<]+)</h1>', webpage,
|
|
||||||
'title').strip()
|
|
||||||
|
|
||||||
return {
|
|
||||||
'_type': 'url_transparent',
|
|
||||||
'ie_key': 'Ooyala',
|
|
||||||
'url': 'ooyala:%s' % ooyala_id,
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
}
|
|
||||||
|
96
youtube_dl/extractor/cammodels.py
Normal file
96
youtube_dl/extractor/cammodels.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
int_or_none,
|
||||||
|
url_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CamModelsIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?cammodels\.com/cam/(?P<id>[^/?#&]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://www.cammodels.com/cam/AutumnKnight/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
user_id = self._match_id(url)
|
||||||
|
|
||||||
|
webpage = self._download_webpage(
|
||||||
|
url, user_id, headers=self.geo_verification_headers())
|
||||||
|
|
||||||
|
manifest_root = self._html_search_regex(
|
||||||
|
r'manifestUrlRoot=([^&\']+)', webpage, 'manifest', default=None)
|
||||||
|
|
||||||
|
if not manifest_root:
|
||||||
|
ERRORS = (
|
||||||
|
("I'm offline, but let's stay connected", 'This user is currently offline'),
|
||||||
|
('in a private show', 'This user is in a private show'),
|
||||||
|
('is currently performing LIVE', 'This model is currently performing live'),
|
||||||
|
)
|
||||||
|
for pattern, message in ERRORS:
|
||||||
|
if pattern in webpage:
|
||||||
|
error = message
|
||||||
|
expected = True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
error = 'Unable to find manifest URL root'
|
||||||
|
expected = False
|
||||||
|
raise ExtractorError(error, expected=expected)
|
||||||
|
|
||||||
|
manifest = self._download_json(
|
||||||
|
'%s%s.json' % (manifest_root, user_id), user_id)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for format_id, format_dict in manifest['formats'].items():
|
||||||
|
if not isinstance(format_dict, dict):
|
||||||
|
continue
|
||||||
|
encodings = format_dict.get('encodings')
|
||||||
|
if not isinstance(encodings, list):
|
||||||
|
continue
|
||||||
|
vcodec = format_dict.get('videoCodec')
|
||||||
|
acodec = format_dict.get('audioCodec')
|
||||||
|
for media in encodings:
|
||||||
|
if not isinstance(media, dict):
|
||||||
|
continue
|
||||||
|
media_url = url_or_none(media.get('location'))
|
||||||
|
if not media_url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
format_id_list = [format_id]
|
||||||
|
height = int_or_none(media.get('videoHeight'))
|
||||||
|
if height is not None:
|
||||||
|
format_id_list.append('%dp' % height)
|
||||||
|
f = {
|
||||||
|
'url': media_url,
|
||||||
|
'format_id': '-'.join(format_id_list),
|
||||||
|
'width': int_or_none(media.get('videoWidth')),
|
||||||
|
'height': height,
|
||||||
|
'vbr': int_or_none(media.get('videoKbps')),
|
||||||
|
'abr': int_or_none(media.get('audioKbps')),
|
||||||
|
'fps': int_or_none(media.get('fps')),
|
||||||
|
'vcodec': vcodec,
|
||||||
|
'acodec': acodec,
|
||||||
|
}
|
||||||
|
if 'rtmp' in format_id:
|
||||||
|
f['ext'] = 'flv'
|
||||||
|
elif 'hls' in format_id:
|
||||||
|
f.update({
|
||||||
|
'ext': 'mp4',
|
||||||
|
# hls skips fragments, preferring rtmp
|
||||||
|
'preference': -1,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
formats.append(f)
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': user_id,
|
||||||
|
'title': self._live_title(user_id),
|
||||||
|
'is_live': True,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
69
youtube_dl/extractor/camtube.py
Normal file
69
youtube_dl/extractor/camtube.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
int_or_none,
|
||||||
|
unified_timestamp,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CamTubeIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:(?:www|api)\.)?camtube\.co/recordings?/(?P<id>[^/?#&]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://camtube.co/recording/minafay-030618-1136-chaturbate-female',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '42ad3956-dd5b-445a-8313-803ea6079fac',
|
||||||
|
'display_id': 'minafay-030618-1136-chaturbate-female',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'minafay-030618-1136-chaturbate-female',
|
||||||
|
'duration': 1274,
|
||||||
|
'timestamp': 1528018608,
|
||||||
|
'upload_date': '20180603',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
|
_API_BASE = 'https://api.camtube.co'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
|
token = self._download_json(
|
||||||
|
'%s/rpc/session/new' % self._API_BASE, display_id,
|
||||||
|
'Downloading session token')['token']
|
||||||
|
|
||||||
|
self._set_cookie('api.camtube.co', 'session', token)
|
||||||
|
|
||||||
|
video = self._download_json(
|
||||||
|
'%s/recordings/%s' % (self._API_BASE, display_id), display_id,
|
||||||
|
headers={'Referer': url})
|
||||||
|
|
||||||
|
video_id = video['uuid']
|
||||||
|
timestamp = unified_timestamp(video.get('createdAt'))
|
||||||
|
duration = int_or_none(video.get('duration'))
|
||||||
|
view_count = int_or_none(video.get('viewCount'))
|
||||||
|
like_count = int_or_none(video.get('likeCount'))
|
||||||
|
creator = video.get('stageName')
|
||||||
|
|
||||||
|
formats = [{
|
||||||
|
'url': '%s/recordings/%s/manifest.m3u8'
|
||||||
|
% (self._API_BASE, video_id),
|
||||||
|
'format_id': 'hls',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'protocol': 'm3u8_native',
|
||||||
|
}]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': display_id,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
'duration': duration,
|
||||||
|
'view_count': view_count,
|
||||||
|
'like_count': like_count,
|
||||||
|
'creator': creator,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
@ -31,6 +31,10 @@ class Canalc2IE(InfoExtractor):
|
|||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(
|
||||||
'http://www.canalc2.tv/video/%s' % video_id, video_id)
|
'http://www.canalc2.tv/video/%s' % video_id, video_id)
|
||||||
|
|
||||||
|
title = self._html_search_regex(
|
||||||
|
r'(?s)class="[^"]*col_description[^"]*">.*?<h3>(.+?)</h3>',
|
||||||
|
webpage, 'title')
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for _, video_url in re.findall(r'file\s*=\s*(["\'])(.+?)\1', webpage):
|
for _, video_url in re.findall(r'file\s*=\s*(["\'])(.+?)\1', webpage):
|
||||||
if video_url.startswith('rtmp://'):
|
if video_url.startswith('rtmp://'):
|
||||||
@ -49,17 +53,21 @@ class Canalc2IE(InfoExtractor):
|
|||||||
'url': video_url,
|
'url': video_url,
|
||||||
'format_id': 'http',
|
'format_id': 'http',
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
title = self._html_search_regex(
|
if formats:
|
||||||
r'(?s)class="[^"]*col_description[^"]*">.*?<h3>(.*?)</h3>', webpage, 'title')
|
info = {
|
||||||
duration = parse_duration(self._search_regex(
|
|
||||||
r'id=["\']video_duree["\'][^>]*>([^<]+)',
|
|
||||||
webpage, 'duration', fatal=False))
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'duration': duration,
|
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
info = self._parse_html5_media_entries(url, webpage, url)[0]
|
||||||
|
|
||||||
|
self._sort_formats(info['formats'])
|
||||||
|
|
||||||
|
info.update({
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'duration': parse_duration(self._search_regex(
|
||||||
|
r'id=["\']video_duree["\'][^>]*>([^<]+)',
|
||||||
|
webpage, 'duration', fatal=False)),
|
||||||
|
})
|
||||||
|
return info
|
||||||
|
@ -4,59 +4,36 @@ from __future__ import unicode_literals
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_urllib_parse_urlparse
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
dict_get,
|
|
||||||
# ExtractorError,
|
# ExtractorError,
|
||||||
# HEADRequest,
|
# HEADRequest,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
qualities,
|
qualities,
|
||||||
remove_end,
|
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CanalplusIE(InfoExtractor):
|
class CanalplusIE(InfoExtractor):
|
||||||
IE_DESC = 'canalplus.fr, piwiplus.fr and d8.tv'
|
IE_DESC = 'mycanal.fr and piwiplus.fr'
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'https?://(?:www\.)?(?P<site>mycanal|piwiplus)\.fr/(?:[^/]+/)*(?P<display_id>[^?/]+)(?:\.html\?.*\bvid=|/p/)(?P<id>\d+)'
|
||||||
https?://
|
|
||||||
(?:
|
|
||||||
(?:
|
|
||||||
(?:(?:www|m)\.)?canalplus\.fr|
|
|
||||||
(?:www\.)?piwiplus\.fr|
|
|
||||||
(?:www\.)?d8\.tv|
|
|
||||||
(?:www\.)?c8\.fr|
|
|
||||||
(?:www\.)?d17\.tv|
|
|
||||||
(?:(?:football|www)\.)?cstar\.fr|
|
|
||||||
(?:www\.)?itele\.fr
|
|
||||||
)/(?:(?:[^/]+/)*(?P<display_id>[^/?#&]+))?(?:\?.*\bvid=(?P<vid>\d+))?|
|
|
||||||
player\.canalplus\.fr/#/(?P<id>\d+)
|
|
||||||
)
|
|
||||||
|
|
||||||
'''
|
|
||||||
_VIDEO_INFO_TEMPLATE = 'http://service.canal-plus.com/video/rest/getVideosLiees/%s/%s?format=json'
|
_VIDEO_INFO_TEMPLATE = 'http://service.canal-plus.com/video/rest/getVideosLiees/%s/%s?format=json'
|
||||||
_SITE_ID_MAP = {
|
_SITE_ID_MAP = {
|
||||||
'canalplus': 'cplus',
|
'mycanal': 'cplus',
|
||||||
'piwiplus': 'teletoon',
|
'piwiplus': 'teletoon',
|
||||||
'd8': 'd8',
|
|
||||||
'c8': 'd8',
|
|
||||||
'd17': 'd17',
|
|
||||||
'cstar': 'd17',
|
|
||||||
'itele': 'itele',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only works for direct mp4 URLs
|
# Only works for direct mp4 URLs
|
||||||
_GEO_COUNTRIES = ['FR']
|
_GEO_COUNTRIES = ['FR']
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.canalplus.fr/c-emissions/pid1830-c-zapping.html?vid=1192814',
|
'url': 'https://www.mycanal.fr/d17-emissions/lolywood/p/1397061',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '1405510',
|
'id': '1397061',
|
||||||
'display_id': 'pid1830-c-zapping',
|
'display_id': 'lolywood',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Zapping - 02/07/2016',
|
'title': 'Euro 2016 : Je préfère te prévenir - Lolywood - Episode 34',
|
||||||
'description': 'Le meilleur de toutes les chaînes, tous les jours',
|
'description': 'md5:7d97039d455cb29cdba0d652a0efaa5e',
|
||||||
'upload_date': '20160702',
|
'upload_date': '20160602',
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
# geo restricted, bypassed
|
# geo restricted, bypassed
|
||||||
@ -70,64 +47,12 @@ class CanalplusIE(InfoExtractor):
|
|||||||
'upload_date': '20140724',
|
'upload_date': '20140724',
|
||||||
},
|
},
|
||||||
'expected_warnings': ['HTTP Error 403: Forbidden'],
|
'expected_warnings': ['HTTP Error 403: Forbidden'],
|
||||||
}, {
|
|
||||||
# geo restricted, bypassed
|
|
||||||
'url': 'http://www.c8.fr/c8-divertissement/ms-touche-pas-a-mon-poste/pid6318-videos-integrales.html?vid=1443684',
|
|
||||||
'md5': 'bb6f9f343296ab7ebd88c97b660ecf8d',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '1443684',
|
|
||||||
'display_id': 'pid6318-videos-integrales',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Guess my iep ! - TPMP - 07/04/2017',
|
|
||||||
'description': 'md5:6f005933f6e06760a9236d9b3b5f17fa',
|
|
||||||
'upload_date': '20170407',
|
|
||||||
},
|
|
||||||
'expected_warnings': ['HTTP Error 403: Forbidden'],
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.itele.fr/chroniques/invite-michael-darmon/rachida-dati-nicolas-sarkozy-est-le-plus-en-phase-avec-les-inquietudes-des-francais-171510',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '1420176',
|
|
||||||
'display_id': 'rachida-dati-nicolas-sarkozy-est-le-plus-en-phase-avec-les-inquietudes-des-francais-171510',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'L\'invité de Michaël Darmon du 14/10/2016 - ',
|
|
||||||
'description': 'Chaque matin du lundi au vendredi, Michaël Darmon reçoit un invité politique à 8h25.',
|
|
||||||
'upload_date': '20161014',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://football.cstar.fr/cstar-minisite-foot/pid7566-feminines-videos.html?vid=1416769',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '1416769',
|
|
||||||
'display_id': 'pid7566-feminines-videos',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'France - Albanie : les temps forts de la soirée - 20/09/2016',
|
|
||||||
'description': 'md5:c3f30f2aaac294c1c969b3294de6904e',
|
|
||||||
'upload_date': '20160921',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'http://m.canalplus.fr/?vid=1398231',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.d17.tv/emissions/pid8303-lolywood.html?vid=1397061',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
site, display_id, video_id = re.match(self._VALID_URL, url).groups()
|
||||||
|
|
||||||
site_id = self._SITE_ID_MAP[compat_urllib_parse_urlparse(url).netloc.rsplit('.', 2)[-2]]
|
site_id = self._SITE_ID_MAP[site]
|
||||||
|
|
||||||
# Beware, some subclasses do not define an id group
|
|
||||||
display_id = remove_end(dict_get(mobj.groupdict(), ('display_id', 'id', 'vid')), '.html')
|
|
||||||
|
|
||||||
webpage = self._download_webpage(url, display_id)
|
|
||||||
video_id = self._search_regex(
|
|
||||||
[r'<canal:player[^>]+?videoId=(["\'])(?P<id>\d+)',
|
|
||||||
r'id=["\']canal_video_player(?P<id>\d+)',
|
|
||||||
r'data-video=["\'](?P<id>\d+)'],
|
|
||||||
webpage, 'video id', default=mobj.group('vid'), group='id')
|
|
||||||
|
|
||||||
info_url = self._VIDEO_INFO_TEMPLATE % (site_id, video_id)
|
info_url = self._VIDEO_INFO_TEMPLATE % (site_id, video_id)
|
||||||
video_data = self._download_json(info_url, video_id, 'Downloading video JSON')
|
video_data = self._download_json(info_url, video_id, 'Downloading video JSON')
|
||||||
@ -161,7 +86,7 @@ class CanalplusIE(InfoExtractor):
|
|||||||
format_url + '?hdcore=2.11.3', video_id, f4m_id=format_id, fatal=False))
|
format_url + '?hdcore=2.11.3', video_id, f4m_id=format_id, fatal=False))
|
||||||
else:
|
else:
|
||||||
formats.append({
|
formats.append({
|
||||||
# the secret extracted ya function in http://player.canalplus.fr/common/js/canalPlayer.js
|
# the secret extracted from ya function in http://player.canalplus.fr/common/js/canalPlayer.js
|
||||||
'url': format_url + '?secret=pqzerjlsmdkjfoiuerhsdlfknaes',
|
'url': format_url + '?secret=pqzerjlsmdkjfoiuerhsdlfknaes',
|
||||||
'format_id': format_id,
|
'format_id': format_id,
|
||||||
'preference': preference(format_id),
|
'preference': preference(format_id),
|
||||||
|
@ -11,6 +11,7 @@ from ..utils import (
|
|||||||
strip_or_none,
|
strip_or_none,
|
||||||
float_or_none,
|
float_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
merge_dicts,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -246,11 +247,15 @@ class VrtNUIE(GigyaBaseIE):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
|
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage, urlh = self._download_webpage_handle(url, display_id)
|
||||||
|
|
||||||
title = self._html_search_regex(
|
info = self._search_json_ld(webpage, display_id, default={})
|
||||||
|
|
||||||
|
# title is optional here since it may be extracted by extractor
|
||||||
|
# that is delegated from here
|
||||||
|
title = strip_or_none(self._html_search_regex(
|
||||||
r'(?ms)<h1 class="content__heading">(.+?)</h1>',
|
r'(?ms)<h1 class="content__heading">(.+?)</h1>',
|
||||||
webpage, 'title').strip()
|
webpage, 'title', default=None))
|
||||||
|
|
||||||
description = self._html_search_regex(
|
description = self._html_search_regex(
|
||||||
r'(?ms)<div class="content__description">(.+?)</div>',
|
r'(?ms)<div class="content__description">(.+?)</div>',
|
||||||
@ -276,7 +281,7 @@ class VrtNUIE(GigyaBaseIE):
|
|||||||
webpage, 'release_date', default=None))
|
webpage, 'release_date', default=None))
|
||||||
|
|
||||||
# If there's a ? or a # in the URL, remove them and everything after
|
# If there's a ? or a # in the URL, remove them and everything after
|
||||||
clean_url = url.split('?')[0].split('#')[0].strip('/')
|
clean_url = urlh.geturl().split('?')[0].split('#')[0].strip('/')
|
||||||
securevideo_url = clean_url + '.mssecurevideo.json'
|
securevideo_url = clean_url + '.mssecurevideo.json'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -295,7 +300,7 @@ class VrtNUIE(GigyaBaseIE):
|
|||||||
# the first one
|
# the first one
|
||||||
video_id = list(video.values())[0].get('videoid')
|
video_id = list(video.values())[0].get('videoid')
|
||||||
|
|
||||||
return {
|
return merge_dicts(info, {
|
||||||
'_type': 'url_transparent',
|
'_type': 'url_transparent',
|
||||||
'url': 'https://mediazone.vrt.be/api/v1/vrtvideo/assets/%s' % video_id,
|
'url': 'https://mediazone.vrt.be/api/v1/vrtvideo/assets/%s' % video_id,
|
||||||
'ie_key': CanvasIE.ie_key(),
|
'ie_key': CanvasIE.ie_key(),
|
||||||
@ -307,4 +312,4 @@ class VrtNUIE(GigyaBaseIE):
|
|||||||
'season_number': season_number,
|
'season_number': season_number,
|
||||||
'episode_number': episode_number,
|
'episode_number': episode_number,
|
||||||
'release_date': release_date,
|
'release_date': release_date,
|
||||||
}
|
})
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import compat_str
|
from ..compat import (
|
||||||
|
compat_str,
|
||||||
|
compat_HTTPError,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
js_to_json,
|
js_to_json,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
@ -13,8 +17,11 @@ from ..utils import (
|
|||||||
xpath_element,
|
xpath_element,
|
||||||
xpath_with_ns,
|
xpath_with_ns,
|
||||||
find_xpath_attr,
|
find_xpath_attr,
|
||||||
|
orderedSet,
|
||||||
|
parse_duration,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
parse_age_limit,
|
parse_age_limit,
|
||||||
|
strip_or_none,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
)
|
)
|
||||||
@ -124,15 +131,23 @@ class CBCIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
title = self._og_search_title(webpage, default=None) or self._html_search_meta(
|
||||||
|
'twitter:title', webpage, 'title', default=None) or self._html_search_regex(
|
||||||
|
r'<title>([^<]+)</title>', webpage, 'title', fatal=False)
|
||||||
entries = [
|
entries = [
|
||||||
self._extract_player_init(player_init, display_id)
|
self._extract_player_init(player_init, display_id)
|
||||||
for player_init in re.findall(r'CBC\.APP\.Caffeine\.initInstance\(({.+?})\);', webpage)]
|
for player_init in re.findall(r'CBC\.APP\.Caffeine\.initInstance\(({.+?})\);', webpage)]
|
||||||
|
media_ids = []
|
||||||
|
for media_id_re in (
|
||||||
|
r'<iframe[^>]+src="[^"]+?mediaId=(\d+)"',
|
||||||
|
r'<div[^>]+\bid=["\']player-(\d+)',
|
||||||
|
r'guid["\']\s*:\s*["\'](\d+)'):
|
||||||
|
media_ids.extend(re.findall(media_id_re, webpage))
|
||||||
entries.extend([
|
entries.extend([
|
||||||
self.url_result('cbcplayer:%s' % media_id, 'CBCPlayer', media_id)
|
self.url_result('cbcplayer:%s' % media_id, 'CBCPlayer', media_id)
|
||||||
for media_id in re.findall(r'<iframe[^>]+src="[^"]+?mediaId=(\d+)"', webpage)])
|
for media_id in orderedSet(media_ids)])
|
||||||
return self.playlist_result(
|
return self.playlist_result(
|
||||||
entries, display_id,
|
entries, display_id, strip_or_none(title),
|
||||||
self._og_search_title(webpage, fatal=False),
|
|
||||||
self._og_search_description(webpage))
|
self._og_search_description(webpage))
|
||||||
|
|
||||||
|
|
||||||
@ -204,23 +219,41 @@ class CBCWatchBaseIE(InfoExtractor):
|
|||||||
|
|
||||||
def _call_api(self, path, video_id):
|
def _call_api(self, path, video_id):
|
||||||
url = path if path.startswith('http') else self._API_BASE_URL + path
|
url = path if path.startswith('http') else self._API_BASE_URL + path
|
||||||
|
for _ in range(2):
|
||||||
|
try:
|
||||||
result = self._download_xml(url, video_id, headers={
|
result = self._download_xml(url, video_id, headers={
|
||||||
'X-Clearleap-DeviceId': self._device_id,
|
'X-Clearleap-DeviceId': self._device_id,
|
||||||
'X-Clearleap-DeviceToken': self._device_token,
|
'X-Clearleap-DeviceToken': self._device_token,
|
||||||
})
|
})
|
||||||
|
except ExtractorError as e:
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||||
|
# Device token has expired, re-acquiring device token
|
||||||
|
self._register_device()
|
||||||
|
continue
|
||||||
|
raise
|
||||||
error_message = xpath_text(result, 'userMessage') or xpath_text(result, 'systemMessage')
|
error_message = xpath_text(result, 'userMessage') or xpath_text(result, 'systemMessage')
|
||||||
if error_message:
|
if error_message:
|
||||||
raise ExtractorError('%s said: %s' % (self.IE_NAME, error_message))
|
raise ExtractorError('%s said: %s' % (self.IE_NAME, error_message))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
if not self._device_id or not self._device_token:
|
if self._valid_device_token():
|
||||||
|
return
|
||||||
device = self._downloader.cache.load('cbcwatch', 'device') or {}
|
device = self._downloader.cache.load('cbcwatch', 'device') or {}
|
||||||
self._device_id, self._device_token = device.get('id'), device.get('token')
|
self._device_id, self._device_token = device.get('id'), device.get('token')
|
||||||
if not self._device_id or not self._device_token:
|
if self._valid_device_token():
|
||||||
|
return
|
||||||
|
self._register_device()
|
||||||
|
|
||||||
|
def _valid_device_token(self):
|
||||||
|
return self._device_id and self._device_token
|
||||||
|
|
||||||
|
def _register_device(self):
|
||||||
|
self._device_id = self._device_token = None
|
||||||
result = self._download_xml(
|
result = self._download_xml(
|
||||||
self._API_BASE_URL + 'device/register',
|
self._API_BASE_URL + 'device/register',
|
||||||
None, data=b'<device><type>web</type></device>')
|
None, 'Acquiring device token',
|
||||||
|
data=b'<device><type>web</type></device>')
|
||||||
self._device_id = xpath_text(result, 'deviceId', fatal=True)
|
self._device_id = xpath_text(result, 'deviceId', fatal=True)
|
||||||
self._device_token = xpath_text(result, 'deviceToken', fatal=True)
|
self._device_token = xpath_text(result, 'deviceToken', fatal=True)
|
||||||
self._downloader.cache.store(
|
self._downloader.cache.store(
|
||||||
@ -359,3 +392,63 @@ class CBCWatchIE(CBCWatchBaseIE):
|
|||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
rss = self._call_api('web/browse/' + video_id, video_id)
|
rss = self._call_api('web/browse/' + video_id, video_id)
|
||||||
return self._parse_rss_feed(rss)
|
return self._parse_rss_feed(rss)
|
||||||
|
|
||||||
|
|
||||||
|
class CBCOlympicsIE(InfoExtractor):
|
||||||
|
IE_NAME = 'cbc.ca:olympics'
|
||||||
|
_VALID_URL = r'https?://olympics\.cbc\.ca/video/[^/]+/(?P<id>[^/?#]+)'
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://olympics.cbc.ca/video/whats-on-tv/olympic-morning-featuring-the-opening-ceremony/',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
video_id = self._hidden_inputs(webpage)['videoId']
|
||||||
|
video_doc = self._download_xml(
|
||||||
|
'https://olympics.cbc.ca/videodata/%s.xml' % video_id, video_id)
|
||||||
|
title = xpath_text(video_doc, 'title', fatal=True)
|
||||||
|
is_live = xpath_text(video_doc, 'kind') == 'Live'
|
||||||
|
if is_live:
|
||||||
|
title = self._live_title(title)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for video_source in video_doc.findall('videoSources/videoSource'):
|
||||||
|
uri = xpath_text(video_source, 'uri')
|
||||||
|
if not uri:
|
||||||
|
continue
|
||||||
|
tokenize = self._download_json(
|
||||||
|
'https://olympics.cbc.ca/api/api-akamai/tokenize',
|
||||||
|
video_id, data=json.dumps({
|
||||||
|
'VideoSource': uri,
|
||||||
|
}).encode(), headers={
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Referer': url,
|
||||||
|
# d3.VideoPlayer._init in https://olympics.cbc.ca/components/script/base.js
|
||||||
|
'Cookie': '_dvp=TK:C0ObxjerU', # AKAMAI CDN cookie
|
||||||
|
}, fatal=False)
|
||||||
|
if not tokenize:
|
||||||
|
continue
|
||||||
|
content_url = tokenize['ContentUrl']
|
||||||
|
video_source_format = video_source.get('format')
|
||||||
|
if video_source_format == 'IIS':
|
||||||
|
formats.extend(self._extract_ism_formats(
|
||||||
|
content_url, video_id, ism_id=video_source_format, fatal=False))
|
||||||
|
else:
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
content_url, video_id, 'mp4',
|
||||||
|
'm3u8' if is_live else 'm3u8_native',
|
||||||
|
m3u8_id=video_source_format, fatal=False))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'display_id': display_id,
|
||||||
|
'title': title,
|
||||||
|
'description': xpath_text(video_doc, 'description'),
|
||||||
|
'thumbnail': xpath_text(video_doc, 'thumbnailUrl'),
|
||||||
|
'duration': parse_duration(xpath_text(video_doc, 'duration')),
|
||||||
|
'formats': formats,
|
||||||
|
'is_live': is_live,
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from .theplatform import ThePlatformFeedIE
|
from .theplatform import ThePlatformFeedIE
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
find_xpath_attr,
|
find_xpath_attr,
|
||||||
xpath_element,
|
xpath_element,
|
||||||
@ -61,9 +62,10 @@ class CBSIE(CBSBaseIE):
|
|||||||
asset_types = []
|
asset_types = []
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
formats = []
|
formats = []
|
||||||
|
last_e = None
|
||||||
for item in items_data.findall('.//item'):
|
for item in items_data.findall('.//item'):
|
||||||
asset_type = xpath_text(item, 'assetType')
|
asset_type = xpath_text(item, 'assetType')
|
||||||
if not asset_type or asset_type in asset_types:
|
if not asset_type or asset_type in asset_types or asset_type in ('HLS_FPS', 'DASH_CENC'):
|
||||||
continue
|
continue
|
||||||
asset_types.append(asset_type)
|
asset_types.append(asset_type)
|
||||||
query = {
|
query = {
|
||||||
@ -74,11 +76,17 @@ class CBSIE(CBSBaseIE):
|
|||||||
query['formats'] = 'MPEG4,M3U'
|
query['formats'] = 'MPEG4,M3U'
|
||||||
elif asset_type in ('RTMP', 'WIFI', '3G'):
|
elif asset_type in ('RTMP', 'WIFI', '3G'):
|
||||||
query['formats'] = 'MPEG4,FLV'
|
query['formats'] = 'MPEG4,FLV'
|
||||||
|
try:
|
||||||
tp_formats, tp_subtitles = self._extract_theplatform_smil(
|
tp_formats, tp_subtitles = self._extract_theplatform_smil(
|
||||||
update_url_query(tp_release_url, query), content_id,
|
update_url_query(tp_release_url, query), content_id,
|
||||||
'Downloading %s SMIL data' % asset_type)
|
'Downloading %s SMIL data' % asset_type)
|
||||||
|
except ExtractorError as e:
|
||||||
|
last_e = e
|
||||||
|
continue
|
||||||
formats.extend(tp_formats)
|
formats.extend(tp_formats)
|
||||||
subtitles = self._merge_subtitles(subtitles, tp_subtitles)
|
subtitles = self._merge_subtitles(subtitles, tp_subtitles)
|
||||||
|
if last_e and not formats:
|
||||||
|
raise last_e
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
info = self._extract_theplatform_metadata(tp_path, content_id)
|
info = self._extract_theplatform_metadata(tp_path, content_id)
|
||||||
|
@ -75,10 +75,10 @@ class CBSInteractiveIE(CBSIE):
|
|||||||
webpage = self._download_webpage(url, display_id)
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
|
||||||
data_json = self._html_search_regex(
|
data_json = self._html_search_regex(
|
||||||
r"data-(?:cnet|zdnet)-video(?:-uvp(?:js)?)?-options='([^']+)'",
|
r"data(?:-(?:cnet|zdnet))?-video(?:-(?:uvp(?:js)?|player))?-options='([^']+)'",
|
||||||
webpage, 'data json')
|
webpage, 'data json')
|
||||||
data = self._parse_json(data_json, display_id)
|
data = self._parse_json(data_json, display_id)
|
||||||
vdata = data.get('video') or data['videos'][0]
|
vdata = data.get('video') or (data.get('videos') or data.get('playlist'))[0]
|
||||||
|
|
||||||
video_id = vdata['mpxRefId']
|
video_id = vdata['mpxRefId']
|
||||||
|
|
||||||
|
@ -91,12 +91,10 @@ class CBSLocalIE(AnvatoIE):
|
|||||||
|
|
||||||
info_dict = self._extract_anvato_videos(webpage, display_id)
|
info_dict = self._extract_anvato_videos(webpage, display_id)
|
||||||
|
|
||||||
time_str = self._html_search_regex(
|
timestamp = unified_timestamp(self._html_search_regex(
|
||||||
r'class="entry-date">([^<]+)<', webpage, 'released date', default=None)
|
r'class="(?:entry|post)-date"[^>]*>([^<]+)', webpage,
|
||||||
if time_str:
|
'released date', default=None)) or parse_iso8601(
|
||||||
timestamp = unified_timestamp(time_str)
|
self._html_search_meta('uploadDate', webpage))
|
||||||
else:
|
|
||||||
timestamp = parse_iso8601(self._html_search_meta('uploadDate', webpage))
|
|
||||||
|
|
||||||
info_dict.update({
|
info_dict.update({
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
|
@ -4,28 +4,35 @@ from .cbs import CBSBaseIE
|
|||||||
|
|
||||||
|
|
||||||
class CBSSportsIE(CBSBaseIE):
|
class CBSSportsIE(CBSBaseIE):
|
||||||
_VALID_URL = r'https?://(?:www\.)?cbssports\.com/video/player/[^/]+/(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:www\.)?cbssports\.com/[^/]+/(?:video|news)/(?P<id>[^/?#&]+)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.cbssports.com/video/player/videos/708337219968/0/ben-simmons-the-next-lebron?-not-so-fast',
|
'url': 'https://www.cbssports.com/nba/video/donovan-mitchell-flashes-star-potential-in-game-2-victory-over-thunder/',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '708337219968',
|
'id': '1214315075735',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Ben Simmons the next LeBron? Not so fast',
|
'title': 'Donovan Mitchell flashes star potential in Game 2 victory over Thunder',
|
||||||
'description': 'md5:854294f627921baba1f4b9a990d87197',
|
'description': 'md5:df6f48622612c2d6bd2e295ddef58def',
|
||||||
'timestamp': 1466293740,
|
'timestamp': 1524111457,
|
||||||
'upload_date': '20160618',
|
'upload_date': '20180419',
|
||||||
'uploader': 'CBSI-NEW',
|
'uploader': 'CBSI-NEW',
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
'url': 'https://www.cbssports.com/nba/news/nba-playoffs-2018-watch-76ers-vs-heat-game-3-series-schedule-tv-channel-online-stream/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _extract_video_info(self, filter_query, video_id):
|
def _extract_video_info(self, filter_query, video_id):
|
||||||
return self._extract_feed_info('dJ5BDC', 'VxxJg8Ymh8sE', filter_query, video_id)
|
return self._extract_feed_info('dJ5BDC', 'VxxJg8Ymh8sE', filter_query, video_id)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
display_id = self._match_id(url)
|
||||||
|
webpage = self._download_webpage(url, display_id)
|
||||||
|
video_id = self._search_regex(
|
||||||
|
[r'(?:=|%26)pcid%3D(\d+)', r'embedVideo(?:Container)?_(\d+)'],
|
||||||
|
webpage, 'video id')
|
||||||
return self._extract_video_info('byId=%s' % video_id, video_id)
|
return self._extract_video_info('byId=%s' % video_id, video_id)
|
||||||
|
@ -5,10 +5,12 @@ import re
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
clean_html,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
parse_duration,
|
parse_duration,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
clean_html,
|
parse_resolution,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -40,34 +42,42 @@ class CCMAIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
media_type, media_id = re.match(self._VALID_URL, url).groups()
|
media_type, media_id = re.match(self._VALID_URL, url).groups()
|
||||||
media_data = {}
|
|
||||||
formats = []
|
media = self._download_json(
|
||||||
profiles = ['pc'] if media_type == 'audio' else ['mobil', 'pc']
|
'http://dinamics.ccma.cat/pvideo/media.jsp', media_id, query={
|
||||||
for i, profile in enumerate(profiles):
|
|
||||||
md = self._download_json('http://dinamics.ccma.cat/pvideo/media.jsp', media_id, query={
|
|
||||||
'media': media_type,
|
'media': media_type,
|
||||||
'idint': media_id,
|
'idint': media_id,
|
||||||
'profile': profile,
|
})
|
||||||
}, fatal=False)
|
|
||||||
if md:
|
formats = []
|
||||||
media_data = md
|
media_url = media['media']['url']
|
||||||
media_url = media_data.get('media', {}).get('url')
|
if isinstance(media_url, list):
|
||||||
if media_url:
|
for format_ in media_url:
|
||||||
|
format_url = url_or_none(format_.get('file'))
|
||||||
|
if not format_url:
|
||||||
|
continue
|
||||||
|
label = format_.get('label')
|
||||||
|
f = parse_resolution(label)
|
||||||
|
f.update({
|
||||||
|
'url': format_url,
|
||||||
|
'format_id': label,
|
||||||
|
})
|
||||||
|
formats.append(f)
|
||||||
|
else:
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': profile,
|
|
||||||
'url': media_url,
|
'url': media_url,
|
||||||
'quality': i,
|
'vcodec': 'none' if media_type == 'audio' else None,
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
informacio = media_data['informacio']
|
informacio = media['informacio']
|
||||||
title = informacio['titol']
|
title = informacio['titol']
|
||||||
durada = informacio.get('durada', {})
|
durada = informacio.get('durada', {})
|
||||||
duration = int_or_none(durada.get('milisegons'), 1000) or parse_duration(durada.get('text'))
|
duration = int_or_none(durada.get('milisegons'), 1000) or parse_duration(durada.get('text'))
|
||||||
timestamp = parse_iso8601(informacio.get('data_emissio', {}).get('utc'))
|
timestamp = parse_iso8601(informacio.get('data_emissio', {}).get('utc'))
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
subtitols = media_data.get('subtitols', {})
|
subtitols = media.get('subtitols', {})
|
||||||
if subtitols:
|
if subtitols:
|
||||||
sub_url = subtitols.get('url')
|
sub_url = subtitols.get('url')
|
||||||
if sub_url:
|
if sub_url:
|
||||||
@ -77,7 +87,7 @@ class CCMAIE(InfoExtractor):
|
|||||||
})
|
})
|
||||||
|
|
||||||
thumbnails = []
|
thumbnails = []
|
||||||
imatges = media_data.get('imatges', {})
|
imatges = media.get('imatges', {})
|
||||||
if imatges:
|
if imatges:
|
||||||
thumbnail_url = imatges.get('url')
|
thumbnail_url = imatges.get('url')
|
||||||
if thumbnail_url:
|
if thumbnail_url:
|
||||||
|
0
youtube_dl/extractor/cda.py
Executable file → Normal file
0
youtube_dl/extractor/cda.py
Executable file → Normal file
@ -13,6 +13,7 @@ from ..utils import (
|
|||||||
float_or_none,
|
float_or_none,
|
||||||
sanitized_Request,
|
sanitized_Request,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
|
update_url_query,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
USER_AGENTS,
|
USER_AGENTS,
|
||||||
)
|
)
|
||||||
@ -107,7 +108,7 @@ class CeskaTelevizeIE(InfoExtractor):
|
|||||||
|
|
||||||
for user_agent in (None, USER_AGENTS['Safari']):
|
for user_agent in (None, USER_AGENTS['Safari']):
|
||||||
req = sanitized_Request(
|
req = sanitized_Request(
|
||||||
'http://www.ceskatelevize.cz/ivysilani/ajax/get-client-playlist',
|
'https://www.ceskatelevize.cz/ivysilani/ajax/get-client-playlist',
|
||||||
data=urlencode_postdata(data))
|
data=urlencode_postdata(data))
|
||||||
|
|
||||||
req.add_header('Content-type', 'application/x-www-form-urlencoded')
|
req.add_header('Content-type', 'application/x-www-form-urlencoded')
|
||||||
@ -265,6 +266,10 @@ class CeskaTelevizePoradyIE(InfoExtractor):
|
|||||||
# m3u8 download
|
# m3u8 download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
# iframe embed
|
||||||
|
'url': 'http://www.ceskatelevize.cz/porady/10614999031-neviditelni/21251212048/',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
@ -272,8 +277,11 @@ class CeskaTelevizePoradyIE(InfoExtractor):
|
|||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
data_url = unescapeHTML(self._search_regex(
|
data_url = update_url_query(unescapeHTML(self._search_regex(
|
||||||
r'<span[^>]*\bdata-url=(["\'])(?P<url>(?:(?!\1).)+)\1',
|
(r'<span[^>]*\bdata-url=(["\'])(?P<url>(?:(?!\1).)+)\1',
|
||||||
webpage, 'iframe player url', group='url'))
|
r'<iframe[^>]+\bsrc=(["\'])(?P<url>(?:https?:)?//(?:www\.)?ceskatelevize\.cz/ivysilani/embed/iFramePlayer\.php.*?)\1'),
|
||||||
|
webpage, 'iframe player url', group='url')), query={
|
||||||
|
'autoStart': 'true',
|
||||||
|
})
|
||||||
|
|
||||||
return self.url_result(data_url, ie=CeskaTelevizeIE.ie_key())
|
return self.url_result(data_url, ie=CeskaTelevizeIE.ie_key())
|
||||||
|
@ -31,7 +31,8 @@ class ChaturbateIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(
|
||||||
|
url, video_id, headers=self.geo_verification_headers())
|
||||||
|
|
||||||
m3u8_urls = []
|
m3u8_urls = []
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import base64
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .youtube import YoutubeIE
|
from .youtube import YoutubeIE
|
||||||
|
from ..compat import compat_b64decode
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
clean_html,
|
clean_html,
|
||||||
ExtractorError
|
ExtractorError
|
||||||
@ -58,7 +58,7 @@ class ChilloutzoneIE(InfoExtractor):
|
|||||||
|
|
||||||
base64_video_info = self._html_search_regex(
|
base64_video_info = self._html_search_regex(
|
||||||
r'var cozVidData = "(.+?)";', webpage, 'video data')
|
r'var cozVidData = "(.+?)";', webpage, 'video data')
|
||||||
decoded_video_info = base64.b64decode(base64_video_info.encode('utf-8')).decode('utf-8')
|
decoded_video_info = compat_b64decode(base64_video_info).decode('utf-8')
|
||||||
video_info_dict = json.loads(decoded_video_info)
|
video_info_dict = json.loads(decoded_video_info)
|
||||||
|
|
||||||
# get video information from dict
|
# get video information from dict
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import base64
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..compat import compat_b64decode
|
||||||
from ..utils import parse_duration
|
from ..utils import parse_duration
|
||||||
|
|
||||||
|
|
||||||
@ -44,8 +44,7 @@ class ChirbitIE(InfoExtractor):
|
|||||||
|
|
||||||
# Reverse engineered from https://chirb.it/js/chirbit.player.js (look
|
# Reverse engineered from https://chirb.it/js/chirbit.player.js (look
|
||||||
# for soundURL)
|
# for soundURL)
|
||||||
audio_url = base64.b64decode(
|
audio_url = compat_b64decode(data_fd[::-1]).decode('utf-8')
|
||||||
data_fd[::-1].encode('ascii')).decode('utf-8')
|
|
||||||
|
|
||||||
title = self._search_regex(
|
title = self._search_regex(
|
||||||
r'class=["\']chirbit-title["\'][^>]*>([^<]+)', webpage, 'title')
|
r'class=["\']chirbit-title["\'][^>]*>([^<]+)', webpage, 'title')
|
||||||
|
60
youtube_dl/extractor/cloudflarestream.py
Normal file
60
youtube_dl/extractor/cloudflarestream.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class CloudflareStreamIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'''(?x)
|
||||||
|
https?://
|
||||||
|
(?:
|
||||||
|
(?:watch\.)?cloudflarestream\.com/|
|
||||||
|
embed\.cloudflarestream\.com/embed/[^/]+\.js\?.*?\bvideo=
|
||||||
|
)
|
||||||
|
(?P<id>[\da-f]+)
|
||||||
|
'''
|
||||||
|
_TESTS = [{
|
||||||
|
'url': 'https://embed.cloudflarestream.com/embed/we4g.fla9.latest.js?video=31c9291ab41fac05471db4e73aa11717',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '31c9291ab41fac05471db4e73aa11717',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': '31c9291ab41fac05471db4e73aa11717',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://watch.cloudflarestream.com/9df17203414fd1db3e3ed74abbe936c1',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': 'https://cloudflarestream.com/31c9291ab41fac05471db4e73aa11717/manifest/video.mpd',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_urls(webpage):
|
||||||
|
return [
|
||||||
|
mobj.group('url')
|
||||||
|
for mobj in re.finditer(
|
||||||
|
r'<script[^>]+\bsrc=(["\'])(?P<url>(?:https?:)?//embed\.cloudflarestream\.com/embed/[^/]+\.js\?.*?\bvideo=[\da-f]+?.*?)\1',
|
||||||
|
webpage)]
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
|
formats = self._extract_m3u8_formats(
|
||||||
|
'https://cloudflarestream.com/%s/manifest/video.m3u8' % video_id,
|
||||||
|
video_id, 'mp4', entry_protocol='m3u8_native', m3u8_id='hls',
|
||||||
|
fatal=False)
|
||||||
|
formats.extend(self._extract_mpd_formats(
|
||||||
|
'https://cloudflarestream.com/%s/manifest/video.mpd' % video_id,
|
||||||
|
video_id, mpd_id='dash', fatal=False))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': video_id,
|
||||||
|
'formats': formats,
|
||||||
|
}
|
@ -1,15 +1,19 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..compat import (
|
||||||
|
compat_parse_qs,
|
||||||
|
compat_urllib_parse_urlparse,
|
||||||
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
float_or_none,
|
float_or_none,
|
||||||
parse_iso8601,
|
unified_timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ClypIE(InfoExtractor):
|
class ClypIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:www\.)?clyp\.it/(?P<id>[a-z0-9]+)'
|
_VALID_URL = r'https?://(?:www\.)?clyp\.it/(?P<id>[a-z0-9]+)'
|
||||||
_TEST = {
|
_TESTS = [{
|
||||||
'url': 'https://clyp.it/ojz2wfah',
|
'url': 'https://clyp.it/ojz2wfah',
|
||||||
'md5': '1d4961036c41247ecfdcc439c0cddcbb',
|
'md5': '1d4961036c41247ecfdcc439c0cddcbb',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
@ -21,13 +25,34 @@ class ClypIE(InfoExtractor):
|
|||||||
'timestamp': 1443515251,
|
'timestamp': 1443515251,
|
||||||
'upload_date': '20150929',
|
'upload_date': '20150929',
|
||||||
},
|
},
|
||||||
}
|
}, {
|
||||||
|
'url': 'https://clyp.it/b04p1odi?token=b0078e077e15835845c528a44417719d',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'b04p1odi',
|
||||||
|
'ext': 'mp3',
|
||||||
|
'title': 'GJ! (Reward Edit)',
|
||||||
|
'description': 'Metal Resistance (THE ONE edition)',
|
||||||
|
'duration': 177.789,
|
||||||
|
'timestamp': 1528241278,
|
||||||
|
'upload_date': '20180605',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True,
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
audio_id = self._match_id(url)
|
audio_id = self._match_id(url)
|
||||||
|
|
||||||
|
qs = compat_parse_qs(compat_urllib_parse_urlparse(url).query)
|
||||||
|
token = qs.get('token', [None])[0]
|
||||||
|
|
||||||
|
query = {}
|
||||||
|
if token:
|
||||||
|
query['token'] = token
|
||||||
|
|
||||||
metadata = self._download_json(
|
metadata = self._download_json(
|
||||||
'https://api.clyp.it/%s' % audio_id, audio_id)
|
'https://api.clyp.it/%s' % audio_id, audio_id, query=query)
|
||||||
|
|
||||||
formats = []
|
formats = []
|
||||||
for secure in ('', 'Secure'):
|
for secure in ('', 'Secure'):
|
||||||
@ -45,7 +70,7 @@ class ClypIE(InfoExtractor):
|
|||||||
title = metadata['Title']
|
title = metadata['Title']
|
||||||
description = metadata.get('Description')
|
description = metadata.get('Description')
|
||||||
duration = float_or_none(metadata.get('Duration'))
|
duration = float_or_none(metadata.get('Duration'))
|
||||||
timestamp = parse_iso8601(metadata.get('DateCreated'))
|
timestamp = unified_timestamp(metadata.get('DateCreated'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': audio_id,
|
'id': audio_id,
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import (
|
|
||||||
float_or_none,
|
|
||||||
int_or_none,
|
|
||||||
sanitized_Request,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CollegeRamaIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://collegerama\.tudelft\.nl/Mediasite/Play/(?P<id>[\da-f]+)'
|
|
||||||
_TESTS = [
|
|
||||||
{
|
|
||||||
'url': 'https://collegerama.tudelft.nl/Mediasite/Play/585a43626e544bdd97aeb71a0ec907a01d',
|
|
||||||
'md5': '481fda1c11f67588c0d9d8fbdced4e39',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '585a43626e544bdd97aeb71a0ec907a01d',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Een nieuwe wereld: waarden, bewustzijn en techniek van de mensheid 2.0.',
|
|
||||||
'description': '',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg(?:\?.*?)?$',
|
|
||||||
'duration': 7713.088,
|
|
||||||
'timestamp': 1413309600,
|
|
||||||
'upload_date': '20141014',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'url': 'https://collegerama.tudelft.nl/Mediasite/Play/86a9ea9f53e149079fbdb4202b521ed21d?catalog=fd32fd35-6c99-466c-89d4-cd3c431bc8a4',
|
|
||||||
'md5': 'ef1fdded95bdf19b12c5999949419c92',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '86a9ea9f53e149079fbdb4202b521ed21d',
|
|
||||||
'ext': 'wmv',
|
|
||||||
'title': '64ste Vakantiecursus: Afvalwater',
|
|
||||||
'description': 'md5:7fd774865cc69d972f542b157c328305',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg(?:\?.*?)?$',
|
|
||||||
'duration': 10853,
|
|
||||||
'timestamp': 1326446400,
|
|
||||||
'upload_date': '20120113',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
|
|
||||||
player_options_request = {
|
|
||||||
'getPlayerOptionsRequest': {
|
|
||||||
'ResourceId': video_id,
|
|
||||||
'QueryString': '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
request = sanitized_Request(
|
|
||||||
'http://collegerama.tudelft.nl/Mediasite/PlayerService/PlayerService.svc/json/GetPlayerOptions',
|
|
||||||
json.dumps(player_options_request))
|
|
||||||
request.add_header('Content-Type', 'application/json')
|
|
||||||
|
|
||||||
player_options = self._download_json(request, video_id)
|
|
||||||
|
|
||||||
presentation = player_options['d']['Presentation']
|
|
||||||
title = presentation['Title']
|
|
||||||
description = presentation.get('Description')
|
|
||||||
thumbnail = None
|
|
||||||
duration = float_or_none(presentation.get('Duration'), 1000)
|
|
||||||
timestamp = int_or_none(presentation.get('UnixTime'), 1000)
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
for stream in presentation['Streams']:
|
|
||||||
for video in stream['VideoUrls']:
|
|
||||||
thumbnail_url = stream.get('ThumbnailUrl')
|
|
||||||
if thumbnail_url:
|
|
||||||
thumbnail = 'http://collegerama.tudelft.nl' + thumbnail_url
|
|
||||||
format_id = video['MediaType']
|
|
||||||
if format_id == 'SS':
|
|
||||||
continue
|
|
||||||
formats.append({
|
|
||||||
'url': video['Location'],
|
|
||||||
'format_id': format_id,
|
|
||||||
})
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
'duration': duration,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
@ -19,6 +19,7 @@ from ..compat import (
|
|||||||
compat_cookies,
|
compat_cookies,
|
||||||
compat_etree_fromstring,
|
compat_etree_fromstring,
|
||||||
compat_getpass,
|
compat_getpass,
|
||||||
|
compat_integer_types,
|
||||||
compat_http_client,
|
compat_http_client,
|
||||||
compat_os_name,
|
compat_os_name,
|
||||||
compat_str,
|
compat_str,
|
||||||
@ -51,6 +52,7 @@ from ..utils import (
|
|||||||
GeoUtils,
|
GeoUtils,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
|
JSON_LD_RE,
|
||||||
mimetype2ext,
|
mimetype2ext,
|
||||||
orderedSet,
|
orderedSet,
|
||||||
parse_codecs,
|
parse_codecs,
|
||||||
@ -174,6 +176,8 @@ class InfoExtractor(object):
|
|||||||
width : height ratio as float.
|
width : height ratio as float.
|
||||||
* no_resume The server does not support resuming the
|
* no_resume The server does not support resuming the
|
||||||
(HTTP or RTMP) download. Boolean.
|
(HTTP or RTMP) download. Boolean.
|
||||||
|
* downloader_options A dictionary of downloader options as
|
||||||
|
described in FileDownloader
|
||||||
|
|
||||||
url: Final video URL.
|
url: Final video URL.
|
||||||
ext: Video filename extension.
|
ext: Video filename extension.
|
||||||
@ -207,6 +211,11 @@ class InfoExtractor(object):
|
|||||||
If not explicitly set, calculated from timestamp.
|
If not explicitly set, calculated from timestamp.
|
||||||
uploader_id: Nickname or id of the video uploader.
|
uploader_id: Nickname or id of the video uploader.
|
||||||
uploader_url: Full URL to a personal webpage of the video uploader.
|
uploader_url: Full URL to a personal webpage of the video uploader.
|
||||||
|
channel: Full name of the channel the video is uploaded on.
|
||||||
|
Note that channel fields may or may not repeat uploader
|
||||||
|
fields. This depends on a particular extractor.
|
||||||
|
channel_id: Id of the channel.
|
||||||
|
channel_url: Full URL to a channel webpage.
|
||||||
location: Physical location where the video was filmed.
|
location: Physical location where the video was filmed.
|
||||||
subtitles: The available subtitles as a dictionary in the format
|
subtitles: The available subtitles as a dictionary in the format
|
||||||
{tag: subformats}. "tag" is usually a language code, and
|
{tag: subformats}. "tag" is usually a language code, and
|
||||||
@ -301,8 +310,9 @@ class InfoExtractor(object):
|
|||||||
There must be a key "entries", which is a list, an iterable, or a PagedList
|
There must be a key "entries", which is a list, an iterable, or a PagedList
|
||||||
object, each element of which is a valid dictionary by this specification.
|
object, each element of which is a valid dictionary by this specification.
|
||||||
|
|
||||||
Additionally, playlists can have "title", "description" and "id" attributes
|
Additionally, playlists can have "id", "title", "description", "uploader",
|
||||||
with the same semantics as videos (see above).
|
"uploader_id", "uploader_url" attributes with the same semantics as videos
|
||||||
|
(see above).
|
||||||
|
|
||||||
|
|
||||||
_type "multi_video" indicates that there are multiple videos that
|
_type "multi_video" indicates that there are multiple videos that
|
||||||
@ -336,15 +346,17 @@ class InfoExtractor(object):
|
|||||||
_GEO_BYPASS attribute may be set to False in order to disable
|
_GEO_BYPASS attribute may be set to False in order to disable
|
||||||
geo restriction bypass mechanisms for a particular extractor.
|
geo restriction bypass mechanisms for a particular extractor.
|
||||||
Though it won't disable explicit geo restriction bypass based on
|
Though it won't disable explicit geo restriction bypass based on
|
||||||
country code provided with geo_bypass_country. (experimental)
|
country code provided with geo_bypass_country.
|
||||||
|
|
||||||
_GEO_COUNTRIES attribute may contain a list of presumably geo unrestricted
|
_GEO_COUNTRIES attribute may contain a list of presumably geo unrestricted
|
||||||
countries for this extractor. One of these countries will be used by
|
countries for this extractor. One of these countries will be used by
|
||||||
geo restriction bypass mechanism right away in order to bypass
|
geo restriction bypass mechanism right away in order to bypass
|
||||||
geo restriction, of course, if the mechanism is not disabled. (experimental)
|
geo restriction, of course, if the mechanism is not disabled.
|
||||||
|
|
||||||
NB: both these geo attributes are experimental and may change in future
|
_GEO_IP_BLOCKS attribute may contain a list of presumably geo unrestricted
|
||||||
or be completely removed.
|
IP blocks in CIDR notation for this extractor. One of these IP blocks
|
||||||
|
will be used by geo restriction bypass mechanism similarly
|
||||||
|
to _GEO_COUNTRIES.
|
||||||
|
|
||||||
Finally, the _WORKING attribute should be set to False for broken IEs
|
Finally, the _WORKING attribute should be set to False for broken IEs
|
||||||
in order to warn the users and skip the tests.
|
in order to warn the users and skip the tests.
|
||||||
@ -355,6 +367,7 @@ class InfoExtractor(object):
|
|||||||
_x_forwarded_for_ip = None
|
_x_forwarded_for_ip = None
|
||||||
_GEO_BYPASS = True
|
_GEO_BYPASS = True
|
||||||
_GEO_COUNTRIES = None
|
_GEO_COUNTRIES = None
|
||||||
|
_GEO_IP_BLOCKS = None
|
||||||
_WORKING = True
|
_WORKING = True
|
||||||
|
|
||||||
def __init__(self, downloader=None):
|
def __init__(self, downloader=None):
|
||||||
@ -389,12 +402,15 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""Initializes an instance (authentication, etc)."""
|
"""Initializes an instance (authentication, etc)."""
|
||||||
self._initialize_geo_bypass(self._GEO_COUNTRIES)
|
self._initialize_geo_bypass({
|
||||||
|
'countries': self._GEO_COUNTRIES,
|
||||||
|
'ip_blocks': self._GEO_IP_BLOCKS,
|
||||||
|
})
|
||||||
if not self._ready:
|
if not self._ready:
|
||||||
self._real_initialize()
|
self._real_initialize()
|
||||||
self._ready = True
|
self._ready = True
|
||||||
|
|
||||||
def _initialize_geo_bypass(self, countries):
|
def _initialize_geo_bypass(self, geo_bypass_context):
|
||||||
"""
|
"""
|
||||||
Initialize geo restriction bypass mechanism.
|
Initialize geo restriction bypass mechanism.
|
||||||
|
|
||||||
@ -405,28 +421,82 @@ class InfoExtractor(object):
|
|||||||
HTTP requests.
|
HTTP requests.
|
||||||
|
|
||||||
This method will be used for initial geo bypass mechanism initialization
|
This method will be used for initial geo bypass mechanism initialization
|
||||||
during the instance initialization with _GEO_COUNTRIES.
|
during the instance initialization with _GEO_COUNTRIES and
|
||||||
|
_GEO_IP_BLOCKS.
|
||||||
|
|
||||||
You may also manually call it from extractor's code if geo countries
|
You may also manually call it from extractor's code if geo bypass
|
||||||
information is not available beforehand (e.g. obtained during
|
information is not available beforehand (e.g. obtained during
|
||||||
extraction) or due to some another reason.
|
extraction) or due to some other reason. In this case you should pass
|
||||||
|
this information in geo bypass context passed as first argument. It may
|
||||||
|
contain following fields:
|
||||||
|
|
||||||
|
countries: List of geo unrestricted countries (similar
|
||||||
|
to _GEO_COUNTRIES)
|
||||||
|
ip_blocks: List of geo unrestricted IP blocks in CIDR notation
|
||||||
|
(similar to _GEO_IP_BLOCKS)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not self._x_forwarded_for_ip:
|
if not self._x_forwarded_for_ip:
|
||||||
country_code = self._downloader.params.get('geo_bypass_country', None)
|
|
||||||
# If there is no explicit country for geo bypass specified and
|
# Geo bypass mechanism is explicitly disabled by user
|
||||||
# the extractor is known to be geo restricted let's fake IP
|
if not self._downloader.params.get('geo_bypass', True):
|
||||||
# as X-Forwarded-For right away.
|
return
|
||||||
if (not country_code and
|
|
||||||
self._GEO_BYPASS and
|
if not geo_bypass_context:
|
||||||
self._downloader.params.get('geo_bypass', True) and
|
geo_bypass_context = {}
|
||||||
countries):
|
|
||||||
country_code = random.choice(countries)
|
# Backward compatibility: previously _initialize_geo_bypass
|
||||||
if country_code:
|
# expected a list of countries, some 3rd party code may still use
|
||||||
self._x_forwarded_for_ip = GeoUtils.random_ipv4(country_code)
|
# it this way
|
||||||
|
if isinstance(geo_bypass_context, (list, tuple)):
|
||||||
|
geo_bypass_context = {
|
||||||
|
'countries': geo_bypass_context,
|
||||||
|
}
|
||||||
|
|
||||||
|
# The whole point of geo bypass mechanism is to fake IP
|
||||||
|
# as X-Forwarded-For HTTP header based on some IP block or
|
||||||
|
# country code.
|
||||||
|
|
||||||
|
# Path 1: bypassing based on IP block in CIDR notation
|
||||||
|
|
||||||
|
# Explicit IP block specified by user, use it right away
|
||||||
|
# regardless of whether extractor is geo bypassable or not
|
||||||
|
ip_block = self._downloader.params.get('geo_bypass_ip_block', None)
|
||||||
|
|
||||||
|
# Otherwise use random IP block from geo bypass context but only
|
||||||
|
# if extractor is known as geo bypassable
|
||||||
|
if not ip_block:
|
||||||
|
ip_blocks = geo_bypass_context.get('ip_blocks')
|
||||||
|
if self._GEO_BYPASS and ip_blocks:
|
||||||
|
ip_block = random.choice(ip_blocks)
|
||||||
|
|
||||||
|
if ip_block:
|
||||||
|
self._x_forwarded_for_ip = GeoUtils.random_ipv4(ip_block)
|
||||||
|
if self._downloader.params.get('verbose', False):
|
||||||
|
self._downloader.to_screen(
|
||||||
|
'[debug] Using fake IP %s as X-Forwarded-For.'
|
||||||
|
% self._x_forwarded_for_ip)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Path 2: bypassing based on country code
|
||||||
|
|
||||||
|
# Explicit country code specified by user, use it right away
|
||||||
|
# regardless of whether extractor is geo bypassable or not
|
||||||
|
country = self._downloader.params.get('geo_bypass_country', None)
|
||||||
|
|
||||||
|
# Otherwise use random country code from geo bypass context but
|
||||||
|
# only if extractor is known as geo bypassable
|
||||||
|
if not country:
|
||||||
|
countries = geo_bypass_context.get('countries')
|
||||||
|
if self._GEO_BYPASS and countries:
|
||||||
|
country = random.choice(countries)
|
||||||
|
|
||||||
|
if country:
|
||||||
|
self._x_forwarded_for_ip = GeoUtils.random_ipv4(country)
|
||||||
if self._downloader.params.get('verbose', False):
|
if self._downloader.params.get('verbose', False):
|
||||||
self._downloader.to_screen(
|
self._downloader.to_screen(
|
||||||
'[debug] Using fake IP %s (%s) as X-Forwarded-For.'
|
'[debug] Using fake IP %s (%s) as X-Forwarded-For.'
|
||||||
% (self._x_forwarded_for_ip, country_code.upper()))
|
% (self._x_forwarded_for_ip, country.upper()))
|
||||||
|
|
||||||
def extract(self, url):
|
def extract(self, url):
|
||||||
"""Extracts URL information and returns it in list of dicts."""
|
"""Extracts URL information and returns it in list of dicts."""
|
||||||
@ -485,8 +555,26 @@ class InfoExtractor(object):
|
|||||||
def IE_NAME(self):
|
def IE_NAME(self):
|
||||||
return compat_str(type(self).__name__[:-2])
|
return compat_str(type(self).__name__[:-2])
|
||||||
|
|
||||||
def _request_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, data=None, headers={}, query={}):
|
@staticmethod
|
||||||
""" Returns the response handle """
|
def __can_accept_status_code(err, expected_status):
|
||||||
|
assert isinstance(err, compat_urllib_error.HTTPError)
|
||||||
|
if expected_status is None:
|
||||||
|
return False
|
||||||
|
if isinstance(expected_status, compat_integer_types):
|
||||||
|
return err.code == expected_status
|
||||||
|
elif isinstance(expected_status, (list, tuple)):
|
||||||
|
return err.code in expected_status
|
||||||
|
elif callable(expected_status):
|
||||||
|
return expected_status(err.code) is True
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
def _request_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, data=None, headers={}, query={}, expected_status=None):
|
||||||
|
"""
|
||||||
|
Return the response handle.
|
||||||
|
|
||||||
|
See _download_webpage docstring for arguments specification.
|
||||||
|
"""
|
||||||
if note is None:
|
if note is None:
|
||||||
self.report_download_webpage(video_id)
|
self.report_download_webpage(video_id)
|
||||||
elif note is not False:
|
elif note is not False:
|
||||||
@ -494,6 +582,16 @@ class InfoExtractor(object):
|
|||||||
self.to_screen('%s' % (note,))
|
self.to_screen('%s' % (note,))
|
||||||
else:
|
else:
|
||||||
self.to_screen('%s: %s' % (video_id, note))
|
self.to_screen('%s: %s' % (video_id, note))
|
||||||
|
|
||||||
|
# Some sites check X-Forwarded-For HTTP header in order to figure out
|
||||||
|
# the origin of the client behind proxy. This allows bypassing geo
|
||||||
|
# restriction by faking this header's value to IP that belongs to some
|
||||||
|
# geo unrestricted country. We will do so once we encounter any
|
||||||
|
# geo restriction error.
|
||||||
|
if self._x_forwarded_for_ip:
|
||||||
|
if 'X-Forwarded-For' not in headers:
|
||||||
|
headers['X-Forwarded-For'] = self._x_forwarded_for_ip
|
||||||
|
|
||||||
if isinstance(url_or_request, compat_urllib_request.Request):
|
if isinstance(url_or_request, compat_urllib_request.Request):
|
||||||
url_or_request = update_Request(
|
url_or_request = update_Request(
|
||||||
url_or_request, data=data, headers=headers, query=query)
|
url_or_request, data=data, headers=headers, query=query)
|
||||||
@ -505,6 +603,10 @@ class InfoExtractor(object):
|
|||||||
try:
|
try:
|
||||||
return self._downloader.urlopen(url_or_request)
|
return self._downloader.urlopen(url_or_request)
|
||||||
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
|
except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
|
||||||
|
if isinstance(err, compat_urllib_error.HTTPError):
|
||||||
|
if self.__can_accept_status_code(err, expected_status):
|
||||||
|
return err.fp
|
||||||
|
|
||||||
if errnote is False:
|
if errnote is False:
|
||||||
return False
|
return False
|
||||||
if errnote is None:
|
if errnote is None:
|
||||||
@ -517,22 +619,17 @@ class InfoExtractor(object):
|
|||||||
self._downloader.report_warning(errmsg)
|
self._downloader.report_warning(errmsg)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None, fatal=True, encoding=None, data=None, headers={}, query={}):
|
def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None, fatal=True, encoding=None, data=None, headers={}, query={}, expected_status=None):
|
||||||
""" Returns a tuple (page content as string, URL handle) """
|
"""
|
||||||
|
Return a tuple (page content as string, URL handle).
|
||||||
|
|
||||||
|
See _download_webpage docstring for arguments specification.
|
||||||
|
"""
|
||||||
# Strip hashes from the URL (#1038)
|
# Strip hashes from the URL (#1038)
|
||||||
if isinstance(url_or_request, (compat_str, str)):
|
if isinstance(url_or_request, (compat_str, str)):
|
||||||
url_or_request = url_or_request.partition('#')[0]
|
url_or_request = url_or_request.partition('#')[0]
|
||||||
|
|
||||||
# Some sites check X-Forwarded-For HTTP header in order to figure out
|
urlh = self._request_webpage(url_or_request, video_id, note, errnote, fatal, data=data, headers=headers, query=query, expected_status=expected_status)
|
||||||
# the origin of the client behind proxy. This allows bypassing geo
|
|
||||||
# restriction by faking this header's value to IP that belongs to some
|
|
||||||
# geo unrestricted country. We will do so once we encounter any
|
|
||||||
# geo restriction error.
|
|
||||||
if self._x_forwarded_for_ip:
|
|
||||||
if 'X-Forwarded-For' not in headers:
|
|
||||||
headers['X-Forwarded-For'] = self._x_forwarded_for_ip
|
|
||||||
|
|
||||||
urlh = self._request_webpage(url_or_request, video_id, note, errnote, fatal, data=data, headers=headers, query=query)
|
|
||||||
if urlh is False:
|
if urlh is False:
|
||||||
assert not fatal
|
assert not fatal
|
||||||
return False
|
return False
|
||||||
@ -621,13 +718,52 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def _download_webpage(self, url_or_request, video_id, note=None, errnote=None, fatal=True, tries=1, timeout=5, encoding=None, data=None, headers={}, query={}):
|
def _download_webpage(
|
||||||
""" Returns the data of the page as a string """
|
self, url_or_request, video_id, note=None, errnote=None,
|
||||||
|
fatal=True, tries=1, timeout=5, encoding=None, data=None,
|
||||||
|
headers={}, query={}, expected_status=None):
|
||||||
|
"""
|
||||||
|
Return the data of the page as a string.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
url_or_request -- plain text URL as a string or
|
||||||
|
a compat_urllib_request.Requestobject
|
||||||
|
video_id -- Video/playlist/item identifier (string)
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
note -- note printed before downloading (string)
|
||||||
|
errnote -- note printed in case of an error (string)
|
||||||
|
fatal -- flag denoting whether error should be considered fatal,
|
||||||
|
i.e. whether it should cause ExtractionError to be raised,
|
||||||
|
otherwise a warning will be reported and extraction continued
|
||||||
|
tries -- number of tries
|
||||||
|
timeout -- sleep interval between tries
|
||||||
|
encoding -- encoding for a page content decoding, guessed automatically
|
||||||
|
when not explicitly specified
|
||||||
|
data -- POST data (bytes)
|
||||||
|
headers -- HTTP headers (dict)
|
||||||
|
query -- URL query (dict)
|
||||||
|
expected_status -- allows to accept failed HTTP requests (non 2xx
|
||||||
|
status code) by explicitly specifying a set of accepted status
|
||||||
|
codes. Can be any of the following entities:
|
||||||
|
- an integer type specifying an exact failed status code to
|
||||||
|
accept
|
||||||
|
- a list or a tuple of integer types specifying a list of
|
||||||
|
failed status codes to accept
|
||||||
|
- a callable accepting an actual failed status code and
|
||||||
|
returning True if it should be accepted
|
||||||
|
Note that this argument does not affect success status codes (2xx)
|
||||||
|
which are always accepted.
|
||||||
|
"""
|
||||||
|
|
||||||
success = False
|
success = False
|
||||||
try_count = 0
|
try_count = 0
|
||||||
while success is False:
|
while success is False:
|
||||||
try:
|
try:
|
||||||
res = self._download_webpage_handle(url_or_request, video_id, note, errnote, fatal, encoding=encoding, data=data, headers=headers, query=query)
|
res = self._download_webpage_handle(
|
||||||
|
url_or_request, video_id, note, errnote, fatal,
|
||||||
|
encoding=encoding, data=data, headers=headers, query=query,
|
||||||
|
expected_status=expected_status)
|
||||||
success = True
|
success = True
|
||||||
except compat_http_client.IncompleteRead as e:
|
except compat_http_client.IncompleteRead as e:
|
||||||
try_count += 1
|
try_count += 1
|
||||||
@ -640,19 +776,43 @@ class InfoExtractor(object):
|
|||||||
content, _ = res
|
content, _ = res
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def _download_xml(self, url_or_request, video_id,
|
def _download_xml_handle(
|
||||||
note='Downloading XML', errnote='Unable to download XML',
|
self, url_or_request, video_id, note='Downloading XML',
|
||||||
transform_source=None, fatal=True, encoding=None,
|
errnote='Unable to download XML', transform_source=None,
|
||||||
data=None, headers={}, query={}):
|
fatal=True, encoding=None, data=None, headers={}, query={},
|
||||||
"""Return the xml as an xml.etree.ElementTree.Element"""
|
expected_status=None):
|
||||||
xml_string = self._download_webpage(
|
"""
|
||||||
|
Return a tuple (xml as an xml.etree.ElementTree.Element, URL handle).
|
||||||
|
|
||||||
|
See _download_webpage docstring for arguments specification.
|
||||||
|
"""
|
||||||
|
res = self._download_webpage_handle(
|
||||||
url_or_request, video_id, note, errnote, fatal=fatal,
|
url_or_request, video_id, note, errnote, fatal=fatal,
|
||||||
encoding=encoding, data=data, headers=headers, query=query)
|
encoding=encoding, data=data, headers=headers, query=query,
|
||||||
if xml_string is False:
|
expected_status=expected_status)
|
||||||
return xml_string
|
if res is False:
|
||||||
|
return res
|
||||||
|
xml_string, urlh = res
|
||||||
return self._parse_xml(
|
return self._parse_xml(
|
||||||
xml_string, video_id, transform_source=transform_source,
|
xml_string, video_id, transform_source=transform_source,
|
||||||
fatal=fatal)
|
fatal=fatal), urlh
|
||||||
|
|
||||||
|
def _download_xml(
|
||||||
|
self, url_or_request, video_id,
|
||||||
|
note='Downloading XML', errnote='Unable to download XML',
|
||||||
|
transform_source=None, fatal=True, encoding=None,
|
||||||
|
data=None, headers={}, query={}, expected_status=None):
|
||||||
|
"""
|
||||||
|
Return the xml as an xml.etree.ElementTree.Element.
|
||||||
|
|
||||||
|
See _download_webpage docstring for arguments specification.
|
||||||
|
"""
|
||||||
|
res = self._download_xml_handle(
|
||||||
|
url_or_request, video_id, note=note, errnote=errnote,
|
||||||
|
transform_source=transform_source, fatal=fatal, encoding=encoding,
|
||||||
|
data=data, headers=headers, query=query,
|
||||||
|
expected_status=expected_status)
|
||||||
|
return res if res is False else res[0]
|
||||||
|
|
||||||
def _parse_xml(self, xml_string, video_id, transform_source=None, fatal=True):
|
def _parse_xml(self, xml_string, video_id, transform_source=None, fatal=True):
|
||||||
if transform_source:
|
if transform_source:
|
||||||
@ -666,18 +826,43 @@ class InfoExtractor(object):
|
|||||||
else:
|
else:
|
||||||
self.report_warning(errmsg + str(ve))
|
self.report_warning(errmsg + str(ve))
|
||||||
|
|
||||||
def _download_json(self, url_or_request, video_id,
|
def _download_json_handle(
|
||||||
note='Downloading JSON metadata',
|
self, url_or_request, video_id, note='Downloading JSON metadata',
|
||||||
errnote='Unable to download JSON metadata',
|
errnote='Unable to download JSON metadata', transform_source=None,
|
||||||
transform_source=None,
|
fatal=True, encoding=None, data=None, headers={}, query={},
|
||||||
fatal=True, encoding=None, data=None, headers={}, query={}):
|
expected_status=None):
|
||||||
json_string = self._download_webpage(
|
"""
|
||||||
|
Return a tuple (JSON object, URL handle).
|
||||||
|
|
||||||
|
See _download_webpage docstring for arguments specification.
|
||||||
|
"""
|
||||||
|
res = self._download_webpage_handle(
|
||||||
url_or_request, video_id, note, errnote, fatal=fatal,
|
url_or_request, video_id, note, errnote, fatal=fatal,
|
||||||
encoding=encoding, data=data, headers=headers, query=query)
|
encoding=encoding, data=data, headers=headers, query=query,
|
||||||
if (not fatal) and json_string is False:
|
expected_status=expected_status)
|
||||||
return None
|
if res is False:
|
||||||
|
return res
|
||||||
|
json_string, urlh = res
|
||||||
return self._parse_json(
|
return self._parse_json(
|
||||||
json_string, video_id, transform_source=transform_source, fatal=fatal)
|
json_string, video_id, transform_source=transform_source,
|
||||||
|
fatal=fatal), urlh
|
||||||
|
|
||||||
|
def _download_json(
|
||||||
|
self, url_or_request, video_id, note='Downloading JSON metadata',
|
||||||
|
errnote='Unable to download JSON metadata', transform_source=None,
|
||||||
|
fatal=True, encoding=None, data=None, headers={}, query={},
|
||||||
|
expected_status=None):
|
||||||
|
"""
|
||||||
|
Return the JSON object as a dict.
|
||||||
|
|
||||||
|
See _download_webpage docstring for arguments specification.
|
||||||
|
"""
|
||||||
|
res = self._download_json_handle(
|
||||||
|
url_or_request, video_id, note=note, errnote=errnote,
|
||||||
|
transform_source=transform_source, fatal=fatal, encoding=encoding,
|
||||||
|
data=data, headers=headers, query=query,
|
||||||
|
expected_status=expected_status)
|
||||||
|
return res if res is False else res[0]
|
||||||
|
|
||||||
def _parse_json(self, json_string, video_id, transform_source=None, fatal=True):
|
def _parse_json(self, json_string, video_id, transform_source=None, fatal=True):
|
||||||
if transform_source:
|
if transform_source:
|
||||||
@ -970,8 +1155,7 @@ class InfoExtractor(object):
|
|||||||
|
|
||||||
def _search_json_ld(self, html, video_id, expected_type=None, **kwargs):
|
def _search_json_ld(self, html, video_id, expected_type=None, **kwargs):
|
||||||
json_ld = self._search_regex(
|
json_ld = self._search_regex(
|
||||||
r'(?s)<script[^>]+type=(["\'])application/ld\+json\1[^>]*>(?P<json_ld>.+?)</script>',
|
JSON_LD_RE, html, 'JSON-LD', group='json_ld', **kwargs)
|
||||||
html, 'JSON-LD', group='json_ld', **kwargs)
|
|
||||||
default = kwargs.get('default', NO_DEFAULT)
|
default = kwargs.get('default', NO_DEFAULT)
|
||||||
if not json_ld:
|
if not json_ld:
|
||||||
return default if default is not NO_DEFAULT else {}
|
return default if default is not NO_DEFAULT else {}
|
||||||
@ -992,6 +1176,40 @@ class InfoExtractor(object):
|
|||||||
if isinstance(json_ld, dict):
|
if isinstance(json_ld, dict):
|
||||||
json_ld = [json_ld]
|
json_ld = [json_ld]
|
||||||
|
|
||||||
|
INTERACTION_TYPE_MAP = {
|
||||||
|
'CommentAction': 'comment',
|
||||||
|
'AgreeAction': 'like',
|
||||||
|
'DisagreeAction': 'dislike',
|
||||||
|
'LikeAction': 'like',
|
||||||
|
'DislikeAction': 'dislike',
|
||||||
|
'ListenAction': 'view',
|
||||||
|
'WatchAction': 'view',
|
||||||
|
'ViewAction': 'view',
|
||||||
|
}
|
||||||
|
|
||||||
|
def extract_interaction_statistic(e):
|
||||||
|
interaction_statistic = e.get('interactionStatistic')
|
||||||
|
if not isinstance(interaction_statistic, list):
|
||||||
|
return
|
||||||
|
for is_e in interaction_statistic:
|
||||||
|
if not isinstance(is_e, dict):
|
||||||
|
continue
|
||||||
|
if is_e.get('@type') != 'InteractionCounter':
|
||||||
|
continue
|
||||||
|
interaction_type = is_e.get('interactionType')
|
||||||
|
if not isinstance(interaction_type, compat_str):
|
||||||
|
continue
|
||||||
|
interaction_count = int_or_none(is_e.get('userInteractionCount'))
|
||||||
|
if interaction_count is None:
|
||||||
|
continue
|
||||||
|
count_kind = INTERACTION_TYPE_MAP.get(interaction_type.split('/')[-1])
|
||||||
|
if not count_kind:
|
||||||
|
continue
|
||||||
|
count_key = '%s_count' % count_kind
|
||||||
|
if info.get(count_key) is not None:
|
||||||
|
continue
|
||||||
|
info[count_key] = interaction_count
|
||||||
|
|
||||||
def extract_video_object(e):
|
def extract_video_object(e):
|
||||||
assert e['@type'] == 'VideoObject'
|
assert e['@type'] == 'VideoObject'
|
||||||
info.update({
|
info.update({
|
||||||
@ -1007,9 +1225,10 @@ class InfoExtractor(object):
|
|||||||
'height': int_or_none(e.get('height')),
|
'height': int_or_none(e.get('height')),
|
||||||
'view_count': int_or_none(e.get('interactionCount')),
|
'view_count': int_or_none(e.get('interactionCount')),
|
||||||
})
|
})
|
||||||
|
extract_interaction_statistic(e)
|
||||||
|
|
||||||
for e in json_ld:
|
for e in json_ld:
|
||||||
if e.get('@context') == 'http://schema.org':
|
if isinstance(e.get('@context'), compat_str) and re.match(r'^https?://schema.org/?$', e.get('@context')):
|
||||||
item_type = e.get('@type')
|
item_type = e.get('@type')
|
||||||
if expected_type is not None and expected_type != item_type:
|
if expected_type is not None and expected_type != item_type:
|
||||||
return info
|
return info
|
||||||
@ -1025,7 +1244,7 @@ class InfoExtractor(object):
|
|||||||
part_of_series = e.get('partOfSeries') or e.get('partOfTVSeries')
|
part_of_series = e.get('partOfSeries') or e.get('partOfTVSeries')
|
||||||
if isinstance(part_of_series, dict) and part_of_series.get('@type') in ('TVSeries', 'Series', 'CreativeWorkSeries'):
|
if isinstance(part_of_series, dict) and part_of_series.get('@type') in ('TVSeries', 'Series', 'CreativeWorkSeries'):
|
||||||
info['series'] = unescapeHTML(part_of_series.get('name'))
|
info['series'] = unescapeHTML(part_of_series.get('name'))
|
||||||
elif item_type == 'Article':
|
elif item_type in ('Article', 'NewsArticle'):
|
||||||
info.update({
|
info.update({
|
||||||
'timestamp': parse_iso8601(e.get('datePublished')),
|
'timestamp': parse_iso8601(e.get('datePublished')),
|
||||||
'title': unescapeHTML(e.get('headline')),
|
'title': unescapeHTML(e.get('headline')),
|
||||||
@ -1487,9 +1706,9 @@ class InfoExtractor(object):
|
|||||||
# However, this is not always respected, for example, [2]
|
# However, this is not always respected, for example, [2]
|
||||||
# contains EXT-X-STREAM-INF tag which references AUDIO
|
# contains EXT-X-STREAM-INF tag which references AUDIO
|
||||||
# rendition group but does not have CODECS and despite
|
# rendition group but does not have CODECS and despite
|
||||||
# referencing audio group an audio group, it represents
|
# referencing an audio group it represents a complete
|
||||||
# a complete (with audio and video) format. So, for such cases
|
# (with audio and video) format. So, for such cases we will
|
||||||
# we will ignore references to rendition groups and treat them
|
# ignore references to rendition groups and treat them
|
||||||
# as complete formats.
|
# as complete formats.
|
||||||
if audio_group_id and codecs and f.get('vcodec') != 'none':
|
if audio_group_id and codecs and f.get('vcodec') != 'none':
|
||||||
audio_group = groups.get(audio_group_id)
|
audio_group = groups.get(audio_group_id)
|
||||||
@ -1645,9 +1864,7 @@ class InfoExtractor(object):
|
|||||||
'height': height,
|
'height': height,
|
||||||
})
|
})
|
||||||
formats.extend(m3u8_formats)
|
formats.extend(m3u8_formats)
|
||||||
continue
|
elif src_ext == 'f4m':
|
||||||
|
|
||||||
if src_ext == 'f4m':
|
|
||||||
f4m_url = src_url
|
f4m_url = src_url
|
||||||
if not f4m_params:
|
if not f4m_params:
|
||||||
f4m_params = {
|
f4m_params = {
|
||||||
@ -1657,9 +1874,13 @@ class InfoExtractor(object):
|
|||||||
f4m_url += '&' if '?' in f4m_url else '?'
|
f4m_url += '&' if '?' in f4m_url else '?'
|
||||||
f4m_url += compat_urllib_parse_urlencode(f4m_params)
|
f4m_url += compat_urllib_parse_urlencode(f4m_params)
|
||||||
formats.extend(self._extract_f4m_formats(f4m_url, video_id, f4m_id='hds', fatal=False))
|
formats.extend(self._extract_f4m_formats(f4m_url, video_id, f4m_id='hds', fatal=False))
|
||||||
continue
|
elif src_ext == 'mpd':
|
||||||
|
formats.extend(self._extract_mpd_formats(
|
||||||
if src_url.startswith('http') and self._is_valid_url(src, video_id):
|
src_url, video_id, mpd_id='dash', fatal=False))
|
||||||
|
elif re.search(r'\.ism/[Mm]anifest', src_url):
|
||||||
|
formats.extend(self._extract_ism_formats(
|
||||||
|
src_url, video_id, ism_id='mss', fatal=False))
|
||||||
|
elif src_url.startswith('http') and self._is_valid_url(src, video_id):
|
||||||
http_count += 1
|
http_count += 1
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': src_url,
|
'url': src_url,
|
||||||
@ -1670,7 +1891,6 @@ class InfoExtractor(object):
|
|||||||
'width': width,
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
})
|
})
|
||||||
continue
|
|
||||||
|
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
@ -1690,22 +1910,24 @@ class InfoExtractor(object):
|
|||||||
})
|
})
|
||||||
return subtitles
|
return subtitles
|
||||||
|
|
||||||
def _extract_xspf_playlist(self, playlist_url, playlist_id, fatal=True):
|
def _extract_xspf_playlist(self, xspf_url, playlist_id, fatal=True):
|
||||||
xspf = self._download_xml(
|
xspf = self._download_xml(
|
||||||
playlist_url, playlist_id, 'Downloading xpsf playlist',
|
xspf_url, playlist_id, 'Downloading xpsf playlist',
|
||||||
'Unable to download xspf manifest', fatal=fatal)
|
'Unable to download xspf manifest', fatal=fatal)
|
||||||
if xspf is False:
|
if xspf is False:
|
||||||
return []
|
return []
|
||||||
return self._parse_xspf(xspf, playlist_id)
|
return self._parse_xspf(
|
||||||
|
xspf, playlist_id, xspf_url=xspf_url,
|
||||||
|
xspf_base_url=base_url(xspf_url))
|
||||||
|
|
||||||
def _parse_xspf(self, playlist, playlist_id):
|
def _parse_xspf(self, xspf_doc, playlist_id, xspf_url=None, xspf_base_url=None):
|
||||||
NS_MAP = {
|
NS_MAP = {
|
||||||
'xspf': 'http://xspf.org/ns/0/',
|
'xspf': 'http://xspf.org/ns/0/',
|
||||||
's1': 'http://static.streamone.nl/player/ns/0',
|
's1': 'http://static.streamone.nl/player/ns/0',
|
||||||
}
|
}
|
||||||
|
|
||||||
entries = []
|
entries = []
|
||||||
for track in playlist.findall(xpath_with_ns('./xspf:trackList/xspf:track', NS_MAP)):
|
for track in xspf_doc.findall(xpath_with_ns('./xspf:trackList/xspf:track', NS_MAP)):
|
||||||
title = xpath_text(
|
title = xpath_text(
|
||||||
track, xpath_with_ns('./xspf:title', NS_MAP), 'title', default=playlist_id)
|
track, xpath_with_ns('./xspf:title', NS_MAP), 'title', default=playlist_id)
|
||||||
description = xpath_text(
|
description = xpath_text(
|
||||||
@ -1715,12 +1937,18 @@ class InfoExtractor(object):
|
|||||||
duration = float_or_none(
|
duration = float_or_none(
|
||||||
xpath_text(track, xpath_with_ns('./xspf:duration', NS_MAP), 'duration'), 1000)
|
xpath_text(track, xpath_with_ns('./xspf:duration', NS_MAP), 'duration'), 1000)
|
||||||
|
|
||||||
formats = [{
|
formats = []
|
||||||
'url': location.text,
|
for location in track.findall(xpath_with_ns('./xspf:location', NS_MAP)):
|
||||||
|
format_url = urljoin(xspf_base_url, location.text)
|
||||||
|
if not format_url:
|
||||||
|
continue
|
||||||
|
formats.append({
|
||||||
|
'url': format_url,
|
||||||
|
'manifest_url': xspf_url,
|
||||||
'format_id': location.get(xpath_with_ns('s1:label', NS_MAP)),
|
'format_id': location.get(xpath_with_ns('s1:label', NS_MAP)),
|
||||||
'width': int_or_none(location.get(xpath_with_ns('s1:width', NS_MAP))),
|
'width': int_or_none(location.get(xpath_with_ns('s1:width', NS_MAP))),
|
||||||
'height': int_or_none(location.get(xpath_with_ns('s1:height', NS_MAP))),
|
'height': int_or_none(location.get(xpath_with_ns('s1:height', NS_MAP))),
|
||||||
} for location in track.findall(xpath_with_ns('./xspf:location', NS_MAP))]
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
entries.append({
|
entries.append({
|
||||||
@ -1734,18 +1962,18 @@ class InfoExtractor(object):
|
|||||||
return entries
|
return entries
|
||||||
|
|
||||||
def _extract_mpd_formats(self, mpd_url, video_id, mpd_id=None, note=None, errnote=None, fatal=True, formats_dict={}):
|
def _extract_mpd_formats(self, mpd_url, video_id, mpd_id=None, note=None, errnote=None, fatal=True, formats_dict={}):
|
||||||
res = self._download_webpage_handle(
|
res = self._download_xml_handle(
|
||||||
mpd_url, video_id,
|
mpd_url, video_id,
|
||||||
note=note or 'Downloading MPD manifest',
|
note=note or 'Downloading MPD manifest',
|
||||||
errnote=errnote or 'Failed to download MPD manifest',
|
errnote=errnote or 'Failed to download MPD manifest',
|
||||||
fatal=fatal)
|
fatal=fatal)
|
||||||
if res is False:
|
if res is False:
|
||||||
return []
|
return []
|
||||||
mpd, urlh = res
|
mpd_doc, urlh = res
|
||||||
mpd_base_url = base_url(urlh.geturl())
|
mpd_base_url = base_url(urlh.geturl())
|
||||||
|
|
||||||
return self._parse_mpd_formats(
|
return self._parse_mpd_formats(
|
||||||
compat_etree_fromstring(mpd.encode('utf-8')), mpd_id, mpd_base_url,
|
mpd_doc, mpd_id=mpd_id, mpd_base_url=mpd_base_url,
|
||||||
formats_dict=formats_dict, mpd_url=mpd_url)
|
formats_dict=formats_dict, mpd_url=mpd_url)
|
||||||
|
|
||||||
def _parse_mpd_formats(self, mpd_doc, mpd_id=None, mpd_base_url='', formats_dict={}, mpd_url=None):
|
def _parse_mpd_formats(self, mpd_doc, mpd_id=None, mpd_base_url='', formats_dict={}, mpd_url=None):
|
||||||
@ -1878,12 +2106,27 @@ class InfoExtractor(object):
|
|||||||
'language': lang if lang not in ('mul', 'und', 'zxx', 'mis') else None,
|
'language': lang if lang not in ('mul', 'und', 'zxx', 'mis') else None,
|
||||||
'format_note': 'DASH %s' % content_type,
|
'format_note': 'DASH %s' % content_type,
|
||||||
'filesize': filesize,
|
'filesize': filesize,
|
||||||
|
'container': mimetype2ext(mime_type) + '_dash',
|
||||||
}
|
}
|
||||||
f.update(parse_codecs(representation_attrib.get('codecs')))
|
f.update(parse_codecs(representation_attrib.get('codecs')))
|
||||||
representation_ms_info = extract_multisegment_info(representation, adaption_set_ms_info)
|
representation_ms_info = extract_multisegment_info(representation, adaption_set_ms_info)
|
||||||
|
|
||||||
def prepare_template(template_name, identifiers):
|
def prepare_template(template_name, identifiers):
|
||||||
t = representation_ms_info[template_name]
|
tmpl = representation_ms_info[template_name]
|
||||||
|
# First of, % characters outside $...$ templates
|
||||||
|
# must be escaped by doubling for proper processing
|
||||||
|
# by % operator string formatting used further (see
|
||||||
|
# https://github.com/rg3/youtube-dl/issues/16867).
|
||||||
|
t = ''
|
||||||
|
in_template = False
|
||||||
|
for c in tmpl:
|
||||||
|
t += c
|
||||||
|
if c == '$':
|
||||||
|
in_template = not in_template
|
||||||
|
elif c == '%' and not in_template:
|
||||||
|
t += c
|
||||||
|
# Next, $...$ templates are translated to their
|
||||||
|
# %(...) counterparts to be used with % operator
|
||||||
t = t.replace('$RepresentationID$', representation_id)
|
t = t.replace('$RepresentationID$', representation_id)
|
||||||
t = re.sub(r'\$(%s)\$' % '|'.join(identifiers), r'%(\1)d', t)
|
t = re.sub(r'\$(%s)\$' % '|'.join(identifiers), r'%(\1)d', t)
|
||||||
t = re.sub(r'\$(%s)%%([^$]+)\$' % '|'.join(identifiers), r'%(\1)\2', t)
|
t = re.sub(r'\$(%s)%%([^$]+)\$' % '|'.join(identifiers), r'%(\1)\2', t)
|
||||||
@ -1975,6 +2218,22 @@ class InfoExtractor(object):
|
|||||||
})
|
})
|
||||||
segment_index += 1
|
segment_index += 1
|
||||||
representation_ms_info['fragments'] = fragments
|
representation_ms_info['fragments'] = fragments
|
||||||
|
elif 'segment_urls' in representation_ms_info:
|
||||||
|
# Segment URLs with no SegmentTimeline
|
||||||
|
# Example: https://www.seznam.cz/zpravy/clanek/cesko-zasahne-vitr-o-sile-vichrice-muze-byt-i-zivotu-nebezpecny-39091
|
||||||
|
# https://github.com/rg3/youtube-dl/pull/14844
|
||||||
|
fragments = []
|
||||||
|
segment_duration = float_or_none(
|
||||||
|
representation_ms_info['segment_duration'],
|
||||||
|
representation_ms_info['timescale']) if 'segment_duration' in representation_ms_info else None
|
||||||
|
for segment_url in representation_ms_info['segment_urls']:
|
||||||
|
fragment = {
|
||||||
|
location_key(segment_url): segment_url,
|
||||||
|
}
|
||||||
|
if segment_duration:
|
||||||
|
fragment['duration'] = segment_duration
|
||||||
|
fragments.append(fragment)
|
||||||
|
representation_ms_info['fragments'] = fragments
|
||||||
# NB: MPD manifest may contain direct URLs to unfragmented media.
|
# NB: MPD manifest may contain direct URLs to unfragmented media.
|
||||||
# No fragments key is present in this case.
|
# No fragments key is present in this case.
|
||||||
if 'fragments' in representation_ms_info:
|
if 'fragments' in representation_ms_info:
|
||||||
@ -1989,32 +2248,29 @@ class InfoExtractor(object):
|
|||||||
f['url'] = initialization_url
|
f['url'] = initialization_url
|
||||||
f['fragments'].append({location_key(initialization_url): initialization_url})
|
f['fragments'].append({location_key(initialization_url): initialization_url})
|
||||||
f['fragments'].extend(representation_ms_info['fragments'])
|
f['fragments'].extend(representation_ms_info['fragments'])
|
||||||
try:
|
# According to [1, 5.3.5.2, Table 7, page 35] @id of Representation
|
||||||
existing_format = next(
|
# is not necessarily unique within a Period thus formats with
|
||||||
fo for fo in formats
|
# the same `format_id` are quite possible. There are numerous examples
|
||||||
if fo['format_id'] == representation_id)
|
# of such manifests (see https://github.com/rg3/youtube-dl/issues/15111,
|
||||||
except StopIteration:
|
# https://github.com/rg3/youtube-dl/issues/13919)
|
||||||
full_info = formats_dict.get(representation_id, {}).copy()
|
full_info = formats_dict.get(representation_id, {}).copy()
|
||||||
full_info.update(f)
|
full_info.update(f)
|
||||||
formats.append(full_info)
|
formats.append(full_info)
|
||||||
else:
|
|
||||||
existing_format.update(f)
|
|
||||||
else:
|
else:
|
||||||
self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
|
self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
def _extract_ism_formats(self, ism_url, video_id, ism_id=None, note=None, errnote=None, fatal=True):
|
def _extract_ism_formats(self, ism_url, video_id, ism_id=None, note=None, errnote=None, fatal=True):
|
||||||
res = self._download_webpage_handle(
|
res = self._download_xml_handle(
|
||||||
ism_url, video_id,
|
ism_url, video_id,
|
||||||
note=note or 'Downloading ISM manifest',
|
note=note or 'Downloading ISM manifest',
|
||||||
errnote=errnote or 'Failed to download ISM manifest',
|
errnote=errnote or 'Failed to download ISM manifest',
|
||||||
fatal=fatal)
|
fatal=fatal)
|
||||||
if res is False:
|
if res is False:
|
||||||
return []
|
return []
|
||||||
ism, urlh = res
|
ism_doc, urlh = res
|
||||||
|
|
||||||
return self._parse_ism_formats(
|
return self._parse_ism_formats(ism_doc, urlh.geturl(), ism_id)
|
||||||
compat_etree_fromstring(ism.encode('utf-8')), urlh.geturl(), ism_id)
|
|
||||||
|
|
||||||
def _parse_ism_formats(self, ism_doc, ism_url, ism_id=None):
|
def _parse_ism_formats(self, ism_doc, ism_url, ism_id=None):
|
||||||
"""
|
"""
|
||||||
@ -2038,7 +2294,7 @@ class InfoExtractor(object):
|
|||||||
stream_timescale = int_or_none(stream.get('TimeScale')) or timescale
|
stream_timescale = int_or_none(stream.get('TimeScale')) or timescale
|
||||||
stream_name = stream.get('Name')
|
stream_name = stream.get('Name')
|
||||||
for track in stream.findall('QualityLevel'):
|
for track in stream.findall('QualityLevel'):
|
||||||
fourcc = track.get('FourCC')
|
fourcc = track.get('FourCC', 'AACL' if track.get('AudioTag') == '255' else None)
|
||||||
# TODO: add support for WVC1 and WMAP
|
# TODO: add support for WVC1 and WMAP
|
||||||
if fourcc not in ('H264', 'AVC1', 'AACL'):
|
if fourcc not in ('H264', 'AVC1', 'AACL'):
|
||||||
self.report_warning('%s is not a supported codec' % fourcc)
|
self.report_warning('%s is not a supported codec' % fourcc)
|
||||||
@ -2112,8 +2368,8 @@ class InfoExtractor(object):
|
|||||||
return formats
|
return formats
|
||||||
|
|
||||||
def _parse_html5_media_entries(self, base_url, webpage, video_id, m3u8_id=None, m3u8_entry_protocol='m3u8', mpd_id=None, preference=None):
|
def _parse_html5_media_entries(self, base_url, webpage, video_id, m3u8_id=None, m3u8_entry_protocol='m3u8', mpd_id=None, preference=None):
|
||||||
def absolute_url(video_url):
|
def absolute_url(item_url):
|
||||||
return compat_urlparse.urljoin(base_url, video_url)
|
return urljoin(base_url, item_url)
|
||||||
|
|
||||||
def parse_content_type(content_type):
|
def parse_content_type(content_type):
|
||||||
if not content_type:
|
if not content_type:
|
||||||
@ -2170,7 +2426,7 @@ class InfoExtractor(object):
|
|||||||
if src:
|
if src:
|
||||||
_, formats = _media_formats(src, media_type)
|
_, formats = _media_formats(src, media_type)
|
||||||
media_info['formats'].extend(formats)
|
media_info['formats'].extend(formats)
|
||||||
media_info['thumbnail'] = media_attributes.get('poster')
|
media_info['thumbnail'] = absolute_url(media_attributes.get('poster'))
|
||||||
if media_content:
|
if media_content:
|
||||||
for source_tag in re.findall(r'<source[^>]+>', media_content):
|
for source_tag in re.findall(r'<source[^>]+>', media_content):
|
||||||
source_attributes = extract_attributes(source_tag)
|
source_attributes = extract_attributes(source_tag)
|
||||||
@ -2201,6 +2457,8 @@ class InfoExtractor(object):
|
|||||||
media_info['subtitles'].setdefault(lang, []).append({
|
media_info['subtitles'].setdefault(lang, []).append({
|
||||||
'url': absolute_url(src),
|
'url': absolute_url(src),
|
||||||
})
|
})
|
||||||
|
for f in media_info['formats']:
|
||||||
|
f.setdefault('http_headers', {})['Referer'] = base_url
|
||||||
if media_info['formats'] or media_info['subtitles']:
|
if media_info['formats'] or media_info['subtitles']:
|
||||||
entries.append(media_info)
|
entries.append(media_info)
|
||||||
return entries
|
return entries
|
||||||
@ -2231,9 +2489,10 @@ class InfoExtractor(object):
|
|||||||
def _extract_wowza_formats(self, url, video_id, m3u8_entry_protocol='m3u8_native', skip_protocols=[]):
|
def _extract_wowza_formats(self, url, video_id, m3u8_entry_protocol='m3u8_native', skip_protocols=[]):
|
||||||
query = compat_urlparse.urlparse(url).query
|
query = compat_urlparse.urlparse(url).query
|
||||||
url = re.sub(r'/(?:manifest|playlist|jwplayer)\.(?:m3u8|f4m|mpd|smil)', '', url)
|
url = re.sub(r'/(?:manifest|playlist|jwplayer)\.(?:m3u8|f4m|mpd|smil)', '', url)
|
||||||
url_base = self._search_regex(
|
mobj = re.search(
|
||||||
r'(?:(?:https?|rtmp|rtsp):)?(//[^?]+)', url, 'format url')
|
r'(?:(?:http|rtmp|rtsp)(?P<s>s)?:)?(?P<url>//[^?]+)', url)
|
||||||
http_base_url = '%s:%s' % ('http', url_base)
|
url_base = mobj.group('url')
|
||||||
|
http_base_url = '%s%s:%s' % ('http', mobj.group('s') or '', url_base)
|
||||||
formats = []
|
formats = []
|
||||||
|
|
||||||
def manifest_url(manifest):
|
def manifest_url(manifest):
|
||||||
@ -2333,7 +2592,10 @@ class InfoExtractor(object):
|
|||||||
for track in tracks:
|
for track in tracks:
|
||||||
if not isinstance(track, dict):
|
if not isinstance(track, dict):
|
||||||
continue
|
continue
|
||||||
if track.get('kind') != 'captions':
|
track_kind = track.get('kind')
|
||||||
|
if not track_kind or not isinstance(track_kind, compat_str):
|
||||||
|
continue
|
||||||
|
if track_kind.lower() not in ('captions', 'subtitles'):
|
||||||
continue
|
continue
|
||||||
track_url = urljoin(base_url, track.get('file'))
|
track_url = urljoin(base_url, track.get('file'))
|
||||||
if not track_url:
|
if not track_url:
|
||||||
@ -2387,7 +2649,7 @@ class InfoExtractor(object):
|
|||||||
formats.extend(self._extract_m3u8_formats(
|
formats.extend(self._extract_m3u8_formats(
|
||||||
source_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
source_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
m3u8_id=m3u8_id, fatal=False))
|
m3u8_id=m3u8_id, fatal=False))
|
||||||
elif ext == 'mpd':
|
elif source_type == 'dash' or ext == 'mpd':
|
||||||
formats.extend(self._extract_mpd_formats(
|
formats.extend(self._extract_mpd_formats(
|
||||||
source_url, video_id, mpd_id=mpd_id, fatal=False))
|
source_url, video_id, mpd_id=mpd_id, fatal=False))
|
||||||
elif ext == 'smil':
|
elif ext == 'smil':
|
||||||
|
@ -1,141 +1,165 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals, division
|
from __future__ import unicode_literals, division
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import int_or_none
|
from ..compat import compat_HTTPError
|
||||||
|
from ..utils import (
|
||||||
|
determine_ext,
|
||||||
|
float_or_none,
|
||||||
|
int_or_none,
|
||||||
|
parse_age_limit,
|
||||||
|
parse_duration,
|
||||||
|
url_or_none,
|
||||||
|
ExtractorError
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CrackleIE(InfoExtractor):
|
class CrackleIE(InfoExtractor):
|
||||||
_GEO_COUNTRIES = ['US']
|
_VALID_URL = r'(?:crackle:|https?://(?:(?:www|m)\.)?(?:sony)?crackle\.com/(?:playlist/\d+/|(?:[^/]+/)+))(?P<id>\d+)'
|
||||||
_VALID_URL = r'(?:crackle:|https?://(?:(?:www|m)\.)?crackle\.com/(?:playlist/\d+/|(?:[^/]+/)+))(?P<id>\d+)'
|
_TESTS = [{
|
||||||
_TEST = {
|
# geo restricted to CA
|
||||||
'url': 'http://www.crackle.com/comedians-in-cars-getting-coffee/2498934',
|
'url': 'https://www.crackle.com/andromeda/2502343',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '2498934',
|
'id': '2502343',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Everybody Respects A Bloody Nose',
|
'title': 'Under The Night',
|
||||||
'description': 'Jerry is kaffeeklatsching in L.A. with funnyman J.B. Smoove (Saturday Night Live, Real Husbands of Hollywood). They’re headed for brew at 10 Speed Coffee in a 1964 Studebaker Avanti.',
|
'description': 'md5:d2b8ca816579ae8a7bf28bfff8cefc8a',
|
||||||
'thumbnail': r're:^https?://.*\.jpg',
|
'duration': 2583,
|
||||||
'duration': 906,
|
'view_count': int,
|
||||||
'series': 'Comedians In Cars Getting Coffee',
|
'average_rating': 0,
|
||||||
'season_number': 8,
|
'age_limit': 14,
|
||||||
'episode_number': 4,
|
'genre': 'Action, Sci-Fi',
|
||||||
'subtitles': {
|
'creator': 'Allan Kroeker',
|
||||||
'en-US': [
|
'artist': 'Keith Hamilton Cobb, Kevin Sorbo, Lisa Ryder, Lexa Doig, Robert Hewitt Wolfe',
|
||||||
{'ext': 'vtt'},
|
'release_year': 2000,
|
||||||
{'ext': 'tt'},
|
'series': 'Andromeda',
|
||||||
]
|
'episode': 'Under The Night',
|
||||||
},
|
'season_number': 1,
|
||||||
|
'episode_number': 1,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# m3u8 download
|
# m3u8 download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
}
|
}
|
||||||
}
|
}, {
|
||||||
|
'url': 'https://www.sonycrackle.com/andromeda/2502343',
|
||||||
_THUMBNAIL_RES = [
|
'only_matching': True,
|
||||||
(120, 90),
|
}]
|
||||||
(208, 156),
|
|
||||||
(220, 124),
|
|
||||||
(220, 220),
|
|
||||||
(240, 180),
|
|
||||||
(250, 141),
|
|
||||||
(315, 236),
|
|
||||||
(320, 180),
|
|
||||||
(360, 203),
|
|
||||||
(400, 300),
|
|
||||||
(421, 316),
|
|
||||||
(460, 330),
|
|
||||||
(460, 460),
|
|
||||||
(462, 260),
|
|
||||||
(480, 270),
|
|
||||||
(587, 330),
|
|
||||||
(640, 480),
|
|
||||||
(700, 330),
|
|
||||||
(700, 394),
|
|
||||||
(854, 480),
|
|
||||||
(1024, 1024),
|
|
||||||
(1920, 1080),
|
|
||||||
]
|
|
||||||
|
|
||||||
# extracted from http://legacyweb-us.crackle.com/flash/ReferrerRedirect.ashx
|
|
||||||
_MEDIA_FILE_SLOTS = {
|
|
||||||
'c544.flv': {
|
|
||||||
'width': 544,
|
|
||||||
'height': 306,
|
|
||||||
},
|
|
||||||
'360p.mp4': {
|
|
||||||
'width': 640,
|
|
||||||
'height': 360,
|
|
||||||
},
|
|
||||||
'480p.mp4': {
|
|
||||||
'width': 852,
|
|
||||||
'height': 478,
|
|
||||||
},
|
|
||||||
'480p_1mbps.mp4': {
|
|
||||||
'width': 852,
|
|
||||||
'height': 478,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
|
|
||||||
config_doc = self._download_xml(
|
country_code = self._downloader.params.get('geo_bypass_country', None)
|
||||||
'http://legacyweb-us.crackle.com/flash/QueryReferrer.ashx?site=16',
|
countries = [country_code] if country_code else (
|
||||||
video_id, 'Downloading config')
|
'US', 'AU', 'CA', 'AS', 'FM', 'GU', 'MP', 'PR', 'PW', 'MH', 'VI')
|
||||||
|
|
||||||
item = self._download_xml(
|
last_e = None
|
||||||
'http://legacyweb-us.crackle.com/app/revamp/vidwallcache.aspx?flags=-1&fm=%s' % video_id,
|
|
||||||
video_id, headers=self.geo_verification_headers()).find('i')
|
for country in countries:
|
||||||
title = item.attrib['t']
|
try:
|
||||||
|
media = self._download_json(
|
||||||
|
'https://web-api-us.crackle.com/Service.svc/details/media/%s/%s'
|
||||||
|
% (video_id, country), video_id,
|
||||||
|
'Downloading media JSON as %s' % country,
|
||||||
|
'Unable to download media JSON', query={
|
||||||
|
'disableProtocols': 'true',
|
||||||
|
'format': 'json'
|
||||||
|
})
|
||||||
|
except ExtractorError as e:
|
||||||
|
# 401 means geo restriction, trying next country
|
||||||
|
if isinstance(e.cause, compat_HTTPError) and e.cause.code == 401:
|
||||||
|
last_e = e
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
|
||||||
|
media_urls = media.get('MediaURLs')
|
||||||
|
if not media_urls or not isinstance(media_urls, list):
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = media['Title']
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for e in media['MediaURLs']:
|
||||||
|
if e.get('UseDRM') is True:
|
||||||
|
continue
|
||||||
|
format_url = url_or_none(e.get('Path'))
|
||||||
|
if not format_url:
|
||||||
|
continue
|
||||||
|
ext = determine_ext(format_url)
|
||||||
|
if ext == 'm3u8':
|
||||||
|
formats.extend(self._extract_m3u8_formats(
|
||||||
|
format_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
||||||
|
m3u8_id='hls', fatal=False))
|
||||||
|
elif ext == 'mpd':
|
||||||
|
formats.extend(self._extract_mpd_formats(
|
||||||
|
format_url, video_id, mpd_id='dash', fatal=False))
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
description = media.get('Description')
|
||||||
|
duration = int_or_none(media.get(
|
||||||
|
'DurationInSeconds')) or parse_duration(media.get('Duration'))
|
||||||
|
view_count = int_or_none(media.get('CountViews'))
|
||||||
|
average_rating = float_or_none(media.get('UserRating'))
|
||||||
|
age_limit = parse_age_limit(media.get('Rating'))
|
||||||
|
genre = media.get('Genre')
|
||||||
|
release_year = int_or_none(media.get('ReleaseYear'))
|
||||||
|
creator = media.get('Directors')
|
||||||
|
artist = media.get('Cast')
|
||||||
|
|
||||||
|
if media.get('MediaTypeDisplayValue') == 'Full Episode':
|
||||||
|
series = media.get('ShowName')
|
||||||
|
episode = title
|
||||||
|
season_number = int_or_none(media.get('Season'))
|
||||||
|
episode_number = int_or_none(media.get('Episode'))
|
||||||
|
else:
|
||||||
|
series = episode = season_number = episode_number = None
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
formats = self._extract_m3u8_formats(
|
cc_files = media.get('ClosedCaptionFiles')
|
||||||
'http://content.uplynk.com/ext/%s/%s.m3u8' % (config_doc.attrib['strUplynkOwnerId'], video_id),
|
if isinstance(cc_files, list):
|
||||||
video_id, 'mp4', m3u8_id='hls', fatal=None)
|
for cc_file in cc_files:
|
||||||
|
if not isinstance(cc_file, dict):
|
||||||
|
continue
|
||||||
|
cc_url = url_or_none(cc_file.get('Path'))
|
||||||
|
if not cc_url:
|
||||||
|
continue
|
||||||
|
lang = cc_file.get('Locale') or 'en'
|
||||||
|
subtitles.setdefault(lang, []).append({'url': cc_url})
|
||||||
|
|
||||||
thumbnails = []
|
thumbnails = []
|
||||||
path = item.attrib.get('p')
|
images = media.get('Images')
|
||||||
if path:
|
if isinstance(images, list):
|
||||||
for width, height in self._THUMBNAIL_RES:
|
for image_key, image_url in images.items():
|
||||||
res = '%dx%d' % (width, height)
|
mobj = re.search(r'Img_(\d+)[xX](\d+)', image_key)
|
||||||
|
if not mobj:
|
||||||
|
continue
|
||||||
thumbnails.append({
|
thumbnails.append({
|
||||||
'id': res,
|
'url': image_url,
|
||||||
'url': 'http://images-us-am.crackle.com/%stnl_%s.jpg' % (path, res),
|
'width': int(mobj.group(1)),
|
||||||
'width': width,
|
'height': int(mobj.group(2)),
|
||||||
'height': height,
|
|
||||||
'resolution': res,
|
|
||||||
})
|
})
|
||||||
http_base_url = 'http://ahttp.crackle.com/' + path
|
|
||||||
for mfs_path, mfs_info in self._MEDIA_FILE_SLOTS.items():
|
|
||||||
formats.append({
|
|
||||||
'url': http_base_url + mfs_path,
|
|
||||||
'format_id': 'http-' + mfs_path.split('.')[0],
|
|
||||||
'width': mfs_info['width'],
|
|
||||||
'height': mfs_info['height'],
|
|
||||||
})
|
|
||||||
for cc in item.findall('cc'):
|
|
||||||
locale = cc.attrib.get('l')
|
|
||||||
v = cc.attrib.get('v')
|
|
||||||
if locale and v:
|
|
||||||
if locale not in subtitles:
|
|
||||||
subtitles[locale] = []
|
|
||||||
for url_ext, ext in (('vtt', 'vtt'), ('xml', 'tt')):
|
|
||||||
subtitles.setdefault(locale, []).append({
|
|
||||||
'url': '%s/%s%s_%s.%s' % (config_doc.attrib['strSubtitleServer'], path, locale, v, url_ext),
|
|
||||||
'ext': ext,
|
|
||||||
})
|
|
||||||
self._sort_formats(formats, ('width', 'height', 'tbr', 'format_id'))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': item.attrib.get('d'),
|
'description': description,
|
||||||
'duration': int(item.attrib.get('r'), 16) / 1000 if item.attrib.get('r') else None,
|
'duration': duration,
|
||||||
'series': item.attrib.get('sn'),
|
'view_count': view_count,
|
||||||
'season_number': int_or_none(item.attrib.get('se')),
|
'average_rating': average_rating,
|
||||||
'episode_number': int_or_none(item.attrib.get('ep')),
|
'age_limit': age_limit,
|
||||||
|
'genre': genre,
|
||||||
|
'creator': creator,
|
||||||
|
'artist': artist,
|
||||||
|
'release_year': release_year,
|
||||||
|
'series': series,
|
||||||
|
'episode': episode,
|
||||||
|
'season_number': season_number,
|
||||||
|
'episode_number': episode_number,
|
||||||
'thumbnails': thumbnails,
|
'thumbnails': thumbnails,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
raise last_e
|
||||||
|
@ -3,13 +3,14 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import base64
|
|
||||||
import zlib
|
import zlib
|
||||||
|
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from math import pow, sqrt, floor
|
from math import pow, sqrt, floor
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from .vrv import VRVIE
|
||||||
from ..compat import (
|
from ..compat import (
|
||||||
|
compat_b64decode,
|
||||||
compat_etree_fromstring,
|
compat_etree_fromstring,
|
||||||
compat_urllib_parse_urlencode,
|
compat_urllib_parse_urlencode,
|
||||||
compat_urllib_request,
|
compat_urllib_request,
|
||||||
@ -18,6 +19,8 @@ from ..compat import (
|
|||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
bytes_to_intlist,
|
bytes_to_intlist,
|
||||||
|
extract_attributes,
|
||||||
|
float_or_none,
|
||||||
intlist_to_bytes,
|
intlist_to_bytes,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
lowercase_escape,
|
lowercase_escape,
|
||||||
@ -26,7 +29,6 @@ from ..utils import (
|
|||||||
unified_strdate,
|
unified_strdate,
|
||||||
urlencode_postdata,
|
urlencode_postdata,
|
||||||
xpath_text,
|
xpath_text,
|
||||||
extract_attributes,
|
|
||||||
)
|
)
|
||||||
from ..aes import (
|
from ..aes import (
|
||||||
aes_cbc_decrypt,
|
aes_cbc_decrypt,
|
||||||
@ -43,13 +45,13 @@ class CrunchyrollBaseIE(InfoExtractor):
|
|||||||
data['req'] = 'RpcApi' + method
|
data['req'] = 'RpcApi' + method
|
||||||
data = compat_urllib_parse_urlencode(data).encode('utf-8')
|
data = compat_urllib_parse_urlencode(data).encode('utf-8')
|
||||||
return self._download_xml(
|
return self._download_xml(
|
||||||
'http://www.crunchyroll.com/xml/',
|
'https://www.crunchyroll.com/xml/',
|
||||||
video_id, note, fatal=False, data=data, headers={
|
video_id, note, fatal=False, data=data, headers={
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
})
|
})
|
||||||
|
|
||||||
def _login(self):
|
def _login(self):
|
||||||
(username, password) = self._get_login_info()
|
username, password = self._get_login_info()
|
||||||
if username is None:
|
if username is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -139,7 +141,8 @@ class CrunchyrollBaseIE(InfoExtractor):
|
|||||||
parsed_url._replace(query=compat_urllib_parse_urlencode(qs, True)))
|
parsed_url._replace(query=compat_urllib_parse_urlencode(qs, True)))
|
||||||
|
|
||||||
|
|
||||||
class CrunchyrollIE(CrunchyrollBaseIE):
|
class CrunchyrollIE(CrunchyrollBaseIE, VRVIE):
|
||||||
|
IE_NAME = 'crunchyroll'
|
||||||
_VALID_URL = r'https?://(?:(?P<prefix>www|m)\.)?(?P<url>crunchyroll\.(?:com|fr)/(?:media(?:-|/\?id=)|[^/]*/[^/?&]*?)(?P<video_id>[0-9]+))(?:[/?&]|$)'
|
_VALID_URL = r'https?://(?:(?P<prefix>www|m)\.)?(?P<url>crunchyroll\.(?:com|fr)/(?:media(?:-|/\?id=)|[^/]*/[^/?&]*?)(?P<video_id>[0-9]+))(?:[/?&]|$)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.crunchyroll.com/wanna-be-the-strongest-in-the-world/episode-1-an-idol-wrestler-is-born-645513',
|
'url': 'http://www.crunchyroll.com/wanna-be-the-strongest-in-the-world/episode-1-an-idol-wrestler-is-born-645513',
|
||||||
@ -148,7 +151,7 @@ class CrunchyrollIE(CrunchyrollBaseIE):
|
|||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'Wanna be the Strongest in the World Episode 1 – An Idol-Wrestler is Born!',
|
'title': 'Wanna be the Strongest in the World Episode 1 – An Idol-Wrestler is Born!',
|
||||||
'description': 'md5:2d17137920c64f2f49981a7797d275ef',
|
'description': 'md5:2d17137920c64f2f49981a7797d275ef',
|
||||||
'thumbnail': 'http://img1.ak.crunchyroll.com/i/spire1-tmb/20c6b5e10f1a47b10516877d3c039cae1380951166_full.jpg',
|
'thumbnail': r're:^https?://.*\.jpg$',
|
||||||
'uploader': 'Yomiuri Telecasting Corporation (YTV)',
|
'uploader': 'Yomiuri Telecasting Corporation (YTV)',
|
||||||
'upload_date': '20131013',
|
'upload_date': '20131013',
|
||||||
'url': 're:(?!.*&)',
|
'url': 're:(?!.*&)',
|
||||||
@ -221,7 +224,7 @@ class CrunchyrollIE(CrunchyrollBaseIE):
|
|||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '535080',
|
'id': '535080',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': '11eyes Episode 1 – Piros éjszaka - Red Night',
|
'title': '11eyes Episode 1 – Red Night ~ Piros éjszaka',
|
||||||
'description': 'Kakeru and Yuka are thrown into an alternate nightmarish world they call "Red Night".',
|
'description': 'Kakeru and Yuka are thrown into an alternate nightmarish world they call "Red Night".',
|
||||||
'uploader': 'Marvelous AQL Inc.',
|
'uploader': 'Marvelous AQL Inc.',
|
||||||
'upload_date': '20091021',
|
'upload_date': '20091021',
|
||||||
@ -262,6 +265,9 @@ class CrunchyrollIE(CrunchyrollBaseIE):
|
|||||||
# Just test metadata extraction
|
# Just test metadata extraction
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'http://www.crunchyroll.com/media-723735',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
_FORMAT_IDS = {
|
_FORMAT_IDS = {
|
||||||
@ -272,8 +278,8 @@ class CrunchyrollIE(CrunchyrollBaseIE):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _decrypt_subtitles(self, data, iv, id):
|
def _decrypt_subtitles(self, data, iv, id):
|
||||||
data = bytes_to_intlist(base64.b64decode(data.encode('utf-8')))
|
data = bytes_to_intlist(compat_b64decode(data))
|
||||||
iv = bytes_to_intlist(base64.b64decode(iv.encode('utf-8')))
|
iv = bytes_to_intlist(compat_b64decode(iv))
|
||||||
id = int(id)
|
id = int(id)
|
||||||
|
|
||||||
def obfuscate_key_aux(count, modulo, start):
|
def obfuscate_key_aux(count, modulo, start):
|
||||||
@ -392,7 +398,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
'Downloading subtitles for ' + sub_name, data={
|
'Downloading subtitles for ' + sub_name, data={
|
||||||
'subtitle_script_id': sub_id,
|
'subtitle_script_id': sub_id,
|
||||||
})
|
})
|
||||||
if not sub_doc:
|
if sub_doc is None:
|
||||||
continue
|
continue
|
||||||
sid = sub_doc.get('id')
|
sid = sub_doc.get('id')
|
||||||
iv = xpath_text(sub_doc, 'iv', 'subtitle iv')
|
iv = xpath_text(sub_doc, 'iv', 'subtitle iv')
|
||||||
@ -434,13 +440,22 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
if 'To view this, please log in to verify you are 18 or older.' in webpage:
|
if 'To view this, please log in to verify you are 18 or older.' in webpage:
|
||||||
self.raise_login_required()
|
self.raise_login_required()
|
||||||
|
|
||||||
|
media = self._parse_json(self._search_regex(
|
||||||
|
r'vilos\.config\.media\s*=\s*({.+?});',
|
||||||
|
webpage, 'vilos media', default='{}'), video_id)
|
||||||
|
media_metadata = media.get('metadata') or {}
|
||||||
|
|
||||||
|
language = self._search_regex(
|
||||||
|
r'(?:vilos\.config\.player\.language|LOCALE)\s*=\s*(["\'])(?P<lang>(?:(?!\1).)+)\1',
|
||||||
|
webpage, 'language', default=None, group='lang')
|
||||||
|
|
||||||
video_title = self._html_search_regex(
|
video_title = self._html_search_regex(
|
||||||
r'(?s)<h1[^>]*>((?:(?!<h1).)*?<span[^>]+itemprop=["\']title["\'][^>]*>(?:(?!<h1).)+?)</h1>',
|
r'(?s)<h1[^>]*>((?:(?!<h1).)*?<span[^>]+itemprop=["\']title["\'][^>]*>(?:(?!<h1).)+?)</h1>',
|
||||||
webpage, 'video_title')
|
webpage, 'video_title')
|
||||||
video_title = re.sub(r' {2,}', ' ', video_title)
|
video_title = re.sub(r' {2,}', ' ', video_title)
|
||||||
video_description = self._parse_json(self._html_search_regex(
|
video_description = (self._parse_json(self._html_search_regex(
|
||||||
r'<script[^>]*>\s*.+?\[media_id=%s\].+?({.+?"description"\s*:.+?})\);' % video_id,
|
r'<script[^>]*>\s*.+?\[media_id=%s\].+?({.+?"description"\s*:.+?})\);' % video_id,
|
||||||
webpage, 'description', default='{}'), video_id).get('description')
|
webpage, 'description', default='{}'), video_id) or media_metadata).get('description')
|
||||||
if video_description:
|
if video_description:
|
||||||
video_description = lowercase_escape(video_description.replace(r'\r\n', '\n'))
|
video_description = lowercase_escape(video_description.replace(r'\r\n', '\n'))
|
||||||
video_upload_date = self._html_search_regex(
|
video_upload_date = self._html_search_regex(
|
||||||
@ -453,6 +468,25 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
[r'<a[^>]+href="/publisher/[^"]+"[^>]*>([^<]+)</a>', r'<div>\s*Publisher:\s*<span>\s*(.+?)\s*</span>\s*</div>'],
|
[r'<a[^>]+href="/publisher/[^"]+"[^>]*>([^<]+)</a>', r'<div>\s*Publisher:\s*<span>\s*(.+?)\s*</span>\s*</div>'],
|
||||||
webpage, 'video_uploader', fatal=False)
|
webpage, 'video_uploader', fatal=False)
|
||||||
|
|
||||||
|
formats = []
|
||||||
|
for stream in media.get('streams', []):
|
||||||
|
audio_lang = stream.get('audio_lang')
|
||||||
|
hardsub_lang = stream.get('hardsub_lang')
|
||||||
|
vrv_formats = self._extract_vrv_formats(
|
||||||
|
stream.get('url'), video_id, stream.get('format'),
|
||||||
|
audio_lang, hardsub_lang)
|
||||||
|
for f in vrv_formats:
|
||||||
|
if not hardsub_lang:
|
||||||
|
f['preference'] = 1
|
||||||
|
language_preference = 0
|
||||||
|
if audio_lang == language:
|
||||||
|
language_preference += 1
|
||||||
|
if hardsub_lang == language:
|
||||||
|
language_preference += 1
|
||||||
|
if language_preference:
|
||||||
|
f['language_preference'] = language_preference
|
||||||
|
formats.extend(vrv_formats)
|
||||||
|
if not formats:
|
||||||
available_fmts = []
|
available_fmts = []
|
||||||
for a, fmt in re.findall(r'(<a[^>]+token=["\']showmedia\.([0-9]{3,4})p["\'][^>]+>)', webpage):
|
for a, fmt in re.findall(r'(<a[^>]+token=["\']showmedia\.([0-9]{3,4})p["\'][^>]+>)', webpage):
|
||||||
attrs = extract_attributes(a)
|
attrs = extract_attributes(a)
|
||||||
@ -465,8 +499,10 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
available_fmts = re.findall(p, webpage)
|
available_fmts = re.findall(p, webpage)
|
||||||
if available_fmts:
|
if available_fmts:
|
||||||
break
|
break
|
||||||
|
if not available_fmts:
|
||||||
|
available_fmts = self._FORMAT_IDS.keys()
|
||||||
video_encode_ids = []
|
video_encode_ids = []
|
||||||
formats = []
|
|
||||||
for fmt in available_fmts:
|
for fmt in available_fmts:
|
||||||
stream_quality, stream_format = self._FORMAT_IDS[fmt]
|
stream_quality, stream_format = self._FORMAT_IDS[fmt]
|
||||||
video_format = fmt + 'p'
|
video_format = fmt + 'p'
|
||||||
@ -479,9 +515,9 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
'video_quality': stream_quality,
|
'video_quality': stream_quality,
|
||||||
'current_page': url,
|
'current_page': url,
|
||||||
})
|
})
|
||||||
if streamdata:
|
if streamdata is not None:
|
||||||
stream_info = streamdata.find('./{default}preload/stream_info')
|
stream_info = streamdata.find('./{default}preload/stream_info')
|
||||||
if stream_info:
|
if stream_info is not None:
|
||||||
stream_infos.append(stream_info)
|
stream_infos.append(stream_info)
|
||||||
stream_info = self._call_rpc_api(
|
stream_info = self._call_rpc_api(
|
||||||
'VideoEncode_GetStreamInfo', video_id,
|
'VideoEncode_GetStreamInfo', video_id,
|
||||||
@ -490,7 +526,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
'video_format': stream_format,
|
'video_format': stream_format,
|
||||||
'video_encode_quality': stream_quality,
|
'video_encode_quality': stream_quality,
|
||||||
})
|
})
|
||||||
if stream_info:
|
if stream_info is not None:
|
||||||
stream_infos.append(stream_info)
|
stream_infos.append(stream_info)
|
||||||
for stream_info in stream_infos:
|
for stream_info in stream_infos:
|
||||||
video_encode_id = xpath_text(stream_info, './video_encode_id')
|
video_encode_id = xpath_text(stream_info, './video_encode_id')
|
||||||
@ -538,7 +574,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
})
|
})
|
||||||
formats.append(format_info)
|
formats.append(format_info)
|
||||||
self._sort_formats(formats, ('height', 'width', 'tbr', 'fps'))
|
self._sort_formats(formats, ('preference', 'language_preference', 'height', 'width', 'tbr', 'fps'))
|
||||||
|
|
||||||
metadata = self._call_rpc_api(
|
metadata = self._call_rpc_api(
|
||||||
'VideoPlayer_GetMediaMetadata', video_id,
|
'VideoPlayer_GetMediaMetadata', video_id,
|
||||||
@ -546,6 +582,16 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
'media_id': video_id,
|
'media_id': video_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
subtitles = {}
|
||||||
|
for subtitle in media.get('subtitles', []):
|
||||||
|
subtitle_url = subtitle.get('url')
|
||||||
|
if not subtitle_url:
|
||||||
|
continue
|
||||||
|
subtitles.setdefault(subtitle.get('language', 'enUS'), []).append({
|
||||||
|
'url': subtitle_url,
|
||||||
|
'ext': subtitle.get('format', 'ass'),
|
||||||
|
})
|
||||||
|
if not subtitles:
|
||||||
subtitles = self.extract_subtitles(video_id, webpage)
|
subtitles = self.extract_subtitles(video_id, webpage)
|
||||||
|
|
||||||
# webpage provide more accurate data than series_title from XML
|
# webpage provide more accurate data than series_title from XML
|
||||||
@ -554,8 +600,8 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
webpage, 'series', fatal=False)
|
webpage, 'series', fatal=False)
|
||||||
season = xpath_text(metadata, 'series_title')
|
season = xpath_text(metadata, 'series_title')
|
||||||
|
|
||||||
episode = xpath_text(metadata, 'episode_title')
|
episode = xpath_text(metadata, 'episode_title') or media_metadata.get('title')
|
||||||
episode_number = int_or_none(xpath_text(metadata, 'episode_number'))
|
episode_number = int_or_none(xpath_text(metadata, 'episode_number') or media_metadata.get('episode_number'))
|
||||||
|
|
||||||
season_number = int_or_none(self._search_regex(
|
season_number = int_or_none(self._search_regex(
|
||||||
r'(?s)<h\d[^>]+id=["\']showmedia_about_episode_num[^>]+>.+?</h\d>\s*<h4>\s*Season (\d+)',
|
r'(?s)<h\d[^>]+id=["\']showmedia_about_episode_num[^>]+>.+?</h\d>\s*<h4>\s*Season (\d+)',
|
||||||
@ -565,7 +611,8 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': video_title,
|
'title': video_title,
|
||||||
'description': video_description,
|
'description': video_description,
|
||||||
'thumbnail': xpath_text(metadata, 'episode_image_url'),
|
'duration': float_or_none(media_metadata.get('duration'), 1000),
|
||||||
|
'thumbnail': xpath_text(metadata, 'episode_image_url') or media_metadata.get('thumbnail', {}).get('url'),
|
||||||
'uploader': video_uploader,
|
'uploader': video_uploader,
|
||||||
'upload_date': video_upload_date,
|
'upload_date': video_upload_date,
|
||||||
'series': series,
|
'series': series,
|
||||||
@ -580,7 +627,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|||||||
|
|
||||||
class CrunchyrollShowPlaylistIE(CrunchyrollBaseIE):
|
class CrunchyrollShowPlaylistIE(CrunchyrollBaseIE):
|
||||||
IE_NAME = 'crunchyroll:playlist'
|
IE_NAME = 'crunchyroll:playlist'
|
||||||
_VALID_URL = r'https?://(?:(?P<prefix>www|m)\.)?(?P<url>crunchyroll\.com/(?!(?:news|anime-news|library|forum|launchcalendar|lineup|store|comics|freetrial|login))(?P<id>[\w\-]+))/?(?:\?|$)'
|
_VALID_URL = r'https?://(?:(?P<prefix>www|m)\.)?(?P<url>crunchyroll\.com/(?!(?:news|anime-news|library|forum|launchcalendar|lineup|store|comics|freetrial|login|media-\d+))(?P<id>[\w\-]+))/?(?:\?|$)'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.crunchyroll.com/a-bridge-to-the-starry-skies-hoshizora-e-kakaru-hashi',
|
'url': 'http://www.crunchyroll.com/a-bridge-to-the-starry-skies-hoshizora-e-kakaru-hashi',
|
||||||
|
@ -4,13 +4,14 @@ import re
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
|
||||||
unescapeHTML,
|
|
||||||
find_xpath_attr,
|
|
||||||
smuggle_url,
|
|
||||||
determine_ext,
|
determine_ext,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
extract_attributes,
|
extract_attributes,
|
||||||
|
find_xpath_attr,
|
||||||
|
get_element_by_class,
|
||||||
|
int_or_none,
|
||||||
|
smuggle_url,
|
||||||
|
unescapeHTML,
|
||||||
)
|
)
|
||||||
from .senateisvp import SenateISVPIE
|
from .senateisvp import SenateISVPIE
|
||||||
from .ustream import UstreamIE
|
from .ustream import UstreamIE
|
||||||
@ -68,6 +69,10 @@ class CSpanIE(InfoExtractor):
|
|||||||
'uploader': 'HouseCommittee',
|
'uploader': 'HouseCommittee',
|
||||||
'uploader_id': '12987475',
|
'uploader_id': '12987475',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
# Audio Only
|
||||||
|
'url': 'https://www.c-span.org/video/?437336-1/judiciary-antitrust-competition-policy-consumer-rights',
|
||||||
|
'only_matching': True,
|
||||||
}]
|
}]
|
||||||
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/%s_%s/index.html?videoId=%s'
|
BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/%s_%s/index.html?videoId=%s'
|
||||||
|
|
||||||
@ -111,7 +116,15 @@ class CSpanIE(InfoExtractor):
|
|||||||
title = self._og_search_title(webpage)
|
title = self._og_search_title(webpage)
|
||||||
surl = smuggle_url(senate_isvp_url, {'force_title': title})
|
surl = smuggle_url(senate_isvp_url, {'force_title': title})
|
||||||
return self.url_result(surl, 'SenateISVP', video_id, title)
|
return self.url_result(surl, 'SenateISVP', video_id, title)
|
||||||
|
video_id = self._search_regex(
|
||||||
|
r'jwsetup\.clipprog\s*=\s*(\d+);',
|
||||||
|
webpage, 'jwsetup program id', default=None)
|
||||||
|
if video_id:
|
||||||
|
video_type = 'program'
|
||||||
if video_type is None or video_id is None:
|
if video_type is None or video_id is None:
|
||||||
|
error_message = get_element_by_class('VLplayer-error-message', webpage)
|
||||||
|
if error_message:
|
||||||
|
raise ExtractorError(error_message)
|
||||||
raise ExtractorError('unable to find video id and type')
|
raise ExtractorError('unable to find video id and type')
|
||||||
|
|
||||||
def get_text_attr(d, attr):
|
def get_text_attr(d, attr):
|
||||||
@ -138,7 +151,7 @@ class CSpanIE(InfoExtractor):
|
|||||||
entries = []
|
entries = []
|
||||||
for partnum, f in enumerate(files):
|
for partnum, f in enumerate(files):
|
||||||
formats = []
|
formats = []
|
||||||
for quality in f['qualities']:
|
for quality in f.get('qualities', []):
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': '%s-%sp' % (get_text_attr(quality, 'bitrate'), get_text_attr(quality, 'height')),
|
'format_id': '%s-%sp' % (get_text_attr(quality, 'bitrate'), get_text_attr(quality, 'height')),
|
||||||
'url': unescapeHTML(get_text_attr(quality, 'file')),
|
'url': unescapeHTML(get_text_attr(quality, 'file')),
|
||||||
|
@ -11,10 +11,10 @@ class CTVNewsIE(InfoExtractor):
|
|||||||
_VALID_URL = r'https?://(?:.+?\.)?ctvnews\.ca/(?:video\?(?:clip|playlist|bin)Id=|.*?)(?P<id>[0-9.]+)'
|
_VALID_URL = r'https?://(?:.+?\.)?ctvnews\.ca/(?:video\?(?:clip|playlist|bin)Id=|.*?)(?P<id>[0-9.]+)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.ctvnews.ca/video?clipId=901995',
|
'url': 'http://www.ctvnews.ca/video?clipId=901995',
|
||||||
'md5': '10deb320dc0ccb8d01d34d12fc2ea672',
|
'md5': '9b8624ba66351a23e0b6e1391971f9af',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '901995',
|
'id': '901995',
|
||||||
'ext': 'mp4',
|
'ext': 'flv',
|
||||||
'title': 'Extended: \'That person cannot be me\' Johnson says',
|
'title': 'Extended: \'That person cannot be me\' Johnson says',
|
||||||
'description': 'md5:958dd3b4f5bbbf0ed4d045c790d89285',
|
'description': 'md5:958dd3b4f5bbbf0ed4d045c790d89285',
|
||||||
'timestamp': 1467286284,
|
'timestamp': 1467286284,
|
||||||
|
@ -35,7 +35,7 @@ class CuriosityStreamBaseIE(InfoExtractor):
|
|||||||
return result['data']
|
return result['data']
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
(email, password) = self._get_login_info()
|
email, password = self._get_login_info()
|
||||||
if email is None:
|
if email is None:
|
||||||
return
|
return
|
||||||
result = self._download_json(
|
result = self._download_json(
|
||||||
|
@ -4,7 +4,10 @@ from __future__ import unicode_literals
|
|||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
parse_age_limit,
|
||||||
parse_iso8601,
|
parse_iso8601,
|
||||||
|
smuggle_url,
|
||||||
|
str_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -40,10 +43,15 @@ class CWTVIE(InfoExtractor):
|
|||||||
'duration': 1263,
|
'duration': 1263,
|
||||||
'series': 'Whose Line Is It Anyway?',
|
'series': 'Whose Line Is It Anyway?',
|
||||||
'season_number': 11,
|
'season_number': 11,
|
||||||
'season': '11',
|
|
||||||
'episode_number': 20,
|
'episode_number': 20,
|
||||||
'upload_date': '20151006',
|
'upload_date': '20151006',
|
||||||
'timestamp': 1444107300,
|
'timestamp': 1444107300,
|
||||||
|
'age_limit': 14,
|
||||||
|
'uploader': 'CWTV',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
# m3u8 download
|
||||||
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'http://cwtv.com/thecw/chroniclesofcisco/?play=8adebe35-f447-465f-ab52-e863506ff6d6',
|
'url': 'http://cwtv.com/thecw/chroniclesofcisco/?play=8adebe35-f447-465f-ab52-e863506ff6d6',
|
||||||
@ -58,60 +66,28 @@ class CWTVIE(InfoExtractor):
|
|||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
video_id = self._match_id(url)
|
video_id = self._match_id(url)
|
||||||
video_data = None
|
video_data = self._download_json(
|
||||||
formats = []
|
'http://images.cwtv.com/feed/mobileapp/video-meta/apiversion_8/guid_' + video_id,
|
||||||
for partner in (154, 213):
|
video_id)['video']
|
||||||
vdata = self._download_json(
|
title = video_data['title']
|
||||||
'http://metaframe.digitalsmiths.tv/v2/CWtv/assets/%s/partner/%d?format=json' % (video_id, partner), video_id, fatal=False)
|
mpx_url = video_data.get('mpx_url') or 'http://link.theplatform.com/s/cwtv/media/guid/2703454149/%s?formats=M3U' % video_id
|
||||||
if not vdata:
|
|
||||||
continue
|
|
||||||
video_data = vdata
|
|
||||||
for quality, quality_data in vdata.get('videos', {}).items():
|
|
||||||
quality_url = quality_data.get('uri')
|
|
||||||
if not quality_url:
|
|
||||||
continue
|
|
||||||
if quality == 'variantplaylist':
|
|
||||||
formats.extend(self._extract_m3u8_formats(
|
|
||||||
quality_url, video_id, 'mp4', m3u8_id='hls', fatal=False))
|
|
||||||
else:
|
|
||||||
tbr = int_or_none(quality_data.get('bitrate'))
|
|
||||||
format_id = 'http' + ('-%d' % tbr if tbr else '')
|
|
||||||
if self._is_valid_url(quality_url, video_id, format_id):
|
|
||||||
formats.append({
|
|
||||||
'format_id': format_id,
|
|
||||||
'url': quality_url,
|
|
||||||
'tbr': tbr,
|
|
||||||
})
|
|
||||||
video_metadata = video_data['assetFields']
|
|
||||||
ism_url = video_metadata.get('smoothStreamingUrl')
|
|
||||||
if ism_url:
|
|
||||||
formats.extend(self._extract_ism_formats(
|
|
||||||
ism_url, video_id, ism_id='mss', fatal=False))
|
|
||||||
self._sort_formats(formats)
|
|
||||||
|
|
||||||
thumbnails = [{
|
season = str_or_none(video_data.get('season'))
|
||||||
'url': image['uri'],
|
episode = str_or_none(video_data.get('episode'))
|
||||||
'width': image.get('width'),
|
if episode and season:
|
||||||
'height': image.get('height'),
|
episode = episode.lstrip(season)
|
||||||
} for image_id, image in video_data['images'].items() if image.get('uri')] if video_data.get('images') else None
|
|
||||||
|
|
||||||
subtitles = {
|
|
||||||
'en': [{
|
|
||||||
'url': video_metadata['UnicornCcUrl'],
|
|
||||||
}],
|
|
||||||
} if video_metadata.get('UnicornCcUrl') else None
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': video_metadata['title'],
|
'title': title,
|
||||||
'description': video_metadata.get('description'),
|
'url': smuggle_url(mpx_url, {'force_smil_url': True}),
|
||||||
'duration': int_or_none(video_metadata.get('duration')),
|
'description': video_data.get('description_long'),
|
||||||
'series': video_metadata.get('seriesName'),
|
'duration': int_or_none(video_data.get('duration_secs')),
|
||||||
'season_number': int_or_none(video_metadata.get('seasonNumber')),
|
'series': video_data.get('series_name'),
|
||||||
'season': video_metadata.get('seasonName'),
|
'season_number': int_or_none(season),
|
||||||
'episode_number': int_or_none(video_metadata.get('episodeNumber')),
|
'episode_number': int_or_none(episode),
|
||||||
'timestamp': parse_iso8601(video_data.get('startTime')),
|
'timestamp': parse_iso8601(video_data.get('start_time')),
|
||||||
'thumbnails': thumbnails,
|
'age_limit': parse_age_limit(video_data.get('rating')),
|
||||||
'formats': formats,
|
'ie_key': 'ThePlatform',
|
||||||
'subtitles': subtitles,
|
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user