├── test ├── __init__.py ├── swftests.unused │ ├── .gitignore │ ├── EqualsOperator.as │ ├── DictCall.as │ ├── ConstantInt.as │ ├── StringBasics.as │ ├── StringConversion.as │ ├── StaticAssignment.as │ ├── ClassConstruction.as │ ├── StringCharCodeAt.as │ ├── LocalVars.as │ ├── StaticRetrieval.as │ ├── ClassCall.as │ ├── PrivateCall.as │ ├── ConstArrayAccess.as │ ├── PrivateVoidCall.as │ ├── MemberAssignment.as │ ├── ArrayAccess.as │ └── NeOperator.as ├── testdata │ ├── thumbnails │ │ └── foo %d bar │ │ │ └── foo_%d.webp │ ├── cookies │ │ ├── session_cookies.txt │ │ ├── httponly_cookies.txt │ │ └── malformed_cookies.txt │ ├── f4m │ │ └── custom_base_url.f4m │ ├── xspf │ │ └── foo_xspf.xspf │ └── mpd │ │ ├── float_duration.mpd │ │ └── unfragmented.mpd ├── test_netrc.py ├── test_youtube_misc.py ├── test_update.py.disabled ├── test_iqiyi_sdk_interpreter.py ├── parameters.json ├── test_age_restriction.py ├── versions.json └── test_cache.py ├── docs ├── .gitignore ├── requirements.txt ├── README.md ├── Changelog.md ├── Contributing.md ├── LICENSE.md ├── Collaborators.md ├── supportedsites.md └── ytdlp_plugins.md ├── yt-dlp.cmd ├── requirements.txt ├── devscripts ├── logo.ico ├── SizeOfImage.patch ├── SizeOfImage_w.patch ├── fish-completion.in ├── posix-locale.sh ├── run_tests.sh ├── run_tests.bat ├── gh-pages.unused │ ├── update-copyright.py │ ├── generate-download.py │ ├── sign-versions.py │ ├── update-sites.py │ └── add-version.py ├── make_readme.py ├── make_issue_template.py ├── bash-completion.in ├── make_contributing.py ├── zsh-completion.in ├── bash-completion.py ├── lazy_load_template.py ├── update-formulae.py ├── update-version.py ├── generate_aes_testdata.py ├── make_supportedsites.py ├── show-downloads-statistics.py └── zsh-completion.py ├── pytest.ini ├── yt-dlp.sh ├── .gitattributes ├── yt_dlp ├── version.py ├── extractor │ ├── anvato_token_generator │ │ ├── __init__.py │ │ ├── common.py │ │ └── nfl.py │ ├── ufctv.py │ ├── spiegeltv.py │ ├── gigya.py │ ├── embedly.py │ ├── usanetwork.py │ ├── engadget.py │ ├── m6.py │ ├── videodetective.py │ ├── myvidster.py │ ├── cinemax.py │ ├── lci.py │ ├── vimm.py │ ├── nrl.py │ ├── formula1.py │ ├── outsidetv.py │ ├── ku6.py │ ├── vodpl.py │ ├── cliprs.py │ ├── streamff.py │ ├── freespeech.py │ ├── savefrom.py │ ├── teachingchannel.py │ ├── bibeltv.py │ ├── ebaumsworld.py │ ├── foxsports.py │ ├── cableav.py │ ├── people.py │ ├── nerdcubed.py │ ├── nonktube.py │ ├── lovehomeporn.py │ ├── cam4.py │ ├── gputechconf.py │ ├── maoritv.py │ ├── unity.py │ ├── hentaistigma.py │ ├── nuevo.py │ ├── defense.py │ ├── rottentomatoes.py │ ├── bandaichannel.py │ ├── adobeconnect.py │ ├── europeantour.py │ ├── googlesearch.py │ ├── bfi.py │ ├── echomsk.py │ ├── stretchinternet.py │ ├── uktvplay.py │ ├── worldstarhiphop.py │ ├── bild.py │ ├── helsinki.py │ ├── restudy.py │ ├── howcast.py │ ├── thestar.py │ ├── thescene.py │ ├── thesun.py │ ├── academicearth.py │ ├── moviezine.py │ ├── vh1.py │ ├── nzz.py │ ├── cozytv.py │ ├── hgtv.py │ ├── yourupload.py │ ├── skylinewebcams.py │ ├── trunews.py │ ├── ro220.py │ ├── xbef.py │ ├── filmweb.py │ ├── fox9.py │ ├── tastytrade.py │ ├── miaopai.py │ ├── fujitv.py │ ├── rtvs.py │ ├── ehow.py │ ├── livejournal.py │ ├── oktoberfesttv.py │ ├── breitbart.py │ ├── odatv.py │ ├── vodplatform.py │ ├── __init__.py │ ├── glide.py │ ├── ruhd.py │ ├── thisamericanlife.py │ ├── commonmistakes.py │ ├── hornbunny.py │ ├── mychannels.py │ ├── tvland.py │ ├── hypem.py │ ├── goshgay.py │ ├── historicfilms.py │ ├── dreisat.py │ ├── tvnoe.py │ ├── aliexpress.py │ ├── sztvhu.py │ ├── behindkink.py │ ├── lenta.py │ └── reverbnation.py ├── __main__.py ├── postprocessor │ └── __init__.py └── downloader │ └── rtsp.py ├── ytdlp_plugins ├── extractor │ ├── __init__.py │ └── sample.py └── postprocessor │ ├── __init__.py │ └── sample.py ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── 5_feature_request.yml ├── FUNDING.yml ├── workflows │ ├── download.yml │ ├── quick-test.yml │ └── core.yml ├── ISSUE_TEMPLATE_tmpl │ └── 5_feature_request.yml └── PULL_REQUEST_TEMPLATE.md ├── MANIFEST.in ├── setup.cfg ├── .readthedocs.yml ├── tox.ini ├── LICENSE └── .gitignore /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | myst-parser 2 | -------------------------------------------------------------------------------- /test/swftests.unused/.gitignore: -------------------------------------------------------------------------------- 1 | *.swf 2 | -------------------------------------------------------------------------------- /yt-dlp.cmd: -------------------------------------------------------------------------------- 1 | @py "%~dp0yt_dlp\__main__.py" %* -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ```{include} ../README.md 2 | ``` 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mutagen 2 | pycryptodomex 3 | websockets 4 | -------------------------------------------------------------------------------- /devscripts/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/yt-dlp/master/devscripts/logo.ico -------------------------------------------------------------------------------- /docs/Changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | orphan: true 3 | --- 4 | ```{include} ../Changelog.md 5 | ``` 6 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -ra -v --strict-markers 3 | markers = 4 | download 5 | -------------------------------------------------------------------------------- /docs/Contributing.md: -------------------------------------------------------------------------------- 1 | --- 2 | orphan: true 3 | --- 4 | ```{include} ../Contributing.md 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | --- 2 | orphan: true 3 | --- 4 | # LICENSE 5 | ```{include} ../LICENSE 6 | ``` 7 | -------------------------------------------------------------------------------- /yt-dlp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec python3 "$(dirname "$(realpath "$0")")/yt_dlp/__main__.py" "$@" 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | Makefile* text whitespace=-tab-in-indent 4 | *.sh text eol=lf 5 | -------------------------------------------------------------------------------- /docs/Collaborators.md: -------------------------------------------------------------------------------- 1 | --- 2 | orphan: true 3 | --- 4 | ```{include} ../Collaborators.md 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/supportedsites.md: -------------------------------------------------------------------------------- 1 | --- 2 | orphan: true 3 | --- 4 | ```{include} ../supportedsites.md 5 | ``` 6 | -------------------------------------------------------------------------------- /devscripts/SizeOfImage.patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/yt-dlp/master/devscripts/SizeOfImage.patch -------------------------------------------------------------------------------- /devscripts/SizeOfImage_w.patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/yt-dlp/master/devscripts/SizeOfImage_w.patch -------------------------------------------------------------------------------- /test/testdata/thumbnails/foo %d bar/foo_%d.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/yt-dlp/master/test/testdata/thumbnails/foo %d bar/foo_%d.webp -------------------------------------------------------------------------------- /yt_dlp/version.py: -------------------------------------------------------------------------------- 1 | # Autogenerated by devscripts/update-version.py 2 | 3 | __version__ = '2022.01.21' 4 | 5 | RELEASE_GIT_HEAD = 'f20d607b0' 6 | -------------------------------------------------------------------------------- /ytdlp_plugins/extractor/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | 3 | # ℹ️ The imported name must end in "IE" 4 | from .sample import SamplePluginIE 5 | -------------------------------------------------------------------------------- /devscripts/fish-completion.in: -------------------------------------------------------------------------------- 1 | 2 | {{commands}} 3 | 4 | 5 | complete --command yt-dlp --arguments ":ytfavorites :ytrecommended :ytsubscriptions :ytwatchlater :ythistory" 6 | -------------------------------------------------------------------------------- /yt_dlp/extractor/anvato_token_generator/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .nfl import NFLTokenGenerator 4 | 5 | __all__ = [ 6 | 'NFLTokenGenerator', 7 | ] 8 | -------------------------------------------------------------------------------- /ytdlp_plugins/postprocessor/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | 3 | # ℹ️ The imported name must end in "PP" and is the name to be used in --use-postprocessor 4 | from .sample import SamplePluginPP 5 | -------------------------------------------------------------------------------- /docs/ytdlp_plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | orphan: true 3 | --- 4 | # ytdlp_plugins 5 | 6 | See [https://github.com/yt-dlp/yt-dlp/tree/master/ytdlp_plugins](https://github.com/yt-dlp/yt-dlp/tree/master/ytdlp_plugins). 7 | -------------------------------------------------------------------------------- /devscripts/posix-locale.sh: -------------------------------------------------------------------------------- 1 | 2 | # source this file in your shell to get a POSIX locale (which will break many programs, but that's kind of the point) 3 | 4 | export LC_ALL=POSIX 5 | export LANG=POSIX 6 | export LANGUAGE=POSIX 7 | -------------------------------------------------------------------------------- /test/swftests.unused/EqualsOperator.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: false 3 | 4 | package { 5 | public class EqualsOperator { 6 | public static function main():Boolean{ 7 | return 1 == 2; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/swftests.unused/DictCall.as: -------------------------------------------------------------------------------- 1 | // input: [{"x": 1, "y": 2}] 2 | // output: 3 3 | 4 | package { 5 | public class DictCall { 6 | public static function main(d:Object):int{ 7 | return d.x + d.y; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Get help from the community on Discord 4 | url: https://discord.gg/H5MNcFW63r 5 | about: Join the yt-dlp Discord for community-powered support! 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include Changelog.md 3 | include LICENSE 4 | include README.md 5 | include completions/*/* 6 | include supportedsites.md 7 | include yt-dlp.1 8 | recursive-include devscripts * 9 | recursive-include test * 10 | -------------------------------------------------------------------------------- /test/swftests.unused/ConstantInt.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: 2 3 | 4 | package { 5 | public class ConstantInt { 6 | private static const x:int = 2; 7 | 8 | public static function main():int{ 9 | return x; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/swftests.unused/StringBasics.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: 3 3 | 4 | package { 5 | public class StringBasics { 6 | public static function main():int{ 7 | var s:String = "abc"; 8 | return s.length; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /yt_dlp/extractor/anvato_token_generator/common.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | 4 | class TokenGenerator: 5 | def generate(self, anvack, mcp_id): 6 | raise NotImplementedError('This method must be implemented by subclasses') 7 | -------------------------------------------------------------------------------- /test/swftests.unused/StringConversion.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: 2 3 | 4 | package { 5 | public class StringConversion { 6 | public static function main():int{ 7 | var s:String = String(99); 8 | return s.length; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/swftests.unused/StaticAssignment.as: -------------------------------------------------------------------------------- 1 | // input: [1] 2 | // output: 1 3 | 4 | package { 5 | public class StaticAssignment { 6 | public static var v:int; 7 | 8 | public static function main(a:int):int{ 9 | v = a; 10 | return v; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/swftests.unused/ClassConstruction.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: 0 3 | 4 | package { 5 | public class ClassConstruction { 6 | public static function main():int{ 7 | var f:Foo = new Foo(); 8 | return 0; 9 | } 10 | } 11 | } 12 | 13 | class Foo { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /test/swftests.unused/StringCharCodeAt.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: 9897 3 | 4 | package { 5 | public class StringCharCodeAt { 6 | public static function main():int{ 7 | var s:String = "abc"; 8 | return s.charCodeAt(1) * 100 + s.charCodeAt(); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/swftests.unused/LocalVars.as: -------------------------------------------------------------------------------- 1 | // input: [1, 2] 2 | // output: 3 3 | 4 | package { 5 | public class LocalVars { 6 | public static function main(a:int, b:int):int{ 7 | var c:int = a + b + b; 8 | var d:int = c - b; 9 | var e:int = d; 10 | return e; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/testdata/cookies/session_cookies.txt: -------------------------------------------------------------------------------- 1 | # Netscape HTTP Cookie File 2 | # http://curl.haxx.se/rfc/cookie_spec.html 3 | # This is a generated file! Do not edit. 4 | 5 | www.foobar.foobar FALSE / TRUE YoutubeDLExpiresEmpty YoutubeDLExpiresEmptyValue 6 | www.foobar.foobar FALSE / TRUE 0 YoutubeDLExpires0 YoutubeDLExpires0Value 7 | -------------------------------------------------------------------------------- /test/swftests.unused/StaticRetrieval.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: 1 3 | 4 | package { 5 | public class StaticRetrieval { 6 | public static var v:int; 7 | 8 | public static function main():int{ 9 | if (v) { 10 | return 0; 11 | } else { 12 | return 1; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = True 3 | 4 | [flake8] 5 | exclude = yt_dlp/extractor/__init__.py,devscripts/buildserver.py,devscripts/lazy_load_template.py,devscripts/make_issue_template.py,setup.py,build,.git,venv,devscripts/create-github-release.py,devscripts/release.sh,devscripts/show-downloads-statistics.py 6 | ignore = E402,E501,E731,E741,W503 -------------------------------------------------------------------------------- /test/testdata/cookies/httponly_cookies.txt: -------------------------------------------------------------------------------- 1 | # Netscape HTTP Cookie File 2 | # http://curl.haxx.se/rfc/cookie_spec.html 3 | # This is a generated file! Do not edit. 4 | 5 | #HttpOnly_www.foobar.foobar FALSE / TRUE 2147483647 HTTPONLY_COOKIE HTTPONLY_COOKIE_VALUE 6 | www.foobar.foobar FALSE / TRUE 2147483647 JS_ACCESSIBLE_COOKIE JS_ACCESSIBLE_COOKIE_VALUE 7 | -------------------------------------------------------------------------------- /devscripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z $1 ]; then 4 | test_set='test' 5 | elif [ $1 = 'core' ]; then 6 | test_set="-m not download" 7 | elif [ $1 = 'download' ]; then 8 | test_set="-m download" 9 | else 10 | echo 'Invalid test type "'$1'". Use "core" | "download"' 11 | exit 1 12 | fi 13 | 14 | python3 -m pytest "$test_set" 15 | -------------------------------------------------------------------------------- /test/swftests.unused/ClassCall.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: 121 3 | 4 | package { 5 | public class ClassCall { 6 | public static function main():int{ 7 | var f:OtherClass = new OtherClass(); 8 | return f.func(100,20); 9 | } 10 | } 11 | } 12 | 13 | class OtherClass { 14 | public function func(x: int, y: int):int { 15 | return x+y+1; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /devscripts/run_tests.bat: -------------------------------------------------------------------------------- 1 | @setlocal 2 | @echo off 3 | cd /d %~dp0.. 4 | 5 | if ["%~1"]==[""] ( 6 | set "test_set="test"" 7 | ) else if ["%~1"]==["core"] ( 8 | set "test_set="-m not download"" 9 | ) else if ["%~1"]==["download"] ( 10 | set "test_set="-m "download"" 11 | ) else ( 12 | echo.Invalid test type "%~1". Use "core" ^| "download" 13 | exit /b 1 14 | ) 15 | 16 | pytest %test_set% 17 | -------------------------------------------------------------------------------- /test/testdata/cookies/malformed_cookies.txt: -------------------------------------------------------------------------------- 1 | # Netscape HTTP Cookie File 2 | # http://curl.haxx.se/rfc/cookie_spec.html 3 | # This is a generated file! Do not edit. 4 | 5 | # Cookie file entry with invalid number of fields - 6 instead of 7 6 | www.foobar.foobar FALSE / FALSE 0 COOKIE 7 | 8 | # Cookie file entry with invalid expires at 9 | www.foobar.foobar FALSE / FALSE 1.7976931348623157e+308 COOKIE VALUE 10 | -------------------------------------------------------------------------------- /test/swftests.unused/PrivateCall.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: 9 3 | 4 | package { 5 | public class PrivateCall { 6 | public static function main():int{ 7 | var f:OtherClass = new OtherClass(); 8 | return f.func(); 9 | } 10 | } 11 | } 12 | 13 | class OtherClass { 14 | private function pf():int { 15 | return 9; 16 | } 17 | 18 | public function func():int { 19 | return this.pf(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/swftests.unused/ConstArrayAccess.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: 4 3 | 4 | package { 5 | public class ConstArrayAccess { 6 | private static const x:int = 2; 7 | private static const ar:Array = ["42", "3411"]; 8 | 9 | public static function main():int{ 10 | var c:ConstArrayAccess = new ConstArrayAccess(); 11 | return c.f(); 12 | } 13 | 14 | public function f(): int { 15 | return ar[1].length; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/swftests.unused/PrivateVoidCall.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: 9 3 | 4 | package { 5 | public class PrivateVoidCall { 6 | public static function main():int{ 7 | var f:OtherClass = new OtherClass(); 8 | f.func(); 9 | return 9; 10 | } 11 | } 12 | } 13 | 14 | class OtherClass { 15 | private function pf():void { 16 | ; 17 | } 18 | 19 | public function func():void { 20 | this.pf(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/swftests.unused/MemberAssignment.as: -------------------------------------------------------------------------------- 1 | // input: [1] 2 | // output: 2 3 | 4 | package { 5 | public class MemberAssignment { 6 | public var v:int; 7 | 8 | public function g():int { 9 | return this.v; 10 | } 11 | 12 | public function f(a:int):int{ 13 | this.v = a; 14 | return this.v + this.g(); 15 | } 16 | 17 | public static function main(a:int): int { 18 | var v:MemberAssignment = new MemberAssignment(); 19 | return v.f(a); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/swftests.unused/ArrayAccess.as: -------------------------------------------------------------------------------- 1 | // input: [["a", "b", "c", "d"]] 2 | // output: ["c", "b", "a", "d"] 3 | 4 | package { 5 | public class ArrayAccess { 6 | public static function main(ar:Array):Array { 7 | var aa:ArrayAccess = new ArrayAccess(); 8 | return aa.f(ar, 2); 9 | } 10 | 11 | private function f(ar:Array, num:Number):Array{ 12 | var x:String = ar[0]; 13 | var y:String = ar[num % ar.length]; 14 | ar[0] = y; 15 | ar[num] = x; 16 | return ar; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/swftests.unused/NeOperator.as: -------------------------------------------------------------------------------- 1 | // input: [] 2 | // output: 123 3 | 4 | package { 5 | public class NeOperator { 6 | public static function main(): int { 7 | var res:int = 0; 8 | if (1 != 2) { 9 | res += 3; 10 | } else { 11 | res += 4; 12 | } 13 | if (2 != 2) { 14 | res += 10; 15 | } else { 16 | res += 20; 17 | } 18 | if (9 == 9) { 19 | res += 100; 20 | } 21 | return res; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /yt_dlp/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | # Execute with 5 | # $ python yt_dlp/__main__.py (2.6+) 6 | # $ python -m yt_dlp (2.7+) 7 | 8 | import sys 9 | 10 | if __package__ is None and not hasattr(sys, 'frozen'): 11 | # direct call of __main__.py 12 | import os.path 13 | path = os.path.realpath(os.path.abspath(__file__)) 14 | sys.path.insert(0, os.path.dirname(os.path.dirname(path))) 15 | 16 | import yt_dlp 17 | 18 | if __name__ == '__main__': 19 | yt_dlp.main() 20 | -------------------------------------------------------------------------------- /ytdlp_plugins/extractor/sample.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # ⚠ Don't use relative imports 4 | from yt_dlp.extractor.common import InfoExtractor 5 | 6 | 7 | # ℹ️ Instructions on making extractors can be found at: 8 | # 🔗 https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#adding-support-for-a-new-site 9 | 10 | class SamplePluginIE(InfoExtractor): 11 | _WORKING = False 12 | IE_DESC = False 13 | _VALID_URL = r'^sampleplugin:' 14 | 15 | def _real_extract(self, url): 16 | self.to_screen('URL "%s" sucessfully captured' % url) 17 | -------------------------------------------------------------------------------- /yt_dlp/extractor/ufctv.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .imggaming import ImgGamingBaseIE 5 | 6 | 7 | class UFCTVIE(ImgGamingBaseIE): 8 | _VALID_URL = ImgGamingBaseIE._VALID_URL_TEMPL % r'(?:(?:app|www)\.)?(?:ufc\.tv|(?:ufc)?fightpass\.com)|ufcfightpass\.img(?:dge|gaming)\.com' 9 | _NETRC_MACHINE = 'ufctv' 10 | _REALM = 'ufc' 11 | 12 | 13 | class UFCArabiaIE(ImgGamingBaseIE): 14 | _VALID_URL = ImgGamingBaseIE._VALID_URL_TEMPL % r'(?:(?:app|www)\.)?ufcarabia\.(?:ae|com)' 15 | _NETRC_MACHINE = 'ufcarabia' 16 | _REALM = 'admufc' 17 | -------------------------------------------------------------------------------- /yt_dlp/extractor/spiegeltv.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from .nexx import NexxIE 5 | 6 | 7 | class SpiegeltvIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?spiegel\.tv/videos/(?P\d+)' 9 | _TEST = { 10 | 'url': 'http://www.spiegel.tv/videos/161681-flug-mh370/', 11 | 'only_matching': True, 12 | } 13 | 14 | def _real_extract(self, url): 15 | return self.url_result( 16 | 'https://api.nexx.cloud/v3/748/videos/byid/%s' 17 | % self._match_id(url), ie=NexxIE.ie_key()) 18 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Optionally build your docs in additional formats such as PDF 13 | formats: 14 | - epub 15 | - pdf 16 | - htmlzip 17 | 18 | # Optionally set the version of Python and requirements required to build your docs 19 | python: 20 | version: 3 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py26,py27,py33,py34,py35 3 | 4 | # Needed? 5 | [testenv] 6 | deps = 7 | nose 8 | coverage 9 | # We need a valid $HOME for test_compat_expanduser 10 | passenv = HOME 11 | defaultargs = test --exclude test_download.py --exclude test_age_restriction.py 12 | --exclude test_subtitles.py --exclude test_write_annotations.py 13 | --exclude test_youtube_lists.py --exclude test_iqiyi_sdk_interpreter.py 14 | --exclude test_socks.py 15 | commands = nosetests --verbose {posargs:{[testenv]defaultargs}} # --with-coverage --cover-package=yt_dlp --cover-html 16 | # test.test_download:TestDownload.test_NowVideo 17 | -------------------------------------------------------------------------------- /devscripts/gh-pages.unused/update-copyright.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | from __future__ import with_statement, unicode_literals 5 | 6 | import datetime 7 | import glob 8 | import io # For Python 2 compatibility 9 | import os 10 | import re 11 | 12 | year = str(datetime.datetime.now().year) 13 | for fn in glob.glob('*.html*'): 14 | with io.open(fn, encoding='utf-8') as f: 15 | content = f.read() 16 | newc = re.sub(r'(?PCopyright © 2011-)(?P[0-9]{4})', 'Copyright © 2011-' + year, content) 17 | if content != newc: 18 | tmpFn = fn + '.part' 19 | with io.open(tmpFn, 'wt', encoding='utf-8') as outf: 20 | outf.write(newc) 21 | os.rename(tmpFn, fn) 22 | -------------------------------------------------------------------------------- /test/test_netrc.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | import sys 6 | import unittest 7 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 8 | 9 | 10 | from yt_dlp.extractor import ( 11 | gen_extractors, 12 | ) 13 | 14 | 15 | class TestNetRc(unittest.TestCase): 16 | def test_netrc_present(self): 17 | for ie in gen_extractors(): 18 | if not hasattr(ie, '_login'): 19 | continue 20 | self.assertTrue( 21 | hasattr(ie, '_NETRC_MACHINE'), 22 | 'Extractor %s supports login, but is missing a _NETRC_MACHINE property' % ie.IE_NAME) 23 | 24 | 25 | if __name__ == '__main__': 26 | unittest.main() 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | 13 | custom: ['https://github.com/yt-dlp/yt-dlp/blob/master/Collaborators.md#collaborators'] 14 | -------------------------------------------------------------------------------- /yt_dlp/extractor/gigya.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | from ..utils import ( 6 | ExtractorError, 7 | urlencode_postdata, 8 | ) 9 | 10 | 11 | class GigyaBaseIE(InfoExtractor): 12 | def _gigya_login(self, auth_data): 13 | auth_info = self._download_json( 14 | 'https://accounts.eu1.gigya.com/accounts.login', None, 15 | note='Logging in', errnote='Unable to log in', 16 | data=urlencode_postdata(auth_data)) 17 | 18 | error_message = auth_info.get('errorDetails') or auth_info.get('errorMessage') 19 | if error_message: 20 | raise ExtractorError( 21 | 'Unable to login: %s' % error_message, expected=True) 22 | return auth_info 23 | -------------------------------------------------------------------------------- /devscripts/make_readme.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # yt-dlp --help | make_readme.py 4 | # This must be run in a console of correct width 5 | 6 | from __future__ import unicode_literals 7 | 8 | import io 9 | import sys 10 | import re 11 | 12 | README_FILE = 'README.md' 13 | helptext = sys.stdin.read() 14 | 15 | if isinstance(helptext, bytes): 16 | helptext = helptext.decode('utf-8') 17 | 18 | with io.open(README_FILE, encoding='utf-8') as f: 19 | oldreadme = f.read() 20 | 21 | header = oldreadme[:oldreadme.index('## General Options:')] 22 | footer = oldreadme[oldreadme.index('# CONFIGURATION'):] 23 | 24 | options = helptext[helptext.index(' General Options:'):] 25 | options = re.sub(r'(?m)^ (\w.+)$', r'## \1', options) 26 | options = options + '\n' 27 | 28 | with io.open(README_FILE, 'w', encoding='utf-8') as f: 29 | f.write(header) 30 | f.write(options) 31 | f.write(footer) 32 | -------------------------------------------------------------------------------- /yt_dlp/extractor/embedly.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | from ..compat import compat_urllib_parse_unquote 6 | 7 | 8 | class EmbedlyIE(InfoExtractor): 9 | _VALID_URL = r'https?://(?:www|cdn\.)?embedly\.com/widgets/media\.html\?(?:[^#]*?&)?url=(?P[^#&]+)' 10 | _TESTS = [{ 11 | 'url': 'https://cdn.embedly.com/widgets/media.html?src=http%3A%2F%2Fwww.youtube.com%2Fembed%2Fvideoseries%3Flist%3DUUGLim4T2loE5rwCMdpCIPVg&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSU4fj_aEMVw%26list%3DUUGLim4T2loE5rwCMdpCIPVg&image=http%3A%2F%2Fi.ytimg.com%2Fvi%2FSU4fj_aEMVw%2Fhqdefault.jpg&key=8ee8a2e6a8cc47aab1a5ee67f9a178e0&type=text%2Fhtml&schema=youtube&autoplay=1', 12 | 'only_matching': True, 13 | }] 14 | 15 | def _real_extract(self, url): 16 | return self.url_result(compat_urllib_parse_unquote(self._match_id(url))) 17 | -------------------------------------------------------------------------------- /devscripts/make_issue_template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | import io 5 | import optparse 6 | 7 | 8 | def main(): 9 | parser = optparse.OptionParser(usage='%prog INFILE OUTFILE') 10 | options, args = parser.parse_args() 11 | if len(args) != 2: 12 | parser.error('Expected an input and an output filename') 13 | 14 | infile, outfile = args 15 | 16 | with io.open(infile, encoding='utf-8') as inf: 17 | issue_template_tmpl = inf.read() 18 | 19 | # Get the version from yt_dlp/version.py without importing the package 20 | exec(compile(open('yt_dlp/version.py').read(), 21 | 'yt_dlp/version.py', 'exec')) 22 | 23 | out = issue_template_tmpl % {'version': locals()['__version__']} 24 | 25 | with io.open(outfile, 'w', encoding='utf-8') as outf: 26 | outf.write(out) 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /yt_dlp/extractor/usanetwork.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .nbc import NBCIE 5 | 6 | 7 | class USANetworkIE(NBCIE): 8 | _VALID_URL = r'https?(?P://(?:www\.)?usanetwork\.com/(?:[^/]+/videos?|movies?)/(?:[^/]+/)?(?P\d+))' 9 | _TESTS = [{ 10 | 'url': 'https://www.usanetwork.com/peacock-trailers/video/intelligence-trailer/4185302', 11 | 'info_dict': { 12 | 'id': '4185302', 13 | 'ext': 'mp4', 14 | 'title': 'Intelligence (Trailer)', 15 | 'description': 'A maverick NSA agent enlists the help of a junior systems analyst in a workplace power grab.', 16 | 'upload_date': '20200715', 17 | 'timestamp': 1594785600, 18 | 'uploader': 'NBCU-MPAT', 19 | }, 20 | 'params': { 21 | # m3u8 download 22 | 'skip_download': True, 23 | }, 24 | }] 25 | -------------------------------------------------------------------------------- /yt_dlp/extractor/engadget.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class EngadgetIE(InfoExtractor): 7 | _VALID_URL = r'https?://(?:www\.)?engadget\.com/video/(?P[^/?#]+)' 8 | 9 | _TESTS = [{ 10 | # video with 5min ID 11 | 'url': 'http://www.engadget.com/video/518153925/', 12 | 'md5': 'c6820d4828a5064447a4d9fc73f312c9', 13 | 'info_dict': { 14 | 'id': '518153925', 15 | 'ext': 'mp4', 16 | 'title': 'Samsung Galaxy Tab Pro 8.4 Review', 17 | }, 18 | 'add_ie': ['FiveMin'], 19 | }, { 20 | # video with vidible ID 21 | 'url': 'https://www.engadget.com/video/57a28462134aa15a39f0421a/', 22 | 'only_matching': True, 23 | }] 24 | 25 | def _real_extract(self, url): 26 | video_id = self._match_id(url) 27 | return self.url_result('aol-video:%s' % video_id) 28 | -------------------------------------------------------------------------------- /devscripts/bash-completion.in: -------------------------------------------------------------------------------- 1 | __yt_dlp() 2 | { 3 | local cur prev opts fileopts diropts keywords 4 | COMPREPLY=() 5 | cur="${COMP_WORDS[COMP_CWORD]}" 6 | prev="${COMP_WORDS[COMP_CWORD-1]}" 7 | opts="{{flags}}" 8 | keywords=":ytfavorites :ytrecommended :ytsubscriptions :ytwatchlater :ythistory" 9 | fileopts="-a|--batch-file|--download-archive|--cookies|--load-info" 10 | diropts="--cache-dir" 11 | 12 | if [[ ${prev} =~ ${fileopts} ]]; then 13 | COMPREPLY=( $(compgen -f -- ${cur}) ) 14 | return 0 15 | elif [[ ${prev} =~ ${diropts} ]]; then 16 | COMPREPLY=( $(compgen -d -- ${cur}) ) 17 | return 0 18 | fi 19 | 20 | if [[ ${cur} =~ : ]]; then 21 | COMPREPLY=( $(compgen -W "${keywords}" -- ${cur}) ) 22 | return 0 23 | elif [[ ${cur} == * ]] ; then 24 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 25 | return 0 26 | fi 27 | } 28 | 29 | complete -F __yt_dlp yt-dlp 30 | -------------------------------------------------------------------------------- /.github/workflows/download.yml: -------------------------------------------------------------------------------- 1 | name: Download Tests 2 | on: [push, pull_request] 3 | jobs: 4 | tests: 5 | name: Download Tests 6 | if: "contains(github.event.head_commit.message, 'ci run dl')" 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-18.04] 12 | python-version: [3.7, 3.8, 3.9, 3.10-dev, pypy-3.6, pypy-3.7] 13 | run-tests-ext: [sh] 14 | include: 15 | - os: windows-latest 16 | python-version: 3.6 17 | run-tests-ext: bat 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install pytest 25 | run: pip install pytest 26 | - name: Run tests 27 | continue-on-error: true 28 | run: ./devscripts/run_tests.${{ matrix.run-tests-ext }} download 29 | -------------------------------------------------------------------------------- /devscripts/make_contributing.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | import io 5 | import optparse 6 | import re 7 | 8 | 9 | def main(): 10 | return # This is unused in yt-dlp 11 | 12 | parser = optparse.OptionParser(usage='%prog INFILE OUTFILE') 13 | options, args = parser.parse_args() 14 | if len(args) != 2: 15 | parser.error('Expected an input and an output filename') 16 | 17 | infile, outfile = args 18 | 19 | with io.open(infile, encoding='utf-8') as inf: 20 | readme = inf.read() 21 | 22 | bug_text = re.search( 23 | r'(?s)#\s*BUGS\s*[^\n]*\s*(.*?)#\s*COPYRIGHT', readme).group(1) 24 | dev_text = re.search( 25 | r'(?s)(#\s*DEVELOPER INSTRUCTIONS.*?)#\s*EMBEDDING yt-dlp', readme).group(1) 26 | 27 | out = bug_text + dev_text 28 | 29 | with io.open(outfile, 'w', encoding='utf-8') as outf: 30 | outf.write(out) 31 | 32 | 33 | if __name__ == '__main__': 34 | main() 35 | -------------------------------------------------------------------------------- /devscripts/zsh-completion.in: -------------------------------------------------------------------------------- 1 | #compdef yt-dlp 2 | 3 | __yt_dlp() { 4 | local curcontext="$curcontext" fileopts diropts cur prev 5 | typeset -A opt_args 6 | fileopts="{{fileopts}}" 7 | diropts="{{diropts}}" 8 | cur=$words[CURRENT] 9 | case $cur in 10 | :) 11 | _arguments '*: :(::ytfavorites ::ytrecommended ::ytsubscriptions ::ytwatchlater ::ythistory)' 12 | ;; 13 | *) 14 | prev=$words[CURRENT-1] 15 | if [[ ${prev} =~ ${fileopts} ]]; then 16 | _path_files 17 | elif [[ ${prev} =~ ${diropts} ]]; then 18 | _path_files -/ 19 | elif [[ ${prev} == "--remux-video" ]]; then 20 | _arguments '*: :(mp4 mkv)' 21 | elif [[ ${prev} == "--recode-video" ]]; then 22 | _arguments '*: :(mp4 flv ogg webm mkv)' 23 | else 24 | _arguments '*: :({{flags}})' 25 | fi 26 | ;; 27 | esac 28 | } 29 | 30 | __yt_dlp -------------------------------------------------------------------------------- /yt_dlp/extractor/m6.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class M6IE(InfoExtractor): 8 | IE_NAME = 'm6' 9 | _VALID_URL = r'https?://(?:www\.)?m6\.fr/[^/]+/videos/(?P\d+)-[^\.]+\.html' 10 | 11 | _TEST = { 12 | 'url': 'http://www.m6.fr/emission-les_reines_du_shopping/videos/11323908-emeline_est_la_reine_du_shopping_sur_le_theme_ma_fete_d_8217_anniversaire.html', 13 | 'md5': '242994a87de2c316891428e0176bcb77', 14 | 'info_dict': { 15 | 'id': '11323908', 16 | 'ext': 'mp4', 17 | 'title': 'Emeline est la Reine du Shopping sur le thème « Ma fête d’anniversaire ! »', 18 | 'description': 'md5:1212ae8fb4b7baa4dc3886c5676007c2', 19 | 'duration': 100, 20 | } 21 | } 22 | 23 | def _real_extract(self, url): 24 | video_id = self._match_id(url) 25 | return self.url_result('6play:%s' % video_id, 'SixPlay', video_id) 26 | -------------------------------------------------------------------------------- /devscripts/bash-completion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | from os.path import dirname as dirn 6 | import sys 7 | 8 | sys.path.insert(0, dirn(dirn((os.path.abspath(__file__))))) 9 | import yt_dlp 10 | 11 | BASH_COMPLETION_FILE = "completions/bash/yt-dlp" 12 | BASH_COMPLETION_TEMPLATE = "devscripts/bash-completion.in" 13 | 14 | 15 | def build_completion(opt_parser): 16 | opts_flag = [] 17 | for group in opt_parser.option_groups: 18 | for option in group.option_list: 19 | # for every long flag 20 | opts_flag.append(option.get_opt_string()) 21 | with open(BASH_COMPLETION_TEMPLATE) as f: 22 | template = f.read() 23 | with open(BASH_COMPLETION_FILE, "w") as f: 24 | # just using the special char 25 | filled_template = template.replace("{{flags}}", " ".join(opts_flag)) 26 | f.write(filled_template) 27 | 28 | 29 | parser = yt_dlp.parseOpts()[0] 30 | build_completion(parser) 31 | -------------------------------------------------------------------------------- /devscripts/gh-pages.unused/generate-download.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | import json 5 | 6 | versions_info = json.load(open('update/versions.json')) 7 | version = versions_info['latest'] 8 | version_dict = versions_info['versions'][version] 9 | 10 | # Read template page 11 | with open('download.html.in', 'r', encoding='utf-8') as tmplf: 12 | template = tmplf.read() 13 | 14 | template = template.replace('@PROGRAM_VERSION@', version) 15 | template = template.replace('@PROGRAM_URL@', version_dict['bin'][0]) 16 | template = template.replace('@PROGRAM_SHA256SUM@', version_dict['bin'][1]) 17 | template = template.replace('@EXE_URL@', version_dict['exe'][0]) 18 | template = template.replace('@EXE_SHA256SUM@', version_dict['exe'][1]) 19 | template = template.replace('@TAR_URL@', version_dict['tar'][0]) 20 | template = template.replace('@TAR_SHA256SUM@', version_dict['tar'][1]) 21 | with open('download.html', 'w', encoding='utf-8') as dlf: 22 | dlf.write(template) 23 | -------------------------------------------------------------------------------- /test/testdata/f4m/custom_base_url.f4m: -------------------------------------------------------------------------------- 1 | 2 | 3 | recorded 4 | http://vod.livestream.com/events/0000000000673980/ 5 | 269.293 6 | AAAAm2Fic3QAAAAAAAAAAQAAAAPoAAAAAAAEG+0AAAAAAAAAAAAAAAAAAQAAABlhc3J0AAAAAAAAAAABAAAAAQAAAC4BAAAAVmFmcnQAAAAAAAAD6AAAAAAEAAAAAQAAAAAAAAAAAAAXcAAAAC0AAAAAAAQHQAAAE5UAAAAuAAAAAAAEGtUAAAEYAAAAAAAAAAAAAAAAAAAAAAA= 7 | 8 | AgAKb25NZXRhRGF0YQgAAAAIAAhkdXJhdGlvbgBAcNSwIMSbpgAFd2lkdGgAQJQAAAAAAAAABmhlaWdodABAhoAAAAAAAAAJZnJhbWVyYXRlAEA4/7DoLwW3AA12aWRlb2RhdGFyYXRlAECe1DLgjcobAAx2aWRlb2NvZGVjaWQAQBwAAAAAAAAADWF1ZGlvZGF0YXJhdGUAQGSimlvaPKQADGF1ZGlvY29kZWNpZABAJAAAAAAAAAAACQ== 9 | 10 | 11 | -------------------------------------------------------------------------------- /yt_dlp/extractor/videodetective.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from .internetvideoarchive import InternetVideoArchiveIE 5 | 6 | 7 | class VideoDetectiveIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?videodetective\.com/[^/]+/[^/]+/(?P\d+)' 9 | 10 | _TEST = { 11 | 'url': 'http://www.videodetective.com/movies/kick-ass-2/194487', 12 | 'info_dict': { 13 | 'id': '194487', 14 | 'ext': 'mp4', 15 | 'title': 'Kick-Ass 2', 16 | 'description': 'md5:c189d5b7280400630a1d3dd17eaa8d8a', 17 | }, 18 | 'params': { 19 | # m3u8 download 20 | 'skip_download': True, 21 | }, 22 | } 23 | 24 | def _real_extract(self, url): 25 | video_id = self._match_id(url) 26 | query = 'customerid=69249&publishedid=' + video_id 27 | return self.url_result( 28 | InternetVideoArchiveIE._build_json_url(query), 29 | ie=InternetVideoArchiveIE.ie_key()) 30 | -------------------------------------------------------------------------------- /yt_dlp/extractor/myvidster.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class MyVidsterIE(InfoExtractor): 7 | _VALID_URL = r'https?://(?:www\.)?myvidster\.com/video/(?P\d+)/' 8 | 9 | _TEST = { 10 | 'url': 'http://www.myvidster.com/video/32059805/Hot_chemistry_with_raw_love_making', 11 | 'md5': '95296d0231c1363222c3441af62dc4ca', 12 | 'info_dict': { 13 | 'id': '3685814', 14 | 'title': 'md5:7d8427d6d02c4fbcef50fe269980c749', 15 | 'upload_date': '20141027', 16 | 'uploader': 'utkualp', 17 | 'ext': 'mp4', 18 | 'age_limit': 18, 19 | }, 20 | 'add_ie': ['XHamster'], 21 | } 22 | 23 | def _real_extract(self, url): 24 | video_id = self._match_id(url) 25 | webpage = self._download_webpage(url, video_id) 26 | 27 | return self.url_result(self._html_search_regex( 28 | r'rel="videolink" href="(?P.*)">', 29 | webpage, 'real video url')) 30 | -------------------------------------------------------------------------------- /yt_dlp/extractor/cinemax.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | 5 | from .hbo import HBOBaseIE 6 | 7 | 8 | class CinemaxIE(HBOBaseIE): 9 | _VALID_URL = r'https?://(?:www\.)?cinemax\.com/(?P[^/]+/video/[0-9a-z-]+-(?P\d+))' 10 | _TESTS = [{ 11 | 'url': 'https://www.cinemax.com/warrior/video/s1-ep-1-recap-20126903', 12 | 'md5': '82e0734bba8aa7ef526c9dd00cf35a05', 13 | 'info_dict': { 14 | 'id': '20126903', 15 | 'ext': 'mp4', 16 | 'title': 'S1 Ep 1: Recap', 17 | }, 18 | 'expected_warnings': ['Unknown MIME type application/mp4 in DASH manifest'], 19 | }, { 20 | 'url': 'https://www.cinemax.com/warrior/video/s1-ep-1-recap-20126903.embed', 21 | 'only_matching': True, 22 | }] 23 | 24 | def _real_extract(self, url): 25 | path, video_id = self._match_valid_url(url).groups() 26 | info = self._extract_info('https://www.cinemax.com/%s.xml' % path, video_id) 27 | info['id'] = video_id 28 | return info 29 | -------------------------------------------------------------------------------- /yt_dlp/extractor/lci.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class LCIIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?lci\.fr/[^/]+/[\w-]+-(?P\d+)\.html' 9 | _TEST = { 10 | 'url': 'http://www.lci.fr/international/etats-unis-a-j-62-hillary-clinton-reste-sans-voix-2001679.html', 11 | 'md5': '2fdb2538b884d4d695f9bd2bde137e6c', 12 | 'info_dict': { 13 | 'id': '13244802', 14 | 'ext': 'mp4', 15 | 'title': 'Hillary Clinton et sa quinte de toux, en plein meeting', 16 | 'description': 'md5:a4363e3a960860132f8124b62f4a01c9', 17 | } 18 | } 19 | 20 | def _real_extract(self, url): 21 | video_id = self._match_id(url) 22 | webpage = self._download_webpage(url, video_id) 23 | wat_id = self._search_regex( 24 | (r'data-watid=[\'"](\d+)', r'idwat["\']?\s*:\s*["\']?(\d+)'), 25 | webpage, 'wat id') 26 | return self.url_result('wat:' + wat_id, 'Wat', wat_id) 27 | -------------------------------------------------------------------------------- /.github/workflows/quick-test.yml: -------------------------------------------------------------------------------- 1 | name: Quick Test 2 | on: [push, pull_request] 3 | jobs: 4 | tests: 5 | name: Core Test 6 | if: "!contains(github.event.head_commit.message, 'ci skip all')" 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up Python 11 | uses: actions/setup-python@v2 12 | with: 13 | python-version: 3.9 14 | - name: Install test requirements 15 | run: pip install pytest pycryptodomex 16 | - name: Run tests 17 | run: ./devscripts/run_tests.sh core 18 | flake8: 19 | name: Linter 20 | if: "!contains(github.event.head_commit.message, 'ci skip all')" 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Set up Python 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: 3.9 28 | - name: Install flake8 29 | run: pip install flake8 30 | - name: Make lazy extractors 31 | run: python devscripts/make_lazy_extractors.py 32 | - name: Run flake8 33 | run: flake8 . 34 | -------------------------------------------------------------------------------- /yt_dlp/extractor/vimm.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from .common import InfoExtractor 3 | 4 | 5 | class VimmIE(InfoExtractor): 6 | _VALID_URL = r'https?://(?:www\.)?vimm\.tv/c/(?P[0-9a-z-]+)' 7 | _TESTS = [{ 8 | 'url': 'https://www.vimm.tv/c/calimeatwagon', 9 | 'info_dict': { 10 | 'id': 'calimeatwagon', 11 | 'ext': 'mp4', 12 | 'title': 're:^calimeatwagon [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$', 13 | 'live_status': 'is_live', 14 | }, 15 | 'skip': 'Live', 16 | }] 17 | 18 | def _real_extract(self, url): 19 | channel_id = self._match_id(url) 20 | 21 | formats, subs = self._extract_m3u8_formats_and_subtitles( 22 | f'https://www.vimm.tv/hls/{channel_id}.m3u8', channel_id, 'mp4', m3u8_id='hls', live=True) 23 | self._sort_formats(formats) 24 | 25 | return { 26 | 'id': channel_id, 27 | 'title': channel_id, 28 | 'is_live': True, 29 | 'formats': formats, 30 | 'subtitles': subs, 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/core.yml: -------------------------------------------------------------------------------- 1 | name: Core Tests 2 | on: [push, pull_request] 3 | jobs: 4 | tests: 5 | name: Core Tests 6 | if: "!contains(github.event.head_commit.message, 'ci skip')" 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-18.04] 12 | # py3.9 is in quick-test 13 | python-version: [3.7, 3.8, 3.10-dev, pypy-3.6, pypy-3.7] 14 | run-tests-ext: [sh] 15 | include: 16 | # atleast one of the tests must be in windows 17 | - os: windows-latest 18 | python-version: 3.6 19 | run-tests-ext: bat 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install pytest 27 | run: pip install pytest 28 | - name: Run tests 29 | continue-on-error: False 30 | run: ./devscripts/run_tests.${{ matrix.run-tests-ext }} core 31 | # Linter is in quick-test 32 | -------------------------------------------------------------------------------- /devscripts/gh-pages.unused/sign-versions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals, with_statement 3 | 4 | import rsa 5 | import json 6 | from binascii import hexlify 7 | 8 | try: 9 | input = raw_input 10 | except NameError: 11 | pass 12 | 13 | versions_info = json.load(open('update/versions.json')) 14 | if 'signature' in versions_info: 15 | del versions_info['signature'] 16 | 17 | print('Enter the PKCS1 private key, followed by a blank line:') 18 | privkey = b'' 19 | while True: 20 | try: 21 | line = input() 22 | except EOFError: 23 | break 24 | if line == '': 25 | break 26 | privkey += line.encode('ascii') + b'\n' 27 | privkey = rsa.PrivateKey.load_pkcs1(privkey) 28 | 29 | signature = hexlify(rsa.pkcs1.sign(json.dumps(versions_info, sort_keys=True).encode('utf-8'), privkey, 'SHA-256')).decode() 30 | print('signature: ' + signature) 31 | 32 | versions_info['signature'] = signature 33 | with open('update/versions.json', 'w') as versionsf: 34 | json.dump(versions_info, versionsf, indent=4, sort_keys=True) 35 | -------------------------------------------------------------------------------- /yt_dlp/extractor/nrl.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class NRLTVIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?nrl\.com/tv(/[^/]+)*/(?P[^/?&#]+)' 9 | _TEST = { 10 | 'url': 'https://www.nrl.com/tv/news/match-highlights-titans-v-knights-862805/', 11 | 'info_dict': { 12 | 'id': 'YyNnFuaDE6kPJqlDhG4CGQ_w89mKTau4', 13 | 'ext': 'mp4', 14 | 'title': 'Match Highlights: Titans v Knights', 15 | }, 16 | 'params': { 17 | # m3u8 download 18 | 'skip_download': True, 19 | }, 20 | } 21 | 22 | def _real_extract(self, url): 23 | display_id = self._match_id(url) 24 | webpage = self._download_webpage(url, display_id) 25 | q_data = self._parse_json(self._html_search_regex( 26 | r'(?s)q-data="({.+?})"', webpage, 'player data'), display_id) 27 | ooyala_id = q_data['videoId'] 28 | return self.url_result( 29 | 'ooyala:' + ooyala_id, 'Ooyala', ooyala_id, q_data.get('title')) 30 | -------------------------------------------------------------------------------- /test/test_youtube_misc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | # Allow direct execution 5 | import os 6 | import sys 7 | import unittest 8 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 9 | 10 | 11 | from yt_dlp.extractor import YoutubeIE 12 | 13 | 14 | class TestYoutubeMisc(unittest.TestCase): 15 | def test_youtube_extract(self): 16 | assertExtractId = lambda url, id: self.assertEqual(YoutubeIE.extract_id(url), id) 17 | assertExtractId('http://www.youtube.com/watch?&v=BaW_jenozKc', 'BaW_jenozKc') 18 | assertExtractId('https://www.youtube.com/watch?&v=BaW_jenozKc', 'BaW_jenozKc') 19 | assertExtractId('https://www.youtube.com/watch?feature=player_embedded&v=BaW_jenozKc', 'BaW_jenozKc') 20 | assertExtractId('https://www.youtube.com/watch_popup?v=BaW_jenozKc', 'BaW_jenozKc') 21 | assertExtractId('http://www.youtube.com/watch?v=BaW_jenozKcsharePLED17F32AD9753930', 'BaW_jenozKc') 22 | assertExtractId('BaW_jenozKc', 'BaW_jenozKc') 23 | 24 | 25 | if __name__ == '__main__': 26 | unittest.main() 27 | -------------------------------------------------------------------------------- /yt_dlp/extractor/formula1.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class Formula1IE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?formula1\.com/en/latest/video\.[^.]+\.(?P\d+)\.html' 9 | _TEST = { 10 | 'url': 'https://www.formula1.com/en/latest/video.race-highlights-spain-2016.6060988138001.html', 11 | 'md5': 'be7d3a8c2f804eb2ab2aa5d941c359f8', 12 | 'info_dict': { 13 | 'id': '6060988138001', 14 | 'ext': 'mp4', 15 | 'title': 'Race highlights - Spain 2016', 16 | 'timestamp': 1463332814, 17 | 'upload_date': '20160515', 18 | 'uploader_id': '6057949432001', 19 | }, 20 | 'add_ie': ['BrightcoveNew'], 21 | } 22 | BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/6057949432001/S1WMrhjlh_default/index.html?videoId=%s' 23 | 24 | def _real_extract(self, url): 25 | bc_id = self._match_id(url) 26 | return self.url_result( 27 | self.BRIGHTCOVE_URL_TEMPLATE % bc_id, 'BrightcoveNew', bc_id) 28 | -------------------------------------------------------------------------------- /yt_dlp/extractor/outsidetv.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class OutsideTVIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?outsidetv\.com/(?:[^/]+/)*?play/[a-zA-Z0-9]{8}/\d+/\d+/(?P[a-zA-Z0-9]{8})' 9 | _TESTS = [{ 10 | 'url': 'http://www.outsidetv.com/category/snow/play/ZjQYboH6/1/10/Hdg0jukV/4', 11 | 'md5': '192d968fedc10b2f70ec31865ffba0da', 12 | 'info_dict': { 13 | 'id': 'Hdg0jukV', 14 | 'ext': 'mp4', 15 | 'title': 'Home - Jackson Ep 1 | Arbor Snowboards', 16 | 'description': 'md5:41a12e94f3db3ca253b04bb1e8d8f4cd', 17 | 'upload_date': '20181225', 18 | 'timestamp': 1545742800, 19 | } 20 | }, { 21 | 'url': 'http://www.outsidetv.com/home/play/ZjQYboH6/1/10/Hdg0jukV/4', 22 | 'only_matching': True, 23 | }] 24 | 25 | def _real_extract(self, url): 26 | jw_media_id = self._match_id(url) 27 | return self.url_result( 28 | 'jwplatform:' + jw_media_id, 'JWPlatform', jw_media_id) 29 | -------------------------------------------------------------------------------- /devscripts/lazy_load_template.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import re 3 | 4 | from ..utils import bug_reports_message, write_string 5 | 6 | 7 | class LazyLoadMetaClass(type): 8 | def __getattr__(cls, name): 9 | if '_real_class' not in cls.__dict__: 10 | write_string( 11 | f'WARNING: Falling back to normal extractor since lazy extractor ' 12 | f'{cls.__name__} does not have attribute {name}{bug_reports_message()}') 13 | return getattr(cls._get_real_class(), name) 14 | 15 | 16 | class LazyLoadExtractor(metaclass=LazyLoadMetaClass): 17 | _module = None 18 | _WORKING = True 19 | 20 | @classmethod 21 | def _get_real_class(cls): 22 | if '_real_class' not in cls.__dict__: 23 | mod = __import__(cls._module, fromlist=(cls.__name__,)) 24 | cls._real_class = getattr(mod, cls.__name__) 25 | return cls._real_class 26 | 27 | def __new__(cls, *args, **kwargs): 28 | real_cls = cls._get_real_class() 29 | instance = real_cls.__new__(real_cls) 30 | instance.__init__(*args, **kwargs) 31 | return instance 32 | -------------------------------------------------------------------------------- /yt_dlp/extractor/ku6.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class Ku6IE(InfoExtractor): 7 | _VALID_URL = r'https?://v\.ku6\.com/show/(?P[a-zA-Z0-9\-\_]+)(?:\.)*html' 8 | _TEST = { 9 | 'url': 'http://v.ku6.com/show/JG-8yS14xzBr4bCn1pu0xw...html', 10 | 'md5': '01203549b9efbb45f4b87d55bdea1ed1', 11 | 'info_dict': { 12 | 'id': 'JG-8yS14xzBr4bCn1pu0xw', 13 | 'ext': 'f4v', 14 | 'title': 'techniques test', 15 | } 16 | } 17 | 18 | def _real_extract(self, url): 19 | video_id = self._match_id(url) 20 | webpage = self._download_webpage(url, video_id) 21 | 22 | title = self._html_search_regex( 23 | r'

(.*?)

', webpage, 'title') 24 | dataUrl = 'http://v.ku6.com/fetchVideo4Player/%s.html' % video_id 25 | jsonData = self._download_json(dataUrl, video_id) 26 | downloadUrl = jsonData['data']['f'] 27 | 28 | return { 29 | 'id': video_id, 30 | 'title': title, 31 | 'url': downloadUrl 32 | } 33 | -------------------------------------------------------------------------------- /yt_dlp/extractor/anvato_token_generator/nfl.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import json 4 | 5 | from .common import TokenGenerator 6 | 7 | 8 | class NFLTokenGenerator(TokenGenerator): 9 | _AUTHORIZATION = None 10 | 11 | def generate(ie, anvack, mcp_id): 12 | if not NFLTokenGenerator._AUTHORIZATION: 13 | reroute = ie._download_json( 14 | 'https://api.nfl.com/v1/reroute', mcp_id, 15 | data=b'grant_type=client_credentials', 16 | headers={'X-Domain-Id': 100}) 17 | NFLTokenGenerator._AUTHORIZATION = '%s %s' % (reroute.get('token_type') or 'Bearer', reroute['access_token']) 18 | return ie._download_json( 19 | 'https://api.nfl.com/v3/shield/', mcp_id, data=json.dumps({ 20 | 'query': '''{ 21 | viewer { 22 | mediaToken(anvack: "%s", id: %s) { 23 | token 24 | } 25 | } 26 | }''' % (anvack, mcp_id), 27 | }).encode(), headers={ 28 | 'Authorization': NFLTokenGenerator._AUTHORIZATION, 29 | 'Content-Type': 'application/json', 30 | })['data']['viewer']['mediaToken']['token'] 31 | -------------------------------------------------------------------------------- /yt_dlp/extractor/vodpl.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .onet import OnetBaseIE 5 | 6 | 7 | class VODPlIE(OnetBaseIE): 8 | _VALID_URL = r'https?://vod\.pl/(?:[^/]+/)+(?P[0-9a-zA-Z]+)' 9 | 10 | _TESTS = [{ 11 | 'url': 'https://vod.pl/filmy/chlopaki-nie-placza/3ep3jns', 12 | 'md5': 'a7dc3b2f7faa2421aefb0ecaabf7ec74', 13 | 'info_dict': { 14 | 'id': '3ep3jns', 15 | 'ext': 'mp4', 16 | 'title': 'Chłopaki nie płaczą', 17 | 'description': 'md5:f5f03b84712e55f5ac9f0a3f94445224', 18 | 'timestamp': 1463415154, 19 | 'duration': 5765, 20 | 'upload_date': '20160516', 21 | }, 22 | }, { 23 | 'url': 'https://vod.pl/seriale/belfer-na-planie-praca-kamery-online/2c10heh', 24 | 'only_matching': True, 25 | }] 26 | 27 | def _real_extract(self, url): 28 | video_id = self._match_id(url) 29 | webpage = self._download_webpage(url, video_id) 30 | info_dict = self._extract_from_id(self._search_mvp_id(webpage), webpage) 31 | info_dict['id'] = video_id 32 | return info_dict 33 | -------------------------------------------------------------------------------- /yt_dlp/extractor/cliprs.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .onet import OnetBaseIE 5 | 6 | 7 | class ClipRsIE(OnetBaseIE): 8 | _VALID_URL = r'https?://(?:www\.)?clip\.rs/(?P[^/]+)/\d+' 9 | _TEST = { 10 | 'url': 'http://www.clip.rs/premijera-frajle-predstavljaju-novi-spot-za-pesmu-moli-me-moli/3732', 11 | 'md5': 'c412d57815ba07b56f9edc7b5d6a14e5', 12 | 'info_dict': { 13 | 'id': '1488842.1399140381', 14 | 'ext': 'mp4', 15 | 'title': 'PREMIJERA Frajle predstavljaju novi spot za pesmu Moli me, moli', 16 | 'description': 'md5:56ce2c3b4ab31c5a2e0b17cb9a453026', 17 | 'duration': 229, 18 | 'timestamp': 1459850243, 19 | 'upload_date': '20160405', 20 | } 21 | } 22 | 23 | def _real_extract(self, url): 24 | display_id = self._match_id(url) 25 | 26 | webpage = self._download_webpage(url, display_id) 27 | 28 | mvp_id = self._search_mvp_id(webpage) 29 | 30 | info_dict = self._extract_from_id(mvp_id, webpage) 31 | info_dict['display_id'] = display_id 32 | 33 | return info_dict 34 | -------------------------------------------------------------------------------- /yt_dlp/extractor/streamff.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from .common import InfoExtractor 3 | from ..utils import int_or_none, parse_iso8601 4 | 5 | 6 | class StreamFFIE(InfoExtractor): 7 | _VALID_URL = r'https?://(?:www\.)?streamff\.com/v/(?P[a-zA-Z0-9]+)' 8 | 9 | _TESTS = [{ 10 | 'url': 'https://streamff.com/v/55cc94', 11 | 'md5': '8745a67bb5e5c570738efe7983826370', 12 | 'info_dict': { 13 | 'id': '55cc94', 14 | 'ext': 'mp4', 15 | 'title': '55cc94', 16 | 'timestamp': 1634764643, 17 | 'upload_date': '20211020', 18 | 'view_count': int, 19 | } 20 | }] 21 | 22 | def _real_extract(self, url): 23 | video_id = self._match_id(url) 24 | json_data = self._download_json(f'https://streamff.com/api/videos/{video_id}', video_id) 25 | return { 26 | 'id': video_id, 27 | 'title': json_data.get('name') or video_id, 28 | 'url': 'https://streamff.com/%s' % json_data['videoLink'], 29 | 'view_count': int_or_none(json_data.get('views')), 30 | 'timestamp': parse_iso8601(json_data.get('date')), 31 | } 32 | -------------------------------------------------------------------------------- /yt_dlp/extractor/freespeech.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from .youtube import YoutubeIE 5 | 6 | 7 | class FreespeechIE(InfoExtractor): 8 | IE_NAME = 'freespeech.org' 9 | _VALID_URL = r'https?://(?:www\.)?freespeech\.org/stories/(?P.+)' 10 | _TEST = { 11 | 'add_ie': ['Youtube'], 12 | 'url': 'http://www.freespeech.org/stories/fcc-announces-net-neutrality-rollback-whats-stake/', 13 | 'info_dict': { 14 | 'id': 'waRk6IPqyWM', 15 | 'ext': 'mp4', 16 | 'title': 'What\'s At Stake - Net Neutrality Special', 17 | 'description': 'Presented by MNN and FSTV', 18 | 'upload_date': '20170728', 19 | 'uploader_id': 'freespeechtv', 20 | 'uploader': 'freespeechtv', 21 | }, 22 | } 23 | 24 | def _real_extract(self, url): 25 | display_id = self._match_id(url) 26 | webpage = self._download_webpage(url, display_id) 27 | youtube_url = self._search_regex( 28 | r'data-video-url="([^"]+)"', 29 | webpage, 'youtube url') 30 | 31 | return self.url_result(youtube_url, YoutubeIE.ie_key()) 32 | -------------------------------------------------------------------------------- /devscripts/gh-pages.unused/update-sites.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | import sys 5 | import os 6 | import textwrap 7 | 8 | # We must be able to import yt_dlp 9 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) 10 | 11 | import yt_dlp 12 | 13 | 14 | def main(): 15 | with open('supportedsites.html.in', 'r', encoding='utf-8') as tmplf: 16 | template = tmplf.read() 17 | 18 | ie_htmls = [] 19 | for ie in yt_dlp.list_extractors(age_limit=None): 20 | ie_html = '{}'.format(ie.IE_NAME) 21 | ie_desc = getattr(ie, 'IE_DESC', None) 22 | if ie_desc is False: 23 | continue 24 | elif ie_desc is not None: 25 | ie_html += ': {}'.format(ie.IE_DESC) 26 | if not ie.working(): 27 | ie_html += ' (Currently broken)' 28 | ie_htmls.append('
  • {}
  • '.format(ie_html)) 29 | 30 | template = template.replace('@SITES@', textwrap.indent('\n'.join(ie_htmls), '\t')) 31 | 32 | with open('supportedsites.html', 'w', encoding='utf-8') as sitesf: 33 | sitesf.write(template) 34 | 35 | 36 | if __name__ == '__main__': 37 | main() 38 | -------------------------------------------------------------------------------- /yt_dlp/extractor/savefrom.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | import os.path 5 | 6 | from .common import InfoExtractor 7 | 8 | 9 | class SaveFromIE(InfoExtractor): 10 | IE_NAME = 'savefrom.net' 11 | _VALID_URL = r'https?://[^.]+\.savefrom\.net/\#url=(?P.*)$' 12 | 13 | _TEST = { 14 | 'url': 'http://en.savefrom.net/#url=http://youtube.com/watch?v=UlVRAPW2WJY&utm_source=youtube.com&utm_medium=short_domains&utm_campaign=ssyoutube.com', 15 | 'info_dict': { 16 | 'id': 'UlVRAPW2WJY', 17 | 'ext': 'mp4', 18 | 'title': 'About Team Radical MMA | MMA Fighting', 19 | 'upload_date': '20120816', 20 | 'uploader': 'Howcast', 21 | 'uploader_id': 'Howcast', 22 | 'description': r're:(?s).* Hi, my name is Rene Dreifuss\. And I\'m here to show you some MMA.*', 23 | }, 24 | 'params': { 25 | 'skip_download': True 26 | } 27 | } 28 | 29 | def _real_extract(self, url): 30 | mobj = self._match_valid_url(url) 31 | video_id = os.path.splitext(url.split('/')[-1])[0] 32 | 33 | return self.url_result(mobj.group('url'), video_id=video_id) 34 | -------------------------------------------------------------------------------- /yt_dlp/extractor/teachingchannel.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class TeachingChannelIE(InfoExtractor): 7 | _VALID_URL = r'https?://(?:www\.)?teachingchannel\.org/videos?/(?P[^/?&#]+)' 8 | 9 | _TEST = { 10 | 'url': 'https://www.teachingchannel.org/videos/teacher-teaming-evolution', 11 | 'info_dict': { 12 | 'id': '3swwlzkT', 13 | 'ext': 'mp4', 14 | 'title': 'A History of Teaming', 15 | 'description': 'md5:2a9033db8da81f2edffa4c99888140b3', 16 | 'duration': 422, 17 | 'upload_date': '20170316', 18 | 'timestamp': 1489691297, 19 | }, 20 | 'params': { 21 | 'skip_download': True, 22 | }, 23 | 'add_ie': ['JWPlatform'], 24 | } 25 | 26 | def _real_extract(self, url): 27 | display_id = self._match_id(url) 28 | webpage = self._download_webpage(url, display_id) 29 | mid = self._search_regex( 30 | r'(?:data-mid=["\']|id=["\']jw-video-player-)([a-zA-Z0-9]{8})', 31 | webpage, 'media id') 32 | 33 | return self.url_result('jwplatform:' + mid, 'JWPlatform', mid) 34 | -------------------------------------------------------------------------------- /yt_dlp/extractor/bibeltv.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class BibelTVIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?bibeltv\.de/mediathek/videos/(?:crn/)?(?P\d+)' 9 | _TESTS = [{ 10 | 'url': 'https://www.bibeltv.de/mediathek/videos/329703-sprachkurs-in-malaiisch', 11 | 'md5': '252f908192d611de038b8504b08bf97f', 12 | 'info_dict': { 13 | 'id': 'ref:329703', 14 | 'ext': 'mp4', 15 | 'title': 'Sprachkurs in Malaiisch', 16 | 'description': 'md5:3e9f197d29ee164714e67351cf737dfe', 17 | 'timestamp': 1608316701, 18 | 'uploader_id': '5840105145001', 19 | 'upload_date': '20201218', 20 | } 21 | }, { 22 | 'url': 'https://www.bibeltv.de/mediathek/videos/crn/326374', 23 | 'only_matching': True, 24 | }] 25 | BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/5840105145001/default_default/index.html?videoId=ref:%s' 26 | 27 | def _real_extract(self, url): 28 | crn_id = self._match_id(url) 29 | return self.url_result( 30 | self.BRIGHTCOVE_URL_TEMPLATE % crn_id, 'BrightcoveNew') 31 | -------------------------------------------------------------------------------- /yt_dlp/extractor/ebaumsworld.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class EbaumsWorldIE(InfoExtractor): 7 | _VALID_URL = r'https?://(?:www\.)?ebaumsworld\.com/videos/[^/]+/(?P\d+)' 8 | 9 | _TEST = { 10 | 'url': 'http://www.ebaumsworld.com/videos/a-giant-python-opens-the-door/83367677/', 11 | 'info_dict': { 12 | 'id': '83367677', 13 | 'ext': 'mp4', 14 | 'title': 'A Giant Python Opens The Door', 15 | 'description': 'This is how nightmares start...', 16 | 'uploader': 'jihadpizza', 17 | }, 18 | } 19 | 20 | def _real_extract(self, url): 21 | video_id = self._match_id(url) 22 | config = self._download_xml( 23 | 'http://www.ebaumsworld.com/video/player/%s' % video_id, video_id) 24 | video_url = config.find('file').text 25 | 26 | return { 27 | 'id': video_id, 28 | 'title': config.find('title').text, 29 | 'url': video_url, 30 | 'description': config.find('description').text, 31 | 'thumbnail': config.find('image').text, 32 | 'uploader': config.find('username').text, 33 | } 34 | -------------------------------------------------------------------------------- /test/test_update.py.disabled: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | 5 | # Allow direct execution 6 | import os 7 | import sys 8 | import unittest 9 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 10 | 11 | 12 | import json 13 | from yt_dlp.update import rsa_verify 14 | 15 | 16 | class TestUpdate(unittest.TestCase): 17 | def test_rsa_verify(self): 18 | UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537) 19 | with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'versions.json'), 'rb') as f: 20 | versions_info = f.read().decode() 21 | versions_info = json.loads(versions_info) 22 | signature = versions_info['signature'] 23 | del versions_info['signature'] 24 | self.assertTrue(rsa_verify( 25 | json.dumps(versions_info, sort_keys=True).encode('utf-8'), 26 | signature, UPDATES_RSA_KEY)) 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /yt_dlp/extractor/foxsports.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class FoxSportsIE(InfoExtractor): 7 | _VALID_URL = r'https?://(?:www\.)?foxsports\.com/(?:[^/]+/)*video/(?P\d+)' 8 | 9 | _TEST = { 10 | 'url': 'http://www.foxsports.com/tennessee/video/432609859715', 11 | 'md5': 'b49050e955bebe32c301972e4012ac17', 12 | 'info_dict': { 13 | 'id': '432609859715', 14 | 'ext': 'mp4', 15 | 'title': 'Courtney Lee on going up 2-0 in series vs. Blazers', 16 | 'description': 'Courtney Lee talks about Memphis being focused.', 17 | # TODO: fix timestamp 18 | 'upload_date': '19700101', # '20150423', 19 | # 'timestamp': 1429761109, 20 | 'uploader': 'NEWA-FNG-FOXSPORTS', 21 | }, 22 | 'params': { 23 | # m3u8 download 24 | 'skip_download': True, 25 | }, 26 | 'add_ie': ['ThePlatform'], 27 | } 28 | 29 | def _real_extract(self, url): 30 | video_id = self._match_id(url) 31 | 32 | return self.url_result( 33 | 'https://feed.theplatform.com/f/BKQ29B/foxsports-all?byId=' + video_id, 'ThePlatformFeed') 34 | -------------------------------------------------------------------------------- /devscripts/update-formulae.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | import json 5 | import os 6 | import re 7 | import sys 8 | 9 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 10 | 11 | from yt_dlp.compat import compat_urllib_request 12 | 13 | 14 | # usage: python3 ./devscripts/update-formulae.py 15 | # version can be either 0-aligned (yt-dlp version) or normalized (PyPl version) 16 | 17 | filename, version = sys.argv[1:] 18 | 19 | normalized_version = '.'.join(str(int(x)) for x in version.split('.')) 20 | 21 | pypi_release = json.loads(compat_urllib_request.urlopen( 22 | 'https://pypi.org/pypi/yt-dlp/%s/json' % normalized_version 23 | ).read().decode('utf-8')) 24 | 25 | tarball_file = next(x for x in pypi_release['urls'] if x['filename'].endswith('.tar.gz')) 26 | 27 | sha256sum = tarball_file['digests']['sha256'] 28 | url = tarball_file['url'] 29 | 30 | with open(filename, 'r') as r: 31 | formulae_text = r.read() 32 | 33 | formulae_text = re.sub(r'sha256 "[0-9a-f]*?"', 'sha256 "%s"' % sha256sum, formulae_text) 34 | formulae_text = re.sub(r'url "[^"]*?"', 'url "%s"' % url, formulae_text) 35 | 36 | with open(filename, 'w') as w: 37 | w.write(formulae_text) 38 | -------------------------------------------------------------------------------- /yt_dlp/extractor/cableav.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from .common import InfoExtractor 3 | 4 | 5 | class CableAVIE(InfoExtractor): 6 | _VALID_URL = r'https://cableav\.tv/(?P[a-zA-Z0-9]+)' 7 | _TESTS = [{ 8 | 'url': 'https://cableav.tv/lS4iR9lWjN8/', 9 | 'md5': '7e3fe5e49d61c4233b7f5b0f69b15e18', 10 | 'info_dict': { 11 | 'id': 'lS4iR9lWjN8', 12 | 'ext': 'mp4', 13 | 'title': '國產麻豆AV 叮叮映畫 DDF001 情欲小說家 - CableAV', 14 | 'description': '國產AV 480p, 720p 国产麻豆AV 叮叮映画 DDF001 情欲小说家', 15 | 'thumbnail': r're:^https?://.*\.jpg$', 16 | } 17 | }] 18 | 19 | def _real_extract(self, url): 20 | video_id = self._match_id(url) 21 | webpage = self._download_webpage(url, video_id) 22 | 23 | video_url = self._og_search_video_url(webpage, secure=False) 24 | 25 | formats = self._extract_m3u8_formats(video_url, video_id, 'mp4') 26 | self._sort_formats(formats) 27 | 28 | return { 29 | 'id': video_id, 30 | 'title': self._og_search_title(webpage), 31 | 'description': self._og_search_description(webpage), 32 | 'thumbnail': self._og_search_thumbnail(webpage), 33 | 'formats': formats, 34 | } 35 | -------------------------------------------------------------------------------- /yt_dlp/extractor/people.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class PeopleIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?people\.com/people/videos/0,,(?P\d+),00\.html' 9 | 10 | _TEST = { 11 | 'url': 'http://www.people.com/people/videos/0,,20995451,00.html', 12 | 'info_dict': { 13 | 'id': 'ref:20995451', 14 | 'ext': 'mp4', 15 | 'title': 'Astronaut Love Triangle Victim Speaks Out: “The Crime in 2007 Hasn’t Defined Us”', 16 | 'description': 'Colleen Shipman speaks to PEOPLE for the first time about life after the attack', 17 | 'thumbnail': r're:^https?://.*\.jpg', 18 | 'duration': 246.318, 19 | 'timestamp': 1458720585, 20 | 'upload_date': '20160323', 21 | 'uploader_id': '416418724', 22 | }, 23 | 'params': { 24 | 'skip_download': True, 25 | }, 26 | 'add_ie': ['BrightcoveNew'], 27 | } 28 | 29 | def _real_extract(self, url): 30 | return self.url_result( 31 | 'http://players.brightcove.net/416418724/default_default/index.html?videoId=ref:%s' 32 | % self._match_id(url), 'BrightcoveNew') 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /yt_dlp/extractor/nerdcubed.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | import datetime 5 | 6 | from .common import InfoExtractor 7 | 8 | 9 | class NerdCubedFeedIE(InfoExtractor): 10 | _VALID_URL = r'https?://(?:www\.)?nerdcubed\.co\.uk/feed\.json' 11 | _TEST = { 12 | 'url': 'http://www.nerdcubed.co.uk/feed.json', 13 | 'info_dict': { 14 | 'id': 'nerdcubed-feed', 15 | 'title': 'nerdcubed.co.uk feed', 16 | }, 17 | 'playlist_mincount': 1300, 18 | } 19 | 20 | def _real_extract(self, url): 21 | feed = self._download_json(url, url, 'Downloading NerdCubed JSON feed') 22 | 23 | entries = [{ 24 | '_type': 'url', 25 | 'title': feed_entry['title'], 26 | 'uploader': feed_entry['source']['name'] if feed_entry['source'] else None, 27 | 'upload_date': datetime.datetime.strptime(feed_entry['date'], '%Y-%m-%d').strftime('%Y%m%d'), 28 | 'url': 'http://www.youtube.com/watch?v=' + feed_entry['youtube_id'], 29 | } for feed_entry in feed] 30 | 31 | return { 32 | '_type': 'playlist', 33 | 'title': 'nerdcubed.co.uk feed', 34 | 'id': 'nerdcubed-feed', 35 | 'entries': entries, 36 | } 37 | -------------------------------------------------------------------------------- /yt_dlp/extractor/nonktube.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .nuevo import NuevoBaseIE 4 | 5 | 6 | class NonkTubeIE(NuevoBaseIE): 7 | _VALID_URL = r'https?://(?:www\.)?nonktube\.com/(?:(?:video|embed)/|media/nuevo/embed\.php\?.*?\bid=)(?P\d+)' 8 | _TESTS = [{ 9 | 'url': 'https://www.nonktube.com/video/118636/sensual-wife-uncensored-fucked-in-hairy-pussy-and-facialized', 10 | 'info_dict': { 11 | 'id': '118636', 12 | 'ext': 'mp4', 13 | 'title': 'Sensual Wife Uncensored Fucked In Hairy Pussy And Facialized', 14 | 'age_limit': 18, 15 | 'duration': 1150.98, 16 | }, 17 | 'params': { 18 | 'skip_download': True, 19 | } 20 | }, { 21 | 'url': 'https://www.nonktube.com/embed/118636', 22 | 'only_matching': True, 23 | }] 24 | 25 | def _real_extract(self, url): 26 | video_id = self._match_id(url) 27 | 28 | webpage = self._download_webpage(url, video_id) 29 | 30 | title = self._og_search_title(webpage) 31 | info = self._parse_html5_media_entries(url, webpage, video_id)[0] 32 | 33 | info.update({ 34 | 'id': video_id, 35 | 'title': title, 36 | 'age_limit': 18, 37 | }) 38 | return info 39 | -------------------------------------------------------------------------------- /devscripts/update-version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from datetime import datetime 3 | import sys 4 | import subprocess 5 | 6 | 7 | with open('yt_dlp/version.py', 'rt') as f: 8 | exec(compile(f.read(), 'yt_dlp/version.py', 'exec')) 9 | old_version = locals()['__version__'] 10 | 11 | old_version_list = old_version.split('.') 12 | 13 | old_ver = '.'.join(old_version_list[:3]) 14 | old_rev = old_version_list[3] if len(old_version_list) > 3 else '' 15 | 16 | ver = datetime.utcnow().strftime("%Y.%m.%d") 17 | 18 | rev = (sys.argv[1:] or [''])[0] # Use first argument, if present as revision number 19 | if not rev: 20 | rev = str(int(old_rev or 0) + 1) if old_ver == ver else '' 21 | 22 | VERSION = '.'.join((ver, rev)) if rev else ver 23 | 24 | try: 25 | sp = subprocess.Popen(['git', 'rev-parse', '--short', 'HEAD'], stdout=subprocess.PIPE) 26 | GIT_HEAD = sp.communicate()[0].decode().strip() or None 27 | except Exception: 28 | GIT_HEAD = None 29 | 30 | VERSION_FILE = f'''\ 31 | # Autogenerated by devscripts/update-version.py 32 | 33 | __version__ = {VERSION!r} 34 | 35 | RELEASE_GIT_HEAD = {GIT_HEAD!r} 36 | ''' 37 | 38 | with open('yt_dlp/version.py', 'wt') as f: 39 | f.write(VERSION_FILE) 40 | 41 | print('::set-output name=ytdlp_version::' + VERSION) 42 | print(f'\nVersion = {VERSION}, Git HEAD = {GIT_HEAD}') 43 | -------------------------------------------------------------------------------- /yt_dlp/extractor/lovehomeporn.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | 4 | from .nuevo import NuevoBaseIE 5 | 6 | 7 | class LoveHomePornIE(NuevoBaseIE): 8 | _VALID_URL = r'https?://(?:www\.)?lovehomeporn\.com/video/(?P\d+)(?:/(?P[^/?#&]+))?' 9 | _TEST = { 10 | 'url': 'http://lovehomeporn.com/video/48483/stunning-busty-brunette-girlfriend-sucking-and-riding-a-big-dick#menu', 11 | 'info_dict': { 12 | 'id': '48483', 13 | 'display_id': 'stunning-busty-brunette-girlfriend-sucking-and-riding-a-big-dick', 14 | 'ext': 'mp4', 15 | 'title': 'Stunning busty brunette girlfriend sucking and riding a big dick', 16 | 'age_limit': 18, 17 | 'duration': 238.47, 18 | }, 19 | 'params': { 20 | 'skip_download': True, 21 | } 22 | } 23 | 24 | def _real_extract(self, url): 25 | mobj = self._match_valid_url(url) 26 | video_id = mobj.group('id') 27 | display_id = mobj.group('display_id') 28 | 29 | info = self._extract_nuevo( 30 | 'http://lovehomeporn.com/media/nuevo/config.php?key=%s' % video_id, 31 | video_id) 32 | info.update({ 33 | 'display_id': display_id, 34 | 'age_limit': 18 35 | }) 36 | return info 37 | -------------------------------------------------------------------------------- /devscripts/generate_aes_testdata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | import codecs 5 | import subprocess 6 | 7 | import os 8 | import sys 9 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 10 | 11 | from yt_dlp.utils import intlist_to_bytes 12 | from yt_dlp.aes import aes_encrypt, key_expansion 13 | 14 | secret_msg = b'Secret message goes here' 15 | 16 | 17 | def hex_str(int_list): 18 | return codecs.encode(intlist_to_bytes(int_list), 'hex') 19 | 20 | 21 | def openssl_encode(algo, key, iv): 22 | cmd = ['openssl', 'enc', '-e', '-' + algo, '-K', hex_str(key), '-iv', hex_str(iv)] 23 | prog = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 24 | out, _ = prog.communicate(secret_msg) 25 | return out 26 | 27 | 28 | iv = key = [0x20, 0x15] + 14 * [0] 29 | 30 | r = openssl_encode('aes-128-cbc', key, iv) 31 | print('aes_cbc_decrypt') 32 | print(repr(r)) 33 | 34 | password = key 35 | new_key = aes_encrypt(password, key_expansion(password)) 36 | r = openssl_encode('aes-128-ctr', new_key, iv) 37 | print('aes_decrypt_text 16') 38 | print(repr(r)) 39 | 40 | password = key + 16 * [0] 41 | new_key = aes_encrypt(password, key_expansion(password)) * (32 // 16) 42 | r = openssl_encode('aes-256-ctr', new_key, iv) 43 | print('aes_decrypt_text 32') 44 | print(repr(r)) 45 | -------------------------------------------------------------------------------- /yt_dlp/extractor/cam4.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class CAM4IE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:[^/]+\.)?cam4\.com/(?P[a-z0-9_]+)' 9 | _TEST = { 10 | 'url': 'https://www.cam4.com/foxynesss', 11 | 'info_dict': { 12 | 'id': 'foxynesss', 13 | 'ext': 'mp4', 14 | 'title': 're:^foxynesss [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$', 15 | 'age_limit': 18, 16 | 'live_status': 'is_live', 17 | 'thumbnail': 'https://snapshots.xcdnpro.com/thumbnails/foxynesss', 18 | } 19 | } 20 | 21 | def _real_extract(self, url): 22 | channel_id = self._match_id(url) 23 | m3u8_playlist = self._download_json('https://www.cam4.com/rest/v1.0/profile/{}/streamInfo'.format(channel_id), channel_id).get('cdnURL') 24 | 25 | formats = self._extract_m3u8_formats(m3u8_playlist, channel_id, 'mp4', m3u8_id='hls', live=True) 26 | self._sort_formats(formats) 27 | 28 | return { 29 | 'id': channel_id, 30 | 'title': channel_id, 31 | 'is_live': True, 32 | 'age_limit': 18, 33 | 'formats': formats, 34 | 'thumbnail': f'https://snapshots.xcdnpro.com/thumbnails/{channel_id}', 35 | } 36 | -------------------------------------------------------------------------------- /yt_dlp/extractor/gputechconf.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class GPUTechConfIE(InfoExtractor): 8 | _VALID_URL = r'https?://on-demand\.gputechconf\.com/gtc/2015/video/S(?P\d+)\.html' 9 | _TEST = { 10 | 'url': 'http://on-demand.gputechconf.com/gtc/2015/video/S5156.html', 11 | 'md5': 'a8862a00a0fd65b8b43acc5b8e33f798', 12 | 'info_dict': { 13 | 'id': '5156', 14 | 'ext': 'mp4', 15 | 'title': 'Coordinating More Than 3 Million CUDA Threads for Social Network Analysis', 16 | 'duration': 1219, 17 | } 18 | } 19 | 20 | def _real_extract(self, url): 21 | video_id = self._match_id(url) 22 | webpage = self._download_webpage(url, video_id) 23 | 24 | root_path = self._search_regex( 25 | r'var\s+rootPath\s*=\s*"([^"]+)', webpage, 'root path', 26 | default='http://evt.dispeak.com/nvidia/events/gtc15/') 27 | xml_file_id = self._search_regex( 28 | r'var\s+xmlFileId\s*=\s*"([^"]+)', webpage, 'xml file id') 29 | 30 | return { 31 | '_type': 'url_transparent', 32 | 'id': video_id, 33 | 'url': '%sxml/%s.xml' % (root_path, xml_file_id), 34 | 'ie_key': 'DigitallySpeaking', 35 | } 36 | -------------------------------------------------------------------------------- /devscripts/gh-pages.unused/add-version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | import json 5 | import sys 6 | import hashlib 7 | import os.path 8 | 9 | 10 | if len(sys.argv) <= 1: 11 | print('Specify the version number as parameter') 12 | sys.exit() 13 | version = sys.argv[1] 14 | 15 | with open('update/LATEST_VERSION', 'w') as f: 16 | f.write(version) 17 | 18 | versions_info = json.load(open('update/versions.json')) 19 | if 'signature' in versions_info: 20 | del versions_info['signature'] 21 | 22 | new_version = {} 23 | 24 | filenames = { 25 | 'bin': 'yt-dlp', 26 | 'exe': 'yt-dlp.exe', 27 | 'tar': 'yt-dlp-%s.tar.gz' % version} 28 | build_dir = os.path.join('..', '..', 'build', version) 29 | for key, filename in filenames.items(): 30 | url = 'https://yt-dl.org/downloads/%s/%s' % (version, filename) 31 | fn = os.path.join(build_dir, filename) 32 | with open(fn, 'rb') as f: 33 | data = f.read() 34 | if not data: 35 | raise ValueError('File %s is empty!' % fn) 36 | sha256sum = hashlib.sha256(data).hexdigest() 37 | new_version[key] = (url, sha256sum) 38 | 39 | versions_info['versions'][version] = new_version 40 | versions_info['latest'] = version 41 | 42 | with open('update/versions.json', 'w') as jsonf: 43 | json.dump(versions_info, jsonf, indent=4, sort_keys=True) 44 | -------------------------------------------------------------------------------- /test/test_iqiyi_sdk_interpreter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import unicode_literals 4 | 5 | # Allow direct execution 6 | import os 7 | import sys 8 | import unittest 9 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 10 | 11 | from test.helper import FakeYDL, is_download_test 12 | from yt_dlp.extractor import IqiyiIE 13 | 14 | 15 | class IqiyiIEWithCredentials(IqiyiIE): 16 | def _get_login_info(self): 17 | return 'foo', 'bar' 18 | 19 | 20 | class WarningLogger(object): 21 | def __init__(self): 22 | self.messages = [] 23 | 24 | def warning(self, msg): 25 | self.messages.append(msg) 26 | 27 | def debug(self, msg): 28 | pass 29 | 30 | def error(self, msg): 31 | pass 32 | 33 | 34 | @is_download_test 35 | class TestIqiyiSDKInterpreter(unittest.TestCase): 36 | def test_iqiyi_sdk_interpreter(self): 37 | ''' 38 | Test the functionality of IqiyiSDKInterpreter by trying to log in 39 | 40 | If `sign` is incorrect, /validate call throws an HTTP 556 error 41 | ''' 42 | logger = WarningLogger() 43 | ie = IqiyiIEWithCredentials(FakeYDL({'logger': logger})) 44 | ie._login() 45 | self.assertTrue('unable to log in:' in logger.messages[0]) 46 | 47 | 48 | if __name__ == '__main__': 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /yt_dlp/extractor/maoritv.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class MaoriTVIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?maoritelevision\.com/shows/(?:[^/]+/)+(?P[^/?&#]+)' 9 | _TEST = { 10 | 'url': 'https://www.maoritelevision.com/shows/korero-mai/S01E054/korero-mai-series-1-episode-54', 11 | 'md5': '5ade8ef53851b6a132c051b1cd858899', 12 | 'info_dict': { 13 | 'id': '4774724855001', 14 | 'ext': 'mp4', 15 | 'title': 'Kōrero Mai, Series 1 Episode 54', 16 | 'upload_date': '20160226', 17 | 'timestamp': 1456455018, 18 | 'description': 'md5:59bde32fd066d637a1a55794c56d8dcb', 19 | 'uploader_id': '1614493167001', 20 | }, 21 | } 22 | BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/1614493167001/HJlhIQhQf_default/index.html?videoId=%s' 23 | 24 | def _real_extract(self, url): 25 | display_id = self._match_id(url) 26 | webpage = self._download_webpage(url, display_id) 27 | brightcove_id = self._search_regex( 28 | r'data-main-video-id=["\'](\d+)', webpage, 'brightcove id') 29 | return self.url_result( 30 | self.BRIGHTCOVE_URL_TEMPLATE % brightcove_id, 31 | 'BrightcoveNew', brightcove_id) 32 | -------------------------------------------------------------------------------- /yt_dlp/extractor/unity.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from .youtube import YoutubeIE 5 | 6 | 7 | class UnityIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?unity3d\.com/learn/tutorials/(?:[^/]+/)*(?P[^/?#&]+)' 9 | _TESTS = [{ 10 | 'url': 'https://unity3d.com/learn/tutorials/topics/animation/animate-anything-mecanim', 11 | 'info_dict': { 12 | 'id': 'jWuNtik0C8E', 13 | 'ext': 'mp4', 14 | 'title': 'Live Training 22nd September 2014 - Animate Anything', 15 | 'description': 'md5:e54913114bd45a554c56cdde7669636e', 16 | 'duration': 2893, 17 | 'uploader': 'Unity', 18 | 'uploader_id': 'Unity3D', 19 | 'upload_date': '20140926', 20 | } 21 | }, { 22 | 'url': 'https://unity3d.com/learn/tutorials/projects/2d-ufo-tutorial/following-player-camera?playlist=25844', 23 | 'only_matching': True, 24 | }] 25 | 26 | def _real_extract(self, url): 27 | video_id = self._match_id(url) 28 | webpage = self._download_webpage(url, video_id) 29 | youtube_id = self._search_regex( 30 | r'data-video-id="([_0-9a-zA-Z-]+)"', 31 | webpage, 'youtube ID') 32 | return self.url_result(youtube_id, ie=YoutubeIE.ie_key(), video_id=video_id) 33 | -------------------------------------------------------------------------------- /yt_dlp/extractor/hentaistigma.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class HentaiStigmaIE(InfoExtractor): 7 | _VALID_URL = r'^https?://hentai\.animestigma\.com/(?P[^/]+)' 8 | _TEST = { 9 | 'url': 'http://hentai.animestigma.com/inyouchuu-etsu-bonus/', 10 | 'md5': '4e3d07422a68a4cc363d8f57c8bf0d23', 11 | 'info_dict': { 12 | 'id': 'inyouchuu-etsu-bonus', 13 | 'ext': 'mp4', 14 | 'title': 'Inyouchuu Etsu Bonus', 15 | 'age_limit': 18, 16 | } 17 | } 18 | 19 | def _real_extract(self, url): 20 | video_id = self._match_id(url) 21 | 22 | webpage = self._download_webpage(url, video_id) 23 | 24 | title = self._html_search_regex( 25 | r']+class="posttitle"[^>]*>]*>([^<]+)', 26 | webpage, 'title') 27 | wrap_url = self._html_search_regex( 28 | r']+src="([^"]+mp4)"', webpage, 'wrapper url') 29 | wrap_webpage = self._download_webpage(wrap_url, video_id) 30 | 31 | video_url = self._html_search_regex( 32 | r'file\s*:\s*"([^"]+)"', wrap_webpage, 'video url') 33 | 34 | return { 35 | 'id': video_id, 36 | 'url': video_url, 37 | 'title': title, 38 | 'age_limit': 18, 39 | } 40 | -------------------------------------------------------------------------------- /yt_dlp/extractor/nuevo.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | from ..utils import ( 7 | float_or_none, 8 | xpath_text 9 | ) 10 | 11 | 12 | class NuevoBaseIE(InfoExtractor): 13 | def _extract_nuevo(self, config_url, video_id, headers={}): 14 | config = self._download_xml( 15 | config_url, video_id, transform_source=lambda s: s.strip(), 16 | headers=headers) 17 | 18 | title = xpath_text(config, './title', 'title', fatal=True).strip() 19 | video_id = xpath_text(config, './mediaid', default=video_id) 20 | thumbnail = xpath_text(config, ['./image', './thumb']) 21 | duration = float_or_none(xpath_text(config, './duration')) 22 | 23 | formats = [] 24 | for element_name, format_id in (('file', 'sd'), ('filehd', 'hd')): 25 | video_url = xpath_text(config, element_name) 26 | if video_url: 27 | formats.append({ 28 | 'url': video_url, 29 | 'format_id': format_id, 30 | }) 31 | self._check_formats(formats, video_id) 32 | 33 | return { 34 | 'id': video_id, 35 | 'title': title, 36 | 'thumbnail': thumbnail, 37 | 'duration': duration, 38 | 'formats': formats 39 | } 40 | -------------------------------------------------------------------------------- /test/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "check_formats": false, 3 | "consoletitle": false, 4 | "continuedl": true, 5 | "forcedescription": false, 6 | "forcefilename": false, 7 | "forceformat": false, 8 | "forcethumbnail": false, 9 | "forcetitle": false, 10 | "forceurl": false, 11 | "force_write_download_archive": false, 12 | "format": "b/bv", 13 | "ignoreerrors": false, 14 | "listformats": null, 15 | "logtostderr": false, 16 | "matchtitle": null, 17 | "max_downloads": null, 18 | "overwrites": null, 19 | "nopart": false, 20 | "noprogress": false, 21 | "outtmpl": "%(id)s.%(ext)s", 22 | "password": null, 23 | "playliststart": 1, 24 | "prefer_free_formats": false, 25 | "quiet": false, 26 | "ratelimit": null, 27 | "rejecttitle": null, 28 | "retries": 10, 29 | "simulate": false, 30 | "subtitleslang": null, 31 | "subtitlesformat": "best", 32 | "test": true, 33 | "updatetime": true, 34 | "usenetrc": false, 35 | "username": null, 36 | "verbose": true, 37 | "writedescription": false, 38 | "writeinfojson": true, 39 | "writeannotations": false, 40 | "writelink": false, 41 | "writeurllink": false, 42 | "writewebloclink": false, 43 | "writedesktoplink": false, 44 | "writesubtitles": false, 45 | "allsubtitles": false, 46 | "listsubtitles": false, 47 | "fixup": "never" 48 | } 49 | -------------------------------------------------------------------------------- /yt_dlp/extractor/defense.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class DefenseGouvFrIE(InfoExtractor): 7 | IE_NAME = 'defense.gouv.fr' 8 | _VALID_URL = r'https?://.*?\.defense\.gouv\.fr/layout/set/ligthboxvideo/base-de-medias/webtv/(?P[^/?#]*)' 9 | 10 | _TEST = { 11 | 'url': 'http://www.defense.gouv.fr/layout/set/ligthboxvideo/base-de-medias/webtv/attaque-chimique-syrienne-du-21-aout-2013-1', 12 | 'md5': '75bba6124da7e63d2d60b5244ec9430c', 13 | 'info_dict': { 14 | 'id': '11213', 15 | 'ext': 'mp4', 16 | 'title': 'attaque-chimique-syrienne-du-21-aout-2013-1' 17 | } 18 | } 19 | 20 | def _real_extract(self, url): 21 | title = self._match_id(url) 22 | webpage = self._download_webpage(url, title) 23 | 24 | video_id = self._search_regex( 25 | r"flashvars.pvg_id=\"(\d+)\";", 26 | webpage, 'ID') 27 | 28 | json_url = ( 29 | 'http://static.videos.gouv.fr/brightcovehub/export/json/%s' % 30 | video_id) 31 | info = self._download_json(json_url, title, 'Downloading JSON config') 32 | video_url = info['renditions'][0]['url'] 33 | 34 | return { 35 | 'id': video_id, 36 | 'ext': 'mp4', 37 | 'url': video_url, 38 | 'title': title, 39 | } 40 | -------------------------------------------------------------------------------- /yt_dlp/extractor/rottentomatoes.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from .internetvideoarchive import InternetVideoArchiveIE 5 | 6 | 7 | class RottenTomatoesIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?rottentomatoes\.com/m/[^/]+/trailers/(?P\d+)' 9 | 10 | _TEST = { 11 | 'url': 'http://www.rottentomatoes.com/m/toy_story_3/trailers/11028566/', 12 | 'info_dict': { 13 | 'id': '11028566', 14 | 'ext': 'mp4', 15 | 'title': 'Toy Story 3', 16 | 'description': 'From the creators of the beloved TOY STORY films, comes a story that will reunite the gang in a whole new way.', 17 | 'thumbnail': r're:^https?://.*\.jpg$', 18 | }, 19 | } 20 | 21 | def _real_extract(self, url): 22 | video_id = self._match_id(url) 23 | webpage = self._download_webpage(url, video_id) 24 | iva_id = self._search_regex(r'publishedid=(\d+)', webpage, 'internet video archive id') 25 | 26 | return { 27 | '_type': 'url_transparent', 28 | 'url': 'http://video.internetvideoarchive.net/player/6/configuration.ashx?domain=www.videodetective.com&customerid=69249&playerid=641&publishedid=' + iva_id, 29 | 'ie_key': InternetVideoArchiveIE.ie_key(), 30 | 'id': video_id, 31 | 'title': self._og_search_title(webpage), 32 | } 33 | -------------------------------------------------------------------------------- /yt_dlp/extractor/bandaichannel.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .brightcove import BrightcoveNewIE 5 | from ..utils import extract_attributes 6 | 7 | 8 | class BandaiChannelIE(BrightcoveNewIE): 9 | IE_NAME = 'bandaichannel' 10 | _VALID_URL = r'https?://(?:www\.)?b-ch\.com/titles/(?P\d+/\d+)' 11 | _TESTS = [{ 12 | 'url': 'https://www.b-ch.com/titles/514/001', 13 | 'md5': 'a0f2d787baa5729bed71108257f613a4', 14 | 'info_dict': { 15 | 'id': '6128044564001', 16 | 'ext': 'mp4', 17 | 'title': 'メタルファイターMIKU 第1話', 18 | 'timestamp': 1580354056, 19 | 'uploader_id': '5797077852001', 20 | 'upload_date': '20200130', 21 | 'duration': 1387.733, 22 | }, 23 | 'params': { 24 | 'skip_download': True, 25 | }, 26 | }] 27 | 28 | def _real_extract(self, url): 29 | video_id = self._match_id(url) 30 | webpage = self._download_webpage(url, video_id) 31 | attrs = extract_attributes(self._search_regex( 32 | r'(]+\bid="bcplayer"[^>]*>)', webpage, 'player')) 33 | bc = self._download_json( 34 | 'https://pbifcd.b-ch.com/v1/playbackinfo/ST/70/' + attrs['data-info'], 35 | video_id, headers={'X-API-KEY': attrs['data-auth'].strip()})['bc'] 36 | return self._parse_brightcove_metadata(bc, bc['id']) 37 | -------------------------------------------------------------------------------- /yt_dlp/extractor/adobeconnect.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | from ..compat import ( 6 | compat_parse_qs, 7 | compat_urlparse, 8 | ) 9 | 10 | 11 | class AdobeConnectIE(InfoExtractor): 12 | _VALID_URL = r'https?://\w+\.adobeconnect\.com/(?P[\w-]+)' 13 | 14 | def _real_extract(self, url): 15 | video_id = self._match_id(url) 16 | webpage = self._download_webpage(url, video_id) 17 | title = self._html_search_regex(r'(.+?)', webpage, 'title') 18 | qs = compat_parse_qs(self._search_regex(r"swfUrl\s*=\s*'([^']+)'", webpage, 'swf url').split('?')[1]) 19 | is_live = qs.get('isLive', ['false'])[0] == 'true' 20 | formats = [] 21 | for con_string in qs['conStrings'][0].split(','): 22 | formats.append({ 23 | 'format_id': con_string.split('://')[0], 24 | 'app': compat_urlparse.quote('?' + con_string.split('?')[1] + 'flvplayerapp/' + qs['appInstance'][0]), 25 | 'ext': 'flv', 26 | 'play_path': 'mp4:' + qs['streamName'][0], 27 | 'rtmp_conn': 'S:' + qs['ticket'][0], 28 | 'rtmp_live': is_live, 29 | 'url': con_string, 30 | }) 31 | 32 | return { 33 | 'id': video_id, 34 | 'title': title, 35 | 'formats': formats, 36 | 'is_live': is_live, 37 | } 38 | -------------------------------------------------------------------------------- /yt_dlp/postprocessor/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa: F401 2 | 3 | from ..utils import load_plugins 4 | 5 | from .common import PostProcessor 6 | from .embedthumbnail import EmbedThumbnailPP 7 | from .exec import ExecPP, ExecAfterDownloadPP 8 | from .ffmpeg import ( 9 | FFmpegPostProcessor, 10 | FFmpegCopyStreamPP, 11 | FFmpegConcatPP, 12 | FFmpegEmbedSubtitlePP, 13 | FFmpegExtractAudioPP, 14 | FFmpegFixupDuplicateMoovPP, 15 | FFmpegFixupDurationPP, 16 | FFmpegFixupStretchedPP, 17 | FFmpegFixupTimestampPP, 18 | FFmpegFixupM3u8PP, 19 | FFmpegFixupM4aPP, 20 | FFmpegMergerPP, 21 | FFmpegMetadataPP, 22 | FFmpegSubtitlesConvertorPP, 23 | FFmpegThumbnailsConvertorPP, 24 | FFmpegSplitChaptersPP, 25 | FFmpegVideoConvertorPP, 26 | FFmpegVideoRemuxerPP, 27 | ) 28 | from .metadataparser import ( 29 | MetadataFromFieldPP, 30 | MetadataFromTitlePP, 31 | MetadataParserPP, 32 | ) 33 | from .modify_chapters import ModifyChaptersPP 34 | from .movefilesafterdownload import MoveFilesAfterDownloadPP 35 | from .sponskrub import SponSkrubPP 36 | from .sponsorblock import SponsorBlockPP 37 | from .xattrpp import XAttrMetadataPP 38 | 39 | _PLUGIN_CLASSES = load_plugins('postprocessor', 'PP', globals()) 40 | 41 | 42 | def get_postprocessor(key): 43 | return globals()[key + 'PP'] 44 | 45 | 46 | __all__ = [name for name in globals().keys() if name.endswith('PP')] 47 | __all__.extend(('PostProcessor', 'FFmpegPostProcessor')) 48 | -------------------------------------------------------------------------------- /yt_dlp/extractor/europeantour.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | import re 5 | 6 | from .common import InfoExtractor 7 | 8 | 9 | class EuropeanTourIE(InfoExtractor): 10 | _VALID_URL = r'https?://(?:www\.)?europeantour\.com/dpworld-tour/news/video/(?P[^/&?#$]+)' 11 | 12 | _TESTS = [{ 13 | 'url': 'https://www.europeantour.com/dpworld-tour/news/video/the-best-shots-of-the-2021-seasons/', 14 | 'info_dict': { 15 | 'id': '6287788195001', 16 | 'ext': 'mp4', 17 | 'title': 'The best shots of the 2021 seasons', 18 | 'duration': 2416.512, 19 | 'timestamp': 1640010141, 20 | 'uploader_id': '5136026580001', 21 | 'tags': ['prod-imported'], 22 | 'thumbnail': 'md5:fdac52bc826548860edf8145ee74e71a', 23 | 'upload_date': '20211220' 24 | }, 25 | 'params': {'skip_download': True} 26 | }] 27 | 28 | BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s' 29 | 30 | def _real_extract(self, url): 31 | id = self._match_id(url) 32 | webpage = self._download_webpage(url, id) 33 | vid, aid = re.search(r'(?s)brightcove-player\s?video-id="([^"]+)".*"ACCOUNT_ID":"([^"]+)"', webpage).groups() 34 | if not aid: 35 | aid = '5136026580001' 36 | return self.url_result( 37 | self.BRIGHTCOVE_URL_TEMPLATE % (aid, vid), 'BrightcoveNew') 38 | -------------------------------------------------------------------------------- /yt_dlp/extractor/googlesearch.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import itertools 4 | import re 5 | 6 | from .common import SearchInfoExtractor 7 | 8 | 9 | class GoogleSearchIE(SearchInfoExtractor): 10 | IE_DESC = 'Google Video search' 11 | _MAX_RESULTS = 1000 12 | IE_NAME = 'video.google:search' 13 | _SEARCH_KEY = 'gvsearch' 14 | _WORKING = False 15 | _TEST = { 16 | 'url': 'gvsearch15:python language', 17 | 'info_dict': { 18 | 'id': 'python language', 19 | 'title': 'python language', 20 | }, 21 | 'playlist_count': 15, 22 | } 23 | 24 | def _search_results(self, query): 25 | for pagenum in itertools.count(): 26 | webpage = self._download_webpage( 27 | 'http://www.google.com/search', 28 | 'gvsearch:' + query, 29 | note='Downloading result page %s' % (pagenum + 1), 30 | query={ 31 | 'tbm': 'vid', 32 | 'q': query, 33 | 'start': pagenum * 10, 34 | 'hl': 'en', 35 | }) 36 | 37 | for hit_idx, mobj in enumerate(re.finditer( 38 | r'

    [\w-]+)-online' 13 | _TEST = { 14 | 'url': 'https://player.bfi.org.uk/free/film/watch-computer-doctor-1974-online', 15 | 'md5': 'e8783ebd8e061ec4bc6e9501ed547de8', 16 | 'info_dict': { 17 | 'id': 'htNnhlZjE60C9VySkQEIBtU-cNV1Xx63', 18 | 'ext': 'mp4', 19 | 'title': 'Computer Doctor', 20 | 'description': 'md5:fb6c240d40c4dbe40428bdd62f78203b', 21 | }, 22 | 'skip': 'BFI Player films cannot be played outside of the UK', 23 | } 24 | 25 | def _real_extract(self, url): 26 | video_id = self._match_id(url) 27 | webpage = self._download_webpage(url, video_id) 28 | entries = [] 29 | for player_el in re.findall(r'(?s)<[^>]+class="player"[^>]*>', webpage): 30 | player_attr = extract_attributes(player_el) 31 | ooyala_id = player_attr.get('data-video-id') 32 | if not ooyala_id: 33 | continue 34 | entries.append(self.url_result( 35 | 'ooyala:' + ooyala_id, 'Ooyala', 36 | ooyala_id, player_attr.get('data-label'))) 37 | return self.playlist_result(entries) 38 | -------------------------------------------------------------------------------- /yt_dlp/extractor/echomsk.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | import re 5 | 6 | from .common import InfoExtractor 7 | 8 | 9 | class EchoMskIE(InfoExtractor): 10 | _VALID_URL = r'https?://(?:www\.)?echo\.msk\.ru/sounds/(?P\d+)' 11 | _TEST = { 12 | 'url': 'http://www.echo.msk.ru/sounds/1464134.html', 13 | 'md5': '2e44b3b78daff5b458e4dbc37f191f7c', 14 | 'info_dict': { 15 | 'id': '1464134', 16 | 'ext': 'mp3', 17 | 'title': 'Особое мнение - 29 декабря 2014, 19:08', 18 | }, 19 | } 20 | 21 | def _real_extract(self, url): 22 | video_id = self._match_id(url) 23 | 24 | webpage = self._download_webpage(url, video_id) 25 | 26 | audio_url = self._search_regex( 27 | r'', webpage, 'audio URL') 28 | 29 | title = self._html_search_regex( 30 | r'([^<]+)', 31 | webpage, 'title') 32 | 33 | air_date = self._html_search_regex( 34 | r'(?s)
    (.+?)
    ', 35 | webpage, 'date', fatal=False, default=None) 36 | 37 | if air_date: 38 | air_date = re.sub(r'(\s)\1+', r'\1', air_date) 39 | if air_date: 40 | title = '%s - %s' % (title, air_date) 41 | 42 | return { 43 | 'id': video_id, 44 | 'url': audio_url, 45 | 'title': title, 46 | } 47 | -------------------------------------------------------------------------------- /yt_dlp/extractor/stretchinternet.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class StretchInternetIE(InfoExtractor): 7 | _VALID_URL = r'https?://portal\.stretchinternet\.com/[^/]+/(?:portal|full)\.htm\?.*?\beventId=(?P\d+)' 8 | _TEST = { 9 | 'url': 'https://portal.stretchinternet.com/umary/portal.htm?eventId=573272&streamType=video', 10 | 'info_dict': { 11 | 'id': '573272', 12 | 'ext': 'mp4', 13 | 'title': 'UNIVERSITY OF MARY WRESTLING VS UPPER IOWA', 14 | # 'timestamp': 1575668361, 15 | # 'upload_date': '20191206', 16 | 'uploader_id': '99997', 17 | } 18 | } 19 | 20 | def _real_extract(self, url): 21 | video_id = self._match_id(url) 22 | 23 | media_url = self._download_json( 24 | 'https://core.stretchlive.com/trinity/event/tcg/' + video_id, 25 | video_id)[0]['media'][0]['url'] 26 | event = self._download_json( 27 | 'https://neo-client.stretchinternet.com/portal-ws/getEvent.json', 28 | video_id, query={'eventID': video_id, 'token': 'asdf'})['event'] 29 | 30 | return { 31 | 'id': video_id, 32 | 'title': event['title'], 33 | # TODO: parse US timezone abbreviations 34 | # 'timestamp': event.get('dateTimeString'), 35 | 'url': 'https://' + media_url, 36 | 'uploader_id': event.get('ownerID'), 37 | } 38 | -------------------------------------------------------------------------------- /devscripts/make_supportedsites.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | import io 5 | import optparse 6 | import os 7 | import sys 8 | 9 | 10 | # Import yt_dlp 11 | ROOT_DIR = os.path.join(os.path.dirname(__file__), '..') 12 | sys.path.insert(0, ROOT_DIR) 13 | import yt_dlp 14 | 15 | 16 | def main(): 17 | parser = optparse.OptionParser(usage='%prog OUTFILE.md') 18 | options, args = parser.parse_args() 19 | if len(args) != 1: 20 | parser.error('Expected an output filename') 21 | 22 | outfile, = args 23 | 24 | def gen_ies_md(ies): 25 | for ie in ies: 26 | ie_md = '**{0}**'.format(ie.IE_NAME) 27 | ie_desc = getattr(ie, 'IE_DESC', None) 28 | if ie_desc is False: 29 | continue 30 | if ie_desc is not None: 31 | ie_md += ': {0}'.format(ie.IE_DESC) 32 | search_key = getattr(ie, 'SEARCH_KEY', None) 33 | if search_key is not None: 34 | ie_md += f'; "{ie.SEARCH_KEY}:" prefix' 35 | if not ie.working(): 36 | ie_md += ' (Currently broken)' 37 | yield ie_md 38 | 39 | ies = sorted(yt_dlp.gen_extractors(), key=lambda i: i.IE_NAME.lower()) 40 | out = '# Supported sites\n' + ''.join( 41 | ' - ' + md + '\n' 42 | for md in gen_ies_md(ies)) 43 | 44 | with io.open(outfile, 'w', encoding='utf-8') as outf: 45 | outf.write(out) 46 | 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /yt_dlp/extractor/uktvplay.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class UKTVPlayIE(InfoExtractor): 8 | _VALID_URL = r'https?://uktvplay\.uktv\.co\.uk/(?:.+?\?.*?\bvideo=|([^/]+/)*watch-online/)(?P\d+)' 9 | _TESTS = [{ 10 | 'url': 'https://uktvplay.uktv.co.uk/shows/world-at-war/c/200/watch-online/?video=2117008346001', 11 | 'info_dict': { 12 | 'id': '2117008346001', 13 | 'ext': 'mp4', 14 | 'title': 'Pincers', 15 | 'description': 'Pincers', 16 | 'uploader_id': '1242911124001', 17 | 'upload_date': '20130124', 18 | 'timestamp': 1359049267, 19 | }, 20 | 'params': { 21 | # m3u8 download 22 | 'skip_download': True, 23 | }, 24 | 'expected_warnings': ['Failed to download MPD manifest'] 25 | }, { 26 | 'url': 'https://uktvplay.uktv.co.uk/shows/africa/watch-online/5983349675001', 27 | 'only_matching': True, 28 | }] 29 | # BRIGHTCOVE_URL_TEMPLATE = 'https://players.brightcove.net/1242911124001/OrCyvJ2gyL_default/index.html?videoId=%s' 30 | BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/1242911124001/H1xnMOqP_default/index.html?videoId=%s' 31 | 32 | def _real_extract(self, url): 33 | video_id = self._match_id(url) 34 | return self.url_result( 35 | self.BRIGHTCOVE_URL_TEMPLATE % video_id, 36 | 'BrightcoveNew', video_id) 37 | -------------------------------------------------------------------------------- /yt_dlp/extractor/worldstarhiphop.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class WorldStarHipHopIE(InfoExtractor): 7 | _VALID_URL = r'https?://(?:www|m)\.worldstar(?:candy|hiphop)\.com/(?:videos|android)/video\.php\?.*?\bv=(?P[^&]+)' 8 | _TESTS = [{ 9 | 'url': 'http://www.worldstarhiphop.com/videos/video.php?v=wshh6a7q1ny0G34ZwuIO', 10 | 'md5': '9d04de741161603bf7071bbf4e883186', 11 | 'info_dict': { 12 | 'id': 'wshh6a7q1ny0G34ZwuIO', 13 | 'ext': 'mp4', 14 | 'title': 'KO Of The Week: MMA Fighter Gets Knocked Out By Swift Head Kick!' 15 | } 16 | }, { 17 | 'url': 'http://m.worldstarhiphop.com/android/video.php?v=wshh6a7q1ny0G34ZwuIO', 18 | 'only_matching': True, 19 | }] 20 | 21 | def _real_extract(self, url): 22 | video_id = self._match_id(url) 23 | webpage = self._download_webpage(url, video_id) 24 | 25 | entries = self._parse_html5_media_entries(url, webpage, video_id) 26 | 27 | if not entries: 28 | return self.url_result(url, 'Generic') 29 | 30 | title = self._html_search_regex( 31 | [r'(?s)
    \s*

    (.*?)

    ', 32 | r']+class="tc-sp-pinned-title">(.*)'], 33 | webpage, 'title') 34 | 35 | info = entries[0] 36 | info.update({ 37 | 'id': video_id, 38 | 'title': title, 39 | }) 40 | return info 41 | -------------------------------------------------------------------------------- /test/testdata/xspf/foo_xspf.xspf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2018-03-09T18:01:43Z 4 | 5 | 6 | cd1/track%201.mp3 7 | Pandemonium 8 | Foilverb 9 | Visit http://bigbrother404.bandcamp.com 10 | Pandemonium EP 11 | 1 12 | 202416 13 | 14 | 15 | ../%E3%83%88%E3%83%A9%E3%83%83%E3%82%AF%E3%80%80%EF%BC%92.mp3 16 | Final Cartridge (Nichico Twelve Remix) 17 | Visit http://bigbrother404.bandcamp.com 18 | Foilverb 19 | Pandemonium EP 20 | 2 21 | 255857 22 | 23 | 24 | track3.mp3 25 | https://example.com/track3.mp3 26 | Rebuilding Nightingale 27 | Visit http://bigbrother404.bandcamp.com 28 | Foilverb 29 | Pandemonium EP 30 | 3 31 | 287915 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /yt_dlp/extractor/bild.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | from ..utils import ( 6 | int_or_none, 7 | unescapeHTML, 8 | ) 9 | 10 | 11 | class BildIE(InfoExtractor): 12 | _VALID_URL = r'https?://(?:www\.)?bild\.de/(?:[^/]+/)+(?P[^/]+)-(?P\d+)(?:,auto=true)?\.bild\.html' 13 | IE_DESC = 'Bild.de' 14 | _TEST = { 15 | 'url': 'http://www.bild.de/video/clip/apple-ipad-air/das-koennen-die-neuen-ipads-38184146.bild.html', 16 | 'md5': 'dd495cbd99f2413502a1713a1156ac8a', 17 | 'info_dict': { 18 | 'id': '38184146', 19 | 'ext': 'mp4', 20 | 'title': 'Das können die neuen iPads', 21 | 'description': 'md5:a4058c4fa2a804ab59c00d7244bbf62f', 22 | 'thumbnail': r're:^https?://.*\.jpg$', 23 | 'duration': 196, 24 | } 25 | } 26 | 27 | def _real_extract(self, url): 28 | video_id = self._match_id(url) 29 | 30 | video_data = self._download_json( 31 | url.split('.bild.html')[0] + ',view=json.bild.html', video_id) 32 | 33 | return { 34 | 'id': video_id, 35 | 'title': unescapeHTML(video_data['title']).strip(), 36 | 'description': unescapeHTML(video_data.get('description')), 37 | 'url': video_data['clipList'][0]['srces'][0]['src'], 38 | 'thumbnail': video_data.get('poster'), 39 | 'duration': int_or_none(video_data.get('durationSec')), 40 | } 41 | -------------------------------------------------------------------------------- /yt_dlp/extractor/helsinki.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import unicode_literals 4 | 5 | from .common import InfoExtractor 6 | from ..utils import js_to_json 7 | 8 | 9 | class HelsinkiIE(InfoExtractor): 10 | IE_DESC = 'helsinki.fi' 11 | _VALID_URL = r'https?://video\.helsinki\.fi/Arkisto/flash\.php\?id=(?P\d+)' 12 | _TEST = { 13 | 'url': 'http://video.helsinki.fi/Arkisto/flash.php?id=20258', 14 | 'info_dict': { 15 | 'id': '20258', 16 | 'ext': 'mp4', 17 | 'title': 'Tietotekniikkafoorumi-iltapäivä', 18 | 'description': 'md5:f5c904224d43c133225130fe156a5ee0', 19 | }, 20 | 'params': { 21 | 'skip_download': True, # RTMP 22 | } 23 | } 24 | 25 | def _real_extract(self, url): 26 | video_id = self._match_id(url) 27 | webpage = self._download_webpage(url, video_id) 28 | 29 | params = self._parse_json(self._html_search_regex( 30 | r'(?s)jwplayer\("player"\).setup\((\{.*?\})\);', 31 | webpage, 'player code'), video_id, transform_source=js_to_json) 32 | formats = [{ 33 | 'url': s['file'], 34 | 'ext': 'mp4', 35 | } for s in params['sources']] 36 | self._sort_formats(formats) 37 | 38 | return { 39 | 'id': video_id, 40 | 'title': self._og_search_title(webpage).replace('Video: ', ''), 41 | 'description': self._og_search_description(webpage), 42 | 'formats': formats, 43 | } 44 | -------------------------------------------------------------------------------- /yt_dlp/extractor/restudy.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class RestudyIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:(?:www|portal)\.)?restudy\.dk/video/[^/]+/id/(?P[0-9]+)' 9 | _TESTS = [{ 10 | 'url': 'https://www.restudy.dk/video/play/id/1637', 11 | 'info_dict': { 12 | 'id': '1637', 13 | 'ext': 'flv', 14 | 'title': 'Leiden-frosteffekt', 15 | 'description': 'Denne video er et eksperiment med flydende kvælstof.', 16 | }, 17 | 'params': { 18 | # rtmp download 19 | 'skip_download': True, 20 | } 21 | }, { 22 | 'url': 'https://portal.restudy.dk/video/leiden-frosteffekt/id/1637', 23 | 'only_matching': True, 24 | }] 25 | 26 | def _real_extract(self, url): 27 | video_id = self._match_id(url) 28 | 29 | webpage = self._download_webpage(url, video_id) 30 | 31 | title = self._og_search_title(webpage).strip() 32 | description = self._og_search_description(webpage).strip() 33 | 34 | formats = self._extract_smil_formats( 35 | 'https://cdn.portal.restudy.dk/dynamic/themes/front/awsmedia/SmilDirectory/video_%s.xml' % video_id, 36 | video_id) 37 | self._sort_formats(formats) 38 | 39 | return { 40 | 'id': video_id, 41 | 'title': title, 42 | 'description': description, 43 | 'formats': formats, 44 | } 45 | -------------------------------------------------------------------------------- /yt_dlp/extractor/howcast.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from ..utils import parse_iso8601 5 | 6 | 7 | class HowcastIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?howcast\.com/videos/(?P\d+)' 9 | _TEST = { 10 | 'url': 'http://www.howcast.com/videos/390161-How-to-Tie-a-Square-Knot-Properly', 11 | 'md5': '7d45932269a288149483144f01b99789', 12 | 'info_dict': { 13 | 'id': '390161', 14 | 'ext': 'mp4', 15 | 'title': 'How to Tie a Square Knot Properly', 16 | 'description': 'md5:dbe792e5f6f1489027027bf2eba188a3', 17 | 'timestamp': 1276081287, 18 | 'upload_date': '20100609', 19 | 'duration': 56.823, 20 | }, 21 | 'params': { 22 | 'skip_download': True, 23 | }, 24 | 'add_ie': ['Ooyala'], 25 | } 26 | 27 | def _real_extract(self, url): 28 | video_id = self._match_id(url) 29 | 30 | webpage = self._download_webpage(url, video_id) 31 | 32 | embed_code = self._search_regex( 33 | r']+src="[^"]+\bembed_code=([^\b]+)\b', 34 | webpage, 'ooyala embed code') 35 | 36 | return { 37 | '_type': 'url_transparent', 38 | 'ie_key': 'Ooyala', 39 | 'url': 'ooyala:%s' % embed_code, 40 | 'id': video_id, 41 | 'timestamp': parse_iso8601(self._html_search_meta( 42 | 'article:published_time', webpage, 'timestamp')), 43 | } 44 | -------------------------------------------------------------------------------- /yt_dlp/extractor/thestar.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class TheStarIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?thestar\.com/(?:[^/]+/)*(?P.+)\.html' 9 | _TEST = { 10 | 'url': 'http://www.thestar.com/life/2016/02/01/mankind-why-this-woman-started-a-men-s-skincare-line.html', 11 | 'md5': '2c62dd4db2027e35579fefb97a8b6554', 12 | 'info_dict': { 13 | 'id': '4732393888001', 14 | 'ext': 'mp4', 15 | 'title': 'Mankind: Why this woman started a men\'s skin care line', 16 | 'description': 'Robert Cribb talks to Young Lee, the founder of Uncle Peter\'s MAN.', 17 | 'uploader_id': '794267642001', 18 | 'timestamp': 1454353482, 19 | 'upload_date': '20160201', 20 | }, 21 | 'params': { 22 | # m3u8 download 23 | 'skip_download': True, 24 | } 25 | } 26 | BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/794267642001/default_default/index.html?videoId=%s' 27 | 28 | def _real_extract(self, url): 29 | display_id = self._match_id(url) 30 | webpage = self._download_webpage(url, display_id) 31 | brightcove_id = self._search_regex( 32 | r'mainartBrightcoveVideoId["\']?\s*:\s*["\']?(\d+)', 33 | webpage, 'brightcove id') 34 | return self.url_result( 35 | self.BRIGHTCOVE_URL_TEMPLATE % brightcove_id, 36 | 'BrightcoveNew', brightcove_id) 37 | -------------------------------------------------------------------------------- /yt_dlp/extractor/thescene.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | from ..compat import compat_urlparse 6 | 7 | 8 | class TheSceneIE(InfoExtractor): 9 | _VALID_URL = r'https?://thescene\.com/watch/[^/]+/(?P[^/#?]+)' 10 | 11 | _TEST = { 12 | 'url': 'https://thescene.com/watch/vogue/narciso-rodriguez-spring-2013-ready-to-wear', 13 | 'info_dict': { 14 | 'id': '520e8faac2b4c00e3c6e5f43', 15 | 'ext': 'mp4', 16 | 'title': 'Narciso Rodriguez: Spring 2013 Ready-to-Wear', 17 | 'display_id': 'narciso-rodriguez-spring-2013-ready-to-wear', 18 | 'duration': 127, 19 | 'series': 'Style.com Fashion Shows', 20 | 'season': 'Ready To Wear Spring 2013', 21 | 'tags': list, 22 | 'categories': list, 23 | 'upload_date': '20120913', 24 | 'timestamp': 1347512400, 25 | 'uploader': 'vogue', 26 | }, 27 | } 28 | 29 | def _real_extract(self, url): 30 | display_id = self._match_id(url) 31 | 32 | webpage = self._download_webpage(url, display_id) 33 | 34 | player_url = compat_urlparse.urljoin( 35 | url, 36 | self._html_search_regex( 37 | r'id=\'js-player-script\'[^>]+src=\'(.+?)\'', webpage, 'player url')) 38 | 39 | return { 40 | '_type': 'url_transparent', 41 | 'display_id': display_id, 42 | 'url': player_url, 43 | 'ie_key': 'CondeNast', 44 | } 45 | -------------------------------------------------------------------------------- /yt_dlp/extractor/thesun.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import re 4 | 5 | from .common import InfoExtractor 6 | from ..utils import extract_attributes 7 | 8 | 9 | class TheSunIE(InfoExtractor): 10 | _VALID_URL = r'https://(?:www\.)?thesun\.co\.uk/[^/]+/(?P\d+)' 11 | _TEST = { 12 | 'url': 'https://www.thesun.co.uk/tvandshowbiz/2261604/orlando-bloom-and-katy-perry-post-adorable-instagram-video-together-celebrating-thanksgiving-after-split-rumours/', 13 | 'info_dict': { 14 | 'id': '2261604', 15 | 'title': 'md5:cba22f48bad9218b64d5bbe0e16afddf', 16 | }, 17 | 'playlist_count': 2, 18 | } 19 | BRIGHTCOVE_URL_TEMPLATE = 'http://players.brightcove.net/%s/default_default/index.html?videoId=%s' 20 | 21 | def _real_extract(self, url): 22 | article_id = self._match_id(url) 23 | 24 | webpage = self._download_webpage(url, article_id) 25 | 26 | entries = [] 27 | for video in re.findall( 28 | r']+data-video-id-pending=[^>]+>', 29 | webpage): 30 | attrs = extract_attributes(video) 31 | video_id = attrs['data-video-id-pending'] 32 | account_id = attrs.get('data-account', '5067014667001') 33 | entries.append(self.url_result( 34 | self.BRIGHTCOVE_URL_TEMPLATE % (account_id, video_id), 35 | 'BrightcoveNew', video_id)) 36 | 37 | return self.playlist_result( 38 | entries, article_id, self._og_search_title(webpage, fatal=False)) 39 | -------------------------------------------------------------------------------- /ytdlp_plugins/postprocessor/sample.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # ⚠ Don't use relative imports 4 | from yt_dlp.postprocessor.common import PostProcessor 5 | 6 | 7 | # ℹ️ See the docstring of yt_dlp.postprocessor.common.PostProcessor 8 | class SamplePluginPP(PostProcessor): 9 | def __init__(self, downloader=None, **kwargs): 10 | # ⚠ Only kwargs can be passed from the CLI, and all argument values will be string 11 | # Also, "downloader", "when" and "key" are reserved names 12 | super().__init__(downloader) 13 | self._kwargs = kwargs 14 | 15 | # ℹ️ See docstring of yt_dlp.postprocessor.common.PostProcessor.run 16 | def run(self, info): 17 | if info.get('_type', 'video') != 'video': # PP was called for playlist 18 | self.to_screen(f'Post-processing playlist {info.get("id")!r} with {self._kwargs}') 19 | elif info.get('filepath'): # PP was called after download (default) 20 | filepath = info.get('filepath') 21 | self.to_screen(f'Post-processed {filepath!r} with {self._kwargs}') 22 | elif info.get('requested_downloads'): # PP was called after_video 23 | filepaths = [f.get('filepath') for f in info.get('requested_downloads')] 24 | self.to_screen(f'Post-processed {filepaths!r} with {self._kwargs}') 25 | else: # PP was called before actual download 26 | filepath = info.get('_filename') 27 | self.to_screen(f'Pre-processed {filepath!r} with {self._kwargs}') 28 | return [], info # return list_of_files_to_delete, info_dict 29 | -------------------------------------------------------------------------------- /devscripts/show-downloads-statistics.py: -------------------------------------------------------------------------------- 1 | # Unused 2 | 3 | #!/usr/bin/env python3 4 | from __future__ import unicode_literals 5 | 6 | import itertools 7 | import json 8 | import os 9 | import re 10 | import sys 11 | 12 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 13 | 14 | from yt_dlp.compat import ( 15 | compat_print, 16 | compat_urllib_request, 17 | ) 18 | from yt_dlp.utils import format_bytes 19 | 20 | 21 | def format_size(bytes): 22 | return '%s (%d bytes)' % (format_bytes(bytes), bytes) 23 | 24 | 25 | total_bytes = 0 26 | 27 | for page in itertools.count(1): 28 | releases = json.loads(compat_urllib_request.urlopen( 29 | 'https://api.github.com/repos/ytdl-org/youtube-dl/releases?page=%s' % page 30 | ).read().decode('utf-8')) 31 | 32 | if not releases: 33 | break 34 | 35 | for release in releases: 36 | compat_print(release['name']) 37 | for asset in release['assets']: 38 | asset_name = asset['name'] 39 | total_bytes += asset['download_count'] * asset['size'] 40 | if all(not re.match(p, asset_name) for p in ( 41 | r'^yt-dlp$', 42 | r'^yt-dlp-\d{4}\.\d{2}\.\d{2}(?:\.\d+)?\.tar\.gz$', 43 | r'^yt-dlp\.exe$')): 44 | continue 45 | compat_print( 46 | ' %s size: %s downloads: %d' 47 | % (asset_name, format_size(asset['size']), asset['download_count'])) 48 | 49 | compat_print('total downloads traffic: %s' % format_size(total_bytes)) 50 | -------------------------------------------------------------------------------- /yt_dlp/extractor/academicearth.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import re 4 | 5 | from .common import InfoExtractor 6 | 7 | 8 | class AcademicEarthCourseIE(InfoExtractor): 9 | _VALID_URL = r'^https?://(?:www\.)?academicearth\.org/playlists/(?P[^?#/]+)' 10 | IE_NAME = 'AcademicEarth:Course' 11 | _TEST = { 12 | 'url': 'http://academicearth.org/playlists/laws-of-nature/', 13 | 'info_dict': { 14 | 'id': 'laws-of-nature', 15 | 'title': 'Laws of Nature', 16 | 'description': 'Introduce yourself to the laws of nature with these free online college lectures from Yale, Harvard, and MIT.', 17 | }, 18 | 'playlist_count': 3, 19 | } 20 | 21 | def _real_extract(self, url): 22 | playlist_id = self._match_id(url) 23 | 24 | webpage = self._download_webpage(url, playlist_id) 25 | title = self._html_search_regex( 26 | r'

    ]*?>(.*?)

    ', webpage, 'title') 27 | description = self._html_search_regex( 28 | r'

    ]*?>(.*?)

    ', 29 | webpage, 'description', fatal=False) 30 | urls = re.findall( 31 | r'
  • \s*?', 32 | webpage) 33 | entries = [self.url_result(u) for u in urls] 34 | 35 | return { 36 | '_type': 'playlist', 37 | 'id': playlist_id, 38 | 'title': title, 39 | 'description': description, 40 | 'entries': entries, 41 | } 42 | -------------------------------------------------------------------------------- /yt_dlp/extractor/moviezine.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | 5 | from .common import InfoExtractor 6 | 7 | 8 | class MoviezineIE(InfoExtractor): 9 | _VALID_URL = r'https?://(?:www\.)?moviezine\.se/video/(?P[^?#]+)' 10 | 11 | _TEST = { 12 | 'url': 'http://www.moviezine.se/video/205866', 13 | 'info_dict': { 14 | 'id': '205866', 15 | 'ext': 'mp4', 16 | 'title': 'Oculus - Trailer 1', 17 | 'description': 'md5:40cc6790fc81d931850ca9249b40e8a4', 18 | 'thumbnail': r're:http://.*\.jpg', 19 | }, 20 | } 21 | 22 | def _real_extract(self, url): 23 | mobj = self._match_valid_url(url) 24 | video_id = mobj.group('id') 25 | 26 | webpage = self._download_webpage(url, video_id) 27 | jsplayer = self._download_webpage('http://www.moviezine.se/api/player.js?video=%s' % video_id, video_id, 'Downloading js api player') 28 | 29 | formats = [{ 30 | 'format_id': 'sd', 31 | 'url': self._html_search_regex(r'file: "(.+?)",', jsplayer, 'file'), 32 | 'quality': 0, 33 | 'ext': 'mp4', 34 | }] 35 | 36 | self._sort_formats(formats) 37 | 38 | return { 39 | 'id': video_id, 40 | 'title': self._search_regex(r'title: "(.+?)",', jsplayer, 'title'), 41 | 'thumbnail': self._search_regex(r'image: "(.+?)",', jsplayer, 'image'), 42 | 'formats': formats, 43 | 'description': self._og_search_description(webpage), 44 | } 45 | -------------------------------------------------------------------------------- /yt_dlp/extractor/vh1.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .mtv import MTVServicesInfoExtractor 5 | 6 | # TODO Remove - Reason: Outdated Site 7 | 8 | 9 | class VH1IE(MTVServicesInfoExtractor): 10 | IE_NAME = 'vh1.com' 11 | _FEED_URL = 'http://www.vh1.com/feeds/mrss/' 12 | _TESTS = [{ 13 | 'url': 'https://www.vh1.com/episodes/0aqivv/nick-cannon-presents-wild-n-out-foushee-season-16-ep-12', 14 | 'info_dict': { 15 | 'title': 'Fousheé', 16 | 'description': 'Fousheé joins Team Evolutions fight against Nick and Team Revolution in Baby Daddy, Baby Mama; Kick Em Out the Classroom; Backseat of My Ride and Wildstyle; and Fousheé performs.', 17 | }, 18 | 'playlist_mincount': 4, 19 | 'skip': '404 Not found', 20 | }, { 21 | # Clip 22 | 'url': 'https://www.vh1.com/video-clips/e0sja0/nick-cannon-presents-wild-n-out-foushee-clap-for-him', 23 | 'info_dict': { 24 | 'id': 'a07563f7-a37b-4e7f-af68-85855c2c7cc3', 25 | 'ext': 'mp4', 26 | 'title': 'Fousheé - "clap for him"', 27 | 'description': 'Singer Fousheé hits the Wild N Out: In the Dark stage with a performance of the tongue-in-cheek track "clap for him" from her 2021 album "time machine."', 28 | 'upload_date': '20210826', 29 | }, 30 | 'params': { 31 | # m3u8 download 32 | 'skip_download': True, 33 | }, 34 | }] 35 | 36 | _VALID_URL = r'https?://(?:www\.)?vh1\.com/(?:video-clips|episodes)/(?P[^/?#.]+)' 37 | -------------------------------------------------------------------------------- /yt_dlp/extractor/nzz.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | import re 5 | 6 | from .common import InfoExtractor 7 | from ..utils import ( 8 | extract_attributes, 9 | ) 10 | 11 | 12 | class NZZIE(InfoExtractor): 13 | _VALID_URL = r'https?://(?:www\.)?nzz\.ch/(?:[^/]+/)*[^/?#]+-ld\.(?P\d+)' 14 | _TESTS = [{ 15 | 'url': 'http://www.nzz.ch/zuerich/gymizyte/gymizyte-schreiben-schueler-heute-noch-diktate-ld.9153', 16 | 'info_dict': { 17 | 'id': '9153', 18 | }, 19 | 'playlist_mincount': 6, 20 | }, { 21 | 'url': 'https://www.nzz.ch/video/nzz-standpunkte/cvp-auf-der-suche-nach-dem-mass-der-mitte-ld.1368112', 22 | 'info_dict': { 23 | 'id': '1368112', 24 | }, 25 | 'playlist_count': 1, 26 | }] 27 | 28 | def _real_extract(self, url): 29 | page_id = self._match_id(url) 30 | webpage = self._download_webpage(url, page_id) 31 | 32 | entries = [] 33 | for player_element in re.findall( 34 | r'(<[^>]+class="kalturaPlayer[^"]*"[^>]*>)', webpage): 35 | player_params = extract_attributes(player_element) 36 | if player_params.get('data-type') not in ('kaltura_singleArticle',): 37 | self.report_warning('Unsupported player type') 38 | continue 39 | entry_id = player_params['data-id'] 40 | entries.append(self.url_result( 41 | 'kaltura:1750922:' + entry_id, 'Kaltura', entry_id)) 42 | 43 | return self.playlist_result(entries, page_id) 44 | -------------------------------------------------------------------------------- /yt_dlp/extractor/cozytv.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | from ..utils import unified_strdate 6 | 7 | 8 | class CozyTVIE(InfoExtractor): 9 | _VALID_URL = r'https?://(?:www\.)?cozy\.tv/(?P[^/]+)/replays/(?P[^/$#&?]+)' 10 | 11 | _TESTS = [{ 12 | 'url': 'https://cozy.tv/beardson/replays/2021-11-19_1', 13 | 'info_dict': { 14 | 'id': 'beardson-2021-11-19_1', 15 | 'ext': 'mp4', 16 | 'title': 'pokemon pt2', 17 | 'uploader': 'beardson', 18 | 'upload_date': '20211119', 19 | 'was_live': True, 20 | 'duration': 7981, 21 | }, 22 | 'params': {'skip_download': True} 23 | }] 24 | 25 | def _real_extract(self, url): 26 | uploader, date = self._match_valid_url(url).groups() 27 | id = f'{uploader}-{date}' 28 | data_json = self._download_json(f'https://api.cozy.tv/cache/{uploader}/replay/{date}', id) 29 | formats, subtitles = self._extract_m3u8_formats_and_subtitles( 30 | f'https://cozycdn.foxtrotstream.xyz/replays/{uploader}/{date}/index.m3u8', id, ext='mp4') 31 | return { 32 | 'id': id, 33 | 'title': data_json.get('title'), 34 | 'uploader': data_json.get('user') or uploader, 35 | 'upload_date': unified_strdate(data_json.get('date')), 36 | 'was_live': True, 37 | 'duration': data_json.get('duration'), 38 | 'formats': formats, 39 | 'subtitles': subtitles, 40 | } 41 | -------------------------------------------------------------------------------- /devscripts/zsh-completion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | import os 5 | from os.path import dirname as dirn 6 | import sys 7 | 8 | sys.path.insert(0, dirn(dirn((os.path.abspath(__file__))))) 9 | import yt_dlp 10 | 11 | ZSH_COMPLETION_FILE = "completions/zsh/_yt-dlp" 12 | ZSH_COMPLETION_TEMPLATE = "devscripts/zsh-completion.in" 13 | 14 | 15 | def build_completion(opt_parser): 16 | opts = [opt for group in opt_parser.option_groups 17 | for opt in group.option_list] 18 | opts_file = [opt for opt in opts if opt.metavar == "FILE"] 19 | opts_dir = [opt for opt in opts if opt.metavar == "DIR"] 20 | 21 | fileopts = [] 22 | for opt in opts_file: 23 | if opt._short_opts: 24 | fileopts.extend(opt._short_opts) 25 | if opt._long_opts: 26 | fileopts.extend(opt._long_opts) 27 | 28 | diropts = [] 29 | for opt in opts_dir: 30 | if opt._short_opts: 31 | diropts.extend(opt._short_opts) 32 | if opt._long_opts: 33 | diropts.extend(opt._long_opts) 34 | 35 | flags = [opt.get_opt_string() for opt in opts] 36 | 37 | with open(ZSH_COMPLETION_TEMPLATE) as f: 38 | template = f.read() 39 | 40 | template = template.replace("{{fileopts}}", "|".join(fileopts)) 41 | template = template.replace("{{diropts}}", "|".join(diropts)) 42 | template = template.replace("{{flags}}", " ".join(flags)) 43 | 44 | with open(ZSH_COMPLETION_FILE, "w") as f: 45 | f.write(template) 46 | 47 | 48 | parser = yt_dlp.parseOpts()[0] 49 | build_completion(parser) 50 | -------------------------------------------------------------------------------- /yt_dlp/extractor/hgtv.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class HGTVComShowIE(InfoExtractor): 8 | IE_NAME = 'hgtv.com:show' 9 | _VALID_URL = r'https?://(?:www\.)?hgtv\.com/shows/[^/]+/(?P[^/?#&]+)' 10 | _TESTS = [{ 11 | # data-module="video" 12 | 'url': 'http://www.hgtv.com/shows/flip-or-flop/flip-or-flop-full-episodes-season-4-videos', 13 | 'info_dict': { 14 | 'id': 'flip-or-flop-full-episodes-season-4-videos', 15 | 'title': 'Flip or Flop Full Episodes', 16 | }, 17 | 'playlist_mincount': 15, 18 | }, { 19 | # data-deferred-module="video" 20 | 'url': 'http://www.hgtv.com/shows/good-bones/episodes/an-old-victorian-house-gets-a-new-facelift', 21 | 'only_matching': True, 22 | }] 23 | 24 | def _real_extract(self, url): 25 | display_id = self._match_id(url) 26 | 27 | webpage = self._download_webpage(url, display_id) 28 | 29 | config = self._parse_json( 30 | self._search_regex( 31 | r'(?s)data-(?:deferred-)?module=["\']video["\'][^>]*>.*?]+type=["\']text/x-config["\'][^>]*>(.+?)[A-Za-z0-9]+)' 10 | _TESTS = [{ 11 | 'url': 'http://yourupload.com/watch/14i14h', 12 | 'md5': '5e2c63385454c557f97c4c4131a393cd', 13 | 'info_dict': { 14 | 'id': '14i14h', 15 | 'ext': 'mp4', 16 | 'title': 'BigBuckBunny_320x180.mp4', 17 | 'thumbnail': r're:^https?://.*\.jpe?g', 18 | } 19 | }, { 20 | 'url': 'http://www.yourupload.com/embed/14i14h', 21 | 'only_matching': True, 22 | }, { 23 | 'url': 'http://embed.yourupload.com/14i14h', 24 | 'only_matching': True, 25 | }] 26 | 27 | def _real_extract(self, url): 28 | video_id = self._match_id(url) 29 | 30 | embed_url = 'http://www.yourupload.com/embed/%s' % video_id 31 | 32 | webpage = self._download_webpage(embed_url, video_id) 33 | 34 | title = self._og_search_title(webpage) 35 | video_url = urljoin(embed_url, self._og_search_video_url(webpage)) 36 | thumbnail = self._og_search_thumbnail(webpage, default=None) 37 | 38 | return { 39 | 'id': video_id, 40 | 'title': title, 41 | 'url': video_url, 42 | 'thumbnail': thumbnail, 43 | 'http_headers': { 44 | 'Referer': embed_url, 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /yt_dlp/extractor/skylinewebcams.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class SkylineWebcamsIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?skylinewebcams\.com/[^/]+/webcam/(?:[^/]+/)+(?P[^/]+)\.html' 9 | _TEST = { 10 | 'url': 'https://www.skylinewebcams.com/it/webcam/italia/lazio/roma/scalinata-piazza-di-spagna-barcaccia.html', 11 | 'info_dict': { 12 | 'id': 'scalinata-piazza-di-spagna-barcaccia', 13 | 'ext': 'mp4', 14 | 'title': 're:^Live Webcam Scalinata di Piazza di Spagna - La Barcaccia [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$', 15 | 'description': 'Roma, veduta sulla Scalinata di Piazza di Spagna e sulla Barcaccia', 16 | 'is_live': True, 17 | }, 18 | 'params': { 19 | 'skip_download': True, 20 | } 21 | } 22 | 23 | def _real_extract(self, url): 24 | video_id = self._match_id(url) 25 | 26 | webpage = self._download_webpage(url, video_id) 27 | 28 | stream_url = self._search_regex( 29 | r'(?:url|source)\s*:\s*(["\'])(?P(?:https?:)?//.+?\.m3u8.*?)\1', webpage, 30 | 'stream url', group='url') 31 | 32 | title = self._og_search_title(webpage) 33 | description = self._og_search_description(webpage) 34 | 35 | return { 36 | 'id': video_id, 37 | 'url': stream_url, 38 | 'ext': 'mp4', 39 | 'title': title, 40 | 'description': description, 41 | 'is_live': True, 42 | } 43 | -------------------------------------------------------------------------------- /yt_dlp/extractor/trunews.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class TruNewsIE(InfoExtractor): 7 | _VALID_URL = r'https?://(?:www\.)?trunews\.com/stream/(?P[^/?#&]+)' 8 | _TEST = { 9 | 'url': 'https://www.trunews.com/stream/will-democrats-stage-a-circus-during-president-trump-s-state-of-the-union-speech', 10 | 'info_dict': { 11 | 'id': '5c5a21e65d3c196e1c0020cc', 12 | 'display_id': 'will-democrats-stage-a-circus-during-president-trump-s-state-of-the-union-speech', 13 | 'ext': 'mp4', 14 | 'title': "Will Democrats Stage a Circus During President Trump's State of the Union Speech?", 15 | 'description': 'md5:c583b72147cc92cf21f56a31aff7a670', 16 | 'duration': 3685, 17 | 'timestamp': 1549411440, 18 | 'upload_date': '20190206', 19 | }, 20 | 'add_ie': ['Zype'], 21 | } 22 | _ZYPE_TEMPL = 'https://player.zype.com/embed/%s.js?api_key=X5XnahkjCwJrT_l5zUqypnaLEObotyvtUKJWWlONxDoHVjP8vqxlArLV8llxMbyt' 23 | 24 | def _real_extract(self, url): 25 | display_id = self._match_id(url) 26 | 27 | zype_id = self._download_json( 28 | 'https://api.zype.com/videos', display_id, query={ 29 | 'app_key': 'PUVKp9WgGUb3-JUw6EqafLx8tFVP6VKZTWbUOR-HOm__g4fNDt1bCsm_LgYf_k9H', 30 | 'per_page': 1, 31 | 'active': 'true', 32 | 'friendly_title': display_id, 33 | })['response'][0]['_id'] 34 | return self.url_result(self._ZYPE_TEMPL % zype_id, 'Zype', zype_id) 35 | -------------------------------------------------------------------------------- /test/test_age_restriction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import unicode_literals 3 | 4 | # Allow direct execution 5 | import os 6 | import sys 7 | import unittest 8 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 9 | 10 | from test.helper import try_rm, is_download_test 11 | 12 | from yt_dlp import YoutubeDL 13 | 14 | 15 | def _download_restricted(url, filename, age): 16 | """ Returns true if the file has been downloaded """ 17 | 18 | params = { 19 | 'age_limit': age, 20 | 'skip_download': True, 21 | 'writeinfojson': True, 22 | 'outtmpl': '%(id)s.%(ext)s', 23 | } 24 | ydl = YoutubeDL(params) 25 | ydl.add_default_info_extractors() 26 | json_filename = os.path.splitext(filename)[0] + '.info.json' 27 | try_rm(json_filename) 28 | ydl.download([url]) 29 | res = os.path.exists(json_filename) 30 | try_rm(json_filename) 31 | return res 32 | 33 | 34 | @is_download_test 35 | class TestAgeRestriction(unittest.TestCase): 36 | def _assert_restricted(self, url, filename, age, old_age=None): 37 | self.assertTrue(_download_restricted(url, filename, old_age)) 38 | self.assertFalse(_download_restricted(url, filename, age)) 39 | 40 | def test_youtube(self): 41 | self._assert_restricted('07FYdnEawAQ', '07FYdnEawAQ.mp4', 10) 42 | 43 | def test_youporn(self): 44 | self._assert_restricted( 45 | 'http://www.youporn.com/watch/505835/sex-ed-is-it-safe-to-masturbate-daily/', 46 | '505835.mp4', 2, old_age=25) 47 | 48 | 49 | if __name__ == '__main__': 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /yt_dlp/extractor/ro220.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from ..compat import compat_urllib_parse_unquote 5 | 6 | 7 | class Ro220IE(InfoExtractor): 8 | IE_NAME = '220.ro' 9 | _VALID_URL = r'(?x)(?:https?://)?(?:www\.)?220\.ro/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)' 10 | _TEST = { 11 | 'url': 'http://www.220.ro/sport/Luati-Le-Banii-Sez-4-Ep-1/LYV6doKo7f/', 12 | 'md5': '03af18b73a07b4088753930db7a34add', 13 | 'info_dict': { 14 | 'id': 'LYV6doKo7f', 15 | 'ext': 'mp4', 16 | 'title': 'Luati-le Banii sez 4 ep 1', 17 | 'description': r're:^Iata-ne reveniti dupa o binemeritata vacanta\. +Va astept si pe Facebook cu pareri si comentarii.$', 18 | } 19 | } 20 | 21 | def _real_extract(self, url): 22 | video_id = self._match_id(url) 23 | 24 | webpage = self._download_webpage(url, video_id) 25 | url = compat_urllib_parse_unquote(self._search_regex( 26 | r'(?s)clip\s*:\s*{.*?url\s*:\s*\'([^\']+)\'', webpage, 'url')) 27 | title = self._og_search_title(webpage) 28 | description = self._og_search_description(webpage) 29 | thumbnail = self._og_search_thumbnail(webpage) 30 | 31 | formats = [{ 32 | 'format_id': 'sd', 33 | 'url': url, 34 | 'ext': 'mp4', 35 | }] 36 | 37 | return { 38 | 'id': video_id, 39 | 'formats': formats, 40 | 'title': title, 41 | 'description': description, 42 | 'thumbnail': thumbnail, 43 | } 44 | -------------------------------------------------------------------------------- /yt_dlp/extractor/xbef.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from ..compat import compat_urllib_parse_unquote 5 | 6 | 7 | class XBefIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?xbef\.com/video/(?P[0-9]+)' 9 | _TEST = { 10 | 'url': 'http://xbef.com/video/5119-glamourous-lesbians-smoking-drinking-and-fucking', 11 | 'md5': 'a478b565baff61634a98f5e5338be995', 12 | 'info_dict': { 13 | 'id': '5119', 14 | 'ext': 'mp4', 15 | 'title': 'md5:7358a9faef8b7b57acda7c04816f170e', 16 | 'age_limit': 18, 17 | 'thumbnail': r're:^http://.*\.jpg', 18 | } 19 | } 20 | 21 | def _real_extract(self, url): 22 | video_id = self._match_id(url) 23 | webpage = self._download_webpage(url, video_id) 24 | 25 | title = self._html_search_regex( 26 | r']*>(.*?)
  • ', webpage, 'title') 27 | 28 | config_url_enc = self._download_webpage( 29 | 'http://xbef.com/Main/GetVideoURLEncoded/%s' % video_id, video_id, 30 | note='Retrieving config URL') 31 | config_url = compat_urllib_parse_unquote(config_url_enc) 32 | config = self._download_xml( 33 | config_url, video_id, note='Retrieving config') 34 | 35 | video_url = config.find('./file').text 36 | thumbnail = config.find('./image').text 37 | 38 | return { 39 | 'id': video_id, 40 | 'url': video_url, 41 | 'title': title, 42 | 'thumbnail': thumbnail, 43 | 'age_limit': 18, 44 | } 45 | -------------------------------------------------------------------------------- /yt_dlp/extractor/filmweb.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class FilmwebIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?filmweb\.no/(?Ptrailere|filmnytt)/article(?P\d+)\.ece' 9 | _TEST = { 10 | 'url': 'http://www.filmweb.no/trailere/article1264921.ece', 11 | 'md5': 'e353f47df98e557d67edaceda9dece89', 12 | 'info_dict': { 13 | 'id': '13033574', 14 | 'ext': 'mp4', 15 | 'title': 'Det som en gang var', 16 | 'upload_date': '20160316', 17 | 'timestamp': 1458140101, 18 | 'uploader_id': '12639966', 19 | 'uploader': 'Live Roaldset', 20 | } 21 | } 22 | 23 | def _real_extract(self, url): 24 | article_type, article_id = self._match_valid_url(url).groups() 25 | if article_type == 'filmnytt': 26 | webpage = self._download_webpage(url, article_id) 27 | article_id = self._search_regex(r'data-videoid="(\d+)"', webpage, 'article id') 28 | embed_code = self._download_json( 29 | 'https://www.filmweb.no/template_v2/ajax/json_trailerEmbed.jsp', 30 | article_id, query={ 31 | 'articleId': article_id, 32 | })['embedCode'] 33 | iframe_url = self._proto_relative_url(self._search_regex( 34 | r']+src="([^"]+)', embed_code, 'iframe url')) 35 | 36 | return { 37 | '_type': 'url_transparent', 38 | 'id': article_id, 39 | 'url': iframe_url, 40 | 'ie_key': 'TwentyThreeVideo', 41 | } 42 | -------------------------------------------------------------------------------- /yt_dlp/extractor/fox9.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class FOX9IE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?fox9\.com/video/(?P\d+)' 9 | 10 | def _real_extract(self, url): 11 | video_id = self._match_id(url) 12 | return self.url_result( 13 | 'anvato:anvato_epfox_app_web_prod_b3373168e12f423f41504f207000188daf88251b:' + video_id, 14 | 'Anvato', video_id) 15 | 16 | 17 | class FOX9NewsIE(InfoExtractor): 18 | _VALID_URL = r'https?://(?:www\.)?fox9\.com/news/(?P[^/?&#]+)' 19 | _TEST = { 20 | 'url': 'https://www.fox9.com/news/black-bear-in-tree-draws-crowd-in-downtown-duluth-minnesota', 21 | 'md5': 'd6e1b2572c3bab8a849c9103615dd243', 22 | 'info_dict': { 23 | 'id': '314473', 24 | 'ext': 'mp4', 25 | 'title': 'Bear climbs tree in downtown Duluth', 26 | 'description': 'md5:6a36bfb5073a411758a752455408ac90', 27 | 'duration': 51, 28 | 'timestamp': 1478123580, 29 | 'upload_date': '20161102', 30 | 'uploader': 'EPFOX', 31 | 'categories': ['News', 'Sports'], 32 | 'tags': ['news', 'video'], 33 | }, 34 | } 35 | 36 | def _real_extract(self, url): 37 | display_id = self._match_id(url) 38 | webpage = self._download_webpage(url, display_id) 39 | anvato_id = self._search_regex( 40 | r'anvatoId\s*:\s*[\'"](\d+)', webpage, 'anvato id') 41 | return self.url_result('https://www.fox9.com/video/' + anvato_id, 'FOX9') 42 | -------------------------------------------------------------------------------- /yt_dlp/extractor/tastytrade.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from .ooyala import OoyalaIE 5 | 6 | 7 | class TastyTradeIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?tastytrade\.com/tt/shows/[^/]+/episodes/(?P[^/?#&]+)' 9 | 10 | _TESTS = [{ 11 | 'url': 'https://www.tastytrade.com/tt/shows/market-measures/episodes/correlation-in-short-volatility-06-28-2017', 12 | 'info_dict': { 13 | 'id': 'F3bnlzbToeI6pLEfRyrlfooIILUjz4nM', 14 | 'ext': 'mp4', 15 | 'title': 'A History of Teaming', 16 | 'description': 'md5:2a9033db8da81f2edffa4c99888140b3', 17 | 'duration': 422.255, 18 | }, 19 | 'params': { 20 | 'skip_download': True, 21 | }, 22 | 'add_ie': ['Ooyala'], 23 | }, { 24 | 'url': 'https://www.tastytrade.com/tt/shows/daily-dose/episodes/daily-dose-06-30-2017', 25 | 'only_matching': True, 26 | }] 27 | 28 | def _real_extract(self, url): 29 | display_id = self._match_id(url) 30 | webpage = self._download_webpage(url, display_id) 31 | 32 | ooyala_code = self._search_regex( 33 | r'data-media-id=(["\'])(?P(?:(?!\1).)+)\1', 34 | webpage, 'ooyala code', group='code') 35 | 36 | info = self._search_json_ld(webpage, display_id, fatal=False) 37 | info.update({ 38 | '_type': 'url_transparent', 39 | 'ie_key': OoyalaIE.ie_key(), 40 | 'url': 'ooyala:%s' % ooyala_code, 41 | 'display_id': display_id, 42 | }) 43 | return info 44 | -------------------------------------------------------------------------------- /yt_dlp/extractor/miaopai.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class MiaoPaiIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?miaopai\.com/show/(?P[-A-Za-z0-9~_]+)' 9 | _TEST = { 10 | 'url': 'http://www.miaopai.com/show/n~0hO7sfV1nBEw4Y29-Hqg__.htm', 11 | 'md5': '095ed3f1cd96b821add957bdc29f845b', 12 | 'info_dict': { 13 | 'id': 'n~0hO7sfV1nBEw4Y29-Hqg__', 14 | 'ext': 'mp4', 15 | 'title': '西游记音乐会的秒拍视频', 16 | 'thumbnail': 're:^https?://.*/n~0hO7sfV1nBEw4Y29-Hqg___m.jpg', 17 | } 18 | } 19 | 20 | _USER_AGENT_IPAD = 'Mozilla/5.0 (iPad; CPU OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1' 21 | 22 | def _real_extract(self, url): 23 | video_id = self._match_id(url) 24 | webpage = self._download_webpage( 25 | url, video_id, headers={'User-Agent': self._USER_AGENT_IPAD}) 26 | 27 | title = self._html_search_regex( 28 | r'([^<]+)', webpage, 'title') 29 | thumbnail = self._html_search_regex( 30 | r']+class=(?P[\'"]).*\bvideo_img\b.*(?P=q1)[^>]+data-url=(?P[\'"])(?P[^\'"]+)(?P=q2)', 31 | webpage, 'thumbnail', fatal=False, group='url') 32 | videos = self._parse_html5_media_entries(url, webpage, video_id) 33 | info = videos[0] 34 | 35 | info.update({ 36 | 'id': video_id, 37 | 'title': title, 38 | 'thumbnail': thumbnail, 39 | }) 40 | return info 41 | -------------------------------------------------------------------------------- /yt_dlp/extractor/fujitv.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class FujiTVFODPlus7IE(InfoExtractor): 8 | _VALID_URL = r'https?://fod\.fujitv\.co\.jp/title/[0-9a-z]{4}/(?P[0-9a-z]+)' 9 | _BASE_URL = 'http://i.fod.fujitv.co.jp/' 10 | _BITRATE_MAP = { 11 | 300: (320, 180), 12 | 800: (640, 360), 13 | 1200: (1280, 720), 14 | 2000: (1280, 720), 15 | 4000: (1920, 1080), 16 | } 17 | 18 | _TESTS = [{ 19 | 'url': 'https://fod.fujitv.co.jp/title/5d40/5d40810075', 20 | 'info_dict': { 21 | 'id': '5d40810075', 22 | 'title': '5d40810075', 23 | 'ext': 'mp4', 24 | 'format_id': '4000', 25 | 'thumbnail': 'http://i.fod.fujitv.co.jp/pc/image/wbtn/wbtn_5d40810075.jpg' 26 | }, 27 | 'skip': 'Expires after a week' 28 | }] 29 | 30 | def _real_extract(self, url): 31 | video_id = self._match_id(url) 32 | formats = self._extract_m3u8_formats( 33 | self._BASE_URL + 'abr/tv_android/%s.m3u8' % video_id, video_id, 'mp4') 34 | for f in formats: 35 | wh = self._BITRATE_MAP.get(f.get('tbr')) 36 | if wh: 37 | f.update({ 38 | 'width': wh[0], 39 | 'height': wh[1], 40 | }) 41 | self._sort_formats(formats) 42 | 43 | return { 44 | 'id': video_id, 45 | 'title': video_id, 46 | 'formats': formats, 47 | 'thumbnail': self._BASE_URL + 'pc/image/wbtn/wbtn_%s.jpg' % video_id, 48 | } 49 | -------------------------------------------------------------------------------- /yt_dlp/extractor/rtvs.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class RTVSIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?rtvs\.sk/(?:radio|televizia)/archiv/\d+/(?P\d+)' 9 | _TESTS = [{ 10 | # radio archive 11 | 'url': 'http://www.rtvs.sk/radio/archiv/11224/414872', 12 | 'md5': '134d5d6debdeddf8a5d761cbc9edacb8', 13 | 'info_dict': { 14 | 'id': '414872', 15 | 'ext': 'mp3', 16 | 'title': 'Ostrov pokladov 1 časť.mp3' 17 | }, 18 | 'params': { 19 | 'skip_download': True, 20 | } 21 | }, { 22 | # tv archive 23 | 'url': 'http://www.rtvs.sk/televizia/archiv/8249/63118', 24 | 'md5': '85e2c55cf988403b70cac24f5c086dc6', 25 | 'info_dict': { 26 | 'id': '63118', 27 | 'ext': 'mp4', 28 | 'title': 'Amaro Džives - Náš deň', 29 | 'description': 'Galavečer pri príležitosti Medzinárodného dňa Rómov.' 30 | }, 31 | 'params': { 32 | 'skip_download': True, 33 | } 34 | }] 35 | 36 | def _real_extract(self, url): 37 | video_id = self._match_id(url) 38 | 39 | webpage = self._download_webpage(url, video_id) 40 | 41 | playlist_url = self._search_regex( 42 | r'playlist["\']?\s*:\s*(["\'])(?P(?:(?!\1).)+)\1', webpage, 43 | 'playlist url', group='url') 44 | 45 | data = self._download_json( 46 | playlist_url, video_id, 'Downloading playlist')[0] 47 | return self._parse_jwplayer_data(data, video_id=video_id) 48 | -------------------------------------------------------------------------------- /yt_dlp/extractor/ehow.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from ..compat import compat_urllib_parse_unquote 5 | 6 | 7 | class EHowIE(InfoExtractor): 8 | IE_NAME = 'eHow' 9 | _VALID_URL = r'https?://(?:www\.)?ehow\.com/[^/_?]*_(?P[0-9]+)' 10 | _TEST = { 11 | 'url': 'http://www.ehow.com/video_12245069_hardwood-flooring-basics.html', 12 | 'md5': '9809b4e3f115ae2088440bcb4efbf371', 13 | 'info_dict': { 14 | 'id': '12245069', 15 | 'ext': 'flv', 16 | 'title': 'Hardwood Flooring Basics', 17 | 'description': 'Hardwood flooring may be time consuming, but its ultimately a pretty straightforward concept. Learn about hardwood flooring basics with help from a hardware flooring business owner in this free video...', 18 | 'uploader': 'Erick Nathan', 19 | } 20 | } 21 | 22 | def _real_extract(self, url): 23 | video_id = self._match_id(url) 24 | webpage = self._download_webpage(url, video_id) 25 | video_url = self._search_regex( 26 | r'(?:file|source)=(http[^\'"&]*)', webpage, 'video URL') 27 | final_url = compat_urllib_parse_unquote(video_url) 28 | uploader = self._html_search_meta('uploader', webpage) 29 | title = self._og_search_title(webpage).replace(' | eHow', '') 30 | 31 | return { 32 | 'id': video_id, 33 | 'url': final_url, 34 | 'title': title, 35 | 'thumbnail': self._og_search_thumbnail(webpage), 36 | 'description': self._og_search_description(webpage), 37 | 'uploader': uploader, 38 | } 39 | -------------------------------------------------------------------------------- /yt_dlp/extractor/livejournal.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | from ..compat import compat_str 6 | from ..utils import int_or_none 7 | 8 | 9 | class LiveJournalIE(InfoExtractor): 10 | _VALID_URL = r'https?://(?:[^.]+\.)?livejournal\.com/video/album/\d+.+?\bid=(?P\d+)' 11 | _TEST = { 12 | 'url': 'https://andrei-bt.livejournal.com/video/album/407/?mode=view&id=51272', 13 | 'md5': 'adaf018388572ced8a6f301ace49d4b2', 14 | 'info_dict': { 15 | 'id': '1263729', 16 | 'ext': 'mp4', 17 | 'title': 'Истребители против БПЛА', 18 | 'upload_date': '20190624', 19 | 'timestamp': 1561406715, 20 | } 21 | } 22 | 23 | def _real_extract(self, url): 24 | video_id = self._match_id(url) 25 | webpage = self._download_webpage(url, video_id) 26 | record = self._parse_json(self._search_regex( 27 | r'Site\.page\s*=\s*({.+?});', webpage, 28 | 'page data'), video_id)['video']['record'] 29 | storage_id = compat_str(record['storageid']) 30 | title = record.get('name') 31 | if title: 32 | # remove filename extension(.mp4, .mov, etc...) 33 | title = title.rsplit('.', 1)[0] 34 | return { 35 | '_type': 'url_transparent', 36 | 'id': video_id, 37 | 'title': title, 38 | 'thumbnail': record.get('thumbnail'), 39 | 'timestamp': int_or_none(record.get('timecreate')), 40 | 'url': 'eagleplatform:vc.videos.livejournal.com:' + storage_id, 41 | 'ie_key': 'EaglePlatform', 42 | } 43 | -------------------------------------------------------------------------------- /yt_dlp/extractor/oktoberfesttv.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class OktoberfestTVIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?oktoberfest-tv\.de/[^/]+/[^/]+/video/(?P[^/?#]+)' 9 | 10 | _TEST = { 11 | 'url': 'http://www.oktoberfest-tv.de/de/kameras/video/hb-zelt', 12 | 'info_dict': { 13 | 'id': 'hb-zelt', 14 | 'ext': 'mp4', 15 | 'title': 're:^Live-Kamera: Hofbräuzelt [0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}$', 16 | 'thumbnail': r're:^https?://.*\.jpg$', 17 | 'is_live': True, 18 | }, 19 | 'params': { 20 | 'skip_download': True, 21 | } 22 | } 23 | 24 | def _real_extract(self, url): 25 | video_id = self._match_id(url) 26 | webpage = self._download_webpage(url, video_id) 27 | 28 | title = self._html_search_regex( 29 | r'

    .*?(.*?)

    ', webpage, 'title') 30 | 31 | clip = self._search_regex( 32 | r"clip:\s*\{\s*url:\s*'([^']+)'", webpage, 'clip') 33 | ncurl = self._search_regex( 34 | r"netConnectionUrl:\s*'([^']+)'", webpage, 'rtmp base') 35 | video_url = ncurl + clip 36 | thumbnail = self._search_regex( 37 | r"canvas:\s*\{\s*backgroundImage:\s*'url\(([^)]+)\)'", webpage, 38 | 'thumbnail', fatal=False) 39 | 40 | return { 41 | 'id': video_id, 42 | 'title': title, 43 | 'url': video_url, 44 | 'ext': 'mp4', 45 | 'is_live': True, 46 | 'thumbnail': thumbnail, 47 | } 48 | -------------------------------------------------------------------------------- /yt_dlp/extractor/breitbart.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | 5 | 6 | class BreitBartIE(InfoExtractor): 7 | _VALID_URL = r'https?:\/\/(?:www\.)breitbart.com/videos/v/(?P[^/]+)' 8 | _TESTS = [{ 9 | 'url': 'https://www.breitbart.com/videos/v/5cOz1yup/?pl=Ij6NDOji', 10 | 'md5': '0aa6d1d6e183ac5ca09207fe49f17ade', 11 | 'info_dict': { 12 | 'id': '5cOz1yup', 13 | 'ext': 'mp4', 14 | 'title': 'Watch \u2013 Clyburn: Statues in Congress Have to Go Because they Are Honoring Slavery', 15 | 'description': 'md5:bac35eb0256d1cb17f517f54c79404d5', 16 | 'thumbnail': 'https://cdn.jwplayer.com/thumbs/5cOz1yup-1920.jpg', 17 | 'age_limit': 0, 18 | } 19 | }, { 20 | 'url': 'https://www.breitbart.com/videos/v/eaiZjVOn/', 21 | 'only_matching': True, 22 | }] 23 | 24 | def _real_extract(self, url): 25 | video_id = self._match_id(url) 26 | webpage = self._download_webpage(url, video_id) 27 | 28 | formats = self._extract_m3u8_formats(f'https://cdn.jwplayer.com/manifests/{video_id}.m3u8', video_id, ext='mp4') 29 | self._sort_formats(formats) 30 | return { 31 | 'id': video_id, 32 | 'title': self._og_search_title( 33 | webpage, default=None) or self._html_search_regex( 34 | r'(?s)(.*?)', webpage, 'video title'), 35 | 'description': self._og_search_description(webpage), 36 | 'thumbnail': self._og_search_thumbnail(webpage), 37 | 'age_limit': self._rta_search(webpage), 38 | 'formats': formats 39 | } 40 | -------------------------------------------------------------------------------- /yt_dlp/extractor/odatv.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | from ..utils import ( 6 | ExtractorError, 7 | NO_DEFAULT, 8 | remove_start 9 | ) 10 | 11 | 12 | class OdaTVIE(InfoExtractor): 13 | _VALID_URL = r'https?://(?:www\.)?odatv\.com/(?:mob|vid)_video\.php\?.*\bid=(?P[^&]+)' 14 | _TESTS = [{ 15 | 'url': 'http://odatv.com/vid_video.php?id=8E388', 16 | 'md5': 'dc61d052f205c9bf2da3545691485154', 17 | 'info_dict': { 18 | 'id': '8E388', 19 | 'ext': 'mp4', 20 | 'title': 'Artık Davutoğlu ile devam edemeyiz' 21 | } 22 | }, { 23 | # mobile URL 24 | 'url': 'http://odatv.com/mob_video.php?id=8E388', 25 | 'only_matching': True, 26 | }, { 27 | # no video 28 | 'url': 'http://odatv.com/mob_video.php?id=8E900', 29 | 'only_matching': True, 30 | }] 31 | 32 | def _real_extract(self, url): 33 | video_id = self._match_id(url) 34 | webpage = self._download_webpage(url, video_id) 35 | 36 | no_video = 'NO VIDEO!' in webpage 37 | 38 | video_url = self._search_regex( 39 | r'mp4\s*:\s*(["\'])(?Phttp.+?)\1', webpage, 'video url', 40 | default=None if no_video else NO_DEFAULT, group='url') 41 | 42 | if no_video: 43 | raise ExtractorError('Video %s does not exist' % video_id, expected=True) 44 | 45 | return { 46 | 'id': video_id, 47 | 'url': video_url, 48 | 'title': remove_start(self._og_search_title(webpage), 'Video: '), 49 | 'thumbnail': self._og_search_thumbnail(webpage), 50 | } 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/5_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Request a new functionality unrelated to any particular site or extractor 3 | labels: [triage, enhancement] 4 | body: 5 | - type: checkboxes 6 | id: checklist 7 | attributes: 8 | label: Checklist 9 | description: | 10 | Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: 11 | options: 12 | - label: I'm reporting a feature request 13 | required: true 14 | - label: I've verified that I'm running yt-dlp version **2022.01.21**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) 15 | required: true 16 | - label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates 17 | required: true 18 | - label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue) 19 | required: true 20 | - type: textarea 21 | id: description 22 | attributes: 23 | label: Description 24 | description: | 25 | Provide an explanation of your site feature request in an arbitrary form. 26 | Please make sure the description is worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient). 27 | Provide any additional information, any suggested solutions, and as much context and examples as possible 28 | placeholder: WRITE DESCRIPTION HERE 29 | validations: 30 | required: true 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE_tmpl/5_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Request a new functionality unrelated to any particular site or extractor 3 | labels: [triage, enhancement] 4 | body: 5 | - type: checkboxes 6 | id: checklist 7 | attributes: 8 | label: Checklist 9 | description: | 10 | Carefully read and work through this check list in order to prevent the most common mistakes and misuse of yt-dlp: 11 | options: 12 | - label: I'm reporting a feature request 13 | required: true 14 | - label: I've verified that I'm running yt-dlp version **%(version)s**. ([update instructions](https://github.com/yt-dlp/yt-dlp#update)) 15 | required: true 16 | - label: I've searched the [bugtracker](https://github.com/yt-dlp/yt-dlp/issues?q=) for similar issues including closed ones. DO NOT post duplicates 17 | required: true 18 | - label: I've read the [guidelines for opening an issue](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#opening-an-issue) 19 | required: true 20 | - type: textarea 21 | id: description 22 | attributes: 23 | label: Description 24 | description: | 25 | Provide an explanation of your site feature request in an arbitrary form. 26 | Please make sure the description is worded well enough to be understood, see [is-the-description-of-the-issue-itself-sufficient](https://github.com/ytdl-org/youtube-dl#is-the-description-of-the-issue-itself-sufficient). 27 | Provide any additional information, any suggested solutions, and as much context and examples as possible 28 | placeholder: WRITE DESCRIPTION HERE 29 | validations: 30 | required: true 31 | -------------------------------------------------------------------------------- /yt_dlp/extractor/vodplatform.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | from ..utils import unescapeHTML 6 | 7 | 8 | class VODPlatformIE(InfoExtractor): 9 | _VALID_URL = r'https?://(?:(?:www\.)?vod-platform\.net|embed\.kwikmotion\.com)/[eE]mbed/(?P[^/?#]+)' 10 | _TESTS = [{ 11 | # from http://www.lbcgroup.tv/watch/chapter/29143/52844/%D8%A7%D9%84%D9%86%D8%B5%D8%B1%D8%A9-%D9%81%D9%8A-%D8%B6%D9%8A%D8%A7%D9%81%D8%A9-%D8%A7%D9%84%D9%80-cnn/ar 12 | 'url': 'http://vod-platform.net/embed/RufMcytHDolTH1MuKHY9Fw', 13 | 'md5': '1db2b7249ce383d6be96499006e951fc', 14 | 'info_dict': { 15 | 'id': 'RufMcytHDolTH1MuKHY9Fw', 16 | 'ext': 'mp4', 17 | 'title': 'LBCi News_ النصرة في ضيافة الـ "سي.أن.أن"', 18 | } 19 | }, { 20 | 'url': 'http://embed.kwikmotion.com/embed/RufMcytHDolTH1MuKHY9Fw', 21 | 'only_matching': True, 22 | }] 23 | 24 | def _real_extract(self, url): 25 | video_id = self._match_id(url) 26 | webpage = self._download_webpage(url, video_id) 27 | 28 | title = unescapeHTML(self._og_search_title(webpage)) 29 | hidden_inputs = self._hidden_inputs(webpage) 30 | 31 | formats = self._extract_wowza_formats( 32 | hidden_inputs.get('HiddenmyhHlsLink') or hidden_inputs['HiddenmyDashLink'], video_id, skip_protocols=['f4m', 'smil']) 33 | self._sort_formats(formats) 34 | 35 | return { 36 | 'id': video_id, 37 | 'title': title, 38 | 'thumbnail': hidden_inputs.get('HiddenThumbnail') or self._og_search_thumbnail(webpage), 39 | 'formats': formats, 40 | } 41 | -------------------------------------------------------------------------------- /yt_dlp/extractor/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ..utils import load_plugins 4 | 5 | _LAZY_LOADER = False 6 | if not os.environ.get('YTDLP_NO_LAZY_EXTRACTORS'): 7 | try: 8 | from .lazy_extractors import * 9 | from .lazy_extractors import _ALL_CLASSES 10 | _LAZY_LOADER = True 11 | except ImportError: 12 | pass 13 | 14 | if not _LAZY_LOADER: 15 | from .extractors import * 16 | _ALL_CLASSES = [ 17 | klass 18 | for name, klass in globals().items() 19 | if name.endswith('IE') and name != 'GenericIE' 20 | ] 21 | _ALL_CLASSES.append(GenericIE) 22 | 23 | _PLUGIN_CLASSES = load_plugins('extractor', 'IE', globals()) 24 | _ALL_CLASSES = list(_PLUGIN_CLASSES.values()) + _ALL_CLASSES 25 | 26 | 27 | def gen_extractor_classes(): 28 | """ Return a list of supported extractors. 29 | The order does matter; the first extractor matched is the one handling the URL. 30 | """ 31 | return _ALL_CLASSES 32 | 33 | 34 | def gen_extractors(): 35 | """ Return a list of an instance of every supported extractor. 36 | The order does matter; the first extractor matched is the one handling the URL. 37 | """ 38 | return [klass() for klass in gen_extractor_classes()] 39 | 40 | 41 | def list_extractors(age_limit): 42 | """ 43 | Return a list of extractors that are suitable for the given age, 44 | sorted by extractor ID. 45 | """ 46 | 47 | return sorted( 48 | filter(lambda ie: ie.is_suitable(age_limit), gen_extractors()), 49 | key=lambda ie: ie.IE_NAME.lower()) 50 | 51 | 52 | def get_info_extractor(ie_name): 53 | """Returns the info extractor class with the given ie_name""" 54 | return globals()[ie_name + 'IE'] 55 | -------------------------------------------------------------------------------- /yt_dlp/extractor/glide.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class GlideIE(InfoExtractor): 8 | IE_DESC = 'Glide mobile video messages (glide.me)' 9 | _VALID_URL = r'https?://share\.glide\.me/(?P[A-Za-z0-9\-=_+]+)' 10 | _TEST = { 11 | 'url': 'http://share.glide.me/UZF8zlmuQbe4mr+7dCiQ0w==', 12 | 'md5': '4466372687352851af2d131cfaa8a4c7', 13 | 'info_dict': { 14 | 'id': 'UZF8zlmuQbe4mr+7dCiQ0w==', 15 | 'ext': 'mp4', 16 | 'title': "Damon's Glide message", 17 | 'thumbnail': r're:^https?://.*?\.cloudfront\.net/.*\.jpg$', 18 | } 19 | } 20 | 21 | def _real_extract(self, url): 22 | video_id = self._match_id(url) 23 | 24 | webpage = self._download_webpage(url, video_id) 25 | 26 | title = self._html_search_regex( 27 | r'(.+?)', webpage, 28 | 'title', default=None) or self._og_search_title(webpage) 29 | video_url = self._proto_relative_url(self._search_regex( 30 | r']+src=(["\'])(?P.+?)\1', 31 | webpage, 'video URL', default=None, 32 | group='url')) or self._og_search_video_url(webpage) 33 | thumbnail = self._proto_relative_url(self._search_regex( 34 | r']+id=["\']video-thumbnail["\'][^>]+src=(["\'])(?P.+?)\1', 35 | webpage, 'thumbnail url', default=None, 36 | group='url')) or self._og_search_thumbnail(webpage) 37 | 38 | return { 39 | 'id': video_id, 40 | 'title': title, 41 | 'url': video_url, 42 | 'thumbnail': thumbnail, 43 | } 44 | -------------------------------------------------------------------------------- /yt_dlp/extractor/ruhd.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class RUHDIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?ruhd\.ru/play\.php\?vid=(?P\d+)' 9 | _TEST = { 10 | 'url': 'http://www.ruhd.ru/play.php?vid=207', 11 | 'md5': 'd1a9ec4edf8598e3fbd92bb16072ba83', 12 | 'info_dict': { 13 | 'id': '207', 14 | 'ext': 'divx', 15 | 'title': 'КОТ бааааам', 16 | 'description': 'классный кот)', 17 | 'thumbnail': r're:^http://.*\.jpg$', 18 | } 19 | } 20 | 21 | def _real_extract(self, url): 22 | video_id = self._match_id(url) 23 | webpage = self._download_webpage(url, video_id) 24 | 25 | video_url = self._html_search_regex( 26 | r'([^<]+)   RUHD\.ru - Видео Высокого качества №1 в России!', 29 | webpage, 'title') 30 | description = self._html_search_regex( 31 | r'(?s)
    (.+?)', 32 | webpage, 'description', fatal=False) 33 | thumbnail = self._html_search_regex( 34 | r'\d+)' 8 | _TESTS = [{ 9 | 'url': 'http://www.thisamericanlife.org/radio-archives/episode/487/harper-high-school-part-one', 10 | 'md5': '8f7d2da8926298fdfca2ee37764c11ce', 11 | 'info_dict': { 12 | 'id': '487', 13 | 'ext': 'm4a', 14 | 'title': '487: Harper High School, Part One', 15 | 'description': 'md5:ee40bdf3fb96174a9027f76dbecea655', 16 | 'thumbnail': r're:^https?://.*\.jpg$', 17 | }, 18 | }, { 19 | 'url': 'http://www.thisamericanlife.org/play_full.php?play=487', 20 | 'only_matching': True, 21 | }] 22 | 23 | def _real_extract(self, url): 24 | video_id = self._match_id(url) 25 | 26 | webpage = self._download_webpage( 27 | 'http://www.thisamericanlife.org/radio-archives/episode/%s' % video_id, video_id) 28 | 29 | return { 30 | 'id': video_id, 31 | 'url': 'http://stream.thisamericanlife.org/{0}/stream/{0}_64k.m3u8'.format(video_id), 32 | 'protocol': 'm3u8_native', 33 | 'ext': 'm4a', 34 | 'acodec': 'aac', 35 | 'vcodec': 'none', 36 | 'abr': 64, 37 | 'title': self._html_search_meta(r'twitter:title', webpage, 'title', fatal=True), 38 | 'description': self._html_search_meta(r'description', webpage, 'description'), 39 | 'thumbnail': self._og_search_thumbnail(webpage), 40 | } 41 | -------------------------------------------------------------------------------- /test/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "latest": "2013.01.06", 3 | "signature": "72158cdba391628569ffdbea259afbcf279bbe3d8aeb7492690735dc1cfa6afa754f55c61196f3871d429599ab22f2667f1fec98865527b32632e7f4b3675a7ef0f0fbe084d359256ae4bba68f0d33854e531a70754712f244be71d4b92e664302aa99653ee4df19800d955b6c4149cd2b3f24288d6e4b40b16126e01f4c8ce6", 4 | "versions": { 5 | "2013.01.02": { 6 | "bin": [ 7 | "http://youtube-dl.org/downloads/2013.01.02/youtube-dl", 8 | "f5b502f8aaa77675c4884938b1e4871ebca2611813a0c0e74f60c0fbd6dcca6b" 9 | ], 10 | "exe": [ 11 | "http://youtube-dl.org/downloads/2013.01.02/youtube-dl.exe", 12 | "75fa89d2ce297d102ff27675aa9d92545bbc91013f52ec52868c069f4f9f0422" 13 | ], 14 | "tar": [ 15 | "http://youtube-dl.org/downloads/2013.01.02/youtube-dl-2013.01.02.tar.gz", 16 | "6a66d022ac8e1c13da284036288a133ec8dba003b7bd3a5179d0c0daca8c8196" 17 | ] 18 | }, 19 | "2013.01.06": { 20 | "bin": [ 21 | "http://youtube-dl.org/downloads/2013.01.06/youtube-dl", 22 | "64b6ed8865735c6302e836d4d832577321b4519aa02640dc508580c1ee824049" 23 | ], 24 | "exe": [ 25 | "http://youtube-dl.org/downloads/2013.01.06/youtube-dl.exe", 26 | "58609baf91e4389d36e3ba586e21dab882daaaee537e4448b1265392ae86ff84" 27 | ], 28 | "tar": [ 29 | "http://youtube-dl.org/downloads/2013.01.06/youtube-dl-2013.01.06.tar.gz", 30 | "fe77ab20a95d980ed17a659aa67e371fdd4d656d19c4c7950e7b720b0c2f1a86" 31 | ] 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Config 2 | *.conf 3 | cookies 4 | *cookies.txt 5 | .netrc 6 | 7 | # Downloaded 8 | *.annotations.xml 9 | *.aria2 10 | *.description 11 | *.dump 12 | *.frag 13 | *.frag.aria2 14 | *.frag.urls 15 | *.info.json 16 | *.live_chat.json 17 | *.meta 18 | *.part* 19 | *.tmp 20 | *.temp 21 | *.unknown_video 22 | *.ytdl 23 | .cache/ 24 | 25 | *.3gp 26 | *.ape 27 | *.avi 28 | *.desktop 29 | *.flac 30 | *.flv 31 | *.jpeg 32 | *.jpg 33 | *.m4a 34 | *.m4v 35 | *.mhtml 36 | *.mkv 37 | *.mov 38 | *.mp3 39 | *.mp4 40 | *.ogg 41 | *.opus 42 | *.png 43 | *.sbv 44 | *.srt 45 | *.swf 46 | *.swp 47 | *.ttml 48 | *.url 49 | *.vtt 50 | *.wav 51 | *.webloc 52 | *.webm 53 | *.webp 54 | 55 | # Allow config/media files in testdata 56 | !test/** 57 | 58 | # Python 59 | *.pyc 60 | *.pyo 61 | .pytest_cache 62 | wine-py2exe/ 63 | py2exe.log 64 | build/ 65 | dist/ 66 | zip/ 67 | tmp/ 68 | venv/ 69 | completions/ 70 | 71 | # Misc 72 | *~ 73 | *.DS_Store 74 | *.kate-swp 75 | MANIFEST 76 | test/local_parameters.json 77 | .coverage 78 | cover/ 79 | secrets/ 80 | updates_key.pem 81 | *.egg-info 82 | .tox 83 | *.class 84 | 85 | # Generated 86 | AUTHORS 87 | README.txt 88 | .mailmap 89 | *.1 90 | *.bash-completion 91 | *.fish 92 | *.tar.gz 93 | *.zsh 94 | *.spec 95 | test/testdata/player-*.js 96 | 97 | # Binary 98 | /youtube-dl 99 | /youtube-dlc 100 | /yt-dlp 101 | yt-dlp.zip 102 | *.exe 103 | 104 | # Text Editor / IDE 105 | .idea 106 | *.iml 107 | .vscode 108 | *.sublime-* 109 | 110 | # Lazy extractors 111 | */extractor/lazy_extractors.py 112 | 113 | # Plugins 114 | ytdlp_plugins/extractor/* 115 | !ytdlp_plugins/extractor/__init__.py 116 | !ytdlp_plugins/extractor/sample.py 117 | -------------------------------------------------------------------------------- /yt_dlp/extractor/commonmistakes.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import sys 4 | 5 | from .common import InfoExtractor 6 | from ..utils import ExtractorError 7 | 8 | 9 | class CommonMistakesIE(InfoExtractor): 10 | IE_DESC = False # Do not list 11 | _VALID_URL = r'''(?x) 12 | (?:url|URL)$ 13 | ''' 14 | 15 | _TESTS = [{ 16 | 'url': 'url', 17 | 'only_matching': True, 18 | }, { 19 | 'url': 'URL', 20 | 'only_matching': True, 21 | }] 22 | 23 | def _real_extract(self, url): 24 | msg = ( 25 | 'You\'ve asked yt-dlp to download the URL "%s". ' 26 | 'That doesn\'t make any sense. ' 27 | 'Simply remove the parameter in your command or configuration.' 28 | ) % url 29 | if not self.get_param('verbose'): 30 | msg += ' Add -v to the command line to see what arguments and configuration yt-dlp has' 31 | raise ExtractorError(msg, expected=True) 32 | 33 | 34 | class UnicodeBOMIE(InfoExtractor): 35 | IE_DESC = False 36 | _VALID_URL = r'(?P\ufeff)(?P.*)$' 37 | 38 | # Disable test for python 3.2 since BOM is broken in re in this version 39 | # (see https://github.com/ytdl-org/youtube-dl/issues/9751) 40 | _TESTS = [] if (3, 0) < sys.version_info <= (3, 3) else [{ 41 | 'url': '\ufeffhttp://www.youtube.com/watch?v=BaW_jenozKc', 42 | 'only_matching': True, 43 | }] 44 | 45 | def _real_extract(self, url): 46 | real_url = self._match_id(url) 47 | self.report_warning( 48 | 'Your URL starts with a Byte Order Mark (BOM). ' 49 | 'Removing the BOM and looking for "%s" ...' % real_url) 50 | return self.url_result(real_url) 51 | -------------------------------------------------------------------------------- /yt_dlp/extractor/hornbunny.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | from ..utils import ( 6 | int_or_none, 7 | parse_duration, 8 | ) 9 | 10 | 11 | class HornBunnyIE(InfoExtractor): 12 | _VALID_URL = r'http?://(?:www\.)?hornbunny\.com/videos/(?P[a-z-]+)-(?P\d+)\.html' 13 | _TEST = { 14 | 'url': 'http://hornbunny.com/videos/panty-slut-jerk-off-instruction-5227.html', 15 | 'md5': 'e20fd862d1894b67564c96f180f43924', 16 | 'info_dict': { 17 | 'id': '5227', 18 | 'ext': 'mp4', 19 | 'title': 'panty slut jerk off instruction', 20 | 'duration': 550, 21 | 'age_limit': 18, 22 | 'view_count': int, 23 | 'thumbnail': r're:^https?://.*\.jpg$', 24 | } 25 | } 26 | 27 | def _real_extract(self, url): 28 | video_id = self._match_id(url) 29 | 30 | webpage = self._download_webpage(url, video_id) 31 | title = self._og_search_title(webpage) 32 | info_dict = self._parse_html5_media_entries(url, webpage, video_id)[0] 33 | 34 | duration = parse_duration(self._search_regex( 35 | r'Runtime:\s*([0-9:]+)
    ', 36 | webpage, 'duration', fatal=False)) 37 | view_count = int_or_none(self._search_regex( 38 | r'Views:\s*(\d+)', 39 | webpage, 'view count', fatal=False)) 40 | 41 | info_dict.update({ 42 | 'id': video_id, 43 | 'title': title, 44 | 'duration': duration, 45 | 'view_count': view_count, 46 | 'age_limit': 18, 47 | }) 48 | 49 | return info_dict 50 | -------------------------------------------------------------------------------- /yt_dlp/extractor/mychannels.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | 5 | from .common import InfoExtractor 6 | 7 | 8 | class MyChannelsIE(InfoExtractor): 9 | _VALID_URL = r'https?://(?:www\.)?mychannels\.com/.*(?Pvideo|production)_id=(?P[0-9]+)' 10 | _TEST = { 11 | 'url': 'https://mychannels.com/missholland/miss-holland?production_id=3416', 12 | 'md5': 'b8993daad4262dd68d89d651c0c52c45', 13 | 'info_dict': { 14 | 'id': 'wUUDZZep6vQD', 15 | 'ext': 'mp4', 16 | 'title': 'Miss Holland joins VOTE LEAVE', 17 | 'description': 'Miss Holland | #13 Not a potato', 18 | 'uploader': 'Miss Holland', 19 | } 20 | } 21 | 22 | def _real_extract(self, url): 23 | id_type, url_id = self._match_valid_url(url).groups() 24 | webpage = self._download_webpage(url, url_id) 25 | video_data = self._html_search_regex(r']+data-%s-id="%s"[^>]+)>' % (id_type, url_id), webpage, 'video data') 26 | 27 | def extract_data_val(attr, fatal=False): 28 | return self._html_search_regex(r'data-%s\s*=\s*"([^"]+)"' % attr, video_data, attr, fatal=fatal) 29 | minoto_id = extract_data_val('minoto-id') or self._search_regex(r'/id/([a-zA-Z0-9]+)', extract_data_val('video-src', True), 'minoto id') 30 | 31 | return { 32 | '_type': 'url_transparent', 33 | 'url': 'minoto:%s' % minoto_id, 34 | 'id': url_id, 35 | 'title': extract_data_val('title', True), 36 | 'description': extract_data_val('description'), 37 | 'thumbnail': extract_data_val('image'), 38 | 'uploader': extract_data_val('channel'), 39 | } 40 | -------------------------------------------------------------------------------- /yt_dlp/extractor/tvland.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .mtv import MTVServicesInfoExtractor 5 | 6 | # TODO: Remove - Reason not used anymore - Service moved to youtube 7 | 8 | 9 | class TVLandIE(MTVServicesInfoExtractor): 10 | IE_NAME = 'tvland.com' 11 | _VALID_URL = r'https?://(?:www\.)?tvland\.com/(?:video-clips|(?:full-)?episodes)/(?P[^/?#.]+)' 12 | _FEED_URL = 'http://www.tvland.com/feeds/mrss/' 13 | _TESTS = [{ 14 | # Geo-restricted. Without a proxy metadata are still there. With a 15 | # proxy it redirects to http://m.tvland.com/app/ 16 | 'url': 'https://www.tvland.com/episodes/s04pzf/everybody-loves-raymond-the-dog-season-1-ep-19', 17 | 'info_dict': { 18 | 'description': 'md5:84928e7a8ad6649371fbf5da5e1ad75a', 19 | 'title': 'The Dog', 20 | }, 21 | 'playlist_mincount': 5, 22 | 'skip': '404 Not found', 23 | }, { 24 | 'url': 'https://www.tvland.com/video-clips/4n87f2/younger-a-first-look-at-younger-season-6', 25 | 'md5': 'e2c6389401cf485df26c79c247b08713', 26 | 'info_dict': { 27 | 'id': '891f7d3c-5b5b-4753-b879-b7ba1a601757', 28 | 'ext': 'mp4', 29 | 'title': 'Younger|April 30, 2019|6|NO-EPISODE#|A First Look at Younger Season 6', 30 | 'description': 'md5:595ea74578d3a888ae878dfd1c7d4ab2', 31 | 'upload_date': '20190430', 32 | 'timestamp': 1556658000, 33 | }, 34 | 'params': { 35 | 'skip_download': True, 36 | }, 37 | }, { 38 | 'url': 'http://www.tvland.com/full-episodes/iu0hz6/younger-a-kiss-is-just-a-kiss-season-3-ep-301', 39 | 'only_matching': True, 40 | }] 41 | -------------------------------------------------------------------------------- /yt_dlp/extractor/hypem.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from ..utils import int_or_none 5 | 6 | 7 | class HypemIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?hypem\.com/track/(?P[0-9a-z]{5})' 9 | _TEST = { 10 | 'url': 'http://hypem.com/track/1v6ga/BODYWORK+-+TAME', 11 | 'md5': 'b9cc91b5af8995e9f0c1cee04c575828', 12 | 'info_dict': { 13 | 'id': '1v6ga', 14 | 'ext': 'mp3', 15 | 'title': 'Tame', 16 | 'uploader': 'BODYWORK', 17 | 'timestamp': 1371810457, 18 | 'upload_date': '20130621', 19 | } 20 | } 21 | 22 | def _real_extract(self, url): 23 | track_id = self._match_id(url) 24 | 25 | response = self._download_webpage(url, track_id) 26 | 27 | track = self._parse_json(self._html_search_regex( 28 | r'(?s)(.+?)', 29 | response, 'tracks'), track_id)['tracks'][0] 30 | 31 | track_id = track['id'] 32 | title = track['song'] 33 | 34 | final_url = self._download_json( 35 | 'http://hypem.com/serve/source/%s/%s' % (track_id, track['key']), 36 | track_id, 'Downloading metadata', headers={ 37 | 'Content-Type': 'application/json' 38 | })['url'] 39 | 40 | return { 41 | 'id': track_id, 42 | 'url': final_url, 43 | 'ext': 'mp3', 44 | 'title': title, 45 | 'uploader': track.get('artist'), 46 | 'duration': int_or_none(track.get('time')), 47 | 'timestamp': int_or_none(track.get('ts')), 48 | 'track': title, 49 | } 50 | -------------------------------------------------------------------------------- /yt_dlp/extractor/goshgay.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | from ..compat import ( 6 | compat_parse_qs, 7 | ) 8 | from ..utils import ( 9 | parse_duration, 10 | ) 11 | 12 | 13 | class GoshgayIE(InfoExtractor): 14 | _VALID_URL = r'https?://(?:www\.)?goshgay\.com/video(?P\d+?)($|/)' 15 | _TEST = { 16 | 'url': 'http://www.goshgay.com/video299069/diesel_sfw_xxx_video', 17 | 'md5': '4b6db9a0a333142eb9f15913142b0ed1', 18 | 'info_dict': { 19 | 'id': '299069', 20 | 'ext': 'flv', 21 | 'title': 'DIESEL SFW XXX Video', 22 | 'thumbnail': r're:^http://.*\.jpg$', 23 | 'duration': 80, 24 | 'age_limit': 18, 25 | } 26 | } 27 | 28 | def _real_extract(self, url): 29 | video_id = self._match_id(url) 30 | webpage = self._download_webpage(url, video_id) 31 | 32 | title = self._html_search_regex( 33 | r'

    (.*?)<', webpage, 'title') 34 | duration = parse_duration(self._html_search_regex( 35 | r'\s*-?\s*(.*?)', 36 | webpage, 'duration', fatal=False)) 37 | 38 | flashvars = compat_parse_qs(self._html_search_regex( 39 | r' 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /yt_dlp/extractor/historicfilms.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from ..utils import parse_duration 5 | 6 | 7 | class HistoricFilmsIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?historicfilms\.com/(?:tapes/|play)(?P\d+)' 9 | _TEST = { 10 | 'url': 'http://www.historicfilms.com/tapes/4728', 11 | 'md5': 'd4a437aec45d8d796a38a215db064e9a', 12 | 'info_dict': { 13 | 'id': '4728', 14 | 'ext': 'mov', 15 | 'title': 'Historic Films: GP-7', 16 | 'description': 'md5:1a86a0f3ac54024e419aba97210d959a', 17 | 'thumbnail': r're:^https?://.*\.jpg$', 18 | 'duration': 2096, 19 | }, 20 | } 21 | 22 | def _real_extract(self, url): 23 | video_id = self._match_id(url) 24 | 25 | webpage = self._download_webpage(url, video_id) 26 | 27 | tape_id = self._search_regex( 28 | [r'class="tapeId"[^>]*>([^<]+)<', r'tapeId\s*:\s*"([^"]+)"'], 29 | webpage, 'tape id') 30 | 31 | title = self._og_search_title(webpage) 32 | description = self._og_search_description(webpage) 33 | thumbnail = self._html_search_meta( 34 | 'thumbnailUrl', webpage, 'thumbnails') or self._og_search_thumbnail(webpage) 35 | duration = parse_duration(self._html_search_meta( 36 | 'duration', webpage, 'duration')) 37 | 38 | video_url = 'http://www.historicfilms.com/video/%s_%s_web.mov' % (tape_id, video_id) 39 | 40 | return { 41 | 'id': video_id, 42 | 'url': video_url, 43 | 'title': title, 44 | 'description': description, 45 | 'thumbnail': thumbnail, 46 | 'duration': duration, 47 | } 48 | -------------------------------------------------------------------------------- /yt_dlp/extractor/dreisat.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .zdf import ZDFIE 4 | 5 | 6 | class DreiSatIE(ZDFIE): 7 | IE_NAME = '3sat' 8 | _VALID_URL = r'https?://(?:www\.)?3sat\.de/(?:[^/]+/)*(?P[^/?#&]+)\.html' 9 | _TESTS = [{ 10 | # Same as https://www.zdf.de/dokumentation/ab-18/10-wochen-sommer-102.html 11 | 'url': 'https://www.3sat.de/film/ab-18/10-wochen-sommer-108.html', 12 | 'md5': '0aff3e7bc72c8813f5e0fae333316a1d', 13 | 'info_dict': { 14 | 'id': '141007_ab18_10wochensommer_film', 15 | 'ext': 'mp4', 16 | 'title': 'Ab 18! - 10 Wochen Sommer', 17 | 'description': 'md5:8253f41dc99ce2c3ff892dac2d65fe26', 18 | 'duration': 2660, 19 | 'timestamp': 1608604200, 20 | 'upload_date': '20201222', 21 | }, 22 | }, { 23 | 'url': 'https://www.3sat.de/gesellschaft/schweizweit/waidmannsheil-100.html', 24 | 'info_dict': { 25 | 'id': '140913_sendung_schweizweit', 26 | 'ext': 'mp4', 27 | 'title': 'Waidmannsheil', 28 | 'description': 'md5:cce00ca1d70e21425e72c86a98a56817', 29 | 'timestamp': 1410623100, 30 | 'upload_date': '20140913' 31 | }, 32 | 'params': { 33 | 'skip_download': True, 34 | } 35 | }, { 36 | # Same as https://www.zdf.de/filme/filme-sonstige/der-hauptmann-112.html 37 | 'url': 'https://www.3sat.de/film/spielfilm/der-hauptmann-100.html', 38 | 'only_matching': True, 39 | }, { 40 | # Same as https://www.zdf.de/wissen/nano/nano-21-mai-2019-102.html, equal media ids 41 | 'url': 'https://www.3sat.de/wissen/nano/nano-21-mai-2019-102.html', 42 | 'only_matching': True, 43 | }] 44 | -------------------------------------------------------------------------------- /yt_dlp/extractor/tvnoe.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | from ..utils import ( 6 | clean_html, 7 | get_element_by_class, 8 | js_to_json, 9 | ) 10 | 11 | 12 | class TVNoeIE(InfoExtractor): 13 | _VALID_URL = r'https?://(?:www\.)?tvnoe\.cz/video/(?P[0-9]+)' 14 | _TEST = { 15 | 'url': 'http://www.tvnoe.cz/video/10362', 16 | 'md5': 'aee983f279aab96ec45ab6e2abb3c2ca', 17 | 'info_dict': { 18 | 'id': '10362', 19 | 'ext': 'mp4', 20 | 'series': 'Noční univerzita', 21 | 'title': 'prof. Tomáš Halík, Th.D. - Návrat náboženství a střet civilizací', 22 | 'description': 'md5:f337bae384e1a531a52c55ebc50fff41', 23 | } 24 | } 25 | 26 | def _real_extract(self, url): 27 | video_id = self._match_id(url) 28 | webpage = self._download_webpage(url, video_id) 29 | 30 | iframe_url = self._search_regex( 31 | r']+src="([^"]+)"', webpage, 'iframe URL') 32 | 33 | ifs_page = self._download_webpage(iframe_url, video_id) 34 | jwplayer_data = self._find_jwplayer_data( 35 | ifs_page, video_id, transform_source=js_to_json) 36 | info_dict = self._parse_jwplayer_data( 37 | jwplayer_data, video_id, require_title=False, base_url=iframe_url) 38 | 39 | info_dict.update({ 40 | 'id': video_id, 41 | 'title': clean_html(get_element_by_class( 42 | 'field-name-field-podnazev', webpage)), 43 | 'description': clean_html(get_element_by_class( 44 | 'field-name-body', webpage)), 45 | 'series': clean_html(get_element_by_class('title', webpage)) 46 | }) 47 | 48 | return info_dict 49 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Please follow the guide below 2 | 3 | - You will be asked some questions, please read them **carefully** and answer honestly 4 | - Put an `x` into all the boxes [ ] relevant to your *pull request* (like that [x]) 5 | - Use *Preview* tab to see how your *pull request* will actually look like 6 | 7 | --- 8 | 9 | ### Before submitting a *pull request* make sure you have: 10 | - [ ] At least skimmed through [contributing guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) including [yt-dlp coding conventions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#yt-dlp-coding-conventions) 11 | - [ ] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests 12 | - [ ] Checked the code with [flake8](https://pypi.python.org/pypi/flake8) 13 | 14 | ### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check one of the following options: 15 | - [ ] I am the original author of this code and I am willing to release it under [Unlicense](http://unlicense.org/) 16 | - [ ] I am not the original author of this code but it is in public domain or released under [Unlicense](http://unlicense.org/) (provide reliable evidence) 17 | 18 | ### What is the purpose of your *pull request*? 19 | - [ ] Bug fix 20 | - [ ] Improvement 21 | - [ ] New extractor 22 | - [ ] New feature 23 | 24 | --- 25 | 26 | ### Description of your *pull request* and other information 27 | 28 | Explanation of your *pull request* in arbitrary form goes here. Please make sure the description explains the purpose and effect of your *pull request* and is worded well enough to be understood. Provide as much context and examples as possible. 29 | -------------------------------------------------------------------------------- /yt_dlp/extractor/aliexpress.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | from ..compat import compat_str 6 | from ..utils import ( 7 | float_or_none, 8 | try_get, 9 | ) 10 | 11 | 12 | class AliExpressLiveIE(InfoExtractor): 13 | _VALID_URL = r'https?://live\.aliexpress\.com/live/(?P\d+)' 14 | _TEST = { 15 | 'url': 'https://live.aliexpress.com/live/2800002704436634', 16 | 'md5': 'e729e25d47c5e557f2630eaf99b740a5', 17 | 'info_dict': { 18 | 'id': '2800002704436634', 19 | 'ext': 'mp4', 20 | 'title': 'CASIMA7.22', 21 | 'thumbnail': r're:http://.*\.jpg', 22 | 'uploader': 'CASIMA Official Store', 23 | 'timestamp': 1500717600, 24 | 'upload_date': '20170722', 25 | }, 26 | } 27 | 28 | def _real_extract(self, url): 29 | video_id = self._match_id(url) 30 | 31 | webpage = self._download_webpage(url, video_id) 32 | 33 | data = self._parse_json( 34 | self._search_regex( 35 | r'(?s)runParams\s*=\s*({.+?})\s*;?\s*var', 36 | webpage, 'runParams'), 37 | video_id) 38 | 39 | title = data['title'] 40 | 41 | formats = self._extract_m3u8_formats( 42 | data['replyStreamUrl'], video_id, 'mp4', 43 | entry_protocol='m3u8_native', m3u8_id='hls') 44 | 45 | return { 46 | 'id': video_id, 47 | 'title': title, 48 | 'thumbnail': data.get('coverUrl'), 49 | 'uploader': try_get( 50 | data, lambda x: x['followBar']['name'], compat_str), 51 | 'timestamp': float_or_none(data.get('startTimeLong'), scale=1000), 52 | 'formats': formats, 53 | } 54 | -------------------------------------------------------------------------------- /yt_dlp/extractor/sztvhu.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class SztvHuIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:(?:www\.)?sztv\.hu|www\.tvszombathely\.hu)/(?:[^/]+)/.+-(?P[0-9]+)' 9 | _TEST = { 10 | 'url': 'http://sztv.hu/hirek/cserkeszek-nepszerusitettek-a-kornyezettudatos-eletmodot-a-savaria-teren-20130909', 11 | 'md5': 'a6df607b11fb07d0e9f2ad94613375cb', 12 | 'info_dict': { 13 | 'id': '20130909', 14 | 'ext': 'mp4', 15 | 'title': 'Cserkészek népszerűsítették a környezettudatos életmódot a Savaria téren', 16 | 'description': 'A zöld nap játékos ismeretterjesztő programjait a Magyar Cserkész Szövetség szervezte, akik az ország nyolc városában adják át tudásukat az érdeklődőknek. A PET...', 17 | }, 18 | } 19 | 20 | def _real_extract(self, url): 21 | video_id = self._match_id(url) 22 | webpage = self._download_webpage(url, video_id) 23 | video_file = self._search_regex( 24 | r'file: "...:(.*?)",', webpage, 'video file') 25 | title = self._html_search_regex( 26 | r'', 30 | webpage, 'video description', fatal=False) 31 | thumbnail = self._og_search_thumbnail(webpage) 32 | 33 | video_url = 'http://media.sztv.hu/vod/' + video_file 34 | 35 | return { 36 | 'id': video_id, 37 | 'url': video_url, 38 | 'title': title, 39 | 'description': description, 40 | 'thumbnail': thumbnail, 41 | } 42 | -------------------------------------------------------------------------------- /test/test_cache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | from __future__ import unicode_literals 5 | 6 | import shutil 7 | 8 | # Allow direct execution 9 | import os 10 | import sys 11 | import unittest 12 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 13 | 14 | 15 | from test.helper import FakeYDL 16 | from yt_dlp.cache import Cache 17 | 18 | 19 | def _is_empty(d): 20 | return not bool(os.listdir(d)) 21 | 22 | 23 | def _mkdir(d): 24 | if not os.path.exists(d): 25 | os.mkdir(d) 26 | 27 | 28 | class TestCache(unittest.TestCase): 29 | def setUp(self): 30 | TEST_DIR = os.path.dirname(os.path.abspath(__file__)) 31 | TESTDATA_DIR = os.path.join(TEST_DIR, 'testdata') 32 | _mkdir(TESTDATA_DIR) 33 | self.test_dir = os.path.join(TESTDATA_DIR, 'cache_test') 34 | self.tearDown() 35 | 36 | def tearDown(self): 37 | if os.path.exists(self.test_dir): 38 | shutil.rmtree(self.test_dir) 39 | 40 | def test_cache(self): 41 | ydl = FakeYDL({ 42 | 'cachedir': self.test_dir, 43 | }) 44 | c = Cache(ydl) 45 | obj = {'x': 1, 'y': ['ä', '\\a', True]} 46 | self.assertEqual(c.load('test_cache', 'k.'), None) 47 | c.store('test_cache', 'k.', obj) 48 | self.assertEqual(c.load('test_cache', 'k2'), None) 49 | self.assertFalse(_is_empty(self.test_dir)) 50 | self.assertEqual(c.load('test_cache', 'k.'), obj) 51 | self.assertEqual(c.load('test_cache', 'y'), None) 52 | self.assertEqual(c.load('test_cache2', 'k.'), None) 53 | c.remove() 54 | self.assertFalse(os.path.exists(self.test_dir)) 55 | self.assertEqual(c.load('test_cache', 'k.'), None) 56 | 57 | 58 | if __name__ == '__main__': 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /yt_dlp/extractor/behindkink.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | 5 | from .common import InfoExtractor 6 | from ..utils import url_basename 7 | 8 | 9 | class BehindKinkIE(InfoExtractor): 10 | _VALID_URL = r'https?://(?:www\.)?behindkink\.com/(?P[0-9]{4})/(?P[0-9]{2})/(?P[0-9]{2})/(?P[^/#?_]+)' 11 | _TEST = { 12 | 'url': 'http://www.behindkink.com/2014/12/05/what-are-you-passionate-about-marley-blaze/', 13 | 'md5': '507b57d8fdcd75a41a9a7bdb7989c762', 14 | 'info_dict': { 15 | 'id': '37127', 16 | 'ext': 'mp4', 17 | 'title': 'What are you passionate about – Marley Blaze', 18 | 'description': 'md5:aee8e9611b4ff70186f752975d9b94b4', 19 | 'upload_date': '20141205', 20 | 'thumbnail': 'http://www.behindkink.com/wp-content/uploads/2014/12/blaze-1.jpg', 21 | 'age_limit': 18, 22 | } 23 | } 24 | 25 | def _real_extract(self, url): 26 | mobj = self._match_valid_url(url) 27 | display_id = mobj.group('id') 28 | 29 | webpage = self._download_webpage(url, display_id) 30 | 31 | video_url = self._search_regex( 32 | r' 2 | 3 | 4 | 5 | 6 | DASH_360 7 | 8 | 9 | 10 | 11 | 12 | DASH_240 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | audio 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /yt_dlp/extractor/lenta.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | from .common import InfoExtractor 5 | 6 | 7 | class LentaIE(InfoExtractor): 8 | _VALID_URL = r'https?://(?:www\.)?lenta\.ru/[^/]+/\d+/\d+/\d+/(?P[^/?#&]+)' 9 | _TESTS = [{ 10 | 'url': 'https://lenta.ru/news/2018/03/22/savshenko_go/', 11 | 'info_dict': { 12 | 'id': '964400', 13 | 'ext': 'mp4', 14 | 'title': 'Надежду Савченко задержали', 15 | 'thumbnail': r're:^https?://.*\.jpg$', 16 | 'duration': 61, 17 | 'view_count': int, 18 | }, 19 | 'params': { 20 | 'skip_download': True, 21 | }, 22 | }, { 23 | # EaglePlatform iframe embed 24 | 'url': 'http://lenta.ru/news/2015/03/06/navalny/', 25 | 'info_dict': { 26 | 'id': '227304', 27 | 'ext': 'mp4', 28 | 'title': 'Навальный вышел на свободу', 29 | 'description': 'md5:d97861ac9ae77377f3f20eaf9d04b4f5', 30 | 'thumbnail': r're:^https?://.*\.jpg$', 31 | 'duration': 87, 32 | 'view_count': int, 33 | 'age_limit': 0, 34 | }, 35 | 'params': { 36 | 'skip_download': True, 37 | }, 38 | }] 39 | 40 | def _real_extract(self, url): 41 | display_id = self._match_id(url) 42 | 43 | webpage = self._download_webpage(url, display_id) 44 | 45 | video_id = self._search_regex( 46 | r'vid\s*:\s*["\']?(\d+)', webpage, 'eagleplatform id', 47 | default=None) 48 | if video_id: 49 | return self.url_result( 50 | 'eagleplatform:lentaru.media.eagleplatform.com:%s' % video_id, 51 | ie='EaglePlatform', video_id=video_id) 52 | 53 | return self.url_result(url, ie='Generic') 54 | -------------------------------------------------------------------------------- /yt_dlp/extractor/reverbnation.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .common import InfoExtractor 4 | from ..utils import ( 5 | qualities, 6 | str_or_none, 7 | ) 8 | 9 | 10 | class ReverbNationIE(InfoExtractor): 11 | _VALID_URL = r'^https?://(?:www\.)?reverbnation\.com/.*?/song/(?P\d+).*?$' 12 | _TESTS = [{ 13 | 'url': 'http://www.reverbnation.com/alkilados/song/16965047-mona-lisa', 14 | 'md5': 'c0aaf339bcee189495fdf5a8c8ba8645', 15 | 'info_dict': { 16 | 'id': '16965047', 17 | 'ext': 'mp3', 18 | 'title': 'MONA LISA', 19 | 'uploader': 'ALKILADOS', 20 | 'uploader_id': '216429', 21 | 'thumbnail': r're:^https?://.*\.jpg', 22 | }, 23 | }] 24 | 25 | def _real_extract(self, url): 26 | song_id = self._match_id(url) 27 | 28 | api_res = self._download_json( 29 | 'https://api.reverbnation.com/song/%s' % song_id, 30 | song_id, 31 | note='Downloading information of song %s' % song_id 32 | ) 33 | 34 | THUMBNAILS = ('thumbnail', 'image') 35 | quality = qualities(THUMBNAILS) 36 | thumbnails = [] 37 | for thumb_key in THUMBNAILS: 38 | if api_res.get(thumb_key): 39 | thumbnails.append({ 40 | 'url': api_res[thumb_key], 41 | 'preference': quality(thumb_key) 42 | }) 43 | 44 | return { 45 | 'id': song_id, 46 | 'title': api_res['name'], 47 | 'url': api_res['url'], 48 | 'uploader': api_res.get('artist', {}).get('name'), 49 | 'uploader_id': str_or_none(api_res.get('artist', {}).get('id')), 50 | 'thumbnails': thumbnails, 51 | 'ext': 'mp3', 52 | 'vcodec': 'none', 53 | } 54 | --------------------------------------------------------------------------------