From: Rogério Brito Date: Thu, 23 May 2013 00:00:29 +0000 (-0300) Subject: Merge tag 'upstream/2013.05.14' X-Git-Url: https://git.rapsys.eu/youtubedl/commitdiff_plain/c7b4d76a372777e3af76ecf9966a8ab9952e52f4?hp=1e9dbd83ef395035f4ec9be7b1d9ffefdd31484d Merge tag 'upstream/2013.05.14' Upstream version 2013.05.14. We remove our own changes to avoid future conflicts. Conflicts: .gitignore --- diff --git a/.gitignore b/.gitignore index b13aa17..77469b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ *.pyc *.pyo *~ -/.pc +*.DS_Store wine-py2exe/ py2exe.log *.kate-swp @@ -14,3 +14,7 @@ youtube-dl.bash-completion youtube-dl youtube-dl.exe youtube-dl.tar.gz +.coverage +cover/ +updates_key.pem +*.egg-info diff --git a/.travis.yml b/.travis.yml index 0aabce6..7f1fa8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,14 @@ language: python python: - "2.6" - "2.7" -# - "3.3" + - "3.3" script: nosetests test --verbose notifications: email: - filippo.valsorda@gmail.com - irc: - channels: - - "irc.freenode.org#youtube-dl" - skip_join: true + - phihag@phihag.de + - jaime.marquinez.ferrandiz+travis@gmail.com +# irc: +# channels: +# - "irc.freenode.org#youtube-dl" +# skip_join: true diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..3fa1167 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,14 @@ +2013.01.02 Codename: GIULIA + + * Add support for ComedyCentral clips + * Corrected Vimeo description fetching + * Added the --no-post-overwrites argument + * --verbose offers more environment info + * New info_dict field: uploader_id + * New updates system, with signature checking + * New IEs: NBA, JustinTV, FunnyOrDie, TweetReel, Steam, Ustream + * Fixed IEs: BlipTv + * Fixed for Python 3 IEs: Xvideo, Youku, XNXX, Dailymotion, Vimeo, InfoQ + * Simplified IEs and test code + * Various (Python 3 and other) fixes + * Revamped and expanded tests diff --git a/LATEST_VERSION b/LATEST_VERSION index 275de03..a334573 100644 --- a/LATEST_VERSION +++ b/LATEST_VERSION @@ -1 +1 @@ -9999.99.99 \ No newline at end of file +2012.12.99 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/MANIFEST.in b/MANIFEST.in index 81f8e05..8f8af7a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,5 @@ include README.md include test/*.py -include test/*.json \ No newline at end of file +include test/*.json +include youtube-dl.bash-completion +include youtube-dl.1 diff --git a/Makefile b/Makefile index c97d6ff..e00f5e6 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,26 @@ all: youtube-dl README.md README.txt youtube-dl.1 youtube-dl.bash-completion clean: - rm -rf youtube-dl youtube-dl.exe youtube-dl.1 youtube-dl.bash-completion README.txt MANIFEST build/ dist/ + rm -rf youtube-dl.1 youtube-dl.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ youtube-dl.tar.gz + +cleanall: clean + rm -f youtube-dl youtube-dl.exe PREFIX=/usr/local BINDIR=$(PREFIX)/bin MANDIR=$(PREFIX)/man -SYSCONFDIR=/etc +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 -d $(DESTDIR)$(BINDIR) @@ -17,14 +31,19 @@ install: youtube-dl youtube-dl.1 youtube-dl.bash-completion install -m 644 youtube-dl.bash-completion $(DESTDIR)$(SYSCONFDIR)/bash_completion.d/youtube-dl test: - nosetests2 --nocapture test + #nosetests --with-coverage --cover-package=youtube_dl --cover-html --verbose --processes 4 test + nosetests --verbose test + +tar: youtube-dl.tar.gz -.PHONY: all clean install test +.PHONY: all clean install test tar bash-completion pypi-files + +pypi-files: youtube-dl.bash-completion README.txt youtube-dl.1 youtube-dl: youtube_dl/*.py zip --quiet youtube-dl youtube_dl/*.py zip --quiet --junk-paths youtube-dl youtube_dl/__main__.py - echo '#!/usr/bin/env python' > youtube-dl + echo '#!$(PYTHON)' > youtube-dl cat youtube-dl.zip >> youtube-dl rm youtube-dl.zip chmod a+x youtube-dl @@ -38,11 +57,22 @@ README.txt: README.md youtube-dl.1: README.md pandoc -s -f markdown -t man README.md -o youtube-dl.1 -youtube-dl.bash-completion: youtube_dl/*.py devscripts/bash-completion.template +youtube-dl.bash-completion: youtube_dl/*.py devscripts/bash-completion.in python devscripts/bash-completion.py -youtube-dl.tar.gz: all - tar -czf youtube-dl.tar.gz -s "|^./|./youtube-dl/|" \ - --exclude="*.pyc" --exclude="*.pyo" --exclude="*~" --exclude="youtube-dl.exe" \ - --exclude="wine-py2exe/" --exclude="py2exe.log" --exclude="*.kate-swp" \ - --exclude="build/" --exclude="dist/" --exclude="MANIFEST" --exclude=".git/" . +bash-completion: youtube-dl.bash-completion + +youtube-dl.tar.gz: youtube-dl README.md README.txt youtube-dl.1 youtube-dl.bash-completion + @tar -czf youtube-dl.tar.gz --transform "s|^|youtube-dl/|" --owner 0 --group 0 \ + --exclude '*.DS_Store' \ + --exclude '*.kate-swp' \ + --exclude '*.pyc' \ + --exclude '*.pyo' \ + --exclude '*~' \ + --exclude '__pycache' \ + --exclude '.git' \ + -- \ + bin devscripts test youtube_dl \ + CHANGELOG LICENSE README.md README.txt \ + Makefile MANIFEST.in youtube-dl.1 youtube-dl.bash-completion setup.py \ + youtube-dl diff --git a/README.md b/README.md index 2ca0211..2f3c81a 100644 --- a/README.md +++ b/README.md @@ -9,112 +9,146 @@ youtube-dl # DESCRIPTION **youtube-dl** is a small command-line program to download videos from YouTube.com and a few more sites. It requires the Python interpreter, version -2.x (x being at least 6), and it is not platform specific. It should work in -your Unix box, in Windows or in Mac OS X. It is released to the public domain, +2.6, 2.7, or 3.3+, and it is not platform specific. It should work on +your Unix box, on Windows or on Mac OS X. It is released to the public domain, which means you can modify it, redistribute it or use it however you like. # OPTIONS - -h, --help print this help text and exit - --version print program version and exit - -U, --update update this program to latest version - -i, --ignore-errors continue on download errors - -r, --rate-limit LIMIT download rate limit (e.g. 50k or 44.6m) - -R, --retries RETRIES number of retries (default is 10) - --buffer-size SIZE size of download buffer (e.g. 1024 or 16k) (default - is 1024) - --no-resize-buffer do not automatically adjust the buffer size. By - default, the buffer size is automatically resized - from an initial value of SIZE. - --dump-user-agent display the current browser identification - --user-agent UA specify a custom user agent - --list-extractors List all supported extractors and the URLs they - would handle + -h, --help print this help text and exit + --version print program version and exit + -U, --update update this program to latest version + -i, --ignore-errors continue on download errors + -r, --rate-limit LIMIT maximum download rate (e.g. 50k or 44.6m) + -R, --retries RETRIES number of retries (default is 10) + --buffer-size SIZE size of download buffer (e.g. 1024 or 16k) + (default is 1024) + --no-resize-buffer do not automatically adjust the buffer size. By + default, the buffer size is automatically resized + from an initial value of SIZE. + --dump-user-agent display the current browser identification + --user-agent UA specify a custom user agent + --referer REF specify a custom referer, use if the video access + 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: - --playlist-start NUMBER playlist video to start at (default is 1) - --playlist-end NUMBER playlist video to end at (default is last) - --match-title REGEX download only matching titles (regex or caseless - sub-string) - --reject-title REGEX skip download for matching titles (regex or - caseless sub-string) - --max-downloads NUMBER Abort after downloading NUMBER files + --playlist-start NUMBER playlist video to start at (default is 1) + --playlist-end NUMBER playlist video to end at (default is last) + --match-title REGEX download only matching titles (regex or caseless + sub-string) + --reject-title REGEX skip download for matching titles (regex or + caseless sub-string) + --max-downloads NUMBER Abort after downloading NUMBER files + --min-filesize SIZE Do not download any videos smaller than SIZE + (e.g. 50k or 44.6m) + --max-filesize SIZE Do not download any videos larger than SIZE (e.g. + 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: - -t, --title use title in file name - --id use video ID in file name - -l, --literal [deprecated] alias of --title - -A, --auto-number number downloaded files starting from 00000 - -o, --output TEMPLATE output filename template. Use %(title)s to get the - title, %(uploader)s for the uploader name, - %(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' . - --restrict-filenames Restrict filenames to only ASCII characters, and - avoid "&" and spaces in filenames - -a, --batch-file FILE file containing URLs to download ('-' for stdin) - -w, --no-overwrites do not overwrite files - -c, --continue resume partially downloaded files - --no-continue do not resume partially downloaded files (restart - from beginning) - --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 + -t, --title use title in file name (default) + --id use only video ID in file name + -l, --literal [deprecated] alias of --title + -A, --auto-number number downloaded files starting from 00000 + -o, --output TEMPLATE 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/d + ownloads/%(uploader)s/%(title)s-%(id)s.%(ext)s' . + --autonumber-size NUMBER Specifies the number of digits in %(autonumber)s + when it is present in output filename template or + --autonumber option is given + --restrict-filenames Restrict filenames to only ASCII characters, and + avoid "&" and spaces in filenames + -a, --batch-file FILE file containing URLs to download ('-' for stdin) + -w, --no-overwrites do not overwrite files + -c, --continue resume partially downloaded files + --no-continue do not resume partially downloaded files (restart + from beginning) + --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: - -q, --quiet activates quiet mode - -s, --simulate do not download the video and do not write anything - to disk - --skip-download do not download the video - -g, --get-url simulate, quiet but print URL - -e, --get-title simulate, quiet but print title - --get-thumbnail simulate, quiet but print thumbnail URL - --get-description simulate, quiet but print video description - --get-filename simulate, quiet but print output filename - --get-format simulate, quiet but print output format - --no-progress do not print progress bar - --console-title display progress in console titlebar - -v, --verbose print various debugging information + -q, --quiet activates quiet mode + -s, --simulate do not download the video and do not write + anything to disk + --skip-download do not download the video + -g, --get-url simulate, quiet but print URL + -e, --get-title simulate, quiet but print title + --get-id simulate, quiet but print id + --get-thumbnail simulate, quiet but print thumbnail URL + --get-description simulate, quiet but print video description + --get-filename simulate, quiet but print output filename + --get-format simulate, quiet but print output format + --newline output progress bar as new lines + --no-progress do not print progress bar + --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: - -f, --format FORMAT video format code - --all-formats download all available video formats - --prefer-free-formats prefer free video formats unless a specific one is - requested - --max-quality FORMAT highest quality format to download - -F, --list-formats list all available formats (currently youtube only) - --write-srt write video closed captions to a .srt file - (currently youtube only) - --srt-lang LANG language of the closed captions to download - (optional) use IETF language tags like 'en' + -f, --format FORMAT video format code, specifiy the order of + preference using slashes: "-f 22/17/18" + --all-formats download all available video formats + --prefer-free-formats prefer free video formats unless a specific one + is requested + --max-quality FORMAT highest quality format to download + -F, --list-formats list all available formats (currently youtube + only) + --write-sub write subtitle file (currently youtube only) + --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: - -u, --username USERNAME account username - -p, --password PASSWORD account password - -n, --netrc use .netrc authentication data + -u, --username USERNAME account username + -p, --password PASSWORD account password + -n, --netrc use .netrc authentication data ## Post-processing Options: - -x, --extract-audio convert video files to audio-only files (requires - ffmpeg or avconv and ffprobe or avprobe) - --audio-format FORMAT "best", "aac", "vorbis", "mp3", "m4a", or "wav"; - best by default - --audio-quality QUALITY ffmpeg/avconv audio quality specification, insert a - value between 0 (better) and 9 (worse) for VBR or a - specific bitrate like 128K (default 5) - -k, --keep-video keeps the video file on disk after the post- - processing; the video is erased by default + -x, --extract-audio convert video files to audio-only files (requires + ffmpeg or avconv and ffprobe or avprobe) + --audio-format FORMAT "best", "aac", "vorbis", "mp3", "m4a", "opus", or + "wav"; best by default + --audio-quality QUALITY ffmpeg/avconv audio quality specification, insert + a value between 0 (better) and 9 (worse) for VBR + or a specific bitrate like 128K (default 5) + --recode-video FORMAT Encode the video to another format if necessary + (currently supported: mp4|flv|ogg|webm) + -k, --keep-video keeps the video file on disk after the post- + processing; the video is erased by default + --no-post-overwrites do not overwrite post-processed files; the post- + processed files are overwritten by default # CONFIGURATION -You can configure youtube-dl by placing default arguments (such as `--extract-audio --no-mtime` to always extract the audio and not copy the mtime) into `/etc/youtube-dl.conf` and/or `~/.local/config/youtube-dl.conf`. +You can configure youtube-dl by placing default arguments (such as `--extract-audio --no-mtime` to always extract the audio and not copy the mtime) into `/etc/youtube-dl.conf` and/or `~/.config/youtube-dl.conf`. # OUTPUT TEMPLATE @@ -128,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). - `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. + - `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). @@ -138,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_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 ### Can you please put the -b option back? @@ -194,6 +243,7 @@ Bugs and suggestions should be reported at: Copyright © 2006-)(?P[0-9]{4})', u'Copyright © 2006-' + year, content) + if content != newc: + tmpFn = fn + '.part' + with io.open(tmpFn, 'wt', encoding='utf-8') as outf: + outf.write(newc) + os.rename(tmpFn, fn) diff --git a/devscripts/gh-pages/update-feed.py b/devscripts/gh-pages/update-feed.py new file mode 100755 index 0000000..cfff05f --- /dev/null +++ b/devscripts/gh-pages/update-feed.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +import datetime + +import textwrap + +import json + +atom_template=textwrap.dedent("""\ + + + youtube-dl releases + youtube-dl-updates-feed + @TIMESTAMP@ + @ENTRIES@ + """) + +entry_template=textwrap.dedent(""" + + youtube-dl-@VERSION@ + New version @VERSION@ + + +
+ Downloads available at http://youtube-dl.org/downloads/@VERSION@/ +
+
+ + The youtube-dl maintainers + + @TIMESTAMP@ +
+ """) + +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) + + diff --git a/devscripts/make_readme.py b/devscripts/make_readme.py old mode 100644 new mode 100755 diff --git a/devscripts/release.sh b/devscripts/release.sh index 963a6c2..b2a91f8 100755 --- a/devscripts/release.sh +++ b/devscripts/release.sh @@ -1,11 +1,92 @@ -#!/bin/sh +#!/bin/bash + +# IMPORTANT: the following assumptions are made +# * the GH repo is on the origin remote +# * the gh-pages branch is named so locally +# * the git config user.signingkey is properly set + +# You will need +# pip install coverage nose rsa + +# TODO +# release notes +# make hash on local files + +set -e if [ -z "$1" ]; then echo "ERROR: specify version number like this: $0 1994.09.06"; exit 1; fi version="$1" if [ ! -z "`git tag | grep "$version"`" ]; then echo 'ERROR: version already present'; exit 1; fi -if [ ! -z "`git status --porcelain`" ]; then echo 'ERROR: the working directory is not clean; commit or stash changes'; exit 1; fi -sed -i "s/__version__ = '.*'/__version__ = '$version'/" youtube_dl/__init__.py -make all -git add -A +if [ ! -z "`git status --porcelain | grep -v CHANGELOG`" ]; then echo 'ERROR: the working directory is not clean; commit or stash changes'; exit 1; fi +if [ ! -f "updates_key.pem" ]; then echo 'ERROR: updates_key.pem missing'; exit 1; fi + +/bin/echo -e "\n### First of all, testing..." +make cleanall +nosetests --with-coverage --cover-package=youtube_dl --cover-html test --stop || exit 1 + +/bin/echo -e "\n### Changing version in version.py..." +sed -i "s/__version__ = '.*'/__version__ = '$version'/" youtube_dl/version.py + +/bin/echo -e "\n### Committing CHANGELOG README.md and youtube_dl/version.py..." +make README.md +git add CHANGELOG README.md youtube_dl/version.py git commit -m "release $version" -git tag -m "Release $version" "$version" \ No newline at end of file + +/bin/echo -e "\n### Now tagging, signing and pushing..." +git tag -s -m "Release $version" "$version" +git show "$version" +read -p "Is it good, can I push? (y/n) " -n 1 +if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; fi +echo +MASTER=$(git rev-parse --abbrev-ref HEAD) +git push origin $MASTER:master +git push origin "$version" + +/bin/echo -e "\n### OK, now it is time to build the binaries..." +REV=$(git rev-parse HEAD) +make youtube-dl youtube-dl.tar.gz +wget "http://jeromelaheurte.net:8142/download/rg3/youtube-dl/youtube-dl.exe?rev=$REV" -O youtube-dl.exe || \ + wget "http://jeromelaheurte.net:8142/build/rg3/youtube-dl/youtube-dl.exe?rev=$REV" -O youtube-dl.exe +mkdir -p "build/$version" +mv youtube-dl youtube-dl.exe "build/$version" +mv youtube-dl.tar.gz "build/$version/youtube-dl-$version.tar.gz" +RELEASE_FILES="youtube-dl youtube-dl.exe youtube-dl-$version.tar.gz" +(cd build/$version/ && md5sum $RELEASE_FILES > MD5SUMS) +(cd build/$version/ && sha1sum $RELEASE_FILES > SHA1SUMS) +(cd build/$version/ && sha256sum $RELEASE_FILES > SHA2-256SUMS) +(cd build/$version/ && sha512sum $RELEASE_FILES > SHA2-512SUMS) +git checkout HEAD -- youtube-dl youtube-dl.exe + +/bin/echo -e "\n### Signing and uploading the new binaries to youtube-dl.org..." +for f in $RELEASE_FILES; do gpg --detach-sig "build/$version/$f"; done +scp -r "build/$version" ytdl@youtube-dl.org:html/downloads/ + +/bin/echo -e "\n### Now switching to gh-pages..." +git clone --branch gh-pages --single-branch . build/gh-pages +ROOT=$(pwd) +( + set -e + ORIGIN_URL=$(git config --get remote.origin.url) + cd build/gh-pages + "$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/generate-download.py" + "$ROOT/devscripts/gh-pages/update-copyright.py" + git add *.html *.html.in update + git commit -m "release $version" + git show HEAD + read -p "Is it good, can I push? (y/n) " -n 1 + if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; fi + echo + git push "$ROOT" gh-pages + git push "$ORIGIN_URL" gh-pages +) +rm -rf build + +make pypi-files +echo "Uploading to PyPi ..." +python setup.py sdist upload +make clean + +/bin/echo -e "\n### DONE!" diff --git a/devscripts/transition_helper_exe/youtube-dl.py b/devscripts/transition_helper_exe/youtube-dl.py index 409f980..6297dfd 100644 --- a/devscripts/transition_helper_exe/youtube-dl.py +++ b/devscripts/transition_helper_exe/youtube-dl.py @@ -2,17 +2,48 @@ import sys, os import urllib2 +import json, hashlib + +def rsa_verify(message, signature, key): + from struct import pack + from hashlib import sha256 + from sys import version_info + def b(x): + if version_info[0] == 2: return x + else: return x.encode('latin1') + assert(type(message) == type(b(''))) + block_size = 0 + n = key[0] + while n: + block_size += 1 + n >>= 8 + signature = pow(int(signature, 16), key[1], key[0]) + raw_bytes = [] + while signature: + raw_bytes.insert(0, pack("B", signature & 0xFF)) + signature >>= 8 + signature = (block_size - len(raw_bytes)) * b('\x00') + b('').join(raw_bytes) + if signature[0:2] != b('\x00\x01'): return False + signature = signature[2:] + if not b('\x00') in signature: return False + signature = signature[signature.index(b('\x00'))+1:] + if not signature.startswith(b('\x30\x31\x30\x0D\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20')): return False + signature = signature[19:] + if signature != sha256(message).digest(): return False + return True sys.stderr.write(u'Hi! We changed distribution method and now youtube-dl needs to update itself one more time.\n') sys.stderr.write(u'This will only happen once. Simply press enter to go on. Sorry for the trouble!\n') -sys.stderr.write(u'The new location of the binaries is https://github.com/rg3/youtube-dl/downloads, not the git repository.\n\n') +sys.stderr.write(u'From now on, get the binaries from http://rg3.github.com/youtube-dl/download.html, not from the git repository.\n\n') raw_input() filename = sys.argv[0] -API_URL = "https://api.github.com/repos/rg3/youtube-dl/downloads" -EXE_URL = "https://github.com/downloads/rg3/youtube-dl/youtube-dl.exe" +UPDATE_URL = "http://rg3.github.io/youtube-dl/update/" +VERSION_URL = UPDATE_URL + 'LATEST_VERSION' +JSON_URL = UPDATE_URL + 'versions.json' +UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537) if not os.access(filename, os.W_OK): sys.exit('ERROR: no write permissions on %s' % filename) @@ -23,13 +54,35 @@ if not os.access(directory, os.W_OK): sys.exit('ERROR: no write permissions on %s' % directory) try: - urlh = urllib2.urlopen(EXE_URL) + versions_info = urllib2.urlopen(JSON_URL).read().decode('utf-8') + versions_info = json.loads(versions_info) +except: + sys.exit(u'ERROR: can\'t obtain versions info. Please try again later.') +if not 'signature' in versions_info: + sys.exit(u'ERROR: the versions file is not signed or corrupted. Aborting.') +signature = versions_info['signature'] +del versions_info['signature'] +if not rsa_verify(json.dumps(versions_info, sort_keys=True), signature, UPDATES_RSA_KEY): + sys.exit(u'ERROR: the versions file signature is invalid. Aborting.') + +version = versions_info['versions'][versions_info['latest']] + +try: + urlh = urllib2.urlopen(version['exe'][0]) newcontent = urlh.read() urlh.close() +except (IOError, OSError) as err: + sys.exit('ERROR: unable to download latest version') + +newcontent_hash = hashlib.sha256(newcontent).hexdigest() +if newcontent_hash != version['exe'][1]: + sys.exit(u'ERROR: the downloaded file hash does not match. Aborting.') + +try: with open(exe + '.new', 'wb') as outf: outf.write(newcontent) except (IOError, OSError) as err: - sys.exit('ERROR: unable to download latest version') + sys.exit(u'ERROR: unable to write the new version') try: bat = os.path.join(directory, 'youtube-dl-updater.bat') diff --git a/setup.py b/setup.py index 6d019dc..eb7b1a2 100644 --- a/setup.py +++ b/setup.py @@ -2,10 +2,14 @@ # -*- coding: utf-8 -*- from __future__ import print_function -from distutils.core import setup import pkg_resources import sys +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + try: import py2exe """This will create an exe that needs Microsoft Visual C++ 2008 Redistributable Package""" diff --git a/test/gentests.py b/test/gentests.py deleted file mode 100755 index 0ea5d9f..0000000 --- a/test/gentests.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python3 - -import io # for python 2 -import json -import os -import sys -import unittest - -# Allow direct execution -import os -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -import youtube_dl.InfoExtractors - -HEADER = u'''#!/usr/bin/env python - -# DO NOT EDIT THIS FILE BY HAND! -# It is auto-generated from tests.json and gentests.py. - -import hashlib -import io -import os -import json -import unittest -import sys -import socket - -# Allow direct execution -import os -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -import youtube_dl.FileDownloader -import youtube_dl.InfoExtractors -from youtube_dl.utils import * - -# General configuration (from __init__, not very elegant...) -jar = compat_cookiejar.CookieJar() -cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar) -proxy_handler = compat_urllib_request.ProxyHandler() -opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler()) -compat_urllib_request.install_opener(opener) -socket.setdefaulttimeout(300) # 5 minutes should be enough (famous last words) - -class FileDownloader(youtube_dl.FileDownloader): - def __init__(self, *args, **kwargs): - youtube_dl.FileDownloader.__init__(self, *args, **kwargs) - self.to_stderr = self.to_screen - -def _file_md5(fn): - with open(fn, 'rb') as f: - return hashlib.md5(f.read()).hexdigest() -try: - _skip_unless = unittest.skipUnless -except AttributeError: # Python 2.6 - def _skip_unless(cond, reason='No reason given'): - def resfunc(f): - # Start the function name with test to appease nosetests-2.6 - def test_wfunc(*args, **kwargs): - if cond: - return f(*args, **kwargs) - else: - print('Skipped test') - return - return test_wfunc - return resfunc -_skip = lambda *args, **kwargs: _skip_unless(False, *args, **kwargs) - -class DownloadTest(unittest.TestCase): - PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json") - - def setUp(self): - # Clear old files - self.tearDown() - - with io.open(self.PARAMETERS_FILE, encoding='utf-8') as pf: - self.parameters = json.load(pf) -''' - -FOOTER = u''' - -if __name__ == '__main__': - unittest.main() -''' - -DEF_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tests.json') -TEST_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test_download.py') - -def gentests(): - with io.open(DEF_FILE, encoding='utf-8') as deff: - defs = json.load(deff) - with io.open(TEST_FILE, 'w', encoding='utf-8') as testf: - testf.write(HEADER) - spaces = ' ' * 4 - write = lambda l: testf.write(spaces + l + u'\n') - - for d in defs: - name = d['name'] - ie = getattr(youtube_dl.InfoExtractors, name + 'IE') - testf.write(u'\n') - write('@_skip_unless(youtube_dl.InfoExtractors.' + name + 'IE._WORKING, "IE marked as not _WORKING")') - if not d['file']: - write('@_skip("No output file specified")') - elif 'skip' in d: - write('@_skip(' + repr(d['skip']) + ')') - write('def test_' + name + '(self):') - write(' filename = ' + repr(d['file'])) - write(' params = self.parameters') - for p in d.get('params', {}): - write(' params["' + p + '"] = ' + repr(d['params'][p])) - write(' fd = FileDownloader(params)') - write(' fd.add_info_extractor(youtube_dl.InfoExtractors.' + name + 'IE())') - for ien in d.get('addIEs', []): - write(' fd.add_info_extractor(youtube_dl.InfoExtractors.' + ien + 'IE())') - write(' fd.download([' + repr(d['url']) + '])') - write(' self.assertTrue(os.path.exists(filename))') - if 'md5' in d: - write(' md5_for_file = _file_md5(filename)') - write(' self.assertEqual(md5_for_file, ' + repr(d['md5']) + ')') - - testf.write(u'\n\n') - write('def tearDown(self):') - for d in defs: - if d['file']: - write(' if os.path.exists(' + repr(d['file']) + '):') - write(' os.remove(' + repr(d['file']) + ')') - else: - write(' # No file specified for ' + d['name']) - testf.write(u'\n') - testf.write(FOOTER) - -if __name__ == '__main__': - gentests() diff --git a/test/parameters.json b/test/parameters.json index c2082df..96998b5 100644 --- a/test/parameters.json +++ b/test/parameters.json @@ -29,12 +29,16 @@ "simulate": false, "skip_download": false, "subtitleslang": null, + "subtitlesformat": "srt", "test": true, "updatetime": true, "usenetrc": false, "username": null, "verbose": true, "writedescription": false, - "writeinfojson": false, - "writesubtitles": false -} \ No newline at end of file + "writeinfojson": true, + "writesubtitles": false, + "onlysubtitles": false, + "allsubtitles": false, + "listssubtitles": false +} diff --git a/test/test_all_urls.py b/test/test_all_urls.py new file mode 100644 index 0000000..a403601 --- /dev/null +++ b/test/test_all_urls.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + +import sys +import unittest + +# Allow direct execution +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from youtube_dl.InfoExtractors import YoutubeIE, YoutubePlaylistIE, YoutubeChannelIE + +class TestAllURLsMatching(unittest.TestCase): + def test_youtube_playlist_matching(self): + self.assertTrue(YoutubePlaylistIE.suitable(u'ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')) + self.assertTrue(YoutubePlaylistIE.suitable(u'UUBABnxM4Ar9ten8Mdjj1j0Q')) #585 + self.assertTrue(YoutubePlaylistIE.suitable(u'PL63F0C78739B09958')) + self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')) + self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')) + self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')) + self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012')) #668 + self.assertFalse(YoutubePlaylistIE.suitable(u'PLtS2H6bU1M')) + + def test_youtube_matching(self): + self.assertTrue(YoutubeIE.suitable(u'PLtS2H6bU1M')) + 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): + 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?feature=player_embedded&v=BaW_jenozKc'), 'BaW_jenozKc') + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_download.py b/test/test_download.py index f45c500..3eca333 100644 --- a/test/test_download.py +++ b/test/test_download.py @@ -1,197 +1,141 @@ #!/usr/bin/env python -# DO NOT EDIT THIS FILE BY HAND! -# It is auto-generated from tests.json and gentests.py. - +import errno import hashlib import io import os import json import unittest import sys +import hashlib import socket # Allow direct execution -import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import youtube_dl.FileDownloader import youtube_dl.InfoExtractors from youtube_dl.utils import * +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") + +RETRIES = 3 + # General configuration (from __init__, not very elegant...) jar = compat_cookiejar.CookieJar() cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar) proxy_handler = compat_urllib_request.ProxyHandler() opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler()) compat_urllib_request.install_opener(opener) -socket.setdefaulttimeout(300) # 5 minutes should be enough (famous last words) +socket.setdefaulttimeout(10) + +def _try_rm(filename): + """ Remove a file if it exists """ + try: + os.remove(filename) + except OSError as ose: + if ose.errno != errno.ENOENT: + raise class FileDownloader(youtube_dl.FileDownloader): def __init__(self, *args, **kwargs): - youtube_dl.FileDownloader.__init__(self, *args, **kwargs) self.to_stderr = self.to_screen + self.processed_info_dicts = [] + return youtube_dl.FileDownloader.__init__(self, *args, **kwargs) + def process_info(self, info_dict): + self.processed_info_dicts.append(info_dict) + return youtube_dl.FileDownloader.process_info(self, info_dict) def _file_md5(fn): with open(fn, 'rb') as f: return hashlib.md5(f.read()).hexdigest() -try: - _skip_unless = unittest.skipUnless -except AttributeError: # Python 2.6 - def _skip_unless(cond, reason='No reason given'): - def resfunc(f): - # Start the function name with test to appease nosetests-2.6 - def test_wfunc(*args, **kwargs): - if cond: - return f(*args, **kwargs) - else: - print('Skipped test') - return - return test_wfunc - return resfunc -_skip = lambda *args, **kwargs: _skip_unless(False, *args, **kwargs) -class DownloadTest(unittest.TestCase): - PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json") +with io.open(DEF_FILE, encoding='utf-8') as deff: + defs = json.load(deff) +with io.open(PARAMETERS_FILE, encoding='utf-8') as pf: + parameters = json.load(pf) - def setUp(self): - # Clear old files - self.tearDown() - with io.open(self.PARAMETERS_FILE, encoding='utf-8') as pf: - self.parameters = json.load(pf) +class TestDownload(unittest.TestCase): + maxDiff = None + def setUp(self): + self.parameters = parameters + self.defs = defs + +### Dynamically generate tests +def generator(test_case): + + def test_template(self): + ie = youtube_dl.InfoExtractors.get_info_extractor(test_case['name']) + if not ie._WORKING: + print('Skipping: IE marked as not _WORKING') + return + if 'playlist' not in test_case and not test_case['file']: + print('Skipping: No output file specified') + return + if 'skip' in test_case: + print('Skipping: {0}'.format(test_case['skip'])) + return + + params = self.parameters.copy() + params.update(test_case.get('params', {})) - @_skip_unless(youtube_dl.InfoExtractors.YoutubeIE._WORKING, "IE marked as not _WORKING") - def test_Youtube(self): - filename = u'BaW_jenozKc.mp4' - params = self.parameters - fd = FileDownloader(params) - fd.add_info_extractor(youtube_dl.InfoExtractors.YoutubeIE()) - fd.download([u'http://www.youtube.com/watch?v=BaW_jenozKc']) - self.assertTrue(os.path.exists(filename)) - - @_skip_unless(youtube_dl.InfoExtractors.DailymotionIE._WORKING, "IE marked as not _WORKING") - def test_Dailymotion(self): - filename = u'x33vw9.mp4' - params = self.parameters fd = FileDownloader(params) - fd.add_info_extractor(youtube_dl.InfoExtractors.DailymotionIE()) - fd.download([u'http://www.dailymotion.com/video/x33vw9_tutoriel-de-youtubeur-dl-des-video_tech']) - self.assertTrue(os.path.exists(filename)) - md5_for_file = _file_md5(filename) - self.assertEqual(md5_for_file, u'392c4b85a60a90dc4792da41ce3144eb') - - @_skip_unless(youtube_dl.InfoExtractors.MetacafeIE._WORKING, "IE marked as not _WORKING") - def test_Metacafe(self): - filename = u'_aUehQsCQtM.flv' - params = self.parameters - fd = FileDownloader(params) - fd.add_info_extractor(youtube_dl.InfoExtractors.MetacafeIE()) - fd.add_info_extractor(youtube_dl.InfoExtractors.YoutubeIE()) - fd.download([u'http://www.metacafe.com/watch/yt-_aUehQsCQtM/the_electric_company_short_i_pbs_kids_go/']) - self.assertTrue(os.path.exists(filename)) - - @_skip_unless(youtube_dl.InfoExtractors.BlipTVIE._WORKING, "IE marked as not _WORKING") - def test_BlipTV(self): - filename = u'5779306.m4v' - params = self.parameters - fd = FileDownloader(params) - fd.add_info_extractor(youtube_dl.InfoExtractors.BlipTVIE()) - fd.download([u'http://blip.tv/cbr/cbr-exclusive-gotham-city-imposters-bats-vs-jokerz-short-3-5796352']) - self.assertTrue(os.path.exists(filename)) - md5_for_file = _file_md5(filename) - self.assertEqual(md5_for_file, u'b2d849efcf7ee18917e4b4d9ff37cafe') - - @_skip_unless(youtube_dl.InfoExtractors.XVideosIE._WORKING, "IE marked as not _WORKING") - def test_XVideos(self): - filename = u'939581.flv' - params = self.parameters - fd = FileDownloader(params) - fd.add_info_extractor(youtube_dl.InfoExtractors.XVideosIE()) - fd.download([u'http://www.xvideos.com/video939581/funny_porns_by_s_-1']) - self.assertTrue(os.path.exists(filename)) - md5_for_file = _file_md5(filename) - self.assertEqual(md5_for_file, u'1d0c835822f0a71a7bf011855db929d0') - - @_skip_unless(youtube_dl.InfoExtractors.VimeoIE._WORKING, "IE marked as not _WORKING") - def test_Vimeo(self): - filename = u'14160053.mp4' - params = self.parameters - fd = FileDownloader(params) - fd.add_info_extractor(youtube_dl.InfoExtractors.VimeoIE()) - fd.download([u'http://vimeo.com/14160053']) - self.assertTrue(os.path.exists(filename)) - md5_for_file = _file_md5(filename) - self.assertEqual(md5_for_file, u'60540a4ec7cc378ec84b919c0aed5023') - - @_skip_unless(youtube_dl.InfoExtractors.SoundcloudIE._WORKING, "IE marked as not _WORKING") - def test_Soundcloud(self): - filename = u'62986583.mp3' - params = self.parameters - fd = FileDownloader(params) - fd.add_info_extractor(youtube_dl.InfoExtractors.SoundcloudIE()) - fd.download([u'http://soundcloud.com/ethmusic/lostin-powers-she-so-heavy']) - self.assertTrue(os.path.exists(filename)) - md5_for_file = _file_md5(filename) - self.assertEqual(md5_for_file, u'ebef0a451b909710ed1d7787dddbf0d7') - - @_skip_unless(youtube_dl.InfoExtractors.StanfordOpenClassroomIE._WORKING, "IE marked as not _WORKING") - def test_StanfordOpenClassroom(self): - filename = u'PracticalUnix_intro-environment.mp4' - params = self.parameters - fd = FileDownloader(params) - fd.add_info_extractor(youtube_dl.InfoExtractors.StanfordOpenClassroomIE()) - fd.download([u'http://openclassroom.stanford.edu/MainFolder/VideoPage.php?course=PracticalUnix&video=intro-environment&speed=100']) - self.assertTrue(os.path.exists(filename)) - md5_for_file = _file_md5(filename) - self.assertEqual(md5_for_file, u'544a9468546059d4e80d76265b0443b8') - - @_skip_unless(youtube_dl.InfoExtractors.XNXXIE._WORKING, "IE marked as not _WORKING") - def test_XNXX(self): - filename = u'1135332.flv' - params = self.parameters - fd = FileDownloader(params) - fd.add_info_extractor(youtube_dl.InfoExtractors.XNXXIE()) - fd.download([u'http://video.xnxx.com/video1135332/lida_naked_funny_actress_5_']) - self.assertTrue(os.path.exists(filename)) - md5_for_file = _file_md5(filename) - self.assertEqual(md5_for_file, u'0831677e2b4761795f68d417e0b7b445') - - @_skip_unless(youtube_dl.InfoExtractors.YoukuIE._WORKING, "IE marked as not _WORKING") - def test_Youku(self): - filename = u'XNDgyMDQ2NTQw_part00.flv' - params = self.parameters - params["test"] = False - fd = FileDownloader(params) - fd.add_info_extractor(youtube_dl.InfoExtractors.YoukuIE()) - fd.download([u'http://v.youku.com/v_show/id_XNDgyMDQ2NTQw.html']) - self.assertTrue(os.path.exists(filename)) - md5_for_file = _file_md5(filename) - self.assertEqual(md5_for_file, u'ffe3f2e435663dc2d1eea34faeff5b5b') - - - def tearDown(self): - if os.path.exists(u'BaW_jenozKc.mp4'): - os.remove(u'BaW_jenozKc.mp4') - if os.path.exists(u'x33vw9.mp4'): - os.remove(u'x33vw9.mp4') - if os.path.exists(u'_aUehQsCQtM.flv'): - os.remove(u'_aUehQsCQtM.flv') - if os.path.exists(u'5779306.m4v'): - os.remove(u'5779306.m4v') - if os.path.exists(u'939581.flv'): - os.remove(u'939581.flv') - if os.path.exists(u'14160053.mp4'): - os.remove(u'14160053.mp4') - if os.path.exists(u'62986583.mp3'): - os.remove(u'62986583.mp3') - if os.path.exists(u'PracticalUnix_intro-environment.mp4'): - os.remove(u'PracticalUnix_intro-environment.mp4') - if os.path.exists(u'1135332.flv'): - os.remove(u'1135332.flv') - if os.path.exists(u'XNDgyMDQ2NTQw_part00.flv'): - os.remove(u'XNDgyMDQ2NTQw_part00.flv') - + for ie in youtube_dl.InfoExtractors.gen_extractors(): + fd.add_info_extractor(ie) + finished_hook_called = set() + def _hook(status): + if status['status'] == 'finished': + finished_hook_called.add(status['filename']) + fd.add_progress_hook(_hook) + + test_cases = test_case.get('playlist', [test_case]) + for tc in test_cases: + _try_rm(tc['file']) + _try_rm(tc['file'] + '.part') + _try_rm(tc['file'] + '.info.json') + try: + 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: + if not test_case.get('params', {}).get('skip_download', False): + self.assertTrue(os.path.exists(tc['file']), msg='Missing file ' + tc['file']) + self.assertTrue(tc['file'] in finished_hook_called) + self.assertTrue(os.path.exists(tc['file'] + '.info.json')) + if 'md5' in tc: + md5_for_file = _file_md5(tc['file']) + self.assertEqual(md5_for_file, tc['md5']) + with io.open(tc['file'] + '.info.json', encoding='utf-8') as infof: + info_dict = json.load(infof) + for (info_field, value) in tc.get('info_dict', {}).items(): + self.assertEqual(value, info_dict.get(info_field)) + finally: + for tc in test_cases: + _try_rm(tc['file']) + _try_rm(tc['file'] + '.part') + _try_rm(tc['file'] + '.info.json') + + return test_template + +### And add them to TestDownload +for test_case in defs: + test_method = generator(test_case) + test_method.__name__ = "test_{0}".format(test_case["name"]) + setattr(TestDownload, test_method.__name__, test_method) + del test_method if __name__ == '__main__': diff --git a/test/test_utils.py b/test/test_utils.py index 796ed4a..c4b7136 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -14,6 +14,8 @@ from youtube_dl.utils import timeconvert from youtube_dl.utils import sanitize_filename from youtube_dl.utils import unescapeHTML 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): _compat_str = lambda b: b.decode('unicode-escape') @@ -82,9 +84,9 @@ class TestUtil(unittest.TestCase): self.assertTrue(sanitize_filename(':', restricted=True) != '') def test_sanitize_ids(self): - self.assertEquals(sanitize_filename('_n_cd26wFpw', is_id=True), '_n_cd26wFpw') - self.assertEquals(sanitize_filename('_BD_eEpuzXw', is_id=True), '_BD_eEpuzXw') - self.assertEquals(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI') + self.assertEqual(sanitize_filename('_n_cd26wFpw', is_id=True), '_n_cd26wFpw') + self.assertEqual(sanitize_filename('_BD_eEpuzXw', is_id=True), '_BD_eEpuzXw') + self.assertEqual(sanitize_filename('N0Y__7-UOdI', is_id=True), 'N0Y__7-UOdI') def test_ordered_set(self): self.assertEqual(orderedSet([1, 1, 2, 3, 4, 4, 5, 6, 7, 3, 5]), [1, 2, 3, 4, 5, 6, 7]) @@ -95,6 +97,20 @@ class TestUtil(unittest.TestCase): def test_unescape_html(self): 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__': unittest.main() diff --git a/test/test_write_info_json.py b/test/test_write_info_json.py new file mode 100644 index 0000000..8134dda --- /dev/null +++ b/test/test_write_info_json.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# coding: utf-8 + +import json +import os +import sys +import unittest + +# Allow direct execution +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import youtube_dl.FileDownloader +import youtube_dl.InfoExtractors +from youtube_dl.utils import * + +PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json") + +# General configuration (from __init__, not very elegant...) +jar = compat_cookiejar.CookieJar() +cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar) +proxy_handler = compat_urllib_request.ProxyHandler() +opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler()) +compat_urllib_request.install_opener(opener) + +class FileDownloader(youtube_dl.FileDownloader): + def __init__(self, *args, **kwargs): + youtube_dl.FileDownloader.__init__(self, *args, **kwargs) + self.to_stderr = self.to_screen + +with io.open(PARAMETERS_FILE, encoding='utf-8') as pf: + params = json.load(pf) +params['writeinfojson'] = True +params['skip_download'] = True +params['writedescription'] = True + +TEST_ID = 'BaW_jenozKc' +INFO_JSON_FILE = TEST_ID + '.mp4.info.json' +DESCRIPTION_FILE = TEST_ID + '.mp4.description' +EXPECTED_DESCRIPTION = u'''test chars: "'/\ä↭𝕐 + +This is a test video for youtube-dl. + +For more information, contact phihag@phihag.de .''' + +class TestInfoJSON(unittest.TestCase): + def setUp(self): + # Clear old files + self.tearDown() + + def test_info_json(self): + ie = youtube_dl.InfoExtractors.YoutubeIE() + fd = FileDownloader(params) + fd.add_info_extractor(ie) + fd.download([TEST_ID]) + self.assertTrue(os.path.exists(INFO_JSON_FILE)) + with io.open(INFO_JSON_FILE, 'r', encoding='utf-8') as jsonf: + jd = json.load(jsonf) + self.assertEqual(jd['upload_date'], u'20121002') + self.assertEqual(jd['description'], EXPECTED_DESCRIPTION) + self.assertEqual(jd['id'], TEST_ID) + self.assertEqual(jd['extractor'], 'youtube') + self.assertEqual(jd['title'], u'''youtube-dl test video "'/\ä↭𝕐''') + self.assertEqual(jd['uploader'], 'Philipp Hagemeister') + + self.assertTrue(os.path.exists(DESCRIPTION_FILE)) + with io.open(DESCRIPTION_FILE, 'r', encoding='utf-8') as descf: + descr = descf.read() + self.assertEqual(descr, EXPECTED_DESCRIPTION) + + def tearDown(self): + if os.path.exists(INFO_JSON_FILE): + os.remove(INFO_JSON_FILE) + if os.path.exists(DESCRIPTION_FILE): + os.remove(DESCRIPTION_FILE) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_youtube_lists.py b/test/test_youtube_lists.py index a11a59e..78657b5 100644 --- a/test/test_youtube_lists.py +++ b/test/test_youtube_lists.py @@ -2,14 +2,19 @@ import sys import unittest -import socket +import json # Allow direct execution import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from youtube_dl.InfoExtractors import YoutubePlaylistIE +from youtube_dl.InfoExtractors import YoutubeUserIE, YoutubePlaylistIE, YoutubeIE, YoutubeChannelIE 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") +with io.open(PARAMETERS_FILE, encoding='utf-8') as pf: + parameters = json.load(pf) # General configuration (from __init__, not very elegant...) jar = compat_cookiejar.CookieJar() @@ -17,54 +22,88 @@ cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar) proxy_handler = compat_urllib_request.ProxyHandler() opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler()) compat_urllib_request.install_opener(opener) -socket.setdefaulttimeout(300) # 5 minutes should be enough (famous last words) -class FakeDownloader(object): +class FakeDownloader(FileDownloader): def __init__(self): self.result = [] - self.params = {} + self.params = parameters def to_screen(self, s): print(s) - def trouble(self, s): + def trouble(self, s, tb=None): raise Exception(s) - def download(self, x): - self.result.append(x) + def extract_info(self, url): + self.result.append(url) + return url 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): - DL = FakeDownloader() - IE = YoutubePlaylistIE(DL) - IE.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re') - self.assertEqual(DL.result, [ - ['http://www.youtube.com/watch?v=bV9L5Ht9LgY'], - ['http://www.youtube.com/watch?v=FXxLjLQi3Fg'], - ['http://www.youtube.com/watch?v=tU3Bgo5qJZE'] - ]) + dl = FakeDownloader() + ie = YoutubePlaylistIE(dl) + result = ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')[0] + 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']) + + def test_issue_673(self): + dl = FakeDownloader() + ie = YoutubePlaylistIE(dl) + result = ie.extract('PLBB231211A4F62143')[0] + self.assertEqual(result['title'], 'Team Fortress 2') + self.assertTrue(len(result['entries']) > 40) def test_youtube_playlist_long(self): - DL = FakeDownloader() - IE = YoutubePlaylistIE(DL) - IE.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q') - self.assertTrue(len(DL.result) >= 799) + dl = FakeDownloader() + ie = YoutubePlaylistIE(dl) + result = ie.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')[0] + self.assertIsPlaylist(result) + self.assertTrue(len(result['entries']) >= 799) + + def test_youtube_playlist_with_deleted(self): + #651 + dl = FakeDownloader() + ie = YoutubePlaylistIE(dl) + result = ie.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')[0] + ytie_results = [YoutubeIE()._extract_id(url['url']) for url in result['entries']] + self.assertFalse('pElCt5oNDuI' 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): - DL = FakeDownloader() - IE = YoutubePlaylistIE(DL) + dl = FakeDownloader() + ie = YoutubePlaylistIE(dl) # TODO find a > 100 (paginating?) videos course - IE.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8') - self.assertEqual(DL.result[0], ['http://www.youtube.com/watch?v=j9WZyLZCBzs']) - self.assertEqual(len(DL.result), 25) - self.assertEqual(DL.result[-1], ['http://www.youtube.com/watch?v=rYefUsYuEp0']) + result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')[0] + entries = result['entries'] + self.assertEqual(YoutubeIE()._extract_id(entries[0]['url']), 'j9WZyLZCBzs') + self.assertEqual(len(entries), 25) + self.assertEqual(YoutubeIE()._extract_id(entries[-1]['url']), 'rYefUsYuEp0') def test_youtube_channel(self): - """I give up, please find a channel that does paginate and test this like test_youtube_playlist_long""" - pass # TODO + dl = FakeDownloader() + 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): - DL = FakeDownloader() - IE = YoutubePlaylistIE(DL) - IE.extract('https://www.youtube.com/user/TheLinuxFoundation') - self.assertTrue(len(DL.result) >= 320) + dl = FakeDownloader() + ie = YoutubeUserIE(dl) + result = ie.extract('https://www.youtube.com/user/TheLinuxFoundation')[0] + self.assertTrue(len(result['entries']) >= 320) if __name__ == '__main__': unittest.main() diff --git a/test/test_youtube_playlist_ids.py b/test/test_youtube_playlist_ids.py deleted file mode 100644 index 2eeb321..0000000 --- a/test/test_youtube_playlist_ids.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python - -import sys -import unittest - -# Allow direct execution -import os -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from youtube_dl.InfoExtractors import YoutubeIE, YoutubePlaylistIE - -class TestYoutubePlaylistMatching(unittest.TestCase): - def test_playlist_matching(self): - self.assertTrue(YoutubePlaylistIE().suitable(u'ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')) - self.assertTrue(YoutubePlaylistIE().suitable(u'PL63F0C78739B09958')) - self.assertFalse(YoutubePlaylistIE().suitable(u'PLtS2H6bU1M')) - - def test_youtube_matching(self): - self.assertTrue(YoutubeIE().suitable(u'PLtS2H6bU1M')) - -if __name__ == '__main__': - unittest.main() diff --git a/test/test_youtube_subtitles.py b/test/test_youtube_subtitles.py new file mode 100644 index 0000000..a123e6d --- /dev/null +++ b/test/test_youtube_subtitles.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python + +import sys +import unittest +import json +import io +import hashlib + +# Allow direct execution +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from youtube_dl.InfoExtractors import YoutubeIE +from youtube_dl.utils import * +from youtube_dl import FileDownloader + +PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json") +with io.open(PARAMETERS_FILE, encoding='utf-8') as pf: + parameters = json.load(pf) + +# General configuration (from __init__, not very elegant...) +jar = compat_cookiejar.CookieJar() +cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar) +proxy_handler = compat_urllib_request.ProxyHandler() +opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler()) +compat_urllib_request.install_opener(opener) + +class FakeDownloader(FileDownloader): + def __init__(self): + self.result = [] + self.params = parameters + def to_screen(self, s): + print(s) + def trouble(self, s, tb=None): + raise Exception(s) + def download(self, x): + self.result.append(x) + +md5 = lambda s: hashlib.md5(s.encode('utf-8')).hexdigest() + +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): + DL = FakeDownloader() + DL.params['writesubtitles'] = True + IE = YoutubeIE(DL) + info_dict = IE.extract('QRS8MkLhQmM') + sub = info_dict[0]['subtitles'][0] + self.assertEqual(md5(sub[2]), '4cd9278a35ba2305f47354ee13472260') + def test_youtube_subtitles_it(self): + DL = FakeDownloader() + DL.params['writesubtitles'] = True + DL.params['subtitleslang'] = 'it' + IE = YoutubeIE(DL) + info_dict = IE.extract('QRS8MkLhQmM') + 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__': + unittest.main() diff --git a/test/tests.json b/test/tests.json index 34992a3..f0e5cd8 100644 --- a/test/tests.json +++ b/test/tests.json @@ -2,7 +2,14 @@ { "name": "Youtube", "url": "http://www.youtube.com/watch?v=BaW_jenozKc", - "file": "BaW_jenozKc.mp4" + "file": "BaW_jenozKc.mp4", + "info_dict": { + "title": "youtube-dl test video \"'/\\ä↭𝕐", + "uploader": "Philipp Hagemeister", + "uploader_id": "phihag", + "upload_date": "20121002", + "description": "test chars: \"'/\\ä↭𝕐\n\nThis is a test video for youtube-dl.\n\nFor more information, contact phihag@phihag.de ." + } }, { "name": "Dailymotion", @@ -12,8 +19,8 @@ }, { "name": "Metacafe", - "addIEs": ["Youtube"], - "url": "http://www.metacafe.com/watch/yt-_aUehQsCQtM/the_electric_company_short_i_pbs_kids_go/", + "add_ie": ["Youtube"], + "url": "http://metacafe.com/watch/yt-_aUehQsCQtM/the_electric_company_short_i_pbs_kids_go/", "file": "_aUehQsCQtM.flv" }, { @@ -28,11 +35,36 @@ "url": "http://www.xvideos.com/video939581/funny_porns_by_s_-1", "file": "939581.flv" }, + { + "name": "YouPorn", + "md5": "c37ddbaaa39058c76a7e86c6813423c1", + "url": "http://www.youporn.com/watch/505835/sex-ed-is-it-safe-to-masturbate-daily/", + "file": "505835.mp4" + }, + { + "name": "Pornotube", + "md5": "374dd6dcedd24234453b295209aa69b6", + "url": "http://pornotube.com/c/173/m/1689755/Marilyn-Monroe-Bathing", + "file": "1689755.flv" + }, + { + "name": "YouJizz", + "md5": "07e15fa469ba384c7693fd246905547c", + "url": "http://www.youjizz.com/videos/zeichentrick-1-2189178.html", + "file": "2189178.flv" + }, { "name": "Vimeo", - "md5": "60540a4ec7cc378ec84b919c0aed5023", - "url": "http://vimeo.com/14160053", - "file": "14160053.mp4" + "md5": "8879b6cc097e987f02484baf890129e5", + "url": "http://vimeo.com/56015672", + "file": "56015672.mp4", + "info_dict": { + "title": "youtube-dl test video - ★ \" ' 幸 / \\ ä ↭ 𝕐", + "uploader": "Filippo Valsorda", + "uploader_id": "user7108434", + "upload_date": "20121220", + "description": "This is a test case for youtube-dl.\nFor more information, see github.com/rg3/youtube-dl\nTest chars: ★ \" ' 幸 / \\ ä ↭ 𝕐" + } }, { "name": "Soundcloud", @@ -58,5 +90,355 @@ "file": "XNDgyMDQ2NTQw_part00.flv", "md5": "ffe3f2e435663dc2d1eea34faeff5b5b", "params": { "test": false } + }, + { + "name": "NBA", + "url": "http://www.nba.com/video/games/nets/2012/12/04/0021200253-okc-bkn-recap.nba/index.html", + "file": "0021200253-okc-bkn-recap.nba.mp4", + "md5": "c0edcfc37607344e2ff8f13c378c88a4" + }, + { + "name": "JustinTV", + "url": "http://www.twitch.tv/thegamedevhub/b/296128360", + "file": "296128360.flv", + "md5": "ecaa8a790c22a40770901460af191c9a" + }, + { + "name": "MyVideo", + "url": "http://www.myvideo.de/watch/8229274/bowling_fail_or_win", + "file": "8229274.flv", + "md5": "2d2753e8130479ba2cb7e0a37002053e" + }, + { + "name": "Escapist", + "url": "http://www.escapistmagazine.com/videos/view/the-escapist-presents/6618-Breaking-Down-Baldurs-Gate", + "file": "6618-Breaking-Down-Baldurs-Gate.mp4", + "md5": "c6793dbda81388f4264c1ba18684a74d" + }, + { + "name": "GooglePlus", + "url": "https://plus.google.com/u/0/108897254135232129896/posts/ZButuJc6CtH", + "file": "ZButuJc6CtH.flv" + }, + { + "name": "FunnyOrDie", + "url": "http://www.funnyordie.com/videos/0732f586d7/heart-shaped-box-literal-video-version", + "file": "0732f586d7.mp4", + "md5": "f647e9e90064b53b6e046e75d0241fbd" + }, + { + "name": "Steam", + "url": "http://store.steampowered.com/video/105600/", + "playlist": [ + { + "file": "81300.flv", + "md5": "f870007cee7065d7c76b88f0a45ecc07", + "info_dict": { + "title": "Terraria 1.1 Trailer" + } + }, + { + "file": "80859.flv", + "md5": "61aaf31a5c5c3041afb58fb83cbb5751", + "info_dict": { + "title": "Terraria Trailer" + } + } + ] + }, + { + "name": "Ustream", + "url": "http://www.ustream.tv/recorded/20274954", + "file": "20274954.flv", + "md5": "088f151799e8f572f84eb62f17d73e5c", + "info_dict": { + "title": "Young Americans for Liberty February 7, 2012 2:28 AM" + } + }, + { + "name": "InfoQ", + "url": "http://www.infoq.com/presentations/A-Few-of-My-Favorite-Python-Things", + "file": "12-jan-pythonthings.mp4", + "info_dict": { + "title": "A Few of My Favorite [Python] Things" + }, + "params": { + "skip_download": true + } + }, + { + "name": "ComedyCentral", + "url": "http://www.thedailyshow.com/watch/thu-december-13-2012/kristen-stewart", + "file": "422212.mp4", + "md5": "4e2f5cb088a83cd8cdb7756132f9739d", + "info_dict": { + "title": "thedailyshow-kristen-stewart part 1" + } + }, + { + "name": "RBMARadio", + "url": "http://www.rbmaradio.com/shows/ford-lopatin-live-at-primavera-sound-2011", + "file": "ford-lopatin-live-at-primavera-sound-2011.mp3", + "md5": "6bc6f9bcb18994b4c983bc3bf4384d95", + "info_dict": { + "title": "Live at Primavera Sound 2011", + "description": "Joel Ford and Daniel \u2019Oneohtrix Point Never\u2019 Lopatin fly their midified pop extravaganza to Spain. Live at Primavera Sound 2011.", + "uploader": "Ford & Lopatin", + "uploader_id": "ford-lopatin", + "location": "Spain" + } + }, + { + "name": "Facebook", + "url": "https://www.facebook.com/photo.php?v=120708114770723", + "file": "120708114770723.mp4", + "md5": "48975a41ccc4b7a581abd68651c1a5a8", + "info_dict": { + "title": "PEOPLE ARE AWESOME 2013", + "duration": 279 + } + }, + { + "name": "EightTracks", + "url": "http://8tracks.com/ytdl/youtube-dl-test-tracks-a", + "playlist": [ + { + "file": "11885610.m4a", + "md5": "96ce57f24389fc8734ce47f4c1abcc55", + "info_dict": { + "title": "youtue-dl project<>\"' - youtube-dl test track 1 \"'/\\\u00e4\u21ad", + "uploader_id": "ytdl" + } + }, + { + "file": "11885608.m4a", + "md5": "4ab26f05c1f7291ea460a3920be8021f", + "info_dict": { + "title": "youtube-dl project - youtube-dl test track 2 \"'/\\\u00e4\u21ad", + "uploader_id": "ytdl" + + } + }, + { + "file": "11885679.m4a", + "md5": "d30b5b5f74217410f4689605c35d1fd7", + "info_dict": { + "title": "youtube-dl project as well - youtube-dl test track 3 \"'/\\\u00e4\u21ad" + } + }, + { + "file": "11885680.m4a", + "md5": "4eb0a669317cd725f6bbd336a29f923a", + "info_dict": { + "title": "youtube-dl project as well - youtube-dl test track 4 \"'/\\\u00e4\u21ad" + } + }, + { + "file": "11885682.m4a", + "md5": "1893e872e263a2705558d1d319ad19e8", + "info_dict": { + "title": "PH - youtube-dl test track 5 \"'/\\\u00e4\u21ad" + } + }, + { + "file": "11885683.m4a", + "md5": "b673c46f47a216ab1741ae8836af5899", + "info_dict": { + "title": "PH - youtube-dl test track 6 \"'/\\\u00e4\u21ad" + } + }, + { + "file": "11885684.m4a", + "md5": "1d74534e95df54986da7f5abf7d842b7", + "info_dict": { + "title": "phihag - youtube-dl test track 7 \"'/\\\u00e4\u21ad" + } + }, + { + "file": "11885685.m4a", + "md5": "f081f47af8f6ae782ed131d38b9cd1c0", + "info_dict": { + "title": "phihag - youtube-dl test track 8 \"'/\\\u00e4\u21ad" + } + } + ] + }, + { + "name": "Keek", + "url": "http://www.keek.com/ytdl/keeks/NODfbab", + "file": "NODfbab.mp4", + "md5": "9b0636f8c0f7614afa4ea5e4c6e57e83", + "info_dict": { + "title": "test chars: \"'/\\ä<>This is a test video for youtube-dl.For more information, contact phihag@phihag.de ." + } + + }, + { + "name": "TED", + "url": "http://www.ted.com/talks/dan_dennett_on_our_consciousness.html", + "file": "102.mp4", + "md5": "7bc087e71d16f18f9b8ab9fa62a8a031", + "info_dict": { + "title": "Dan Dennett: The illusion of consciousness", + "thumbnail": "http://images.ted.com/images/ted/488_389x292.jpg" + } + }, + { + "name": "MySpass", + "url": "http://www.myspass.de/myspass/shows/tvshows/absolute-mehrheit/Absolute-Mehrheit-vom-17022013-Die-Highlights-Teil-2--/11741/", + "file": "11741.mp4", + "md5": "0b49f4844a068f8b33f4b7c88405862b", + "info_dict": { + "title": "Absolute Mehrheit vom 17.02.2013 - Die Highlights, Teil 2" + } + }, + { + "name": "Generic", + "url": "http://www.hodiho.fr/2013/02/regis-plante-sa-jeep.html", + "file": "13601338388002.mp4", + "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" } ] diff --git a/youtube-dl b/youtube-dl index 794718c..e3eb877 100755 Binary files a/youtube-dl and b/youtube-dl differ diff --git a/youtube-dl.1 b/youtube-dl.1 deleted file mode 100644 index 6b01f24..0000000 --- a/youtube-dl.1 +++ /dev/null @@ -1,298 +0,0 @@ -.TH YOUTUBE-DL 1 "" -.SH NAME -.PP -youtube-dl -.SH SYNOPSIS -.PP -\f[B]youtube-dl\f[] [OPTIONS] URL [URL...] -.SH DESCRIPTION -.PP -\f[B]youtube-dl\f[] is a small command-line program to download videos -from YouTube.com and a few more sites. -It requires the Python interpreter, version 2.x (x being at least 6), -and it is not platform specific. -It should work in your Unix box, in Windows or in Mac OS X. -It is released to the public domain, which means you can modify it, -redistribute it or use it however you like. -.SH OPTIONS -.IP -.nf -\f[C] --h,\ --help\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ print\ this\ help\ text\ and\ exit ---version\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ print\ program\ version\ and\ exit --U,\ --update\ \ \ \ \ \ \ \ \ \ \ \ \ update\ this\ program\ to\ latest\ version --i,\ --ignore-errors\ \ \ \ \ \ continue\ on\ download\ errors --r,\ --rate-limit\ LIMIT\ \ \ download\ rate\ limit\ (e.g.\ 50k\ or\ 44.6m) --R,\ --retries\ RETRIES\ \ \ \ number\ of\ retries\ (default\ is\ 10) ---buffer-size\ SIZE\ \ \ \ \ \ \ size\ of\ download\ buffer\ (e.g.\ 1024\ or\ 16k)\ (default -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ is\ 1024) ---no-resize-buffer\ \ \ \ \ \ \ do\ not\ automatically\ adjust\ the\ buffer\ size.\ By -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ default,\ the\ buffer\ size\ is\ automatically\ resized -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ from\ an\ initial\ value\ of\ SIZE. ---dump-user-agent\ \ \ \ \ \ \ \ display\ the\ current\ browser\ identification ---user-agent\ UA\ \ \ \ \ \ \ \ \ \ specify\ a\ custom\ user\ agent ---list-extractors\ \ \ \ \ \ \ \ List\ all\ supported\ extractors\ and\ the\ URLs\ they -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ would\ handle -\f[] -.fi -.SS Video Selection: -.IP -.nf -\f[C] ---playlist-start\ NUMBER\ \ playlist\ video\ to\ start\ at\ (default\ is\ 1) ---playlist-end\ NUMBER\ \ \ \ playlist\ video\ to\ end\ at\ (default\ is\ last) ---match-title\ REGEX\ \ \ \ \ \ download\ only\ matching\ titles\ (regex\ or\ caseless -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ sub-string) ---reject-title\ REGEX\ \ \ \ \ skip\ download\ for\ matching\ titles\ (regex\ or -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ caseless\ sub-string) ---max-downloads\ NUMBER\ \ \ Abort\ after\ downloading\ NUMBER\ files -\f[] -.fi -.SS Filesystem Options: -.IP -.nf -\f[C] --t,\ --title\ \ \ \ \ \ \ \ \ \ \ \ \ \ use\ title\ in\ file\ name ---id\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ use\ video\ ID\ in\ file\ name --l,\ --literal\ \ \ \ \ \ \ \ \ \ \ \ [deprecated]\ alias\ of\ --title --A,\ --auto-number\ \ \ \ \ \ \ \ number\ downloaded\ files\ starting\ from\ 00000 --o,\ --output\ TEMPLATE\ \ \ \ output\ filename\ template.\ Use\ %(title)s\ to\ get\ the -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ title,\ %(uploader)s\ for\ the\ uploader\ name, -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ %(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\ \[aq]/my/downloads/%(uploader)s/%(title -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ )s-%(id)s.%(ext)s\[aq]\ . ---restrict-filenames\ \ \ \ \ Restrict\ filenames\ to\ only\ ASCII\ characters,\ and -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ avoid\ "&"\ and\ spaces\ in\ filenames --a,\ --batch-file\ FILE\ \ \ \ file\ containing\ URLs\ to\ download\ (\[aq]-\[aq]\ for\ stdin) --w,\ --no-overwrites\ \ \ \ \ \ do\ not\ overwrite\ files --c,\ --continue\ \ \ \ \ \ \ \ \ \ \ resume\ partially\ downloaded\ files ---no-continue\ \ \ \ \ \ \ \ \ \ \ \ do\ not\ resume\ partially\ downloaded\ files\ (restart -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ from\ beginning) ---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 -\f[] -.fi -.SS Verbosity / Simulation Options: -.IP -.nf -\f[C] --q,\ --quiet\ \ \ \ \ \ \ \ \ \ \ \ \ \ activates\ quiet\ mode --s,\ --simulate\ \ \ \ \ \ \ \ \ \ \ do\ not\ download\ the\ video\ and\ do\ not\ write\ anything -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ to\ disk ---skip-download\ \ \ \ \ \ \ \ \ \ do\ not\ download\ the\ video --g,\ --get-url\ \ \ \ \ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ URL --e,\ --get-title\ \ \ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ title ---get-thumbnail\ \ \ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ thumbnail\ URL ---get-description\ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ video\ description ---get-filename\ \ \ \ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ output\ filename ---get-format\ \ \ \ \ \ \ \ \ \ \ \ \ simulate,\ quiet\ but\ print\ output\ format ---no-progress\ \ \ \ \ \ \ \ \ \ \ \ do\ not\ print\ progress\ bar ---console-title\ \ \ \ \ \ \ \ \ \ display\ progress\ in\ console\ titlebar --v,\ --verbose\ \ \ \ \ \ \ \ \ \ \ \ print\ various\ debugging\ information -\f[] -.fi -.SS Video Format Options: -.IP -.nf -\f[C] --f,\ --format\ FORMAT\ \ \ \ \ \ video\ format\ code ---all-formats\ \ \ \ \ \ \ \ \ \ \ \ download\ all\ available\ video\ formats ---prefer-free-formats\ \ \ \ prefer\ free\ video\ formats\ unless\ a\ specific\ one\ is -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ requested ---max-quality\ FORMAT\ \ \ \ \ highest\ quality\ format\ to\ download --F,\ --list-formats\ \ \ \ \ \ \ list\ all\ available\ formats\ (currently\ youtube\ only) ---write-srt\ \ \ \ \ \ \ \ \ \ \ \ \ \ write\ video\ closed\ captions\ to\ a\ .srt\ file -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (currently\ youtube\ only) ---srt-lang\ LANG\ \ \ \ \ \ \ \ \ \ language\ of\ the\ closed\ captions\ to\ download -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ (optional)\ use\ IETF\ language\ tags\ like\ \[aq]en\[aq] -\f[] -.fi -.SS Authentication Options: -.IP -.nf -\f[C] --u,\ --username\ USERNAME\ \ account\ username --p,\ --password\ PASSWORD\ \ account\ password --n,\ --netrc\ \ \ \ \ \ \ \ \ \ \ \ \ \ use\ .netrc\ authentication\ data -\f[] -.fi -.SS Post-processing Options: -.IP -.nf -\f[C] --x,\ --extract-audio\ \ \ \ \ \ convert\ video\ files\ to\ audio-only\ files\ (requires -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ffmpeg\ or\ avconv\ and\ ffprobe\ or\ avprobe) ---audio-format\ FORMAT\ \ \ \ "best",\ "aac",\ "vorbis",\ "mp3",\ "m4a",\ or\ "wav"; -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ best\ by\ default ---audio-quality\ QUALITY\ \ ffmpeg/avconv\ audio\ quality\ specification,\ insert\ a -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ value\ between\ 0\ (better)\ and\ 9\ (worse)\ for\ VBR\ or\ a -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ specific\ bitrate\ like\ 128K\ (default\ 5) --k,\ --keep-video\ \ \ \ \ \ \ \ \ keeps\ the\ video\ file\ on\ disk\ after\ the\ post- -\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ processing;\ the\ video\ is\ erased\ by\ default -\f[] -.fi -.SH CONFIGURATION -.PP -You can configure youtube-dl by placing default arguments (such as -\f[C]--extract-audio\ --no-mtime\f[] to always extract the audio and not -copy the mtime) into \f[C]/etc/youtube-dl.conf\f[] and/or -\f[C]~/.local/config/youtube-dl.conf\f[]. -.SH OUTPUT TEMPLATE -.PP -The \f[C]-o\f[] option allows users to indicate a template for the -output file names. -The basic usage is not to set any template arguments when downloading a -single file, like in -\f[C]youtube-dl\ -o\ funny_video.flv\ "http://some/video"\f[]. -However, it may contain special sequences that will be replaced when -downloading each video. -The special sequences have the format \f[C]%(NAME)s\f[]. -To clarify, that is a percent symbol followed by a name in parenthesis, -followed by a lowercase S. -Allowed names are: -.IP \[bu] 2 -\f[C]id\f[]: The sequence will be replaced by the video identifier. -.IP \[bu] 2 -\f[C]url\f[]: The sequence will be replaced by the video URL. -.IP \[bu] 2 -\f[C]uploader\f[]: The sequence will be replaced by the nickname of the -person who uploaded the video. -.IP \[bu] 2 -\f[C]upload_date\f[]: The sequence will be replaced by the upload date -in YYYYMMDD format. -.IP \[bu] 2 -\f[C]title\f[]: The sequence will be replaced by the video title. -.IP \[bu] 2 -\f[C]ext\f[]: The sequence will be replaced by the appropriate extension -(like flv or mp4). -.IP \[bu] 2 -\f[C]epoch\f[]: The sequence will be replaced by the Unix epoch when -creating the file. -.IP \[bu] 2 -\f[C]autonumber\f[]: The sequence will be replaced by a five-digit -number that will be increased with each download, starting at zero. -.PP -The current default template is \f[C]%(id)s.%(ext)s\f[], but that will -be switchted to \f[C]%(title)s-%(id)s.%(ext)s\f[] (which can be -requested with \f[C]-t\f[] at the moment). -.PP -In some cases, you don\[aq]t want special characters such as 中, spaces, -or &, such as when transferring the downloaded filename to a Windows -system or the filename through an 8bit-unsafe channel. -In these cases, add the \f[C]--restrict-filenames\f[] flag to get a -shorter title: -.IP -.nf -\f[C] -$\ youtube-dl\ --get-filename\ -o\ "%(title)s.%(ext)s"\ BaW_jenozKc -youtube-dl\ test\ video\ \[aq]\[aq]_ä↭𝕐.mp4\ \ \ \ #\ All\ kinds\ of\ weird\ characters -$\ youtube-dl\ --get-filename\ -o\ "%(title)s.%(ext)s"\ BaW_jenozKc\ --restrict-filenames -youtube-dl_test_video_.mp4\ \ \ \ \ \ \ \ \ \ #\ A\ simple\ file\ name -\f[] -.fi -.SH FAQ -.SS Can you please put the -b option back? -.PP -Most people asking this question are not aware that youtube-dl now -defaults to downloading the highest available quality as reported by -YouTube, which will be 1080p or 720p in some cases, so you no longer -need the -b option. -For some specific videos, maybe YouTube does not report them to be -available in a specific high quality format you\[aq]\[aq]re interested -in. -In that case, simply request it with the -f option and youtube-dl will -try to download it. -.SS I get HTTP error 402 when trying to download a video. What\[aq]s -this? -.PP -Apparently YouTube requires you to pass a CAPTCHA test if you download -too much. -We\[aq]\[aq]re considering to provide a way to let you solve the -CAPTCHA (https://github.com/rg3/youtube-dl/issues/154), but at the -moment, your best course of action is pointing a webbrowser to the -youtube URL, solving the CAPTCHA, and restart youtube-dl. -.SS I have downloaded a video but how can I play it? -.PP -Once the video is fully downloaded, use any video player, such as -vlc (http://www.videolan.org) or mplayer (http://www.mplayerhq.hu/). -.SS The links provided by youtube-dl -g are not working anymore -.PP -The URLs youtube-dl outputs require the downloader to have the correct -cookies. -Use the \f[C]--cookies\f[] option to write the required cookies into a -file, and advise your downloader to read cookies from that file. -Some sites also require a common user agent to be used, use -\f[C]--dump-user-agent\f[] to see the one in use by youtube-dl. -.SS ERROR: no fmt_url_map or conn information found in video info -.PP -youtube has switched to a new video info format in July 2011 which is -not supported by old versions of youtube-dl. -You can update youtube-dl with \f[C]sudo\ youtube-dl\ --update\f[]. -.SS ERROR: unable to download video -.PP -youtube requires an additional signature since September 2012 which is -not supported by old versions of youtube-dl. -You can update youtube-dl with \f[C]sudo\ youtube-dl\ --update\f[]. -.SS SyntaxError: Non-ASCII character -.PP -The error -.IP -.nf -\f[C] -File\ "youtube-dl",\ line\ 2 -SyntaxError:\ Non-ASCII\ character\ \[aq]\\x93\[aq]\ ... -\f[] -.fi -.PP -means you\[aq]re using an outdated version of Python. -Please update to Python 2.6 or 2.7. -.SS What is this binary file? Where has the code gone? -.PP -Since June 2012 (#342) youtube-dl is packed as an executable zipfile, -simply unzip it (might need renaming to \f[C]youtube-dl.zip\f[] first on -some systems) or clone the git repository, as laid out above. -If you modify the code, you can run it by executing the -\f[C]__main__.py\f[] file. -To recompile the executable, run \f[C]make\ youtube-dl\f[]. -.SS The exe throws a \f[I]Runtime error from Visual C++\f[] -.PP -To run the exe you need to install first the Microsoft Visual C++ 2008 -Redistributable -Package (http://www.microsoft.com/en-us/download/details.aspx?id=29). -.SH COPYRIGHT -.PP -youtube-dl is released into the public domain by the copyright holders. -.PP -This README file was originally written by Daniel Bolton -() and is likewise released into the public -domain. -.SH BUGS -.PP -Bugs and suggestions should be reported at: - -.PP -Please include: -.IP \[bu] 2 -Your exact command line, like -\f[C]youtube-dl\ -t\ "http://www.youtube.com/watch?v=uHlDtZ6Oc3s&feature=channel_video_title"\f[]. -A common mistake is not to escape the \f[C]&\f[]. -Putting URLs in quotes should solve this problem. -.IP \[bu] 2 -The output of \f[C]youtube-dl\ --version\f[] -.IP \[bu] 2 -The output of \f[C]python\ --version\f[] -.IP \[bu] 2 -The name and version of your Operating System ("Ubuntu 11.04 x64" or -"Windows 7 x64" is usually enough). -.PP -For discussions, join us in the irc channel #youtube-dl on freenode. diff --git a/youtube-dl.bash-completion b/youtube-dl.bash-completion deleted file mode 100644 index 0f62e74..0000000 --- a/youtube-dl.bash-completion +++ /dev/null @@ -1,14 +0,0 @@ -__youtube-dl() -{ - local cur prev opts - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - opts="--help --version --update --ignore-errors --rate-limit --retries --buffer-size --no-resize-buffer --dump-user-agent --user-agent --list-extractors --playlist-start --playlist-end --match-title --reject-title --max-downloads --title --id --literal --auto-number --output --restrict-filenames --batch-file --no-overwrites --continue --no-continue --cookies --no-part --no-mtime --write-description --write-info-json --quiet --simulate --skip-download --get-url --get-title --get-thumbnail --get-description --get-filename --get-format --no-progress --console-title --verbose --format --all-formats --prefer-free-formats --max-quality --list-formats --write-srt --srt-lang --username --password --netrc --extract-audio --audio-format --audio-quality --keep-video" - - if [[ ${cur} == * ]] ; then - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - fi -} - -complete -F __youtube-dl youtube-dl diff --git a/youtube-dl.exe b/youtube-dl.exe new file mode 100644 index 0000000..45eee04 Binary files /dev/null and b/youtube-dl.exe differ diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index b43acd1..8a3bdf2 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -4,8 +4,10 @@ from __future__ import absolute_import import math +import io import os import re +import shutil import socket import subprocess import sys @@ -16,6 +18,7 @@ if os.name == 'nt': import ctypes from .utils import * +from .InfoExtractors import get_info_extractor class FileDownloader(object): @@ -51,6 +54,7 @@ class FileDownloader(object): quiet: Do not print messages to stdout. forceurl: Force printing final URL. forcetitle: Force printing title. + forceid: Force printing ID. forcethumbnail: Force printing thumbnail URL. forcedescription: Force printing description. forcefilename: Force printing final filename. @@ -77,9 +81,18 @@ class FileDownloader(object): updatetime: Use the Last-modified header to set output file timestamps. writedescription: Write the video description to a .description 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 test: Download only first bytes to test the downloader. + keepvideo: Keep the video file after post-processing + min_filesize: Skip files smaller 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 @@ -93,13 +106,14 @@ class FileDownloader(object): """Create a FileDownloader object with the given options.""" self._ies = [] self._pps = [] + self._progress_hooks = [] self._download_retcode = 0 self._num_downloads = 0 self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)] self.params = params if '%(stitle)s' in self.params['outtmpl']: - self.to_stderr(u'WARNING: %(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.') + self.report_warning(u'%(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.') @staticmethod def format_bytes(bytes): @@ -111,7 +125,7 @@ class FileDownloader(object): exponent = 0 else: 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) return '%.2f%s' % (converted, suffix) @@ -203,27 +217,66 @@ class FileDownloader(object): # already of type unicode() ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message)) elif 'TERM' in os.environ: - sys.stderr.write('\033]0;%s\007' % message.encode(preferredencoding())) + self.to_screen('\033]0;%s\007' % message, skip_eol=True) def fixed_template(self): """Checks if the output template is fixed.""" return (re.search(u'(?u)%\\(.+?\\)s', self.params['outtmpl']) is None) - def trouble(self, message=None): + def trouble(self, message=None, tb=None): """Determine action to take when a download problem appears. Depending on if the downloader has been configured to ignore download errors or not, this method may throw an exception or not when errors are found, after printing the message. + + tb, if given, is additional traceback information. """ if message is not None: self.to_stderr(message) if self.params.get('verbose'): - self.to_stderr(u''.join(traceback.format_list(traceback.extract_stack()))) + if tb is None: + if sys.exc_info()[0]: # if .trouble has been called from an except block + 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) 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 + def report_warning(self, message): + ''' + Print the message to stderr, it will be prefixed with 'WARNING:' + If stderr is a tty file the 'WARNING:' will be colored + ''' + if sys.stderr.isatty() and os.name != 'nt': + _msg_header=u'\033[0;33mWARNING:\033[0m' + else: + _msg_header=u'WARNING:' + warning_message=u'%s %s' % (_msg_header,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): """Sleep if the download speed is over the rate limit.""" rate_limit = self.params.get('ratelimit', None) @@ -255,7 +308,7 @@ class FileDownloader(object): return os.rename(encodeFilename(old_filename), encodeFilename(new_filename)) 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): """Try to set the last-modified time of the given file.""" @@ -279,9 +332,9 @@ class FileDownloader(object): """ Report that the description file is being written """ 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 """ - 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): """ Report that the metadata file has been written """ @@ -295,8 +348,13 @@ class FileDownloader(object): """Report download progress.""" if self.params.get('noprogress', False): return - self.to_screen(u'\r[download] %s of %s at %s ETA %s' % - (percent_str, data_len_str, speed_str, eta_str), skip_eol=True) + clear_line = (u'\x1b[K' if sys.stderr.isatty() and os.name != 'nt' else u'') + if self.params.get('progress_with_newline', False): + self.to_screen(u'[download] %s of %s at %s ETA %s' % + (percent_str, data_len_str, speed_str, eta_str)) + else: + self.to_screen(u'\r%s[download] %s of %s at %s ETA %s' % + (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' % (percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip())) @@ -336,7 +394,13 @@ class FileDownloader(object): template_dict = dict(info_dict) 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( u'NA' if v is None else compat_str(v), @@ -346,8 +410,11 @@ class FileDownloader(object): filename = self.params['outtmpl'] % template_dict return filename - except (ValueError, KeyError) as err: - self.trouble(u'ERROR: invalid system charset or erroneous output template') + except KeyError as err: + self.report_error(u'Erroneous output template') + return None + except ValueError as err: + self.report_error(u'Insufficient system charset ' + repr(preferredencoding())) return None def _match_entry(self, info_dict): @@ -356,18 +423,136 @@ class FileDownloader(object): title = info_dict['title'] matchtitle = self.params.get('matchtitle', False) if matchtitle: - matchtitle = matchtitle.decode('utf8') if not re.search(matchtitle, title, re.IGNORECASE): return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"' rejecttitle = self.params.get('rejecttitle', False) if rejecttitle: - rejecttitle = rejecttitle.decode('utf8') if re.search(rejecttitle, title, re.IGNORECASE): 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 + + def extract_info(self, url, download=True, ie_key=None): + ''' + Returns a list with a dictionary for each video we find. + If 'download', also downloads the videos. + ''' + + 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 + ie_result = { + '_type': 'compat_list', + 'entries': ie_result, + } + 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): + """ + 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': + return self.extract_info(ie_result['url'], download, ie_key=ie_result.get('ie_key')) + 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)) + entry['playlist'] = playlist + entry['playlist_index'] = i + playliststart + entry_result = self.process_ie_result(entry, download=download) + 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): - """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 info_dict['stitle'] = info_dict['title'] @@ -390,6 +575,8 @@ class FileDownloader(object): # Forced printings if self.params.get('forcetitle', False): compat_print(info_dict['title']) + if self.params.get('forceid', False): + compat_print(info_dict['id']) if self.params.get('forceurl', False): compat_print(info_dict['url']) if self.params.get('forcethumbnail', False) and 'thumbnail' in info_dict: @@ -410,59 +597,81 @@ class FileDownloader(object): try: 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) 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 if self.params.get('writedescription', False): try: descfn = filename + u'.description' self.report_writedescription(descfn) - descfile = open(encodeFilename(descfn), 'wb') - try: - descfile.write(info_dict['description'].encode('utf-8')) - finally: - descfile.close() + with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile: + descfile.write(info_dict['description']) except (OSError, IOError): - self.trouble(u'ERROR: Cannot write description file ' + descfn) + self.report_error(u'Cannot write description file ' + descfn) return 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 # that way it will silently go on when used with unsupporting IE - try: - srtfn = filename.rsplit('.', 1)[0] + u'.srt' - self.report_writesubtitles(srtfn) - srtfile = open(encodeFilename(srtfn), 'wb') + subtitle = info_dict['subtitles'][0] + (sub_error, sub_lang, sub) = subtitle + sub_format = self.params.get('subtitlesformat') + if sub_error: + self.report_warning("Some error while getting the subtitles") + else: try: - srtfile.write(info_dict['subtitles'].encode('utf-8')) - finally: - srtfile.close() - except (OSError, IOError): - self.trouble(u'ERROR: Cannot write subtitles file ' + descfn) - 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): infofn = filename + u'.info.json' self.report_writeinfojson(infofn) try: - json.dump - except (NameError,AttributeError): - self.trouble(u'ERROR: No JSON encoder found. Update to Python 2.6+, setup a json module, or leave out --write-info-json.') - return - try: - infof = open(encodeFilename(infofn), 'wb') - try: - json_info_dict = dict((k,v) for k,v in info_dict.iteritems() if not k in ('urlhandle',)) - json.dump(json_info_dict, infof) - finally: - infof.close() + 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)) 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 + 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 self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)): success = True @@ -472,17 +681,17 @@ class FileDownloader(object): except (OSError, IOError) as err: raise UnavailableVideoError() 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 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 if success: try: self.post_process(filename, info_dict) except (PostProcessingError) as err: - self.trouble(u'ERROR: postprocessing: %s' % str(err)) + self.report_error(u'postprocessing: %s' % str(err)) return def download(self, url_list): @@ -491,62 +700,61 @@ class FileDownloader(object): raise SameFileError(self.params['outtmpl']) for url in url_list: - suitable_found = False - for ie in self._ies: - # Go to next InfoExtractor if not suitable - if not ie.suitable(url): - continue - - # Warn if the _WORKING attribute is False - if not ie.working(): - self.trouble(u'WARNING: 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 - videos = ie.extract(url) - 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) + try: + #It also downloads the videos + videos = self.extract_info(url) + except UnavailableVideoError: + self.report_error(u'unable to download video') + except MaxDownloadsReached: + self.to_screen(u'[info] Maximum number of downloaded files reached.') + raise return self._download_retcode def post_process(self, filename, ie_info): - """Run the postprocessing chain on the given file.""" + """Run all the postprocessors on the given file.""" info = dict(ie_info) info['filepath'] = filename + keep_video = None for pp in self._pps: - info = pp.run(info) - if info is None: - break + try: + keep_video_wish,new_info = pp.run(info) + if keep_video_wish is not None: + if keep_video_wish: + keep_video = keep_video_wish + elif keep_video is None: + # No clear decision yet, let IE decide + keep_video = keep_video_wish + except PostProcessingError as e: + self.to_stderr(u'ERROR: ' + e.msg) + if keep_video is False and not self.params.get('keepvideo', False): + try: + self.to_screen(u'Deleting original file %s (pass -k to keep)' % filename) + os.remove(encodeFilename(filename)) + except (IOError, OSError): + self.report_warning(u'Unable to remove downloaded video file') - def _download_with_rtmpdump(self, filename, url, player_url): + def _download_with_rtmpdump(self, filename, url, player_url, page_url, play_path): self.report_destination(filename) tmpfilename = self.temp_name(filename) # Check for rtmpdump first try: - subprocess.call(['rtmpdump', '-h'], stdout=(file(os.path.devnull, 'w')), stderr=subprocess.STDOUT) + subprocess.call(['rtmpdump', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT) 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 # Download using rtmpdump. rtmpdump returns exit code 2 when # the connection was interrumpted and resuming appears to be # possible. This is part of rtmpdump's normal usage, AFAIK. - basic_args = ['rtmpdump', '-q'] + [[], ['-W', player_url]][player_url is not None] + ['-r', url, '-o', tmpfilename] + basic_args = ['rtmpdump', '-q', '-r', url, '-o', tmpfilename] + if player_url is not None: + basic_args += ['-W', player_url] + if page_url is not None: + basic_args += ['--pageUrl', page_url] + if play_path is not None: + basic_args += ['-y', play_path] args = basic_args + [[], ['-e', '-k', '1']][self.params.get('continuedl', False)] if self.params.get('verbose', False): try: @@ -570,31 +778,47 @@ class FileDownloader(object): retval = 0 break if retval == 0: - self.to_screen(u'\r[rtmpdump] %s bytes' % os.path.getsize(encodeFilename(tmpfilename))) + fsize = os.path.getsize(encodeFilename(tmpfilename)) + self.to_screen(u'\r[rtmpdump] %s bytes' % fsize) self.try_rename(tmpfilename, filename) + self._hook_progress({ + 'downloaded_bytes': fsize, + 'total_bytes': fsize, + 'filename': filename, + 'status': 'finished', + }) return True 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 def _do_download(self, filename, info_dict): url = info_dict['url'] - player_url = info_dict.get('player_url', None) # Check file already present if self.params.get('continuedl', False) and os.path.isfile(encodeFilename(filename)) and not self.params.get('nopart', False): self.report_file_already_downloaded(filename) + self._hook_progress({ + 'filename': filename, + 'status': 'finished', + }) return True # Attempt to download using rtmpdump if url.startswith('rtmp'): - return self._download_with_rtmpdump(filename, url, player_url) + return self._download_with_rtmpdump(filename, url, + info_dict.get('player_url', None), + info_dict.get('page_url', None), + info_dict.get('play_path', None)) tmpfilename = self.temp_name(filename) stream = None # Do not include the Accept-Encoding header headers = {'Youtubedl-no-compression': 'True'} + if 'user_agent' in info_dict: + headers['Youtubedl-user-agent'] = info_dict['user_agent'] basic_request = compat_urllib_request.Request(url, None, headers) request = compat_urllib_request.Request(url, None, headers) @@ -651,6 +875,10 @@ class FileDownloader(object): # the one in the hard drive. self.report_file_already_downloaded(filename) self.try_rename(tmpfilename, filename) + self._hook_progress({ + 'filename': filename, + 'status': 'finished', + }) return True else: # The length does not match, we start the download over @@ -663,12 +891,21 @@ class FileDownloader(object): self.report_retry(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 data_len = data.info().get('Content-length', None) if data_len is not None: data_len = int(data_len) + resume_len + min_data_len = self.params.get("min_filesize", None) + max_data_len = self.params.get("max_filesize", None) + if min_data_len is not None and data_len < min_data_len: + self.to_screen(u'\r[download] File is smaller than min-filesize (%s bytes < %s bytes). Aborting.' % (data_len, min_data_len)) + return False + if max_data_len is not None and data_len > max_data_len: + self.to_screen(u'\r[download] File is larger than max-filesize (%s bytes > %s bytes). Aborting.' % (data_len, max_data_len)) + return False + data_len_str = self.format_bytes(data_len) byte_counter = 0 + resume_len block_size = self.params.get('buffersize', 1024) @@ -690,12 +927,13 @@ class FileDownloader(object): filename = self.undo_temp_name(tmpfilename) self.report_destination(filename) 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 try: stream.write(data_block) 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 if not self.params.get('noresizebuffer', False): block_size = self.best_block_size(after - before, len(data_block)) @@ -709,11 +947,20 @@ class FileDownloader(object): eta_str = self.calc_eta(start, time.time(), data_len - resume_len, byte_counter - resume_len) self.report_progress(percent_str, data_len_str, speed_str, eta_str) + self._hook_progress({ + 'downloaded_bytes': byte_counter, + 'total_bytes': data_len, + 'tmpfilename': tmpfilename, + 'filename': filename, + 'status': 'downloading', + }) + # Apply rate limit self.slow_down(start, byte_counter - resume_len) 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 stream.close() self.report_finish() @@ -725,4 +972,31 @@ class FileDownloader(object): if self.params.get('updatetime', True): info_dict['filetime'] = self.try_utime(filename, data.info().get('last-modified', None)) + self._hook_progress({ + 'downloaded_bytes': byte_counter, + 'total_bytes': byte_counter, + 'filename': filename, + 'status': 'finished', + }) + return True + + def _hook_progress(self, status): + for ph in self._progress_hooks: + ph(status) + + def add_progress_hook(self, ph): + """ ph gets called on download progress, with a dictionary with the entries + * filename: The final filename + * status: One of "downloading" and "finished" + + It can also have some of the following entries: + + * downloaded_bytes: Bytes on disks + * total_bytes: Total bytes, None if unknown + * tmpfilename: The filename we're currently writing to + + Hooks are guaranteed to be called at least once (with status "finished") + if the download is successful. + """ + self._progress_hooks.append(ph) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py old mode 100644 new mode 100755 index 5ebe64b..4bcd8b9 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3,7 +3,9 @@ from __future__ import absolute_import +import base64 import datetime +import itertools import netrc import os import re @@ -13,6 +15,7 @@ import email.utils import xml.etree.ElementTree import random import math +import operator from .utils import * @@ -23,7 +26,7 @@ class InfoExtractor(object): Information extractors are the classes that, given a URL, extract information about the video (or videos) the URL refers to. This information includes the real video URL, the video title, author and - others. The information is stored in a dictionary which is then + others. The information is stored in a dictionary which is then passed to the FileDownloader. The FileDownloader processes this information possibly downloading the video to the file system, among other possible outcomes. @@ -32,8 +35,6 @@ class InfoExtractor(object): id: Video identifier. url: Final video URL. - uploader: Nickname of the video uploader, unescaped. - upload_date: Video upload date (YYYYMMDD). title: Video title, unescaped. ext: Video filename extension. @@ -42,8 +43,12 @@ class InfoExtractor(object): format: The video format, defaults to ext (used for --get-format) thumbnail: Full URL to a video thumbnail image. description: One-line video description. + uploader: Full name of the video uploader. + upload_date: Video upload date (YYYYMMDD). + uploader_id: Nickname or id of the video uploader. + location: Physical location of the video. player_url: SWF Player URL (used for rtmpdump). - subtitles: The .srt file contents. + subtitles: The subtitle file contents. urlhandle: [internal] The urlHandle to be used to download the file, like returned by urllib.request.urlopen @@ -69,13 +74,15 @@ class InfoExtractor(object): self._ready = False self.set_downloader(downloader) - def suitable(self, url): + @classmethod + def suitable(cls, url): """Receives a URL and returns True if suitable for this IE.""" - return re.match(self._VALID_URL, url) is not None + return re.match(cls._VALID_URL, url) is not None - def working(self): + @classmethod + def working(cls): """Getter method for _WORKING.""" - return self._WORKING + return cls._WORKING def initialize(self): """Initializes an instance (authentication, etc).""" @@ -100,6 +107,87 @@ class InfoExtractor(object): """Real extraction process. Redefine in subclasses.""" pass + @property + def IE_NAME(self): + return type(self).__name__[:-2] + + def _request_webpage(self, url_or_request, video_id, note=None, errnote=None): + """ Returns the response handle """ + if note is None: + self.report_download_webpage(video_id) + elif note is not False: + self.to_screen(u'%s: %s' % (video_id, note)) + try: + return compat_urllib_request.urlopen(url_or_request) + except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: + if errnote is None: + errnote = u'Unable to download webpage' + raise ExtractorError(u'%s: %s' % (errnote, compat_str(err)), sys.exc_info()[2]) + + def _download_webpage_handle(self, url_or_request, video_id, note=None, errnote=None): + """ Returns a tuple (page content as string, URL handle) """ + urlh = self._request_webpage(url_or_request, video_id, note, errnote) + content_type = urlh.headers.get('Content-Type', '') + m = re.match(r'[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+\s*;\s*charset=(.+)', content_type) + if m: + encoding = m.group(1) + else: + encoding = 'utf-8' + webpage_bytes = urlh.read() + if self._downloader.params.get('dump_intermediate_pages', False): + try: + url = url_or_request.get_full_url() + except AttributeError: + url = url_or_request + self.to_screen(u'Dumping request to ' + url) + dump = base64.b64encode(webpage_bytes).decode('ascii') + self._downloader.to_screen(dump) + content = webpage_bytes.decode(encoding, 'replace') + return (content, urlh) + + def _download_webpage(self, url_or_request, video_id, note=None, errnote=None): + """ Returns the data of the page as a string """ + return self._download_webpage_handle(url_or_request, video_id, note, errnote)[0] + + def to_screen(self, msg): + """Print msg to screen, prefixing it with '[ie_name]'""" + self._downloader.to_screen(u'[%s] %s' % (self.IE_NAME, msg)) + + def report_extraction(self, id_or_name): + """Report information extraction.""" + self.to_screen(u'%s: Extracting information' % id_or_name) + + def report_download_webpage(self, video_id): + """Report webpage download.""" + self.to_screen(u'%s: Downloading webpage' % video_id) + + def report_age_confirmation(self): + """Report attempt to confirm age.""" + self.to_screen(u'Confirming age') + + #Methods for following #608 + #They set the correct value of the '_type' key + def video_result(self, video_info): + """Returns a video""" + video_info['_type'] = 'video' + return video_info + def url_result(self, url, ie=None): + """Returns a url that points to a page that should be processed""" + #TODO: ie should be the class used for getting the info + video_info = {'_type': 'url', + 'url': url, + 'ie_key': ie} + return video_info + def playlist_result(self, entries, playlist_id=None, playlist_title=None): + """Returns a playlist""" + video_info = {'_type': 'playlist', + 'entries': entries} + if playlist_id: + video_info['id'] = playlist_id + if playlist_title: + video_info['title'] = playlist_title + return video_info + class YoutubeIE(InfoExtractor): """Information extractor for youtube.com.""" @@ -110,13 +198,12 @@ class YoutubeIE(InfoExtractor): (?:youtu\.be/|(?:\w+\.)?youtube(?:-nocookie)?\.com/| tube\.majestyc\.net/) # the various hostnames, with wildcard subdomains (?:.*?\#/)? # handle anchor (#/) redirect urls - (?!view_play_list|my_playlists|artist|playlist) # ignore playlist URLs (?: # the various things that can precede the ID: (?:(?:v|embed|e)/) # v/ or embed/ or e/ |(?: # or the v= param in all its forms (?:watch(?:_popup)?(?:\.php)?)? # preceding watch(_popup|.php) or nothing (like /?v=xxxx) (?:\?|\#!?) # the params delimiter ? or # or #! - (?:.+&)? # any other preceding param (like /?s=tuff&v=xxxx) + (?:.*?&)? # any other preceding param (like /?s=tuff&v=xxxx) v= ) )? # optional -> youtube.com/xxxx is OK @@ -124,8 +211,8 @@ class YoutubeIE(InfoExtractor): ([0-9A-Za-z_-]+) # here is it! the YouTube video ID (?(1).+)? # if we found the ID, everything can follow $""" - _LANG_URL = r'http://www.youtube.com/?hl=en&persist_hl=1&gl=US&persist_gl=1&opt_out_ackd=1' - _LOGIN_URL = 'https://www.youtube.com/signup?next=/&gl=US&hl=en' + _LANG_URL = r'https://www.youtube.com/?hl=en&persist_hl=1&gl=US&persist_gl=1&opt_out_ackd=1' + _LOGIN_URL = 'https://accounts.google.com/ServiceLogin' _AGE_URL = 'http://www.youtube.com/verify_age?next_url=/&gl=US&hl=en' _NEXT_URL_RE = r'[\?&]next_url=([^&]+)' _NETRC_MACHINE = 'youtube' @@ -159,65 +246,125 @@ class YoutubeIE(InfoExtractor): '44': '480x854', '45': '720x1280', '46': '1080x1920', - } + } IE_NAME = u'youtube' - def suitable(self, url): + @classmethod + def suitable(cls, url): """Receives a URL and returns True if suitable for this IE.""" - return re.match(self._VALID_URL, url, re.VERBOSE) is not None + if YoutubePlaylistIE.suitable(url): return False + return re.match(cls._VALID_URL, url, re.VERBOSE) is not None def report_lang(self): """Report attempt to set language.""" - self._downloader.to_screen(u'[youtube] Setting language') + self.to_screen(u'Setting language') def report_login(self): """Report attempt to log in.""" - self._downloader.to_screen(u'[youtube] Logging in') - - def report_age_confirmation(self): - """Report attempt to confirm age.""" - self._downloader.to_screen(u'[youtube] Confirming age') + self.to_screen(u'Logging in') def report_video_webpage_download(self, video_id): """Report attempt to download video webpage.""" - self._downloader.to_screen(u'[youtube] %s: Downloading video webpage' % video_id) + self.to_screen(u'%s: Downloading video webpage' % video_id) def report_video_info_webpage_download(self, video_id): """Report attempt to download video info webpage.""" - self._downloader.to_screen(u'[youtube] %s: Downloading video info webpage' % video_id) + self.to_screen(u'%s: Downloading video info webpage' % video_id) def report_video_subtitles_download(self, video_id): """Report attempt to download video info webpage.""" - self._downloader.to_screen(u'[youtube] %s: Downloading video subtitles' % video_id) + self.to_screen(u'%s: Checking available subtitles' % video_id) + + def report_video_subtitles_request(self, video_id, sub_lang, format): + """Report attempt to download video info webpage.""" + self.to_screen(u'%s: Downloading video subtitles for %s.%s' % (video_id, sub_lang, format)) + + def report_video_subtitles_available(self, video_id, sub_lang_list): + """Report available subtitles.""" + sub_lang = ",".join(list(sub_lang_list.keys())) + self.to_screen(u'%s: Available subtitles for video: %s' % (video_id, sub_lang)) def report_information_extraction(self, video_id): """Report attempt to extract video information.""" - self._downloader.to_screen(u'[youtube] %s: Extracting video information' % video_id) + self.to_screen(u'%s: Extracting video information' % video_id) def report_unavailable_format(self, video_id, format): """Report extracted video URL.""" - self._downloader.to_screen(u'[youtube] %s: Format %s not available' % (video_id, format)) + self.to_screen(u'%s: Format %s not available' % (video_id, format)) def report_rtmp_download(self): """Indicate the download will use the RTMP protocol.""" - self._downloader.to_screen(u'[youtube] RTMP download detected') - - def _closed_captions_xml_to_srt(self, xml_string): - srt = '' - texts = re.findall(r'([^<]+)', xml_string, re.MULTILINE) - # TODO parse xml instead of regex - for n, (start, dur_tag, dur, caption) in enumerate(texts): - if not dur: dur = '4' - start = float(start) - end = start + float(dur) - start = "%02i:%02i:%02i,%03i" %(start/(60*60), start/60%60, start%60, start%1*1000) - end = "%02i:%02i:%02i,%03i" %(end/(60*60), end/60%60, end%60, end%1*1000) - caption = unescapeHTML(caption) - caption = unescapeHTML(caption) # double cycle, intentional - srt += str(n+1) + '\n' - srt += start + ' --> ' + end + '\n' - srt += caption + '\n\n' - return srt + self.to_screen(u'RTMP download detected') + + def _get_available_subtitles(self, video_id): + self.report_video_subtitles_download(video_id) + request = compat_urllib_request.Request('http://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id) + try: + sub_list = compat_urllib_request.urlopen(request).read().decode('utf-8') + except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: + return (u'unable to download video subtitles: %s' % compat_str(err), None) + sub_lang_list = re.findall(r'name="([^"]*)"[^>]+lang_code="([\w\-]+)"', sub_list) + sub_lang_list = dict((l[1], l[0]) for l in sub_lang_list) + if not sub_lang_list: + return (u'video doesn\'t have subtitles', None) + return sub_lang_list + + def _list_available_subtitles(self, video_id): + sub_lang_list = self._get_available_subtitles(video_id) + self.report_video_subtitles_available(video_id, sub_lang_list) + + def _request_subtitle(self, sub_lang, sub_name, video_id, format): + """ + Return tuple: + (error_message, sub_lang, sub) + """ + self.report_video_subtitles_request(video_id, sub_lang, format) + params = compat_urllib_parse.urlencode({ + 'lang': sub_lang, + 'name': sub_name, + 'v': video_id, + 'fmt': format, + }) + url = 'http://www.youtube.com/api/timedtext?' + params + try: + sub = compat_urllib_request.urlopen(url).read().decode('utf-8') + except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: + return (u'unable to download video subtitles: %s' % compat_str(err), None, None) + if not sub: + return (u'Did not fetch video subtitles', None, None) + return (None, sub_lang, sub) + + def _extract_subtitle(self, video_id): + """ + Return a list with a tuple: + [(error_message, sub_lang, sub)] + """ + sub_lang_list = self._get_available_subtitles(video_id) + sub_format = self._downloader.params.get('subtitlesformat') + if isinstance(sub_lang_list,tuple): #There was some error, it didn't get the available subtitles + return [(sub_lang_list[0], None, None)] + if self._downloader.params.get('subtitleslang', False): + sub_lang = self._downloader.params.get('subtitleslang') + elif 'en' in sub_lang_list: + sub_lang = 'en' + else: + sub_lang = list(sub_lang_list.keys())[0] + if not sub_lang in sub_lang_list: + return [(u'no closed captions found in the specified language "%s"' % sub_lang, None, None)] + + subtitle = self._request_subtitle(sub_lang, sub_lang_list[sub_lang].encode('utf-8'), video_id, sub_format) + return [subtitle] + + def _extract_all_subtitles(self, video_id): + sub_lang_list = self._get_available_subtitles(video_id) + sub_format = self._downloader.params.get('subtitlesformat') + if isinstance(sub_lang_list,tuple): #There was some error, it didn't get the available subtitles + return [(sub_lang_list[0], None, None)] + subtitles = [] + for sub_lang in sub_lang_list: + subtitle = self._request_subtitle(sub_lang, sub_lang_list[sub_lang].encode('utf-8'), video_id, sub_format) + subtitles.append(subtitle) + return subtitles def _print_formats(self, formats): print('Available formats:') @@ -245,7 +392,7 @@ class YoutubeIE(InfoExtractor): else: raise netrc.NetrcParseError('No authenticators for %s' % self._NETRC_MACHINE) except (IOError, netrc.NetrcParseError) as err: - self._downloader.to_stderr(u'WARNING: parsing .netrc: %s' % compat_str(err)) + self._downloader.report_warning(u'parsing .netrc: %s' % compat_str(err)) return # Set language @@ -254,30 +401,65 @@ class YoutubeIE(InfoExtractor): self.report_lang() compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.to_stderr(u'WARNING: unable to set language: %s' % compat_str(err)) + self._downloader.report_warning(u'unable to set language: %s' % compat_str(err)) return # No authentication to be performed if username is None: return + request = compat_urllib_request.Request(self._LOGIN_URL) + try: + login_page = compat_urllib_request.urlopen(request).read().decode('utf-8') + except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: + self._downloader.report_warning(u'unable to fetch login page: %s' % compat_str(err)) + return + + galx = None + dsh = None + match = re.search(re.compile(r']* name="loginForm"', login_results) is not None: - self._downloader.to_stderr(u'WARNING: unable to log in: bad username or password') + login_results = compat_urllib_request.urlopen(request).read().decode('utf-8') + if re.search(r'(?i)]* id="gaia_loginform"', login_results) is not None: + self._downloader.report_warning(u'unable to log in: bad username or password') return except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.to_stderr(u'WARNING: unable to log in: %s' % compat_str(err)) + self._downloader.report_warning(u'unable to log in: %s' % compat_str(err)) return # Confirm age @@ -288,32 +470,32 @@ class YoutubeIE(InfoExtractor): request = compat_urllib_request.Request(self._AGE_URL, compat_urllib_parse.urlencode(age_form)) try: self.report_age_confirmation() - age_results = compat_urllib_request.urlopen(request).read() + age_results = compat_urllib_request.urlopen(request).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to confirm age: %s' % compat_str(err)) - return + raise ExtractorError(u'Unable to confirm age: %s' % compat_str(err)) + + def _extract_id(self, url): + mobj = re.match(self._VALID_URL, url, re.VERBOSE) + if mobj is None: + raise ExtractorError(u'Invalid URL: %s' % url) + video_id = mobj.group(2) + return video_id def _real_extract(self, url): # Extract original video URL from URL with redirection, like age verification, using next_url parameter mobj = re.search(self._NEXT_URL_RE, url) if mobj: - url = 'http://www.youtube.com/' + compat_urllib_parse.unquote(mobj.group(1)).lstrip('/') - - # Extract video id from URL - mobj = re.match(self._VALID_URL, url, re.VERBOSE) - if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) - return - video_id = mobj.group(2) + url = 'https://www.youtube.com/' + compat_urllib_parse.unquote(mobj.group(1)).lstrip('/') + video_id = self._extract_id(url) # Get video webpage self.report_video_webpage_download(video_id) - request = compat_urllib_request.Request('http://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1' % video_id) + url = 'https://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1' % video_id + request = compat_urllib_request.Request(url) try: video_webpage_bytes = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download video webpage: %s' % compat_str(err)) - return + raise ExtractorError(u'Unable to download video webpage: %s' % compat_str(err)) video_webpage = video_webpage_bytes.decode('utf-8', 'ignore') @@ -327,48 +509,48 @@ class YoutubeIE(InfoExtractor): # Get video info self.report_video_info_webpage_download(video_id) for el_type in ['&el=embedded', '&el=detailpage', '&el=vevo', '']: - video_info_url = ('http://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en' + video_info_url = ('https://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en' % (video_id, el_type)) - request = compat_urllib_request.Request(video_info_url) - try: - video_info_webpage_bytes = compat_urllib_request.urlopen(request).read() - video_info_webpage = video_info_webpage_bytes.decode('utf-8', 'ignore') - video_info = compat_parse_qs(video_info_webpage) - if 'token' in video_info: - break - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download video info webpage: %s' % compat_str(err)) - return + video_info_webpage = self._download_webpage(video_info_url, video_id, + note=False, + errnote='unable to download video info webpage') + video_info = compat_parse_qs(video_info_webpage) + if 'token' in video_info: + break if 'token' not in video_info: if 'reason' in video_info: - self._downloader.trouble(u'ERROR: YouTube said: %s' % video_info['reason'][0]) + raise ExtractorError(u'YouTube said: %s' % video_info['reason'][0]) else: - self._downloader.trouble(u'ERROR: "token" parameter not in video info for unknown reason') - return + raise ExtractorError(u'"token" parameter not in video info for unknown reason') # Check for "rental" videos if 'ypc_video_rental_bar_text' in video_info and 'author' not in video_info: - self._downloader.trouble(u'ERROR: "rental" videos not supported') - return + raise ExtractorError(u'"rental" videos not supported') # Start extracting information self.report_information_extraction(video_id) # uploader if 'author' not in video_info: - self._downloader.trouble(u'ERROR: unable to extract uploader nickname') - return + raise ExtractorError(u'Unable to extract uploader name') video_uploader = compat_urllib_parse.unquote_plus(video_info['author'][0]) + # uploader_id + video_uploader_id = None + mobj = re.search(r'', video_webpage) + if mobj is not None: + video_uploader_id = mobj.group(1) + else: + self._downloader.report_warning(u'unable to extract uploader nickname') + # title if 'title' not in video_info: - self._downloader.trouble(u'ERROR: unable to extract video title') - return + raise ExtractorError(u'Unable to extract video title') video_title = compat_urllib_parse.unquote_plus(video_info['title'][0]) # thumbnail image if 'thumbnail_url' not in video_info: - self._downloader.trouble(u'WARNING: unable to extract video thumbnail') + self._downloader.report_warning(u'unable to extract video thumbnail') video_thumbnail = '' else: # don't panic if we can't find it video_thumbnail = compat_urllib_parse.unquote_plus(video_info['thumbnail_url'][0]) @@ -378,55 +560,42 @@ class YoutubeIE(InfoExtractor): mobj = re.search(r'id="eow-date.*?>(.*?)', video_webpage, re.DOTALL) if mobj is not None: upload_date = ' '.join(re.sub(r'[/,-]', r' ', mobj.group(1)).split()) - format_expressions = ['%d %B %Y', '%B %d %Y', '%b %d %Y'] - for expression in format_expressions: - try: - upload_date = datetime.datetime.strptime(upload_date, expression).strftime('%Y%m%d') - except: - pass + upload_date = unified_strdate(upload_date) # description video_description = get_element_by_id("eow-description", video_webpage) if video_description: video_description = clean_html(video_description) else: - video_description = '' + fd_mobj = re.search(r']+lang_code="([\w\-]+)"', srt_list) - srt_lang_list = dict((l[1], l[0]) for l in srt_lang_list) - if not srt_lang_list: - raise Trouble(u'WARNING: video has no closed captions') - if self._downloader.params.get('subtitleslang', False): - srt_lang = self._downloader.params.get('subtitleslang') - elif 'en' in srt_lang_list: - srt_lang = 'en' - else: - srt_lang = srt_lang_list.keys()[0] - if not srt_lang in srt_lang_list: - raise Trouble(u'WARNING: no closed captions found in the specified language') - request = compat_urllib_request.Request('http://www.youtube.com/api/timedtext?lang=%s&name=%s&v=%s' % (srt_lang, srt_lang_list[srt_lang], video_id)) - try: - srt_xml = compat_urllib_request.urlopen(request).read() - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - raise Trouble(u'WARNING: unable to download video subtitles: %s' % compat_str(err)) - if not srt_xml: - raise Trouble(u'WARNING: unable to download video subtitles') - video_subtitles = self._closed_captions_xml_to_srt(srt_xml.decode('utf-8')) - except Trouble as trouble: - self._downloader.trouble(trouble[0]) + video_subtitles = self._extract_subtitle(video_id) + if video_subtitles: + (sub_error, sub_lang, sub) = video_subtitles[0] + if sub_error: + self._downloader.report_error(sub_error) + + if self._downloader.params.get('allsubtitles', False): + video_subtitles = self._extract_all_subtitles(video_id) + for video_subtitle in video_subtitles: + (sub_error, sub_lang, sub) = video_subtitle + if sub_error: + self._downloader.report_error(sub_error) + + if self._downloader.params.get('listsubtitles', False): + sub_lang_list = self._list_available_subtitles(video_id) + return if 'length_seconds' not in video_info: - self._downloader.trouble(u'WARNING: unable to extract video duration') + self._downloader.report_warning(u'unable to extract video duration') video_duration = '' else: video_duration = compat_urllib_parse.unquote_plus(video_info['length_seconds'][0]) @@ -441,10 +610,13 @@ class YoutubeIE(InfoExtractor): self.report_rtmp_download() video_url_list = [(None, video_info['conn'][0])] elif 'url_encoded_fmt_stream_map' in video_info and len(video_info['url_encoded_fmt_stream_map']) >= 1: - url_data_strs = video_info['url_encoded_fmt_stream_map'][0].split(',') - url_data = [compat_parse_qs(uds) for uds in url_data_strs] - url_data = filter(lambda ud: 'itag' in ud and 'url' in ud, url_data) - url_map = dict((ud['itag'][0], ud['url'][0] + '&signature=' + ud['sig'][0]) for ud in url_data) + url_map = {} + for url_data_str in video_info['url_encoded_fmt_stream_map'][0].split(','): + url_data = compat_parse_qs(url_data_str) + if 'itag' in url_data and 'url' in url_data: + url = url_data['url'][0] + '&signature=' + url_data['sig'][0] + if not 'ratebypass' in url: url += '&ratebypass=yes' + url_map[url_data['itag'][0]] = url format_limit = self._downloader.params.get('format_limit', None) available_formats = self._available_formats_prefer_free if self._downloader.params.get('prefer_free_formats', False) else self._available_formats @@ -454,8 +626,7 @@ class YoutubeIE(InfoExtractor): format_list = available_formats existing_formats = [x for x in format_list if x in url_map] if len(existing_formats) == 0: - self._downloader.trouble(u'ERROR: no known formats available for video') - return + raise ExtractorError(u'no known formats available for video') if self._downloader.params.get('listformats', None): self._print_formats(existing_formats) return @@ -475,11 +646,9 @@ class YoutubeIE(InfoExtractor): video_url_list = [(rf, url_map[rf])] break if video_url_list is None: - self._downloader.trouble(u'ERROR: requested format not available') - return + raise ExtractorError(u'requested format not available') else: - self._downloader.trouble(u'ERROR: no conn or url_encoded_fmt_stream_map information found in video info') - return + raise ExtractorError(u'no conn or url_encoded_fmt_stream_map information found in video info') results = [] for format_param, video_real_url in video_url_list: @@ -493,6 +662,7 @@ class YoutubeIE(InfoExtractor): 'id': video_id, 'url': video_real_url, 'uploader': video_uploader, + 'uploader_id': video_uploader_id, 'upload_date': upload_date, 'title': video_title, 'ext': video_extension, @@ -514,24 +684,9 @@ class MetacafeIE(InfoExtractor): _FILTER_POST = 'http://www.metacafe.com/f/index.php?inputType=filter&controllerGroup=user' IE_NAME = u'metacafe' - def __init__(self, downloader=None): - InfoExtractor.__init__(self, downloader) - def report_disclaimer(self): """Report disclaimer retrieval.""" - self._downloader.to_screen(u'[metacafe] Retrieving disclaimer') - - def report_age_confirmation(self): - """Report attempt to confirm age.""" - self._downloader.to_screen(u'[metacafe] Confirming age') - - def report_download_webpage(self, video_id): - """Report webpage download.""" - self._downloader.to_screen(u'[metacafe] %s: Downloading webpage' % video_id) - - def report_extraction(self, video_id): - """Report information extraction.""" - self._downloader.to_screen(u'[metacafe] %s: Extracting information' % video_id) + self.to_screen(u'Retrieving disclaimer') def _real_initialize(self): # Retrieve disclaimer @@ -540,8 +695,7 @@ class MetacafeIE(InfoExtractor): self.report_disclaimer() disclaimer = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to retrieve disclaimer: %s' % compat_str(err)) - return + raise ExtractorError(u'Unable to retrieve disclaimer: %s' % compat_str(err)) # Confirm age disclaimer_form = { @@ -553,32 +707,23 @@ class MetacafeIE(InfoExtractor): self.report_age_confirmation() disclaimer = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to confirm age: %s' % compat_str(err)) - return + raise ExtractorError(u'Unable to confirm age: %s' % compat_str(err)) def _real_extract(self, url): # Extract id and simplified title from URL mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) - return + raise ExtractorError(u'Invalid URL: %s' % url) video_id = mobj.group(1) # Check if video comes from YouTube mobj2 = re.match(r'^yt-(.*)$', video_id) if mobj2 is not None: - self._downloader.download(['http://www.youtube.com/watch?v=%s' % mobj2.group(1)]) - return + return [self.url_result('http://www.youtube.com/watch?v=%s' % mobj2.group(1), 'Youtube')] # Retrieve video webpage to extract further information - request = compat_urllib_request.Request('http://www.metacafe.com/watch/%s/' % video_id) - try: - self.report_download_webpage(video_id) - webpage = compat_urllib_request.urlopen(request).read() - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable retrieve video webpage: %s' % compat_str(err)) - return + webpage = self._download_webpage('http://www.metacafe.com/watch/%s/' % video_id, video_id) # Extract URL, uploader and title from webpage self.report_extraction(video_id) @@ -597,30 +742,25 @@ class MetacafeIE(InfoExtractor): else: mobj = re.search(r' name="flashvars" value="(.*?)"', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract media URL') - return + raise ExtractorError(u'Unable to extract media URL') vardict = compat_parse_qs(mobj.group(1)) if 'mediaData' not in vardict: - self._downloader.trouble(u'ERROR: unable to extract media URL') - return - mobj = re.search(r'"mediaURL":"(http.*?)","key":"(.*?)"', vardict['mediaData'][0]) + raise ExtractorError(u'Unable to extract media URL') + mobj = re.search(r'"mediaURL":"(?Phttp.*?)",(.*?)"key":"(?P.*?)"', vardict['mediaData'][0]) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract media URL') - return - mediaURL = mobj.group(1).replace('\\/', '/') + raise ExtractorError(u'Unable to extract media URL') + mediaURL = mobj.group('mediaURL').replace('\\/', '/') video_extension = mediaURL[-3:] - video_url = '%s?__gda__=%s' % (mediaURL, mobj.group(2)) + video_url = '%s?__gda__=%s' % (mediaURL, mobj.group('key')) mobj = re.search(r'(?im)(.*) - Video', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract title') - return + raise ExtractorError(u'Unable to extract title') video_title = mobj.group(1).decode('utf-8') mobj = re.search(r'submitter=(.*?);', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract uploader nickname') - return + raise ExtractorError(u'Unable to extract uploader nickname') video_uploader = mobj.group(1) return [{ @@ -632,30 +772,17 @@ class MetacafeIE(InfoExtractor): 'ext': video_extension.decode('utf-8'), }] - class DailymotionIE(InfoExtractor): """Information Extractor for Dailymotion""" _VALID_URL = r'(?i)(?:https?://)?(?:www\.)?dailymotion\.[a-z]{2,3}/video/([^/]+)' IE_NAME = u'dailymotion' - def __init__(self, downloader=None): - InfoExtractor.__init__(self, downloader) - - def report_download_webpage(self, video_id): - """Report webpage download.""" - self._downloader.to_screen(u'[dailymotion] %s: Downloading webpage' % video_id) - - def report_extraction(self, video_id): - """Report information extraction.""" - self._downloader.to_screen(u'[dailymotion] %s: Extracting information' % video_id) - def _real_extract(self, url): # Extract id and simplified title from URL mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) - return + raise ExtractorError(u'Invalid URL: %s' % url) video_id = mobj.group(1).split('_')[0].split('?')[0] @@ -664,34 +791,26 @@ class DailymotionIE(InfoExtractor): # Retrieve video webpage to extract further information request = compat_urllib_request.Request(url) request.add_header('Cookie', 'family_filter=off') - try: - self.report_download_webpage(video_id) - webpage = compat_urllib_request.urlopen(request).read() - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable retrieve video webpage: %s' % compat_str(err)) - return + webpage = self._download_webpage(request, video_id) # Extract URL, uploader and title from webpage self.report_extraction(video_id) mobj = re.search(r'\s*var flashvars = (.*)', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract media URL') - return + raise ExtractorError(u'Unable to extract media URL') flashvars = compat_urllib_parse.unquote(mobj.group(1)) for key in ['hd1080URL', 'hd720URL', 'hqURL', 'sdURL', 'ldURL', 'video_url']: if key in flashvars: max_quality = key - self._downloader.to_screen(u'[dailymotion] Using %s' % key) + self.to_screen(u'Using %s' % key) break else: - self._downloader.trouble(u'ERROR: unable to extract video URL') - return + raise ExtractorError(u'Unable to extract video URL') mobj = re.search(r'"' + max_quality + r'":"(.+?)"', flashvars) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video URL') - return + raise ExtractorError(u'Unable to extract video URL') video_url = compat_urllib_parse.unquote(mobj.group(1)).replace('\\/', '/') @@ -699,9 +818,8 @@ class DailymotionIE(InfoExtractor): mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract title') - return - video_title = unescapeHTML(mobj.group('title').decode('utf-8')) + raise ExtractorError(u'Unable to extract title') + video_title = unescapeHTML(mobj.group('title')) video_uploader = None mobj = re.search(r'(?im)[^<]+?]+?>([^<]+?)', webpage) @@ -709,7 +827,7 @@ class DailymotionIE(InfoExtractor): # lookin for official user mobj_official = re.search(r'', webpage) if mobj_official is None: - self._downloader.trouble(u'WARNING: unable to extract uploader nickname') + self._downloader.report_warning(u'unable to extract uploader nickname') else: video_uploader = mobj_official.group(1) else: @@ -721,159 +839,64 @@ class DailymotionIE(InfoExtractor): video_upload_date = mobj.group(3) + mobj.group(2) + mobj.group(1) return [{ - 'id': video_id.decode('utf-8'), - 'url': video_url.decode('utf-8'), - 'uploader': video_uploader.decode('utf-8'), + 'id': video_id, + 'url': video_url, + 'uploader': video_uploader, 'upload_date': video_upload_date, 'title': video_title, - 'ext': video_extension.decode('utf-8'), - }] - - -class GoogleIE(InfoExtractor): - """Information extractor for video.google.com.""" - - _VALID_URL = r'(?:http://)?video\.google\.(?:com(?:\.au)?|co\.(?:uk|jp|kr|cr)|ca|de|es|fr|it|nl|pl)/videoplay\?docid=([^\&]+).*' - IE_NAME = u'video.google' - - def __init__(self, downloader=None): - InfoExtractor.__init__(self, downloader) - - def report_download_webpage(self, video_id): - """Report webpage download.""" - self._downloader.to_screen(u'[video.google] %s: Downloading webpage' % video_id) - - def report_extraction(self, video_id): - """Report information extraction.""" - self._downloader.to_screen(u'[video.google] %s: Extracting information' % video_id) - - def _real_extract(self, url): - # Extract id from URL - mobj = re.match(self._VALID_URL, url) - if mobj is None: - self._downloader.trouble(u'ERROR: Invalid URL: %s' % url) - return - - video_id = mobj.group(1) - - video_extension = 'mp4' - - # Retrieve video webpage to extract further information - request = compat_urllib_request.Request('http://video.google.com/videoplay?docid=%s&hl=en&oe=utf-8' % video_id) - try: - self.report_download_webpage(video_id) - webpage = compat_urllib_request.urlopen(request).read() - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) - return - - # Extract URL, uploader, and title from webpage - self.report_extraction(video_id) - mobj = re.search(r"download_url:'([^']+)'", webpage) - if mobj is None: - video_extension = 'flv' - mobj = re.search(r"(?i)videoUrl\\x3d(.+?)\\x26", webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract media URL') - return - mediaURL = compat_urllib_parse.unquote(mobj.group(1)) - mediaURL = mediaURL.replace('\\x3d', '\x3d') - mediaURL = mediaURL.replace('\\x26', '\x26') - - video_url = mediaURL - - mobj = re.search(r'(.*)', webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract title') - return - video_title = mobj.group(1).decode('utf-8') - - # Extract video description - mobj = re.search(r'([^<]*)', webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video description') - return - video_description = mobj.group(1).decode('utf-8') - if not video_description: - video_description = 'No description available.' - - # Extract video thumbnail - if self._downloader.params.get('forcethumbnail', False): - request = compat_urllib_request.Request('http://video.google.com/videosearch?q=%s+site:video.google.com&hl=en' % abs(int(video_id))) - try: - webpage = compat_urllib_request.urlopen(request).read() - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) - return - mobj = re.search(r'', webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video thumbnail') - return - video_thumbnail = mobj.group(1) - else: # we need something to pass to process_info - video_thumbnail = '' - - return [{ - 'id': video_id.decode('utf-8'), - 'url': video_url.decode('utf-8'), - 'uploader': None, - 'upload_date': None, - 'title': video_title, - 'ext': video_extension.decode('utf-8'), + 'ext': video_extension, }] class PhotobucketIE(InfoExtractor): """Information extractor for photobucket.com.""" - _VALID_URL = r'(?:http://)?(?:[a-z0-9]+\.)?photobucket\.com/.*[\?\&]current=(.*\.flv)' + # TODO: the original _VALID_URL was: + # r'(?:http://)?(?:[a-z0-9]+\.)?photobucket\.com/.*[\?\&]current=(.*\.flv)' + # Check if it's necessary to keep the old extracion process + _VALID_URL = r'(?:http://)?(?:[a-z0-9]+\.)?photobucket\.com/.*(([\?\&]current=)|_)(?P.*)\.(?P(flv)|(mp4))' IE_NAME = u'photobucket' - def __init__(self, downloader=None): - InfoExtractor.__init__(self, downloader) - - def report_download_webpage(self, video_id): - """Report webpage download.""" - self._downloader.to_screen(u'[photobucket] %s: Downloading webpage' % video_id) - - def report_extraction(self, video_id): - """Report information extraction.""" - self._downloader.to_screen(u'[photobucket] %s: Extracting information' % video_id) - def _real_extract(self, url): # Extract id from URL mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: Invalid URL: %s' % url) - return + raise ExtractorError(u'Invalid URL: %s' % url) - video_id = mobj.group(1) + video_id = mobj.group('id') - video_extension = 'flv' + video_extension = mobj.group('ext') # Retrieve video webpage to extract further information - request = compat_urllib_request.Request(url) - try: - self.report_download_webpage(video_id) - webpage = compat_urllib_request.urlopen(request).read() - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) - return + webpage = self._download_webpage(url, video_id) # Extract URL, uploader, and title from webpage self.report_extraction(video_id) + # We try first by looking the javascript code: + mobj = re.search(r'Pb\.Data\.Shared\.put\(Pb\.Data\.Shared\.MEDIA, (?P.*?)\);', webpage) + if mobj is not None: + info = json.loads(mobj.group('json')) + return [{ + 'id': video_id, + 'url': info[u'downloadUrl'], + 'uploader': info[u'username'], + 'upload_date': datetime.date.fromtimestamp(info[u'creationDate']).strftime('%Y%m%d'), + 'title': info[u'title'], + 'ext': video_extension, + 'thumbnail': info[u'thumbUrl'], + }] + + # We try looking in other parts of the webpage mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract media URL') - return + raise ExtractorError(u'Unable to extract media URL') mediaURL = compat_urllib_parse.unquote(mobj.group(1)) video_url = mediaURL mobj = re.search(r'(.*) video by (.*) - Photobucket', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract title') - return + raise ExtractorError(u'Unable to extract title') video_title = mobj.group(1).decode('utf-8') video_uploader = mobj.group(2).decode('utf-8') @@ -889,186 +912,99 @@ class PhotobucketIE(InfoExtractor): class YahooIE(InfoExtractor): - """Information extractor for video.yahoo.com.""" + """Information extractor for screen.yahoo.com.""" + _VALID_URL = r'http://screen\.yahoo\.com/.*?-(?P\d*?)\.html' - # _VALID_URL matches all Yahoo! Video URLs - # _VPAGE_URL matches only the extractable '/watch/' URLs - _VALID_URL = r'(?:http://)?(?:[a-z]+\.)?video\.yahoo\.com/(?:watch|network)/([0-9]+)(?:/|\?v=)([0-9]+)(?:[#\?].*)?' - _VPAGE_URL = r'(?:http://)?video\.yahoo\.com/watch/([0-9]+)/([0-9]+)(?:[#\?].*)?' - IE_NAME = u'video.yahoo' - - def __init__(self, downloader=None): - InfoExtractor.__init__(self, downloader) + def _real_extract(self, url): + mobj = re.match(self._VALID_URL, url) + if mobj is None: + raise ExtractorError(u'Invalid URL: %s' % url) + video_id = mobj.group('id') + webpage = self._download_webpage(url, video_id) + m_id = re.search(r'YUI\.namespace\("Media"\)\.CONTENT_ID = "(?P.+?)";', webpage) + + if m_id is None: + # TODO: Check which url parameters are required + info_url = 'http://cosmos.bcst.yahoo.com/rest/v2/pops;lmsoverride=1;outputformat=mrss;cb=974419660;id=%s;rd=news.yahoo.com;datacontext=mdb;lg=KCa2IihxG3qE60vQ7HtyUy' % video_id + webpage = self._download_webpage(info_url, video_id, u'Downloading info webpage') + info_re = r'''<!\[CDATA\[(?P<title>.*?)\]\]>.* + .*?)\]\]>.* + .*?)\ .*\]\]>.* + https?://)?(?:(?:www|player)\.)?vimeo\.com/(?:(?:groups|album)/[^/]+/)?(?Pplay_redirect_hls\?clip_id=)?(?:videos?/)?(?P[0-9]+)' + IE_NAME = u'vimeo' def _real_extract(self, url, new_video=True): # Extract ID from URL mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: Invalid URL: %s' % url) - return - - video_id = mobj.group(2) - video_extension = 'flv' - - # Rewrite valid but non-extractable URLs as - # extractable English language /watch/ URLs - if re.match(self._VPAGE_URL, url) is None: - request = compat_urllib_request.Request(url) - try: - webpage = compat_urllib_request.urlopen(request).read() - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) - return - - mobj = re.search(r'\("id", "([0-9]+)"\);', webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: Unable to extract id field') - return - yahoo_id = mobj.group(1) - - mobj = re.search(r'\("vid", "([0-9]+)"\);', webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: Unable to extract vid field') - return - yahoo_vid = mobj.group(1) + raise ExtractorError(u'Invalid URL: %s' % url) - url = 'http://video.yahoo.com/watch/%s/%s' % (yahoo_vid, yahoo_id) - return self._real_extract(url, new_video=False) + video_id = mobj.group('id') + if not mobj.group('proto'): + url = 'https://' + url + if mobj.group('direct_link'): + url = 'https://vimeo.com/' + video_id # Retrieve video webpage to extract further information - request = compat_urllib_request.Request(url) - try: - self.report_download_webpage(video_id) - webpage = compat_urllib_request.urlopen(request).read() - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) - return + request = compat_urllib_request.Request(url, None, std_headers) + webpage = self._download_webpage(request, video_id) - # Extract uploader and title from webpage - self.report_extraction(video_id) - mobj = re.search(r'', webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video title') - return - video_title = mobj.group(1).decode('utf-8') - - mobj = re.search(r'

(.*)

', webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video uploader') - return - video_uploader = mobj.group(1).decode('utf-8') - - # Extract video thumbnail - mobj = re.search(r'', webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video thumbnail') - return - video_thumbnail = mobj.group(1).decode('utf-8') - - # Extract video description - mobj = re.search(r'', webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video description') - return - video_description = mobj.group(1).decode('utf-8') - if not video_description: - video_description = 'No description available.' - - # Extract video height and width - mobj = re.search(r'', webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video height') - return - yv_video_height = mobj.group(1) - - mobj = re.search(r'', webpage) - if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video width') - return - yv_video_width = mobj.group(1) - - # Retrieve video playlist to extract media URL - # I'm not completely sure what all these options are, but we - # seem to need most of them, otherwise the server sends a 401. - yv_lg = 'R0xx6idZnW2zlrKP8xxAIR' # not sure what this represents - yv_bitrate = '700' # according to Wikipedia this is hard-coded - request = compat_urllib_request.Request('http://cosmos.bcst.yahoo.com/up/yep/process/getPlaylistFOP.php?node_id=' + video_id + - '&tech=flash&mode=playlist&lg=' + yv_lg + '&bitrate=' + yv_bitrate + '&vidH=' + yv_video_height + - '&vidW=' + yv_video_width + '&swf=as3&rd=video.yahoo.com&tk=null&adsupported=v1,v2,&eventid=1301797') - try: - self.report_download_webpage(video_id) - webpage = compat_urllib_request.urlopen(request).read() - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) - return - - # Extract media URL from playlist XML - mobj = re.search(r'[^:]*: (.*?)( \([^\(]*\))?
', webpage) + mobj = re.search(r' tag: %s' % url) + (1, 'url', u'Could not find