Merge branch 'master' into use-other-downloaders
Conflicts: README.md youtube-dl youtube_dl/__init__.py
This commit is contained in:
commit
03fe9b2e81
2
.gitignore
vendored
2
.gitignore
vendored
@ -17,4 +17,4 @@ youtube-dl.tar.gz
|
|||||||
.coverage
|
.coverage
|
||||||
cover/
|
cover/
|
||||||
updates_key.pem
|
updates_key.pem
|
||||||
*.egg-info
|
*.egg-info
|
@ -8,6 +8,7 @@ notifications:
|
|||||||
email:
|
email:
|
||||||
- filippo.valsorda@gmail.com
|
- filippo.valsorda@gmail.com
|
||||||
- phihag@phihag.de
|
- phihag@phihag.de
|
||||||
|
- jaime.marquinez.ferrandiz+travis@gmail.com
|
||||||
# irc:
|
# irc:
|
||||||
# channels:
|
# channels:
|
||||||
# - "irc.freenode.org#youtube-dl"
|
# - "irc.freenode.org#youtube-dl"
|
||||||
|
12
Makefile
12
Makefile
@ -9,9 +9,19 @@ cleanall: clean
|
|||||||
PREFIX=/usr/local
|
PREFIX=/usr/local
|
||||||
BINDIR=$(PREFIX)/bin
|
BINDIR=$(PREFIX)/bin
|
||||||
MANDIR=$(PREFIX)/man
|
MANDIR=$(PREFIX)/man
|
||||||
SYSCONFDIR=/etc
|
|
||||||
PYTHON=/usr/bin/env python
|
PYTHON=/usr/bin/env python
|
||||||
|
|
||||||
|
# set SYSCONFDIR to /etc if PREFIX=/usr or PREFIX=/usr/local
|
||||||
|
ifeq ($(PREFIX),/usr)
|
||||||
|
SYSCONFDIR=/etc
|
||||||
|
else
|
||||||
|
ifeq ($(PREFIX),/usr/local)
|
||||||
|
SYSCONFDIR=/etc
|
||||||
|
else
|
||||||
|
SYSCONFDIR=$(PREFIX)/etc
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
install: youtube-dl youtube-dl.1 youtube-dl.bash-completion
|
install: youtube-dl youtube-dl.1 youtube-dl.bash-completion
|
||||||
install -d $(DESTDIR)$(BINDIR)
|
install -d $(DESTDIR)$(BINDIR)
|
||||||
install -m 755 youtube-dl $(DESTDIR)$(BINDIR)
|
install -m 755 youtube-dl $(DESTDIR)$(BINDIR)
|
||||||
|
229
README.md
229
README.md
@ -14,113 +14,137 @@ 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.
|
which means you can modify it, redistribute it or use it however you like.
|
||||||
|
|
||||||
# OPTIONS
|
# OPTIONS
|
||||||
-h, --help print this help text and exit
|
-h, --help print this help text and exit
|
||||||
--version print program version and exit
|
--version print program version and exit
|
||||||
-U, --update update this program to latest version
|
-U, --update update this program to latest version
|
||||||
-i, --ignore-errors continue on download errors
|
-i, --ignore-errors continue on download errors
|
||||||
-r, --rate-limit LIMIT maximum download rate (e.g. 50k or 44.6m)
|
-r, --rate-limit LIMIT maximum download rate (e.g. 50k or 44.6m)
|
||||||
-R, --retries RETRIES number of retries (default is 10)
|
-R, --retries RETRIES number of retries (default is 10)
|
||||||
--buffer-size SIZE size of download buffer (e.g. 1024 or 16k) (default
|
--buffer-size SIZE size of download buffer (e.g. 1024 or 16k)
|
||||||
is 1024)
|
(default is 1024)
|
||||||
--no-resize-buffer do not automatically adjust the buffer size. By
|
--no-resize-buffer do not automatically adjust the buffer size. By
|
||||||
default, the buffer size is automatically resized
|
default, the buffer size is automatically resized
|
||||||
from an initial value of SIZE.
|
from an initial value of SIZE.
|
||||||
--dump-user-agent display the current browser identification
|
--dump-user-agent display the current browser identification
|
||||||
--user-agent UA specify a custom user agent
|
--user-agent UA specify a custom user agent
|
||||||
--list-extractors List all supported extractors and the URLs they
|
--referer REF specify a custom referer, use if the video access
|
||||||
would handle
|
is restricted to one domain
|
||||||
|
--list-extractors List all supported extractors and the URLs they
|
||||||
|
would handle
|
||||||
|
--proxy URL Use the specified HTTP/HTTPS proxy
|
||||||
|
--no-check-certificate Suppress HTTPS certificate validation.
|
||||||
|
|
||||||
## 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)
|
||||||
--playlist-end NUMBER playlist video to end at (default is last)
|
--playlist-end NUMBER playlist video to end at (default is last)
|
||||||
--match-title REGEX download only matching titles (regex or caseless
|
--match-title REGEX download only matching titles (regex or caseless
|
||||||
sub-string)
|
sub-string)
|
||||||
--reject-title REGEX skip download for matching titles (regex or
|
--reject-title REGEX skip download for matching titles (regex or
|
||||||
caseless sub-string)
|
caseless sub-string)
|
||||||
--max-downloads NUMBER Abort after downloading NUMBER files
|
--max-downloads NUMBER Abort after downloading NUMBER files
|
||||||
--min-filesize SIZE Do not download any videos smaller than SIZE (e.g.
|
--min-filesize SIZE Do not download any videos smaller than SIZE
|
||||||
50k or 44.6m)
|
(e.g. 50k or 44.6m)
|
||||||
--max-filesize SIZE Do not download any videos larger than SIZE (e.g.
|
--max-filesize SIZE Do not download any videos larger than SIZE (e.g.
|
||||||
50k or 44.6m)
|
50k or 44.6m)
|
||||||
|
--date DATE download only videos uploaded in this date
|
||||||
|
--datebefore DATE download only videos uploaded before this date
|
||||||
|
--dateafter DATE download only videos uploaded after this date
|
||||||
|
|
||||||
## Filesystem Options:
|
## Filesystem Options:
|
||||||
-t, --title use title in file name
|
-t, --title use title in file name (default)
|
||||||
--id use video ID in file name
|
--id use only video ID in file name
|
||||||
-l, --literal [deprecated] alias of --title
|
-l, --literal [deprecated] alias of --title
|
||||||
-A, --auto-number number downloaded files starting from 00000
|
-A, --auto-number number downloaded files starting from 00000
|
||||||
-o, --output TEMPLATE output filename template. Use %(title)s to get the
|
-o, --output TEMPLATE output filename template. Use %(title)s to get
|
||||||
title, %(uploader)s for the uploader name,
|
the title, %(uploader)s for the uploader name,
|
||||||
%(uploader_id)s for the uploader nickname if
|
%(uploader_id)s for the uploader nickname if
|
||||||
different, %(autonumber)s to get an automatically
|
different, %(autonumber)s to get an automatically
|
||||||
incremented number, %(ext)s for the filename
|
incremented number, %(ext)s for the filename
|
||||||
extension, %(upload_date)s for the upload date
|
extension, %(upload_date)s for the upload date
|
||||||
(YYYYMMDD), %(extractor)s for the provider
|
(YYYYMMDD), %(extractor)s for the provider
|
||||||
(youtube, metacafe, etc), %(id)s for the video id
|
(youtube, metacafe, etc), %(id)s for the video id
|
||||||
and %% for a literal percent. Use - to output to
|
, %(playlist)s for the playlist the video is in,
|
||||||
stdout. Can also be used to download to a different
|
%(playlist_index)s for the position in the
|
||||||
directory, for example with -o '/my/downloads/%(upl
|
playlist and %% for a literal percent. Use - to
|
||||||
oader)s/%(title)s-%(id)s.%(ext)s' .
|
output to stdout. Can also be used to download to
|
||||||
--restrict-filenames Restrict filenames to only ASCII characters, and
|
a different directory, for example with -o '/my/d
|
||||||
avoid "&" and spaces in filenames
|
ownloads/%(uploader)s/%(title)s-%(id)s.%(ext)s' .
|
||||||
-a, --batch-file FILE file containing URLs to download ('-' for stdin)
|
--autonumber-size NUMBER Specifies the number of digits in %(autonumber)s
|
||||||
-w, --no-overwrites do not overwrite files
|
when it is present in output filename template or
|
||||||
-c, --continue resume partially downloaded files
|
--autonumber option is given
|
||||||
--no-continue do not resume partially downloaded files (restart
|
--restrict-filenames Restrict filenames to only ASCII characters, and
|
||||||
from beginning)
|
avoid "&" and spaces in filenames
|
||||||
--cookies FILE file to read cookies from and dump cookie jar in
|
-a, --batch-file FILE file containing URLs to download ('-' for stdin)
|
||||||
--no-part do not use .part files
|
-w, --no-overwrites do not overwrite files
|
||||||
--no-mtime do not use the Last-modified header to set the file
|
-c, --continue resume partially downloaded files
|
||||||
modification time
|
--no-continue do not resume partially downloaded files (restart
|
||||||
--write-description write video description to a .description file
|
from beginning)
|
||||||
--write-info-json write video metadata to a .info.json file
|
--cookies FILE file to read cookies from and dump cookie jar in
|
||||||
|
--no-part do not use .part files
|
||||||
|
--no-mtime do not use the Last-modified header to set the
|
||||||
|
file modification time
|
||||||
|
--write-description write video description to a .description file
|
||||||
|
--write-info-json write video metadata to a .info.json file
|
||||||
|
--write-thumbnail write thumbnail image to disk
|
||||||
|
|
||||||
## Verbosity / Simulation Options:
|
## Verbosity / Simulation Options:
|
||||||
-q, --quiet activates quiet mode
|
-q, --quiet activates quiet mode
|
||||||
-s, --simulate do not download the video and do not write anything
|
-s, --simulate do not download the video and do not write
|
||||||
to disk
|
anything to disk
|
||||||
--skip-download do not download the video
|
--skip-download do not download the video
|
||||||
-g, --get-url simulate, quiet but print URL
|
-g, --get-url simulate, quiet but print URL
|
||||||
-e, --get-title simulate, quiet but print title
|
-e, --get-title simulate, quiet but print title
|
||||||
--get-thumbnail simulate, quiet but print thumbnail URL
|
--get-id simulate, quiet but print id
|
||||||
--get-description simulate, quiet but print video description
|
--get-thumbnail simulate, quiet but print thumbnail URL
|
||||||
--get-filename simulate, quiet but print output filename
|
--get-description simulate, quiet but print video description
|
||||||
--get-format simulate, quiet but print output format
|
--get-filename simulate, quiet but print output filename
|
||||||
--newline output progress bar as new lines
|
--get-format simulate, quiet but print output format
|
||||||
--no-progress do not print progress bar
|
--newline output progress bar as new lines
|
||||||
--console-title display progress in console titlebar
|
--no-progress do not print progress bar
|
||||||
-v, --verbose print various debugging information
|
--console-title display progress in console titlebar
|
||||||
|
-v, --verbose print various debugging information
|
||||||
|
--dump-intermediate-pages print downloaded pages to debug problems(very
|
||||||
|
verbose)
|
||||||
|
|
||||||
## Video Format Options:
|
## Video Format Options:
|
||||||
-f, --format FORMAT video format code
|
-f, --format FORMAT video format code, specifiy the order of
|
||||||
--all-formats download all available video formats
|
preference using slashes: "-f 22/17/18"
|
||||||
--prefer-free-formats prefer free video formats unless a specific one is
|
--all-formats download all available video formats
|
||||||
requested
|
--prefer-free-formats prefer free video formats unless a specific one
|
||||||
--max-quality FORMAT highest quality format to download
|
is requested
|
||||||
-F, --list-formats list all available formats (currently youtube only)
|
--max-quality FORMAT highest quality format to download
|
||||||
--write-srt write video closed captions to a .srt file
|
-F, --list-formats list all available formats (currently youtube
|
||||||
(currently youtube only)
|
only)
|
||||||
--srt-lang LANG language of the closed captions to download
|
--write-sub write subtitle file (currently youtube only)
|
||||||
(optional) use IETF language tags like 'en'
|
--only-sub [deprecated] alias of --skip-download
|
||||||
|
--all-subs downloads all the available subtitles of the
|
||||||
|
video (currently youtube only)
|
||||||
|
--list-subs lists all available subtitles for the video
|
||||||
|
(currently youtube only)
|
||||||
|
--sub-format LANG subtitle format [srt/sbv] (default=srt)
|
||||||
|
(currently youtube only)
|
||||||
|
--sub-lang LANG language of the subtitles to download (optional)
|
||||||
|
use IETF language tags like 'en'
|
||||||
|
|
||||||
## Authentication Options:
|
## Authentication Options:
|
||||||
-u, --username USERNAME account username
|
-u, --username USERNAME account username
|
||||||
-p, --password PASSWORD account password
|
-p, --password PASSWORD account password
|
||||||
-n, --netrc use .netrc authentication data
|
-n, --netrc use .netrc authentication data
|
||||||
|
|
||||||
## Post-processing Options:
|
## Post-processing Options:
|
||||||
-x, --extract-audio convert video files to audio-only files (requires
|
-x, --extract-audio convert video files to audio-only files (requires
|
||||||
ffmpeg or avconv and ffprobe or avprobe)
|
ffmpeg or avconv and ffprobe or avprobe)
|
||||||
--audio-format FORMAT "best", "aac", "vorbis", "mp3", "m4a", "opus", or
|
--audio-format FORMAT "best", "aac", "vorbis", "mp3", "m4a", "opus", or
|
||||||
"wav"; best by default
|
"wav"; best by default
|
||||||
--audio-quality QUALITY ffmpeg/avconv audio quality specification, insert a
|
--audio-quality QUALITY ffmpeg/avconv audio quality specification, insert
|
||||||
value between 0 (better) and 9 (worse) for VBR or a
|
a value between 0 (better) and 9 (worse) for VBR
|
||||||
specific bitrate like 128K (default 5)
|
or a specific bitrate like 128K (default 5)
|
||||||
--recode-video FORMAT Encode the video to another format if necessary
|
--recode-video FORMAT Encode the video to another format if necessary
|
||||||
(currently supported: mp4|flv|ogg|webm)
|
(currently supported: mp4|flv|ogg|webm)
|
||||||
-k, --keep-video keeps the video file on disk after the post-
|
-k, --keep-video keeps the video file on disk after the post-
|
||||||
processing; the video is erased by default
|
processing; the video is erased by default
|
||||||
--no-post-overwrites do not overwrite post-processed files; the post-
|
--no-post-overwrites do not overwrite post-processed files; the post-
|
||||||
processed files are overwritten by default
|
processed files are overwritten by default
|
||||||
|
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
|
|
||||||
@ -138,6 +162,8 @@ The `-o` option allows users to indicate a template for the output file names. T
|
|||||||
- `ext`: The sequence will be replaced by the appropriate extension (like flv or mp4).
|
- `ext`: The sequence will be replaced by the appropriate extension (like flv or mp4).
|
||||||
- `epoch`: The sequence will be replaced by the Unix epoch when creating the file.
|
- `epoch`: The sequence will be replaced by the Unix epoch when creating the file.
|
||||||
- `autonumber`: The sequence will be replaced by a five-digit number that will be increased with each download, starting at zero.
|
- `autonumber`: The sequence will be replaced by a five-digit number that will be increased with each download, starting at zero.
|
||||||
|
- `playlist`: The name or the id of the playlist that contains the video.
|
||||||
|
- `playlist_index`: The index of the video in the playlist, a five-digit number.
|
||||||
|
|
||||||
The current default template is `%(id)s.%(ext)s`, but that will be switchted to `%(title)s-%(id)s.%(ext)s` (which can be requested with `-t` at the moment).
|
The current default template is `%(id)s.%(ext)s`, but that will be switchted to `%(title)s-%(id)s.%(ext)s` (which can be requested with `-t` at the moment).
|
||||||
|
|
||||||
@ -148,6 +174,19 @@ In some cases, you don't want special characters such as 中, spaces, or &, such
|
|||||||
$ youtube-dl --get-filename -o "%(title)s.%(ext)s" BaW_jenozKc --restrict-filenames
|
$ youtube-dl --get-filename -o "%(title)s.%(ext)s" BaW_jenozKc --restrict-filenames
|
||||||
youtube-dl_test_video_.mp4 # A simple file name
|
youtube-dl_test_video_.mp4 # A simple file name
|
||||||
|
|
||||||
|
# VIDEO SELECTION
|
||||||
|
|
||||||
|
Videos can be filtered by their upload date using the options `--date`, `--datebefore` or `--dateafter`, they accept dates in two formats:
|
||||||
|
|
||||||
|
- Absolute dates: Dates in the format `YYYYMMDD`.
|
||||||
|
- Relative dates: Dates in the format `(now|today)[+-][0-9](day|week|month|year)(s)?`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ youtube-dl --dateafter now-6months #will only download the videos uploaded in the last 6 months
|
||||||
|
$ youtube-dl --date 19700101 #will only download the videos uploaded in January 1, 1970
|
||||||
|
$ youtube-dl --dateafter 20000101 --datebefore 20100101 #will only download the videos uploaded between 2000 and 2010
|
||||||
|
|
||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
### Can you please put the -b option back?
|
### Can you please put the -b option back?
|
||||||
|
57
devscripts/gh-pages/update-feed.py
Executable file
57
devscripts/gh-pages/update-feed.py
Executable file
@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
atom_template=textwrap.dedent("""\
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<atom:feed xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<atom:title>youtube-dl releases</atom:title>
|
||||||
|
<atom:id>youtube-dl-updates-feed</atom:id>
|
||||||
|
<atom:updated>@TIMESTAMP@</atom:updated>
|
||||||
|
@ENTRIES@
|
||||||
|
</atom:feed>""")
|
||||||
|
|
||||||
|
entry_template=textwrap.dedent("""
|
||||||
|
<atom:entry>
|
||||||
|
<atom:id>youtube-dl-@VERSION@</atom:id>
|
||||||
|
<atom:title>New version @VERSION@</atom:title>
|
||||||
|
<atom:link href="http://rg3.github.io/youtube-dl" />
|
||||||
|
<atom:content type="xhtml">
|
||||||
|
<div xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
Downloads available at <a href="http://youtube-dl.org/downloads/@VERSION@/">http://youtube-dl.org/downloads/@VERSION@/</a>
|
||||||
|
</div>
|
||||||
|
</atom:content>
|
||||||
|
<atom:author>
|
||||||
|
<atom:name>The youtube-dl maintainers</atom:name>
|
||||||
|
</atom:author>
|
||||||
|
<atom:updated>@TIMESTAMP@</atom:updated>
|
||||||
|
</atom:entry>
|
||||||
|
""")
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
now_iso = now.isoformat()
|
||||||
|
|
||||||
|
atom_template = atom_template.replace('@TIMESTAMP@',now_iso)
|
||||||
|
|
||||||
|
entries=[]
|
||||||
|
|
||||||
|
versions_info = json.load(open('update/versions.json'))
|
||||||
|
versions = list(versions_info['versions'].keys())
|
||||||
|
versions.sort()
|
||||||
|
|
||||||
|
for v in versions:
|
||||||
|
entry = entry_template.replace('@TIMESTAMP@',v.replace('.','-'))
|
||||||
|
entry = entry.replace('@VERSION@',v)
|
||||||
|
entries.append(entry)
|
||||||
|
|
||||||
|
entries_str = textwrap.indent(''.join(entries), '\t')
|
||||||
|
atom_template = atom_template.replace('@ENTRIES@', entries_str)
|
||||||
|
|
||||||
|
with open('update/releases.atom','w',encoding='utf-8') as atom_file:
|
||||||
|
atom_file.write(atom_template)
|
||||||
|
|
||||||
|
|
@ -69,6 +69,7 @@ ROOT=$(pwd)
|
|||||||
ORIGIN_URL=$(git config --get remote.origin.url)
|
ORIGIN_URL=$(git config --get remote.origin.url)
|
||||||
cd build/gh-pages
|
cd build/gh-pages
|
||||||
"$ROOT/devscripts/gh-pages/add-version.py" $version
|
"$ROOT/devscripts/gh-pages/add-version.py" $version
|
||||||
|
"$ROOT/devscripts/gh-pages/update-feed.py"
|
||||||
"$ROOT/devscripts/gh-pages/sign-versions.py" < "$ROOT/updates_key.pem"
|
"$ROOT/devscripts/gh-pages/sign-versions.py" < "$ROOT/updates_key.pem"
|
||||||
"$ROOT/devscripts/gh-pages/generate-download.py"
|
"$ROOT/devscripts/gh-pages/generate-download.py"
|
||||||
"$ROOT/devscripts/gh-pages/update-copyright.py"
|
"$ROOT/devscripts/gh-pages/update-copyright.py"
|
||||||
|
@ -40,7 +40,7 @@ raw_input()
|
|||||||
|
|
||||||
filename = sys.argv[0]
|
filename = sys.argv[0]
|
||||||
|
|
||||||
UPDATE_URL = "http://rg3.github.com/youtube-dl/update/"
|
UPDATE_URL = "http://rg3.github.io/youtube-dl/update/"
|
||||||
VERSION_URL = UPDATE_URL + 'LATEST_VERSION'
|
VERSION_URL = UPDATE_URL + 'LATEST_VERSION'
|
||||||
JSON_URL = UPDATE_URL + 'versions.json'
|
JSON_URL = UPDATE_URL + 'versions.json'
|
||||||
UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537)
|
UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537)
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"simulate": false,
|
"simulate": false,
|
||||||
"skip_download": false,
|
"skip_download": false,
|
||||||
"subtitleslang": null,
|
"subtitleslang": null,
|
||||||
|
"subtitlesformat": "srt",
|
||||||
"test": true,
|
"test": true,
|
||||||
"updatetime": true,
|
"updatetime": true,
|
||||||
"usenetrc": false,
|
"usenetrc": false,
|
||||||
@ -36,5 +37,8 @@
|
|||||||
"verbose": true,
|
"verbose": true,
|
||||||
"writedescription": false,
|
"writedescription": false,
|
||||||
"writeinfojson": true,
|
"writeinfojson": true,
|
||||||
"writesubtitles": false
|
"writesubtitles": false,
|
||||||
}
|
"onlysubtitles": false,
|
||||||
|
"allsubtitles": false,
|
||||||
|
"listssubtitles": false
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@ import unittest
|
|||||||
import os
|
import os
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from youtube_dl.InfoExtractors import YoutubeIE, YoutubePlaylistIE
|
from youtube_dl.InfoExtractors import YoutubeIE, YoutubePlaylistIE, YoutubeChannelIE
|
||||||
|
|
||||||
class TestAllURLsMatching(unittest.TestCase):
|
class TestAllURLsMatching(unittest.TestCase):
|
||||||
def test_youtube_playlist_matching(self):
|
def test_youtube_playlist_matching(self):
|
||||||
@ -24,6 +24,11 @@ class TestAllURLsMatching(unittest.TestCase):
|
|||||||
self.assertTrue(YoutubeIE.suitable(u'PLtS2H6bU1M'))
|
self.assertTrue(YoutubeIE.suitable(u'PLtS2H6bU1M'))
|
||||||
self.assertFalse(YoutubeIE.suitable(u'https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012')) #668
|
self.assertFalse(YoutubeIE.suitable(u'https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012')) #668
|
||||||
|
|
||||||
|
def test_youtube_channel_matching(self):
|
||||||
|
self.assertTrue(YoutubeChannelIE.suitable('https://www.youtube.com/channel/HCtnHdj3df7iM'))
|
||||||
|
self.assertTrue(YoutubeChannelIE.suitable('https://www.youtube.com/channel/HCtnHdj3df7iM?feature=gb_ch_rec'))
|
||||||
|
self.assertTrue(YoutubeChannelIE.suitable('https://www.youtube.com/channel/HCtnHdj3df7iM/videos'))
|
||||||
|
|
||||||
def test_youtube_extract(self):
|
def test_youtube_extract(self):
|
||||||
self.assertEqual(YoutubeIE()._extract_id('http://www.youtube.com/watch?&v=BaW_jenozKc'), 'BaW_jenozKc')
|
self.assertEqual(YoutubeIE()._extract_id('http://www.youtube.com/watch?&v=BaW_jenozKc'), 'BaW_jenozKc')
|
||||||
self.assertEqual(YoutubeIE()._extract_id('https://www.youtube.com/watch?&v=BaW_jenozKc'), 'BaW_jenozKc')
|
self.assertEqual(YoutubeIE()._extract_id('https://www.youtube.com/watch?&v=BaW_jenozKc'), 'BaW_jenozKc')
|
||||||
|
@ -20,6 +20,8 @@ from youtube_dl.utils import *
|
|||||||
DEF_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tests.json')
|
DEF_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tests.json')
|
||||||
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json")
|
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json")
|
||||||
|
|
||||||
|
RETRIES = 3
|
||||||
|
|
||||||
# General configuration (from __init__, not very elegant...)
|
# General configuration (from __init__, not very elegant...)
|
||||||
jar = compat_cookiejar.CookieJar()
|
jar = compat_cookiejar.CookieJar()
|
||||||
cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar)
|
cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar)
|
||||||
@ -56,6 +58,7 @@ with io.open(PARAMETERS_FILE, encoding='utf-8') as pf:
|
|||||||
|
|
||||||
|
|
||||||
class TestDownload(unittest.TestCase):
|
class TestDownload(unittest.TestCase):
|
||||||
|
maxDiff = None
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
self.defs = defs
|
self.defs = defs
|
||||||
@ -64,7 +67,7 @@ class TestDownload(unittest.TestCase):
|
|||||||
def generator(test_case):
|
def generator(test_case):
|
||||||
|
|
||||||
def test_template(self):
|
def test_template(self):
|
||||||
ie = getattr(youtube_dl.InfoExtractors, test_case['name'] + 'IE')
|
ie = youtube_dl.InfoExtractors.get_info_extractor(test_case['name'])
|
||||||
if not ie._WORKING:
|
if not ie._WORKING:
|
||||||
print('Skipping: IE marked as not _WORKING')
|
print('Skipping: IE marked as not _WORKING')
|
||||||
return
|
return
|
||||||
@ -79,9 +82,8 @@ def generator(test_case):
|
|||||||
params.update(test_case.get('params', {}))
|
params.update(test_case.get('params', {}))
|
||||||
|
|
||||||
fd = FileDownloader(params)
|
fd = FileDownloader(params)
|
||||||
fd.add_info_extractor(ie())
|
for ie in youtube_dl.InfoExtractors.gen_extractors():
|
||||||
for ien in test_case.get('add_ie', []):
|
fd.add_info_extractor(ie)
|
||||||
fd.add_info_extractor(getattr(youtube_dl.InfoExtractors, ien + 'IE')())
|
|
||||||
finished_hook_called = set()
|
finished_hook_called = set()
|
||||||
def _hook(status):
|
def _hook(status):
|
||||||
if status['status'] == 'finished':
|
if status['status'] == 'finished':
|
||||||
@ -94,7 +96,19 @@ def generator(test_case):
|
|||||||
_try_rm(tc['file'] + '.part')
|
_try_rm(tc['file'] + '.part')
|
||||||
_try_rm(tc['file'] + '.info.json')
|
_try_rm(tc['file'] + '.info.json')
|
||||||
try:
|
try:
|
||||||
fd.download([test_case['url']])
|
for retry in range(1, RETRIES + 1):
|
||||||
|
try:
|
||||||
|
fd.download([test_case['url']])
|
||||||
|
except (DownloadError, ExtractorError) as err:
|
||||||
|
if retry == RETRIES: raise
|
||||||
|
|
||||||
|
# Check if the exception is not a network related one
|
||||||
|
if not err.exc_info[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
|
||||||
|
raise
|
||||||
|
|
||||||
|
print('Retrying: {0} failed tries\n\n##########\n\n'.format(retry))
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
for tc in test_cases:
|
for tc in test_cases:
|
||||||
if not test_case.get('params', {}).get('skip_download', False):
|
if not test_case.get('params', {}).get('skip_download', False):
|
||||||
|
@ -14,6 +14,8 @@ from youtube_dl.utils import timeconvert
|
|||||||
from youtube_dl.utils import sanitize_filename
|
from youtube_dl.utils import sanitize_filename
|
||||||
from youtube_dl.utils import unescapeHTML
|
from youtube_dl.utils import unescapeHTML
|
||||||
from youtube_dl.utils import orderedSet
|
from youtube_dl.utils import orderedSet
|
||||||
|
from youtube_dl.utils import DateRange
|
||||||
|
from youtube_dl.utils import unified_strdate
|
||||||
|
|
||||||
if sys.version_info < (3, 0):
|
if sys.version_info < (3, 0):
|
||||||
_compat_str = lambda b: b.decode('unicode-escape')
|
_compat_str = lambda b: b.decode('unicode-escape')
|
||||||
@ -95,6 +97,20 @@ class TestUtil(unittest.TestCase):
|
|||||||
|
|
||||||
def test_unescape_html(self):
|
def test_unescape_html(self):
|
||||||
self.assertEqual(unescapeHTML(_compat_str('%20;')), _compat_str('%20;'))
|
self.assertEqual(unescapeHTML(_compat_str('%20;')), _compat_str('%20;'))
|
||||||
|
|
||||||
|
def test_daterange(self):
|
||||||
|
_20century = DateRange("19000101","20000101")
|
||||||
|
self.assertFalse("17890714" in _20century)
|
||||||
|
_ac = DateRange("00010101")
|
||||||
|
self.assertTrue("19690721" in _ac)
|
||||||
|
_firstmilenium = DateRange(end="10000101")
|
||||||
|
self.assertTrue("07110427" in _firstmilenium)
|
||||||
|
|
||||||
|
def test_unified_dates(self):
|
||||||
|
self.assertEqual(unified_strdate('December 21, 2010'), '20101221')
|
||||||
|
self.assertEqual(unified_strdate('8/7/2009'), '20090708')
|
||||||
|
self.assertEqual(unified_strdate('Dec 14, 2012'), '20121214')
|
||||||
|
self.assertEqual(unified_strdate('2012/10/11 01:56:38 +0000'), '20121011')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -8,8 +8,9 @@ import json
|
|||||||
import os
|
import os
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from youtube_dl.InfoExtractors import YoutubeUserIE, YoutubePlaylistIE, YoutubeIE
|
from youtube_dl.InfoExtractors import YoutubeUserIE, YoutubePlaylistIE, YoutubeIE, YoutubeChannelIE
|
||||||
from youtube_dl.utils import *
|
from youtube_dl.utils import *
|
||||||
|
from youtube_dl.FileDownloader import FileDownloader
|
||||||
|
|
||||||
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json")
|
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json")
|
||||||
with io.open(PARAMETERS_FILE, encoding='utf-8') as pf:
|
with io.open(PARAMETERS_FILE, encoding='utf-8') as pf:
|
||||||
@ -22,64 +23,87 @@ proxy_handler = compat_urllib_request.ProxyHandler()
|
|||||||
opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
|
opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
|
||||||
compat_urllib_request.install_opener(opener)
|
compat_urllib_request.install_opener(opener)
|
||||||
|
|
||||||
class FakeDownloader(object):
|
class FakeDownloader(FileDownloader):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.result = []
|
self.result = []
|
||||||
self.params = parameters
|
self.params = parameters
|
||||||
def to_screen(self, s):
|
def to_screen(self, s):
|
||||||
print(s)
|
print(s)
|
||||||
def trouble(self, s):
|
def trouble(self, s, tb=None):
|
||||||
raise Exception(s)
|
raise Exception(s)
|
||||||
def download(self, x):
|
def extract_info(self, url):
|
||||||
self.result.append(x)
|
self.result.append(url)
|
||||||
|
return url
|
||||||
|
|
||||||
class TestYoutubeLists(unittest.TestCase):
|
class TestYoutubeLists(unittest.TestCase):
|
||||||
|
def assertIsPlaylist(self,info):
|
||||||
|
"""Make sure the info has '_type' set to 'playlist'"""
|
||||||
|
self.assertEqual(info['_type'], 'playlist')
|
||||||
|
|
||||||
def test_youtube_playlist(self):
|
def test_youtube_playlist(self):
|
||||||
dl = FakeDownloader()
|
dl = FakeDownloader()
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubePlaylistIE(dl)
|
||||||
ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')
|
result = ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')[0]
|
||||||
ytie_results = [YoutubeIE()._extract_id(r[0]) for r in dl.result]
|
self.assertIsPlaylist(result)
|
||||||
|
self.assertEqual(result['title'], 'ytdl test PL')
|
||||||
|
ytie_results = [YoutubeIE()._extract_id(url['url']) for url in result['entries']]
|
||||||
self.assertEqual(ytie_results, [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE'])
|
self.assertEqual(ytie_results, [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE'])
|
||||||
|
|
||||||
def test_issue_673(self):
|
def test_issue_673(self):
|
||||||
dl = FakeDownloader()
|
dl = FakeDownloader()
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubePlaylistIE(dl)
|
||||||
ie.extract('PLBB231211A4F62143')
|
result = ie.extract('PLBB231211A4F62143')[0]
|
||||||
self.assertTrue(len(dl.result) > 40)
|
self.assertEqual(result['title'], 'Team Fortress 2')
|
||||||
|
self.assertTrue(len(result['entries']) > 40)
|
||||||
|
|
||||||
def test_youtube_playlist_long(self):
|
def test_youtube_playlist_long(self):
|
||||||
dl = FakeDownloader()
|
dl = FakeDownloader()
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubePlaylistIE(dl)
|
||||||
ie.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')
|
result = ie.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')[0]
|
||||||
self.assertTrue(len(dl.result) >= 799)
|
self.assertIsPlaylist(result)
|
||||||
|
self.assertTrue(len(result['entries']) >= 799)
|
||||||
|
|
||||||
def test_youtube_playlist_with_deleted(self):
|
def test_youtube_playlist_with_deleted(self):
|
||||||
#651
|
#651
|
||||||
dl = FakeDownloader()
|
dl = FakeDownloader()
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubePlaylistIE(dl)
|
||||||
ie.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')
|
result = ie.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')[0]
|
||||||
ytie_results = [YoutubeIE()._extract_id(r[0]) for r in dl.result]
|
ytie_results = [YoutubeIE()._extract_id(url['url']) for url in result['entries']]
|
||||||
self.assertFalse('pElCt5oNDuI' in ytie_results)
|
self.assertFalse('pElCt5oNDuI' in ytie_results)
|
||||||
self.assertFalse('KdPEApIVdWM' in ytie_results)
|
self.assertFalse('KdPEApIVdWM' in ytie_results)
|
||||||
|
|
||||||
|
def test_youtube_playlist_empty(self):
|
||||||
|
dl = FakeDownloader()
|
||||||
|
ie = YoutubePlaylistIE(dl)
|
||||||
|
result = ie.extract('https://www.youtube.com/playlist?list=PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx')[0]
|
||||||
|
self.assertIsPlaylist(result)
|
||||||
|
self.assertEqual(len(result['entries']), 0)
|
||||||
|
|
||||||
def test_youtube_course(self):
|
def test_youtube_course(self):
|
||||||
dl = FakeDownloader()
|
dl = FakeDownloader()
|
||||||
ie = YoutubePlaylistIE(dl)
|
ie = YoutubePlaylistIE(dl)
|
||||||
# TODO find a > 100 (paginating?) videos course
|
# TODO find a > 100 (paginating?) videos course
|
||||||
ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')
|
result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')[0]
|
||||||
self.assertEqual(YoutubeIE()._extract_id(dl.result[0][0]), 'j9WZyLZCBzs')
|
entries = result['entries']
|
||||||
self.assertEqual(len(dl.result), 25)
|
self.assertEqual(YoutubeIE()._extract_id(entries[0]['url']), 'j9WZyLZCBzs')
|
||||||
self.assertEqual(YoutubeIE()._extract_id(dl.result[-1][0]), 'rYefUsYuEp0')
|
self.assertEqual(len(entries), 25)
|
||||||
|
self.assertEqual(YoutubeIE()._extract_id(entries[-1]['url']), 'rYefUsYuEp0')
|
||||||
|
|
||||||
def test_youtube_channel(self):
|
def test_youtube_channel(self):
|
||||||
# I give up, please find a channel that does paginate and test this like test_youtube_playlist_long
|
dl = FakeDownloader()
|
||||||
pass # TODO
|
ie = YoutubeChannelIE(dl)
|
||||||
|
#test paginated channel
|
||||||
|
result = ie.extract('https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w')[0]
|
||||||
|
self.assertTrue(len(result['entries']) > 90)
|
||||||
|
#test autogenerated channel
|
||||||
|
result = ie.extract('https://www.youtube.com/channel/HCtnHdj3df7iM/videos')[0]
|
||||||
|
self.assertTrue(len(result['entries']) >= 18)
|
||||||
|
|
||||||
def test_youtube_user(self):
|
def test_youtube_user(self):
|
||||||
dl = FakeDownloader()
|
dl = FakeDownloader()
|
||||||
ie = YoutubeUserIE(dl)
|
ie = YoutubeUserIE(dl)
|
||||||
ie.extract('https://www.youtube.com/user/TheLinuxFoundation')
|
result = ie.extract('https://www.youtube.com/user/TheLinuxFoundation')[0]
|
||||||
self.assertTrue(len(dl.result) >= 320)
|
self.assertTrue(len(result['entries']) >= 320)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -12,6 +12,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||||||
|
|
||||||
from youtube_dl.InfoExtractors import YoutubeIE
|
from youtube_dl.InfoExtractors import YoutubeIE
|
||||||
from youtube_dl.utils import *
|
from youtube_dl.utils import *
|
||||||
|
from youtube_dl import FileDownloader
|
||||||
|
|
||||||
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json")
|
PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json")
|
||||||
with io.open(PARAMETERS_FILE, encoding='utf-8') as pf:
|
with io.open(PARAMETERS_FILE, encoding='utf-8') as pf:
|
||||||
@ -24,13 +25,13 @@ proxy_handler = compat_urllib_request.ProxyHandler()
|
|||||||
opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
|
opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
|
||||||
compat_urllib_request.install_opener(opener)
|
compat_urllib_request.install_opener(opener)
|
||||||
|
|
||||||
class FakeDownloader(object):
|
class FakeDownloader(FileDownloader):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.result = []
|
self.result = []
|
||||||
self.params = parameters
|
self.params = parameters
|
||||||
def to_screen(self, s):
|
def to_screen(self, s):
|
||||||
print(s)
|
print(s)
|
||||||
def trouble(self, s):
|
def trouble(self, s, tb=None):
|
||||||
raise Exception(s)
|
raise Exception(s)
|
||||||
def download(self, x):
|
def download(self, x):
|
||||||
self.result.append(x)
|
self.result.append(x)
|
||||||
@ -38,20 +39,63 @@ class FakeDownloader(object):
|
|||||||
md5 = lambda s: hashlib.md5(s.encode('utf-8')).hexdigest()
|
md5 = lambda s: hashlib.md5(s.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
class TestYoutubeSubtitles(unittest.TestCase):
|
class TestYoutubeSubtitles(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['allsubtitles'] = False
|
||||||
|
DL.params['writesubtitles'] = False
|
||||||
|
DL.params['subtitlesformat'] = 'srt'
|
||||||
|
DL.params['listsubtitles'] = False
|
||||||
|
def test_youtube_no_subtitles(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['writesubtitles'] = False
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
subtitles = info_dict[0]['subtitles']
|
||||||
|
self.assertEqual(subtitles, None)
|
||||||
def test_youtube_subtitles(self):
|
def test_youtube_subtitles(self):
|
||||||
DL = FakeDownloader()
|
DL = FakeDownloader()
|
||||||
DL.params['writesubtitles'] = True
|
DL.params['writesubtitles'] = True
|
||||||
IE = YoutubeIE(DL)
|
IE = YoutubeIE(DL)
|
||||||
info_dict = IE.extract('QRS8MkLhQmM')
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
self.assertEqual(md5(info_dict[0]['subtitles']), 'c3228550d59116f3c29fba370b55d033')
|
sub = info_dict[0]['subtitles'][0]
|
||||||
|
self.assertEqual(md5(sub[2]), '4cd9278a35ba2305f47354ee13472260')
|
||||||
def test_youtube_subtitles_it(self):
|
def test_youtube_subtitles_it(self):
|
||||||
DL = FakeDownloader()
|
DL = FakeDownloader()
|
||||||
DL.params['writesubtitles'] = True
|
DL.params['writesubtitles'] = True
|
||||||
DL.params['subtitleslang'] = 'it'
|
DL.params['subtitleslang'] = 'it'
|
||||||
IE = YoutubeIE(DL)
|
IE = YoutubeIE(DL)
|
||||||
info_dict = IE.extract('QRS8MkLhQmM')
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
self.assertEqual(md5(info_dict[0]['subtitles']), '132a88a0daf8e1520f393eb58f1f646a')
|
sub = info_dict[0]['subtitles'][0]
|
||||||
|
self.assertEqual(md5(sub[2]), '164a51f16f260476a05b50fe4c2f161d')
|
||||||
|
def test_youtube_onlysubtitles(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['writesubtitles'] = True
|
||||||
|
DL.params['onlysubtitles'] = True
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
sub = info_dict[0]['subtitles'][0]
|
||||||
|
self.assertEqual(md5(sub[2]), '4cd9278a35ba2305f47354ee13472260')
|
||||||
|
def test_youtube_allsubtitles(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['allsubtitles'] = True
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
subtitles = info_dict[0]['subtitles']
|
||||||
|
self.assertEqual(len(subtitles), 13)
|
||||||
|
def test_youtube_subtitles_format(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['writesubtitles'] = True
|
||||||
|
DL.params['subtitlesformat'] = 'sbv'
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
sub = info_dict[0]['subtitles'][0]
|
||||||
|
self.assertEqual(md5(sub[2]), '13aeaa0c245a8bed9a451cb643e3ad8b')
|
||||||
|
def test_youtube_list_subtitles(self):
|
||||||
|
DL = FakeDownloader()
|
||||||
|
DL.params['listsubtitles'] = True
|
||||||
|
IE = YoutubeIE(DL)
|
||||||
|
info_dict = IE.extract('QRS8MkLhQmM')
|
||||||
|
self.assertEqual(info_dict, None)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
195
test/tests.json
195
test/tests.json
@ -76,8 +76,7 @@
|
|||||||
"name": "StanfordOpenClassroom",
|
"name": "StanfordOpenClassroom",
|
||||||
"md5": "544a9468546059d4e80d76265b0443b8",
|
"md5": "544a9468546059d4e80d76265b0443b8",
|
||||||
"url": "http://openclassroom.stanford.edu/MainFolder/VideoPage.php?course=PracticalUnix&video=intro-environment&speed=100",
|
"url": "http://openclassroom.stanford.edu/MainFolder/VideoPage.php?course=PracticalUnix&video=intro-environment&speed=100",
|
||||||
"file": "PracticalUnix_intro-environment.mp4",
|
"file": "PracticalUnix_intro-environment.mp4"
|
||||||
"skip": "Currently offline"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "XNXX",
|
"name": "XNXX",
|
||||||
@ -113,9 +112,8 @@
|
|||||||
{
|
{
|
||||||
"name": "Escapist",
|
"name": "Escapist",
|
||||||
"url": "http://www.escapistmagazine.com/videos/view/the-escapist-presents/6618-Breaking-Down-Baldurs-Gate",
|
"url": "http://www.escapistmagazine.com/videos/view/the-escapist-presents/6618-Breaking-Down-Baldurs-Gate",
|
||||||
"file": "6618-Breaking-Down-Baldurs-Gate.flv",
|
"file": "6618-Breaking-Down-Baldurs-Gate.mp4",
|
||||||
"md5": "c6793dbda81388f4264c1ba18684a74d",
|
"md5": "c6793dbda81388f4264c1ba18684a74d"
|
||||||
"skip": "Fails with timeout on Travis"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "GooglePlus",
|
"name": "GooglePlus",
|
||||||
@ -154,7 +152,8 @@
|
|||||||
"file": "20274954.flv",
|
"file": "20274954.flv",
|
||||||
"md5": "088f151799e8f572f84eb62f17d73e5c",
|
"md5": "088f151799e8f572f84eb62f17d73e5c",
|
||||||
"info_dict": {
|
"info_dict": {
|
||||||
"title": "Young Americans for Liberty February 7, 2012 2:28 AM"
|
"title": "Young Americans for Liberty February 7, 2012 2:28 AM",
|
||||||
|
"uploader": "Young Americans for Liberty"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -299,5 +298,189 @@
|
|||||||
"url": "http://www.hodiho.fr/2013/02/regis-plante-sa-jeep.html",
|
"url": "http://www.hodiho.fr/2013/02/regis-plante-sa-jeep.html",
|
||||||
"file": "13601338388002.mp4",
|
"file": "13601338388002.mp4",
|
||||||
"md5": "85b90ccc9d73b4acd9138d3af4c27f89"
|
"md5": "85b90ccc9d73b4acd9138d3af4c27f89"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Spiegel",
|
||||||
|
"url": "http://www.spiegel.de/video/vulkan-tungurahua-in-ecuador-ist-wieder-aktiv-video-1259285.html",
|
||||||
|
"file": "1259285.mp4",
|
||||||
|
"md5": "2c2754212136f35fb4b19767d242f66e",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Vulkanausbruch in Ecuador: Der \"Feuerschlund\" ist wieder aktiv"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LiveLeak",
|
||||||
|
"md5": "0813c2430bea7a46bf13acf3406992f4",
|
||||||
|
"url": "http://www.liveleak.com/view?i=757_1364311680",
|
||||||
|
"file": "757_1364311680.mp4",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Most unlucky car accident",
|
||||||
|
"description": "extremely bad day for this guy..!",
|
||||||
|
"uploader": "ljfriel2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WorldStarHipHop",
|
||||||
|
"url": "http://www.worldstarhiphop.com/videos/video.php?v=wshh6a7q1ny0G34ZwuIO",
|
||||||
|
"file": "wshh6a7q1ny0G34ZwuIO.mp4",
|
||||||
|
"md5": "9d04de741161603bf7071bbf4e883186",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Video: KO Of The Week: MMA Fighter Gets Knocked Out By Swift Head Kick! "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ARD",
|
||||||
|
"url": "http://www.ardmediathek.de/das-erste/tagesschau-in-100-sek?documentId=14077640",
|
||||||
|
"file": "14077640.mp4",
|
||||||
|
"md5": "6ca8824255460c787376353f9e20bbd8",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "11.04.2013 09:23 Uhr - Tagesschau in 100 Sekunden"
|
||||||
|
},
|
||||||
|
"skip": "Requires rtmpdump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tumblr",
|
||||||
|
"url": "http://birthdayproject2012.tumblr.com/post/17258355236/a-sample-video-from-leeann-if-you-need-an-idea",
|
||||||
|
"file": "17258355236.mp4",
|
||||||
|
"md5": "7c6a514d691b034ccf8567999e9e88a3",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Calling all Pris! - A sample video from LeeAnn. (If you need an idea..."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SoundcloudSet",
|
||||||
|
"url":"https://soundcloud.com/the-concept-band/sets/the-royal-concept-ep",
|
||||||
|
"playlist":[
|
||||||
|
{
|
||||||
|
"file":"30510138.mp3",
|
||||||
|
"md5":"f9136bf103901728f29e419d2c70f55d",
|
||||||
|
"info_dict": {
|
||||||
|
"title":"D-D-Dance"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file":"47127625.mp3",
|
||||||
|
"md5":"09b6758a018470570f8fd423c9453dd8",
|
||||||
|
"info_dict": {
|
||||||
|
"title":"The Royal Concept - Gimme Twice"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file":"47127627.mp3",
|
||||||
|
"md5":"154abd4e418cea19c3b901f1e1306d9c",
|
||||||
|
"info_dict": {
|
||||||
|
"title":"Goldrushed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file":"47127629.mp3",
|
||||||
|
"md5":"2f5471edc79ad3f33a683153e96a79c1",
|
||||||
|
"info_dict": {
|
||||||
|
"title":"In the End"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file":"47127631.mp3",
|
||||||
|
"md5":"f9ba87aa940af7213f98949254f1c6e2",
|
||||||
|
"info_dict": {
|
||||||
|
"title":"Knocked Up"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file":"75206121.mp3",
|
||||||
|
"md5":"f9d1fe9406717e302980c30de4af9353",
|
||||||
|
"info_dict": {
|
||||||
|
"title":"World On Fire"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"Bandcamp",
|
||||||
|
"url":"http://youtube-dl.bandcamp.com/track/youtube-dl-test-song",
|
||||||
|
"file":"1812978515.mp3",
|
||||||
|
"md5":"cdeb30cdae1921719a3cbcab696ef53c",
|
||||||
|
"info_dict": {
|
||||||
|
"title":"youtube-dl test song \"'/\\ä↭"
|
||||||
|
},
|
||||||
|
"skip": "There is a limit of 200 free downloads / month for the test song"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RedTube",
|
||||||
|
"url": "http://www.redtube.com/66418",
|
||||||
|
"file": "66418.mp4",
|
||||||
|
"md5": "7b8c22b5e7098a3e1c09709df1126d2d",
|
||||||
|
"info_dict":{
|
||||||
|
"title":"Sucked on a toilet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Photobucket",
|
||||||
|
"url": "http://media.photobucket.com/user/rachaneronas/media/TiredofLinkBuildingTryBacklinkMyDomaincom_zpsc0c3b9fa.mp4.html?filters[term]=search&filters[primary]=videos&filters[secondary]=images&sort=1&o=0",
|
||||||
|
"file": "zpsc0c3b9fa.mp4",
|
||||||
|
"md5": "7dabfb92b0a31f6c16cebc0f8e60ff99",
|
||||||
|
"info_dict":{
|
||||||
|
"title":"Tired of Link Building? Try BacklinkMyDomain.com!"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ina",
|
||||||
|
"url": "www.ina.fr/video/I12055569/francois-hollande-je-crois-que-c-est-clair-video.html",
|
||||||
|
"file": "I12055569.mp4",
|
||||||
|
"md5": "a667021bf2b41f8dc6049479d9bb38a3",
|
||||||
|
"info_dict":{
|
||||||
|
"title":"François Hollande \"Je crois que c'est clair\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Yahoo",
|
||||||
|
"url": "http://screen.yahoo.com/obama-celebrates-iraq-victory-27592561.html",
|
||||||
|
"file": "27592561.flv",
|
||||||
|
"md5": "c6179bed843512823fd284fa2e7f012d",
|
||||||
|
"info_dict": {
|
||||||
|
"title": "Obama Celebrates Iraq Victory"
|
||||||
|
},
|
||||||
|
"skip": "Requires rtmpdump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Howcast",
|
||||||
|
"url": "http://www.howcast.com/videos/390161-How-to-Tie-a-Square-Knot-Properly",
|
||||||
|
"file": "390161.mp4",
|
||||||
|
"md5": "1d7ba54e2c9d7dc6935ef39e00529138",
|
||||||
|
"info_dict":{
|
||||||
|
"title":"How to Tie a Square Knot Properly",
|
||||||
|
"description":"The square knot, also known as the reef knot, is one of the oldest, most basic knots to tie, and can be used in many different ways. Here's the proper way to tie a square knot."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vine",
|
||||||
|
"url": "https://vine.co/v/b9KOOWX7HUx",
|
||||||
|
"file": "b9KOOWX7HUx.mp4",
|
||||||
|
"md5": "2f36fed6235b16da96ce9b4dc890940d",
|
||||||
|
"info_dict":{
|
||||||
|
"title": "Chicken.",
|
||||||
|
"uploader": "Jack Dorsey"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Flickr",
|
||||||
|
"url": "http://www.flickr.com/photos/forestwander-nature-pictures/5645318632/in/photostream/",
|
||||||
|
"file": "5645318632.mp4",
|
||||||
|
"md5": "6fdc01adbc89d72fc9c4f15b4a4ba87b",
|
||||||
|
"info_dict":{
|
||||||
|
"title": "Dark Hollow Waterfalls",
|
||||||
|
"uploader_id": "forestwander-nature-pictures",
|
||||||
|
"description": "Waterfalls in the Springtime at Dark Hollow Waterfalls. These are located just off of Skyline Drive in Virginia. They are only about 6/10 of a mile hike but it is a pretty steep hill and a good climb back up."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Teamcoco",
|
||||||
|
"url": "http://teamcoco.com/video/louis-ck-interview-george-w-bush",
|
||||||
|
"file": "19705.mp4",
|
||||||
|
"md5": "27b6f7527da5acf534b15f21b032656e",
|
||||||
|
"info_dict":{
|
||||||
|
"title": "Louis C.K. Interview Pt. 1 11/3/11",
|
||||||
|
"description": "Louis C.K. got starstruck by George W. Bush, so what? Part one."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
BIN
youtube-dl
BIN
youtube-dl
Binary file not shown.
@ -7,6 +7,7 @@ import math
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@ -17,6 +18,7 @@ if os.name == 'nt':
|
|||||||
import ctypes
|
import ctypes
|
||||||
|
|
||||||
from .utils import *
|
from .utils import *
|
||||||
|
from .InfoExtractors import get_info_extractor
|
||||||
|
|
||||||
|
|
||||||
class FileDownloader(object):
|
class FileDownloader(object):
|
||||||
@ -52,6 +54,7 @@ class FileDownloader(object):
|
|||||||
quiet: Do not print messages to stdout.
|
quiet: Do not print messages to stdout.
|
||||||
forceurl: Force printing final URL.
|
forceurl: Force printing final URL.
|
||||||
forcetitle: Force printing title.
|
forcetitle: Force printing title.
|
||||||
|
forceid: Force printing ID.
|
||||||
forcethumbnail: Force printing thumbnail URL.
|
forcethumbnail: Force printing thumbnail URL.
|
||||||
forcedescription: Force printing description.
|
forcedescription: Force printing description.
|
||||||
forcefilename: Force printing final filename.
|
forcefilename: Force printing final filename.
|
||||||
@ -78,12 +81,18 @@ class FileDownloader(object):
|
|||||||
updatetime: Use the Last-modified header to set output file timestamps.
|
updatetime: Use the Last-modified header to set output file timestamps.
|
||||||
writedescription: Write the video description to a .description file
|
writedescription: Write the video description to a .description file
|
||||||
writeinfojson: Write the video description to a .info.json file
|
writeinfojson: Write the video description to a .info.json file
|
||||||
writesubtitles: Write the video subtitles to a .srt file
|
writethumbnail: Write the thumbnail image to a file
|
||||||
|
writesubtitles: Write the video subtitles to a file
|
||||||
|
allsubtitles: Downloads all the subtitles of the video
|
||||||
|
listsubtitles: Lists all available subtitles for the video
|
||||||
|
subtitlesformat: Subtitle format [sbv/srt] (default=srt)
|
||||||
subtitleslang: Language of the subtitles to download
|
subtitleslang: Language of the subtitles to download
|
||||||
test: Download only first bytes to test the downloader.
|
test: Download only first bytes to test the downloader.
|
||||||
keepvideo: Keep the video file after post-processing
|
keepvideo: Keep the video file after post-processing
|
||||||
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
|
||||||
|
daterange: A DateRange object, download only if the upload_date is in the range.
|
||||||
|
skip_download: Skip the actual download of the video file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
params = None
|
params = None
|
||||||
@ -116,7 +125,7 @@ class FileDownloader(object):
|
|||||||
exponent = 0
|
exponent = 0
|
||||||
else:
|
else:
|
||||||
exponent = int(math.log(bytes, 1024.0))
|
exponent = int(math.log(bytes, 1024.0))
|
||||||
suffix = 'bkMGTPEZY'[exponent]
|
suffix = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'][exponent]
|
||||||
converted = float(bytes) / float(1024 ** exponent)
|
converted = float(bytes) / float(1024 ** exponent)
|
||||||
return '%.2f%s' % (converted, suffix)
|
return '%.2f%s' % (converted, suffix)
|
||||||
|
|
||||||
@ -227,11 +236,21 @@ class FileDownloader(object):
|
|||||||
self.to_stderr(message)
|
self.to_stderr(message)
|
||||||
if self.params.get('verbose'):
|
if self.params.get('verbose'):
|
||||||
if tb is None:
|
if tb is None:
|
||||||
tb_data = traceback.format_list(traceback.extract_stack())
|
if sys.exc_info()[0]: # if .trouble has been called from an except block
|
||||||
tb = u''.join(tb_data)
|
tb = u''
|
||||||
|
if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
|
||||||
|
tb += u''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
|
||||||
|
tb += compat_str(traceback.format_exc())
|
||||||
|
else:
|
||||||
|
tb_data = traceback.format_list(traceback.extract_stack())
|
||||||
|
tb = u''.join(tb_data)
|
||||||
self.to_stderr(tb)
|
self.to_stderr(tb)
|
||||||
if not self.params.get('ignoreerrors', False):
|
if not self.params.get('ignoreerrors', False):
|
||||||
raise DownloadError(message)
|
if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
|
||||||
|
exc_info = sys.exc_info()[1].exc_info
|
||||||
|
else:
|
||||||
|
exc_info = sys.exc_info()
|
||||||
|
raise DownloadError(message, exc_info)
|
||||||
self._download_retcode = 1
|
self._download_retcode = 1
|
||||||
|
|
||||||
def report_warning(self, message):
|
def report_warning(self, message):
|
||||||
@ -239,13 +258,25 @@ class FileDownloader(object):
|
|||||||
Print the message to stderr, it will be prefixed with 'WARNING:'
|
Print the message to stderr, it will be prefixed with 'WARNING:'
|
||||||
If stderr is a tty file the 'WARNING:' will be colored
|
If stderr is a tty file the 'WARNING:' will be colored
|
||||||
'''
|
'''
|
||||||
if sys.stderr.isatty():
|
if sys.stderr.isatty() and os.name != 'nt':
|
||||||
_msg_header=u'\033[0;33mWARNING:\033[0m'
|
_msg_header=u'\033[0;33mWARNING:\033[0m'
|
||||||
else:
|
else:
|
||||||
_msg_header=u'WARNING:'
|
_msg_header=u'WARNING:'
|
||||||
warning_message=u'%s %s' % (_msg_header,message)
|
warning_message=u'%s %s' % (_msg_header,message)
|
||||||
self.to_stderr(warning_message)
|
self.to_stderr(warning_message)
|
||||||
|
|
||||||
|
def report_error(self, message, tb=None):
|
||||||
|
'''
|
||||||
|
Do the same as trouble, but prefixes the message with 'ERROR:', colored
|
||||||
|
in red if stderr is a tty file.
|
||||||
|
'''
|
||||||
|
if sys.stderr.isatty() and os.name != 'nt':
|
||||||
|
_msg_header = u'\033[0;31mERROR:\033[0m'
|
||||||
|
else:
|
||||||
|
_msg_header = u'ERROR:'
|
||||||
|
error_message = u'%s %s' % (_msg_header, message)
|
||||||
|
self.trouble(error_message, tb)
|
||||||
|
|
||||||
def slow_down(self, start_time, byte_counter):
|
def slow_down(self, start_time, byte_counter):
|
||||||
"""Sleep if the download speed is over the rate limit."""
|
"""Sleep if the download speed is over the rate limit."""
|
||||||
rate_limit = self.params.get('ratelimit', None)
|
rate_limit = self.params.get('ratelimit', None)
|
||||||
@ -277,7 +308,7 @@ class FileDownloader(object):
|
|||||||
return
|
return
|
||||||
os.rename(encodeFilename(old_filename), encodeFilename(new_filename))
|
os.rename(encodeFilename(old_filename), encodeFilename(new_filename))
|
||||||
except (IOError, OSError) as err:
|
except (IOError, OSError) as err:
|
||||||
self.trouble(u'ERROR: unable to rename file')
|
self.report_error(u'unable to rename file')
|
||||||
|
|
||||||
def try_utime(self, filename, last_modified_hdr):
|
def try_utime(self, filename, last_modified_hdr):
|
||||||
"""Try to set the last-modified time of the given file."""
|
"""Try to set the last-modified time of the given file."""
|
||||||
@ -301,9 +332,9 @@ class FileDownloader(object):
|
|||||||
""" Report that the description file is being written """
|
""" Report that the description file is being written """
|
||||||
self.to_screen(u'[info] Writing video description to: ' + descfn)
|
self.to_screen(u'[info] Writing video description to: ' + descfn)
|
||||||
|
|
||||||
def report_writesubtitles(self, srtfn):
|
def report_writesubtitles(self, sub_filename):
|
||||||
""" Report that the subtitles file is being written """
|
""" Report that the subtitles file is being written """
|
||||||
self.to_screen(u'[info] Writing video subtitles to: ' + srtfn)
|
self.to_screen(u'[info] Writing video subtitles to: ' + sub_filename)
|
||||||
|
|
||||||
def report_writeinfojson(self, infofn):
|
def report_writeinfojson(self, infofn):
|
||||||
""" Report that the metadata file has been written """
|
""" Report that the metadata file has been written """
|
||||||
@ -317,12 +348,13 @@ class FileDownloader(object):
|
|||||||
"""Report download progress."""
|
"""Report download progress."""
|
||||||
if self.params.get('noprogress', False):
|
if self.params.get('noprogress', False):
|
||||||
return
|
return
|
||||||
|
clear_line = (u'\x1b[K' if sys.stderr.isatty() and os.name != 'nt' else u'')
|
||||||
if self.params.get('progress_with_newline', False):
|
if self.params.get('progress_with_newline', False):
|
||||||
self.to_screen(u'[download] %s of %s at %s ETA %s' %
|
self.to_screen(u'[download] %s of %s at %s ETA %s' %
|
||||||
(percent_str, data_len_str, speed_str, eta_str))
|
(percent_str, data_len_str, speed_str, eta_str))
|
||||||
else:
|
else:
|
||||||
self.to_screen(u'\r[download] %s of %s at %s ETA %s' %
|
self.to_screen(u'\r%s[download] %s of %s at %s ETA %s' %
|
||||||
(percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
|
(clear_line, percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
|
||||||
self.to_cons_title(u'youtube-dl - %s of %s at %s ETA %s' %
|
self.to_cons_title(u'youtube-dl - %s of %s at %s ETA %s' %
|
||||||
(percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip()))
|
(percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip()))
|
||||||
|
|
||||||
@ -362,7 +394,13 @@ class FileDownloader(object):
|
|||||||
template_dict = dict(info_dict)
|
template_dict = dict(info_dict)
|
||||||
|
|
||||||
template_dict['epoch'] = int(time.time())
|
template_dict['epoch'] = int(time.time())
|
||||||
template_dict['autonumber'] = u'%05d' % self._num_downloads
|
autonumber_size = self.params.get('autonumber_size')
|
||||||
|
if autonumber_size is None:
|
||||||
|
autonumber_size = 5
|
||||||
|
autonumber_templ = u'%0' + str(autonumber_size) + u'd'
|
||||||
|
template_dict['autonumber'] = autonumber_templ % self._num_downloads
|
||||||
|
if template_dict['playlist_index'] is not None:
|
||||||
|
template_dict['playlist_index'] = u'%05d' % template_dict['playlist_index']
|
||||||
|
|
||||||
sanitize = lambda k,v: sanitize_filename(
|
sanitize = lambda k,v: sanitize_filename(
|
||||||
u'NA' if v is None else compat_str(v),
|
u'NA' if v is None else compat_str(v),
|
||||||
@ -373,10 +411,10 @@ class FileDownloader(object):
|
|||||||
filename = self.params['outtmpl'] % template_dict
|
filename = self.params['outtmpl'] % template_dict
|
||||||
return filename
|
return filename
|
||||||
except KeyError as err:
|
except KeyError as err:
|
||||||
self.trouble(u'ERROR: Erroneous output template')
|
self.report_error(u'Erroneous output template')
|
||||||
return None
|
return None
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
self.trouble(u'ERROR: Insufficient system charset ' + repr(preferredencoding()))
|
self.report_error(u'Insufficient system charset ' + repr(preferredencoding()))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _match_entry(self, info_dict):
|
def _match_entry(self, info_dict):
|
||||||
@ -391,10 +429,144 @@ class FileDownloader(object):
|
|||||||
if rejecttitle:
|
if rejecttitle:
|
||||||
if re.search(rejecttitle, title, re.IGNORECASE):
|
if re.search(rejecttitle, title, re.IGNORECASE):
|
||||||
return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
|
return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
|
||||||
|
date = info_dict.get('upload_date', None)
|
||||||
|
if date is not None:
|
||||||
|
dateRange = self.params.get('daterange', DateRange())
|
||||||
|
if date not in dateRange:
|
||||||
|
return u'[download] %s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def extract_info(self, url, download=True, ie_key=None, extra_info={}):
|
||||||
|
'''
|
||||||
|
Returns a list with a dictionary for each video we find.
|
||||||
|
If 'download', also downloads the videos.
|
||||||
|
extra_info is a dict containing the extra values to add to each result
|
||||||
|
'''
|
||||||
|
|
||||||
|
if ie_key:
|
||||||
|
ie = get_info_extractor(ie_key)()
|
||||||
|
ie.set_downloader(self)
|
||||||
|
ies = [ie]
|
||||||
|
else:
|
||||||
|
ies = self._ies
|
||||||
|
|
||||||
|
for ie in ies:
|
||||||
|
if not ie.suitable(url):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not ie.working():
|
||||||
|
self.report_warning(u'The program functionality for this site has been marked as broken, '
|
||||||
|
u'and will probably not work.')
|
||||||
|
|
||||||
|
try:
|
||||||
|
ie_result = ie.extract(url)
|
||||||
|
if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
|
||||||
|
break
|
||||||
|
if isinstance(ie_result, list):
|
||||||
|
# Backwards compatibility: old IE result format
|
||||||
|
for result in ie_result:
|
||||||
|
result.update(extra_info)
|
||||||
|
ie_result = {
|
||||||
|
'_type': 'compat_list',
|
||||||
|
'entries': ie_result,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
ie_result.update(extra_info)
|
||||||
|
if 'extractor' not in ie_result:
|
||||||
|
ie_result['extractor'] = ie.IE_NAME
|
||||||
|
return self.process_ie_result(ie_result, download=download)
|
||||||
|
except ExtractorError as de: # An error we somewhat expected
|
||||||
|
self.report_error(compat_str(de), de.format_traceback())
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
if self.params.get('ignoreerrors', False):
|
||||||
|
self.report_error(compat_str(e), tb=compat_str(traceback.format_exc()))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.report_error(u'no suitable InfoExtractor: %s' % url)
|
||||||
|
|
||||||
|
def process_ie_result(self, ie_result, download=True, extra_info={}):
|
||||||
|
"""
|
||||||
|
Take the result of the ie(may be modified) and resolve all unresolved
|
||||||
|
references (URLs, playlist items).
|
||||||
|
|
||||||
|
It will also download the videos if 'download'.
|
||||||
|
Returns the resolved ie_result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
result_type = ie_result.get('_type', 'video') # If not given we suppose it's a video, support the default old system
|
||||||
|
if result_type == 'video':
|
||||||
|
if 'playlist' not in ie_result:
|
||||||
|
# It isn't part of a playlist
|
||||||
|
ie_result['playlist'] = None
|
||||||
|
ie_result['playlist_index'] = None
|
||||||
|
if download:
|
||||||
|
self.process_info(ie_result)
|
||||||
|
return ie_result
|
||||||
|
elif result_type == 'url':
|
||||||
|
# We have to add extra_info to the results because it may be
|
||||||
|
# contained in a playlist
|
||||||
|
return self.extract_info(ie_result['url'],
|
||||||
|
download,
|
||||||
|
ie_key=ie_result.get('ie_key'),
|
||||||
|
extra_info=extra_info)
|
||||||
|
elif result_type == 'playlist':
|
||||||
|
# We process each entry in the playlist
|
||||||
|
playlist = ie_result.get('title', None) or ie_result.get('id', None)
|
||||||
|
self.to_screen(u'[download] Downloading playlist: %s' % playlist)
|
||||||
|
|
||||||
|
playlist_results = []
|
||||||
|
|
||||||
|
n_all_entries = len(ie_result['entries'])
|
||||||
|
playliststart = self.params.get('playliststart', 1) - 1
|
||||||
|
playlistend = self.params.get('playlistend', -1)
|
||||||
|
|
||||||
|
if playlistend == -1:
|
||||||
|
entries = ie_result['entries'][playliststart:]
|
||||||
|
else:
|
||||||
|
entries = ie_result['entries'][playliststart:playlistend]
|
||||||
|
|
||||||
|
n_entries = len(entries)
|
||||||
|
|
||||||
|
self.to_screen(u"[%s] playlist '%s': Collected %d video ids (downloading %d of them)" %
|
||||||
|
(ie_result['extractor'], playlist, n_all_entries, n_entries))
|
||||||
|
|
||||||
|
for i,entry in enumerate(entries,1):
|
||||||
|
self.to_screen(u'[download] Downloading video #%s of %s' %(i, n_entries))
|
||||||
|
extra = {
|
||||||
|
'playlist': playlist,
|
||||||
|
'playlist_index': i + playliststart,
|
||||||
|
}
|
||||||
|
entry_result = self.process_ie_result(entry,
|
||||||
|
download=download,
|
||||||
|
extra_info=extra)
|
||||||
|
playlist_results.append(entry_result)
|
||||||
|
ie_result['entries'] = playlist_results
|
||||||
|
return ie_result
|
||||||
|
elif result_type == 'compat_list':
|
||||||
|
def _fixup(r):
|
||||||
|
r.setdefault('extractor', ie_result['extractor'])
|
||||||
|
return r
|
||||||
|
ie_result['entries'] = [
|
||||||
|
self.process_ie_result(_fixup(r), download=download)
|
||||||
|
for r in ie_result['entries']
|
||||||
|
]
|
||||||
|
return ie_result
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid result type: %s' % result_type)
|
||||||
|
|
||||||
def process_info(self, info_dict):
|
def process_info(self, info_dict):
|
||||||
"""Process a single dictionary returned by an InfoExtractor."""
|
"""Process a single resolved IE result."""
|
||||||
|
|
||||||
|
assert info_dict.get('_type', 'video') == 'video'
|
||||||
|
#We increment the download the download count here to match the previous behaviour.
|
||||||
|
self.increment_downloads()
|
||||||
|
|
||||||
|
info_dict['fulltitle'] = info_dict['title']
|
||||||
|
if len(info_dict['title']) > 200:
|
||||||
|
info_dict['title'] = info_dict['title'][:197] + u'...'
|
||||||
|
|
||||||
# Keep for backwards compatibility
|
# Keep for backwards compatibility
|
||||||
info_dict['stitle'] = info_dict['title']
|
info_dict['stitle'] = info_dict['title']
|
||||||
@ -417,6 +589,8 @@ class FileDownloader(object):
|
|||||||
# Forced printings
|
# Forced printings
|
||||||
if self.params.get('forcetitle', False):
|
if self.params.get('forcetitle', False):
|
||||||
compat_print(info_dict['title'])
|
compat_print(info_dict['title'])
|
||||||
|
if self.params.get('forceid', False):
|
||||||
|
compat_print(info_dict['id'])
|
||||||
if self.params.get('forceurl', False):
|
if self.params.get('forceurl', False):
|
||||||
compat_print(info_dict['url'])
|
compat_print(info_dict['url'])
|
||||||
if self.params.get('forcethumbnail', False) and 'thumbnail' in info_dict:
|
if self.params.get('forcethumbnail', False) and 'thumbnail' in info_dict:
|
||||||
@ -437,10 +611,10 @@ class FileDownloader(object):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
dn = os.path.dirname(encodeFilename(filename))
|
dn = os.path.dirname(encodeFilename(filename))
|
||||||
if dn != '' and not os.path.exists(dn): # dn is already encoded
|
if dn != '' and not os.path.exists(dn):
|
||||||
os.makedirs(dn)
|
os.makedirs(dn)
|
||||||
except (OSError, IOError) as err:
|
except (OSError, IOError) as err:
|
||||||
self.trouble(u'ERROR: unable to create directory ' + compat_str(err))
|
self.report_error(u'unable to create directory ' + compat_str(err))
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writedescription', False):
|
if self.params.get('writedescription', False):
|
||||||
@ -450,20 +624,43 @@ class FileDownloader(object):
|
|||||||
with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
|
with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
|
||||||
descfile.write(info_dict['description'])
|
descfile.write(info_dict['description'])
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
self.trouble(u'ERROR: Cannot write description file ' + descfn)
|
self.report_error(u'Cannot write description file ' + descfn)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.params.get('writesubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']:
|
if self.params.get('writesubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']:
|
||||||
# subtitles download errors are already managed as troubles in relevant IE
|
# subtitles download errors are already managed as troubles in relevant IE
|
||||||
# that way it will silently go on when used with unsupporting IE
|
# that way it will silently go on when used with unsupporting IE
|
||||||
try:
|
subtitle = info_dict['subtitles'][0]
|
||||||
srtfn = filename.rsplit('.', 1)[0] + u'.srt'
|
(sub_error, sub_lang, sub) = subtitle
|
||||||
self.report_writesubtitles(srtfn)
|
sub_format = self.params.get('subtitlesformat')
|
||||||
with io.open(encodeFilename(srtfn), 'w', encoding='utf-8') as srtfile:
|
if sub_error:
|
||||||
srtfile.write(info_dict['subtitles'])
|
self.report_warning("Some error while getting the subtitles")
|
||||||
except (OSError, IOError):
|
else:
|
||||||
self.trouble(u'ERROR: Cannot write subtitles file ' + descfn)
|
try:
|
||||||
return
|
sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format
|
||||||
|
self.report_writesubtitles(sub_filename)
|
||||||
|
with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
|
||||||
|
subfile.write(sub)
|
||||||
|
except (OSError, IOError):
|
||||||
|
self.report_error(u'Cannot write subtitles file ' + descfn)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.params.get('allsubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']:
|
||||||
|
subtitles = info_dict['subtitles']
|
||||||
|
sub_format = self.params.get('subtitlesformat')
|
||||||
|
for subtitle in subtitles:
|
||||||
|
(sub_error, sub_lang, sub) = subtitle
|
||||||
|
if sub_error:
|
||||||
|
self.report_warning("Some error while getting the subtitles")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format
|
||||||
|
self.report_writesubtitles(sub_filename)
|
||||||
|
with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
|
||||||
|
subfile.write(sub)
|
||||||
|
except (OSError, IOError):
|
||||||
|
self.report_error(u'Cannot write subtitles file ' + descfn)
|
||||||
|
return
|
||||||
|
|
||||||
if self.params.get('writeinfojson', False):
|
if self.params.get('writeinfojson', False):
|
||||||
infofn = filename + u'.info.json'
|
infofn = filename + u'.info.json'
|
||||||
@ -472,9 +669,23 @@ class FileDownloader(object):
|
|||||||
json_info_dict = dict((k, v) for k,v in info_dict.items() if not k in ['urlhandle'])
|
json_info_dict = dict((k, v) for k,v in info_dict.items() if not k in ['urlhandle'])
|
||||||
write_json_file(json_info_dict, encodeFilename(infofn))
|
write_json_file(json_info_dict, encodeFilename(infofn))
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
self.trouble(u'ERROR: Cannot write metadata to JSON file ' + infofn)
|
self.report_error(u'Cannot write metadata to JSON file ' + infofn)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.params.get('writethumbnail', False):
|
||||||
|
if 'thumbnail' in info_dict:
|
||||||
|
thumb_format = info_dict['thumbnail'].rpartition(u'/')[2].rpartition(u'.')[2]
|
||||||
|
if not thumb_format:
|
||||||
|
thumb_format = 'jpg'
|
||||||
|
thumb_filename = filename.rpartition('.')[0] + u'.' + thumb_format
|
||||||
|
self.to_screen(u'[%s] %s: Downloading thumbnail ...' %
|
||||||
|
(info_dict['extractor'], info_dict['id']))
|
||||||
|
uf = compat_urllib_request.urlopen(info_dict['thumbnail'])
|
||||||
|
with open(thumb_filename, 'wb') as thumbf:
|
||||||
|
shutil.copyfileobj(uf, thumbf)
|
||||||
|
self.to_screen(u'[%s] %s: Writing thumbnail to: %s' %
|
||||||
|
(info_dict['extractor'], info_dict['id'], thumb_filename))
|
||||||
|
|
||||||
if not self.params.get('skip_download', False):
|
if not self.params.get('skip_download', False):
|
||||||
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
|
if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
|
||||||
success = True
|
success = True
|
||||||
@ -484,17 +695,17 @@ class FileDownloader(object):
|
|||||||
except (OSError, IOError) as err:
|
except (OSError, IOError) as err:
|
||||||
raise UnavailableVideoError()
|
raise UnavailableVideoError()
|
||||||
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:
|
||||||
self.trouble(u'ERROR: unable to download video data: %s' % str(err))
|
self.report_error(u'unable to download video data: %s' % str(err))
|
||||||
return
|
return
|
||||||
except (ContentTooShortError, ) as err:
|
except (ContentTooShortError, ) as err:
|
||||||
self.trouble(u'ERROR: content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
|
self.report_error(u'content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
|
||||||
return
|
return
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
try:
|
try:
|
||||||
self.post_process(filename, info_dict)
|
self.post_process(filename, info_dict)
|
||||||
except (PostProcessingError) as err:
|
except (PostProcessingError) as err:
|
||||||
self.trouble(u'ERROR: postprocessing: %s' % str(err))
|
self.report_error(u'postprocessing: %s' % str(err))
|
||||||
return
|
return
|
||||||
|
|
||||||
def download(self, url_list):
|
def download(self, url_list):
|
||||||
@ -503,49 +714,14 @@ class FileDownloader(object):
|
|||||||
raise SameFileError(self.params['outtmpl'])
|
raise SameFileError(self.params['outtmpl'])
|
||||||
|
|
||||||
for url in url_list:
|
for url in url_list:
|
||||||
suitable_found = False
|
try:
|
||||||
for ie in self._ies:
|
#It also downloads the videos
|
||||||
# Go to next InfoExtractor if not suitable
|
videos = self.extract_info(url)
|
||||||
if not ie.suitable(url):
|
except UnavailableVideoError:
|
||||||
continue
|
self.report_error(u'unable to download video')
|
||||||
|
except MaxDownloadsReached:
|
||||||
# Warn if the _WORKING attribute is False
|
self.to_screen(u'[info] Maximum number of downloaded files reached.')
|
||||||
if not ie.working():
|
raise
|
||||||
self.report_warning(u'the program functionality for this site has been marked as broken, '
|
|
||||||
u'and will probably not work. If you want to go on, use the -i option.')
|
|
||||||
|
|
||||||
# Suitable InfoExtractor found
|
|
||||||
suitable_found = True
|
|
||||||
|
|
||||||
# Extract information from URL and process it
|
|
||||||
try:
|
|
||||||
videos = ie.extract(url)
|
|
||||||
except ExtractorError as de: # An error we somewhat expected
|
|
||||||
self.trouble(u'ERROR: ' + compat_str(de), de.format_traceback())
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
if self.params.get('ignoreerrors', False):
|
|
||||||
self.trouble(u'ERROR: ' + compat_str(e), tb=compat_str(traceback.format_exc()))
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
if len(videos or []) > 1 and self.fixed_template():
|
|
||||||
raise SameFileError(self.params['outtmpl'])
|
|
||||||
|
|
||||||
for video in videos or []:
|
|
||||||
video['extractor'] = ie.IE_NAME
|
|
||||||
try:
|
|
||||||
self.increment_downloads()
|
|
||||||
self.process_info(video)
|
|
||||||
except UnavailableVideoError:
|
|
||||||
self.trouble(u'\nERROR: unable to download video')
|
|
||||||
|
|
||||||
# Suitable InfoExtractor had been found; go to next URL
|
|
||||||
break
|
|
||||||
|
|
||||||
if not suitable_found:
|
|
||||||
self.trouble(u'ERROR: no suitable InfoExtractor: %s' % url)
|
|
||||||
|
|
||||||
return self._download_retcode
|
return self._download_retcode
|
||||||
|
|
||||||
@ -572,7 +748,7 @@ class FileDownloader(object):
|
|||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
self.report_warning(u'Unable to remove downloaded video file')
|
self.report_warning(u'Unable to remove downloaded video file')
|
||||||
|
|
||||||
def _download_with_rtmpdump(self, filename, url, player_url, page_url):
|
def _download_with_rtmpdump(self, filename, url, player_url, page_url, play_path, tc_url):
|
||||||
self.report_destination(filename)
|
self.report_destination(filename)
|
||||||
tmpfilename = self.temp_name(filename)
|
tmpfilename = self.temp_name(filename)
|
||||||
|
|
||||||
@ -580,17 +756,22 @@ class FileDownloader(object):
|
|||||||
try:
|
try:
|
||||||
subprocess.call(['rtmpdump', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
|
subprocess.call(['rtmpdump', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
|
||||||
except (OSError, IOError):
|
except (OSError, IOError):
|
||||||
self.trouble(u'ERROR: RTMP download detected but "rtmpdump" could not be run')
|
self.report_error(u'RTMP download detected but "rtmpdump" could not be run')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Download using rtmpdump. rtmpdump returns exit code 2 when
|
# Download using rtmpdump. rtmpdump returns exit code 2 when
|
||||||
# the connection was interrumpted and resuming appears to be
|
# the connection was interrumpted and resuming appears to be
|
||||||
# possible. This is part of rtmpdump's normal usage, AFAIK.
|
# possible. This is part of rtmpdump's normal usage, AFAIK.
|
||||||
basic_args = ['rtmpdump', '-q', '-r', url, '-o', tmpfilename]
|
basic_args = ['rtmpdump', '-q', '-r', url, '-o', tmpfilename]
|
||||||
|
if self.params.get('verbose', False): basic_args[1] = '-v'
|
||||||
if player_url is not None:
|
if player_url is not None:
|
||||||
basic_args += ['-W', player_url]
|
basic_args += ['-W', player_url]
|
||||||
if page_url is not None:
|
if page_url is not None:
|
||||||
basic_args += ['--pageUrl', page_url]
|
basic_args += ['--pageUrl', page_url]
|
||||||
|
if play_path is not None:
|
||||||
|
basic_args += ['-y', play_path]
|
||||||
|
if tc_url is not None:
|
||||||
|
basic_args += ['--tcUrl', url]
|
||||||
args = basic_args + [[], ['-e', '-k', '1']][self.params.get('continuedl', False)]
|
args = basic_args + [[], ['-e', '-k', '1']][self.params.get('continuedl', False)]
|
||||||
if self.params.get('verbose', False):
|
if self.params.get('verbose', False):
|
||||||
try:
|
try:
|
||||||
@ -625,7 +806,8 @@ class FileDownloader(object):
|
|||||||
})
|
})
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.trouble(u'\nERROR: rtmpdump exited with code %d' % retval)
|
self.to_stderr(u"\n")
|
||||||
|
self.report_error(u'rtmpdump exited with code %d' % retval)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Keep a copy of the main cookiejar to pass to external downloaders
|
# Keep a copy of the main cookiejar to pass to external downloaders
|
||||||
@ -701,7 +883,9 @@ class FileDownloader(object):
|
|||||||
if url.startswith('rtmp'):
|
if url.startswith('rtmp'):
|
||||||
return self._download_with_rtmpdump(filename, url,
|
return self._download_with_rtmpdump(filename, url,
|
||||||
info_dict.get('player_url', None),
|
info_dict.get('player_url', None),
|
||||||
info_dict.get('page_url', None))
|
info_dict.get('page_url', None),
|
||||||
|
info_dict.get('play_path', None),
|
||||||
|
info_dict.get('tc_url', None))
|
||||||
|
|
||||||
tmpfilename = self.temp_name(filename)
|
tmpfilename = self.temp_name(filename)
|
||||||
stream = None
|
stream = None
|
||||||
@ -784,7 +968,7 @@ class FileDownloader(object):
|
|||||||
self.report_retry(count, retries)
|
self.report_retry(count, retries)
|
||||||
|
|
||||||
if count > retries:
|
if count > retries:
|
||||||
self.trouble(u'ERROR: giving up after %s retries' % retries)
|
self.report_error(u'giving up after %s retries' % retries)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
data_len = data.info().get('Content-length', None)
|
data_len = data.info().get('Content-length', None)
|
||||||
@ -820,12 +1004,13 @@ class FileDownloader(object):
|
|||||||
filename = self.undo_temp_name(tmpfilename)
|
filename = self.undo_temp_name(tmpfilename)
|
||||||
self.report_destination(filename)
|
self.report_destination(filename)
|
||||||
except (OSError, IOError) as err:
|
except (OSError, IOError) as err:
|
||||||
self.trouble(u'ERROR: unable to open for writing: %s' % str(err))
|
self.report_error(u'unable to open for writing: %s' % str(err))
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
stream.write(data_block)
|
stream.write(data_block)
|
||||||
except (IOError, OSError) as err:
|
except (IOError, OSError) as err:
|
||||||
self.trouble(u'\nERROR: unable to write data: %s' % str(err))
|
self.to_stderr(u"\n")
|
||||||
|
self.report_error(u'unable to write data: %s' % str(err))
|
||||||
return False
|
return False
|
||||||
if not self.params.get('noresizebuffer', False):
|
if not self.params.get('noresizebuffer', False):
|
||||||
block_size = self.best_block_size(after - before, len(data_block))
|
block_size = self.best_block_size(after - before, len(data_block))
|
||||||
@ -851,7 +1036,8 @@ class FileDownloader(object):
|
|||||||
self.slow_down(start, byte_counter - resume_len)
|
self.slow_down(start, byte_counter - resume_len)
|
||||||
|
|
||||||
if stream is None:
|
if stream is None:
|
||||||
self.trouble(u'\nERROR: Did not get any data blocks')
|
self.to_stderr(u"\n")
|
||||||
|
self.report_error(u'Did not get any data blocks')
|
||||||
return False
|
return False
|
||||||
stream.close()
|
stream.close()
|
||||||
self.report_finish()
|
self.report_finish()
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -85,8 +85,9 @@ class FFmpegPostProcessor(PostProcessor):
|
|||||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
stdout,stderr = p.communicate()
|
stdout,stderr = p.communicate()
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
|
stderr = stderr.decode('utf-8', 'replace')
|
||||||
msg = stderr.strip().split('\n')[-1]
|
msg = stderr.strip().split('\n')[-1]
|
||||||
raise FFmpegPostProcessorError(msg.decode('utf-8', 'replace'))
|
raise FFmpegPostProcessorError(msg)
|
||||||
|
|
||||||
def _ffmpeg_filename_argument(self, fn):
|
def _ffmpeg_filename_argument(self, fn):
|
||||||
# ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
|
# ffmpeg broke --, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details
|
||||||
@ -188,6 +189,11 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
|||||||
|
|
||||||
prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
|
prefix, sep, ext = path.rpartition(u'.') # not os.path.splitext, since the latter does not work on unicode in all setups
|
||||||
new_path = prefix + sep + extension
|
new_path = prefix + sep + extension
|
||||||
|
|
||||||
|
# If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
|
||||||
|
if new_path == path:
|
||||||
|
self._nopostoverwrites = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
|
if self._nopostoverwrites and os.path.exists(encodeFilename(new_path)):
|
||||||
self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
|
self._downloader.to_screen(u'[youtube] Post-process file %s exists, skipping' % new_path)
|
||||||
@ -210,7 +216,7 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
|||||||
self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
|
self._downloader.to_stderr(u'WARNING: Cannot update utime of audio file')
|
||||||
|
|
||||||
information['filepath'] = new_path
|
information['filepath'] = new_path
|
||||||
return False,information
|
return self._nopostoverwrites,information
|
||||||
|
|
||||||
class FFmpegVideoConvertor(FFmpegPostProcessor):
|
class FFmpegVideoConvertor(FFmpegPostProcessor):
|
||||||
def __init__(self, downloader=None,preferedformat=None):
|
def __init__(self, downloader=None,preferedformat=None):
|
||||||
|
@ -24,10 +24,15 @@ __authors__ = (
|
|||||||
'Jaime Marquínez Ferrándiz',
|
'Jaime Marquínez Ferrándiz',
|
||||||
'Jeff Crouse',
|
'Jeff Crouse',
|
||||||
'Osama Khalid',
|
'Osama Khalid',
|
||||||
|
'Michael Walter',
|
||||||
|
'M. Yasoob Ullah Khalid',
|
||||||
|
'Julien Fraichard',
|
||||||
|
'Johny Mo Swag',
|
||||||
)
|
)
|
||||||
|
|
||||||
__license__ = 'Public Domain'
|
__license__ = 'Public Domain'
|
||||||
|
|
||||||
|
import codecs
|
||||||
import getpass
|
import getpass
|
||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
@ -46,7 +51,7 @@ from .FileDownloader import *
|
|||||||
from .InfoExtractors import gen_extractors
|
from .InfoExtractors import gen_extractors
|
||||||
from .PostProcessor import *
|
from .PostProcessor import *
|
||||||
|
|
||||||
def parseOpts():
|
def parseOpts(overrideArguments=None):
|
||||||
def _readOptions(filename_bytes):
|
def _readOptions(filename_bytes):
|
||||||
try:
|
try:
|
||||||
optionf = open(filename_bytes)
|
optionf = open(filename_bytes)
|
||||||
@ -139,9 +144,14 @@ def parseOpts():
|
|||||||
help='display the current browser identification', default=False)
|
help='display the current browser identification', default=False)
|
||||||
general.add_option('--user-agent',
|
general.add_option('--user-agent',
|
||||||
dest='user_agent', help='specify a custom user agent', metavar='UA')
|
dest='user_agent', help='specify a custom user agent', metavar='UA')
|
||||||
|
general.add_option('--referer',
|
||||||
|
dest='referer', help='specify a custom referer, use if the video access is restricted to one domain',
|
||||||
|
metavar='REF', default=None)
|
||||||
general.add_option('--list-extractors',
|
general.add_option('--list-extractors',
|
||||||
action='store_true', dest='list_extractors',
|
action='store_true', dest='list_extractors',
|
||||||
help='List all supported extractors and the URLs they would handle', default=False)
|
help='List all supported extractors and the URLs they would handle', default=False)
|
||||||
|
general.add_option('--proxy', dest='proxy', default=None, help='Use the specified HTTP/HTTPS proxy', metavar='URL')
|
||||||
|
general.add_option('--no-check-certificate', action='store_true', dest='no_check_certificate', default=False, help='Suppress HTTPS certificate validation.')
|
||||||
general.add_option('--test', action='store_true', dest='test', default=False, help=optparse.SUPPRESS_HELP)
|
general.add_option('--test', action='store_true', dest='test', default=False, help=optparse.SUPPRESS_HELP)
|
||||||
|
|
||||||
selection.add_option('--playlist-start',
|
selection.add_option('--playlist-start',
|
||||||
@ -153,6 +163,9 @@ def parseOpts():
|
|||||||
selection.add_option('--max-downloads', metavar='NUMBER', dest='max_downloads', help='Abort after downloading NUMBER files', default=None)
|
selection.add_option('--max-downloads', metavar='NUMBER', dest='max_downloads', help='Abort after downloading NUMBER files', default=None)
|
||||||
selection.add_option('--min-filesize', metavar='SIZE', dest='min_filesize', help="Do not download any videos smaller than SIZE (e.g. 50k or 44.6m)", default=None)
|
selection.add_option('--min-filesize', metavar='SIZE', dest='min_filesize', help="Do not download any videos smaller than SIZE (e.g. 50k or 44.6m)", default=None)
|
||||||
selection.add_option('--max-filesize', metavar='SIZE', dest='max_filesize', help="Do not download any videos larger than SIZE (e.g. 50k or 44.6m)", default=None)
|
selection.add_option('--max-filesize', metavar='SIZE', dest='max_filesize', help="Do not download any videos larger than SIZE (e.g. 50k or 44.6m)", default=None)
|
||||||
|
selection.add_option('--date', metavar='DATE', dest='date', help='download only videos uploaded in this date', default=None)
|
||||||
|
selection.add_option('--datebefore', metavar='DATE', dest='datebefore', help='download only videos uploaded before this date', default=None)
|
||||||
|
selection.add_option('--dateafter', metavar='DATE', dest='dateafter', help='download only videos uploaded after this date', default=None)
|
||||||
|
|
||||||
|
|
||||||
authentication.add_option('-u', '--username',
|
authentication.add_option('-u', '--username',
|
||||||
@ -164,7 +177,8 @@ def parseOpts():
|
|||||||
|
|
||||||
|
|
||||||
video_format.add_option('-f', '--format',
|
video_format.add_option('-f', '--format',
|
||||||
action='store', dest='format', metavar='FORMAT', help='video format code')
|
action='store', dest='format', metavar='FORMAT',
|
||||||
|
help='video format code, specifiy the order of preference using slashes: "-f 22/17/18"')
|
||||||
video_format.add_option('--all-formats',
|
video_format.add_option('--all-formats',
|
||||||
action='store_const', dest='format', help='download all available video formats', const='all')
|
action='store_const', dest='format', help='download all available video formats', const='all')
|
||||||
video_format.add_option('--prefer-free-formats',
|
video_format.add_option('--prefer-free-formats',
|
||||||
@ -173,12 +187,24 @@ def parseOpts():
|
|||||||
action='store', dest='format_limit', metavar='FORMAT', help='highest quality format to download')
|
action='store', dest='format_limit', metavar='FORMAT', help='highest quality format to download')
|
||||||
video_format.add_option('-F', '--list-formats',
|
video_format.add_option('-F', '--list-formats',
|
||||||
action='store_true', dest='listformats', help='list all available formats (currently youtube only)')
|
action='store_true', dest='listformats', help='list all available formats (currently youtube only)')
|
||||||
video_format.add_option('--write-srt',
|
video_format.add_option('--write-sub', '--write-srt',
|
||||||
action='store_true', dest='writesubtitles',
|
action='store_true', dest='writesubtitles',
|
||||||
help='write video closed captions to a .srt file (currently youtube only)', default=False)
|
help='write subtitle file (currently youtube only)', default=False)
|
||||||
video_format.add_option('--srt-lang',
|
video_format.add_option('--only-sub',
|
||||||
|
action='store_true', dest='skip_download',
|
||||||
|
help='[deprecated] alias of --skip-download', default=False)
|
||||||
|
video_format.add_option('--all-subs',
|
||||||
|
action='store_true', dest='allsubtitles',
|
||||||
|
help='downloads all the available subtitles of the video (currently youtube only)', default=False)
|
||||||
|
video_format.add_option('--list-subs',
|
||||||
|
action='store_true', dest='listsubtitles',
|
||||||
|
help='lists all available subtitles for the video (currently youtube only)', default=False)
|
||||||
|
video_format.add_option('--sub-format',
|
||||||
|
action='store', dest='subtitlesformat', metavar='LANG',
|
||||||
|
help='subtitle format [srt/sbv] (default=srt) (currently youtube only)', default='srt')
|
||||||
|
video_format.add_option('--sub-lang', '--srt-lang',
|
||||||
action='store', dest='subtitleslang', metavar='LANG',
|
action='store', dest='subtitleslang', metavar='LANG',
|
||||||
help='language of the closed captions to download (optional) use IETF language tags like \'en\'')
|
help='language of the subtitles to download (optional) use IETF language tags like \'en\'')
|
||||||
|
|
||||||
verbosity.add_option('-q', '--quiet',
|
verbosity.add_option('-q', '--quiet',
|
||||||
action='store_true', dest='quiet', help='activates quiet mode', default=False)
|
action='store_true', dest='quiet', help='activates quiet mode', default=False)
|
||||||
@ -190,6 +216,8 @@ def parseOpts():
|
|||||||
action='store_true', dest='geturl', help='simulate, quiet but print URL', default=False)
|
action='store_true', dest='geturl', help='simulate, quiet but print URL', default=False)
|
||||||
verbosity.add_option('-e', '--get-title',
|
verbosity.add_option('-e', '--get-title',
|
||||||
action='store_true', dest='gettitle', help='simulate, quiet but print title', default=False)
|
action='store_true', dest='gettitle', help='simulate, quiet but print title', default=False)
|
||||||
|
verbosity.add_option('--get-id',
|
||||||
|
action='store_true', dest='getid', help='simulate, quiet but print id', default=False)
|
||||||
verbosity.add_option('--get-thumbnail',
|
verbosity.add_option('--get-thumbnail',
|
||||||
action='store_true', dest='getthumbnail',
|
action='store_true', dest='getthumbnail',
|
||||||
help='simulate, quiet but print thumbnail URL', default=False)
|
help='simulate, quiet but print thumbnail URL', default=False)
|
||||||
@ -211,18 +239,33 @@ def parseOpts():
|
|||||||
help='display progress in console titlebar', default=False)
|
help='display progress in console titlebar', default=False)
|
||||||
verbosity.add_option('-v', '--verbose',
|
verbosity.add_option('-v', '--verbose',
|
||||||
action='store_true', dest='verbose', help='print various debugging information', default=False)
|
action='store_true', dest='verbose', help='print various debugging information', default=False)
|
||||||
|
verbosity.add_option('--dump-intermediate-pages',
|
||||||
|
action='store_true', dest='dump_intermediate_pages', default=False,
|
||||||
|
help='print downloaded pages to debug problems(very verbose)')
|
||||||
|
|
||||||
filesystem.add_option('-t', '--title',
|
filesystem.add_option('-t', '--title',
|
||||||
action='store_true', dest='usetitle', help='use title in file name', default=False)
|
action='store_true', dest='usetitle', help='use title in file name (default)', default=False)
|
||||||
filesystem.add_option('--id',
|
filesystem.add_option('--id',
|
||||||
action='store_true', dest='useid', help='use video ID in file name', default=False)
|
action='store_true', dest='useid', help='use only video ID in file name', default=False)
|
||||||
filesystem.add_option('-l', '--literal',
|
filesystem.add_option('-l', '--literal',
|
||||||
action='store_true', dest='usetitle', help='[deprecated] alias of --title', default=False)
|
action='store_true', dest='usetitle', help='[deprecated] alias of --title', default=False)
|
||||||
filesystem.add_option('-A', '--auto-number',
|
filesystem.add_option('-A', '--auto-number',
|
||||||
action='store_true', dest='autonumber',
|
action='store_true', dest='autonumber',
|
||||||
help='number downloaded files starting from 00000', default=False)
|
help='number downloaded files starting from 00000', default=False)
|
||||||
filesystem.add_option('-o', '--output',
|
filesystem.add_option('-o', '--output',
|
||||||
dest='outtmpl', metavar='TEMPLATE', help='output filename template. Use %(title)s to get the title, %(uploader)s for the uploader name, %(uploader_id)s for the uploader nickname if different, %(autonumber)s to get an automatically incremented number, %(ext)s for the filename extension, %(upload_date)s for the upload date (YYYYMMDD), %(extractor)s for the provider (youtube, metacafe, etc), %(id)s for the video id and %% for a literal percent. Use - to output to stdout. Can also be used to download to a different directory, for example with -o \'/my/downloads/%(uploader)s/%(title)s-%(id)s.%(ext)s\' .')
|
dest='outtmpl', metavar='TEMPLATE',
|
||||||
|
help=('output filename template. Use %(title)s to get the title, '
|
||||||
|
'%(uploader)s for the uploader name, %(uploader_id)s for the uploader nickname if different, '
|
||||||
|
'%(autonumber)s to get an automatically incremented number, '
|
||||||
|
'%(ext)s for the filename extension, %(upload_date)s for the upload date (YYYYMMDD), '
|
||||||
|
'%(extractor)s for the provider (youtube, metacafe, etc), '
|
||||||
|
'%(id)s for the video id , %(playlist)s for the playlist the video is in, '
|
||||||
|
'%(playlist_index)s for the position in the playlist and %% for a literal percent. '
|
||||||
|
'Use - to output to stdout. Can also be used to download to a different directory, '
|
||||||
|
'for example with -o \'/my/downloads/%(uploader)s/%(title)s-%(id)s.%(ext)s\' .'))
|
||||||
|
filesystem.add_option('--autonumber-size',
|
||||||
|
dest='autonumber_size', metavar='NUMBER',
|
||||||
|
help='Specifies the number of digits in %(autonumber)s when it is present in output filename template or --autonumber option is given')
|
||||||
filesystem.add_option('--restrict-filenames',
|
filesystem.add_option('--restrict-filenames',
|
||||||
action='store_true', dest='restrictfilenames',
|
action='store_true', dest='restrictfilenames',
|
||||||
help='Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames', default=False)
|
help='Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames', default=False)
|
||||||
@ -248,6 +291,9 @@ def parseOpts():
|
|||||||
filesystem.add_option('--write-info-json',
|
filesystem.add_option('--write-info-json',
|
||||||
action='store_true', dest='writeinfojson',
|
action='store_true', dest='writeinfojson',
|
||||||
help='write video metadata to a .info.json file', default=False)
|
help='write video metadata to a .info.json file', default=False)
|
||||||
|
filesystem.add_option('--write-thumbnail',
|
||||||
|
action='store_true', dest='writethumbnail',
|
||||||
|
help='write thumbnail image to disk', default=False)
|
||||||
|
|
||||||
|
|
||||||
postproc.add_option('-x', '--extract-audio', action='store_true', dest='extractaudio', default=False,
|
postproc.add_option('-x', '--extract-audio', action='store_true', dest='extractaudio', default=False,
|
||||||
@ -272,29 +318,38 @@ def parseOpts():
|
|||||||
parser.add_option_group(authentication)
|
parser.add_option_group(authentication)
|
||||||
parser.add_option_group(postproc)
|
parser.add_option_group(postproc)
|
||||||
|
|
||||||
xdg_config_home = os.environ.get('XDG_CONFIG_HOME')
|
if overrideArguments is not None:
|
||||||
if xdg_config_home:
|
opts, args = parser.parse_args(overrideArguments)
|
||||||
userConfFile = os.path.join(xdg_config_home, 'youtube-dl.conf')
|
if opts.verbose:
|
||||||
|
print(u'[debug] Override config: ' + repr(overrideArguments))
|
||||||
else:
|
else:
|
||||||
userConfFile = os.path.join(os.path.expanduser('~'), '.config', 'youtube-dl.conf')
|
xdg_config_home = os.environ.get('XDG_CONFIG_HOME')
|
||||||
systemConf = _readOptions('/etc/youtube-dl.conf')
|
if xdg_config_home:
|
||||||
userConf = _readOptions(userConfFile)
|
userConfFile = os.path.join(xdg_config_home, 'youtube-dl.conf')
|
||||||
commandLineConf = sys.argv[1:]
|
else:
|
||||||
argv = systemConf + userConf + commandLineConf
|
userConfFile = os.path.join(os.path.expanduser('~'), '.config', 'youtube-dl.conf')
|
||||||
opts, args = parser.parse_args(argv)
|
systemConf = _readOptions('/etc/youtube-dl.conf')
|
||||||
|
userConf = _readOptions(userConfFile)
|
||||||
if opts.verbose:
|
commandLineConf = sys.argv[1:]
|
||||||
print(u'[debug] System config: ' + repr(systemConf))
|
argv = systemConf + userConf + commandLineConf
|
||||||
print(u'[debug] User config: ' + repr(userConf))
|
opts, args = parser.parse_args(argv)
|
||||||
print(u'[debug] Command-line args: ' + repr(commandLineConf))
|
if opts.verbose:
|
||||||
|
print(u'[debug] System config: ' + repr(systemConf))
|
||||||
|
print(u'[debug] User config: ' + repr(userConf))
|
||||||
|
print(u'[debug] Command-line args: ' + repr(commandLineConf))
|
||||||
|
|
||||||
return parser, opts, args
|
return parser, opts, args
|
||||||
|
|
||||||
|
|
||||||
jar = None
|
jar = None
|
||||||
|
|
||||||
def _real_main():
|
def _real_main(argv=None):
|
||||||
parser, opts, args = parseOpts()
|
# Compatibility fixes for Windows
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
# https://github.com/rg3/youtube-dl/issues/820
|
||||||
|
codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)
|
||||||
|
|
||||||
|
parser, opts, args = parseOpts(argv)
|
||||||
|
|
||||||
global jar
|
global jar
|
||||||
# Open appropriate CookieJar
|
# Open appropriate CookieJar
|
||||||
@ -313,6 +368,10 @@ def _real_main():
|
|||||||
# Set user agent
|
# Set user agent
|
||||||
if opts.user_agent is not None:
|
if opts.user_agent is not None:
|
||||||
std_headers['User-Agent'] = opts.user_agent
|
std_headers['User-Agent'] = opts.user_agent
|
||||||
|
|
||||||
|
# Set referer
|
||||||
|
if opts.referer is not None:
|
||||||
|
std_headers['Referer'] = opts.referer
|
||||||
|
|
||||||
# Dump user agent
|
# Dump user agent
|
||||||
if opts.dump_user_agent:
|
if opts.dump_user_agent:
|
||||||
@ -337,8 +396,16 @@ def _real_main():
|
|||||||
|
|
||||||
# General configuration
|
# General configuration
|
||||||
cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar)
|
cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar)
|
||||||
proxy_handler = compat_urllib_request.ProxyHandler()
|
if opts.proxy:
|
||||||
opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler())
|
proxies = {'http': opts.proxy, 'https': opts.proxy}
|
||||||
|
else:
|
||||||
|
proxies = compat_urllib_request.getproxies()
|
||||||
|
# Set HTTPS proxy to HTTP one if given (https://github.com/rg3/youtube-dl/issues/805)
|
||||||
|
if 'http' in proxies and 'https' not in proxies:
|
||||||
|
proxies['https'] = proxies['http']
|
||||||
|
proxy_handler = compat_urllib_request.ProxyHandler(proxies)
|
||||||
|
https_handler = make_HTTPS_handler(opts)
|
||||||
|
opener = compat_urllib_request.build_opener(https_handler, proxy_handler, cookie_processor, YoutubeDLHandler())
|
||||||
compat_urllib_request.install_opener(opener)
|
compat_urllib_request.install_opener(opener)
|
||||||
socket.setdefaulttimeout(300) # 5 minutes should be enough (famous last words)
|
socket.setdefaulttimeout(300) # 5 minutes should be enough (famous last words)
|
||||||
|
|
||||||
@ -411,6 +478,10 @@ def _real_main():
|
|||||||
if opts.recodevideo is not None:
|
if opts.recodevideo is not None:
|
||||||
if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg']:
|
if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg']:
|
||||||
parser.error(u'invalid video recode format specified')
|
parser.error(u'invalid video recode format specified')
|
||||||
|
if opts.date is not None:
|
||||||
|
date = DateRange.day(opts.date)
|
||||||
|
else:
|
||||||
|
date = DateRange(opts.dateafter, opts.datebefore)
|
||||||
|
|
||||||
if sys.version_info < (3,):
|
if sys.version_info < (3,):
|
||||||
# In Python 2, sys.argv is a bytestring (also note http://bugs.python.org/issue2128 for Windows systems)
|
# In Python 2, sys.argv is a bytestring (also note http://bugs.python.org/issue2128 for Windows systems)
|
||||||
@ -423,26 +494,28 @@ def _real_main():
|
|||||||
or (opts.usetitle and u'%(title)s-%(id)s.%(ext)s')
|
or (opts.usetitle and u'%(title)s-%(id)s.%(ext)s')
|
||||||
or (opts.useid and u'%(id)s.%(ext)s')
|
or (opts.useid and u'%(id)s.%(ext)s')
|
||||||
or (opts.autonumber and u'%(autonumber)s-%(id)s.%(ext)s')
|
or (opts.autonumber and u'%(autonumber)s-%(id)s.%(ext)s')
|
||||||
or u'%(id)s.%(ext)s')
|
or u'%(title)s-%(id)s.%(ext)s')
|
||||||
|
|
||||||
# File downloader
|
# File downloader
|
||||||
fd = FileDownloader({
|
fd = FileDownloader({
|
||||||
'usenetrc': opts.usenetrc,
|
'usenetrc': opts.usenetrc,
|
||||||
'username': opts.username,
|
'username': opts.username,
|
||||||
'password': opts.password,
|
'password': opts.password,
|
||||||
'quiet': (opts.quiet or opts.geturl or opts.gettitle or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
|
'quiet': (opts.quiet or opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
|
||||||
'forceurl': opts.geturl,
|
'forceurl': opts.geturl,
|
||||||
'forcetitle': opts.gettitle,
|
'forcetitle': opts.gettitle,
|
||||||
|
'forceid': opts.getid,
|
||||||
'forcethumbnail': opts.getthumbnail,
|
'forcethumbnail': opts.getthumbnail,
|
||||||
'forcedescription': opts.getdescription,
|
'forcedescription': opts.getdescription,
|
||||||
'forcefilename': opts.getfilename,
|
'forcefilename': opts.getfilename,
|
||||||
'forceformat': opts.getformat,
|
'forceformat': opts.getformat,
|
||||||
'simulate': opts.simulate,
|
'simulate': opts.simulate,
|
||||||
'skip_download': (opts.skip_download or opts.simulate or opts.geturl or opts.gettitle or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
|
'skip_download': (opts.skip_download or opts.simulate or opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat),
|
||||||
'format': opts.format,
|
'format': opts.format,
|
||||||
'format_limit': opts.format_limit,
|
'format_limit': opts.format_limit,
|
||||||
'listformats': opts.listformats,
|
'listformats': opts.listformats,
|
||||||
'outtmpl': outtmpl,
|
'outtmpl': outtmpl,
|
||||||
|
'autonumber_size': opts.autonumber_size,
|
||||||
'restrictfilenames': opts.restrictfilenames,
|
'restrictfilenames': opts.restrictfilenames,
|
||||||
'ignoreerrors': opts.ignoreerrors,
|
'ignoreerrors': opts.ignoreerrors,
|
||||||
'ratelimit': opts.ratelimit,
|
'ratelimit': opts.ratelimit,
|
||||||
@ -461,17 +534,23 @@ def _real_main():
|
|||||||
'updatetime': opts.updatetime,
|
'updatetime': opts.updatetime,
|
||||||
'writedescription': opts.writedescription,
|
'writedescription': opts.writedescription,
|
||||||
'writeinfojson': opts.writeinfojson,
|
'writeinfojson': opts.writeinfojson,
|
||||||
|
'writethumbnail': opts.writethumbnail,
|
||||||
'writesubtitles': opts.writesubtitles,
|
'writesubtitles': opts.writesubtitles,
|
||||||
|
'allsubtitles': opts.allsubtitles,
|
||||||
|
'listsubtitles': opts.listsubtitles,
|
||||||
|
'subtitlesformat': opts.subtitlesformat,
|
||||||
'subtitleslang': opts.subtitleslang,
|
'subtitleslang': opts.subtitleslang,
|
||||||
'matchtitle': decodeOption(opts.matchtitle),
|
'matchtitle': decodeOption(opts.matchtitle),
|
||||||
'rejecttitle': decodeOption(opts.rejecttitle),
|
'rejecttitle': decodeOption(opts.rejecttitle),
|
||||||
'max_downloads': opts.max_downloads,
|
'max_downloads': opts.max_downloads,
|
||||||
'prefer_free_formats': opts.prefer_free_formats,
|
'prefer_free_formats': opts.prefer_free_formats,
|
||||||
'verbose': opts.verbose,
|
'verbose': opts.verbose,
|
||||||
|
'dump_intermediate_pages': opts.dump_intermediate_pages,
|
||||||
'test': opts.test,
|
'test': opts.test,
|
||||||
'keepvideo': opts.keepvideo,
|
'keepvideo': opts.keepvideo,
|
||||||
'min_filesize': opts.min_filesize,
|
'min_filesize': opts.min_filesize,
|
||||||
'max_filesize': opts.max_filesize
|
'max_filesize': opts.max_filesize,
|
||||||
|
'daterange': date,
|
||||||
})
|
})
|
||||||
|
|
||||||
if opts.verbose:
|
if opts.verbose:
|
||||||
@ -523,9 +602,9 @@ def _real_main():
|
|||||||
|
|
||||||
sys.exit(retcode)
|
sys.exit(retcode)
|
||||||
|
|
||||||
def main():
|
def main(argv=None):
|
||||||
try:
|
try:
|
||||||
_real_main()
|
_real_main(argv)
|
||||||
except DownloadError:
|
except DownloadError:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except SameFileError:
|
except SameFileError:
|
||||||
|
@ -9,7 +9,8 @@ import sys
|
|||||||
if __package__ is None and not hasattr(sys, "frozen"):
|
if __package__ is None and not hasattr(sys, "frozen"):
|
||||||
# direct call of __main__.py
|
# direct call of __main__.py
|
||||||
import os.path
|
import os.path
|
||||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
path = os.path.realpath(os.path.abspath(__file__))
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(path)))
|
||||||
|
|
||||||
import youtube_dl
|
import youtube_dl
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ def rsa_verify(message, signature, key):
|
|||||||
def update_self(to_screen, verbose, filename):
|
def update_self(to_screen, verbose, filename):
|
||||||
"""Update the program file with the latest version from the repository"""
|
"""Update the program file with the latest version from the repository"""
|
||||||
|
|
||||||
UPDATE_URL = "http://rg3.github.com/youtube-dl/update/"
|
UPDATE_URL = "http://rg3.github.io/youtube-dl/update/"
|
||||||
VERSION_URL = UPDATE_URL + 'LATEST_VERSION'
|
VERSION_URL = UPDATE_URL + 'LATEST_VERSION'
|
||||||
JSON_URL = UPDATE_URL + 'versions.json'
|
JSON_URL = UPDATE_URL + 'versions.json'
|
||||||
UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537)
|
UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537)
|
||||||
@ -78,7 +78,7 @@ def update_self(to_screen, verbose, filename):
|
|||||||
to_screen(u'Updating to version ' + versions_info['latest'] + '...')
|
to_screen(u'Updating to version ' + versions_info['latest'] + '...')
|
||||||
version = versions_info['versions'][versions_info['latest']]
|
version = versions_info['versions'][versions_info['latest']]
|
||||||
|
|
||||||
print_notes(versions_info['versions'])
|
print_notes(to_screen, versions_info['versions'])
|
||||||
|
|
||||||
if not os.access(filename, os.W_OK):
|
if not os.access(filename, os.W_OK):
|
||||||
to_screen(u'ERROR: no write permissions on %s' % filename)
|
to_screen(u'ERROR: no write permissions on %s' % filename)
|
||||||
@ -157,11 +157,15 @@ del "%s"
|
|||||||
|
|
||||||
to_screen(u'Updated youtube-dl. Restart youtube-dl to use the new version.')
|
to_screen(u'Updated youtube-dl. Restart youtube-dl to use the new version.')
|
||||||
|
|
||||||
def print_notes(versions, fromVersion=__version__):
|
def get_notes(versions, fromVersion):
|
||||||
notes = []
|
notes = []
|
||||||
for v,vdata in sorted(versions.items()):
|
for v,vdata in sorted(versions.items()):
|
||||||
if v > fromVersion:
|
if v > fromVersion:
|
||||||
notes.extend(vdata.get('notes', []))
|
notes.extend(vdata.get('notes', []))
|
||||||
|
return notes
|
||||||
|
|
||||||
|
def print_notes(to_screen, versions, fromVersion=__version__):
|
||||||
|
notes = get_notes(versions, fromVersion)
|
||||||
if notes:
|
if notes:
|
||||||
to_screen(u'PLEASE NOTE:')
|
to_screen(u'PLEASE NOTE:')
|
||||||
for note in notes:
|
for note in notes:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import errno
|
||||||
import gzip
|
import gzip
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
@ -12,6 +13,7 @@ import traceback
|
|||||||
import zlib
|
import zlib
|
||||||
import email.utils
|
import email.utils
|
||||||
import json
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import urllib.request as compat_urllib_request
|
import urllib.request as compat_urllib_request
|
||||||
@ -148,6 +150,10 @@ try:
|
|||||||
except NameError:
|
except NameError:
|
||||||
compat_chr = chr
|
compat_chr = chr
|
||||||
|
|
||||||
|
def compat_ord(c):
|
||||||
|
if type(c) is int: return c
|
||||||
|
else: return ord(c)
|
||||||
|
|
||||||
std_headers = {
|
std_headers = {
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0',
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0',
|
||||||
'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
|
'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
|
||||||
@ -311,7 +317,7 @@ def clean_html(html):
|
|||||||
html = re.sub('<.*?>', '', html)
|
html = re.sub('<.*?>', '', html)
|
||||||
# Replace html entities
|
# Replace html entities
|
||||||
html = unescapeHTML(html)
|
html = unescapeHTML(html)
|
||||||
return html
|
return html.strip()
|
||||||
|
|
||||||
|
|
||||||
def sanitize_open(filename, open_mode):
|
def sanitize_open(filename, open_mode):
|
||||||
@ -329,16 +335,24 @@ def sanitize_open(filename, open_mode):
|
|||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
import msvcrt
|
import msvcrt
|
||||||
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
|
||||||
return (sys.stdout, filename)
|
return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
|
||||||
stream = open(encodeFilename(filename), open_mode)
|
stream = open(encodeFilename(filename), open_mode)
|
||||||
return (stream, filename)
|
return (stream, filename)
|
||||||
except (IOError, OSError) as err:
|
except (IOError, OSError) as err:
|
||||||
# In case of error, try to remove win32 forbidden chars
|
if err.errno in (errno.EACCES,):
|
||||||
filename = re.sub(u'[/<>:"\\|\\\\?\\*]', u'#', filename)
|
raise
|
||||||
|
|
||||||
# An exception here should be caught in the caller
|
# In case of error, try to remove win32 forbidden chars
|
||||||
stream = open(encodeFilename(filename), open_mode)
|
alt_filename = os.path.join(
|
||||||
return (stream, filename)
|
re.sub(u'[/<>:"\\|\\\\?\\*]', u'#', path_part)
|
||||||
|
for path_part in os.path.split(filename)
|
||||||
|
)
|
||||||
|
if alt_filename == filename:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# An exception here should be caught in the caller
|
||||||
|
stream = open(encodeFilename(filename), open_mode)
|
||||||
|
return (stream, alt_filename)
|
||||||
|
|
||||||
|
|
||||||
def timeconvert(timestr):
|
def timeconvert(timestr):
|
||||||
@ -429,12 +443,35 @@ def decodeOption(optval):
|
|||||||
assert isinstance(optval, compat_str)
|
assert isinstance(optval, compat_str)
|
||||||
return optval
|
return optval
|
||||||
|
|
||||||
|
def formatSeconds(secs):
|
||||||
|
if secs > 3600:
|
||||||
|
return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60)
|
||||||
|
elif secs > 60:
|
||||||
|
return '%d:%02d' % (secs // 60, secs % 60)
|
||||||
|
else:
|
||||||
|
return '%d' % secs
|
||||||
|
|
||||||
|
def make_HTTPS_handler(opts):
|
||||||
|
if sys.version_info < (3,2):
|
||||||
|
# Python's 2.x handler is very simplistic
|
||||||
|
return compat_urllib_request.HTTPSHandler()
|
||||||
|
else:
|
||||||
|
import ssl
|
||||||
|
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
|
||||||
|
context.set_default_verify_paths()
|
||||||
|
|
||||||
|
context.verify_mode = (ssl.CERT_NONE
|
||||||
|
if opts.no_check_certificate
|
||||||
|
else ssl.CERT_REQUIRED)
|
||||||
|
return compat_urllib_request.HTTPSHandler(context=context)
|
||||||
|
|
||||||
class ExtractorError(Exception):
|
class ExtractorError(Exception):
|
||||||
"""Error during info extraction."""
|
"""Error during info extraction."""
|
||||||
def __init__(self, msg, tb=None):
|
def __init__(self, msg, tb=None):
|
||||||
""" tb, if given, is the original traceback (so that it can be printed out). """
|
""" tb, if given, is the original traceback (so that it can be printed out). """
|
||||||
super(ExtractorError, self).__init__(msg)
|
super(ExtractorError, self).__init__(msg)
|
||||||
self.traceback = tb
|
self.traceback = tb
|
||||||
|
self.exc_info = sys.exc_info() # preserve original exception
|
||||||
|
|
||||||
def format_traceback(self):
|
def format_traceback(self):
|
||||||
if self.traceback is None:
|
if self.traceback is None:
|
||||||
@ -449,7 +486,10 @@ class DownloadError(Exception):
|
|||||||
configured to continue on errors. They will contain the appropriate
|
configured to continue on errors. They will contain the appropriate
|
||||||
error message.
|
error message.
|
||||||
"""
|
"""
|
||||||
pass
|
def __init__(self, msg, exc_info=None):
|
||||||
|
""" exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
|
||||||
|
super(DownloadError, self).__init__(msg)
|
||||||
|
self.exc_info = exc_info
|
||||||
|
|
||||||
|
|
||||||
class SameFileError(Exception):
|
class SameFileError(Exception):
|
||||||
@ -564,3 +604,70 @@ class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
|
|||||||
|
|
||||||
https_request = http_request
|
https_request = http_request
|
||||||
https_response = http_response
|
https_response = http_response
|
||||||
|
|
||||||
|
def unified_strdate(date_str):
|
||||||
|
"""Return a string with the date in the format YYYYMMDD"""
|
||||||
|
upload_date = None
|
||||||
|
#Replace commas
|
||||||
|
date_str = date_str.replace(',',' ')
|
||||||
|
# %z (UTC offset) is only supported in python>=3.2
|
||||||
|
date_str = re.sub(r' (\+|-)[\d]*$', '', date_str)
|
||||||
|
format_expressions = ['%d %B %Y', '%B %d %Y', '%b %d %Y', '%Y-%m-%d', '%d/%m/%Y', '%Y/%m/%d %H:%M:%S']
|
||||||
|
for expression in format_expressions:
|
||||||
|
try:
|
||||||
|
upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return upload_date
|
||||||
|
|
||||||
|
def date_from_str(date_str):
|
||||||
|
"""
|
||||||
|
Return a datetime object from a string in the format YYYYMMDD or
|
||||||
|
(now|today)[+-][0-9](day|week|month|year)(s)?"""
|
||||||
|
today = datetime.date.today()
|
||||||
|
if date_str == 'now'or date_str == 'today':
|
||||||
|
return today
|
||||||
|
match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
|
||||||
|
if match is not None:
|
||||||
|
sign = match.group('sign')
|
||||||
|
time = int(match.group('time'))
|
||||||
|
if sign == '-':
|
||||||
|
time = -time
|
||||||
|
unit = match.group('unit')
|
||||||
|
#A bad aproximation?
|
||||||
|
if unit == 'month':
|
||||||
|
unit = 'day'
|
||||||
|
time *= 30
|
||||||
|
elif unit == 'year':
|
||||||
|
unit = 'day'
|
||||||
|
time *= 365
|
||||||
|
unit += 's'
|
||||||
|
delta = datetime.timedelta(**{unit: time})
|
||||||
|
return today + delta
|
||||||
|
return datetime.datetime.strptime(date_str, "%Y%m%d").date()
|
||||||
|
|
||||||
|
class DateRange(object):
|
||||||
|
"""Represents a time interval between two dates"""
|
||||||
|
def __init__(self, start=None, end=None):
|
||||||
|
"""start and end must be strings in the format accepted by date"""
|
||||||
|
if start is not None:
|
||||||
|
self.start = date_from_str(start)
|
||||||
|
else:
|
||||||
|
self.start = datetime.datetime.min.date()
|
||||||
|
if end is not None:
|
||||||
|
self.end = date_from_str(end)
|
||||||
|
else:
|
||||||
|
self.end = datetime.datetime.max.date()
|
||||||
|
if self.start > self.end:
|
||||||
|
raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
|
||||||
|
@classmethod
|
||||||
|
def day(cls, day):
|
||||||
|
"""Returns a range that only contains the given day"""
|
||||||
|
return cls(day,day)
|
||||||
|
def __contains__(self, date):
|
||||||
|
"""Check if the date is in the range"""
|
||||||
|
if not isinstance(date, datetime.date):
|
||||||
|
date = date_from_str(date)
|
||||||
|
return self.start <= date <= self.end
|
||||||
|
def __str__(self):
|
||||||
|
return '%s - %s' % ( self.start.isoformat(), self.end.isoformat())
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
|
|
||||||
__version__ = '2013.02.25'
|
__version__ = '2013.05.14'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user