Compare commits
68 Commits
2014.02.13
...
2014.02.22
Author | SHA1 | Date | |
---|---|---|---|
5e0b652344 | |||
0f8f097183 | |||
491ed3dda2 | |||
af284c6d1b | |||
41d3ec5fba | |||
0568c352f3 | |||
2e7b4cb714 | |||
9767726b66 | |||
9ddfd84e41 | |||
1cf563d84b | |||
f7300c5c90 | |||
3489b7d26c | |||
acd2bcc384 | |||
43e77ca455 | |||
da36297988 | |||
dbb94fb044 | |||
d68f0cdb23 | |||
eae16eb67b | |||
4fc946b546 | |||
280bc5dad6 | |||
f43770d8c9 | |||
98c4b8fa1b | |||
ccb079ee67 | |||
2ea237472c | |||
0d4b4865cc | |||
fe52f9f956 | |||
882907a818 | |||
572a89cc4e | |||
c377110539 | |||
a9c7198a0b | |||
f6f01ea17b | |||
f2d0fc6823 | |||
f7000f3a1b | |||
c7f0177fa7 | |||
09c4d50944 | |||
2eb5d315d4 | |||
ad5976b4d9 | |||
a0dfcdce5e | |||
96d1637082 | |||
960f317171 | |||
4412ca751d | |||
cbffec0c95 | |||
0cea52cc18 | |||
6d784e87f4 | |||
ae6cae78f1 | |||
0f99566c01 | |||
2db806b4aa | |||
3f32c0ba4c | |||
541cb26c0d | |||
5544e038ab | |||
9032dc28a6 | |||
03635e2a71 | |||
00cf938aa5 | |||
a5f707c495 | |||
1824b48169 | |||
07ad22b8af | |||
b53466e168 | |||
6a7a389679 | |||
4edff78531 | |||
99043c2ea5 | |||
e68abba910 | |||
3165dc4d9f | |||
66c43a53e4 | |||
463b334616 | |||
b71dbc57c4 | |||
72ca1d7f45 | |||
cf1eb45153 | |||
a97bcd80ba |
12
README.md
12
README.md
@ -20,7 +20,7 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
sure that you have sufficient permissions
|
sure that you have sufficient permissions
|
||||||
(run with sudo if needed)
|
(run with sudo if needed)
|
||||||
-i, --ignore-errors continue on download errors, for example to
|
-i, --ignore-errors continue on download errors, for example to
|
||||||
to skip unavailable videos in a playlist
|
skip unavailable videos in a playlist
|
||||||
--abort-on-error Abort downloading of further videos (in the
|
--abort-on-error Abort downloading of further videos (in the
|
||||||
playlist or the command line) if an error
|
playlist or the command line) if an error
|
||||||
occurs
|
occurs
|
||||||
@ -246,7 +246,7 @@ which means you can modify it, redistribute it or use it however you like.
|
|||||||
|
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
|
|
||||||
You can configure youtube-dl by placing default arguments (such as `--extract-audio --no-mtime` to always extract the audio and not copy the mtime) into `/etc/youtube-dl.conf` and/or `~/.config/youtube-dl.conf`. On Windows, the configuration file locations are `%APPDATA%\youtube-dl\config.txt` and `C:\Users\<Yourname>\youtube-dl.conf`.
|
You can configure youtube-dl by placing default arguments (such as `--extract-audio --no-mtime` to always extract the audio and not copy the mtime) into `/etc/youtube-dl.conf` and/or `~/.config/youtube-dl/config`. On Windows, the configuration file locations are `%APPDATA%\youtube-dl\config.txt` and `C:\Users\<Yourname>\youtube-dl.conf`.
|
||||||
|
|
||||||
# OUTPUT TEMPLATE
|
# OUTPUT TEMPLATE
|
||||||
|
|
||||||
@ -281,10 +281,12 @@ Videos can be filtered by their upload date using the options `--date`, `--dateb
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
$ # Download only the videos uploaded in the last 6 months
|
# Download only the videos uploaded in the last 6 months
|
||||||
$ youtube-dl --dateafter now-6months
|
$ youtube-dl --dateafter now-6months
|
||||||
$ # Download only the videos uploaded on January 1, 1970
|
|
||||||
|
# Download only the videos uploaded on January 1, 1970
|
||||||
$ youtube-dl --date 19700101
|
$ youtube-dl --date 19700101
|
||||||
|
|
||||||
$ # will only download the videos uploaded in the 200x decade
|
$ # will only download the videos uploaded in the 200x decade
|
||||||
$ youtube-dl --dateafter 20000101 --datebefore 20091231
|
$ youtube-dl --dateafter 20000101 --datebefore 20091231
|
||||||
|
|
||||||
@ -355,7 +357,7 @@ If you want to create a build of youtube-dl yourself, you'll need
|
|||||||
|
|
||||||
### Adding support for a new site
|
### Adding support for a new site
|
||||||
|
|
||||||
If you want to add support for a new site, copy *any* [recently modified](https://github.com/rg3/youtube-dl/commits/master/youtube_dl/extractor) file in `youtube_dl/extractor`, add an import in [`youtube_dl/extractor/__init__.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/__init__.py). Have a look at [`youtube_dl/common/extractor/common.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should return](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py#L38). Don't forget to run the tests with `python test/test_download.py Test_Download.test_YourExtractor`! For a detailed tutorial, refer to [this blog post](http://filippo.io/add-support-for-a-new-video-site-to-youtube-dl/).
|
If you want to add support for a new site, copy *any* [recently modified](https://github.com/rg3/youtube-dl/commits/master/youtube_dl/extractor) file in `youtube_dl/extractor`, add an import in [`youtube_dl/extractor/__init__.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/__init__.py). Have a look at [`youtube_dl/common/extractor/common.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should return](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py#L38). Don't forget to run the tests with `python test/test_download.py TestDownload.test_YourExtractor`! For a detailed tutorial, refer to [this blog post](http://filippo.io/add-support-for-a-new-video-site-to-youtube-dl/).
|
||||||
|
|
||||||
# BUGS
|
# BUGS
|
||||||
|
|
||||||
|
@ -14,9 +14,9 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
skip_tests=false
|
skip_tests=true
|
||||||
if [ "$1" = '--skip-test' ]; then
|
if [ "$1" = '--run-tests' ]; then
|
||||||
skip_tests=true
|
skip_tests=false
|
||||||
shift
|
shift
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
@ -68,6 +68,9 @@ class TestAllURLsMatching(unittest.TestCase):
|
|||||||
def test_youtube_show_matching(self):
|
def test_youtube_show_matching(self):
|
||||||
self.assertMatch('http://www.youtube.com/show/airdisasters', ['youtube:show'])
|
self.assertMatch('http://www.youtube.com/show/airdisasters', ['youtube:show'])
|
||||||
|
|
||||||
|
def test_youtube_truncated(self):
|
||||||
|
self.assertMatch('http://www.youtube.com/watch?', ['youtube:truncated_url'])
|
||||||
|
|
||||||
def test_justin_tv_channelid_matching(self):
|
def test_justin_tv_channelid_matching(self):
|
||||||
self.assertTrue(JustinTVIE.suitable(u"justin.tv/vanillatv"))
|
self.assertTrue(JustinTVIE.suitable(u"justin.tv/vanillatv"))
|
||||||
self.assertTrue(JustinTVIE.suitable(u"twitch.tv/vanillatv"))
|
self.assertTrue(JustinTVIE.suitable(u"twitch.tv/vanillatv"))
|
||||||
|
@ -18,6 +18,7 @@ from test.helper import (
|
|||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
import youtube_dl.YoutubeDL
|
import youtube_dl.YoutubeDL
|
||||||
@ -137,6 +138,15 @@ def generator(test_case):
|
|||||||
with io.open(info_json_fn, encoding='utf-8') as infof:
|
with io.open(info_json_fn, encoding='utf-8') as infof:
|
||||||
info_dict = json.load(infof)
|
info_dict = json.load(infof)
|
||||||
for (info_field, expected) in tc.get('info_dict', {}).items():
|
for (info_field, expected) in tc.get('info_dict', {}).items():
|
||||||
|
if isinstance(expected, compat_str) and expected.startswith('re:'):
|
||||||
|
got = info_dict.get(info_field)
|
||||||
|
match_str = expected[len('re:'):]
|
||||||
|
match_rex = re.compile(match_str)
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
isinstance(got, compat_str) and match_rex.match(got),
|
||||||
|
u'field %s (value: %r) should match %r' % (info_field, got, match_str))
|
||||||
|
else:
|
||||||
if isinstance(expected, compat_str) and expected.startswith('md5:'):
|
if isinstance(expected, compat_str) and expected.startswith('md5:'):
|
||||||
got = 'md5:' + md5(info_dict.get(info_field))
|
got = 'md5:' + md5(info_dict.get(info_field))
|
||||||
else:
|
else:
|
||||||
|
@ -55,10 +55,10 @@ class TestPlaylists(unittest.TestCase):
|
|||||||
def test_dailymotion_user(self):
|
def test_dailymotion_user(self):
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
ie = DailymotionUserIE(dl)
|
ie = DailymotionUserIE(dl)
|
||||||
result = ie.extract('http://www.dailymotion.com/user/generation-quoi/')
|
result = ie.extract('https://www.dailymotion.com/user/nqtv')
|
||||||
self.assertIsPlaylist(result)
|
self.assertIsPlaylist(result)
|
||||||
self.assertEqual(result['title'], 'Génération Quoi')
|
self.assertEqual(result['title'], 'Rémi Gaillard')
|
||||||
self.assertTrue(len(result['entries']) >= 26)
|
self.assertTrue(len(result['entries']) >= 100)
|
||||||
|
|
||||||
def test_vimeo_channel(self):
|
def test_vimeo_channel(self):
|
||||||
dl = FakeYDL()
|
dl = FakeYDL()
|
||||||
@ -250,5 +250,14 @@ class TestPlaylists(unittest.TestCase):
|
|||||||
self.assertEqual(result['title'], 'python language')
|
self.assertEqual(result['title'], 'python language')
|
||||||
self.assertTrue(len(result['entries']) == 15)
|
self.assertTrue(len(result['entries']) == 15)
|
||||||
|
|
||||||
|
def test_generic_rss_feed(self):
|
||||||
|
dl = FakeYDL()
|
||||||
|
ie = GenericIE(dl)
|
||||||
|
result = ie.extract('http://www.escapistmagazine.com/rss/videos/list/1.xml')
|
||||||
|
self.assertIsPlaylist(result)
|
||||||
|
self.assertEqual(result['id'], 'http://www.escapistmagazine.com/rss/videos/list/1.xml')
|
||||||
|
self.assertEqual(result['title'], 'Zero Punctuation')
|
||||||
|
self.assertTrue(len(result['entries']) > 10)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -25,6 +25,7 @@ from youtube_dl.utils import (
|
|||||||
shell_quote,
|
shell_quote,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
str_to_int,
|
str_to_int,
|
||||||
|
struct_unpack,
|
||||||
timeconvert,
|
timeconvert,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
unified_strdate,
|
unified_strdate,
|
||||||
@ -201,7 +202,16 @@ class TestUtil(unittest.TestCase):
|
|||||||
self.assertEqual(parse_duration('1'), 1)
|
self.assertEqual(parse_duration('1'), 1)
|
||||||
self.assertEqual(parse_duration('1337:12'), 80232)
|
self.assertEqual(parse_duration('1337:12'), 80232)
|
||||||
self.assertEqual(parse_duration('9:12:43'), 33163)
|
self.assertEqual(parse_duration('9:12:43'), 33163)
|
||||||
|
self.assertEqual(parse_duration('12:00'), 720)
|
||||||
|
self.assertEqual(parse_duration('00:01:01'), 61)
|
||||||
self.assertEqual(parse_duration('x:y'), None)
|
self.assertEqual(parse_duration('x:y'), None)
|
||||||
|
self.assertEqual(parse_duration('3h11m53s'), 11513)
|
||||||
|
self.assertEqual(parse_duration('62m45s'), 3765)
|
||||||
|
self.assertEqual(parse_duration('6m59s'), 419)
|
||||||
|
self.assertEqual(parse_duration('49s'), 49)
|
||||||
|
self.assertEqual(parse_duration('0h0m0s'), 0)
|
||||||
|
self.assertEqual(parse_duration('0m0s'), 0)
|
||||||
|
self.assertEqual(parse_duration('0s'), 0)
|
||||||
|
|
||||||
def test_fix_xml_ampersands(self):
|
def test_fix_xml_ampersands(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -237,5 +247,8 @@ class TestUtil(unittest.TestCase):
|
|||||||
testPL(5, 2, (2, 99), [2, 3, 4])
|
testPL(5, 2, (2, 99), [2, 3, 4])
|
||||||
testPL(5, 2, (20, 99), [])
|
testPL(5, 2, (20, 99), [])
|
||||||
|
|
||||||
|
def test_struct_unpack(self):
|
||||||
|
self.assertEqual(struct_unpack(u'!B', b'\x00'), (0,))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -41,7 +41,11 @@ __authors__ = (
|
|||||||
'Chris Gahan',
|
'Chris Gahan',
|
||||||
'Saimadhav Heblikar',
|
'Saimadhav Heblikar',
|
||||||
'Mike Col',
|
'Mike Col',
|
||||||
|
'Oleg Prutz',
|
||||||
|
'pulpe',
|
||||||
'Andreas Schmitz',
|
'Andreas Schmitz',
|
||||||
|
'Michael Kaiser',
|
||||||
|
'Niklas Laxström',
|
||||||
)
|
)
|
||||||
|
|
||||||
__license__ = 'Public Domain'
|
__license__ = 'Public Domain'
|
||||||
@ -204,7 +208,7 @@ def parseOpts(overrideArguments=None):
|
|||||||
general.add_option('-U', '--update',
|
general.add_option('-U', '--update',
|
||||||
action='store_true', dest='update_self', help='update this program to latest version. Make sure that you have sufficient permissions (run with sudo if needed)')
|
action='store_true', dest='update_self', help='update this program to latest version. Make sure that you have sufficient permissions (run with sudo if needed)')
|
||||||
general.add_option('-i', '--ignore-errors',
|
general.add_option('-i', '--ignore-errors',
|
||||||
action='store_true', dest='ignoreerrors', help='continue on download errors, for example to to skip unavailable videos in a playlist', default=False)
|
action='store_true', dest='ignoreerrors', help='continue on download errors, for example to skip unavailable videos in a playlist', default=False)
|
||||||
general.add_option('--abort-on-error',
|
general.add_option('--abort-on-error',
|
||||||
action='store_false', dest='ignoreerrors',
|
action='store_false', dest='ignoreerrors',
|
||||||
help='Abort downloading of further videos (in the playlist or the command line) if an error occurs')
|
help='Abort downloading of further videos (in the playlist or the command line) if an error occurs')
|
||||||
|
@ -5,6 +5,7 @@ from .hls import HlsFD
|
|||||||
from .http import HttpFD
|
from .http import HttpFD
|
||||||
from .mplayer import MplayerFD
|
from .mplayer import MplayerFD
|
||||||
from .rtmp import RtmpFD
|
from .rtmp import RtmpFD
|
||||||
|
from .f4m import F4mFD
|
||||||
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
determine_ext,
|
determine_ext,
|
||||||
@ -22,5 +23,7 @@ def get_suitable_downloader(info_dict):
|
|||||||
return HlsFD
|
return HlsFD
|
||||||
if url.startswith('mms') or url.startswith('rtsp'):
|
if url.startswith('mms') or url.startswith('rtsp'):
|
||||||
return MplayerFD
|
return MplayerFD
|
||||||
|
if determine_ext(url) == 'f4m':
|
||||||
|
return F4mFD
|
||||||
else:
|
else:
|
||||||
return HttpFD
|
return HttpFD
|
||||||
|
315
youtube_dl/downloader/f4m.py
Normal file
315
youtube_dl/downloader/f4m.py
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import itertools
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
from .common import FileDownloader
|
||||||
|
from .http import HttpFD
|
||||||
|
from ..utils import (
|
||||||
|
struct_pack,
|
||||||
|
struct_unpack,
|
||||||
|
compat_urllib_request,
|
||||||
|
compat_urlparse,
|
||||||
|
format_bytes,
|
||||||
|
encodeFilename,
|
||||||
|
sanitize_open,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FlvReader(io.BytesIO):
|
||||||
|
"""
|
||||||
|
Reader for Flv files
|
||||||
|
The file format is documented in https://www.adobe.com/devnet/f4v.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Utility functions for reading numbers and strings
|
||||||
|
def read_unsigned_long_long(self):
|
||||||
|
return struct_unpack('!Q', self.read(8))[0]
|
||||||
|
|
||||||
|
def read_unsigned_int(self):
|
||||||
|
return struct_unpack('!I', self.read(4))[0]
|
||||||
|
|
||||||
|
def read_unsigned_char(self):
|
||||||
|
return struct_unpack('!B', self.read(1))[0]
|
||||||
|
|
||||||
|
def read_string(self):
|
||||||
|
res = b''
|
||||||
|
while True:
|
||||||
|
char = self.read(1)
|
||||||
|
if char == b'\x00':
|
||||||
|
break
|
||||||
|
res += char
|
||||||
|
return res
|
||||||
|
|
||||||
|
def read_box_info(self):
|
||||||
|
"""
|
||||||
|
Read a box and return the info as a tuple: (box_size, box_type, box_data)
|
||||||
|
"""
|
||||||
|
real_size = size = self.read_unsigned_int()
|
||||||
|
box_type = self.read(4)
|
||||||
|
header_end = 8
|
||||||
|
if size == 1:
|
||||||
|
real_size = self.read_unsigned_long_long()
|
||||||
|
header_end = 16
|
||||||
|
return real_size, box_type, self.read(real_size-header_end)
|
||||||
|
|
||||||
|
def read_asrt(self):
|
||||||
|
# version
|
||||||
|
self.read_unsigned_char()
|
||||||
|
# flags
|
||||||
|
self.read(3)
|
||||||
|
quality_entry_count = self.read_unsigned_char()
|
||||||
|
# QualityEntryCount
|
||||||
|
for i in range(quality_entry_count):
|
||||||
|
self.read_string()
|
||||||
|
|
||||||
|
segment_run_count = self.read_unsigned_int()
|
||||||
|
segments = []
|
||||||
|
for i in range(segment_run_count):
|
||||||
|
first_segment = self.read_unsigned_int()
|
||||||
|
fragments_per_segment = self.read_unsigned_int()
|
||||||
|
segments.append((first_segment, fragments_per_segment))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'segment_run': segments,
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_afrt(self):
|
||||||
|
# version
|
||||||
|
self.read_unsigned_char()
|
||||||
|
# flags
|
||||||
|
self.read(3)
|
||||||
|
# time scale
|
||||||
|
self.read_unsigned_int()
|
||||||
|
|
||||||
|
quality_entry_count = self.read_unsigned_char()
|
||||||
|
# QualitySegmentUrlModifiers
|
||||||
|
for i in range(quality_entry_count):
|
||||||
|
self.read_string()
|
||||||
|
|
||||||
|
fragments_count = self.read_unsigned_int()
|
||||||
|
fragments = []
|
||||||
|
for i in range(fragments_count):
|
||||||
|
first = self.read_unsigned_int()
|
||||||
|
first_ts = self.read_unsigned_long_long()
|
||||||
|
duration = self.read_unsigned_int()
|
||||||
|
if duration == 0:
|
||||||
|
discontinuity_indicator = self.read_unsigned_char()
|
||||||
|
else:
|
||||||
|
discontinuity_indicator = None
|
||||||
|
fragments.append({
|
||||||
|
'first': first,
|
||||||
|
'ts': first_ts,
|
||||||
|
'duration': duration,
|
||||||
|
'discontinuity_indicator': discontinuity_indicator,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
'fragments': fragments,
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_abst(self):
|
||||||
|
# version
|
||||||
|
self.read_unsigned_char()
|
||||||
|
# flags
|
||||||
|
self.read(3)
|
||||||
|
# BootstrapinfoVersion
|
||||||
|
bootstrap_info_version = self.read_unsigned_int()
|
||||||
|
# Profile,Live,Update,Reserved
|
||||||
|
self.read(1)
|
||||||
|
# time scale
|
||||||
|
self.read_unsigned_int()
|
||||||
|
# CurrentMediaTime
|
||||||
|
self.read_unsigned_long_long()
|
||||||
|
# SmpteTimeCodeOffset
|
||||||
|
self.read_unsigned_long_long()
|
||||||
|
# MovieIdentifier
|
||||||
|
movie_identifier = self.read_string()
|
||||||
|
server_count = self.read_unsigned_char()
|
||||||
|
# ServerEntryTable
|
||||||
|
for i in range(server_count):
|
||||||
|
self.read_string()
|
||||||
|
quality_count = self.read_unsigned_char()
|
||||||
|
# QualityEntryTable
|
||||||
|
for i in range(server_count):
|
||||||
|
self.read_string()
|
||||||
|
# DrmData
|
||||||
|
self.read_string()
|
||||||
|
# MetaData
|
||||||
|
self.read_string()
|
||||||
|
|
||||||
|
segments_count = self.read_unsigned_char()
|
||||||
|
segments = []
|
||||||
|
for i in range(segments_count):
|
||||||
|
box_size, box_type, box_data = self.read_box_info()
|
||||||
|
assert box_type == b'asrt'
|
||||||
|
segment = FlvReader(box_data).read_asrt()
|
||||||
|
segments.append(segment)
|
||||||
|
fragments_run_count = self.read_unsigned_char()
|
||||||
|
fragments = []
|
||||||
|
for i in range(fragments_run_count):
|
||||||
|
box_size, box_type, box_data = self.read_box_info()
|
||||||
|
assert box_type == b'afrt'
|
||||||
|
fragments.append(FlvReader(box_data).read_afrt())
|
||||||
|
|
||||||
|
return {
|
||||||
|
'segments': segments,
|
||||||
|
'fragments': fragments,
|
||||||
|
}
|
||||||
|
|
||||||
|
def read_bootstrap_info(self):
|
||||||
|
total_size, box_type, box_data = self.read_box_info()
|
||||||
|
assert box_type == b'abst'
|
||||||
|
return FlvReader(box_data).read_abst()
|
||||||
|
|
||||||
|
|
||||||
|
def read_bootstrap_info(bootstrap_bytes):
|
||||||
|
return FlvReader(bootstrap_bytes).read_bootstrap_info()
|
||||||
|
|
||||||
|
|
||||||
|
def build_fragments_list(boot_info):
|
||||||
|
""" Return a list of (segment, fragment) for each fragment in the video """
|
||||||
|
res = []
|
||||||
|
segment_run_table = boot_info['segments'][0]
|
||||||
|
# I've only found videos with one segment
|
||||||
|
segment_run_entry = segment_run_table['segment_run'][0]
|
||||||
|
n_frags = segment_run_entry[1]
|
||||||
|
fragment_run_entry_table = boot_info['fragments'][0]['fragments']
|
||||||
|
first_frag_number = fragment_run_entry_table[0]['first']
|
||||||
|
for (i, frag_number) in zip(range(1, n_frags+1), itertools.count(first_frag_number)):
|
||||||
|
res.append((1, frag_number))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def write_flv_header(stream, metadata):
|
||||||
|
"""Writes the FLV header and the metadata to stream"""
|
||||||
|
# FLV header
|
||||||
|
stream.write(b'FLV\x01')
|
||||||
|
stream.write(b'\x05')
|
||||||
|
stream.write(b'\x00\x00\x00\x09')
|
||||||
|
# FLV File body
|
||||||
|
stream.write(b'\x00\x00\x00\x00')
|
||||||
|
# FLVTAG
|
||||||
|
# Script data
|
||||||
|
stream.write(b'\x12')
|
||||||
|
# Size of the metadata with 3 bytes
|
||||||
|
stream.write(struct_pack('!L', len(metadata))[1:])
|
||||||
|
stream.write(b'\x00\x00\x00\x00\x00\x00\x00')
|
||||||
|
stream.write(metadata)
|
||||||
|
# Magic numbers extracted from the output files produced by AdobeHDS.php
|
||||||
|
#(https://github.com/K-S-V/Scripts)
|
||||||
|
stream.write(b'\x00\x00\x01\x73')
|
||||||
|
|
||||||
|
|
||||||
|
def _add_ns(prop):
|
||||||
|
return '{http://ns.adobe.com/f4m/1.0}%s' % prop
|
||||||
|
|
||||||
|
|
||||||
|
class HttpQuietDownloader(HttpFD):
|
||||||
|
def to_screen(self, *args, **kargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class F4mFD(FileDownloader):
|
||||||
|
"""
|
||||||
|
A downloader for f4m manifests or AdobeHDS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def real_download(self, filename, info_dict):
|
||||||
|
man_url = info_dict['url']
|
||||||
|
self.to_screen('[download] Downloading f4m manifest')
|
||||||
|
manifest = self.ydl.urlopen(man_url).read()
|
||||||
|
self.report_destination(filename)
|
||||||
|
http_dl = HttpQuietDownloader(self.ydl,
|
||||||
|
{
|
||||||
|
'continuedl': True,
|
||||||
|
'quiet': True,
|
||||||
|
'noprogress': True,
|
||||||
|
'test': self.params.get('test', False),
|
||||||
|
})
|
||||||
|
|
||||||
|
doc = etree.fromstring(manifest)
|
||||||
|
formats = [(int(f.attrib.get('bitrate', -1)), f) for f in doc.findall(_add_ns('media'))]
|
||||||
|
formats = sorted(formats, key=lambda f: f[0])
|
||||||
|
rate, media = formats[-1]
|
||||||
|
base_url = compat_urlparse.urljoin(man_url, media.attrib['url'])
|
||||||
|
bootstrap = base64.b64decode(doc.find(_add_ns('bootstrapInfo')).text)
|
||||||
|
metadata = base64.b64decode(media.find(_add_ns('metadata')).text)
|
||||||
|
boot_info = read_bootstrap_info(bootstrap)
|
||||||
|
fragments_list = build_fragments_list(boot_info)
|
||||||
|
if self.params.get('test', False):
|
||||||
|
# We only download the first fragment
|
||||||
|
fragments_list = fragments_list[:1]
|
||||||
|
total_frags = len(fragments_list)
|
||||||
|
|
||||||
|
tmpfilename = self.temp_name(filename)
|
||||||
|
(dest_stream, tmpfilename) = sanitize_open(tmpfilename, 'wb')
|
||||||
|
write_flv_header(dest_stream, metadata)
|
||||||
|
|
||||||
|
# This dict stores the download progress, it's updated by the progress
|
||||||
|
# hook
|
||||||
|
state = {
|
||||||
|
'downloaded_bytes': 0,
|
||||||
|
'frag_counter': 0,
|
||||||
|
}
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
def frag_progress_hook(status):
|
||||||
|
frag_total_bytes = status.get('total_bytes', 0)
|
||||||
|
estimated_size = (state['downloaded_bytes'] +
|
||||||
|
(total_frags - state['frag_counter']) * frag_total_bytes)
|
||||||
|
if status['status'] == 'finished':
|
||||||
|
state['downloaded_bytes'] += frag_total_bytes
|
||||||
|
state['frag_counter'] += 1
|
||||||
|
progress = self.calc_percent(state['frag_counter'], total_frags)
|
||||||
|
byte_counter = state['downloaded_bytes']
|
||||||
|
else:
|
||||||
|
frag_downloaded_bytes = status['downloaded_bytes']
|
||||||
|
byte_counter = state['downloaded_bytes'] + frag_downloaded_bytes
|
||||||
|
frag_progress = self.calc_percent(frag_downloaded_bytes,
|
||||||
|
frag_total_bytes)
|
||||||
|
progress = self.calc_percent(state['frag_counter'], total_frags)
|
||||||
|
progress += frag_progress / float(total_frags)
|
||||||
|
|
||||||
|
eta = self.calc_eta(start, time.time(), estimated_size, byte_counter)
|
||||||
|
self.report_progress(progress, format_bytes(estimated_size),
|
||||||
|
status.get('speed'), eta)
|
||||||
|
http_dl.add_progress_hook(frag_progress_hook)
|
||||||
|
|
||||||
|
frags_filenames = []
|
||||||
|
for (seg_i, frag_i) in fragments_list:
|
||||||
|
name = 'Seg%d-Frag%d' % (seg_i, frag_i)
|
||||||
|
url = base_url + name
|
||||||
|
frag_filename = '%s-%s' % (tmpfilename, name)
|
||||||
|
success = http_dl.download(frag_filename, {'url': url})
|
||||||
|
if not success:
|
||||||
|
return False
|
||||||
|
with open(frag_filename, 'rb') as down:
|
||||||
|
down_data = down.read()
|
||||||
|
reader = FlvReader(down_data)
|
||||||
|
while True:
|
||||||
|
_, box_type, box_data = reader.read_box_info()
|
||||||
|
if box_type == b'mdat':
|
||||||
|
dest_stream.write(box_data)
|
||||||
|
break
|
||||||
|
frags_filenames.append(frag_filename)
|
||||||
|
|
||||||
|
self.report_finish(format_bytes(state['downloaded_bytes']), time.time() - start)
|
||||||
|
|
||||||
|
self.try_rename(tmpfilename, filename)
|
||||||
|
for frag_file in frags_filenames:
|
||||||
|
os.remove(frag_file)
|
||||||
|
|
||||||
|
fsize = os.path.getsize(encodeFilename(filename))
|
||||||
|
self._hook_progress({
|
||||||
|
'downloaded_bytes': fsize,
|
||||||
|
'total_bytes': fsize,
|
||||||
|
'filename': filename,
|
||||||
|
'status': 'finished',
|
||||||
|
})
|
||||||
|
|
||||||
|
return True
|
@ -73,6 +73,7 @@ from .fktv import (
|
|||||||
FKTVPosteckeIE,
|
FKTVPosteckeIE,
|
||||||
)
|
)
|
||||||
from .flickr import FlickrIE
|
from .flickr import FlickrIE
|
||||||
|
from .fourtube import FourTubeIE
|
||||||
from .franceinter import FranceInterIE
|
from .franceinter import FranceInterIE
|
||||||
from .francetv import (
|
from .francetv import (
|
||||||
PluzzIE,
|
PluzzIE,
|
||||||
@ -91,6 +92,7 @@ from .generic import GenericIE
|
|||||||
from .googleplus import GooglePlusIE
|
from .googleplus import GooglePlusIE
|
||||||
from .googlesearch import GoogleSearchIE
|
from .googlesearch import GoogleSearchIE
|
||||||
from .hark import HarkIE
|
from .hark import HarkIE
|
||||||
|
from .helsinki import HelsinkiIE
|
||||||
from .hotnewhiphop import HotNewHipHopIE
|
from .hotnewhiphop import HotNewHipHopIE
|
||||||
from .howcast import HowcastIE
|
from .howcast import HowcastIE
|
||||||
from .huffpost import HuffPostIE
|
from .huffpost import HuffPostIE
|
||||||
@ -184,6 +186,7 @@ from .rutube import (
|
|||||||
RutubeMovieIE,
|
RutubeMovieIE,
|
||||||
RutubePersonIE,
|
RutubePersonIE,
|
||||||
)
|
)
|
||||||
|
from .savefrom import SaveFromIE
|
||||||
from .servingsys import ServingSysIE
|
from .servingsys import ServingSysIE
|
||||||
from .sina import SinaIE
|
from .sina import SinaIE
|
||||||
from .slashdot import SlashdotIE
|
from .slashdot import SlashdotIE
|
||||||
@ -209,10 +212,12 @@ from .statigram import StatigramIE
|
|||||||
from .steam import SteamIE
|
from .steam import SteamIE
|
||||||
from .streamcloud import StreamcloudIE
|
from .streamcloud import StreamcloudIE
|
||||||
from .streamcz import StreamCZIE
|
from .streamcz import StreamCZIE
|
||||||
|
from .syfy import SyfyIE
|
||||||
from .sztvhu import SztvHuIE
|
from .sztvhu import SztvHuIE
|
||||||
from .teamcoco import TeamcocoIE
|
from .teamcoco import TeamcocoIE
|
||||||
from .techtalks import TechTalksIE
|
from .techtalks import TechTalksIE
|
||||||
from .ted import TEDIE
|
from .ted import TEDIE
|
||||||
|
from .testurl import TestURLIE
|
||||||
from .tf1 import TF1IE
|
from .tf1 import TF1IE
|
||||||
from .theplatform import ThePlatformIE
|
from .theplatform import ThePlatformIE
|
||||||
from .thisav import ThisAVIE
|
from .thisav import ThisAVIE
|
||||||
@ -220,6 +225,7 @@ from .tinypic import TinyPicIE
|
|||||||
from .toutv import TouTvIE
|
from .toutv import TouTvIE
|
||||||
from .traileraddict import TrailerAddictIE
|
from .traileraddict import TrailerAddictIE
|
||||||
from .trilulilu import TriluliluIE
|
from .trilulilu import TriluliluIE
|
||||||
|
from .trutube import TruTubeIE
|
||||||
from .tube8 import Tube8IE
|
from .tube8 import Tube8IE
|
||||||
from .tudou import TudouIE
|
from .tudou import TudouIE
|
||||||
from .tumblr import TumblrIE
|
from .tumblr import TumblrIE
|
||||||
|
@ -13,13 +13,13 @@ class BBCCoUkIE(SubtitlesInfoExtractor):
|
|||||||
|
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'http://www.bbc.co.uk/programmes/p01q7wz1',
|
'url': 'http://www.bbc.co.uk/programmes/b039g8p7',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': 'p01q7wz4',
|
'id': 'b039d07m',
|
||||||
'ext': 'flv',
|
'ext': 'flv',
|
||||||
'title': 'Friction: Blu Mar Ten guest mix: Blu Mar Ten - Guest Mix',
|
'title': 'Kaleidoscope: Leonard Cohen',
|
||||||
'description': 'Blu Mar Ten deliver a Guest Mix for Friction.',
|
'description': 'md5:db4755d7a665ae72343779f7dacb402c',
|
||||||
'duration': 1936,
|
'duration': 1740,
|
||||||
},
|
},
|
||||||
'params': {
|
'params': {
|
||||||
# rtmp download
|
# rtmp download
|
||||||
@ -38,7 +38,8 @@ class BBCCoUkIE(SubtitlesInfoExtractor):
|
|||||||
'params': {
|
'params': {
|
||||||
# rtmp download
|
# rtmp download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
}
|
},
|
||||||
|
'skip': 'Episode is no longer available on BBC iPlayer Radio',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://www.bbc.co.uk/iplayer/episode/b03vhd1f/The_Voice_UK_Series_3_Blind_Auditions_5/',
|
'url': 'http://www.bbc.co.uk/iplayer/episode/b03vhd1f/The_Voice_UK_Series_3_Blind_Auditions_5/',
|
||||||
@ -161,6 +162,11 @@ class BBCCoUkIE(SubtitlesInfoExtractor):
|
|||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
group_id = mobj.group('id')
|
group_id = mobj.group('id')
|
||||||
|
|
||||||
|
webpage = self._download_webpage(url, group_id, 'Downloading video page')
|
||||||
|
if re.search(r'id="emp-error" class="notinuk">', webpage):
|
||||||
|
raise ExtractorError('Currently BBC iPlayer TV programmes are available to play in the UK only',
|
||||||
|
expected=True)
|
||||||
|
|
||||||
playlist = self._download_xml('http://www.bbc.co.uk/iplayer/playlist/%s' % group_id, group_id,
|
playlist = self._download_xml('http://www.bbc.co.uk/iplayer/playlist/%s' % group_id, group_id,
|
||||||
'Downloading playlist XML')
|
'Downloading playlist XML')
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
@ -9,11 +11,12 @@ class Canalc2IE(InfoExtractor):
|
|||||||
_VALID_URL = r'http://.*?\.canalc2\.tv/video\.asp\?.*?idVideo=(?P<id>\d+)'
|
_VALID_URL = r'http://.*?\.canalc2\.tv/video\.asp\?.*?idVideo=(?P<id>\d+)'
|
||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
u'url': u'http://www.canalc2.tv/video.asp?idVideo=12163&voir=oui',
|
'url': 'http://www.canalc2.tv/video.asp?idVideo=12163&voir=oui',
|
||||||
u'file': u'12163.mp4',
|
'md5': '060158428b650f896c542dfbb3d6487f',
|
||||||
u'md5': u'060158428b650f896c542dfbb3d6487f',
|
'info_dict': {
|
||||||
u'info_dict': {
|
'id': '12163',
|
||||||
u'title': u'Terrasses du Numérique'
|
'ext': 'mp4',
|
||||||
|
'title': 'Terrasses du Numérique'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,9 +31,10 @@ class Canalc2IE(InfoExtractor):
|
|||||||
video_url = 'http://vod-flash.u-strasbg.fr:8080/' + file_name
|
video_url = 'http://vod-flash.u-strasbg.fr:8080/' + file_name
|
||||||
|
|
||||||
title = self._html_search_regex(
|
title = self._html_search_regex(
|
||||||
r'class="evenement8">(.*?)</a>', webpage, u'title')
|
r'class="evenement8">(.*?)</a>', webpage, 'title')
|
||||||
|
|
||||||
return {'id': video_id,
|
return {
|
||||||
|
'id': video_id,
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
'title': title,
|
'title': title,
|
||||||
|
95
youtube_dl/extractor/fourtube.py
Normal file
95
youtube_dl/extractor/fourtube.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
compat_urllib_request,
|
||||||
|
unified_strdate,
|
||||||
|
str_to_int,
|
||||||
|
parse_duration,
|
||||||
|
)
|
||||||
|
from youtube_dl.utils import clean_html
|
||||||
|
|
||||||
|
|
||||||
|
class FourTubeIE(InfoExtractor):
|
||||||
|
IE_NAME = '4tube'
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?4tube\.com/videos/(?P<id>\d+)'
|
||||||
|
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://www.4tube.com/videos/209733/hot-babe-holly-michaels-gets-her-ass-stuffed-by-black',
|
||||||
|
'md5': '6516c8ac63b03de06bc8eac14362db4f',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '209733',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Hot Babe Holly Michaels gets her ass stuffed by black',
|
||||||
|
'uploader': 'WCP Club',
|
||||||
|
'uploader_id': 'wcp-club',
|
||||||
|
'upload_date': '20131031',
|
||||||
|
'duration': 583,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
|
||||||
|
video_id = mobj.group('id')
|
||||||
|
webpage_url = 'http://www.4tube.com/videos/' + video_id
|
||||||
|
webpage = self._download_webpage(webpage_url, video_id)
|
||||||
|
|
||||||
|
self.report_extraction(video_id)
|
||||||
|
|
||||||
|
playlist_json = self._html_search_regex(r'var playerConfigPlaylist\s+=\s+([^;]+)', webpage, 'Playlist')
|
||||||
|
media_id = self._search_regex(r'idMedia:\s*(\d+)', playlist_json, 'Media Id')
|
||||||
|
sources = self._search_regex(r'sources:\s*\[([^\]]*)\]', playlist_json, 'Sources').split(',')
|
||||||
|
title = self._search_regex(r'title:\s*"([^"]*)', playlist_json, 'Title')
|
||||||
|
thumbnail_url = self._search_regex(r'image:\s*"([^"]*)', playlist_json, 'Thumbnail', fatal=False)
|
||||||
|
|
||||||
|
uploader_str = self._search_regex(r'<span>Uploaded by</span>(.*?)<span>', webpage, 'uploader', fatal=False)
|
||||||
|
mobj = re.search(r'<a href="/sites/(?P<id>[^"]+)"><strong>(?P<name>[^<]+)</strong></a>', uploader_str)
|
||||||
|
(uploader, uploader_id) = (mobj.group('name'), mobj.group('id')) if mobj else (clean_html(uploader_str), None)
|
||||||
|
|
||||||
|
upload_date = None
|
||||||
|
view_count = None
|
||||||
|
duration = None
|
||||||
|
description = self._html_search_meta('description', webpage, 'description')
|
||||||
|
if description:
|
||||||
|
upload_date = self._search_regex(r'Published Date: (\d{2} [a-zA-Z]{3} \d{4})', description, 'upload date',
|
||||||
|
fatal=False)
|
||||||
|
if upload_date:
|
||||||
|
upload_date = unified_strdate(upload_date)
|
||||||
|
view_count = self._search_regex(r'Views: ([\d,\.]+)', description, 'view count', fatal=False)
|
||||||
|
if view_count:
|
||||||
|
view_count = str_to_int(view_count)
|
||||||
|
duration = parse_duration(self._search_regex(r'Length: (\d+m\d+s)', description, 'duration', fatal=False))
|
||||||
|
|
||||||
|
token_url = "http://tkn.4tube.com/{0}/desktop/{1}".format(media_id, "+".join(sources))
|
||||||
|
headers = {
|
||||||
|
b'Content-Type': b'application/x-www-form-urlencoded',
|
||||||
|
b'Origin': b'http://www.4tube.com',
|
||||||
|
}
|
||||||
|
token_req = compat_urllib_request.Request(token_url, b'{}', headers)
|
||||||
|
tokens = self._download_json(token_req, video_id)
|
||||||
|
|
||||||
|
formats = [{
|
||||||
|
'url': tokens[format]['token'],
|
||||||
|
'format_id': format + 'p',
|
||||||
|
'resolution': format + 'p',
|
||||||
|
'quality': int(format),
|
||||||
|
} for format in sources]
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnail': thumbnail_url,
|
||||||
|
'uploader': uploader,
|
||||||
|
'uploader_id': uploader_id,
|
||||||
|
'upload_date': upload_date,
|
||||||
|
'view_count': view_count,
|
||||||
|
'duration': duration,
|
||||||
|
'age_limit': 18,
|
||||||
|
'webpage_url': webpage_url,
|
||||||
|
}
|
@ -184,6 +184,7 @@ class GenerationQuoiIE(InfoExtractor):
|
|||||||
# It uses Dailymotion
|
# It uses Dailymotion
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'skip': 'Only available from France',
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import xml.etree.ElementTree
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from .youtube import YoutubeIE
|
from .youtube import YoutubeIE
|
||||||
@ -12,6 +13,7 @@ from ..utils import (
|
|||||||
compat_urllib_parse,
|
compat_urllib_parse,
|
||||||
compat_urllib_request,
|
compat_urllib_request,
|
||||||
compat_urlparse,
|
compat_urlparse,
|
||||||
|
compat_xml_parse_error,
|
||||||
|
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
HEADRequest,
|
HEADRequest,
|
||||||
@ -159,6 +161,25 @@ class GenericIE(InfoExtractor):
|
|||||||
raise ExtractorError('Invalid URL protocol')
|
raise ExtractorError('Invalid URL protocol')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def _extract_rss(self, url, video_id, doc):
|
||||||
|
playlist_title = doc.find('./channel/title').text
|
||||||
|
playlist_desc_el = doc.find('./channel/description')
|
||||||
|
playlist_desc = None if playlist_desc_el is None else playlist_desc_el.text
|
||||||
|
|
||||||
|
entries = [{
|
||||||
|
'_type': 'url',
|
||||||
|
'url': e.find('link').text,
|
||||||
|
'title': e.find('title').text,
|
||||||
|
} for e in doc.findall('./channel/item')]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'playlist',
|
||||||
|
'id': url,
|
||||||
|
'title': playlist_title,
|
||||||
|
'description': playlist_desc,
|
||||||
|
'entries': entries,
|
||||||
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
parsed_url = compat_urlparse.urlparse(url)
|
parsed_url = compat_urlparse.urlparse(url)
|
||||||
if not parsed_url.scheme:
|
if not parsed_url.scheme:
|
||||||
@ -219,6 +240,14 @@ class GenericIE(InfoExtractor):
|
|||||||
|
|
||||||
self.report_extraction(video_id)
|
self.report_extraction(video_id)
|
||||||
|
|
||||||
|
# Is it an RSS feed?
|
||||||
|
try:
|
||||||
|
doc = xml.etree.ElementTree.fromstring(webpage.encode('utf-8'))
|
||||||
|
if doc.tag == 'rss':
|
||||||
|
return self._extract_rss(url, video_id, doc)
|
||||||
|
except compat_xml_parse_error:
|
||||||
|
pass
|
||||||
|
|
||||||
# it's tempting to parse this further, but you would
|
# it's tempting to parse this further, but you would
|
||||||
# have to take into account all the variations like
|
# have to take into account all the variations like
|
||||||
# Video Title - Site Name
|
# Video Title - Site Name
|
||||||
|
62
youtube_dl/extractor/helsinki.py
Normal file
62
youtube_dl/extractor/helsinki.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class HelsinkiIE(InfoExtractor):
|
||||||
|
IE_DESC = 'helsinki.fi'
|
||||||
|
_VALID_URL = r'https?://video\.helsinki\.fi/Arkisto/flash\.php\?id=(?P<id>\d+)'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://video.helsinki.fi/Arkisto/flash.php?id=20258',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '20258',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'Tietotekniikkafoorumi-iltapäivä',
|
||||||
|
'description': 'md5:f5c904224d43c133225130fe156a5ee0',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True, # RTMP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
video_id = mobj.group('id')
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
formats = []
|
||||||
|
|
||||||
|
mobj = re.search(r'file=((\w+):[^&]+)', webpage)
|
||||||
|
if mobj:
|
||||||
|
formats.append({
|
||||||
|
'ext': mobj.group(2),
|
||||||
|
'play_path': mobj.group(1),
|
||||||
|
'url': 'rtmp://flashvideo.it.helsinki.fi/vod/',
|
||||||
|
'player_url': 'http://video.helsinki.fi/player.swf',
|
||||||
|
'format_note': 'sd',
|
||||||
|
'quality': 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
mobj = re.search(r'hd\.file=((\w+):[^&]+)', webpage)
|
||||||
|
if mobj:
|
||||||
|
formats.append({
|
||||||
|
'ext': mobj.group(2),
|
||||||
|
'play_path': mobj.group(1),
|
||||||
|
'url': 'rtmp://flashvideo.it.helsinki.fi/vod/',
|
||||||
|
'player_url': 'http://video.helsinki.fi/player.swf',
|
||||||
|
'format_note': 'hd',
|
||||||
|
'quality': 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': self._og_search_title(webpage).replace('Video: ', ''),
|
||||||
|
'description': self._og_search_description(webpage),
|
||||||
|
'thumbnail': self._og_search_thumbnail(webpage),
|
||||||
|
'formats': formats,
|
||||||
|
}
|
@ -4,15 +4,17 @@ import json
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
|
from ..utils import int_or_none
|
||||||
|
|
||||||
|
|
||||||
class LiveLeakIE(InfoExtractor):
|
class LiveLeakIE(InfoExtractor):
|
||||||
_VALID_URL = r'^(?:http://)?(?:\w+\.)?liveleak\.com/view\?(?:.*?)i=(?P<video_id>[\w_]+)(?:.*)'
|
_VALID_URL = r'^(?:http://)?(?:\w+\.)?liveleak\.com/view\?(?:.*?)i=(?P<video_id>[\w_]+)(?:.*)'
|
||||||
_TESTS = [{
|
_TESTS = [{
|
||||||
'url': 'http://www.liveleak.com/view?i=757_1364311680',
|
'url': 'http://www.liveleak.com/view?i=757_1364311680',
|
||||||
'file': '757_1364311680.mp4',
|
|
||||||
'md5': '0813c2430bea7a46bf13acf3406992f4',
|
'md5': '0813c2430bea7a46bf13acf3406992f4',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
'id': '757_1364311680',
|
||||||
|
'ext': 'mp4',
|
||||||
'description': 'extremely bad day for this guy..!',
|
'description': 'extremely bad day for this guy..!',
|
||||||
'uploader': 'ljfriel2',
|
'uploader': 'ljfriel2',
|
||||||
'title': 'Most unlucky car accident'
|
'title': 'Most unlucky car accident'
|
||||||
@ -20,25 +22,62 @@ class LiveLeakIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://www.liveleak.com/view?i=f93_1390833151',
|
'url': 'http://www.liveleak.com/view?i=f93_1390833151',
|
||||||
'file': 'f93_1390833151.mp4',
|
|
||||||
'md5': 'd3f1367d14cc3c15bf24fbfbe04b9abf',
|
'md5': 'd3f1367d14cc3c15bf24fbfbe04b9abf',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
'id': 'f93_1390833151',
|
||||||
|
'ext': 'mp4',
|
||||||
'description': 'German Television Channel NDR does an exclusive interview with Edward Snowden.\r\nUploaded on LiveLeak cause German Television thinks the rest of the world isn\'t intereseted in Edward Snowden.',
|
'description': 'German Television Channel NDR does an exclusive interview with Edward Snowden.\r\nUploaded on LiveLeak cause German Television thinks the rest of the world isn\'t intereseted in Edward Snowden.',
|
||||||
'uploader': 'ARD_Stinkt',
|
'uploader': 'ARD_Stinkt',
|
||||||
'title': 'German Television does first Edward Snowden Interview (ENGLISH)',
|
'title': 'German Television does first Edward Snowden Interview (ENGLISH)',
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://www.liveleak.com/view?i=4f7_1392687779',
|
||||||
|
'md5': '42c6d97d54f1db107958760788c5f48f',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '4f7_1392687779',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'description': "The guy with the cigarette seems amazingly nonchalant about the whole thing... I really hope my friends' reactions would be a bit stronger.\r\n\r\nAction-go to 0:55.",
|
||||||
|
'uploader': 'CapObveus',
|
||||||
|
'title': 'Man is Fatally Struck by Reckless Car While Packing up a Moving Truck',
|
||||||
|
'age_limit': 18,
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
|
||||||
video_id = mobj.group('video_id')
|
video_id = mobj.group('video_id')
|
||||||
webpage = self._download_webpage(url, video_id)
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
|
||||||
|
video_title = self._og_search_title(webpage).replace('LiveLeak.com -', '').strip()
|
||||||
|
video_description = self._og_search_description(webpage)
|
||||||
|
video_uploader = self._html_search_regex(
|
||||||
|
r'By:.*?(\w+)</a>', webpage, 'uploader', fatal=False)
|
||||||
|
age_limit = int_or_none(self._search_regex(
|
||||||
|
r'you confirm that you are ([0-9]+) years and over.',
|
||||||
|
webpage, 'age limit', default=None))
|
||||||
|
|
||||||
sources_raw = self._search_regex(
|
sources_raw = self._search_regex(
|
||||||
r'(?s)sources:\s*(\[.*?\]),', webpage, 'video URLs', default=None)
|
r'(?s)sources:\s*(\[.*?\]),', webpage, 'video URLs', default=None)
|
||||||
if sources_raw is None:
|
if sources_raw is None:
|
||||||
sources_raw = '[{ %s}]' % (
|
alt_source = self._search_regex(
|
||||||
self._search_regex(r'(file: ".*?"),', webpage, 'video URL'))
|
r'(file: ".*?"),', webpage, 'video URL', default=None)
|
||||||
|
if alt_source:
|
||||||
|
sources_raw = '[{ %s}]' % alt_source
|
||||||
|
else:
|
||||||
|
# Maybe an embed?
|
||||||
|
embed_url = self._search_regex(
|
||||||
|
r'<iframe[^>]+src="(http://www.prochan.com/embed\?[^"]+)"',
|
||||||
|
webpage, 'embed URL')
|
||||||
|
return {
|
||||||
|
'_type': 'url_transparent',
|
||||||
|
'url': embed_url,
|
||||||
|
'id': video_id,
|
||||||
|
'title': video_title,
|
||||||
|
'description': video_description,
|
||||||
|
'uploader': video_uploader,
|
||||||
|
'age_limit': age_limit,
|
||||||
|
}
|
||||||
|
|
||||||
sources_json = re.sub(r'\s([a-z]+):\s', r'"\1": ', sources_raw)
|
sources_json = re.sub(r'\s([a-z]+):\s', r'"\1": ', sources_raw)
|
||||||
sources = json.loads(sources_json)
|
sources = json.loads(sources_json)
|
||||||
@ -49,15 +88,11 @@ class LiveLeakIE(InfoExtractor):
|
|||||||
} for s in sources]
|
} for s in sources]
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
video_title = self._og_search_title(webpage).replace('LiveLeak.com -', '').strip()
|
|
||||||
video_description = self._og_search_description(webpage)
|
|
||||||
video_uploader = self._html_search_regex(
|
|
||||||
r'By:.*?(\w+)</a>', webpage, 'uploader', fatal=False)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': video_title,
|
'title': video_title,
|
||||||
'description': video_description,
|
'description': video_description,
|
||||||
'uploader': video_uploader,
|
'uploader': video_uploader,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
|
'age_limit': age_limit,
|
||||||
}
|
}
|
||||||
|
@ -26,15 +26,15 @@ class NDRIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://www.ndr.de/903/audio191719.html',
|
'url': 'http://www.ndr.de/info/audio51535.html',
|
||||||
'md5': '41ed601768534dd18a9ae34d84798129',
|
'md5': 'bb3cd38e24fbcc866d13b50ca59307b8',
|
||||||
'note': 'Audio file',
|
'note': 'Audio file',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '191719',
|
'id': '51535',
|
||||||
'ext': 'mp3',
|
'ext': 'mp3',
|
||||||
'title': '"Es war schockierend"',
|
'title': 'La Valette entgeht der Hinrichtung',
|
||||||
'description': 'md5:ed7ff8364793545021a6355b97e95f10',
|
'description': 'md5:22f9541913a40fe50091d5cdd7c9f536',
|
||||||
'duration': 112,
|
'duration': 884,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -74,7 +74,8 @@ class NFBIE(InfoExtractor):
|
|||||||
description = media.find('description').text
|
description = media.find('description').text
|
||||||
# It seems assets always go from lower to better quality, so no need to sort
|
# It seems assets always go from lower to better quality, so no need to sort
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': x.find('default/streamerURI').text + '/',
|
'url': x.find('default/streamerURI').text,
|
||||||
|
'app': x.find('default/streamerURI').text.split('/', 3)[3],
|
||||||
'play_path': x.find('default/url').text,
|
'play_path': x.find('default/url').text,
|
||||||
'rtmp_live': False,
|
'rtmp_live': False,
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
|
37
youtube_dl/extractor/savefrom.py
Normal file
37
youtube_dl/extractor/savefrom.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class SaveFromIE(InfoExtractor):
|
||||||
|
IE_NAME = 'savefrom.net'
|
||||||
|
_VALID_URL = r'https?://[^.]+\.savefrom\.net/\#url=(?P<url>.*)$'
|
||||||
|
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://en.savefrom.net/#url=http://youtube.com/watch?v=UlVRAPW2WJY&utm_source=youtube.com&utm_medium=short_domains&utm_campaign=ssyoutube.com',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'UlVRAPW2WJY',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'title': 'About Team Radical MMA | MMA Fighting',
|
||||||
|
'upload_date': '20120816',
|
||||||
|
'uploader': 'Howcast',
|
||||||
|
'uploader_id': 'Howcast',
|
||||||
|
'description': 'md5:4f0aac94361a12e1ce57d74f85265175',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
video_id = os.path.splitext(url.split('/')[-1])[0]
|
||||||
|
return {
|
||||||
|
'_type': 'url',
|
||||||
|
'id': video_id,
|
||||||
|
'url': mobj.group('url'),
|
||||||
|
}
|
@ -20,6 +20,7 @@ class SmotriIE(InfoExtractor):
|
|||||||
IE_DESC = 'Smotri.com'
|
IE_DESC = 'Smotri.com'
|
||||||
IE_NAME = 'smotri'
|
IE_NAME = 'smotri'
|
||||||
_VALID_URL = r'^https?://(?:www\.)?(?P<url>smotri\.com/video/view/\?id=(?P<videoid>v(?P<realvideoid>[0-9]+)[a-z0-9]{4}))'
|
_VALID_URL = r'^https?://(?:www\.)?(?P<url>smotri\.com/video/view/\?id=(?P<videoid>v(?P<realvideoid>[0-9]+)[a-z0-9]{4}))'
|
||||||
|
_NETRC_MACHINE = 'smotri'
|
||||||
|
|
||||||
_TESTS = [
|
_TESTS = [
|
||||||
# real video id 2610366
|
# real video id 2610366
|
||||||
|
@ -17,6 +17,7 @@ class SohuIE(InfoExtractor):
|
|||||||
u'info_dict': {
|
u'info_dict': {
|
||||||
u'title': u'MV:Far East Movement《The Illest》',
|
u'title': u'MV:Far East Movement《The Illest》',
|
||||||
},
|
},
|
||||||
|
u'skip': u'Only available from China',
|
||||||
}
|
}
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
@ -8,14 +10,14 @@ from ..utils import RegexNotFoundError, ExtractorError
|
|||||||
class SpaceIE(InfoExtractor):
|
class SpaceIE(InfoExtractor):
|
||||||
_VALID_URL = r'https?://(?:(?:www|m)\.)?space\.com/\d+-(?P<title>[^/\.\?]*?)-video\.html'
|
_VALID_URL = r'https?://(?:(?:www|m)\.)?space\.com/\d+-(?P<title>[^/\.\?]*?)-video\.html'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
u'add_ie': ['Brightcove'],
|
'add_ie': ['Brightcove'],
|
||||||
u'url': u'http://www.space.com/23373-huge-martian-landforms-detail-revealed-by-european-probe-video.html',
|
'url': 'http://www.space.com/23373-huge-martian-landforms-detail-revealed-by-european-probe-video.html',
|
||||||
u'info_dict': {
|
'info_dict': {
|
||||||
u'id': u'2780937028001',
|
'id': '2780937028001',
|
||||||
u'ext': u'mp4',
|
'ext': 'mp4',
|
||||||
u'title': u'Huge Martian Landforms\' Detail Revealed By European Probe | Video',
|
'title': 'Huge Martian Landforms\' Detail Revealed By European Probe | Video',
|
||||||
u'description': u'md5:db81cf7f3122f95ed234b631a6ea1e61',
|
'description': 'md5:db81cf7f3122f95ed234b631a6ea1e61',
|
||||||
u'uploader': u'TechMedia Networks',
|
'uploader': 'TechMedia Networks',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
@ -8,23 +7,27 @@ from ..utils import (
|
|||||||
compat_urllib_parse_urlparse,
|
compat_urllib_parse_urlparse,
|
||||||
compat_urllib_request,
|
compat_urllib_request,
|
||||||
compat_urllib_parse,
|
compat_urllib_parse,
|
||||||
|
unified_strdate,
|
||||||
|
str_to_int,
|
||||||
|
int_or_none,
|
||||||
)
|
)
|
||||||
from ..aes import (
|
from ..aes import aes_decrypt_text
|
||||||
aes_decrypt_text
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SpankwireIE(InfoExtractor):
|
class SpankwireIE(InfoExtractor):
|
||||||
_VALID_URL = r'^(?:https?://)?(?:www\.)?(?P<url>spankwire\.com/[^/]*/video(?P<videoid>[0-9]+)/?)'
|
_VALID_URL = r'https?://(?:www\.)?(?P<url>spankwire\.com/[^/]*/video(?P<videoid>[0-9]+)/?)'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://www.spankwire.com/Buckcherry-s-X-Rated-Music-Video-Crazy-Bitch/video103545/',
|
'url': 'http://www.spankwire.com/Buckcherry-s-X-Rated-Music-Video-Crazy-Bitch/video103545/',
|
||||||
'file': '103545.mp4',
|
'md5': '8bbfde12b101204b39e4b9fe7eb67095',
|
||||||
'md5': '1b3f55e345500552dbc252a3e9c1af43',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
"uploader": "oreusz",
|
'id': '103545',
|
||||||
"title": "Buckcherry`s X Rated Music Video Crazy Bitch",
|
'ext': 'mp4',
|
||||||
"description": "Crazy Bitch X rated music video.",
|
'title': 'Buckcherry`s X Rated Music Video Crazy Bitch',
|
||||||
"age_limit": 18,
|
'description': 'Crazy Bitch X rated music video.',
|
||||||
|
'uploader': 'oreusz',
|
||||||
|
'uploader_id': '124697',
|
||||||
|
'upload_date': '20070508',
|
||||||
|
'age_limit': 18,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,13 +40,26 @@ class SpankwireIE(InfoExtractor):
|
|||||||
req.add_header('Cookie', 'age_verified=1')
|
req.add_header('Cookie', 'age_verified=1')
|
||||||
webpage = self._download_webpage(req, video_id)
|
webpage = self._download_webpage(req, video_id)
|
||||||
|
|
||||||
video_title = self._html_search_regex(r'<h1>([^<]+)', webpage, 'title')
|
title = self._html_search_regex(r'<h1>([^<]+)', webpage, 'title')
|
||||||
video_uploader = self._html_search_regex(
|
|
||||||
r'by:\s*<a [^>]*>(.+?)</a>', webpage, 'uploader', fatal=False)
|
|
||||||
thumbnail = self._html_search_regex(
|
|
||||||
r'flashvars\.image_url = "([^"]+)', webpage, 'thumbnail', fatal=False)
|
|
||||||
description = self._html_search_regex(
|
description = self._html_search_regex(
|
||||||
r'<div\s+id="descriptionContent">([^<]+)<', webpage, 'description', fatal=False)
|
r'<div\s+id="descriptionContent">([^<]+)<', webpage, 'description', fatal=False)
|
||||||
|
thumbnail = self._html_search_regex(
|
||||||
|
r'flashvars\.image_url = "([^"]+)', webpage, 'thumbnail', fatal=False)
|
||||||
|
|
||||||
|
uploader = self._html_search_regex(
|
||||||
|
r'by:\s*<a [^>]*>(.+?)</a>', webpage, 'uploader', fatal=False)
|
||||||
|
uploader_id = self._html_search_regex(
|
||||||
|
r'by:\s*<a href="/Profile\.aspx\?.*?UserId=(\d+).*?"', webpage, 'uploader id', fatal=False)
|
||||||
|
upload_date = self._html_search_regex(r'</a> on (.+?) at \d+:\d+', webpage, 'upload date', fatal=False)
|
||||||
|
if upload_date:
|
||||||
|
upload_date = unified_strdate(upload_date)
|
||||||
|
|
||||||
|
view_count = self._html_search_regex(
|
||||||
|
r'<div id="viewsCounter"><span>([^<]+)</span> views</div>', webpage, 'view count', fatal=False)
|
||||||
|
if view_count:
|
||||||
|
view_count = str_to_int(view_count)
|
||||||
|
comment_count = int_or_none(self._html_search_regex(
|
||||||
|
r'<span id="spCommentCount">\s*(\d+)</span> Comments</div>', webpage, 'comment count', fatal=False))
|
||||||
|
|
||||||
video_urls = list(map(compat_urllib_parse.unquote , re.findall(r'flashvars\.quality_[0-9]{3}p = "([^"]+)', webpage)))
|
video_urls = list(map(compat_urllib_parse.unquote , re.findall(r'flashvars\.quality_[0-9]{3}p = "([^"]+)', webpage)))
|
||||||
if webpage.find('flashvars\.encrypted = "true"') != -1:
|
if webpage.find('flashvars\.encrypted = "true"') != -1:
|
||||||
@ -53,16 +69,13 @@ class SpankwireIE(InfoExtractor):
|
|||||||
formats = []
|
formats = []
|
||||||
for video_url in video_urls:
|
for video_url in video_urls:
|
||||||
path = compat_urllib_parse_urlparse(video_url).path
|
path = compat_urllib_parse_urlparse(video_url).path
|
||||||
extension = os.path.splitext(path)[1][1:]
|
|
||||||
format = path.split('/')[4].split('_')[:2]
|
format = path.split('/')[4].split('_')[:2]
|
||||||
resolution, bitrate_str = format
|
resolution, bitrate_str = format
|
||||||
format = "-".join(format)
|
format = "-".join(format)
|
||||||
height = int(resolution.rstrip('P'))
|
height = int(resolution.rstrip('Pp'))
|
||||||
tbr = int(bitrate_str.rstrip('K'))
|
tbr = int(bitrate_str.rstrip('Kk'))
|
||||||
|
|
||||||
formats.append({
|
formats.append({
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
'ext': extension,
|
|
||||||
'resolution': resolution,
|
'resolution': resolution,
|
||||||
'format': format,
|
'format': format,
|
||||||
'tbr': tbr,
|
'tbr': tbr,
|
||||||
@ -75,10 +88,14 @@ class SpankwireIE(InfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'uploader': video_uploader,
|
'title': title,
|
||||||
'title': video_title,
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
'description': description,
|
'description': description,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
'uploader': uploader,
|
||||||
|
'uploader_id': uploader_id,
|
||||||
|
'upload_date': upload_date,
|
||||||
|
'view_count': view_count,
|
||||||
|
'comment_count': comment_count,
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'age_limit': age_limit,
|
'age_limit': age_limit,
|
||||||
}
|
}
|
||||||
|
27
youtube_dl/extractor/syfy.py
Normal file
27
youtube_dl/extractor/syfy.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
|
||||||
|
|
||||||
|
class SyfyIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://www\.syfy\.com/videos/.+?vid:(?P<id>\d+)'
|
||||||
|
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://www.syfy.com/videos/Robot%20Combat%20League/Behind%20the%20Scenes/vid:2631458',
|
||||||
|
'md5': 'e07de1d52c7278adbb9b9b1c93a66849',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'NmqMrGnXvmO1',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'George Lucas has Advice for his Daughter',
|
||||||
|
'description': 'Listen to what insights George Lucas give his daughter Amanda.',
|
||||||
|
},
|
||||||
|
'add_ie': ['ThePlatform'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
video_id = mobj.group('id')
|
||||||
|
webpage = self._download_webpage(url, video_id)
|
||||||
|
return self.url_result(self._og_search_video_url(webpage))
|
66
youtube_dl/extractor/testurl.py
Normal file
66
youtube_dl/extractor/testurl.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import ExtractorError
|
||||||
|
|
||||||
|
|
||||||
|
class TestURLIE(InfoExtractor):
|
||||||
|
""" Allows adressing of the test cases as test:yout.*be_1 """
|
||||||
|
|
||||||
|
IE_DESC = False # Do not list
|
||||||
|
_VALID_URL = r'test(?:url)?:(?P<id>(?P<extractor>.+?)(?:_(?P<num>[0-9]+))?)$'
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
from ..extractor import gen_extractors
|
||||||
|
|
||||||
|
mobj = re.match(self._VALID_URL, url)
|
||||||
|
video_id = mobj.group('id')
|
||||||
|
extractor_id = mobj.group('extractor')
|
||||||
|
all_extractors = gen_extractors()
|
||||||
|
|
||||||
|
rex = re.compile(extractor_id, flags=re.IGNORECASE)
|
||||||
|
matching_extractors = [
|
||||||
|
e for e in all_extractors if rex.search(e.IE_NAME)]
|
||||||
|
|
||||||
|
if len(matching_extractors) == 0:
|
||||||
|
raise ExtractorError(
|
||||||
|
'No extractors matching %r found' % extractor_id,
|
||||||
|
expected=True)
|
||||||
|
elif len(matching_extractors) > 1:
|
||||||
|
# Is it obvious which one to pick?
|
||||||
|
try:
|
||||||
|
extractor = next(
|
||||||
|
ie for ie in matching_extractors
|
||||||
|
if ie.IE_NAME.lower() == extractor_id.lower())
|
||||||
|
except StopIteration:
|
||||||
|
raise ExtractorError(
|
||||||
|
('Found multiple matching extractors: %s' %
|
||||||
|
' '.join(ie.IE_NAME for ie in matching_extractors)),
|
||||||
|
expected=True)
|
||||||
|
|
||||||
|
num_str = mobj.group('num')
|
||||||
|
num = int(num_str) if num_str else 0
|
||||||
|
|
||||||
|
testcases = []
|
||||||
|
t = getattr(extractor, '_TEST', None)
|
||||||
|
if t:
|
||||||
|
testcases.append(t)
|
||||||
|
testcases.extend(getattr(extractor, '_TESTS', []))
|
||||||
|
|
||||||
|
try:
|
||||||
|
tc = testcases[num]
|
||||||
|
except IndexError:
|
||||||
|
raise ExtractorError(
|
||||||
|
('Test case %d not found, got only %d tests' %
|
||||||
|
(num, len(testcases))),
|
||||||
|
expected=True)
|
||||||
|
|
||||||
|
self.to_screen('Test URL: %s' % tc['url'])
|
||||||
|
|
||||||
|
return {
|
||||||
|
'_type': 'url',
|
||||||
|
'url': tc['url'],
|
||||||
|
'id': video_id,
|
||||||
|
}
|
@ -11,7 +11,10 @@ _x = lambda p: xpath_with_ns(p, {'smil': 'http://www.w3.org/2005/SMIL21/Language
|
|||||||
|
|
||||||
|
|
||||||
class ThePlatformIE(InfoExtractor):
|
class ThePlatformIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:https?://link\.theplatform\.com/s/[^/]+/|theplatform:)(?P<id>[^/\?]+)'
|
_VALID_URL = r'''(?x)
|
||||||
|
(?:https?://(?:link|player)\.theplatform\.com/[sp]/[^/]+/
|
||||||
|
(?P<config>[^/\?]+/(?:swf|config)/select/)?
|
||||||
|
|theplatform:)(?P<id>[^/\?&]+)'''
|
||||||
|
|
||||||
_TEST = {
|
_TEST = {
|
||||||
# from http://www.metacafe.com/watch/cb-e9I_cZgTgIPd/blackberrys_big_bold_z30/
|
# from http://www.metacafe.com/watch/cb-e9I_cZgTgIPd/blackberrys_big_bold_z30/
|
||||||
@ -29,9 +32,7 @@ class ThePlatformIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _get_info(self, video_id):
|
def _get_info(self, video_id, smil_url):
|
||||||
smil_url = ('http://link.theplatform.com/s/dJ5BDC/{0}/meta.smil?'
|
|
||||||
'format=smil&mbr=true'.format(video_id))
|
|
||||||
meta = self._download_xml(smil_url, video_id)
|
meta = self._download_xml(smil_url, video_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -50,6 +51,15 @@ class ThePlatformIE(InfoExtractor):
|
|||||||
|
|
||||||
head = meta.find(_x('smil:head'))
|
head = meta.find(_x('smil:head'))
|
||||||
body = meta.find(_x('smil:body'))
|
body = meta.find(_x('smil:body'))
|
||||||
|
|
||||||
|
f4m_node = body.find(_x('smil:seq/smil:video'))
|
||||||
|
if f4m_node is not None:
|
||||||
|
formats = [{
|
||||||
|
'ext': 'flv',
|
||||||
|
# the parameters are from syfy.com, other sites may use others
|
||||||
|
'url': f4m_node.attrib['src'] + '?g=UXWGVKRWHFSP&hdcore=3.0.3',
|
||||||
|
}]
|
||||||
|
else:
|
||||||
base_url = head.find(_x('smil:meta')).attrib['base']
|
base_url = head.find(_x('smil:meta')).attrib['base']
|
||||||
switch = body.find(_x('smil:switch'))
|
switch = body.find(_x('smil:switch'))
|
||||||
formats = []
|
formats = []
|
||||||
@ -68,7 +78,6 @@ class ThePlatformIE(InfoExtractor):
|
|||||||
'height': height,
|
'height': height,
|
||||||
'vbr': vbr,
|
'vbr': vbr,
|
||||||
})
|
})
|
||||||
|
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -83,4 +92,13 @@ class ThePlatformIE(InfoExtractor):
|
|||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
return self._get_info(video_id)
|
if mobj.group('config'):
|
||||||
|
config_url = url+ '&form=json'
|
||||||
|
config_url = config_url.replace('swf/', 'config/')
|
||||||
|
config_json = self._download_webpage(config_url, video_id, u'Downloading config')
|
||||||
|
config = json.loads(config_json)
|
||||||
|
smil_url = config['releaseUrl'] + '&format=SMIL&formats=MPEG4'
|
||||||
|
else:
|
||||||
|
smil_url = ('http://link.theplatform.com/s/dJ5BDC/{0}/meta.smil?'
|
||||||
|
'format=smil&mbr=true'.format(video_id))
|
||||||
|
return self._get_info(video_id, smil_url)
|
||||||
|
47
youtube_dl/extractor/trutube.py
Normal file
47
youtube_dl/extractor/trutube.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .common import InfoExtractor
|
||||||
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TruTubeIE(InfoExtractor):
|
||||||
|
_VALID_URL = r'https?://(?:www\.)?trutube\.tv/video/(?P<id>[0-9]+)/.*'
|
||||||
|
_TEST = {
|
||||||
|
'url': 'http://trutube.tv/video/14880/Ramses-II-Proven-To-Be-A-Red-Headed-Caucasoid-',
|
||||||
|
'md5': 'c5b6e301b0a2040b074746cbeaa26ca1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '14880',
|
||||||
|
'ext': 'flv',
|
||||||
|
'title': 'Ramses II - Proven To Be A Red Headed Caucasoid',
|
||||||
|
'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_title = self._og_search_title(webpage).strip()
|
||||||
|
thumbnail = self._search_regex(
|
||||||
|
r"var splash_img = '([^']+)';", webpage, 'thumbnail', fatal=False)
|
||||||
|
|
||||||
|
all_formats = re.finditer(
|
||||||
|
r"var (?P<key>[a-z]+)_video_file\s*=\s*'(?P<url>[^']+)';", webpage)
|
||||||
|
formats = [{
|
||||||
|
'format_id': m.group('key'),
|
||||||
|
'quality': -i,
|
||||||
|
'url': m.group('url'),
|
||||||
|
} for i, m in enumerate(all_formats)]
|
||||||
|
self._sort_formats(formats)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': video_id,
|
||||||
|
'title': video_title,
|
||||||
|
'formats': formats,
|
||||||
|
'thumbnail': thumbnail,
|
||||||
|
}
|
@ -57,6 +57,7 @@ class VestiIE(InfoExtractor):
|
|||||||
# m3u8 download
|
# m3u8 download
|
||||||
'skip_download': True,
|
'skip_download': True,
|
||||||
},
|
},
|
||||||
|
'skip': 'Blocked outside Russia'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://sochi2014.vesti.ru/live/play/live_id/301',
|
'url': 'http://sochi2014.vesti.ru/live/play/live_id/301',
|
||||||
@ -86,8 +87,7 @@ class VestiIE(InfoExtractor):
|
|||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
else:
|
else:
|
||||||
mobj = re.search(
|
mobj = re.search(
|
||||||
r'<div.+?id="current-video-holder".*?>\s*<iframe src="http://player\.rutv\.ru/iframe/(?P<type>[^/]+)/id/(?P<id>\d+)[^"]*"',
|
r'<iframe.+?src="http://player\.rutv\.ru/iframe/(?P<type>[^/]+)/id/(?P<id>\d+)[^"]*".*?></iframe>', page)
|
||||||
page)
|
|
||||||
|
|
||||||
if not mobj:
|
if not mobj:
|
||||||
raise ExtractorError('No media found')
|
raise ExtractorError('No media found')
|
||||||
|
@ -37,9 +37,10 @@ class VimeoIE(SubtitlesInfoExtractor):
|
|||||||
_TESTS = [
|
_TESTS = [
|
||||||
{
|
{
|
||||||
'url': 'http://vimeo.com/56015672#at=0',
|
'url': 'http://vimeo.com/56015672#at=0',
|
||||||
'file': '56015672.mp4',
|
|
||||||
'md5': '8879b6cc097e987f02484baf890129e5',
|
'md5': '8879b6cc097e987f02484baf890129e5',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
'id': '56015672',
|
||||||
|
'ext': 'mp4',
|
||||||
"upload_date": "20121220",
|
"upload_date": "20121220",
|
||||||
"description": "This is a test case for youtube-dl.\nFor more information, see github.com/rg3/youtube-dl\nTest chars: \u2605 \" ' \u5e78 / \\ \u00e4 \u21ad \U0001d550",
|
"description": "This is a test case for youtube-dl.\nFor more information, see github.com/rg3/youtube-dl\nTest chars: \u2605 \" ' \u5e78 / \\ \u00e4 \u21ad \U0001d550",
|
||||||
"uploader_id": "user7108434",
|
"uploader_id": "user7108434",
|
||||||
|
@ -6,6 +6,9 @@ import json
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
|
ExtractorError,
|
||||||
|
compat_urllib_request,
|
||||||
|
compat_urllib_parse,
|
||||||
compat_str,
|
compat_str,
|
||||||
unescapeHTML,
|
unescapeHTML,
|
||||||
)
|
)
|
||||||
@ -14,31 +17,80 @@ from ..utils import (
|
|||||||
class VKIE(InfoExtractor):
|
class VKIE(InfoExtractor):
|
||||||
IE_NAME = 'vk.com'
|
IE_NAME = 'vk.com'
|
||||||
_VALID_URL = r'https?://vk\.com/(?:videos.*?\?.*?z=)?video(?P<id>.*?)(?:\?|%2F|$)'
|
_VALID_URL = r'https?://vk\.com/(?:videos.*?\?.*?z=)?video(?P<id>.*?)(?:\?|%2F|$)'
|
||||||
|
_NETRC_MACHINE = 'vk'
|
||||||
|
|
||||||
_TESTS = [{
|
_TESTS = [
|
||||||
|
{
|
||||||
'url': 'http://vk.com/videos-77521?z=video-77521_162222515%2Fclub77521',
|
'url': 'http://vk.com/videos-77521?z=video-77521_162222515%2Fclub77521',
|
||||||
'file': '162222515.flv',
|
|
||||||
'md5': '0deae91935c54e00003c2a00646315f0',
|
'md5': '0deae91935c54e00003c2a00646315f0',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
'id': '162222515',
|
||||||
|
'ext': 'flv',
|
||||||
'title': 'ProtivoGunz - Хуёвая песня',
|
'title': 'ProtivoGunz - Хуёвая песня',
|
||||||
'uploader': 'Noize MC',
|
'uploader': 'Noize MC',
|
||||||
|
'duration': 195,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://vk.com/video4643923_163339118',
|
'url': 'http://vk.com/video4643923_163339118',
|
||||||
'file': '163339118.mp4',
|
|
||||||
'md5': 'f79bccb5cd182b1f43502ca5685b2b36',
|
'md5': 'f79bccb5cd182b1f43502ca5685b2b36',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
'id': '163339118',
|
||||||
|
'ext': 'mp4',
|
||||||
'uploader': 'Elvira Dzhonik',
|
'uploader': 'Elvira Dzhonik',
|
||||||
'title': 'Dream Theater - Hollow Years Live at Budokan 720*',
|
'title': 'Dream Theater - Hollow Years Live at Budokan 720*',
|
||||||
|
'duration': 558,
|
||||||
}
|
}
|
||||||
}]
|
},
|
||||||
|
{
|
||||||
|
'url': 'http://vk.com/video-8871596_164049491',
|
||||||
|
'md5': 'a590bcaf3d543576c9bd162812387666',
|
||||||
|
'note': 'Only available for registered users',
|
||||||
|
'info_dict': {
|
||||||
|
'id': '164049491',
|
||||||
|
'ext': 'mp4',
|
||||||
|
'uploader': 'Триллеры',
|
||||||
|
'title': '► Бойцовский клуб / Fight Club 1999 [HD 720]\u00a0',
|
||||||
|
'duration': 8352,
|
||||||
|
},
|
||||||
|
'skip': 'Requires vk account credentials',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def _login(self):
|
||||||
|
(username, password) = self._get_login_info()
|
||||||
|
if username is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
login_form = {
|
||||||
|
'act': 'login',
|
||||||
|
'role': 'al_frame',
|
||||||
|
'expire': '1',
|
||||||
|
'email': username,
|
||||||
|
'pass': password,
|
||||||
|
}
|
||||||
|
|
||||||
|
request = compat_urllib_request.Request('https://login.vk.com/?act=login',
|
||||||
|
compat_urllib_parse.urlencode(login_form).encode('utf-8'))
|
||||||
|
login_page = self._download_webpage(request, None, note='Logging in as %s' % username)
|
||||||
|
|
||||||
|
if re.search(r'onLoginFailed', login_page):
|
||||||
|
raise ExtractorError('Unable to login, incorrect username and/or password', expected=True)
|
||||||
|
|
||||||
|
def _real_initialize(self):
|
||||||
|
self._login()
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
mobj = re.match(self._VALID_URL, url)
|
mobj = re.match(self._VALID_URL, url)
|
||||||
video_id = mobj.group('id')
|
video_id = mobj.group('id')
|
||||||
|
|
||||||
info_url = 'http://vk.com/al_video.php?act=show&al=1&video=%s' % video_id
|
info_url = 'http://vk.com/al_video.php?act=show&al=1&video=%s' % video_id
|
||||||
info_page = self._download_webpage(info_url, video_id)
|
info_page = self._download_webpage(info_url, video_id)
|
||||||
|
|
||||||
|
if re.search(r'<!>Please log in or <', info_page):
|
||||||
|
raise ExtractorError('This video is only available for registered users, '
|
||||||
|
'use --username and --password options to provide account credentials.', expected=True)
|
||||||
|
|
||||||
m_yt = re.search(r'src="(http://www.youtube.com/.*?)"', info_page)
|
m_yt = re.search(r'src="(http://www.youtube.com/.*?)"', info_page)
|
||||||
if m_yt is not None:
|
if m_yt is not None:
|
||||||
self.to_screen(u'Youtube video detected')
|
self.to_screen(u'Youtube video detected')
|
||||||
@ -60,4 +112,5 @@ class VKIE(InfoExtractor):
|
|||||||
'title': unescapeHTML(data['md_title']),
|
'title': unescapeHTML(data['md_title']),
|
||||||
'thumbnail': data.get('jpg'),
|
'thumbnail': data.get('jpg'),
|
||||||
'uploader': data.get('md_author'),
|
'uploader': data.get('md_author'),
|
||||||
|
'duration': data.get('duration')
|
||||||
}
|
}
|
||||||
|
@ -6,14 +6,15 @@ from .common import InfoExtractor
|
|||||||
|
|
||||||
|
|
||||||
class WimpIE(InfoExtractor):
|
class WimpIE(InfoExtractor):
|
||||||
_VALID_URL = r'(?:http://)?(?:www\.)?wimp\.com/([^/]+)/'
|
_VALID_URL = r'http://(?:www\.)?wimp\.com/([^/]+)/'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'http://www.wimp.com/deerfence/',
|
'url': 'http://www.wimp.com/maruexhausted/',
|
||||||
'file': 'deerfence.flv',
|
'md5': 'f1acced123ecb28d9bb79f2479f2b6a1',
|
||||||
'md5': '8b215e2e0168c6081a1cf84b2846a2b5',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
"title": "Watch Till End: Herd of deer jump over a fence.",
|
'id': 'maruexhausted',
|
||||||
"description": "These deer look as fluid as running water when they jump over this fence as a herd. This video is one that needs to be watched until the very end for the true majesty to be witnessed, but once it comes, it's sure to take your breath away.",
|
'ext': 'flv',
|
||||||
|
'title': 'Maru is exhausted.',
|
||||||
|
'description': 'md5:57e099e857c0a4ea312542b684a869b8',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,51 +4,51 @@ import re
|
|||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
compat_urllib_parse,
|
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
unified_strdate,
|
||||||
|
str_to_int,
|
||||||
|
int_or_none,
|
||||||
|
parse_duration,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class XHamsterIE(InfoExtractor):
|
class XHamsterIE(InfoExtractor):
|
||||||
"""Information Extractor for xHamster"""
|
"""Information Extractor for xHamster"""
|
||||||
_VALID_URL = r'(?:http://)?(?:www\.)?xhamster\.com/movies/(?P<id>[0-9]+)/(?P<seo>.+?)\.html(?:\?.*)?'
|
_VALID_URL = r'http://(?:www\.)?xhamster\.com/movies/(?P<id>[0-9]+)/(?P<seo>.+?)\.html(?:\?.*)?'
|
||||||
_TESTS = [{
|
_TESTS = [
|
||||||
|
{
|
||||||
'url': 'http://xhamster.com/movies/1509445/femaleagent_shy_beauty_takes_the_bait.html',
|
'url': 'http://xhamster.com/movies/1509445/femaleagent_shy_beauty_takes_the_bait.html',
|
||||||
'file': '1509445.mp4',
|
|
||||||
'md5': '8281348b8d3c53d39fffb377d24eac4e',
|
'md5': '8281348b8d3c53d39fffb377d24eac4e',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
"upload_date": "20121014",
|
'id': '1509445',
|
||||||
"uploader_id": "Ruseful2011",
|
'ext': 'mp4',
|
||||||
"title": "FemaleAgent Shy beauty takes the bait",
|
'title': 'FemaleAgent Shy beauty takes the bait',
|
||||||
"age_limit": 18,
|
'upload_date': '20121014',
|
||||||
|
'uploader_id': 'Ruseful2011',
|
||||||
|
'duration': 893,
|
||||||
|
'age_limit': 18,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'url': 'http://xhamster.com/movies/2221348/britney_spears_sexy_booty.html?hd',
|
'url': 'http://xhamster.com/movies/2221348/britney_spears_sexy_booty.html?hd',
|
||||||
'file': '2221348.flv',
|
'md5': '4cbd8d56708ecb4fb4124c23e4acb81a',
|
||||||
'md5': 'e767b9475de189320f691f49c679c4c7',
|
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
"upload_date": "20130914",
|
'id': '2221348',
|
||||||
"uploader_id": "jojo747400",
|
'ext': 'mp4',
|
||||||
"title": "Britney Spears Sexy Booty",
|
'title': 'Britney Spears Sexy Booty',
|
||||||
"age_limit": 18,
|
'upload_date': '20130914',
|
||||||
|
'uploader_id': 'jojo747400',
|
||||||
|
'duration': 200,
|
||||||
|
'age_limit': 18,
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def _real_extract(self,url):
|
def _real_extract(self,url):
|
||||||
def extract_video_url(webpage):
|
def extract_video_url(webpage):
|
||||||
mobj = re.search(r'\'srv\': \'(?P<server>[^\']*)\',\s*\'file\': \'(?P<file>[^\']+)\',', webpage)
|
mp4 = re.search(r'<video\s+.*?file="([^"]+)".*?>', webpage)
|
||||||
if mobj is None:
|
|
||||||
raise ExtractorError('Unable to extract media URL')
|
|
||||||
if len(mobj.group('server')) == 0:
|
|
||||||
return compat_urllib_parse.unquote(mobj.group('file'))
|
|
||||||
else:
|
|
||||||
return mobj.group('server')+'/key='+mobj.group('file')
|
|
||||||
|
|
||||||
def extract_mp4_video_url(webpage):
|
|
||||||
mp4 = re.search(r'<a href=\"(.+?)\" class=\"mp4Play\"',webpage)
|
|
||||||
if mp4 is None:
|
if mp4 is None:
|
||||||
return None
|
raise ExtractorError('Unable to extract media URL')
|
||||||
else:
|
else:
|
||||||
return mp4.group(1)
|
return mp4.group(1)
|
||||||
|
|
||||||
@ -62,50 +62,48 @@ class XHamsterIE(InfoExtractor):
|
|||||||
mrss_url = 'http://xhamster.com/movies/%s/%s.html' % (video_id, seo)
|
mrss_url = 'http://xhamster.com/movies/%s/%s.html' % (video_id, seo)
|
||||||
webpage = self._download_webpage(mrss_url, video_id)
|
webpage = self._download_webpage(mrss_url, video_id)
|
||||||
|
|
||||||
video_title = self._html_search_regex(
|
title = self._html_search_regex(r'<title>(?P<title>.+?) - xHamster\.com</title>', webpage, 'title')
|
||||||
r'<title>(?P<title>.+?) - xHamster\.com</title>', webpage, 'title')
|
|
||||||
|
|
||||||
# Only a few videos have an description
|
# Only a few videos have an description
|
||||||
mobj = re.search(r'<span>Description: </span>([^<]+)', webpage)
|
mobj = re.search(r'<span>Description: </span>([^<]+)', webpage)
|
||||||
video_description = mobj.group(1) if mobj else None
|
description = mobj.group(1) if mobj else None
|
||||||
|
|
||||||
mobj = re.search(r'hint=\'(?P<upload_date_Y>[0-9]{4})-(?P<upload_date_m>[0-9]{2})-(?P<upload_date_d>[0-9]{2}) [0-9]{2}:[0-9]{2}:[0-9]{2} [A-Z]{3,4}\'', webpage)
|
upload_date = self._html_search_regex(r'hint=\'(\d{4}-\d{2}-\d{2}) \d{2}:\d{2}:\d{2} [A-Z]{3,4}\'',
|
||||||
if mobj:
|
webpage, 'upload date', fatal=False)
|
||||||
video_upload_date = mobj.group('upload_date_Y')+mobj.group('upload_date_m')+mobj.group('upload_date_d')
|
if upload_date:
|
||||||
else:
|
upload_date = unified_strdate(upload_date)
|
||||||
video_upload_date = None
|
|
||||||
self._downloader.report_warning('Unable to extract upload date')
|
|
||||||
|
|
||||||
video_uploader_id = self._html_search_regex(
|
uploader_id = self._html_search_regex(r'<a href=\'/user/[^>]+>(?P<uploader_id>[^<]+)',
|
||||||
r'<a href=\'/user/[^>]+>(?P<uploader_id>[^<]+)',
|
|
||||||
webpage, 'uploader id', default='anonymous')
|
webpage, 'uploader id', default='anonymous')
|
||||||
|
|
||||||
video_thumbnail = self._search_regex(
|
thumbnail = self._html_search_regex(r'<video\s+.*?poster="([^"]+)".*?>', webpage, 'thumbnail', fatal=False)
|
||||||
r'\'image\':\'(?P<thumbnail>[^\']+)\'',
|
|
||||||
webpage, 'thumbnail', fatal=False)
|
duration = parse_duration(self._html_search_regex(r'<span>Runtime:</span> (\d+:\d+)</div>',
|
||||||
|
webpage, 'duration', fatal=False))
|
||||||
|
|
||||||
|
view_count = self._html_search_regex(r'<span>Views:</span> ([^<]+)</div>', webpage, 'view count', fatal=False)
|
||||||
|
if view_count:
|
||||||
|
view_count = str_to_int(view_count)
|
||||||
|
|
||||||
|
mobj = re.search(r"hint='(?P<likecount>\d+) Likes / (?P<dislikecount>\d+) Dislikes'", webpage)
|
||||||
|
(like_count, dislike_count) = (mobj.group('likecount'), mobj.group('dislikecount')) if mobj else (None, None)
|
||||||
|
|
||||||
|
mobj = re.search(r'</label>Comments \((?P<commentcount>\d+)\)</div>', webpage)
|
||||||
|
comment_count = mobj.group('commentcount') if mobj else 0
|
||||||
|
|
||||||
age_limit = self._rta_search(webpage)
|
age_limit = self._rta_search(webpage)
|
||||||
|
|
||||||
hd = is_hd(webpage)
|
hd = is_hd(webpage)
|
||||||
|
|
||||||
video_url = extract_video_url(webpage)
|
video_url = extract_video_url(webpage)
|
||||||
formats = [{
|
formats = [{
|
||||||
'url': video_url,
|
'url': video_url,
|
||||||
'format_id': 'hd' if hd else 'sd',
|
'format_id': 'hd' if hd else 'sd',
|
||||||
'preference': 0,
|
'preference': 1,
|
||||||
}]
|
}]
|
||||||
|
|
||||||
video_mp4_url = extract_mp4_video_url(webpage)
|
|
||||||
if video_mp4_url is not None:
|
|
||||||
formats.append({
|
|
||||||
'url': video_mp4_url,
|
|
||||||
'ext': 'mp4',
|
|
||||||
'format_id': 'mp4-hd' if hd else 'mp4-sd',
|
|
||||||
'preference': 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
if not hd:
|
if not hd:
|
||||||
webpage = self._download_webpage(
|
webpage = self._download_webpage(mrss_url + '?hd', video_id, note='Downloading HD webpage')
|
||||||
mrss_url + '?hd', video_id, note='Downloading HD webpage')
|
|
||||||
if is_hd(webpage):
|
if is_hd(webpage):
|
||||||
video_url = extract_video_url(webpage)
|
video_url = extract_video_url(webpage)
|
||||||
formats.append({
|
formats.append({
|
||||||
@ -118,11 +116,16 @@ class XHamsterIE(InfoExtractor):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': video_title,
|
'title': title,
|
||||||
'formats': formats,
|
'description': description,
|
||||||
'description': video_description,
|
'upload_date': upload_date,
|
||||||
'upload_date': video_upload_date,
|
'uploader_id': uploader_id,
|
||||||
'uploader_id': video_uploader_id,
|
'thumbnail': thumbnail,
|
||||||
'thumbnail': video_thumbnail,
|
'duration': duration,
|
||||||
|
'view_count': view_count,
|
||||||
|
'like_count': int_or_none(like_count),
|
||||||
|
'dislike_count': int_or_none(dislike_count),
|
||||||
|
'comment_count': int_or_none(comment_count),
|
||||||
'age_limit': age_limit,
|
'age_limit': age_limit,
|
||||||
|
'formats': formats,
|
||||||
}
|
}
|
||||||
|
@ -138,13 +138,14 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
|
|||||||
(?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/|
|
(?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/|
|
||||||
(?:www\.)?deturl\.com/www\.youtube\.com/|
|
(?:www\.)?deturl\.com/www\.youtube\.com/|
|
||||||
(?:www\.)?pwnyoutube\.com/|
|
(?:www\.)?pwnyoutube\.com/|
|
||||||
|
(?:www\.)?yourepeat\.com/|
|
||||||
tube\.majestyc\.net/|
|
tube\.majestyc\.net/|
|
||||||
youtube\.googleapis\.com/) # the various hostnames, with wildcard subdomains
|
youtube\.googleapis\.com/) # the various hostnames, with wildcard subdomains
|
||||||
(?:.*?\#/)? # handle anchor (#/) redirect urls
|
(?:.*?\#/)? # handle anchor (#/) redirect urls
|
||||||
(?: # the various things that can precede the ID:
|
(?: # the various things that can precede the ID:
|
||||||
(?:(?:v|embed|e)/) # v/ or embed/ or e/
|
(?:(?:v|embed|e)/) # v/ or embed/ or e/
|
||||||
|(?: # or the v= param in all its forms
|
|(?: # or the v= param in all its forms
|
||||||
(?:(?:watch|movie)(?:_popup)?(?:\.php)?)? # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
|
(?:(?:watch|movie)(?:_popup)?(?:\.php)?/?)? # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
|
||||||
(?:\?|\#!?) # the params delimiter ? or # or #!
|
(?:\?|\#!?) # the params delimiter ? or # or #!
|
||||||
(?:.*?&)? # any other preceding param (like /?s=tuff&v=xxxx)
|
(?:.*?&)? # any other preceding param (like /?s=tuff&v=xxxx)
|
||||||
v=
|
v=
|
||||||
@ -296,6 +297,23 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
|
|||||||
u"format": "141",
|
u"format": "141",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
# DASH manifest with encrypted signature
|
||||||
|
{
|
||||||
|
u'url': u'https://www.youtube.com/watch?v=IB3lcPjvWLA',
|
||||||
|
u'info_dict': {
|
||||||
|
u'id': u'IB3lcPjvWLA',
|
||||||
|
u'ext': u'm4a',
|
||||||
|
u'title': u'Afrojack - The Spark ft. Spree Wilson',
|
||||||
|
u'description': u'md5:3199ed45ee8836572865580804d7ac0f',
|
||||||
|
u'uploader': u'AfrojackVEVO',
|
||||||
|
u'uploader_id': u'AfrojackVEVO',
|
||||||
|
u'upload_date': u'20131011',
|
||||||
|
},
|
||||||
|
u"params": {
|
||||||
|
u'youtube_include_dash_manifest': True,
|
||||||
|
u'format': '141',
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -1271,8 +1289,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
|
|||||||
mobj = re.search(r';ytplayer.config = ({.*?});', video_webpage)
|
mobj = re.search(r';ytplayer.config = ({.*?});', video_webpage)
|
||||||
if not mobj:
|
if not mobj:
|
||||||
raise ValueError('Could not find vevo ID')
|
raise ValueError('Could not find vevo ID')
|
||||||
info = json.loads(mobj.group(1))
|
ytplayer_config = json.loads(mobj.group(1))
|
||||||
args = info['args']
|
args = ytplayer_config['args']
|
||||||
# Easy way to know if the 's' value is in url_encoded_fmt_stream_map
|
# Easy way to know if the 's' value is in url_encoded_fmt_stream_map
|
||||||
# this signatures are encrypted
|
# this signatures are encrypted
|
||||||
if 'url_encoded_fmt_stream_map' not in args:
|
if 'url_encoded_fmt_stream_map' not in args:
|
||||||
@ -1365,12 +1383,24 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
|
|||||||
raise ExtractorError(u'no conn, hlsvp or url_encoded_fmt_stream_map information found in video info')
|
raise ExtractorError(u'no conn, hlsvp or url_encoded_fmt_stream_map information found in video info')
|
||||||
|
|
||||||
# Look for the DASH manifest
|
# Look for the DASH manifest
|
||||||
dash_manifest_url_lst = video_info.get('dashmpd')
|
if (self._downloader.params.get('youtube_include_dash_manifest', False)):
|
||||||
if (dash_manifest_url_lst and dash_manifest_url_lst[0] and
|
|
||||||
self._downloader.params.get('youtube_include_dash_manifest', False)):
|
|
||||||
try:
|
try:
|
||||||
|
# The DASH manifest used needs to be the one from the original video_webpage.
|
||||||
|
# The one found in get_video_info seems to be using different signatures.
|
||||||
|
# However, in the case of an age restriction there won't be any embedded dashmpd in the video_webpage.
|
||||||
|
# Luckily, it seems, this case uses some kind of default signature (len == 86), so the
|
||||||
|
# combination of get_video_info and the _static_decrypt_signature() decryption fallback will work here.
|
||||||
|
if age_gate:
|
||||||
|
dash_manifest_url = video_info.get('dashmpd')[0]
|
||||||
|
else:
|
||||||
|
dash_manifest_url = ytplayer_config['args']['dashmpd']
|
||||||
|
def decrypt_sig(mobj):
|
||||||
|
s = mobj.group(1)
|
||||||
|
dec_s = self._decrypt_signature(s, video_id, player_url, age_gate)
|
||||||
|
return '/signature/%s' % dec_s
|
||||||
|
dash_manifest_url = re.sub(r'/s/([\w\.]+)', decrypt_sig, dash_manifest_url)
|
||||||
dash_doc = self._download_xml(
|
dash_doc = self._download_xml(
|
||||||
dash_manifest_url_lst[0], video_id,
|
dash_manifest_url, video_id,
|
||||||
note=u'Downloading DASH manifest',
|
note=u'Downloading DASH manifest',
|
||||||
errnote=u'Could not download DASH manifest')
|
errnote=u'Could not download DASH manifest')
|
||||||
for r in dash_doc.findall(u'.//{urn:mpeg:DASH:schema:MPD:2011}Representation'):
|
for r in dash_doc.findall(u'.//{urn:mpeg:DASH:schema:MPD:2011}Representation'):
|
||||||
@ -1442,9 +1472,9 @@ class YoutubePlaylistIE(YoutubeBaseInfoExtractor):
|
|||||||
|
|
|
|
||||||
((?:PL|EC|UU|FL|RD)[0-9A-Za-z-_]{10,})
|
((?:PL|EC|UU|FL|RD)[0-9A-Za-z-_]{10,})
|
||||||
)"""
|
)"""
|
||||||
_TEMPLATE_URL = 'https://www.youtube.com/playlist?list=%s&page=%s'
|
_TEMPLATE_URL = 'https://www.youtube.com/playlist?list=%s'
|
||||||
_MORE_PAGES_INDICATOR = r'data-link-type="next"'
|
_MORE_PAGES_INDICATOR = r'data-link-type="next"'
|
||||||
_VIDEO_RE = r'href="/watch\?v=(?P<id>[0-9A-Za-z_-]{11})&[^"]*?index=(?P<index>\d+)'
|
_VIDEO_RE = r'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})&[^"]*?index=(?P<index>\d+)'
|
||||||
IE_NAME = u'youtube:playlist'
|
IE_NAME = u'youtube:playlist'
|
||||||
|
|
||||||
def _real_initialize(self):
|
def _real_initialize(self):
|
||||||
@ -1492,29 +1522,31 @@ class YoutubePlaylistIE(YoutubeBaseInfoExtractor):
|
|||||||
raise ExtractorError(u'For downloading YouTube.com top lists, use '
|
raise ExtractorError(u'For downloading YouTube.com top lists, use '
|
||||||
u'the "yttoplist" keyword, for example "youtube-dl \'yttoplist:music:Top Tracks\'"', expected=True)
|
u'the "yttoplist" keyword, for example "youtube-dl \'yttoplist:music:Top Tracks\'"', expected=True)
|
||||||
|
|
||||||
|
url = self._TEMPLATE_URL % playlist_id
|
||||||
|
page = self._download_webpage(url, playlist_id)
|
||||||
|
more_widget_html = content_html = page
|
||||||
|
|
||||||
# Extract the video ids from the playlist pages
|
# Extract the video ids from the playlist pages
|
||||||
ids = []
|
ids = []
|
||||||
|
|
||||||
for page_num in itertools.count(1):
|
for page_num in itertools.count(1):
|
||||||
url = self._TEMPLATE_URL % (playlist_id, page_num)
|
matches = re.finditer(self._VIDEO_RE, content_html)
|
||||||
page = self._download_webpage(url, playlist_id, u'Downloading page #%s' % page_num)
|
|
||||||
matches = re.finditer(self._VIDEO_RE, page)
|
|
||||||
# We remove the duplicates and the link with index 0
|
# We remove the duplicates and the link with index 0
|
||||||
# (it's not the first video of the playlist)
|
# (it's not the first video of the playlist)
|
||||||
new_ids = orderedSet(m.group('id') for m in matches if m.group('index') != '0')
|
new_ids = orderedSet(m.group('id') for m in matches if m.group('index') != '0')
|
||||||
ids.extend(new_ids)
|
ids.extend(new_ids)
|
||||||
|
|
||||||
if re.search(self._MORE_PAGES_INDICATOR, page) is None:
|
mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
|
||||||
|
if not mobj:
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
more = self._download_json(
|
||||||
playlist_title = self._og_search_title(page)
|
'https://youtube.com/%s' % mobj.group('more'), playlist_id, 'Downloading page #%s' % page_num)
|
||||||
except RegexNotFoundError:
|
content_html = more['content_html']
|
||||||
self.report_warning(
|
more_widget_html = more['load_more_widget_html']
|
||||||
u'Playlist page is missing OpenGraph title, falling back ...',
|
|
||||||
playlist_id)
|
|
||||||
playlist_title = self._html_search_regex(
|
playlist_title = self._html_search_regex(
|
||||||
r'<h1 class="pl-header-title">(.*?)</h1>', page, u'title')
|
r'<h1 class="pl-header-title">\s*(.*?)\s*</h1>', page, u'title')
|
||||||
|
|
||||||
url_results = self._ids_to_results(ids)
|
url_results = self._ids_to_results(ids)
|
||||||
return self.playlist_result(url_results, playlist_id, playlist_title)
|
return self.playlist_result(url_results, playlist_id, playlist_title)
|
||||||
@ -1694,7 +1726,8 @@ class YoutubeSearchIE(SearchInfoExtractor):
|
|||||||
api_response = data['data']
|
api_response = data['data']
|
||||||
|
|
||||||
if 'items' not in api_response:
|
if 'items' not in api_response:
|
||||||
raise ExtractorError(u'[youtube] No video results')
|
raise ExtractorError(
|
||||||
|
u'[youtube] No video results', expected=True)
|
||||||
|
|
||||||
new_ids = list(video['id'] for video in api_response['items'])
|
new_ids = list(video['id'] for video in api_response['items'])
|
||||||
video_ids += new_ids
|
video_ids += new_ids
|
||||||
@ -1814,7 +1847,7 @@ class YoutubeTruncatedURLIE(InfoExtractor):
|
|||||||
IE_NAME = 'youtube:truncated_url'
|
IE_NAME = 'youtube:truncated_url'
|
||||||
IE_DESC = False # Do not list
|
IE_DESC = False # Do not list
|
||||||
_VALID_URL = r'''(?x)
|
_VALID_URL = r'''(?x)
|
||||||
(?:https?://)?[^/]+/watch\?feature=[a-z_]+$|
|
(?:https?://)?[^/]+/watch\?(?:feature=[a-z_]+)?$|
|
||||||
(?:https?://)?(?:www\.)?youtube\.com/attribution_link\?a=[^&]+$
|
(?:https?://)?(?:www\.)?youtube\.com/attribution_link\?a=[^&]+$
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import platform
|
|||||||
import re
|
import re
|
||||||
import ssl
|
import ssl
|
||||||
import socket
|
import socket
|
||||||
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
@ -173,6 +174,11 @@ try:
|
|||||||
except NameError:
|
except NameError:
|
||||||
compat_chr = chr
|
compat_chr = chr
|
||||||
|
|
||||||
|
try:
|
||||||
|
from xml.etree.ElementTree import ParseError as compat_xml_parse_error
|
||||||
|
except ImportError: # Python 2.6
|
||||||
|
from xml.parsers.expat import ExpatError as compat_xml_parse_error
|
||||||
|
|
||||||
def compat_ord(c):
|
def compat_ord(c):
|
||||||
if type(c) is int: return c
|
if type(c) is int: return c
|
||||||
else: return ord(c)
|
else: return ord(c)
|
||||||
@ -761,6 +767,7 @@ def unified_strdate(date_str):
|
|||||||
date_str = re.sub(r' ?(\+|-)[0-9]{2}:?[0-9]{2}$', '', date_str)
|
date_str = re.sub(r' ?(\+|-)[0-9]{2}:?[0-9]{2}$', '', date_str)
|
||||||
format_expressions = [
|
format_expressions = [
|
||||||
'%d %B %Y',
|
'%d %B %Y',
|
||||||
|
'%d %b %Y',
|
||||||
'%B %d %Y',
|
'%B %d %Y',
|
||||||
'%b %d %Y',
|
'%b %d %Y',
|
||||||
'%Y-%m-%d',
|
'%Y-%m-%d',
|
||||||
@ -1143,7 +1150,7 @@ def parse_duration(s):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
m = re.match(
|
m = re.match(
|
||||||
r'(?:(?:(?P<hours>[0-9]+):)?(?P<mins>[0-9]+):)?(?P<secs>[0-9]+)$', s)
|
r'(?:(?:(?P<hours>[0-9]+)[:h])?(?P<mins>[0-9]+)[:m])?(?P<secs>[0-9]+)s?$', s)
|
||||||
if not m:
|
if not m:
|
||||||
return None
|
return None
|
||||||
res = int(m.group('secs'))
|
res = int(m.group('secs'))
|
||||||
@ -1220,3 +1227,20 @@ def uppercase_escape(s):
|
|||||||
return re.sub(
|
return re.sub(
|
||||||
r'\\U([0-9a-fA-F]{8})',
|
r'\\U([0-9a-fA-F]{8})',
|
||||||
lambda m: compat_chr(int(m.group(1), base=16)), s)
|
lambda m: compat_chr(int(m.group(1), base=16)), s)
|
||||||
|
|
||||||
|
try:
|
||||||
|
struct.pack(u'!I', 0)
|
||||||
|
except TypeError:
|
||||||
|
# In Python 2.6 (and some 2.7 versions), struct requires a bytes argument
|
||||||
|
def struct_pack(spec, *args):
|
||||||
|
if isinstance(spec, compat_str):
|
||||||
|
spec = spec.encode('ascii')
|
||||||
|
return struct.pack(spec, *args)
|
||||||
|
|
||||||
|
def struct_unpack(spec, *args):
|
||||||
|
if isinstance(spec, compat_str):
|
||||||
|
spec = spec.encode('ascii')
|
||||||
|
return struct.unpack(spec, *args)
|
||||||
|
else:
|
||||||
|
struct_pack = struct.pack
|
||||||
|
struct_unpack = struct.unpack
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
|
|
||||||
__version__ = '2014.02.13'
|
__version__ = '2014.02.22'
|
||||||
|
Reference in New Issue
Block a user