Compare commits
105 Commits
2014.07.11
...
2014.07.20
Author | SHA1 | Date | |
---|---|---|---|
|
cceb5ec237 | ||
|
71a6eaff83 | ||
|
7fd48d0413 | ||
|
1b38b5be86 | ||
|
decf2ae400 | ||
|
0d989011ff | ||
|
01b4b74574 | ||
|
70f767dc65 | ||
|
e75c24e889 | ||
|
0cb2056304 | ||
|
604f292ab7 | ||
|
23d3c422ab | ||
|
0c1ffe980d | ||
|
5e95cb27d6 | ||
|
3b86f936c5 | ||
|
e0942e37aa | ||
|
c45a6caa95 | ||
|
61bbddbaa6 | ||
|
5425626790 | ||
|
5dc3552d85 | ||
|
3fbd27f73e | ||
|
0382ecb78d | ||
|
72edb6fc8c | ||
|
66149e3f2b | ||
|
6e74521d98 | ||
|
cf01013161 | ||
|
1e179c7528 | ||
|
530ed178b7 | ||
|
74aa18f68f | ||
|
d9222264a8 | ||
|
ca14211e93 | ||
|
b1d65c3369 | ||
|
b4c538b02b | ||
|
13059bceb2 | ||
|
d8894e24a4 | ||
|
3b09757bac | ||
|
2f97f76877 | ||
|
43f0537c06 | ||
|
a816da0dc3 | ||
|
7bb49d1057 | ||
|
1aa42fedee | ||
|
ee90ddab94 | ||
|
172240c0a4 | ||
|
ad25aee245 | ||
|
bd1f325b42 | ||
|
00a82ea805 | ||
|
b1b01841af | ||
|
816930c485 | ||
|
76233cda34 | ||
|
9dcea39985 | ||
|
10d00a756a | ||
|
eb50741129 | ||
|
3804b01276 | ||
|
b1298d8e06 | ||
|
6a46dc8db7 | ||
|
36cb99f958 | ||
|
81650f95e2 | ||
|
34dbcb8505 | ||
|
c993c829e2 | ||
|
0d90e0f067 | ||
|
678f58de4b | ||
|
c961a0e63e | ||
|
aaefb347c0 | ||
|
09018e19a5 | ||
|
345e37831c | ||
|
00ac799b68 | ||
|
133af9385b | ||
|
40c696e5c6 | ||
|
d6d5028922 | ||
|
38ad119f97 | ||
|
4e415288d7 | ||
|
fada438acf | ||
|
1df0ae2170 | ||
|
d96b9d40f0 | ||
|
fa19dfccf9 | ||
|
cdc22cb886 | ||
|
04c77a54b0 | ||
|
64a8c39a1f | ||
|
3d55f2806e | ||
|
1eb867f33f | ||
|
e93f4f7578 | ||
|
45ead916d1 | ||
|
3a0879c8c8 | ||
|
ebf361ce18 | ||
|
953b358668 | ||
|
3dfd25b3aa | ||
|
6f66eedc5d | ||
|
4094b6e36d | ||
|
c09cbf0ed9 | ||
|
537ba6f381 | ||
|
d6aa1967ad | ||
|
3941669d69 | ||
|
e66ab17a36 | ||
|
cb437dc2ad | ||
|
0d933b2ad5 | ||
|
e5c3a4b549 | ||
|
1d0668ed5a | ||
|
d415299a80 | ||
|
48fbb1003d | ||
|
305d068362 | ||
|
a231ce87b5 | ||
|
a84d20fc14 | ||
|
9e30092361 | ||
|
10d5c7aa5f | ||
|
412f356e04 |
@@ -12,7 +12,7 @@ To install it right away for all UNIX users (Linux, OS X, etc.), type:
|
||||
|
||||
If you do not have curl, you can alternatively use a recent wget:
|
||||
|
||||
sudo wget https://yt-dl.org/downloads/2014.05.13/youtube-dl -O /usr/local/bin/youtube-dl
|
||||
sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
|
||||
sudo chmod a+x /usr/local/bin/youtube-dl
|
||||
|
||||
Windows users can [download a .exe file](https://yt-dl.org/latest/youtube-dl.exe) and place it in their home directory or any other location on their [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29).
|
||||
@@ -255,7 +255,7 @@ which means you can modify it, redistribute it or use it however you like.
|
||||
128K (default 5)
|
||||
--recode-video FORMAT Encode the video to another format if
|
||||
necessary (currently supported:
|
||||
mp4|flv|ogg|webm)
|
||||
mp4|flv|ogg|webm|mkv)
|
||||
-k, --keep-video keeps the video file on disk after the
|
||||
post-processing; the video is erased by
|
||||
default
|
||||
|
1
test/swftests/.gitignore
vendored
Normal file
1
test/swftests/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.swf
|
19
test/swftests/ArrayAccess.as
Normal file
19
test/swftests/ArrayAccess.as
Normal file
@@ -0,0 +1,19 @@
|
||||
// input: [["a", "b", "c", "d"]]
|
||||
// output: ["c", "b", "a", "d"]
|
||||
|
||||
package {
|
||||
public class ArrayAccess {
|
||||
public static function main(ar:Array):Array {
|
||||
var aa:ArrayAccess = new ArrayAccess();
|
||||
return aa.f(ar, 2);
|
||||
}
|
||||
|
||||
private function f(ar:Array, num:Number):Array{
|
||||
var x:String = ar[0];
|
||||
var y:String = ar[num % ar.length];
|
||||
ar[0] = y;
|
||||
ar[num] = x;
|
||||
return ar;
|
||||
}
|
||||
}
|
||||
}
|
17
test/swftests/ClassCall.as
Normal file
17
test/swftests/ClassCall.as
Normal file
@@ -0,0 +1,17 @@
|
||||
// input: []
|
||||
// output: 121
|
||||
|
||||
package {
|
||||
public class ClassCall {
|
||||
public static function main():int{
|
||||
var f:OtherClass = new OtherClass();
|
||||
return f.func(100,20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OtherClass {
|
||||
public function func(x: int, y: int):int {
|
||||
return x+y+1;
|
||||
}
|
||||
}
|
15
test/swftests/ClassConstruction.as
Normal file
15
test/swftests/ClassConstruction.as
Normal file
@@ -0,0 +1,15 @@
|
||||
// input: []
|
||||
// output: 0
|
||||
|
||||
package {
|
||||
public class ClassConstruction {
|
||||
public static function main():int{
|
||||
var f:Foo = new Foo();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Foo {
|
||||
|
||||
}
|
13
test/swftests/LocalVars.as
Normal file
13
test/swftests/LocalVars.as
Normal file
@@ -0,0 +1,13 @@
|
||||
// input: [1, 2]
|
||||
// output: 3
|
||||
|
||||
package {
|
||||
public class LocalVars {
|
||||
public static function main(a:int, b:int):int{
|
||||
var c:int = a + b + b;
|
||||
var d:int = c - b;
|
||||
var e:int = d;
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
21
test/swftests/PrivateCall.as
Normal file
21
test/swftests/PrivateCall.as
Normal file
@@ -0,0 +1,21 @@
|
||||
// input: []
|
||||
// output: 9
|
||||
|
||||
package {
|
||||
public class PrivateCall {
|
||||
public static function main():int{
|
||||
var f:OtherClass = new OtherClass();
|
||||
return f.func();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OtherClass {
|
||||
private function pf():int {
|
||||
return 9;
|
||||
}
|
||||
|
||||
public function func():int {
|
||||
return this.pf();
|
||||
}
|
||||
}
|
13
test/swftests/StaticAssignment.as
Normal file
13
test/swftests/StaticAssignment.as
Normal file
@@ -0,0 +1,13 @@
|
||||
// input: [1]
|
||||
// output: 1
|
||||
|
||||
package {
|
||||
public class StaticAssignment {
|
||||
public static var v:int;
|
||||
|
||||
public static function main(a:int):int{
|
||||
v = a;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
16
test/swftests/StaticRetrieval.as
Normal file
16
test/swftests/StaticRetrieval.as
Normal file
@@ -0,0 +1,16 @@
|
||||
// input: []
|
||||
// output: 1
|
||||
|
||||
package {
|
||||
public class StaticRetrieval {
|
||||
public static var v:int;
|
||||
|
||||
public static function main():int{
|
||||
if (v) {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -111,7 +111,7 @@ class TestPlaylists(unittest.TestCase):
|
||||
ie = VineUserIE(dl)
|
||||
result = ie.extract('https://vine.co/Visa')
|
||||
self.assertIsPlaylist(result)
|
||||
self.assertTrue(len(result['entries']) >= 50)
|
||||
self.assertTrue(len(result['entries']) >= 47)
|
||||
|
||||
def test_ustream_channel(self):
|
||||
dl = FakeYDL()
|
||||
@@ -137,6 +137,14 @@ class TestPlaylists(unittest.TestCase):
|
||||
self.assertEqual(result['id'], '9615865')
|
||||
self.assertTrue(len(result['entries']) >= 12)
|
||||
|
||||
def test_soundcloud_likes(self):
|
||||
dl = FakeYDL()
|
||||
ie = SoundcloudUserIE(dl)
|
||||
result = ie.extract('https://soundcloud.com/the-concept-band/likes')
|
||||
self.assertIsPlaylist(result)
|
||||
self.assertEqual(result['id'], '9615865')
|
||||
self.assertTrue(len(result['entries']) >= 1)
|
||||
|
||||
def test_soundcloud_playlist(self):
|
||||
dl = FakeYDL()
|
||||
ie = SoundcloudPlaylistIE(dl)
|
||||
|
@@ -87,7 +87,7 @@ class TestYoutubeSubtitles(BaseTestSubtitles):
|
||||
|
||||
def test_youtube_nosubtitles(self):
|
||||
self.DL.expect_warning(u'video doesn\'t have subtitles')
|
||||
self.url = 'sAjKT8FhjI8'
|
||||
self.url = 'n5BB19UTcdA'
|
||||
self.DL.params['writesubtitles'] = True
|
||||
self.DL.params['allsubtitles'] = True
|
||||
subtitles = self.getSubtitles()
|
||||
|
76
test/test_swfinterp.py
Normal file
76
test/test_swfinterp.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Allow direct execution
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from youtube_dl.swfinterp import SWFInterpreter
|
||||
|
||||
|
||||
TEST_DIR = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), 'swftests')
|
||||
|
||||
|
||||
class TestSWFInterpreter(unittest.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
def _make_testfunc(testfile):
|
||||
m = re.match(r'^(.*)\.(as)$', testfile)
|
||||
if not m:
|
||||
return
|
||||
test_id = m.group(1)
|
||||
|
||||
def test_func(self):
|
||||
as_file = os.path.join(TEST_DIR, testfile)
|
||||
swf_file = os.path.join(TEST_DIR, test_id + '.swf')
|
||||
if ((not os.path.exists(swf_file))
|
||||
or os.path.getmtime(swf_file) < os.path.getmtime(as_file)):
|
||||
# Recompile
|
||||
try:
|
||||
subprocess.check_call(['mxmlc', '-output', swf_file, as_file])
|
||||
except OSError as ose:
|
||||
if ose.errno == errno.ENOENT:
|
||||
print('mxmlc not found! Skipping test.')
|
||||
return
|
||||
raise
|
||||
|
||||
with open(swf_file, 'rb') as swf_f:
|
||||
swf_content = swf_f.read()
|
||||
swfi = SWFInterpreter(swf_content)
|
||||
|
||||
with io.open(as_file, 'r', encoding='utf-8') as as_f:
|
||||
as_content = as_f.read()
|
||||
|
||||
def _find_spec(key):
|
||||
m = re.search(
|
||||
r'(?m)^//\s*%s:\s*(.*?)\n' % re.escape(key), as_content)
|
||||
if not m:
|
||||
raise ValueError('Cannot find %s in %s' % (key, testfile))
|
||||
return json.loads(m.group(1))
|
||||
|
||||
input_args = _find_spec('input')
|
||||
output = _find_spec('output')
|
||||
|
||||
swf_class = swfi.extract_class(test_id)
|
||||
func = swfi.extract_function(swf_class, 'main')
|
||||
res = func(input_args)
|
||||
self.assertEqual(res, output)
|
||||
|
||||
test_func.__name__ = str('test_swf_' + test_id)
|
||||
setattr(TestSWFInterpreter, test_func.__name__, test_func)
|
||||
|
||||
|
||||
for testfile in os.listdir(TEST_DIR):
|
||||
_make_testfunc(testfile)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@@ -33,12 +33,24 @@ _TESTS = [
|
||||
90,
|
||||
u']\\[@?>=<;:/.-,+*)(\'&%$#"hZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjiagfedcb39876',
|
||||
),
|
||||
(
|
||||
u'https://s.ytimg.com/yts/jsbin/html5player-en_US-vfl0Cbn9e.js',
|
||||
u'js',
|
||||
84,
|
||||
u'O1I3456789abcde0ghijklmnopqrstuvwxyzABCDEFGHfJKLMN2PQRSTUVW@YZ!"#$%&\'()*+,-./:;<=',
|
||||
),
|
||||
(
|
||||
u'https://s.ytimg.com/yts/jsbin/html5player-en_US-vflXGBaUN.js',
|
||||
u'js',
|
||||
u'2ACFC7A61CA478CD21425E5A57EBD73DDC78E22A.2094302436B2D377D14A3BBA23022D023B8BC25AA',
|
||||
u'A52CB8B320D22032ABB3A41D773D2B6342034902.A22E87CDD37DBE75A5E52412DC874AC16A7CFCA2',
|
||||
),
|
||||
(
|
||||
u'http://s.ytimg.com/yts/swfbin/player-vfl5vIhK2/watch_as3.swf',
|
||||
u'swf',
|
||||
86,
|
||||
u'O1I3456789abcde0ghijklmnopqrstuvwxyzABCDEFGHfJKLMN2PQRSTUVWXY\\!"#$%&\'()*+,-./:;<=>?'
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -51,12 +63,12 @@ class TestSignature(unittest.TestCase):
|
||||
|
||||
|
||||
def make_tfunc(url, stype, sig_input, expected_sig):
|
||||
basename = url.rpartition('/')[2]
|
||||
m = re.match(r'.*-([a-zA-Z0-9_-]+)\.[a-z]+$', basename)
|
||||
assert m, '%r should follow URL format' % basename
|
||||
m = re.match(r'.*-([a-zA-Z0-9_-]+)(?:/watch_as3)?\.[a-z]+$', url)
|
||||
assert m, '%r should follow URL format' % url
|
||||
test_id = m.group(1)
|
||||
|
||||
def test_func(self):
|
||||
basename = 'player-%s.%s' % (test_id, stype)
|
||||
fn = os.path.join(self.TESTDATA_DIR, basename)
|
||||
|
||||
if not os.path.exists(fn):
|
||||
|
@@ -60,6 +60,12 @@ __authors__ = (
|
||||
'Georg Jähnig',
|
||||
'Ralf Haring',
|
||||
'Koki Takahashi',
|
||||
'Ariset Llerena',
|
||||
'Adam Malcontenti-Wilson',
|
||||
'Tobias Bell',
|
||||
'Naglis Jonaitis',
|
||||
'Charles Chen',
|
||||
'Hassaan Ali',
|
||||
)
|
||||
|
||||
__license__ = 'Public Domain'
|
||||
@@ -506,7 +512,7 @@ def parseOpts(overrideArguments=None):
|
||||
postproc.add_option('--audio-quality', metavar='QUALITY', dest='audioquality', default='5',
|
||||
help='ffmpeg/avconv audio quality specification, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default 5)')
|
||||
postproc.add_option('--recode-video', metavar='FORMAT', dest='recodevideo', default=None,
|
||||
help='Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm)')
|
||||
help='Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm|mkv)')
|
||||
postproc.add_option('-k', '--keep-video', action='store_true', dest='keepvideo', default=False,
|
||||
help='keeps the video file on disk after the post-processing; the video is erased by default')
|
||||
postproc.add_option('--no-post-overwrites', action='store_true', dest='nopostoverwrites', default=False,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from .academicearth import AcademicEarthCourseIE
|
||||
from .addanime import AddAnimeIE
|
||||
from .adultswim import AdultSwimIE
|
||||
from .aftonbladet import AftonbladetIE
|
||||
from .anitube import AnitubeIE
|
||||
from .aol import AolIE
|
||||
@@ -52,6 +53,7 @@ from .cnn import (
|
||||
from .collegehumor import CollegeHumorIE
|
||||
from .comedycentral import ComedyCentralIE, ComedyCentralShowsIE
|
||||
from .condenast import CondeNastIE
|
||||
from .cracked import CrackedIE
|
||||
from .criterion import CriterionIE
|
||||
from .crunchyroll import CrunchyrollIE
|
||||
from .cspan import CSpanIE
|
||||
@@ -62,6 +64,7 @@ from .dailymotion import (
|
||||
DailymotionUserIE,
|
||||
)
|
||||
from .daum import DaumIE
|
||||
from .dfb import DFBIE
|
||||
from .dotsub import DotsubIE
|
||||
from .dreisat import DreiSatIE
|
||||
from .drtv import DRTVIE
|
||||
@@ -83,6 +86,7 @@ from .extremetube import ExtremeTubeIE
|
||||
from .facebook import FacebookIE
|
||||
from .faz import FazIE
|
||||
from .fc2 import FC2IE
|
||||
from .firedrive import FiredriveIE
|
||||
from .firstpost import FirstpostIE
|
||||
from .firsttv import FirstTVIE
|
||||
from .fivemin import FiveMinIE
|
||||
@@ -105,6 +109,7 @@ from .freesound import FreesoundIE
|
||||
from .freespeech import FreespeechIE
|
||||
from .funnyordie import FunnyOrDieIE
|
||||
from .gamekings import GamekingsIE
|
||||
from .gameone import GameOneIE
|
||||
from .gamespot import GameSpotIE
|
||||
from .gametrailers import GametrailersIE
|
||||
from .gdcvault import GDCVaultIE
|
||||
@@ -112,6 +117,7 @@ from .generic import GenericIE
|
||||
from .googleplus import GooglePlusIE
|
||||
from .googlesearch import GoogleSearchIE
|
||||
from .gorillavid import GorillaVidIE
|
||||
from .goshgay import GoshgayIE
|
||||
from .hark import HarkIE
|
||||
from .helsinki import HelsinkiIE
|
||||
from .hentaistigma import HentaiStigmaIE
|
||||
@@ -167,6 +173,7 @@ from .metacafe import MetacafeIE
|
||||
from .metacritic import MetacriticIE
|
||||
from .mit import TechTVMITIE, MITIE, OCWMITIE
|
||||
from .mixcloud import MixcloudIE
|
||||
from .mlb import MLBIE
|
||||
from .mpora import MporaIE
|
||||
from .mofosex import MofosexIE
|
||||
from .mooshare import MooshareIE
|
||||
@@ -229,6 +236,7 @@ from .radiofrance import RadioFranceIE
|
||||
from .rai import RaiIE
|
||||
from .rbmaradio import RBMARadioIE
|
||||
from .redtube import RedTubeIE
|
||||
from .reverbnation import ReverbNationIE
|
||||
from .ringtv import RingTVIE
|
||||
from .ro220 import Ro220IE
|
||||
from .rottentomatoes import RottenTomatoesIE
|
||||
@@ -237,6 +245,7 @@ from .rtbf import RTBFIE
|
||||
from .rtlnow import RTLnowIE
|
||||
from .rts import RTSIE
|
||||
from .rtve import RTVEALaCartaIE
|
||||
from .ruhd import RUHDIE
|
||||
from .rutube import (
|
||||
RutubeIE,
|
||||
RutubeChannelIE,
|
||||
@@ -244,8 +253,10 @@ from .rutube import (
|
||||
RutubePersonIE,
|
||||
)
|
||||
from .rutv import RUTVIE
|
||||
from .sapo import SapoIE
|
||||
from .savefrom import SaveFromIE
|
||||
from .scivee import SciVeeIE
|
||||
from .screencast import ScreencastIE
|
||||
from .servingsys import ServingSysIE
|
||||
from .sina import SinaIE
|
||||
from .slideshare import SlideshareIE
|
||||
@@ -264,8 +275,8 @@ from .soundcloud import (
|
||||
SoundcloudPlaylistIE
|
||||
)
|
||||
from .soundgasm import SoundgasmIE
|
||||
from .southparkstudios import (
|
||||
SouthParkStudiosIE,
|
||||
from .southpark import (
|
||||
SouthParkIE,
|
||||
SouthparkDeIE,
|
||||
)
|
||||
from .space import SpaceIE
|
||||
@@ -289,6 +300,7 @@ from .teachingchannel import TeachingChannelIE
|
||||
from .teamcoco import TeamcocoIE
|
||||
from .techtalks import TechTalksIE
|
||||
from .ted import TEDIE
|
||||
from .tenplay import TenPlayIE
|
||||
from .testurl import TestURLIE
|
||||
from .tf1 import TF1IE
|
||||
from .theplatform import ThePlatformIE
|
||||
@@ -336,12 +348,14 @@ from .vimeo import (
|
||||
VimeoReviewIE,
|
||||
VimeoWatchLaterIE,
|
||||
)
|
||||
from .vimple import VimpleIE
|
||||
from .vine import (
|
||||
VineIE,
|
||||
VineUserIE,
|
||||
)
|
||||
from .viki import VikiIE
|
||||
from .vk import VKIE
|
||||
from .vodlocker import VodlockerIE
|
||||
from .vube import VubeIE
|
||||
from .vuclip import VuClipIE
|
||||
from .vulture import VultureIE
|
||||
@@ -387,6 +401,7 @@ from .youtube import (
|
||||
YoutubeUserIE,
|
||||
YoutubeWatchLaterIE,
|
||||
)
|
||||
|
||||
from .zdf import ZDFIE
|
||||
|
||||
|
||||
|
139
youtube_dl/extractor/adultswim.py
Normal file
139
youtube_dl/extractor/adultswim.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
class AdultSwimIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://video\.adultswim\.com/(?P<path>.+?)(?:\.html)?(?:\?.*)?(?:#.*)?$'
|
||||
_TEST = {
|
||||
'url': 'http://video.adultswim.com/rick-and-morty/close-rick-counters-of-the-rick-kind.html?x=y#title',
|
||||
'playlist': [
|
||||
{
|
||||
'md5': '4da359ec73b58df4575cd01a610ba5dc',
|
||||
'info_dict': {
|
||||
'id': '8a250ba1450996e901453d7f02ca02f5',
|
||||
'ext': 'flv',
|
||||
'title': 'Rick and Morty Close Rick-Counters of the Rick Kind part 1',
|
||||
'description': 'Rick has a run in with some old associates, resulting in a fallout with Morty. You got any chips, broh?',
|
||||
'uploader': 'Rick and Morty',
|
||||
'thumbnail': 'http://i.cdn.turner.com/asfix/repository/8a250ba13f865824013fc9db8b6b0400/thumbnail_267549017116827057.jpg'
|
||||
}
|
||||
},
|
||||
{
|
||||
'md5': 'ffbdf55af9331c509d95350bd0cc1819',
|
||||
'info_dict': {
|
||||
'id': '8a250ba1450996e901453d7f4bd102f6',
|
||||
'ext': 'flv',
|
||||
'title': 'Rick and Morty Close Rick-Counters of the Rick Kind part 2',
|
||||
'description': 'Rick has a run in with some old associates, resulting in a fallout with Morty. You got any chips, broh?',
|
||||
'uploader': 'Rick and Morty',
|
||||
'thumbnail': 'http://i.cdn.turner.com/asfix/repository/8a250ba13f865824013fc9db8b6b0400/thumbnail_267549017116827057.jpg'
|
||||
}
|
||||
},
|
||||
{
|
||||
'md5': 'b92409635540304280b4b6c36bd14a0a',
|
||||
'info_dict': {
|
||||
'id': '8a250ba1450996e901453d7fa73c02f7',
|
||||
'ext': 'flv',
|
||||
'title': 'Rick and Morty Close Rick-Counters of the Rick Kind part 3',
|
||||
'description': 'Rick has a run in with some old associates, resulting in a fallout with Morty. You got any chips, broh?',
|
||||
'uploader': 'Rick and Morty',
|
||||
'thumbnail': 'http://i.cdn.turner.com/asfix/repository/8a250ba13f865824013fc9db8b6b0400/thumbnail_267549017116827057.jpg'
|
||||
}
|
||||
},
|
||||
{
|
||||
'md5': 'e8818891d60e47b29cd89d7b0278156d',
|
||||
'info_dict': {
|
||||
'id': '8a250ba1450996e901453d7fc8ba02f8',
|
||||
'ext': 'flv',
|
||||
'title': 'Rick and Morty Close Rick-Counters of the Rick Kind part 4',
|
||||
'description': 'Rick has a run in with some old associates, resulting in a fallout with Morty. You got any chips, broh?',
|
||||
'uploader': 'Rick and Morty',
|
||||
'thumbnail': 'http://i.cdn.turner.com/asfix/repository/8a250ba13f865824013fc9db8b6b0400/thumbnail_267549017116827057.jpg'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
_video_extensions = {
|
||||
'3500': 'flv',
|
||||
'640': 'mp4',
|
||||
'150': 'mp4',
|
||||
'ipad': 'm3u8',
|
||||
'iphone': 'm3u8'
|
||||
}
|
||||
_video_dimensions = {
|
||||
'3500': (1280, 720),
|
||||
'640': (480, 270),
|
||||
'150': (320, 180)
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_path = mobj.group('path')
|
||||
|
||||
webpage = self._download_webpage(url, video_path)
|
||||
episode_id = self._html_search_regex(r'<link rel="video_src" href="http://i\.adultswim\.com/adultswim/adultswimtv/tools/swf/viralplayer.swf\?id=([0-9a-f]+?)"\s*/?\s*>', webpage, 'episode_id')
|
||||
title = self._og_search_title(webpage)
|
||||
|
||||
index_url = 'http://asfix.adultswim.com/asfix-svc/episodeSearch/getEpisodesByIDs?networkName=AS&ids=%s' % episode_id
|
||||
idoc = self._download_xml(index_url, title, 'Downloading episode index', 'Unable to download episode index')
|
||||
|
||||
episode_el = idoc.find('.//episode')
|
||||
show_title = episode_el.attrib.get('collectionTitle')
|
||||
episode_title = episode_el.attrib.get('title')
|
||||
thumbnail = episode_el.attrib.get('thumbnailUrl')
|
||||
description = episode_el.find('./description').text.strip()
|
||||
|
||||
entries = []
|
||||
segment_els = episode_el.findall('./segments/segment')
|
||||
|
||||
for part_num, segment_el in enumerate(segment_els):
|
||||
segment_id = segment_el.attrib.get('id')
|
||||
segment_title = '%s %s part %d' % (show_title, episode_title, part_num + 1)
|
||||
thumbnail = segment_el.attrib.get('thumbnailUrl')
|
||||
duration = segment_el.attrib.get('duration')
|
||||
|
||||
segment_url = 'http://asfix.adultswim.com/asfix-svc/episodeservices/getCvpPlaylist?networkName=AS&id=%s' % segment_id
|
||||
idoc = self._download_xml(segment_url, segment_title, 'Downloading segment information', 'Unable to download segment information')
|
||||
|
||||
formats = []
|
||||
file_els = idoc.findall('.//files/file')
|
||||
|
||||
for file_el in file_els:
|
||||
bitrate = file_el.attrib.get('bitrate')
|
||||
type = file_el.attrib.get('type')
|
||||
width, height = self._video_dimensions.get(bitrate, (None, None))
|
||||
formats.append({
|
||||
'format_id': '%s-%s' % (bitrate, type),
|
||||
'url': file_el.text,
|
||||
'ext': self._video_extensions.get(bitrate, 'mp4'),
|
||||
# The bitrate may not be a number (for example: 'iphone')
|
||||
'tbr': int(bitrate) if bitrate.isdigit() else None,
|
||||
'height': height,
|
||||
'width': width
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
entries.append({
|
||||
'id': segment_id,
|
||||
'title': segment_title,
|
||||
'formats': formats,
|
||||
'uploader': show_title,
|
||||
'thumbnail': thumbnail,
|
||||
'duration': duration,
|
||||
'description': description
|
||||
})
|
||||
|
||||
return {
|
||||
'_type': 'playlist',
|
||||
'id': episode_id,
|
||||
'display_id': video_path,
|
||||
'entries': entries,
|
||||
'title': '%s %s' % (show_title, episode_title),
|
||||
'description': description,
|
||||
'thumbnail': thumbnail
|
||||
}
|
@@ -14,13 +14,13 @@ from ..utils import (
|
||||
|
||||
|
||||
class ComedyCentralIE(MTVServicesInfoExtractor):
|
||||
_VALID_URL = r'''(?x)https?://(?:www\.)?(comedycentral|cc)\.com/
|
||||
(video-clips|episodes|cc-studios|video-collections)
|
||||
_VALID_URL = r'''(?x)https?://(?:www\.)?cc\.com/
|
||||
(video-clips|episodes|cc-studios|video-collections|full-episodes)
|
||||
/(?P<title>.*)'''
|
||||
_FEED_URL = 'http://comedycentral.com/feeds/mrss/'
|
||||
|
||||
_TEST = {
|
||||
'url': 'http://www.comedycentral.com/video-clips/kllhuv/stand-up-greg-fitzsimmons--uncensored---too-good-of-a-mother',
|
||||
'url': 'http://www.cc.com/video-clips/kllhuv/stand-up-greg-fitzsimmons--uncensored---too-good-of-a-mother',
|
||||
'md5': 'c4f48e9eda1b16dd10add0744344b6d8',
|
||||
'info_dict': {
|
||||
'id': 'cef0cbb3-e776-4bc9-b62e-8016deccb354',
|
||||
|
@@ -1,11 +1,12 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import netrc
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import netrc
|
||||
import time
|
||||
import xml.etree.ElementTree
|
||||
|
||||
from ..utils import (
|
||||
@@ -462,14 +463,14 @@ class InfoExtractor(object):
|
||||
def _og_search_url(self, html, **kargs):
|
||||
return self._og_search_property('url', html, **kargs)
|
||||
|
||||
def _html_search_meta(self, name, html, display_name=None, fatal=False):
|
||||
def _html_search_meta(self, name, html, display_name=None, fatal=False, **kwargs):
|
||||
if display_name is None:
|
||||
display_name = name
|
||||
return self._html_search_regex(
|
||||
r'''(?ix)<meta
|
||||
(?=[^>]+(?:itemprop|name|property)=["\']%s["\'])
|
||||
[^>]+content=["\']([^"\']+)["\']''' % re.escape(name),
|
||||
html, display_name, fatal=fatal)
|
||||
html, display_name, fatal=fatal, **kwargs)
|
||||
|
||||
def _dc_search_uploader(self, html):
|
||||
return self._html_search_meta('dc.creator', html, 'uploader')
|
||||
@@ -575,6 +576,13 @@ class InfoExtractor(object):
|
||||
else:
|
||||
return url
|
||||
|
||||
def _sleep(self, timeout, video_id, msg_template=None):
|
||||
if msg_template is None:
|
||||
msg_template = u'%(video_id)s: Waiting for %(timeout)s seconds'
|
||||
msg = msg_template % {'video_id': video_id, 'timeout': timeout}
|
||||
self.to_screen(msg)
|
||||
time.sleep(timeout)
|
||||
|
||||
|
||||
class SearchInfoExtractor(InfoExtractor):
|
||||
"""
|
||||
@@ -618,4 +626,3 @@ class SearchInfoExtractor(InfoExtractor):
|
||||
@property
|
||||
def SEARCH_KEY(self):
|
||||
return self._SEARCH_KEY
|
||||
|
||||
|
65
youtube_dl/extractor/cracked.py
Normal file
65
youtube_dl/extractor/cracked.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
parse_iso8601,
|
||||
str_to_int,
|
||||
)
|
||||
|
||||
|
||||
class CrackedIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?cracked\.com/video_(?P<id>\d+)_[\da-z-]+\.html'
|
||||
_TEST = {
|
||||
'url': 'http://www.cracked.com/video_19006_4-plot-holes-you-didnt-notice-in-your-favorite-movies.html',
|
||||
'md5': '4b29a5eeec292cd5eca6388c7558db9e',
|
||||
'info_dict': {
|
||||
'id': '19006',
|
||||
'ext': 'mp4',
|
||||
'title': '4 Plot Holes You Didn\'t Notice in Your Favorite Movies',
|
||||
'description': 'md5:3b909e752661db86007d10e5ec2df769',
|
||||
'timestamp': 1405659600,
|
||||
'upload_date': '20140718',
|
||||
}
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
video_url = self._html_search_regex(
|
||||
[r'var\s+CK_vidSrc\s*=\s*"([^"]+)"', r'<video\s+src="([^"]+)"'], webpage, 'video URL')
|
||||
|
||||
title = self._og_search_title(webpage)
|
||||
description = self._og_search_description(webpage)
|
||||
|
||||
timestamp = self._html_search_regex(r'<time datetime="([^"]+)"', webpage, 'upload date', fatal=False)
|
||||
if timestamp:
|
||||
timestamp = parse_iso8601(timestamp[:-6])
|
||||
|
||||
view_count = str_to_int(self._html_search_regex(
|
||||
r'<span class="views" id="viewCounts">([\d,\.]+) Views</span>', webpage, 'view count', fatal=False))
|
||||
comment_count = str_to_int(self._html_search_regex(
|
||||
r'<span id="commentCounts">([\d,\.]+)</span>', webpage, 'comment count', fatal=False))
|
||||
|
||||
m = re.search(r'_(?P<width>\d+)X(?P<height>\d+)\.mp4$', video_url)
|
||||
if m:
|
||||
width = int(m.group('width'))
|
||||
height = int(m.group('height'))
|
||||
else:
|
||||
width = height = None
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'url':video_url,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'timestamp': timestamp,
|
||||
'view_count': view_count,
|
||||
'comment_count': comment_count,
|
||||
'height': height,
|
||||
'width': width,
|
||||
}
|
@@ -1,40 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import determine_ext
|
||||
|
||||
|
||||
class CriterionIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://www\.criterion\.com/films/(\d*)-.+'
|
||||
_VALID_URL = r'https?://www\.criterion\.com/films/(?P<id>[0-9]+)-.+'
|
||||
_TEST = {
|
||||
u'url': u'http://www.criterion.com/films/184-le-samourai',
|
||||
u'file': u'184.mp4',
|
||||
u'md5': u'bc51beba55685509883a9a7830919ec3',
|
||||
u'info_dict': {
|
||||
u"title": u"Le Samouraï",
|
||||
u"description" : u'md5:a2b4b116326558149bef81f76dcbb93f',
|
||||
'url': 'http://www.criterion.com/films/184-le-samourai',
|
||||
'md5': 'bc51beba55685509883a9a7830919ec3',
|
||||
'info_dict': {
|
||||
'id': '184',
|
||||
'ext': 'mp4',
|
||||
'title': 'Le Samouraï',
|
||||
'description': 'md5:a2b4b116326558149bef81f76dcbb93f',
|
||||
}
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group(1)
|
||||
video_id = mobj.group('id')
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
final_url = self._search_regex(r'so.addVariable\("videoURL", "(.+?)"\)\;',
|
||||
webpage, 'video url')
|
||||
title = self._html_search_regex(r'<meta content="(.+?)" property="og:title" />',
|
||||
webpage, 'video title')
|
||||
description = self._html_search_regex(r'<meta name="description" content="(.+?)" />',
|
||||
webpage, 'video description')
|
||||
thumbnail = self._search_regex(r'so.addVariable\("thumbnailURL", "(.+?)"\)\;',
|
||||
webpage, 'thumbnail url')
|
||||
final_url = self._search_regex(
|
||||
r'so.addVariable\("videoURL", "(.+?)"\)\;', webpage, 'video url')
|
||||
title = self._og_search_title(webpage)
|
||||
description = self._html_search_regex(
|
||||
r'<meta name="description" content="(.+?)" />',
|
||||
webpage, 'video description')
|
||||
thumbnail = self._search_regex(
|
||||
r'so.addVariable\("thumbnailURL", "(.+?)"\)\;',
|
||||
webpage, 'thumbnail url')
|
||||
|
||||
return {'id': video_id,
|
||||
'url' : final_url,
|
||||
'title': title,
|
||||
'ext': determine_ext(final_url),
|
||||
'description': description,
|
||||
'thumbnail': thumbnail,
|
||||
}
|
||||
return {
|
||||
'id': video_id,
|
||||
'url': final_url,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'thumbnail': thumbnail,
|
||||
}
|
||||
|
44
youtube_dl/extractor/dfb.py
Normal file
44
youtube_dl/extractor/dfb.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
class DFBIE(InfoExtractor):
|
||||
IE_NAME = 'tv.dfb.de'
|
||||
_VALID_URL = r'https?://tv\.dfb\.de/video/[^/]+/(?P<id>\d+)'
|
||||
|
||||
_TEST = {
|
||||
'url': 'http://tv.dfb.de/video/highlights-des-empfangs-in-berlin/9070/',
|
||||
# The md5 is different each time
|
||||
'info_dict': {
|
||||
'id': '9070',
|
||||
'ext': 'flv',
|
||||
'title': 'Highlights des Empfangs in Berlin',
|
||||
'upload_date': '20140716',
|
||||
},
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
player_info = self._download_xml(
|
||||
'http://tv.dfb.de/server/hd_video.php?play=%s' % video_id,
|
||||
video_id)
|
||||
video_info = player_info.find('video')
|
||||
|
||||
f4m_info = self._download_xml(video_info.find('url').text, video_id)
|
||||
token_el = f4m_info.find('token')
|
||||
manifest_url = token_el.attrib['url'] + '?' + 'hdnea=' + token_el.attrib['auth'] + '&hdcore=3.2.0'
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': video_info.find('title').text,
|
||||
'url': manifest_url,
|
||||
'ext': 'flv',
|
||||
'thumbnail': self._og_search_thumbnail(webpage),
|
||||
'upload_date': ''.join(video_info.find('time_date').text.split('.')[::-1]),
|
||||
}
|
83
youtube_dl/extractor/firedrive.py
Normal file
83
youtube_dl/extractor/firedrive.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
compat_urllib_parse,
|
||||
compat_urllib_request,
|
||||
determine_ext,
|
||||
)
|
||||
|
||||
|
||||
class FiredriveIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?firedrive\.com/' + \
|
||||
'(?:file|embed)/(?P<id>[0-9a-zA-Z]+)'
|
||||
_FILE_DELETED_REGEX = r'<div class="removed_file_image">'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'https://www.firedrive.com/file/FEB892FA160EBD01',
|
||||
'md5': 'd5d4252f80ebeab4dc2d5ceaed1b7970',
|
||||
'info_dict': {
|
||||
'id': 'FEB892FA160EBD01',
|
||||
'ext': 'flv',
|
||||
'title': 'bbb_theora_486kbit.flv',
|
||||
'thumbnail': 're:^http://.*\.jpg$',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
|
||||
url = 'http://firedrive.com/file/%s' % video_id
|
||||
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
if re.search(self._FILE_DELETED_REGEX, webpage) is not None:
|
||||
raise ExtractorError('Video %s does not exist' % video_id,
|
||||
expected=True)
|
||||
|
||||
fields = dict(re.findall(r'''(?x)<input\s+
|
||||
type="hidden"\s+
|
||||
name="([^"]+)"\s+
|
||||
(?:id="[^"]+"\s+)?
|
||||
value="([^"]*)"
|
||||
''', webpage))
|
||||
|
||||
post = compat_urllib_parse.urlencode(fields)
|
||||
req = compat_urllib_request.Request(url, post)
|
||||
req.add_header('Content-type', 'application/x-www-form-urlencoded')
|
||||
|
||||
# Apparently, this header is required for confirmation to work.
|
||||
req.add_header('Host', 'www.firedrive.com')
|
||||
|
||||
webpage = self._download_webpage(req, video_id,
|
||||
'Downloading video page')
|
||||
|
||||
title = self._search_regex(r'class="external_title_left">(.+)</div>',
|
||||
webpage, 'title')
|
||||
thumbnail = self._search_regex(r'image:\s?"(//[^\"]+)', webpage,
|
||||
'thumbnail', fatal=False)
|
||||
if thumbnail is not None:
|
||||
thumbnail = 'http:' + thumbnail
|
||||
|
||||
ext = self._search_regex(r'type:\s?\'([^\']+)\',',
|
||||
webpage, 'extension', fatal=False)
|
||||
video_url = self._search_regex(
|
||||
r'file:\s?\'(http[^\']+)\',', webpage, 'file url')
|
||||
|
||||
formats = [{
|
||||
'format_id': 'sd',
|
||||
'url': video_url,
|
||||
'ext': ext,
|
||||
}]
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'thumbnail': thumbnail,
|
||||
'formats': formats,
|
||||
}
|
@@ -48,7 +48,7 @@ class PluzzIE(FranceTVBaseInfoExtractor):
|
||||
|
||||
class FranceTvInfoIE(FranceTVBaseInfoExtractor):
|
||||
IE_NAME = 'francetvinfo.fr'
|
||||
_VALID_URL = r'https?://www\.francetvinfo\.fr/.*/(?P<title>.+)\.html'
|
||||
_VALID_URL = r'https?://(?:www|mobile)\.francetvinfo\.fr/.*/(?P<title>.+)\.html'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'http://www.francetvinfo.fr/replay-jt/france-3/soir-3/jt-grand-soir-3-lundi-26-aout-2013_393427.html',
|
||||
@@ -211,7 +211,7 @@ class GenerationQuoiIE(InfoExtractor):
|
||||
|
||||
class CultureboxIE(FranceTVBaseInfoExtractor):
|
||||
IE_NAME = 'culturebox.francetvinfo.fr'
|
||||
_VALID_URL = r'https?://culturebox\.francetvinfo\.fr/(?P<name>.*?)(\?|$)'
|
||||
_VALID_URL = r'https?://(?:m\.)?culturebox\.francetvinfo\.fr/(?P<name>.*?)(\?|$)'
|
||||
|
||||
_TEST = {
|
||||
'url': 'http://culturebox.francetvinfo.fr/einstein-on-the-beach-au-theatre-du-chatelet-146813',
|
||||
|
90
youtube_dl/extractor/gameone.py
Normal file
90
youtube_dl/extractor/gameone.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
xpath_with_ns,
|
||||
parse_iso8601
|
||||
)
|
||||
|
||||
NAMESPACE_MAP = {
|
||||
'media': 'http://search.yahoo.com/mrss/',
|
||||
}
|
||||
|
||||
# URL prefix to download the mp4 files directly instead of streaming via rtmp
|
||||
# Credits go to XBox-Maniac
|
||||
# http://board.jdownloader.org/showpost.php?p=185835&postcount=31
|
||||
RAW_MP4_URL = 'http://cdn.riptide-mtvn.com/'
|
||||
|
||||
|
||||
class GameOneIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?gameone\.de/tv/(?P<id>\d+)'
|
||||
_TEST = {
|
||||
'url': 'http://www.gameone.de/tv/288',
|
||||
'md5': '136656b7fb4c9cb4a8e2d500651c499b',
|
||||
'info_dict': {
|
||||
'id': '288',
|
||||
'ext': 'mp4',
|
||||
'title': 'Game One - Folge 288',
|
||||
'duration': 1238,
|
||||
'thumbnail': 'http://s3.gameone.de/gameone/assets/video_metas/teaser_images/000/643/636/big/640x360.jpg',
|
||||
'description': 'FIFA-Pressepokal 2014, Star Citizen, Kingdom Come: Deliverance, Project Cars, Schöner Trants Nerdquiz Folge 2 Runde 1',
|
||||
'age_limit': 16,
|
||||
'upload_date': '20140513',
|
||||
'timestamp': 1399980122,
|
||||
}
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
og_video = self._og_search_video_url(webpage, secure=False)
|
||||
description = self._html_search_meta('description', webpage)
|
||||
age_limit = int(
|
||||
self._search_regex(
|
||||
r'age=(\d+)',
|
||||
self._html_search_meta(
|
||||
'age-de-meta-label',
|
||||
webpage),
|
||||
'age_limit',
|
||||
'0'))
|
||||
mrss_url = self._search_regex(r'mrss=([^&]+)', og_video, 'mrss')
|
||||
|
||||
mrss = self._download_xml(mrss_url, video_id, 'Downloading mrss')
|
||||
title = mrss.find('.//item/title').text
|
||||
thumbnail = mrss.find('.//item/image').get('url')
|
||||
timestamp = parse_iso8601(mrss.find('.//pubDate').text, delimiter=' ')
|
||||
content = mrss.find(xpath_with_ns('.//media:content', NAMESPACE_MAP))
|
||||
content_url = content.get('url')
|
||||
|
||||
content = self._download_xml(
|
||||
content_url,
|
||||
video_id,
|
||||
'Downloading media:content')
|
||||
rendition_items = content.findall('.//rendition')
|
||||
duration = int(rendition_items[0].get('duration'))
|
||||
formats = [
|
||||
{
|
||||
'url': re.sub(r'.*/(r2)', RAW_MP4_URL + r'\1', r.find('./src').text),
|
||||
'width': int(r.get('width')),
|
||||
'height': int(r.get('height')),
|
||||
'tbr': int(r.get('bitrate')),
|
||||
}
|
||||
for r in rendition_items
|
||||
]
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'thumbnail': thumbnail,
|
||||
'duration': duration,
|
||||
'formats': formats,
|
||||
'description': description,
|
||||
'age_limit': age_limit,
|
||||
'timestamp': timestamp,
|
||||
}
|
@@ -12,7 +12,12 @@ from ..utils import (
|
||||
|
||||
|
||||
class GorillaVidIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?gorillavid\.in/(?:embed-)?(?P<id>[0-9a-zA-Z]+)(?:-[0-9]+x[0-9]+\.html)?'
|
||||
IE_DESC = 'GorillaVid.in and daclips.in'
|
||||
_VALID_URL = r'''(?x)
|
||||
https?://(?P<host>(?:www\.)?
|
||||
(?:daclips\.in|gorillavid\.in))/
|
||||
(?:embed-)?(?P<id>[0-9a-zA-Z]+)(?:-[0-9]+x[0-9]+\.html)?
|
||||
'''
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'http://gorillavid.in/06y9juieqpmi',
|
||||
@@ -32,15 +37,22 @@ class GorillaVidIE(InfoExtractor):
|
||||
'title': 'Say something nice',
|
||||
'thumbnail': 're:http://.*\.jpg',
|
||||
},
|
||||
}, {
|
||||
'url': 'http://daclips.in/3rso4kdn6f9m',
|
||||
'md5': '1ad8fd39bb976eeb66004d3a4895f106',
|
||||
'info_dict': {
|
||||
'id': '3rso4kdn6f9m',
|
||||
'ext': 'mp4',
|
||||
'title': 'Micro Pig piglets ready on 16th July 2009',
|
||||
'thumbnail': 're:http://.*\.jpg',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
|
||||
url = 'http://gorillavid.in/%s' % video_id
|
||||
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
webpage = self._download_webpage('http://%s/%s' % (mobj.group('host'), video_id), video_id)
|
||||
|
||||
fields = dict(re.findall(r'''(?x)<input\s+
|
||||
type="hidden"\s+
|
||||
|
73
youtube_dl/extractor/goshgay.py
Normal file
73
youtube_dl/extractor/goshgay.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
compat_urlparse,
|
||||
str_to_int,
|
||||
ExtractorError,
|
||||
)
|
||||
import json
|
||||
|
||||
|
||||
class GoshgayIE(InfoExtractor):
|
||||
_VALID_URL = r'^(?:https?://)www.goshgay.com/video(?P<id>\d+?)($|/)'
|
||||
_TEST = {
|
||||
'url': 'http://www.goshgay.com/video4116282',
|
||||
'md5': '268b9f3c3229105c57859e166dd72b03',
|
||||
'info_dict': {
|
||||
'id': '4116282',
|
||||
'ext': 'flv',
|
||||
'title': 'md5:089833a4790b5e103285a07337f245bf',
|
||||
'thumbnail': 're:http://.*\.jpg',
|
||||
'age_limit': 18,
|
||||
}
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
title = self._search_regex(r'class="video-title"><h1>(.+?)<', webpage, 'title')
|
||||
|
||||
player_config = self._search_regex(
|
||||
r'(?s)jwplayer\("player"\)\.setup\(({.+?})\)', webpage, 'config settings')
|
||||
player_vars = json.loads(player_config.replace("'", '"'))
|
||||
width = str_to_int(player_vars.get('width'))
|
||||
height = str_to_int(player_vars.get('height'))
|
||||
config_uri = player_vars.get('config')
|
||||
|
||||
if config_uri is None:
|
||||
raise ExtractorError('Missing config URI')
|
||||
node = self._download_xml(config_uri, video_id, 'Downloading player config XML',
|
||||
errnote='Unable to download XML')
|
||||
if node is None:
|
||||
raise ExtractorError('Missing config XML')
|
||||
if node.tag != 'config':
|
||||
raise ExtractorError('Missing config attribute')
|
||||
fns = node.findall('file')
|
||||
imgs = node.findall('image')
|
||||
if len(fns) != 1:
|
||||
raise ExtractorError('Missing media URI')
|
||||
video_url = fns[0].text
|
||||
if len(imgs) < 1:
|
||||
thumbnail = None
|
||||
else:
|
||||
thumbnail = imgs[0].text
|
||||
|
||||
url_comp = compat_urlparse.urlparse(url)
|
||||
ref = "%s://%s%s" % (url_comp[0], url_comp[1], url_comp[2])
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'url': video_url,
|
||||
'title': title,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'thumbnail': thumbnail,
|
||||
'http_referer': ref,
|
||||
'age_limit': 18,
|
||||
}
|
102
youtube_dl/extractor/mlb.py
Normal file
102
youtube_dl/extractor/mlb.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
parse_duration,
|
||||
parse_iso8601,
|
||||
find_xpath_attr,
|
||||
)
|
||||
|
||||
|
||||
class MLBIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://m\.mlb\.com/video/(?:topic/[\da-z_-]+/)?v(?P<id>n?\d+)'
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'http://m.mlb.com/video/topic/81536970/v34496663/mianym-stanton-practices-for-the-home-run-derby',
|
||||
'md5': 'd9c022c10d21f849f49c05ae12a8a7e9',
|
||||
'info_dict': {
|
||||
'id': '34496663',
|
||||
'ext': 'mp4',
|
||||
'title': 'Stanton prepares for Derby',
|
||||
'description': 'md5:d00ce1e5fd9c9069e9c13ab4faedfa57',
|
||||
'duration': 46,
|
||||
'timestamp': 1405105800,
|
||||
'upload_date': '20140711',
|
||||
'thumbnail': 're:^https?://.*\.jpg$',
|
||||
},
|
||||
},
|
||||
{
|
||||
'url': 'http://m.mlb.com/video/topic/vtp_hrd_sponsor/v34578115/hrd-cespedes-wins-2014-gillette-home-run-derby',
|
||||
'md5': '0e6e73d509321e142409b695eadd541f',
|
||||
'info_dict': {
|
||||
'id': '34578115',
|
||||
'ext': 'mp4',
|
||||
'title': 'Cespedes repeats as Derby champ',
|
||||
'description': 'md5:08df253ce265d4cf6fb09f581fafad07',
|
||||
'duration': 488,
|
||||
'timestamp': 1405399936,
|
||||
'upload_date': '20140715',
|
||||
'thumbnail': 're:^https?://.*\.jpg$',
|
||||
},
|
||||
},
|
||||
{
|
||||
'url': 'http://m.mlb.com/video/v34577915/bautista-on-derby-captaining-duties-his-performance',
|
||||
'md5': 'b8fd237347b844365d74ea61d4245967',
|
||||
'info_dict': {
|
||||
'id': '34577915',
|
||||
'ext': 'mp4',
|
||||
'title': 'Bautista on Home Run Derby',
|
||||
'description': 'md5:b80b34031143d0986dddc64a8839f0fb',
|
||||
'duration': 52,
|
||||
'timestamp': 1405390722,
|
||||
'upload_date': '20140715',
|
||||
'thumbnail': 're:^https?://.*\.jpg$',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
|
||||
detail = self._download_xml(
|
||||
'http://m.mlb.com/gen/multimedia/detail/%s/%s/%s/%s.xml'
|
||||
% (video_id[-3], video_id[-2], video_id[-1], video_id), video_id)
|
||||
|
||||
title = detail.find('./headline').text
|
||||
description = detail.find('./big-blurb').text
|
||||
duration = parse_duration(detail.find('./duration').text)
|
||||
timestamp = parse_iso8601(detail.attrib['date'][:-5])
|
||||
|
||||
thumbnail = find_xpath_attr(
|
||||
detail, './thumbnailScenarios/thumbnailScenario', 'type', '45').text
|
||||
|
||||
formats = []
|
||||
for media_url in detail.findall('./url'):
|
||||
playback_scenario = media_url.attrib['playback_scenario']
|
||||
fmt = {
|
||||
'url': media_url.text,
|
||||
'format_id': playback_scenario,
|
||||
}
|
||||
m = re.search(r'(?P<vbr>\d+)K_(?P<width>\d+)X(?P<height>\d+)', playback_scenario)
|
||||
if m:
|
||||
fmt.update({
|
||||
'vbr': int(m.group('vbr')) * 1000,
|
||||
'width': int(m.group('width')),
|
||||
'height': int(m.group('height')),
|
||||
})
|
||||
formats.append(fmt)
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'duration': duration,
|
||||
'timestamp': timestamp,
|
||||
'formats': formats,
|
||||
'thumbnail': thumbnail,
|
||||
}
|
@@ -158,6 +158,9 @@ class MTVServicesInfoExtractor(InfoExtractor):
|
||||
if mgid.endswith('.swf'):
|
||||
mgid = mgid[:-4]
|
||||
except RegexNotFoundError:
|
||||
mgid = None
|
||||
|
||||
if mgid is None or ':' not in mgid:
|
||||
mgid = self._search_regex(
|
||||
[r'data-mgid="(.*?)"', r'swfobject.embedSWF\(".*?(mgid:.*?)"'],
|
||||
webpage, u'mgid')
|
||||
|
@@ -18,15 +18,15 @@ class NDRIE(InfoExtractor):
|
||||
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'http://www.ndr.de/fernsehen/sendungen/markt/markt7959.html',
|
||||
'md5': 'e7a6079ca39d3568f4996cb858dd6708',
|
||||
'url': 'http://www.ndr.de/fernsehen/media/dienordreportage325.html',
|
||||
'md5': '4a4eeafd17c3058b65f0c8f091355855',
|
||||
'note': 'Video file',
|
||||
'info_dict': {
|
||||
'id': '7959',
|
||||
'id': '325',
|
||||
'ext': 'mp4',
|
||||
'title': 'Markt - die ganze Sendung',
|
||||
'description': 'md5:af9179cf07f67c5c12dc6d9997e05725',
|
||||
'duration': 2655,
|
||||
'title': 'Blaue Bohnen aus Blocken',
|
||||
'description': 'md5:190d71ba2ccddc805ed01547718963bc',
|
||||
'duration': 1715,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@@ -32,7 +32,7 @@ class NPOIE(InfoExtractor):
|
||||
'http://e.omroep.nl/metadata/aflevering/%s' % video_id,
|
||||
video_id,
|
||||
# We have to remove the javascript callback
|
||||
transform_source=lambda j: re.sub(r'parseMetadata\((.*?)\);\n//epc', r'\1', j)
|
||||
transform_source=lambda j: re.sub(r'parseMetadata\((.*?)\);\n//.*$', r'\1', j)
|
||||
)
|
||||
token_page = self._download_webpage(
|
||||
'http://ida.omroep.nl/npoplayer/i.js',
|
||||
|
@@ -46,7 +46,7 @@ class PyvideoIE(InfoExtractor):
|
||||
return self.url_result(m_youtube.group(1), 'Youtube')
|
||||
|
||||
title = self._html_search_regex(
|
||||
r'<div class="section">.*?<h3(?:\s+class="[^"]*")?>([^>]+?)</h3>',
|
||||
r'<div class="section">\s*<h3(?:\s+class="[^"]*"[^>]*)?>([^>]+?)</h3>',
|
||||
webpage, 'title', flags=re.DOTALL)
|
||||
video_url = self._search_regex(
|
||||
[r'<source src="(.*?)"', r'<dt>Download</dt>.*?<a href="(.+?)"'],
|
||||
|
@@ -35,9 +35,7 @@ class RedTubeIE(InfoExtractor):
|
||||
r'<h1 class="videoTitle[^"]*">(.+?)</h1>',
|
||||
webpage, u'title')
|
||||
|
||||
video_thumbnail = self._html_search_regex(
|
||||
r'playerInnerHTML.+?<img\s+src="(.+?)"',
|
||||
webpage, u'thumbnail', fatal=False)
|
||||
video_thumbnail = self._og_search_thumbnail(webpage)
|
||||
|
||||
# No self-labeling, but they describe themselves as
|
||||
# "Home of Videos Porno"
|
||||
|
45
youtube_dl/extractor/reverbnation.py
Normal file
45
youtube_dl/extractor/reverbnation.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import strip_jsonp
|
||||
|
||||
|
||||
class ReverbNationIE(InfoExtractor):
|
||||
_VALID_URL = r'^https?://(?:www\.)?reverbnation\.com/.*?/song/(?P<id>\d+).*?$'
|
||||
_TESTS = [{
|
||||
'url': 'http://www.reverbnation.com/alkilados/song/16965047-mona-lisa',
|
||||
'file': '16965047.mp3',
|
||||
'md5': '3da12ebca28c67c111a7f8b262d3f7a7',
|
||||
'info_dict': {
|
||||
"title": "MONA LISA",
|
||||
"uploader": "ALKILADOS",
|
||||
"uploader_id": 216429,
|
||||
"thumbnail": "//gp1.wac.edgecastcdn.net/802892/production_public/Photo/13761700/image/1366002176_AVATAR_MONA_LISA.jpg"
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
song_id = mobj.group('id')
|
||||
|
||||
api_res = self._download_json(
|
||||
'https://api.reverbnation.com/song/%s?callback=api_response_5&_=%d'
|
||||
% (song_id, int(time.time() * 1000)),
|
||||
song_id,
|
||||
transform_source=strip_jsonp,
|
||||
note='Downloading information of song %s' % song_id
|
||||
)
|
||||
|
||||
return {
|
||||
'id': song_id,
|
||||
'title': api_res.get('name'),
|
||||
'url': api_res.get('url'),
|
||||
'uploader': api_res.get('artist', {}).get('name'),
|
||||
'uploader_id': api_res.get('artist', {}).get('id'),
|
||||
'thumbnail': api_res.get('image', api_res.get('thumbnail')),
|
||||
'ext': 'mp3',
|
||||
'vcodec': 'none',
|
||||
}
|
@@ -30,7 +30,7 @@ class RTBFIE(InfoExtractor):
|
||||
page = self._download_webpage('https://www.rtbf.be/video/embed?id=%s' % video_id, video_id)
|
||||
|
||||
data = json.loads(self._html_search_regex(
|
||||
r'<div class="js-player-embed" data-video="([^"]+)"', page, 'data video'))['data']
|
||||
r'<div class="js-player-embed(?: player-embed)?" data-video="([^"]+)"', page, 'data video'))['data']
|
||||
|
||||
video_url = data.get('downloadUrl') or data.get('url')
|
||||
|
||||
|
46
youtube_dl/extractor/ruhd.py
Normal file
46
youtube_dl/extractor/ruhd.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
class RUHDIE(InfoExtractor):
|
||||
_VALID_URL = r'http://(?:www\.)?ruhd\.ru/play\.php\?vid=(?P<id>\d+)'
|
||||
_TEST = {
|
||||
'url': 'http://www.ruhd.ru/play.php?vid=207',
|
||||
'md5': 'd1a9ec4edf8598e3fbd92bb16072ba83',
|
||||
'info_dict': {
|
||||
'id': '207',
|
||||
'ext': 'divx',
|
||||
'title': 'КОТ бааааам',
|
||||
'description': 'классный кот)',
|
||||
'thumbnail': 're:^http://.*\.jpg$',
|
||||
}
|
||||
}
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
video_url = self._html_search_regex(
|
||||
r'<param name="src" value="([^"]+)"', webpage, 'video url')
|
||||
title = self._html_search_regex(
|
||||
r'<title>([^<]+) RUHD.ru - Видео Высокого качества №1 в России!</title>', webpage, 'title')
|
||||
description = self._html_search_regex(
|
||||
r'(?s)<div id="longdesc">(.+?)<span id="showlink">', webpage, 'description', fatal=False)
|
||||
thumbnail = self._html_search_regex(
|
||||
r'<param name="previewImage" value="([^"]+)"', webpage, 'thumbnail', fatal=False)
|
||||
if thumbnail:
|
||||
thumbnail = 'http://www.ruhd.ru' + thumbnail
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'url': video_url,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'thumbnail': thumbnail,
|
||||
}
|
119
youtube_dl/extractor/sapo.py
Normal file
119
youtube_dl/extractor/sapo.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# encoding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
parse_duration,
|
||||
unified_strdate,
|
||||
)
|
||||
|
||||
|
||||
class SapoIE(InfoExtractor):
|
||||
IE_DESC = 'SAPO Vídeos'
|
||||
_VALID_URL = r'https?://(?:(?:v2|www)\.)?videos\.sapo\.(?:pt|cv|ao|mz|tl)/(?P<id>[\da-zA-Z]{20})'
|
||||
|
||||
_TESTS = [
|
||||
{
|
||||
'url': 'http://videos.sapo.pt/UBz95kOtiWYUMTA5Ghfi',
|
||||
'md5': '79ee523f6ecb9233ac25075dee0eda83',
|
||||
'note': 'SD video',
|
||||
'info_dict': {
|
||||
'id': 'UBz95kOtiWYUMTA5Ghfi',
|
||||
'ext': 'mp4',
|
||||
'title': 'Benfica - Marcas na Hitória',
|
||||
'description': 'md5:c9082000a128c3fd57bf0299e1367f22',
|
||||
'duration': 264,
|
||||
'uploader': 'tiago_1988',
|
||||
'upload_date': '20080229',
|
||||
'categories': ['benfica', 'cabral', 'desporto', 'futebol', 'geovanni', 'hooijdonk', 'joao', 'karel', 'lisboa', 'miccoli'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'url': 'http://videos.sapo.pt/IyusNAZ791ZdoCY5H5IF',
|
||||
'md5': '90a2f283cfb49193fe06e861613a72aa',
|
||||
'note': 'HD video',
|
||||
'info_dict': {
|
||||
'id': 'IyusNAZ791ZdoCY5H5IF',
|
||||
'ext': 'mp4',
|
||||
'title': 'Codebits VII - Report',
|
||||
'description': 'md5:6448d6fd81ce86feac05321f354dbdc8',
|
||||
'duration': 144,
|
||||
'uploader': 'codebits',
|
||||
'upload_date': '20140427',
|
||||
'categories': ['codebits', 'codebits2014'],
|
||||
},
|
||||
},
|
||||
{
|
||||
'url': 'http://v2.videos.sapo.pt/yLqjzPtbTimsn2wWBKHz',
|
||||
'md5': 'e5aa7cc0bdc6db9b33df1a48e49a15ac',
|
||||
'note': 'v2 video',
|
||||
'info_dict': {
|
||||
'id': 'yLqjzPtbTimsn2wWBKHz',
|
||||
'ext': 'mp4',
|
||||
'title': 'Hipnose Condicionativa 4',
|
||||
'description': 'md5:ef0481abf8fb4ae6f525088a6dadbc40',
|
||||
'duration': 692,
|
||||
'uploader': 'sapozen',
|
||||
'upload_date': '20090609',
|
||||
'categories': ['condicionativa', 'heloisa', 'hipnose', 'miranda', 'sapo', 'zen'],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
|
||||
item = self._download_xml(
|
||||
'http://rd3.videos.sapo.pt/%s/rss2' % video_id, video_id).find('./channel/item')
|
||||
|
||||
title = item.find('./title').text
|
||||
description = item.find('./{http://videos.sapo.pt/mrss/}synopse').text
|
||||
thumbnail = item.find('./{http://search.yahoo.com/mrss/}content').get('url')
|
||||
duration = parse_duration(item.find('./{http://videos.sapo.pt/mrss/}time').text)
|
||||
uploader = item.find('./{http://videos.sapo.pt/mrss/}author').text
|
||||
upload_date = unified_strdate(item.find('./pubDate').text)
|
||||
view_count = int(item.find('./{http://videos.sapo.pt/mrss/}views').text)
|
||||
comment_count = int(item.find('./{http://videos.sapo.pt/mrss/}comment_count').text)
|
||||
tags = item.find('./{http://videos.sapo.pt/mrss/}tags').text
|
||||
categories = tags.split() if tags else []
|
||||
age_limit = 18 if item.find('./{http://videos.sapo.pt/mrss/}m18').text == 'true' else 0
|
||||
|
||||
video_url = item.find('./{http://videos.sapo.pt/mrss/}videoFile').text
|
||||
video_size = item.find('./{http://videos.sapo.pt/mrss/}videoSize').text.split('x')
|
||||
|
||||
formats = [{
|
||||
'url': video_url,
|
||||
'ext': 'mp4',
|
||||
'format_id': 'sd',
|
||||
'width': int(video_size[0]),
|
||||
'height': int(video_size[1]),
|
||||
}]
|
||||
|
||||
if item.find('./{http://videos.sapo.pt/mrss/}HD').text == 'true':
|
||||
formats.append({
|
||||
'url': re.sub(r'/mov/1$', '/mov/39', video_url),
|
||||
'ext': 'mp4',
|
||||
'format_id': 'hd',
|
||||
'width': 1280,
|
||||
'height': 720,
|
||||
})
|
||||
|
||||
self._sort_formats(formats)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'thumbnail': thumbnail,
|
||||
'duration': duration,
|
||||
'uploader': uploader,
|
||||
'upload_date': upload_date,
|
||||
'view_count': view_count,
|
||||
'comment_count': comment_count,
|
||||
'categories': categories,
|
||||
'age_limit': age_limit,
|
||||
'formats': formats,
|
||||
}
|
112
youtube_dl/extractor/screencast.py
Normal file
112
youtube_dl/extractor/screencast.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
compat_parse_qs,
|
||||
compat_urllib_request,
|
||||
)
|
||||
|
||||
|
||||
class ScreencastIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://www\.screencast\.com/t/(?P<id>[a-zA-Z0-9]+)'
|
||||
_TESTS = [{
|
||||
'url': 'http://www.screencast.com/t/3ZEjQXlT',
|
||||
'md5': '917df1c13798a3e96211dd1561fded83',
|
||||
'info_dict': {
|
||||
'id': '3ZEjQXlT',
|
||||
'ext': 'm4v',
|
||||
'title': 'Color Measurement with Ocean Optics Spectrometers',
|
||||
'description': 'md5:240369cde69d8bed61349a199c5fb153',
|
||||
'thumbnail': 're:^https?://.*\.(?:gif|jpg)$',
|
||||
}
|
||||
}, {
|
||||
'url': 'http://www.screencast.com/t/V2uXehPJa1ZI',
|
||||
'md5': 'e8e4b375a7660a9e7e35c33973410d34',
|
||||
'info_dict': {
|
||||
'id': 'V2uXehPJa1ZI',
|
||||
'ext': 'mov',
|
||||
'title': 'The Amadeus Spectrometer',
|
||||
'description': 're:^In this video, our friends at.*To learn more about Amadeus, visit',
|
||||
'thumbnail': 're:^https?://.*\.(?:gif|jpg)$',
|
||||
}
|
||||
}, {
|
||||
'url': 'http://www.screencast.com/t/aAB3iowa',
|
||||
'md5': 'dedb2734ed00c9755761ccaee88527cd',
|
||||
'info_dict': {
|
||||
'id': 'aAB3iowa',
|
||||
'ext': 'mp4',
|
||||
'title': 'Google Earth Export',
|
||||
'description': 'Provides a demo of a CommunityViz export to Google Earth, one of the 3D viewing options.',
|
||||
'thumbnail': 're:^https?://.*\.(?:gif|jpg)$',
|
||||
}
|
||||
}, {
|
||||
'url': 'http://www.screencast.com/t/X3ddTrYh',
|
||||
'md5': '669ee55ff9c51988b4ebc0877cc8b159',
|
||||
'info_dict': {
|
||||
'id': 'X3ddTrYh',
|
||||
'ext': 'wmv',
|
||||
'title': 'Toolkit 6 User Group Webinar (2014-03-04) - Default Judgment and First Impression',
|
||||
'description': 'md5:7b9f393bc92af02326a5c5889639eab0',
|
||||
'thumbnail': 're:^https?://.*\.(?:gif|jpg)$',
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
video_url = self._html_search_regex(
|
||||
r'<embed name="Video".*?src="([^"]+)"', webpage,
|
||||
'QuickTime embed', default=None)
|
||||
|
||||
if video_url is None:
|
||||
flash_vars_s = self._html_search_regex(
|
||||
r'<param name="flashVars" value="([^"]+)"', webpage, 'flash vars',
|
||||
default=None)
|
||||
if not flash_vars_s:
|
||||
flash_vars_s = self._html_search_regex(
|
||||
r'<param name="initParams" value="([^"]+)"', webpage, 'flash vars',
|
||||
default=None)
|
||||
if flash_vars_s:
|
||||
flash_vars_s = flash_vars_s.replace(',', '&')
|
||||
if flash_vars_s:
|
||||
flash_vars = compat_parse_qs(flash_vars_s)
|
||||
video_url_raw = compat_urllib_request.quote(
|
||||
flash_vars['content'][0])
|
||||
video_url = video_url_raw.replace('http%3A', 'http:')
|
||||
|
||||
if video_url is None:
|
||||
video_meta = self._html_search_meta(
|
||||
'og:video', webpage, default=None)
|
||||
if video_meta:
|
||||
video_url = self._search_regex(
|
||||
r'src=(.*?)(?:$|&)', video_meta,
|
||||
'meta tag video URL', default=None)
|
||||
|
||||
if video_url is None:
|
||||
raise ExtractorError('Cannot find video')
|
||||
|
||||
title = self._og_search_title(webpage, default=None)
|
||||
if title is None:
|
||||
title = self._html_search_regex(
|
||||
[r'<b>Title:</b> ([^<]*)</div>',
|
||||
r'class="tabSeperator">></span><span class="tabText">(.*?)<'],
|
||||
webpage, 'title')
|
||||
thumbnail = self._og_search_thumbnail(webpage)
|
||||
description = self._og_search_description(webpage, default=None)
|
||||
if description is None:
|
||||
description = self._html_search_meta('description', webpage)
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'url': video_url,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'thumbnail': thumbnail,
|
||||
}
|
@@ -81,16 +81,16 @@ class SoundcloudIE(InfoExtractor):
|
||||
},
|
||||
# downloadable song
|
||||
{
|
||||
'url': 'https://soundcloud.com/simgretina/just-your-problem-baby-1',
|
||||
'md5': '56a8b69568acaa967b4c49f9d1d52d19',
|
||||
'url': 'https://soundcloud.com/oddsamples/bus-brakes',
|
||||
'md5': 'fee7b8747b09bb755cefd4b853e7249a',
|
||||
'info_dict': {
|
||||
'id': '105614606',
|
||||
'id': '128590877',
|
||||
'ext': 'wav',
|
||||
'title': 'Just Your Problem Baby (Acapella)',
|
||||
'description': 'Vocals',
|
||||
'uploader': 'Sim Gretina',
|
||||
'upload_date': '20130815',
|
||||
#'duration': 42,
|
||||
'title': 'Bus Brakes',
|
||||
'description': 'md5:0170be75dd395c96025d210d261c784e',
|
||||
'uploader': 'oddsamples',
|
||||
'upload_date': '20140109',
|
||||
'duration': 17,
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -255,7 +255,7 @@ class SoundcloudSetIE(SoundcloudIE):
|
||||
|
||||
|
||||
class SoundcloudUserIE(SoundcloudIE):
|
||||
_VALID_URL = r'https?://(www\.)?soundcloud\.com/(?P<user>[^/]+)(/?(tracks/)?)?(\?.*)?$'
|
||||
_VALID_URL = r'https?://(www\.)?soundcloud\.com/(?P<user>[^/]+)/?((?P<rsrc>tracks|likes)/?)?(\?.*)?$'
|
||||
IE_NAME = 'soundcloud:user'
|
||||
|
||||
# it's in tests/test_playlists.py
|
||||
@@ -264,24 +264,31 @@ class SoundcloudUserIE(SoundcloudIE):
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
uploader = mobj.group('user')
|
||||
resource = mobj.group('rsrc')
|
||||
if resource is None:
|
||||
resource = 'tracks'
|
||||
elif resource == 'likes':
|
||||
resource = 'favorites'
|
||||
|
||||
url = 'http://soundcloud.com/%s/' % uploader
|
||||
resolv_url = self._resolv_url(url)
|
||||
user = self._download_json(
|
||||
resolv_url, uploader, 'Downloading user info')
|
||||
base_url = 'http://api.soundcloud.com/users/%s/tracks.json?' % uploader
|
||||
base_url = 'http://api.soundcloud.com/users/%s/%s.json?' % (uploader, resource)
|
||||
|
||||
entries = []
|
||||
for i in itertools.count():
|
||||
data = compat_urllib_parse.urlencode({
|
||||
'offset': i * 50,
|
||||
'limit': 50,
|
||||
'client_id': self._CLIENT_ID,
|
||||
})
|
||||
new_entries = self._download_json(
|
||||
base_url + data, uploader, 'Downloading track page %s' % (i + 1))
|
||||
entries.extend(self._extract_info_dict(e, quiet=True) for e in new_entries)
|
||||
if len(new_entries) < 50:
|
||||
if len(new_entries) == 0:
|
||||
self.to_screen('%s: End page received' % uploader)
|
||||
break
|
||||
entries.extend(self._extract_info_dict(e, quiet=True) for e in new_entries)
|
||||
|
||||
return {
|
||||
'_type': 'playlist',
|
||||
|
@@ -3,24 +3,24 @@ from __future__ import unicode_literals
|
||||
from .mtv import MTVServicesInfoExtractor
|
||||
|
||||
|
||||
class SouthParkStudiosIE(MTVServicesInfoExtractor):
|
||||
IE_NAME = 'southparkstudios.com'
|
||||
_VALID_URL = r'https?://(www\.)?(?P<url>southparkstudios\.com/(clips|full-episodes)/(?P<id>.+?)(\?|#|$))'
|
||||
class SouthParkIE(MTVServicesInfoExtractor):
|
||||
IE_NAME = 'southpark.cc.com'
|
||||
_VALID_URL = r'https?://(www\.)?(?P<url>southpark\.cc\.com/(clips|full-episodes)/(?P<id>.+?)(\?|#|$))'
|
||||
|
||||
_FEED_URL = 'http://www.southparkstudios.com/feeds/video-player/mrss'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'http://www.southparkstudios.com/clips/104437/bat-daded#tab=featured',
|
||||
'url': 'http://southpark.cc.com/clips/104437/bat-daded#tab=featured',
|
||||
'info_dict': {
|
||||
'id': 'a7bff6c2-ed00-11e0-aca6-0026b9414f30',
|
||||
'ext': 'mp4',
|
||||
'title': 'Bat Daded',
|
||||
'title': 'South Park|Bat Daded',
|
||||
'description': 'Randy disqualifies South Park by getting into a fight with Bat Dad.',
|
||||
},
|
||||
}]
|
||||
|
||||
|
||||
class SouthparkDeIE(SouthParkStudiosIE):
|
||||
class SouthparkDeIE(SouthParkIE):
|
||||
IE_NAME = 'southpark.de'
|
||||
_VALID_URL = r'https?://(www\.)?(?P<url>southpark\.de/(clips|alle-episoden)/(?P<id>.+?)(\?|#|$))'
|
||||
_FEED_URL = 'http://www.southpark.de/feeds/video-player/mrss/'
|
84
youtube_dl/extractor/tenplay.py
Normal file
84
youtube_dl/extractor/tenplay.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
|
||||
|
||||
class TenPlayIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?ten(play)?\.com\.au/.+'
|
||||
_TEST = {
|
||||
'url': 'http://tenplay.com.au/ten-insider/extra/season-2013/tenplay-tv-your-way',
|
||||
#'md5': 'd68703d9f73dc8fccf3320ab34202590',
|
||||
'info_dict': {
|
||||
'id': '2695695426001',
|
||||
'ext': 'flv',
|
||||
'title': 'TENplay: TV your way',
|
||||
'description': 'Welcome to a new TV experience. Enjoy a taste of the TENplay benefits.',
|
||||
'timestamp': 1380150606.889,
|
||||
'upload_date': '20130925',
|
||||
'uploader': 'TENplay',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True, # Requires rtmpdump
|
||||
}
|
||||
}
|
||||
|
||||
_video_fields = [
|
||||
"id", "name", "shortDescription", "longDescription", "creationDate",
|
||||
"publishedDate", "lastModifiedDate", "customFields", "videoStillURL",
|
||||
"thumbnailURL", "referenceId", "length", "playsTotal",
|
||||
"playsTrailingWeek", "renditions", "captioning", "startDate", "endDate"]
|
||||
|
||||
def _real_extract(self, url):
|
||||
webpage = self._download_webpage(url, url)
|
||||
video_id = self._html_search_regex(
|
||||
r'videoID: "(\d+?)"', webpage, 'video_id')
|
||||
api_token = self._html_search_regex(
|
||||
r'apiToken: "([a-zA-Z0-9-_\.]+?)"', webpage, 'api_token')
|
||||
title = self._html_search_regex(
|
||||
r'<meta property="og:title" content="\s*(.*?)\s*"\s*/?\s*>',
|
||||
webpage, 'title')
|
||||
|
||||
json = self._download_json('https://api.brightcove.com/services/library?command=find_video_by_id&video_id=%s&token=%s&video_fields=%s' % (video_id, api_token, ','.join(self._video_fields)), title)
|
||||
|
||||
formats = []
|
||||
for rendition in json['renditions']:
|
||||
url = rendition['remoteUrl'] or rendition['url']
|
||||
protocol = 'rtmp' if url.startswith('rtmp') else 'http'
|
||||
ext = 'flv' if protocol == 'rtmp' else rendition['videoContainer'].lower()
|
||||
|
||||
if protocol == 'rtmp':
|
||||
url = url.replace('&mp4:', '')
|
||||
|
||||
formats.append({
|
||||
'format_id': '_'.join(['rtmp', rendition['videoContainer'].lower(), rendition['videoCodec'].lower()]),
|
||||
'width': rendition['frameWidth'],
|
||||
'height': rendition['frameHeight'],
|
||||
'tbr': rendition['encodingRate'] / 1024,
|
||||
'filesize': rendition['size'],
|
||||
'protocol': protocol,
|
||||
'ext': ext,
|
||||
'vcodec': rendition['videoCodec'].lower(),
|
||||
'container': rendition['videoContainer'].lower(),
|
||||
'url': url,
|
||||
})
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'display_id': json['referenceId'],
|
||||
'title': json['name'],
|
||||
'description': json['shortDescription'] or json['longDescription'],
|
||||
'formats': formats,
|
||||
'thumbnails': [{
|
||||
'url': json['videoStillURL']
|
||||
}, {
|
||||
'url': json['thumbnailURL']
|
||||
}],
|
||||
'thumbnail': json['videoStillURL'],
|
||||
'duration': json['length'] / 1000,
|
||||
'timestamp': float(json['creationDate']) / 1000,
|
||||
'uploader': json['customFields']['production_company_distributor'] if 'production_company_distributor' in json['customFields'] else 'TENplay',
|
||||
'view_count': json['playsTotal']
|
||||
}
|
@@ -5,6 +5,7 @@ import re
|
||||
from .common import InfoExtractor
|
||||
from .brightcove import BrightcoveIE
|
||||
from .discovery import DiscoveryIE
|
||||
from ..utils import compat_urlparse
|
||||
|
||||
|
||||
class TlcIE(DiscoveryIE):
|
||||
@@ -51,6 +52,10 @@ class TlcDeIE(InfoExtractor):
|
||||
# Otherwise we don't get the correct 'BrightcoveExperience' element,
|
||||
# example: http://www.tlc.de/sendungen/cake-boss/videos/cake-boss-cannoli-drama/
|
||||
iframe_url = iframe_url.replace('.htm?', '.php?')
|
||||
url_fragment = compat_urlparse.urlparse(url).fragment
|
||||
if url_fragment:
|
||||
# Since the fragment is not send to the server, we always get the same iframe
|
||||
iframe_url = re.sub(r'playlist=(\d+)', 'playlist=%s' % url_fragment, iframe_url)
|
||||
iframe = self._download_webpage(iframe_url, title)
|
||||
|
||||
return {
|
||||
|
@@ -1,21 +1,21 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
compat_parse_qs,
|
||||
)
|
||||
from ..utils import compat_parse_qs
|
||||
|
||||
|
||||
class TutvIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?tu\.tv/videos/(?P<id>[^/?]+)'
|
||||
_TEST = {
|
||||
'url': 'http://tu.tv/videos/noah-en-pabellon-cuahutemoc',
|
||||
'file': '2742556.flv',
|
||||
'md5': '5eb766671f69b82e528dc1e7769c5cb2',
|
||||
'url': 'http://tu.tv/videos/robots-futbolistas',
|
||||
'md5': '627c7c124ac2a9b5ab6addb94e0e65f7',
|
||||
'info_dict': {
|
||||
'title': 'Noah en pabellon cuahutemoc',
|
||||
'id': '2973058',
|
||||
'ext': 'flv',
|
||||
'title': 'Robots futbolistas',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26,10 +26,9 @@ class TutvIE(InfoExtractor):
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
internal_id = self._search_regex(r'codVideo=([0-9]+)', webpage, 'internal video ID')
|
||||
|
||||
data_url = 'http://tu.tv/flvurl.php?codVideo=' + str(internal_id)
|
||||
data_content = self._download_webpage(data_url, video_id, note='Downloading video info')
|
||||
data = compat_parse_qs(data_content)
|
||||
video_url = base64.b64decode(data['kpt'][0]).decode('utf-8')
|
||||
data_content = self._download_webpage(
|
||||
'http://tu.tv/flvurl.php?codVideo=%s' % internal_id, video_id, 'Downloading video info')
|
||||
video_url = base64.b64decode(compat_parse_qs(data_content)['kpt'][0]).decode('utf-8')
|
||||
|
||||
return {
|
||||
'id': internal_id,
|
||||
|
86
youtube_dl/extractor/vimple.py
Normal file
86
youtube_dl/extractor/vimple.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# coding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import base64
|
||||
import re
|
||||
import xml.etree.ElementTree
|
||||
import zlib
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..utils import int_or_none
|
||||
|
||||
|
||||
class VimpleIE(InfoExtractor):
|
||||
IE_DESC = 'Vimple.ru'
|
||||
_VALID_URL = r'https?://(player.vimple.ru/iframe|vimple.ru)/(?P<id>[a-f0-9]{10,})'
|
||||
_TESTS = [
|
||||
# Quality: Large, from iframe
|
||||
{
|
||||
'url': 'http://player.vimple.ru/iframe/b132bdfd71b546d3972f9ab9a25f201c',
|
||||
'info_dict': {
|
||||
'id': 'b132bdfd71b546d3972f9ab9a25f201c',
|
||||
'title': 'great-escape-minecraft.flv',
|
||||
'ext': 'mp4',
|
||||
'duration': 352,
|
||||
'webpage_url': 'http://vimple.ru/b132bdfd71b546d3972f9ab9a25f201c',
|
||||
},
|
||||
},
|
||||
# Quality: Medium, from mainpage
|
||||
{
|
||||
'url': 'http://vimple.ru/a15950562888453b8e6f9572dc8600cd',
|
||||
'info_dict': {
|
||||
'id': 'a15950562888453b8e6f9572dc8600cd',
|
||||
'title': 'DB 01',
|
||||
'ext': 'flv',
|
||||
'duration': 1484,
|
||||
'webpage_url': 'http://vimple.ru/a15950562888453b8e6f9572dc8600cd',
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
|
||||
iframe_url = 'http://player.vimple.ru/iframe/%s' % video_id
|
||||
|
||||
iframe = self._download_webpage(
|
||||
iframe_url, video_id,
|
||||
note='Downloading iframe', errnote='unable to fetch iframe')
|
||||
player_url = self._html_search_regex(
|
||||
r'"(http://player.vimple.ru/flash/.+?)"', iframe, 'player url')
|
||||
|
||||
player = self._request_webpage(
|
||||
player_url, video_id, note='Downloading swf player').read()
|
||||
|
||||
player = zlib.decompress(player[8:])
|
||||
|
||||
xml_pieces = re.findall(b'([a-zA-Z0-9 =+/]{500})', player)
|
||||
xml_pieces = [piece[1:-1] for piece in xml_pieces]
|
||||
|
||||
xml_data = b''.join(xml_pieces)
|
||||
xml_data = base64.b64decode(xml_data)
|
||||
|
||||
xml_data = xml.etree.ElementTree.fromstring(xml_data)
|
||||
|
||||
video = xml_data.find('Video')
|
||||
quality = video.get('quality')
|
||||
q_tag = video.find(quality.capitalize())
|
||||
|
||||
formats = [
|
||||
{
|
||||
'url': q_tag.get('url'),
|
||||
'tbr': int(q_tag.get('bitrate')),
|
||||
'filesize': int(q_tag.get('filesize')),
|
||||
'format_id': quality,
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': video.find('Title').text,
|
||||
'formats': formats,
|
||||
'thumbnail': video.find('Poster').get('url'),
|
||||
'duration': int_or_none(video.get('duration')),
|
||||
'webpage_url': video.find('Share').get('videoPageUrl'),
|
||||
}
|
63
youtube_dl/extractor/vodlocker.py
Normal file
63
youtube_dl/extractor/vodlocker.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
from .common import InfoExtractor
|
||||
from ..utils import (
|
||||
compat_urllib_parse,
|
||||
compat_urllib_request,
|
||||
)
|
||||
|
||||
|
||||
class VodlockerIE(InfoExtractor):
|
||||
_VALID_URL = r'https?://(?:www\.)?vodlocker.com/(?P<id>[0-9a-zA-Z]+)(?:\..*?)?'
|
||||
|
||||
_TESTS = [{
|
||||
'url': 'http://vodlocker.com/e8wvyzz4sl42',
|
||||
'md5': 'ce0c2d18fa0735f1bd91b69b0e54aacf',
|
||||
'info_dict': {
|
||||
'id': 'e8wvyzz4sl42',
|
||||
'ext': 'mp4',
|
||||
'title': 'Germany vs Brazil',
|
||||
'thumbnail': 're:http://.*\.jpg',
|
||||
},
|
||||
}]
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = re.match(self._VALID_URL, url)
|
||||
video_id = mobj.group('id')
|
||||
webpage = self._download_webpage(url, video_id)
|
||||
|
||||
fields = dict(re.findall(r'''(?x)<input\s+
|
||||
type="hidden"\s+
|
||||
name="([^"]+)"\s+
|
||||
(?:id="[^"]+"\s+)?
|
||||
value="([^"]*)"
|
||||
''', webpage))
|
||||
|
||||
if fields['op'] == 'download1':
|
||||
self._sleep(3, video_id) # they do detect when requests happen too fast!
|
||||
post = compat_urllib_parse.urlencode(fields)
|
||||
req = compat_urllib_request.Request(url, post)
|
||||
req.add_header('Content-type', 'application/x-www-form-urlencoded')
|
||||
webpage = self._download_webpage(
|
||||
req, video_id, 'Downloading video page')
|
||||
|
||||
title = self._search_regex(
|
||||
r'id="file_title".*?>\s*(.*?)\s*<span', webpage, 'title')
|
||||
thumbnail = self._search_regex(
|
||||
r'image:\s*"(http[^\"]+)",', webpage, 'thumbnail')
|
||||
url = self._search_regex(
|
||||
r'file:\s*"(http[^\"]+)",', webpage, 'file url')
|
||||
|
||||
formats = [{
|
||||
'format_id': 'sd',
|
||||
'url': url,
|
||||
}]
|
||||
|
||||
return {
|
||||
'id': video_id,
|
||||
'title': title,
|
||||
'thumbnail': thumbnail,
|
||||
'formats': formats,
|
||||
}
|
@@ -14,6 +14,7 @@ import zlib
|
||||
from .common import InfoExtractor, SearchInfoExtractor
|
||||
from .subtitles import SubtitlesInfoExtractor
|
||||
from ..jsinterp import JSInterpreter
|
||||
from ..swfinterp import SWFInterpreter
|
||||
from ..utils import (
|
||||
compat_chr,
|
||||
compat_parse_qs,
|
||||
@@ -347,8 +348,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
|
||||
self.to_screen(u'RTMP download detected')
|
||||
|
||||
def _extract_signature_function(self, video_id, player_url, slen):
|
||||
id_m = re.match(r'.*-(?P<id>[a-zA-Z0-9_-]+)\.(?P<ext>[a-z]+)$',
|
||||
player_url)
|
||||
id_m = re.match(
|
||||
r'.*-(?P<id>[a-zA-Z0-9_-]+)(?:/watch_as3)?\.(?P<ext>[a-z]+)$',
|
||||
player_url)
|
||||
player_type = id_m.group('ext')
|
||||
player_id = id_m.group('id')
|
||||
|
||||
@@ -449,417 +451,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
|
||||
return lambda s: initial_function([s])
|
||||
|
||||
def _parse_sig_swf(self, file_contents):
|
||||
if file_contents[1:3] != b'WS':
|
||||
raise ExtractorError(
|
||||
u'Not an SWF file; header is %r' % file_contents[:3])
|
||||
if file_contents[:1] == b'C':
|
||||
content = zlib.decompress(file_contents[8:])
|
||||
else:
|
||||
raise NotImplementedError(u'Unsupported compression format %r' %
|
||||
file_contents[:1])
|
||||
|
||||
def extract_tags(content):
|
||||
pos = 0
|
||||
while pos < len(content):
|
||||
header16 = struct.unpack('<H', content[pos:pos+2])[0]
|
||||
pos += 2
|
||||
tag_code = header16 >> 6
|
||||
tag_len = header16 & 0x3f
|
||||
if tag_len == 0x3f:
|
||||
tag_len = struct.unpack('<I', content[pos:pos+4])[0]
|
||||
pos += 4
|
||||
assert pos+tag_len <= len(content)
|
||||
yield (tag_code, content[pos:pos+tag_len])
|
||||
pos += tag_len
|
||||
|
||||
code_tag = next(tag
|
||||
for tag_code, tag in extract_tags(content)
|
||||
if tag_code == 82)
|
||||
p = code_tag.index(b'\0', 4) + 1
|
||||
code_reader = io.BytesIO(code_tag[p:])
|
||||
|
||||
# Parse ABC (AVM2 ByteCode)
|
||||
def read_int(reader=None):
|
||||
if reader is None:
|
||||
reader = code_reader
|
||||
res = 0
|
||||
shift = 0
|
||||
for _ in range(5):
|
||||
buf = reader.read(1)
|
||||
assert len(buf) == 1
|
||||
b = struct.unpack('<B', buf)[0]
|
||||
res = res | ((b & 0x7f) << shift)
|
||||
if b & 0x80 == 0:
|
||||
break
|
||||
shift += 7
|
||||
return res
|
||||
|
||||
def u30(reader=None):
|
||||
res = read_int(reader)
|
||||
assert res & 0xf0000000 == 0
|
||||
return res
|
||||
u32 = read_int
|
||||
|
||||
def s32(reader=None):
|
||||
v = read_int(reader)
|
||||
if v & 0x80000000 != 0:
|
||||
v = - ((v ^ 0xffffffff) + 1)
|
||||
return v
|
||||
|
||||
def read_string(reader=None):
|
||||
if reader is None:
|
||||
reader = code_reader
|
||||
slen = u30(reader)
|
||||
resb = reader.read(slen)
|
||||
assert len(resb) == slen
|
||||
return resb.decode('utf-8')
|
||||
|
||||
def read_bytes(count, reader=None):
|
||||
if reader is None:
|
||||
reader = code_reader
|
||||
resb = reader.read(count)
|
||||
assert len(resb) == count
|
||||
return resb
|
||||
|
||||
def read_byte(reader=None):
|
||||
resb = read_bytes(1, reader=reader)
|
||||
res = struct.unpack('<B', resb)[0]
|
||||
return res
|
||||
|
||||
# minor_version + major_version
|
||||
read_bytes(2 + 2)
|
||||
|
||||
# Constant pool
|
||||
int_count = u30()
|
||||
for _c in range(1, int_count):
|
||||
s32()
|
||||
uint_count = u30()
|
||||
for _c in range(1, uint_count):
|
||||
u32()
|
||||
double_count = u30()
|
||||
read_bytes((double_count-1) * 8)
|
||||
string_count = u30()
|
||||
constant_strings = [u'']
|
||||
for _c in range(1, string_count):
|
||||
s = read_string()
|
||||
constant_strings.append(s)
|
||||
namespace_count = u30()
|
||||
for _c in range(1, namespace_count):
|
||||
read_bytes(1) # kind
|
||||
u30() # name
|
||||
ns_set_count = u30()
|
||||
for _c in range(1, ns_set_count):
|
||||
count = u30()
|
||||
for _c2 in range(count):
|
||||
u30()
|
||||
multiname_count = u30()
|
||||
MULTINAME_SIZES = {
|
||||
0x07: 2, # QName
|
||||
0x0d: 2, # QNameA
|
||||
0x0f: 1, # RTQName
|
||||
0x10: 1, # RTQNameA
|
||||
0x11: 0, # RTQNameL
|
||||
0x12: 0, # RTQNameLA
|
||||
0x09: 2, # Multiname
|
||||
0x0e: 2, # MultinameA
|
||||
0x1b: 1, # MultinameL
|
||||
0x1c: 1, # MultinameLA
|
||||
}
|
||||
multinames = [u'']
|
||||
for _c in range(1, multiname_count):
|
||||
kind = u30()
|
||||
assert kind in MULTINAME_SIZES, u'Invalid multiname kind %r' % kind
|
||||
if kind == 0x07:
|
||||
u30() # namespace_idx
|
||||
name_idx = u30()
|
||||
multinames.append(constant_strings[name_idx])
|
||||
else:
|
||||
multinames.append('[MULTINAME kind: %d]' % kind)
|
||||
for _c2 in range(MULTINAME_SIZES[kind]):
|
||||
u30()
|
||||
|
||||
# Methods
|
||||
method_count = u30()
|
||||
MethodInfo = collections.namedtuple(
|
||||
'MethodInfo',
|
||||
['NEED_ARGUMENTS', 'NEED_REST'])
|
||||
method_infos = []
|
||||
for method_id in range(method_count):
|
||||
param_count = u30()
|
||||
u30() # return type
|
||||
for _ in range(param_count):
|
||||
u30() # param type
|
||||
u30() # name index (always 0 for youtube)
|
||||
flags = read_byte()
|
||||
if flags & 0x08 != 0:
|
||||
# Options present
|
||||
option_count = u30()
|
||||
for c in range(option_count):
|
||||
u30() # val
|
||||
read_bytes(1) # kind
|
||||
if flags & 0x80 != 0:
|
||||
# Param names present
|
||||
for _ in range(param_count):
|
||||
u30() # param name
|
||||
mi = MethodInfo(flags & 0x01 != 0, flags & 0x04 != 0)
|
||||
method_infos.append(mi)
|
||||
|
||||
# Metadata
|
||||
metadata_count = u30()
|
||||
for _c in range(metadata_count):
|
||||
u30() # name
|
||||
item_count = u30()
|
||||
for _c2 in range(item_count):
|
||||
u30() # key
|
||||
u30() # value
|
||||
|
||||
def parse_traits_info():
|
||||
trait_name_idx = u30()
|
||||
kind_full = read_byte()
|
||||
kind = kind_full & 0x0f
|
||||
attrs = kind_full >> 4
|
||||
methods = {}
|
||||
if kind in [0x00, 0x06]: # Slot or Const
|
||||
u30() # Slot id
|
||||
u30() # type_name_idx
|
||||
vindex = u30()
|
||||
if vindex != 0:
|
||||
read_byte() # vkind
|
||||
elif kind in [0x01, 0x02, 0x03]: # Method / Getter / Setter
|
||||
u30() # disp_id
|
||||
method_idx = u30()
|
||||
methods[multinames[trait_name_idx]] = method_idx
|
||||
elif kind == 0x04: # Class
|
||||
u30() # slot_id
|
||||
u30() # classi
|
||||
elif kind == 0x05: # Function
|
||||
u30() # slot_id
|
||||
function_idx = u30()
|
||||
methods[function_idx] = multinames[trait_name_idx]
|
||||
else:
|
||||
raise ExtractorError(u'Unsupported trait kind %d' % kind)
|
||||
|
||||
if attrs & 0x4 != 0: # Metadata present
|
||||
metadata_count = u30()
|
||||
for _c3 in range(metadata_count):
|
||||
u30() # metadata index
|
||||
|
||||
return methods
|
||||
|
||||
# Classes
|
||||
swfi = SWFInterpreter(file_contents)
|
||||
TARGET_CLASSNAME = u'SignatureDecipher'
|
||||
searched_idx = multinames.index(TARGET_CLASSNAME)
|
||||
searched_class_id = None
|
||||
class_count = u30()
|
||||
for class_id in range(class_count):
|
||||
name_idx = u30()
|
||||
if name_idx == searched_idx:
|
||||
# We found the class we're looking for!
|
||||
searched_class_id = class_id
|
||||
u30() # super_name idx
|
||||
flags = read_byte()
|
||||
if flags & 0x08 != 0: # Protected namespace is present
|
||||
u30() # protected_ns_idx
|
||||
intrf_count = u30()
|
||||
for _c2 in range(intrf_count):
|
||||
u30()
|
||||
u30() # iinit
|
||||
trait_count = u30()
|
||||
for _c2 in range(trait_count):
|
||||
parse_traits_info()
|
||||
|
||||
if searched_class_id is None:
|
||||
raise ExtractorError(u'Target class %r not found' %
|
||||
TARGET_CLASSNAME)
|
||||
|
||||
method_names = {}
|
||||
method_idxs = {}
|
||||
for class_id in range(class_count):
|
||||
u30() # cinit
|
||||
trait_count = u30()
|
||||
for _c2 in range(trait_count):
|
||||
trait_methods = parse_traits_info()
|
||||
if class_id == searched_class_id:
|
||||
method_names.update(trait_methods.items())
|
||||
method_idxs.update(dict(
|
||||
(idx, name)
|
||||
for name, idx in trait_methods.items()))
|
||||
|
||||
# Scripts
|
||||
script_count = u30()
|
||||
for _c in range(script_count):
|
||||
u30() # init
|
||||
trait_count = u30()
|
||||
for _c2 in range(trait_count):
|
||||
parse_traits_info()
|
||||
|
||||
# Method bodies
|
||||
method_body_count = u30()
|
||||
Method = collections.namedtuple('Method', ['code', 'local_count'])
|
||||
methods = {}
|
||||
for _c in range(method_body_count):
|
||||
method_idx = u30()
|
||||
u30() # max_stack
|
||||
local_count = u30()
|
||||
u30() # init_scope_depth
|
||||
u30() # max_scope_depth
|
||||
code_length = u30()
|
||||
code = read_bytes(code_length)
|
||||
if method_idx in method_idxs:
|
||||
m = Method(code, local_count)
|
||||
methods[method_idxs[method_idx]] = m
|
||||
exception_count = u30()
|
||||
for _c2 in range(exception_count):
|
||||
u30() # from
|
||||
u30() # to
|
||||
u30() # target
|
||||
u30() # exc_type
|
||||
u30() # var_name
|
||||
trait_count = u30()
|
||||
for _c2 in range(trait_count):
|
||||
parse_traits_info()
|
||||
|
||||
assert p + code_reader.tell() == len(code_tag)
|
||||
assert len(methods) == len(method_idxs)
|
||||
|
||||
method_pyfunctions = {}
|
||||
|
||||
def extract_function(func_name):
|
||||
if func_name in method_pyfunctions:
|
||||
return method_pyfunctions[func_name]
|
||||
if func_name not in methods:
|
||||
raise ExtractorError(u'Cannot find function %r' % func_name)
|
||||
m = methods[func_name]
|
||||
|
||||
def resfunc(args):
|
||||
registers = ['(this)'] + list(args) + [None] * m.local_count
|
||||
stack = []
|
||||
coder = io.BytesIO(m.code)
|
||||
while True:
|
||||
opcode = struct.unpack('!B', coder.read(1))[0]
|
||||
if opcode == 36: # pushbyte
|
||||
v = struct.unpack('!B', coder.read(1))[0]
|
||||
stack.append(v)
|
||||
elif opcode == 44: # pushstring
|
||||
idx = u30(coder)
|
||||
stack.append(constant_strings[idx])
|
||||
elif opcode == 48: # pushscope
|
||||
# We don't implement the scope register, so we'll just
|
||||
# ignore the popped value
|
||||
stack.pop()
|
||||
elif opcode == 70: # callproperty
|
||||
index = u30(coder)
|
||||
mname = multinames[index]
|
||||
arg_count = u30(coder)
|
||||
args = list(reversed(
|
||||
[stack.pop() for _ in range(arg_count)]))
|
||||
obj = stack.pop()
|
||||
if mname == u'split':
|
||||
assert len(args) == 1
|
||||
assert isinstance(args[0], compat_str)
|
||||
assert isinstance(obj, compat_str)
|
||||
if args[0] == u'':
|
||||
res = list(obj)
|
||||
else:
|
||||
res = obj.split(args[0])
|
||||
stack.append(res)
|
||||
elif mname == u'slice':
|
||||
assert len(args) == 1
|
||||
assert isinstance(args[0], int)
|
||||
assert isinstance(obj, list)
|
||||
res = obj[args[0]:]
|
||||
stack.append(res)
|
||||
elif mname == u'join':
|
||||
assert len(args) == 1
|
||||
assert isinstance(args[0], compat_str)
|
||||
assert isinstance(obj, list)
|
||||
res = args[0].join(obj)
|
||||
stack.append(res)
|
||||
elif mname in method_pyfunctions:
|
||||
stack.append(method_pyfunctions[mname](args))
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
u'Unsupported property %r on %r'
|
||||
% (mname, obj))
|
||||
elif opcode == 72: # returnvalue
|
||||
res = stack.pop()
|
||||
return res
|
||||
elif opcode == 79: # callpropvoid
|
||||
index = u30(coder)
|
||||
mname = multinames[index]
|
||||
arg_count = u30(coder)
|
||||
args = list(reversed(
|
||||
[stack.pop() for _ in range(arg_count)]))
|
||||
obj = stack.pop()
|
||||
if mname == u'reverse':
|
||||
assert isinstance(obj, list)
|
||||
obj.reverse()
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
u'Unsupported (void) property %r on %r'
|
||||
% (mname, obj))
|
||||
elif opcode == 93: # findpropstrict
|
||||
index = u30(coder)
|
||||
mname = multinames[index]
|
||||
res = extract_function(mname)
|
||||
stack.append(res)
|
||||
elif opcode == 97: # setproperty
|
||||
index = u30(coder)
|
||||
value = stack.pop()
|
||||
idx = stack.pop()
|
||||
obj = stack.pop()
|
||||
assert isinstance(obj, list)
|
||||
assert isinstance(idx, int)
|
||||
obj[idx] = value
|
||||
elif opcode == 98: # getlocal
|
||||
index = u30(coder)
|
||||
stack.append(registers[index])
|
||||
elif opcode == 99: # setlocal
|
||||
index = u30(coder)
|
||||
value = stack.pop()
|
||||
registers[index] = value
|
||||
elif opcode == 102: # getproperty
|
||||
index = u30(coder)
|
||||
pname = multinames[index]
|
||||
if pname == u'length':
|
||||
obj = stack.pop()
|
||||
assert isinstance(obj, list)
|
||||
stack.append(len(obj))
|
||||
else: # Assume attribute access
|
||||
idx = stack.pop()
|
||||
assert isinstance(idx, int)
|
||||
obj = stack.pop()
|
||||
assert isinstance(obj, list)
|
||||
stack.append(obj[idx])
|
||||
elif opcode == 128: # coerce
|
||||
u30(coder)
|
||||
elif opcode == 133: # coerce_s
|
||||
assert isinstance(stack[-1], (type(None), compat_str))
|
||||
elif opcode == 164: # modulo
|
||||
value2 = stack.pop()
|
||||
value1 = stack.pop()
|
||||
res = value1 % value2
|
||||
stack.append(res)
|
||||
elif opcode == 208: # getlocal_0
|
||||
stack.append(registers[0])
|
||||
elif opcode == 209: # getlocal_1
|
||||
stack.append(registers[1])
|
||||
elif opcode == 210: # getlocal_2
|
||||
stack.append(registers[2])
|
||||
elif opcode == 211: # getlocal_3
|
||||
stack.append(registers[3])
|
||||
elif opcode == 214: # setlocal_2
|
||||
registers[2] = stack.pop()
|
||||
elif opcode == 215: # setlocal_3
|
||||
registers[3] = stack.pop()
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
u'Unsupported opcode %d' % opcode)
|
||||
|
||||
method_pyfunctions[func_name] = resfunc
|
||||
return resfunc
|
||||
|
||||
initial_function = extract_function(u'decipher')
|
||||
searched_class = swfi.extract_class(TARGET_CLASSNAME)
|
||||
initial_function = swfi.extract_function(searched_class, u'decipher')
|
||||
return lambda s: initial_function([s])
|
||||
|
||||
def _decrypt_signature(self, s, video_id, player_url, age_gate=False):
|
||||
@@ -1220,30 +815,37 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
|
||||
url += '&signature=' + url_data['sig'][0]
|
||||
elif 's' in url_data:
|
||||
encrypted_sig = url_data['s'][0]
|
||||
if self._downloader.params.get('verbose'):
|
||||
if age_gate:
|
||||
if player_url is None:
|
||||
player_version = 'unknown'
|
||||
else:
|
||||
player_version = self._search_regex(
|
||||
r'-(.+)\.swf$', player_url,
|
||||
u'flash player', fatal=False)
|
||||
player_desc = 'flash player %s' % player_version
|
||||
else:
|
||||
player_version = self._search_regex(
|
||||
r'html5player-(.+?)\.js', video_webpage,
|
||||
'html5 player', fatal=False)
|
||||
player_desc = u'html5 player %s' % player_version
|
||||
|
||||
parts_sizes = u'.'.join(compat_str(len(part)) for part in encrypted_sig.split('.'))
|
||||
self.to_screen(u'encrypted signature length %d (%s), itag %s, %s' %
|
||||
(len(encrypted_sig), parts_sizes, url_data['itag'][0], player_desc))
|
||||
|
||||
if not age_gate:
|
||||
jsplayer_url_json = self._search_regex(
|
||||
r'"assets":.+?"js":\s*("[^"]+")',
|
||||
video_webpage, u'JS player URL')
|
||||
player_url = json.loads(jsplayer_url_json)
|
||||
if player_url is None:
|
||||
player_url_json = self._search_regex(
|
||||
r'ytplayer\.config.*?"url"\s*:\s*("[^"]+")',
|
||||
video_webpage, u'age gate player URL')
|
||||
player_url = json.loads(player_url_json)
|
||||
|
||||
if self._downloader.params.get('verbose'):
|
||||
if player_url is None:
|
||||
player_version = 'unknown'
|
||||
player_desc = 'unknown'
|
||||
else:
|
||||
if player_url.endswith('swf'):
|
||||
player_version = self._search_regex(
|
||||
r'-(.+)\.swf$', player_url,
|
||||
u'flash player', fatal=False)
|
||||
player_desc = 'flash player %s' % player_version
|
||||
else:
|
||||
player_version = self._search_regex(
|
||||
r'html5player-(.+?)\.js', video_webpage,
|
||||
'html5 player', fatal=False)
|
||||
player_desc = u'html5 player %s' % player_version
|
||||
|
||||
parts_sizes = u'.'.join(compat_str(len(part)) for part in encrypted_sig.split('.'))
|
||||
self.to_screen(u'encrypted signature length %d (%s), itag %s, %s' %
|
||||
(len(encrypted_sig), parts_sizes, url_data['itag'][0], player_desc))
|
||||
|
||||
signature = self._decrypt_signature(
|
||||
encrypted_sig, video_id, player_url, age_gate)
|
||||
|
@@ -11,6 +11,7 @@ class JSInterpreter(object):
|
||||
def __init__(self, code):
|
||||
self.code = code
|
||||
self._functions = {}
|
||||
self._objects = {}
|
||||
|
||||
def interpret_statement(self, stmt, local_vars, allow_recursion=20):
|
||||
if allow_recursion < 0:
|
||||
@@ -55,7 +56,19 @@ class JSInterpreter(object):
|
||||
m = re.match(r'^(?P<in>[a-z]+)\.(?P<member>.*)$', expr)
|
||||
if m:
|
||||
member = m.group('member')
|
||||
val = local_vars[m.group('in')]
|
||||
variable = m.group('in')
|
||||
|
||||
if variable not in local_vars:
|
||||
if variable not in self._objects:
|
||||
self._objects[variable] = self.extract_object(variable)
|
||||
obj = self._objects[variable]
|
||||
key, args = member.split('(', 1)
|
||||
args = args.strip(')')
|
||||
argvals = [int(v) if v.isdigit() else local_vars[v]
|
||||
for v in args.split(',')]
|
||||
return obj[key](argvals)
|
||||
|
||||
val = local_vars[variable]
|
||||
if member == 'split("")':
|
||||
return list(val)
|
||||
if member == 'join("")':
|
||||
@@ -97,6 +110,25 @@ class JSInterpreter(object):
|
||||
return self._functions[fname](argvals)
|
||||
raise ExtractorError('Unsupported JS expression %r' % expr)
|
||||
|
||||
def extract_object(self, objname):
|
||||
obj = {}
|
||||
obj_m = re.search(
|
||||
(r'(?:var\s+)?%s\s*=\s*\{' % re.escape(objname)) +
|
||||
r'\s*(?P<fields>([a-zA-Z$]+\s*:\s*function\(.*?\)\s*\{.*?\})*)' +
|
||||
r'\}\s*;',
|
||||
self.code)
|
||||
fields = obj_m.group('fields')
|
||||
# Currently, it only supports function definitions
|
||||
fields_m = re.finditer(
|
||||
r'(?P<key>[a-zA-Z$]+)\s*:\s*function'
|
||||
r'\((?P<args>[a-z,]+)\){(?P<code>[^}]+)}',
|
||||
fields)
|
||||
for f in fields_m:
|
||||
argnames = f.group('args').split(',')
|
||||
obj[f.group('key')] = self.build_function(argnames, f.group('code'))
|
||||
|
||||
return obj
|
||||
|
||||
def extract_function(self, funcname):
|
||||
func_m = re.search(
|
||||
(r'(?:function %s|[{;]%s\s*=\s*function)' % (
|
||||
@@ -107,10 +139,12 @@ class JSInterpreter(object):
|
||||
raise ExtractorError('Could not find JS function %r' % funcname)
|
||||
argnames = func_m.group('args').split(',')
|
||||
|
||||
return self.build_function(argnames, func_m.group('code'))
|
||||
|
||||
def build_function(self, argnames, code):
|
||||
def resf(args):
|
||||
local_vars = dict(zip(argnames, args))
|
||||
for stmt in func_m.group('code').split(';'):
|
||||
for stmt in code.split(';'):
|
||||
res = self.interpret_statement(stmt, local_vars)
|
||||
return res
|
||||
return resf
|
||||
|
||||
|
610
youtube_dl/swfinterp.py
Normal file
610
youtube_dl/swfinterp.py
Normal file
@@ -0,0 +1,610 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import collections
|
||||
import io
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
from .utils import (
|
||||
compat_str,
|
||||
ExtractorError,
|
||||
)
|
||||
|
||||
|
||||
def _extract_tags(file_contents):
|
||||
if file_contents[1:3] != b'WS':
|
||||
raise ExtractorError(
|
||||
'Not an SWF file; header is %r' % file_contents[:3])
|
||||
if file_contents[:1] == b'C':
|
||||
content = zlib.decompress(file_contents[8:])
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'Unsupported compression format %r' %
|
||||
file_contents[:1])
|
||||
|
||||
# Determine number of bits in framesize rectangle
|
||||
framesize_nbits = struct.unpack('!B', content[:1])[0] >> 3
|
||||
framesize_len = (5 + 4 * framesize_nbits + 7) // 8
|
||||
|
||||
pos = framesize_len + 2 + 2
|
||||
while pos < len(content):
|
||||
header16 = struct.unpack('<H', content[pos:pos + 2])[0]
|
||||
pos += 2
|
||||
tag_code = header16 >> 6
|
||||
tag_len = header16 & 0x3f
|
||||
if tag_len == 0x3f:
|
||||
tag_len = struct.unpack('<I', content[pos:pos + 4])[0]
|
||||
pos += 4
|
||||
assert pos + tag_len <= len(content), \
|
||||
('Tag %d ends at %d+%d - that\'s longer than the file (%d)'
|
||||
% (tag_code, pos, tag_len, len(content)))
|
||||
yield (tag_code, content[pos:pos + tag_len])
|
||||
pos += tag_len
|
||||
|
||||
|
||||
class _AVMClass_Object(object):
|
||||
def __init__(self, avm_class):
|
||||
self.avm_class = avm_class
|
||||
|
||||
def __repr__(self):
|
||||
return '%s#%x' % (self.avm_class.name, id(self))
|
||||
|
||||
|
||||
class _ScopeDict(dict):
|
||||
def __init__(self, avm_class):
|
||||
super(_ScopeDict, self).__init__()
|
||||
self.avm_class = avm_class
|
||||
|
||||
def __repr__(self):
|
||||
return '%s__Scope(%s)' % (
|
||||
self.avm_class.name,
|
||||
super(_ScopeDict, self).__repr__())
|
||||
|
||||
|
||||
class _AVMClass(object):
|
||||
def __init__(self, name_idx, name):
|
||||
self.name_idx = name_idx
|
||||
self.name = name
|
||||
self.method_names = {}
|
||||
self.method_idxs = {}
|
||||
self.methods = {}
|
||||
self.method_pyfunctions = {}
|
||||
|
||||
self.variables = _ScopeDict(self)
|
||||
|
||||
def make_object(self):
|
||||
return _AVMClass_Object(self)
|
||||
|
||||
def __repr__(self):
|
||||
return '_AVMClass(%s)' % (self.name)
|
||||
|
||||
def register_methods(self, methods):
|
||||
self.method_names.update(methods.items())
|
||||
self.method_idxs.update(dict(
|
||||
(idx, name)
|
||||
for name, idx in methods.items()))
|
||||
|
||||
|
||||
class _Multiname(object):
|
||||
def __init__(self, kind):
|
||||
self.kind = kind
|
||||
|
||||
def __repr__(self):
|
||||
return '[MULTINAME kind: 0x%x]' % self.kind
|
||||
|
||||
|
||||
def _read_int(reader):
|
||||
res = 0
|
||||
shift = 0
|
||||
for _ in range(5):
|
||||
buf = reader.read(1)
|
||||
assert len(buf) == 1
|
||||
b = struct.unpack('<B', buf)[0]
|
||||
res = res | ((b & 0x7f) << shift)
|
||||
if b & 0x80 == 0:
|
||||
break
|
||||
shift += 7
|
||||
return res
|
||||
|
||||
|
||||
def _u30(reader):
|
||||
res = _read_int(reader)
|
||||
assert res & 0xf0000000 == 0
|
||||
return res
|
||||
u32 = _read_int
|
||||
|
||||
|
||||
def _s32(reader):
|
||||
v = _read_int(reader)
|
||||
if v & 0x80000000 != 0:
|
||||
v = - ((v ^ 0xffffffff) + 1)
|
||||
return v
|
||||
|
||||
|
||||
def _s24(reader):
|
||||
bs = reader.read(3)
|
||||
assert len(bs) == 3
|
||||
last_byte = b'\xff' if (ord(bs[2:3]) >= 0x80) else b'\x00'
|
||||
return struct.unpack('<i', bs + last_byte)[0]
|
||||
|
||||
|
||||
def _read_string(reader):
|
||||
slen = _u30(reader)
|
||||
resb = reader.read(slen)
|
||||
assert len(resb) == slen
|
||||
return resb.decode('utf-8')
|
||||
|
||||
|
||||
def _read_bytes(count, reader):
|
||||
assert count >= 0
|
||||
resb = reader.read(count)
|
||||
assert len(resb) == count
|
||||
return resb
|
||||
|
||||
|
||||
def _read_byte(reader):
|
||||
resb = _read_bytes(1, reader=reader)
|
||||
res = struct.unpack('<B', resb)[0]
|
||||
return res
|
||||
|
||||
|
||||
class SWFInterpreter(object):
|
||||
def __init__(self, file_contents):
|
||||
code_tag = next(tag
|
||||
for tag_code, tag in _extract_tags(file_contents)
|
||||
if tag_code == 82)
|
||||
p = code_tag.index(b'\0', 4) + 1
|
||||
code_reader = io.BytesIO(code_tag[p:])
|
||||
|
||||
# Parse ABC (AVM2 ByteCode)
|
||||
|
||||
# Define a couple convenience methods
|
||||
u30 = lambda *args: _u30(*args, reader=code_reader)
|
||||
s32 = lambda *args: _s32(*args, reader=code_reader)
|
||||
u32 = lambda *args: _u32(*args, reader=code_reader)
|
||||
read_bytes = lambda *args: _read_bytes(*args, reader=code_reader)
|
||||
read_byte = lambda *args: _read_byte(*args, reader=code_reader)
|
||||
|
||||
# minor_version + major_version
|
||||
read_bytes(2 + 2)
|
||||
|
||||
# Constant pool
|
||||
int_count = u30()
|
||||
for _c in range(1, int_count):
|
||||
s32()
|
||||
uint_count = u30()
|
||||
for _c in range(1, uint_count):
|
||||
u32()
|
||||
double_count = u30()
|
||||
read_bytes(max(0, (double_count - 1)) * 8)
|
||||
string_count = u30()
|
||||
self.constant_strings = ['']
|
||||
for _c in range(1, string_count):
|
||||
s = _read_string(code_reader)
|
||||
self.constant_strings.append(s)
|
||||
namespace_count = u30()
|
||||
for _c in range(1, namespace_count):
|
||||
read_bytes(1) # kind
|
||||
u30() # name
|
||||
ns_set_count = u30()
|
||||
for _c in range(1, ns_set_count):
|
||||
count = u30()
|
||||
for _c2 in range(count):
|
||||
u30()
|
||||
multiname_count = u30()
|
||||
MULTINAME_SIZES = {
|
||||
0x07: 2, # QName
|
||||
0x0d: 2, # QNameA
|
||||
0x0f: 1, # RTQName
|
||||
0x10: 1, # RTQNameA
|
||||
0x11: 0, # RTQNameL
|
||||
0x12: 0, # RTQNameLA
|
||||
0x09: 2, # Multiname
|
||||
0x0e: 2, # MultinameA
|
||||
0x1b: 1, # MultinameL
|
||||
0x1c: 1, # MultinameLA
|
||||
}
|
||||
self.multinames = ['']
|
||||
for _c in range(1, multiname_count):
|
||||
kind = u30()
|
||||
assert kind in MULTINAME_SIZES, 'Invalid multiname kind %r' % kind
|
||||
if kind == 0x07:
|
||||
u30() # namespace_idx
|
||||
name_idx = u30()
|
||||
self.multinames.append(self.constant_strings[name_idx])
|
||||
else:
|
||||
self.multinames.append(_Multiname(kind))
|
||||
for _c2 in range(MULTINAME_SIZES[kind]):
|
||||
u30()
|
||||
|
||||
# Methods
|
||||
method_count = u30()
|
||||
MethodInfo = collections.namedtuple(
|
||||
'MethodInfo',
|
||||
['NEED_ARGUMENTS', 'NEED_REST'])
|
||||
method_infos = []
|
||||
for method_id in range(method_count):
|
||||
param_count = u30()
|
||||
u30() # return type
|
||||
for _ in range(param_count):
|
||||
u30() # param type
|
||||
u30() # name index (always 0 for youtube)
|
||||
flags = read_byte()
|
||||
if flags & 0x08 != 0:
|
||||
# Options present
|
||||
option_count = u30()
|
||||
for c in range(option_count):
|
||||
u30() # val
|
||||
read_bytes(1) # kind
|
||||
if flags & 0x80 != 0:
|
||||
# Param names present
|
||||
for _ in range(param_count):
|
||||
u30() # param name
|
||||
mi = MethodInfo(flags & 0x01 != 0, flags & 0x04 != 0)
|
||||
method_infos.append(mi)
|
||||
|
||||
# Metadata
|
||||
metadata_count = u30()
|
||||
for _c in range(metadata_count):
|
||||
u30() # name
|
||||
item_count = u30()
|
||||
for _c2 in range(item_count):
|
||||
u30() # key
|
||||
u30() # value
|
||||
|
||||
def parse_traits_info():
|
||||
trait_name_idx = u30()
|
||||
kind_full = read_byte()
|
||||
kind = kind_full & 0x0f
|
||||
attrs = kind_full >> 4
|
||||
methods = {}
|
||||
if kind in [0x00, 0x06]: # Slot or Const
|
||||
u30() # Slot id
|
||||
u30() # type_name_idx
|
||||
vindex = u30()
|
||||
if vindex != 0:
|
||||
read_byte() # vkind
|
||||
elif kind in [0x01, 0x02, 0x03]: # Method / Getter / Setter
|
||||
u30() # disp_id
|
||||
method_idx = u30()
|
||||
methods[self.multinames[trait_name_idx]] = method_idx
|
||||
elif kind == 0x04: # Class
|
||||
u30() # slot_id
|
||||
u30() # classi
|
||||
elif kind == 0x05: # Function
|
||||
u30() # slot_id
|
||||
function_idx = u30()
|
||||
methods[function_idx] = self.multinames[trait_name_idx]
|
||||
else:
|
||||
raise ExtractorError('Unsupported trait kind %d' % kind)
|
||||
|
||||
if attrs & 0x4 != 0: # Metadata present
|
||||
metadata_count = u30()
|
||||
for _c3 in range(metadata_count):
|
||||
u30() # metadata index
|
||||
|
||||
return methods
|
||||
|
||||
# Classes
|
||||
class_count = u30()
|
||||
classes = []
|
||||
for class_id in range(class_count):
|
||||
name_idx = u30()
|
||||
|
||||
cname = self.multinames[name_idx]
|
||||
avm_class = _AVMClass(name_idx, cname)
|
||||
classes.append(avm_class)
|
||||
|
||||
u30() # super_name idx
|
||||
flags = read_byte()
|
||||
if flags & 0x08 != 0: # Protected namespace is present
|
||||
u30() # protected_ns_idx
|
||||
intrf_count = u30()
|
||||
for _c2 in range(intrf_count):
|
||||
u30()
|
||||
u30() # iinit
|
||||
trait_count = u30()
|
||||
for _c2 in range(trait_count):
|
||||
trait_methods = parse_traits_info()
|
||||
avm_class.register_methods(trait_methods)
|
||||
|
||||
assert len(classes) == class_count
|
||||
self._classes_by_name = dict((c.name, c) for c in classes)
|
||||
|
||||
for avm_class in classes:
|
||||
u30() # cinit
|
||||
trait_count = u30()
|
||||
for _c2 in range(trait_count):
|
||||
trait_methods = parse_traits_info()
|
||||
avm_class.register_methods(trait_methods)
|
||||
|
||||
# Scripts
|
||||
script_count = u30()
|
||||
for _c in range(script_count):
|
||||
u30() # init
|
||||
trait_count = u30()
|
||||
for _c2 in range(trait_count):
|
||||
parse_traits_info()
|
||||
|
||||
# Method bodies
|
||||
method_body_count = u30()
|
||||
Method = collections.namedtuple('Method', ['code', 'local_count'])
|
||||
for _c in range(method_body_count):
|
||||
method_idx = u30()
|
||||
u30() # max_stack
|
||||
local_count = u30()
|
||||
u30() # init_scope_depth
|
||||
u30() # max_scope_depth
|
||||
code_length = u30()
|
||||
code = read_bytes(code_length)
|
||||
for avm_class in classes:
|
||||
if method_idx in avm_class.method_idxs:
|
||||
m = Method(code, local_count)
|
||||
avm_class.methods[avm_class.method_idxs[method_idx]] = m
|
||||
exception_count = u30()
|
||||
for _c2 in range(exception_count):
|
||||
u30() # from
|
||||
u30() # to
|
||||
u30() # target
|
||||
u30() # exc_type
|
||||
u30() # var_name
|
||||
trait_count = u30()
|
||||
for _c2 in range(trait_count):
|
||||
parse_traits_info()
|
||||
|
||||
assert p + code_reader.tell() == len(code_tag)
|
||||
|
||||
def extract_class(self, class_name):
|
||||
try:
|
||||
return self._classes_by_name[class_name]
|
||||
except KeyError:
|
||||
raise ExtractorError('Class %r not found' % class_name)
|
||||
|
||||
def extract_function(self, avm_class, func_name):
|
||||
if func_name in avm_class.method_pyfunctions:
|
||||
return avm_class.method_pyfunctions[func_name]
|
||||
if func_name in self._classes_by_name:
|
||||
return self._classes_by_name[func_name].make_object()
|
||||
if func_name not in avm_class.methods:
|
||||
raise ExtractorError('Cannot find function %s.%s' % (
|
||||
avm_class.name, func_name))
|
||||
m = avm_class.methods[func_name]
|
||||
|
||||
def resfunc(args):
|
||||
# Helper functions
|
||||
coder = io.BytesIO(m.code)
|
||||
s24 = lambda: _s24(coder)
|
||||
u30 = lambda: _u30(coder)
|
||||
|
||||
registers = [avm_class.variables] + list(args) + [None] * m.local_count
|
||||
stack = []
|
||||
scopes = collections.deque([
|
||||
self._classes_by_name, avm_class.variables])
|
||||
while True:
|
||||
opcode = _read_byte(coder)
|
||||
if opcode == 17: # iftrue
|
||||
offset = s24()
|
||||
value = stack.pop()
|
||||
if value:
|
||||
coder.seek(coder.tell() + offset)
|
||||
elif opcode == 18: # iffalse
|
||||
offset = s24()
|
||||
value = stack.pop()
|
||||
if not value:
|
||||
coder.seek(coder.tell() + offset)
|
||||
elif opcode == 36: # pushbyte
|
||||
v = _read_byte(coder)
|
||||
stack.append(v)
|
||||
elif opcode == 42: # dup
|
||||
value = stack[-1]
|
||||
stack.append(value)
|
||||
elif opcode == 44: # pushstring
|
||||
idx = u30()
|
||||
stack.append(self.constant_strings[idx])
|
||||
elif opcode == 48: # pushscope
|
||||
new_scope = stack.pop()
|
||||
scopes.append(new_scope)
|
||||
elif opcode == 66: # construct
|
||||
arg_count = u30()
|
||||
args = list(reversed(
|
||||
[stack.pop() for _ in range(arg_count)]))
|
||||
obj = stack.pop()
|
||||
res = obj.avm_class.make_object()
|
||||
stack.append(res)
|
||||
elif opcode == 70: # callproperty
|
||||
index = u30()
|
||||
mname = self.multinames[index]
|
||||
arg_count = u30()
|
||||
args = list(reversed(
|
||||
[stack.pop() for _ in range(arg_count)]))
|
||||
obj = stack.pop()
|
||||
|
||||
if isinstance(obj, _AVMClass_Object):
|
||||
func = self.extract_function(obj.avm_class, mname)
|
||||
res = func(args)
|
||||
stack.append(res)
|
||||
continue
|
||||
elif isinstance(obj, _ScopeDict):
|
||||
if mname in obj.avm_class.method_names:
|
||||
func = self.extract_function(obj.avm_class, mname)
|
||||
res = func(args)
|
||||
else:
|
||||
res = obj[mname]
|
||||
stack.append(res)
|
||||
continue
|
||||
elif isinstance(obj, compat_str):
|
||||
if mname == 'split':
|
||||
assert len(args) == 1
|
||||
assert isinstance(args[0], compat_str)
|
||||
if args[0] == '':
|
||||
res = list(obj)
|
||||
else:
|
||||
res = obj.split(args[0])
|
||||
stack.append(res)
|
||||
continue
|
||||
elif isinstance(obj, list):
|
||||
if mname == 'slice':
|
||||
assert len(args) == 1
|
||||
assert isinstance(args[0], int)
|
||||
res = obj[args[0]:]
|
||||
stack.append(res)
|
||||
continue
|
||||
elif mname == 'join':
|
||||
assert len(args) == 1
|
||||
assert isinstance(args[0], compat_str)
|
||||
res = args[0].join(obj)
|
||||
stack.append(res)
|
||||
continue
|
||||
raise NotImplementedError(
|
||||
'Unsupported property %r on %r'
|
||||
% (mname, obj))
|
||||
elif opcode == 72: # returnvalue
|
||||
res = stack.pop()
|
||||
return res
|
||||
elif opcode == 74: # constructproperty
|
||||
index = u30()
|
||||
arg_count = u30()
|
||||
args = list(reversed(
|
||||
[stack.pop() for _ in range(arg_count)]))
|
||||
obj = stack.pop()
|
||||
|
||||
mname = self.multinames[index]
|
||||
assert isinstance(obj, _AVMClass)
|
||||
construct_method = self.extract_function(
|
||||
obj, mname)
|
||||
# We do not actually call the constructor for now;
|
||||
# we just pretend it does nothing
|
||||
stack.append(obj.make_object())
|
||||
elif opcode == 79: # callpropvoid
|
||||
index = u30()
|
||||
mname = self.multinames[index]
|
||||
arg_count = u30()
|
||||
args = list(reversed(
|
||||
[stack.pop() for _ in range(arg_count)]))
|
||||
obj = stack.pop()
|
||||
if mname == 'reverse':
|
||||
assert isinstance(obj, list)
|
||||
obj.reverse()
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'Unsupported (void) property %r on %r'
|
||||
% (mname, obj))
|
||||
elif opcode == 86: # newarray
|
||||
arg_count = u30()
|
||||
arr = []
|
||||
for i in range(arg_count):
|
||||
arr.append(stack.pop())
|
||||
arr = arr[::-1]
|
||||
stack.append(arr)
|
||||
elif opcode == 93: # findpropstrict
|
||||
index = u30()
|
||||
mname = self.multinames[index]
|
||||
for s in reversed(scopes):
|
||||
if mname in s:
|
||||
res = s
|
||||
break
|
||||
else:
|
||||
res = scopes[0]
|
||||
stack.append(res[mname])
|
||||
elif opcode == 94: # findproperty
|
||||
index = u30()
|
||||
mname = self.multinames[index]
|
||||
for s in reversed(scopes):
|
||||
if mname in s:
|
||||
res = s
|
||||
break
|
||||
else:
|
||||
res = avm_class.variables
|
||||
stack.append(res)
|
||||
elif opcode == 96: # getlex
|
||||
index = u30()
|
||||
mname = self.multinames[index]
|
||||
for s in reversed(scopes):
|
||||
if mname in s:
|
||||
scope = s
|
||||
break
|
||||
else:
|
||||
scope = avm_class.variables
|
||||
# I cannot find where static variables are initialized
|
||||
# so let's just return None
|
||||
res = scope.get(mname)
|
||||
stack.append(res)
|
||||
elif opcode == 97: # setproperty
|
||||
index = u30()
|
||||
value = stack.pop()
|
||||
idx = self.multinames[index]
|
||||
if isinstance(idx, _Multiname):
|
||||
idx = stack.pop()
|
||||
obj = stack.pop()
|
||||
obj[idx] = value
|
||||
elif opcode == 98: # getlocal
|
||||
index = u30()
|
||||
stack.append(registers[index])
|
||||
elif opcode == 99: # setlocal
|
||||
index = u30()
|
||||
value = stack.pop()
|
||||
registers[index] = value
|
||||
elif opcode == 102: # getproperty
|
||||
index = u30()
|
||||
pname = self.multinames[index]
|
||||
if pname == 'length':
|
||||
obj = stack.pop()
|
||||
assert isinstance(obj, list)
|
||||
stack.append(len(obj))
|
||||
else: # Assume attribute access
|
||||
idx = stack.pop()
|
||||
assert isinstance(idx, int)
|
||||
obj = stack.pop()
|
||||
assert isinstance(obj, list)
|
||||
stack.append(obj[idx])
|
||||
elif opcode == 115: # convert_
|
||||
value = stack.pop()
|
||||
intvalue = int(value)
|
||||
stack.append(intvalue)
|
||||
elif opcode == 128: # coerce
|
||||
u30()
|
||||
elif opcode == 133: # coerce_s
|
||||
assert isinstance(stack[-1], (type(None), compat_str))
|
||||
elif opcode == 160: # add
|
||||
value2 = stack.pop()
|
||||
value1 = stack.pop()
|
||||
res = value1 + value2
|
||||
stack.append(res)
|
||||
elif opcode == 161: # subtract
|
||||
value2 = stack.pop()
|
||||
value1 = stack.pop()
|
||||
res = value1 - value2
|
||||
stack.append(res)
|
||||
elif opcode == 164: # modulo
|
||||
value2 = stack.pop()
|
||||
value1 = stack.pop()
|
||||
res = value1 % value2
|
||||
stack.append(res)
|
||||
elif opcode == 175: # greaterequals
|
||||
value2 = stack.pop()
|
||||
value1 = stack.pop()
|
||||
result = value1 >= value2
|
||||
stack.append(result)
|
||||
elif opcode == 208: # getlocal_0
|
||||
stack.append(registers[0])
|
||||
elif opcode == 209: # getlocal_1
|
||||
stack.append(registers[1])
|
||||
elif opcode == 210: # getlocal_2
|
||||
stack.append(registers[2])
|
||||
elif opcode == 211: # getlocal_3
|
||||
stack.append(registers[3])
|
||||
elif opcode == 212: # setlocal_0
|
||||
registers[0] = stack.pop()
|
||||
elif opcode == 213: # setlocal_1
|
||||
registers[1] = stack.pop()
|
||||
elif opcode == 214: # setlocal_2
|
||||
registers[2] = stack.pop()
|
||||
elif opcode == 215: # setlocal_3
|
||||
registers[3] = stack.pop()
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'Unsupported opcode %d' % opcode)
|
||||
|
||||
avm_class.method_pyfunctions[func_name] = resfunc
|
||||
return resfunc
|
||||
|
@@ -775,7 +775,7 @@ class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
|
||||
https_response = http_response
|
||||
|
||||
|
||||
def parse_iso8601(date_str):
|
||||
def parse_iso8601(date_str, delimiter='T'):
|
||||
""" Return a UNIX timestamp from the given date """
|
||||
|
||||
if date_str is None:
|
||||
@@ -795,8 +795,8 @@ def parse_iso8601(date_str):
|
||||
timezone = datetime.timedelta(
|
||||
hours=sign * int(m.group('hours')),
|
||||
minutes=sign * int(m.group('minutes')))
|
||||
|
||||
dt = datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S') - timezone
|
||||
date_format = '%Y-%m-%d{0}%H:%M:%S'.format(delimiter)
|
||||
dt = datetime.datetime.strptime(date_str, date_format) - timezone
|
||||
return calendar.timegm(dt.timetuple())
|
||||
|
||||
|
||||
@@ -1194,6 +1194,8 @@ def format_bytes(bytes):
|
||||
|
||||
|
||||
def str_to_int(int_str):
|
||||
if int_str is None:
|
||||
return None
|
||||
int_str = re.sub(r'[,\.]', u'', int_str)
|
||||
return int(int_str)
|
||||
|
||||
@@ -1428,7 +1430,7 @@ US_RATINGS = {
|
||||
|
||||
|
||||
def strip_jsonp(code):
|
||||
return re.sub(r'(?s)^[a-zA-Z_]+\s*\(\s*(.*)\);\s*?\s*$', r'\1', code)
|
||||
return re.sub(r'(?s)^[a-zA-Z0-9_]+\s*\(\s*(.*)\);?\s*?\s*$', r'\1', code)
|
||||
|
||||
|
||||
def qualities(quality_ids):
|
||||
|
@@ -1,2 +1,2 @@
|
||||
|
||||
__version__ = '2014.07.11'
|
||||
__version__ = '2014.07.20'
|
||||
|
Reference in New Issue
Block a user