Extraction: Replace youtube-dl with custom-built watch page extraction
This commit is contained in:
parent
9abb83fdbc
commit
4c07546e7a
@ -187,8 +187,17 @@
|
|||||||
.format-ext{
|
.format-ext{
|
||||||
width: 60px;
|
width: 60px;
|
||||||
}
|
}
|
||||||
.format-res{
|
.format-video-quality{
|
||||||
width:90px;
|
width: 140px;
|
||||||
|
}
|
||||||
|
.format-audio-quality{
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
.format-file-size{
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
.format-codecs{
|
||||||
|
width: 120px;
|
||||||
}
|
}
|
||||||
{% endblock style %}
|
{% endblock style %}
|
||||||
|
|
||||||
@ -227,8 +236,10 @@
|
|||||||
<a class="download-link" href="{{ format['url'] }}">
|
<a class="download-link" href="{{ format['url'] }}">
|
||||||
<ol class="format-attributes">
|
<ol class="format-attributes">
|
||||||
<li class="format-ext">{{ format['ext'] }}</li>
|
<li class="format-ext">{{ format['ext'] }}</li>
|
||||||
<li class="format-res">{{ format['resolution'] }}</li>
|
<li class="format-video-quality">{{ format['video_quality'] }}</li>
|
||||||
<li class="format-note">{{ format['note'] }}</li>
|
<li class="format-audio-quality">{{ format['audio_quality'] }}</li>
|
||||||
|
<li class="format-file-size">{{ format['file_size'] }}</li>
|
||||||
|
<li class="format-codecs">{{ format['codecs'] }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -238,7 +249,7 @@
|
|||||||
<input class="checkbox" name="video_info_list" value="{{ video_info }}" form="playlist-edit" type="checkbox">
|
<input class="checkbox" name="video_info_list" value="{{ video_info }}" form="playlist-edit" type="checkbox">
|
||||||
|
|
||||||
|
|
||||||
<span class="description">{{ description }}</span>
|
<span class="description">{{ common_elements.text_runs(description) }}</span>
|
||||||
<div class="music-list">
|
<div class="music-list">
|
||||||
{% if music_list.__len__() != 0 %}
|
{% if music_list.__len__() != 0 %}
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -176,7 +176,7 @@ def fetch_url(url, headers=(), timeout=15, report_text=None, data=None, cookieja
|
|||||||
return content, response
|
return content, response
|
||||||
return content
|
return content
|
||||||
|
|
||||||
mobile_user_agent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
|
mobile_user_agent = 'Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4 Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36'
|
||||||
mobile_ua = (('User-Agent', mobile_user_agent),)
|
mobile_ua = (('User-Agent', mobile_user_agent),)
|
||||||
desktop_user_agent = 'Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0'
|
desktop_user_agent = 'Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0'
|
||||||
desktop_ua = (('User-Agent', desktop_user_agent),)
|
desktop_ua = (('User-Agent', desktop_user_agent),)
|
||||||
@ -312,3 +312,10 @@ def uppercase_escape(s):
|
|||||||
def prefix_url(url):
|
def prefix_url(url):
|
||||||
url = url.lstrip('/') # some urls have // before them, which has a special meaning
|
url = url.lstrip('/') # some urls have // before them, which has a special meaning
|
||||||
return '/' + url
|
return '/' + url
|
||||||
|
|
||||||
|
def left_remove(string, substring):
|
||||||
|
'''removes substring from the start of string, if present'''
|
||||||
|
if string.startswith(substring):
|
||||||
|
return string[len(substring):]
|
||||||
|
return string
|
||||||
|
|
||||||
|
148
youtube/watch.py
148
youtube/watch.py
@ -5,49 +5,15 @@ import settings
|
|||||||
from flask import request
|
from flask import request
|
||||||
import flask
|
import flask
|
||||||
|
|
||||||
from youtube_dl.YoutubeDL import YoutubeDL
|
|
||||||
from youtube_dl.extractor.youtube import YoutubeError
|
|
||||||
import json
|
import json
|
||||||
import html
|
import html
|
||||||
import gevent
|
import gevent
|
||||||
import os
|
import os
|
||||||
|
import math
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
def get_related_items(info):
|
|
||||||
results = []
|
|
||||||
for item in info['related_vids']:
|
|
||||||
if 'list' in item: # playlist:
|
|
||||||
result = watch_page_related_playlist_info(item)
|
|
||||||
else:
|
|
||||||
result = watch_page_related_video_info(item)
|
|
||||||
yt_data_extract.prefix_urls(result)
|
|
||||||
yt_data_extract.add_extra_html_info(result)
|
|
||||||
results.append(result)
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
# json of related items retrieved directly from the watch page has different names for everything
|
|
||||||
# converts these to standard names
|
|
||||||
def watch_page_related_video_info(item):
|
|
||||||
result = {key: item[key] for key in ('id', 'title', 'author')}
|
|
||||||
result['duration'] = util.seconds_to_timestamp(item['length_seconds'])
|
|
||||||
try:
|
|
||||||
result['views'] = item['short_view_count_text']
|
|
||||||
except KeyError:
|
|
||||||
result['views'] = ''
|
|
||||||
result['thumbnail'] = util.get_thumbnail_url(item['id'])
|
|
||||||
result['type'] = 'video'
|
|
||||||
return result
|
|
||||||
|
|
||||||
def watch_page_related_playlist_info(item):
|
|
||||||
return {
|
|
||||||
'size': item['playlist_length'] if item['playlist_length'] != "0" else "50+",
|
|
||||||
'title': item['playlist_title'],
|
|
||||||
'id': item['list'],
|
|
||||||
'first_video_id': item['video_id'],
|
|
||||||
'thumbnail': util.get_thumbnail_url(item['video_id']),
|
|
||||||
'type': 'playlist',
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_video_sources(info):
|
def get_video_sources(info):
|
||||||
video_sources = []
|
video_sources = []
|
||||||
@ -55,9 +21,10 @@ def get_video_sources(info):
|
|||||||
max_resolution = 360
|
max_resolution = 360
|
||||||
else:
|
else:
|
||||||
max_resolution = settings.default_resolution
|
max_resolution = settings.default_resolution
|
||||||
|
|
||||||
for format in info['formats']:
|
for format in info['formats']:
|
||||||
if format['acodec'] != 'none' and format['vcodec'] != 'none' and format['height'] <= max_resolution:
|
if not all(attr in format for attr in ('height', 'width', 'ext', 'url')):
|
||||||
|
continue
|
||||||
|
if 'acodec' in format and 'vcodec' in format and format['height'] <= max_resolution:
|
||||||
video_sources.append({
|
video_sources.append({
|
||||||
'src': format['url'],
|
'src': format['url'],
|
||||||
'type': 'video/' + format['ext'],
|
'type': 'video/' + format['ext'],
|
||||||
@ -134,14 +101,57 @@ def get_ordered_music_list_attributes(music_list):
|
|||||||
|
|
||||||
return ordered_attributes
|
return ordered_attributes
|
||||||
|
|
||||||
|
headers = (
|
||||||
|
('Accept', '*/*'),
|
||||||
|
('Accept-Language', 'en-US,en;q=0.5'),
|
||||||
|
('X-YouTube-Client-Name', '2'),
|
||||||
|
('X-YouTube-Client-Version', '2.20180830'),
|
||||||
|
) + util.mobile_ua
|
||||||
|
|
||||||
def extract_info(downloader, *args, **kwargs):
|
def extract_info(video_id):
|
||||||
|
polymer_json = util.fetch_url('https://m.youtube.com/watch?v=' + video_id + '&pbj=1', headers=headers, debug_name='watch')
|
||||||
try:
|
try:
|
||||||
return downloader.extract_info(*args, **kwargs)
|
polymer_json = json.loads(polymer_json)
|
||||||
except YoutubeError as e:
|
except json.decoder.JSONDecodeError:
|
||||||
return str(e)
|
traceback.print_exc()
|
||||||
|
return {'error': 'Failed to parse json response'}
|
||||||
|
return yt_data_extract.extract_watch_info(polymer_json)
|
||||||
|
|
||||||
|
def video_quality_string(format):
|
||||||
|
if 'vcodec' in format:
|
||||||
|
result =str(format.get('width', '?')) + 'x' + str(format.get('height', '?'))
|
||||||
|
if 'fps' in format:
|
||||||
|
result += ' ' + format['fps'] + 'fps'
|
||||||
|
return result
|
||||||
|
elif 'acodec' in format:
|
||||||
|
return 'audio only'
|
||||||
|
|
||||||
|
return '?'
|
||||||
|
|
||||||
|
def audio_quality_string(format):
|
||||||
|
if 'acodec' in format:
|
||||||
|
result = str(format.get('abr', '?')) + 'k'
|
||||||
|
if 'audio_sample_rate' in format:
|
||||||
|
result += ' ' + str(format['audio_sample_rate']) + ' Hz'
|
||||||
|
return result
|
||||||
|
elif 'vcodec' in format:
|
||||||
|
return 'video only'
|
||||||
|
|
||||||
|
return '?'
|
||||||
|
|
||||||
|
# from https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/utils.py
|
||||||
|
def format_bytes(bytes):
|
||||||
|
if bytes is None:
|
||||||
|
return 'N/A'
|
||||||
|
if type(bytes) is str:
|
||||||
|
bytes = float(bytes)
|
||||||
|
if bytes == 0.0:
|
||||||
|
exponent = 0
|
||||||
|
else:
|
||||||
|
exponent = int(math.log(bytes, 1024.0))
|
||||||
|
suffix = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'][exponent]
|
||||||
|
converted = float(bytes) / float(1024 ** exponent)
|
||||||
|
return '%.2f%s' % (converted, suffix)
|
||||||
|
|
||||||
|
|
||||||
@yt_app.route('/watch')
|
@yt_app.route('/watch')
|
||||||
@ -152,38 +162,26 @@ def get_watch_page():
|
|||||||
flask.abort(flask.Response('Incomplete video id (too short): ' + video_id))
|
flask.abort(flask.Response('Incomplete video id (too short): ' + video_id))
|
||||||
|
|
||||||
lc = request.args.get('lc', '')
|
lc = request.args.get('lc', '')
|
||||||
if settings.route_tor:
|
|
||||||
proxy = 'socks5://127.0.0.1:9150/'
|
|
||||||
else:
|
|
||||||
proxy = ''
|
|
||||||
yt_dl_downloader = YoutubeDL(params={'youtube_include_dash_manifest':False, 'proxy':proxy})
|
|
||||||
tasks = (
|
tasks = (
|
||||||
gevent.spawn(comments.video_comments, video_id, int(settings.default_comment_sorting), lc=lc ),
|
gevent.spawn(comments.video_comments, video_id, int(settings.default_comment_sorting), lc=lc ),
|
||||||
gevent.spawn(extract_info, yt_dl_downloader, "https://www.youtube.com/watch?v=" + video_id, download=False)
|
gevent.spawn(extract_info, video_id)
|
||||||
)
|
)
|
||||||
gevent.joinall(tasks)
|
gevent.joinall(tasks)
|
||||||
comments_info, info = tasks[0].value, tasks[1].value
|
comments_info, info = tasks[0].value, tasks[1].value
|
||||||
|
|
||||||
if isinstance(info, str): # youtube error
|
if info['error']:
|
||||||
return flask.render_template('error.html', error_message = info)
|
return flask.render_template('error.html', error_message = info['error'])
|
||||||
|
|
||||||
video_info = {
|
video_info = {
|
||||||
"duration": util.seconds_to_timestamp(info["duration"]),
|
"duration": util.seconds_to_timestamp(info["duration"] or 0),
|
||||||
"id": info['id'],
|
"id": info['id'],
|
||||||
"title": info['title'],
|
"title": info['title'],
|
||||||
"author": info['uploader'],
|
"author": info['author'],
|
||||||
}
|
}
|
||||||
|
|
||||||
upload_year = info["upload_date"][0:4]
|
for item in info['related_videos']:
|
||||||
upload_month = info["upload_date"][4:6]
|
yt_data_extract.prefix_urls(item)
|
||||||
upload_day = info["upload_date"][6:8]
|
yt_data_extract.add_extra_html_info(item)
|
||||||
upload_date = upload_month + "/" + upload_day + "/" + upload_year
|
|
||||||
|
|
||||||
if settings.related_videos_mode:
|
|
||||||
related_videos = get_related_items(info)
|
|
||||||
else:
|
|
||||||
related_videos = []
|
|
||||||
|
|
||||||
|
|
||||||
if settings.gather_googlevideo_domains:
|
if settings.gather_googlevideo_domains:
|
||||||
with open(os.path.join(settings.data_dir, 'googlevideo-domains.txt'), 'a+', encoding='utf-8') as f:
|
with open(os.path.join(settings.data_dir, 'googlevideo-domains.txt'), 'a+', encoding='utf-8') as f:
|
||||||
@ -195,23 +193,29 @@ def get_watch_page():
|
|||||||
download_formats = []
|
download_formats = []
|
||||||
|
|
||||||
for format in info['formats']:
|
for format in info['formats']:
|
||||||
|
if 'acodec' in format and 'vcodec' in format:
|
||||||
|
codecs_string = format['acodec'] + ', ' + format['vcodec']
|
||||||
|
else:
|
||||||
|
codecs_string = format.get('acodec') or format.get('vcodec') or '?'
|
||||||
download_formats.append({
|
download_formats.append({
|
||||||
'url': format['url'],
|
'url': format['url'],
|
||||||
'ext': format['ext'],
|
'ext': format.get('ext', '?'),
|
||||||
'resolution': yt_dl_downloader.format_resolution(format),
|
'audio_quality': audio_quality_string(format),
|
||||||
'note': yt_dl_downloader._format_note(format),
|
'video_quality': video_quality_string(format),
|
||||||
|
'file_size': format_bytes(format['file_size']),
|
||||||
|
'codecs': codecs_string,
|
||||||
})
|
})
|
||||||
|
|
||||||
video_sources = get_video_sources(info)
|
video_sources = get_video_sources(info)
|
||||||
video_height = video_sources[0]['height']
|
video_height = yt_data_extract.default_multi_get(video_sources, 0, 'height', default=360)
|
||||||
|
video_width = yt_data_extract.default_multi_get(video_sources, 0, 'width', default=640)
|
||||||
# 1 second per pixel, or the actual video width
|
# 1 second per pixel, or the actual video width
|
||||||
theater_video_target_width = max(640, info['duration'], video_sources[0]['width'])
|
theater_video_target_width = max(640, info['duration'] or 0, video_width)
|
||||||
|
|
||||||
return flask.render_template('watch.html',
|
return flask.render_template('watch.html',
|
||||||
header_playlist_names = local_playlist.get_playlist_names(),
|
header_playlist_names = local_playlist.get_playlist_names(),
|
||||||
uploader_channel_url = '/' + info['uploader_url'],
|
uploader_channel_url = ('/' + info['author_url']) if info['author_url'] else '',
|
||||||
upload_date = upload_date,
|
upload_date = info['published_date'],
|
||||||
views = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("view_count", None)),
|
views = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("view_count", None)),
|
||||||
likes = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("like_count", None)),
|
likes = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("like_count", None)),
|
||||||
dislikes = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("dislike_count", None)),
|
dislikes = (lambda x: '{:,}'.format(x) if x is not None else "")(info.get("dislike_count", None)),
|
||||||
@ -219,7 +223,7 @@ def get_watch_page():
|
|||||||
video_info = json.dumps(video_info),
|
video_info = json.dumps(video_info),
|
||||||
video_sources = video_sources,
|
video_sources = video_sources,
|
||||||
subtitle_sources = get_subtitle_sources(info),
|
subtitle_sources = get_subtitle_sources(info),
|
||||||
related = related_videos,
|
related = info['related_videos'],
|
||||||
music_list = info['music_list'],
|
music_list = info['music_list'],
|
||||||
music_attributes = get_ordered_music_list_attributes(info['music_list']),
|
music_attributes = get_ordered_music_list_attributes(info['music_list']),
|
||||||
comments_info = comments_info,
|
comments_info = comments_info,
|
||||||
@ -232,7 +236,7 @@ def get_watch_page():
|
|||||||
theater_video_target_width = theater_video_target_width,
|
theater_video_target_width = theater_video_target_width,
|
||||||
|
|
||||||
title = info['title'],
|
title = info['title'],
|
||||||
uploader = info['uploader'],
|
uploader = info['author'],
|
||||||
description = info['description'],
|
description = info['description'],
|
||||||
unlisted = info['unlisted'],
|
unlisted = info['unlisted'],
|
||||||
)
|
)
|
||||||
|
@ -6,6 +6,7 @@ import re
|
|||||||
import urllib
|
import urllib
|
||||||
import collections
|
import collections
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
import traceback
|
||||||
|
|
||||||
# videos (all of type str):
|
# videos (all of type str):
|
||||||
|
|
||||||
@ -36,8 +37,112 @@ from math import ceil
|
|||||||
# size
|
# size
|
||||||
# first_video_id
|
# first_video_id
|
||||||
|
|
||||||
|
# from https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/youtube.py
|
||||||
|
_formats = {
|
||||||
|
'5': {'ext': 'flv', 'width': 400, 'height': 240, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
|
||||||
|
'6': {'ext': 'flv', 'width': 450, 'height': 270, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
|
||||||
|
'13': {'ext': '3gp', 'acodec': 'aac', 'vcodec': 'mp4v'},
|
||||||
|
'17': {'ext': '3gp', 'width': 176, 'height': 144, 'acodec': 'aac', 'abr': 24, 'vcodec': 'mp4v'},
|
||||||
|
'18': {'ext': 'mp4', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 96, 'vcodec': 'h264'},
|
||||||
|
'22': {'ext': 'mp4', 'width': 1280, 'height': 720, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
|
||||||
|
'34': {'ext': 'flv', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
|
||||||
|
'35': {'ext': 'flv', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
|
||||||
|
# itag 36 videos are either 320x180 (BaW_jenozKc) or 320x240 (__2ABJjxzNo), abr varies as well
|
||||||
|
'36': {'ext': '3gp', 'width': 320, 'acodec': 'aac', 'vcodec': 'mp4v'},
|
||||||
|
'37': {'ext': 'mp4', 'width': 1920, 'height': 1080, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
|
||||||
|
'38': {'ext': 'mp4', 'width': 4096, 'height': 3072, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
|
||||||
|
'43': {'ext': 'webm', 'width': 640, 'height': 360, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
|
||||||
|
'44': {'ext': 'webm', 'width': 854, 'height': 480, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
|
||||||
|
'45': {'ext': 'webm', 'width': 1280, 'height': 720, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
|
||||||
|
'46': {'ext': 'webm', 'width': 1920, 'height': 1080, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
|
||||||
|
'59': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
|
||||||
|
'78': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
|
||||||
|
|
||||||
|
|
||||||
|
# 3D videos
|
||||||
|
'82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
|
||||||
|
'83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
|
||||||
|
'84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
|
||||||
|
'85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
|
||||||
|
'100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
|
||||||
|
'101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
|
||||||
|
'102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
|
||||||
|
|
||||||
|
# Apple HTTP Live Streaming
|
||||||
|
'91': {'ext': 'mp4', 'height': 144, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264'},
|
||||||
|
'92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264'},
|
||||||
|
'93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
|
||||||
|
'94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
|
||||||
|
'95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264'},
|
||||||
|
'96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264'},
|
||||||
|
'132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264'},
|
||||||
|
'151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 24, 'vcodec': 'h264'},
|
||||||
|
|
||||||
|
# DASH mp4 video
|
||||||
|
'133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
|
'134': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
|
'135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
|
'136': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
|
'137': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
|
'138': {'ext': 'mp4', 'format_note': 'DASH video', 'vcodec': 'h264'}, # Height can vary (https://github.com/ytdl-org/youtube-dl/issues/4559)
|
||||||
|
'160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
|
'212': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
|
'264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
|
'298': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
|
||||||
|
'299': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
|
||||||
|
'266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
|
|
||||||
|
# Dash mp4 audio
|
||||||
|
'139': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 48, 'container': 'm4a_dash'},
|
||||||
|
'140': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 128, 'container': 'm4a_dash'},
|
||||||
|
'141': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 256, 'container': 'm4a_dash'},
|
||||||
|
'256': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
|
||||||
|
'258': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
|
||||||
|
'325': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'dtse', 'container': 'm4a_dash'},
|
||||||
|
'328': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'ec-3', 'container': 'm4a_dash'},
|
||||||
|
|
||||||
|
# Dash webm
|
||||||
|
'167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
|
||||||
|
'168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
|
||||||
|
'169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
|
||||||
|
'170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
|
||||||
|
'218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
|
||||||
|
'219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
|
||||||
|
'278': {'ext': 'webm', 'height': 144, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp9'},
|
||||||
|
'242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
|
'243': {'ext': 'webm', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
|
'244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
|
'245': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
|
'246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
|
'247': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
|
'248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
|
'271': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
|
# itag 272 videos are either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
|
||||||
|
'272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
|
'302': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
|
||||||
|
'303': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
|
||||||
|
'308': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
|
||||||
|
'313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
|
'315': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
|
||||||
|
|
||||||
|
# Dash webm audio
|
||||||
|
'171': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 128},
|
||||||
|
'172': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 256},
|
||||||
|
|
||||||
|
# Dash webm audio with opus inside
|
||||||
|
'249': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50},
|
||||||
|
'250': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70},
|
||||||
|
'251': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160},
|
||||||
|
|
||||||
|
# RTMP (unnamed)
|
||||||
|
'_rtmp': {'protocol': 'rtmp'},
|
||||||
|
|
||||||
|
# av01 video only formats sometimes served with "unknown" codecs
|
||||||
|
'394': {'vcodec': 'av01.0.05M.08'},
|
||||||
|
'395': {'vcodec': 'av01.0.05M.08'},
|
||||||
|
'396': {'vcodec': 'av01.0.05M.08'},
|
||||||
|
'397': {'vcodec': 'av01.0.05M.08'},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_plain_text(node):
|
def get_plain_text(node):
|
||||||
@ -59,7 +164,7 @@ def format_text_runs(runs):
|
|||||||
result += html.escape(text_run["text"])
|
result += html.escape(text_run["text"])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def default_get(object, key, default, types=()):
|
def default_get(object, key, default=None, types=()):
|
||||||
'''Like dict.get(), but returns default if the result doesn't match one of the types.
|
'''Like dict.get(), but returns default if the result doesn't match one of the types.
|
||||||
Also works for indexing lists.'''
|
Also works for indexing lists.'''
|
||||||
try:
|
try:
|
||||||
@ -74,7 +179,7 @@ def default_get(object, key, default, types=()):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def default_multi_get(object, *keys, default, types=()):
|
def default_multi_get(object, *keys, default=None, types=()):
|
||||||
'''Like dict.get(), but for nested dictionaries/sequences, supporting keys or indices.
|
'''Like dict.get(), but for nested dictionaries/sequences, supporting keys or indices.
|
||||||
Last argument is the default value to use in case of any IndexErrors or KeyErrors.
|
Last argument is the default value to use in case of any IndexErrors or KeyErrors.
|
||||||
If types is given and the result doesn't match one of those types, default is returned'''
|
If types is given and the result doesn't match one of those types, default is returned'''
|
||||||
@ -106,6 +211,11 @@ def multi_default_multi_get(object, *key_sequences, default=None, types=()):
|
|||||||
continue
|
continue
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
def remove_redirect(url):
|
||||||
|
if re.fullmatch(r'(((https?:)?//)?(www.)?youtube.com)?/redirect\?.*', url) is not None: # youtube puts these on external links to do tracking
|
||||||
|
query_string = url[url.find('?')+1: ]
|
||||||
|
return urllib.parse.parse_qs(query_string)['q'][0]
|
||||||
|
return url
|
||||||
|
|
||||||
def get_url(node):
|
def get_url(node):
|
||||||
try:
|
try:
|
||||||
@ -239,9 +349,9 @@ def renderer_info(renderer, additional_info={}):
|
|||||||
type = list(renderer.keys())[0]
|
type = list(renderer.keys())[0]
|
||||||
renderer = renderer[type]
|
renderer = renderer[type]
|
||||||
info = {}
|
info = {}
|
||||||
if type == 'itemSectionRenderer':
|
if type in ('itemSectionRenderer', 'compactAutoplayRenderer'):
|
||||||
return renderer_info(renderer['contents'][0], additional_info)
|
return renderer_info(renderer['contents'][0], additional_info)
|
||||||
|
|
||||||
if type in ('movieRenderer', 'clarificationRenderer'):
|
if type in ('movieRenderer', 'clarificationRenderer'):
|
||||||
info['type'] = 'unsupported'
|
info['type'] = 'unsupported'
|
||||||
return info
|
return info
|
||||||
@ -345,6 +455,7 @@ item_types = {
|
|||||||
|
|
||||||
'videoRenderer',
|
'videoRenderer',
|
||||||
'compactVideoRenderer',
|
'compactVideoRenderer',
|
||||||
|
'compactAutoplayRenderer',
|
||||||
'gridVideoRenderer',
|
'gridVideoRenderer',
|
||||||
'playlistVideoRenderer',
|
'playlistVideoRenderer',
|
||||||
|
|
||||||
@ -378,6 +489,11 @@ def traverse_browse_renderer(renderer):
|
|||||||
print('Could not find tab with content')
|
print('Could not find tab with content')
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def traverse_standard_list(renderer):
|
||||||
|
renderer_list = multi_default_multi_get(renderer, ['contents'], ['items'], default=(), types=(list, tuple))
|
||||||
|
continuation = default_multi_get(renderer, 'continuations', 0, 'nextContinuationData', 'continuation')
|
||||||
|
return renderer_list, continuation
|
||||||
|
|
||||||
# these renderers contain one inside them
|
# these renderers contain one inside them
|
||||||
nested_renderer_dispatch = {
|
nested_renderer_dispatch = {
|
||||||
'singleColumnBrowseResultsRenderer': traverse_browse_renderer,
|
'singleColumnBrowseResultsRenderer': traverse_browse_renderer,
|
||||||
@ -385,7 +501,16 @@ nested_renderer_dispatch = {
|
|||||||
'twoColumnSearchResultsRenderer': lambda renderer: default_get(renderer, 'primaryContents', {}, types=dict),
|
'twoColumnSearchResultsRenderer': lambda renderer: default_get(renderer, 'primaryContents', {}, types=dict),
|
||||||
}
|
}
|
||||||
|
|
||||||
def extract_items(response):
|
# these renderers contain a list of renderers in side them
|
||||||
|
nested_renderer_list_dispatch = {
|
||||||
|
'sectionListRenderer': traverse_standard_list,
|
||||||
|
'itemSectionRenderer': traverse_standard_list,
|
||||||
|
'gridRenderer': traverse_standard_list,
|
||||||
|
'playlistVideoListRenderer': traverse_standard_list,
|
||||||
|
'singleColumnWatchNextResults': lambda r: (default_multi_get(r, 'results', 'results', 'contents', default=[], types=(list, tuple)), None),
|
||||||
|
}
|
||||||
|
|
||||||
|
def extract_items(response, item_types=item_types):
|
||||||
'''return items, ctoken'''
|
'''return items, ctoken'''
|
||||||
if 'continuationContents' in response:
|
if 'continuationContents' in response:
|
||||||
# always has just the one [something]Continuation key, but do this just in case they add some tracking key or something
|
# always has just the one [something]Continuation key, but do this just in case they add some tracking key or something
|
||||||
@ -414,13 +539,11 @@ def extract_items(response):
|
|||||||
key, value = list(renderer.items())[0]
|
key, value = list(renderer.items())[0]
|
||||||
|
|
||||||
# has a list in it, add it to the iter stack
|
# has a list in it, add it to the iter stack
|
||||||
if key in list_types:
|
if key in nested_renderer_list_dispatch:
|
||||||
renderer_list = multi_default_multi_get(value, ['contents'], ['items'], default=(), types=(list, tuple))
|
renderer_list, continuation = nested_renderer_list_dispatch[key](value)
|
||||||
if renderer_list:
|
if renderer_list:
|
||||||
iter_stack.append(current_iter)
|
iter_stack.append(current_iter)
|
||||||
current_iter = iter(renderer_list)
|
current_iter = iter(renderer_list)
|
||||||
|
|
||||||
continuation = default_multi_get(value, 'continuations', 0, 'nextContinuationData', 'continuation', default=None, types=str)
|
|
||||||
if continuation:
|
if continuation:
|
||||||
ctoken = continuation
|
ctoken = continuation
|
||||||
|
|
||||||
@ -506,10 +629,7 @@ def extract_channel_info(polymer_json, tab):
|
|||||||
|
|
||||||
info['links'] = []
|
info['links'] = []
|
||||||
for link_json in channel_metadata.get('primaryLinks', ()):
|
for link_json in channel_metadata.get('primaryLinks', ()):
|
||||||
url = link_json['navigationEndpoint']['urlEndpoint']['url']
|
url = remove_redirect(link_json['navigationEndpoint']['urlEndpoint']['url'])
|
||||||
if url.startswith('/redirect'): # youtube puts these on external links to do tracking
|
|
||||||
query_string = url[url.find('?')+1: ]
|
|
||||||
url = urllib.parse.parse_qs(query_string)['q'][0]
|
|
||||||
|
|
||||||
text = get_plain_text(link_json['title'])
|
text = get_plain_text(link_json['title'])
|
||||||
|
|
||||||
@ -699,5 +819,290 @@ def parse_comments_polymer(polymer_json):
|
|||||||
'sort': metadata['sort'],
|
'sort': metadata['sort'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def check_missing_keys(object, *key_sequences):
|
||||||
|
for key_sequence in key_sequences:
|
||||||
|
_object = object
|
||||||
|
try:
|
||||||
|
for key in key_sequence:
|
||||||
|
_object = object[key]
|
||||||
|
except (KeyError, IndexError, TypeError):
|
||||||
|
return 'Could not find ' + key
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def extract_plain_text(node, default=None):
|
||||||
|
if isinstance(node, str):
|
||||||
|
return node
|
||||||
|
|
||||||
|
try:
|
||||||
|
return node['simpleText']
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
return ''.join(text_run['text'] for text_run in node['runs'])
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
def extract_formatted_text(node):
|
||||||
|
try:
|
||||||
|
result = []
|
||||||
|
runs = node['runs']
|
||||||
|
for run in runs:
|
||||||
|
url = default_multi_get(run, 'navigationEndpoint', 'urlEndpoint', 'url')
|
||||||
|
if url is not None:
|
||||||
|
run['url'] = remove_redirect(url)
|
||||||
|
run['text'] = run['url'] # youtube truncates the url text, we don't want that nonsense
|
||||||
|
return runs
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
traceback.print_exc()
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
return [{'text': node['simpleText']}]
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def extract_integer(string):
|
||||||
|
if not isinstance(string, str):
|
||||||
|
return None
|
||||||
|
match = re.search(r'(\d+)', string.replace(',', ''))
|
||||||
|
if match is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(match.group(1))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def extract_metadata_row_info(video_renderer_info):
|
||||||
|
# extract category and music list
|
||||||
|
info = {
|
||||||
|
'category': None,
|
||||||
|
'music_list': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
current_song = {}
|
||||||
|
for row in default_multi_get(video_renderer_info, 'metadataRowContainer', 'metadataRowContainerRenderer', 'rows', default=[]):
|
||||||
|
row_title = extract_plain_text(default_multi_get(row, 'metadataRowRenderer', 'title'), default='')
|
||||||
|
row_content = extract_plain_text(default_multi_get(row, 'metadataRowRenderer', 'contents', 0))
|
||||||
|
if row_title == 'Category':
|
||||||
|
info['category'] = row_content
|
||||||
|
elif row_title in ('Song', 'Music'):
|
||||||
|
if current_song:
|
||||||
|
info['music_list'].append(current_song)
|
||||||
|
current_song = {'title': row_content}
|
||||||
|
elif row_title == 'Artist':
|
||||||
|
current_song['artist'] = row_content
|
||||||
|
elif row_title == 'Album':
|
||||||
|
current_song['album'] = row_content
|
||||||
|
elif row_title == 'Writers':
|
||||||
|
current_song['writers'] = row_content
|
||||||
|
elif row_title.startswith('Licensed'):
|
||||||
|
current_song['licensor'] = row_content
|
||||||
|
if current_song:
|
||||||
|
info['music_list'].append(current_song)
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def extract_watch_info_mobile(top_level):
|
||||||
|
info = {}
|
||||||
|
microformat = default_multi_get(top_level, 'playerResponse', 'microformat', 'playerMicroformatRenderer', default={})
|
||||||
|
|
||||||
|
info['allowed_countries'] = microformat.get('availableCountries', [])
|
||||||
|
info['published_date'] = microformat.get('publishDate')
|
||||||
|
|
||||||
|
response = top_level.get('response', {})
|
||||||
|
|
||||||
|
# video info from metadata renderers
|
||||||
|
items, _ = extract_items(response, item_types={'slimVideoMetadataRenderer'})
|
||||||
|
if items:
|
||||||
|
video_info = items[0]['slimVideoMetadataRenderer']
|
||||||
|
else:
|
||||||
|
print('Failed to extract video metadata')
|
||||||
|
video_info = {}
|
||||||
|
|
||||||
|
info.update(extract_metadata_row_info(video_info))
|
||||||
|
#info['description'] = extract_formatted_text(video_info.get('description'))
|
||||||
|
info['like_count'] = None
|
||||||
|
info['dislike_count'] = None
|
||||||
|
for button in video_info.get('buttons', ()):
|
||||||
|
button_renderer = button.get('slimMetadataToggleButtonRenderer', {})
|
||||||
|
|
||||||
|
# all the digits can be found in the accessibility data
|
||||||
|
count = extract_integer(default_multi_get(button_renderer, 'button', 'toggleButtonRenderer', 'defaultText', 'accessibility', 'accessibilityData', 'label'))
|
||||||
|
|
||||||
|
# this count doesn't have all the digits, it's like 53K for instance
|
||||||
|
dumb_count = extract_integer(extract_plain_text(default_multi_get(button_renderer, 'button', 'toggleButtonRenderer', 'defaultText')))
|
||||||
|
|
||||||
|
# the accessibility text will be "No likes" or "No dislikes" or something like that, but dumb count will be 0
|
||||||
|
if dumb_count == 0:
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
if 'isLike' in button_renderer:
|
||||||
|
info['like_count'] = count
|
||||||
|
elif 'isDislike' in button_renderer:
|
||||||
|
info['dislike_count'] = count
|
||||||
|
|
||||||
|
# comment section info
|
||||||
|
items, _ = extract_items(response, item_types={'commentSectionRenderer'})
|
||||||
|
if items:
|
||||||
|
comment_info = items[0]['commentSectionRenderer']
|
||||||
|
comment_count_text = extract_plain_text(default_multi_get(comment_info, 'header', 'commentSectionHeaderRenderer', 'countText'))
|
||||||
|
if comment_count_text == 'Comments': # just this with no number, means 0 comments
|
||||||
|
info['comment_count'] = 0
|
||||||
|
else:
|
||||||
|
info['comment_count'] = extract_integer(comment_count_text)
|
||||||
|
info['comments_disabled'] = False
|
||||||
|
else: # no comment section present means comments are disabled
|
||||||
|
info['comment_count'] = 0
|
||||||
|
info['comments_disabled'] = True
|
||||||
|
|
||||||
|
# related videos
|
||||||
|
related, _ = extract_items(response)
|
||||||
|
info['related_videos'] = [renderer_info(renderer) for renderer in related]
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
month_abbreviations = {'jan':'1', 'feb':'2', 'mar':'3', 'apr':'4', 'may':'5', 'jun':'6', 'jul':'7', 'aug':'8', 'sep':'9', 'oct':'10', 'nov':'11', 'dec':'12'}
|
||||||
|
def extract_watch_info_desktop(top_level):
|
||||||
|
info = {
|
||||||
|
'comment_count': None,
|
||||||
|
'comments_disabled': None,
|
||||||
|
'allowed_countries': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
video_info = {}
|
||||||
|
for renderer in default_multi_get(top_level, 'response', 'contents', 'twoColumnWatchNextResults', 'results', 'results', 'contents', default=()):
|
||||||
|
if renderer and list(renderer.keys())[0] in ('videoPrimaryInfoRenderer', 'videoSecondaryInfoRenderer'):
|
||||||
|
video_info.update(list(renderer.values())[0])
|
||||||
|
|
||||||
|
info.update(extract_metadata_row_info(video_info))
|
||||||
|
#info['description'] = extract_formatted_text(video_info.get('description', None))
|
||||||
|
info['published_date'] = None
|
||||||
|
date_text = extract_plain_text(video_info.get('dateText', None))
|
||||||
|
if date_text is not None:
|
||||||
|
date_text = util.left_remove(date_text.lower(), 'published on ').replace(',', '')
|
||||||
|
parts = date_text.split()
|
||||||
|
if len(parts) == 3:
|
||||||
|
month, day, year = date_text.split()
|
||||||
|
month = month_abbreviations.get(month[0:3]) # slicing in case they start writing out the full month name
|
||||||
|
if month and (re.fullmatch(r'\d\d?', day) is not None) and (re.fullmatch(r'\d{4}', year) is not None):
|
||||||
|
info['published_date'] = year + '-' + month + '-' + day
|
||||||
|
|
||||||
|
likes_dislikes = default_multi_get(video_info, 'sentimentBar', 'sentimentBarRenderer', 'tooltip', default='').split('/')
|
||||||
|
if len(likes_dislikes) == 2:
|
||||||
|
info['like_count'] = extract_integer(likes_dislikes[0])
|
||||||
|
info['dislike_count'] = extract_integer(likes_dislikes[1])
|
||||||
|
else:
|
||||||
|
info['like_count'] = None
|
||||||
|
info['dislike_count'] = None
|
||||||
|
|
||||||
|
#info['title'] = extract_plain_text(video_info.get('title', None))
|
||||||
|
#info['author'] = extract_plain_text(default_multi_get(video_info, 'owner', 'videoOwnerRenderer', 'title'))
|
||||||
|
#info['author_id'] = default_multi_get(video_info, 'owner', 'videoOwnerRenderer', 'navigationEndpoint', 'browseEndpoint', 'browseId')
|
||||||
|
#info['view_count'] = extract_integer(extract_plain_text(default_multi_get(video_info, 'viewCount', 'videoViewCountRenderer', 'viewCount')))
|
||||||
|
|
||||||
|
related = default_multi_get(top_level, 'response', 'contents', 'twoColumnWatchNextResults', 'secondaryResults', 'secondaryResults', 'results', default=[])
|
||||||
|
info['related_videos'] = [renderer_info(renderer) for renderer in related]
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
def extract_watch_info(polymer_json):
|
||||||
|
info = {'playability_error': None, 'error': None}
|
||||||
|
|
||||||
|
if isinstance(polymer_json, dict):
|
||||||
|
top_level = polymer_json
|
||||||
|
elif isinstance(polymer_json, (list, tuple)):
|
||||||
|
top_level = {}
|
||||||
|
for page_part in polymer_json:
|
||||||
|
if not isinstance(page_part, dict):
|
||||||
|
return {'error': 'Invalid page part'}
|
||||||
|
top_level.update(page_part)
|
||||||
|
else:
|
||||||
|
return {'error': 'Invalid top level polymer data'}
|
||||||
|
|
||||||
|
error = check_missing_keys(top_level,
|
||||||
|
['playerResponse'],
|
||||||
|
)
|
||||||
|
if error:
|
||||||
|
return {'error': error}
|
||||||
|
|
||||||
|
error = check_missing_keys(top_level,
|
||||||
|
['player', 'args'],
|
||||||
|
['player', 'assets', 'js'],
|
||||||
|
)
|
||||||
|
if error:
|
||||||
|
info['playability_error'] = error
|
||||||
|
|
||||||
|
|
||||||
|
player_args = default_multi_get(top_level, 'player', 'args', default={})
|
||||||
|
parsed_formats = []
|
||||||
|
|
||||||
|
if 'url_encoded_fmt_stream_map' in player_args:
|
||||||
|
string_formats = player_args['url_encoded_fmt_stream_map'].split(',')
|
||||||
|
parsed_formats += [dict(urllib.parse.parse_qsl(fmt_string)) for fmt_string in string_formats if fmt_string]
|
||||||
|
|
||||||
|
if 'adaptive_fmts' in player_args:
|
||||||
|
string_formats = player_args['adaptive_fmts'].split(',')
|
||||||
|
parsed_formats += [dict(urllib.parse.parse_qsl(fmt_string)) for fmt_string in string_formats if fmt_string]
|
||||||
|
|
||||||
|
info['formats'] = []
|
||||||
|
|
||||||
|
for parsed_fmt in parsed_formats:
|
||||||
|
# start with defaults from the big table at the top
|
||||||
|
if 'itag' in parsed_fmt:
|
||||||
|
fmt = _formats.get(parsed_fmt['itag'], {}).copy()
|
||||||
|
else:
|
||||||
|
fmt = {}
|
||||||
|
|
||||||
|
# then override them
|
||||||
|
fmt.update(parsed_fmt)
|
||||||
|
try:
|
||||||
|
fmt['width'], fmt['height'] = map(int, fmt['size'].split('x'))
|
||||||
|
except (KeyError, ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
fmt['file_size'] = None
|
||||||
|
if 'clen' in fmt:
|
||||||
|
fmt['file_size'] = int(fmt.get('clen'))
|
||||||
|
else:
|
||||||
|
match = re.search(r'&clen=(\d+)', fmt.get('url'))
|
||||||
|
if match:
|
||||||
|
fmt['file_size'] = int(match.group(1))
|
||||||
|
info['formats'].append(fmt)
|
||||||
|
|
||||||
|
info['base_js'] = default_multi_get(top_level, 'player', 'assets', 'js')
|
||||||
|
if info['base_js']:
|
||||||
|
info['base_js'] = normalize_url(info['base_js'])
|
||||||
|
|
||||||
|
mobile = 'singleColumnWatchNextResults' in default_multi_get(top_level, 'response', 'contents', default={})
|
||||||
|
if mobile:
|
||||||
|
info.update(extract_watch_info_mobile(top_level))
|
||||||
|
else:
|
||||||
|
info.update(extract_watch_info_desktop(top_level))
|
||||||
|
|
||||||
|
# stuff from videoDetails
|
||||||
|
video_details = default_multi_get(top_level, 'playerResponse', 'videoDetails', default={})
|
||||||
|
info['title'] = extract_plain_text(video_details.get('title'))
|
||||||
|
info['duration'] = extract_integer(video_details.get('lengthSeconds'))
|
||||||
|
info['view_count'] = extract_integer(video_details.get('viewCount'))
|
||||||
|
# videos with no description have a blank string
|
||||||
|
info['description'] = video_details.get('shortDescription')
|
||||||
|
info['id'] = video_details.get('videoId')
|
||||||
|
info['author'] = video_details.get('author')
|
||||||
|
info['author_id'] = video_details.get('channelId')
|
||||||
|
info['live'] = video_details.get('isLiveContent')
|
||||||
|
info['unlisted'] = not video_details.get('isCrawlable', True)
|
||||||
|
info['tags'] = video_details.get('keywords', [])
|
||||||
|
|
||||||
|
# other stuff
|
||||||
|
info['author_url'] = 'https://www.youtube.com/channel/' + info['author_id'] if info['author_id'] else None
|
||||||
|
info['subtitles'] = {} # TODO
|
||||||
|
|
||||||
|
return info
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,481 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# coding: utf-8
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
__license__ = 'Public Domain'
|
|
||||||
|
|
||||||
import codecs
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
from .options import (
|
|
||||||
parseOpts,
|
|
||||||
)
|
|
||||||
from .compat import (
|
|
||||||
compat_getpass,
|
|
||||||
compat_shlex_split,
|
|
||||||
workaround_optparse_bug9161,
|
|
||||||
)
|
|
||||||
from .utils import (
|
|
||||||
DateRange,
|
|
||||||
decodeOption,
|
|
||||||
DEFAULT_OUTTMPL,
|
|
||||||
DownloadError,
|
|
||||||
expand_path,
|
|
||||||
match_filter_func,
|
|
||||||
MaxDownloadsReached,
|
|
||||||
preferredencoding,
|
|
||||||
read_batch_urls,
|
|
||||||
SameFileError,
|
|
||||||
setproctitle,
|
|
||||||
std_headers,
|
|
||||||
write_string,
|
|
||||||
render_table,
|
|
||||||
)
|
|
||||||
from .update import update_self
|
|
||||||
from .downloader import (
|
|
||||||
FileDownloader,
|
|
||||||
)
|
|
||||||
from .extractor import gen_extractors, list_extractors
|
|
||||||
from .extractor.adobepass import MSO_INFO
|
|
||||||
from .YoutubeDL import YoutubeDL
|
|
||||||
|
|
||||||
|
|
||||||
def _real_main(argv=None):
|
|
||||||
# Compatibility fixes for Windows
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
# https://github.com/rg3/youtube-dl/issues/820
|
|
||||||
codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)
|
|
||||||
|
|
||||||
workaround_optparse_bug9161()
|
|
||||||
|
|
||||||
setproctitle('youtube-dl')
|
|
||||||
|
|
||||||
parser, opts, args = parseOpts(argv)
|
|
||||||
|
|
||||||
# Set user agent
|
|
||||||
if opts.user_agent is not None:
|
|
||||||
std_headers['User-Agent'] = opts.user_agent
|
|
||||||
|
|
||||||
# Set referer
|
|
||||||
if opts.referer is not None:
|
|
||||||
std_headers['Referer'] = opts.referer
|
|
||||||
|
|
||||||
# Custom HTTP headers
|
|
||||||
if opts.headers is not None:
|
|
||||||
for h in opts.headers:
|
|
||||||
if ':' not in h:
|
|
||||||
parser.error('wrong header formatting, it should be key:value, not "%s"' % h)
|
|
||||||
key, value = h.split(':', 1)
|
|
||||||
if opts.verbose:
|
|
||||||
write_string('[debug] Adding header from command line option %s:%s\n' % (key, value))
|
|
||||||
std_headers[key] = value
|
|
||||||
|
|
||||||
# Dump user agent
|
|
||||||
if opts.dump_user_agent:
|
|
||||||
write_string(std_headers['User-Agent'] + '\n', out=sys.stdout)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Batch file verification
|
|
||||||
batch_urls = []
|
|
||||||
if opts.batchfile is not None:
|
|
||||||
try:
|
|
||||||
if opts.batchfile == '-':
|
|
||||||
batchfd = sys.stdin
|
|
||||||
else:
|
|
||||||
batchfd = io.open(
|
|
||||||
expand_path(opts.batchfile),
|
|
||||||
'r', encoding='utf-8', errors='ignore')
|
|
||||||
batch_urls = read_batch_urls(batchfd)
|
|
||||||
if opts.verbose:
|
|
||||||
write_string('[debug] Batch file urls: ' + repr(batch_urls) + '\n')
|
|
||||||
except IOError:
|
|
||||||
sys.exit('ERROR: batch file could not be read')
|
|
||||||
all_urls = batch_urls + [url.strip() for url in args] # batch_urls are already striped in read_batch_urls
|
|
||||||
_enc = preferredencoding()
|
|
||||||
all_urls = [url.decode(_enc, 'ignore') if isinstance(url, bytes) else url for url in all_urls]
|
|
||||||
|
|
||||||
if opts.list_extractors:
|
|
||||||
for ie in list_extractors(opts.age_limit):
|
|
||||||
write_string(ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie._WORKING else '') + '\n', out=sys.stdout)
|
|
||||||
matchedUrls = [url for url in all_urls if ie.suitable(url)]
|
|
||||||
for mu in matchedUrls:
|
|
||||||
write_string(' ' + mu + '\n', out=sys.stdout)
|
|
||||||
sys.exit(0)
|
|
||||||
if opts.list_extractor_descriptions:
|
|
||||||
for ie in list_extractors(opts.age_limit):
|
|
||||||
if not ie._WORKING:
|
|
||||||
continue
|
|
||||||
desc = getattr(ie, 'IE_DESC', ie.IE_NAME)
|
|
||||||
if desc is False:
|
|
||||||
continue
|
|
||||||
if hasattr(ie, 'SEARCH_KEY'):
|
|
||||||
_SEARCHES = ('cute kittens', 'slithering pythons', 'falling cat', 'angry poodle', 'purple fish', 'running tortoise', 'sleeping bunny', 'burping cow')
|
|
||||||
_COUNTS = ('', '5', '10', 'all')
|
|
||||||
desc += ' (Example: "%s%s:%s" )' % (ie.SEARCH_KEY, random.choice(_COUNTS), random.choice(_SEARCHES))
|
|
||||||
write_string(desc + '\n', out=sys.stdout)
|
|
||||||
sys.exit(0)
|
|
||||||
if opts.ap_list_mso:
|
|
||||||
table = [[mso_id, mso_info['name']] for mso_id, mso_info in MSO_INFO.items()]
|
|
||||||
write_string('Supported TV Providers:\n' + render_table(['mso', 'mso name'], table) + '\n', out=sys.stdout)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Conflicting, missing and erroneous options
|
|
||||||
if opts.usenetrc and (opts.username is not None or opts.password is not None):
|
|
||||||
parser.error('using .netrc conflicts with giving username/password')
|
|
||||||
if opts.password is not None and opts.username is None:
|
|
||||||
parser.error('account username missing\n')
|
|
||||||
if opts.ap_password is not None and opts.ap_username is None:
|
|
||||||
parser.error('TV Provider account username missing\n')
|
|
||||||
if opts.outtmpl is not None and (opts.usetitle or opts.autonumber or opts.useid):
|
|
||||||
parser.error('using output template conflicts with using title, video ID or auto number')
|
|
||||||
if opts.autonumber_size is not None:
|
|
||||||
if opts.autonumber_size <= 0:
|
|
||||||
parser.error('auto number size must be positive')
|
|
||||||
if opts.autonumber_start is not None:
|
|
||||||
if opts.autonumber_start < 0:
|
|
||||||
parser.error('auto number start must be positive or 0')
|
|
||||||
if opts.usetitle and opts.useid:
|
|
||||||
parser.error('using title conflicts with using video ID')
|
|
||||||
if opts.username is not None and opts.password is None:
|
|
||||||
opts.password = compat_getpass('Type account password and press [Return]: ')
|
|
||||||
if opts.ap_username is not None and opts.ap_password is None:
|
|
||||||
opts.ap_password = compat_getpass('Type TV provider account password and press [Return]: ')
|
|
||||||
if opts.ratelimit is not None:
|
|
||||||
numeric_limit = FileDownloader.parse_bytes(opts.ratelimit)
|
|
||||||
if numeric_limit is None:
|
|
||||||
parser.error('invalid rate limit specified')
|
|
||||||
opts.ratelimit = numeric_limit
|
|
||||||
if opts.min_filesize is not None:
|
|
||||||
numeric_limit = FileDownloader.parse_bytes(opts.min_filesize)
|
|
||||||
if numeric_limit is None:
|
|
||||||
parser.error('invalid min_filesize specified')
|
|
||||||
opts.min_filesize = numeric_limit
|
|
||||||
if opts.max_filesize is not None:
|
|
||||||
numeric_limit = FileDownloader.parse_bytes(opts.max_filesize)
|
|
||||||
if numeric_limit is None:
|
|
||||||
parser.error('invalid max_filesize specified')
|
|
||||||
opts.max_filesize = numeric_limit
|
|
||||||
if opts.sleep_interval is not None:
|
|
||||||
if opts.sleep_interval < 0:
|
|
||||||
parser.error('sleep interval must be positive or 0')
|
|
||||||
if opts.max_sleep_interval is not None:
|
|
||||||
if opts.max_sleep_interval < 0:
|
|
||||||
parser.error('max sleep interval must be positive or 0')
|
|
||||||
if opts.max_sleep_interval < opts.sleep_interval:
|
|
||||||
parser.error('max sleep interval must be greater than or equal to min sleep interval')
|
|
||||||
else:
|
|
||||||
opts.max_sleep_interval = opts.sleep_interval
|
|
||||||
if opts.ap_mso and opts.ap_mso not in MSO_INFO:
|
|
||||||
parser.error('Unsupported TV Provider, use --ap-list-mso to get a list of supported TV Providers')
|
|
||||||
|
|
||||||
def parse_retries(retries):
|
|
||||||
if retries in ('inf', 'infinite'):
|
|
||||||
parsed_retries = float('inf')
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
parsed_retries = int(retries)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
parser.error('invalid retry count specified')
|
|
||||||
return parsed_retries
|
|
||||||
if opts.retries is not None:
|
|
||||||
opts.retries = parse_retries(opts.retries)
|
|
||||||
if opts.fragment_retries is not None:
|
|
||||||
opts.fragment_retries = parse_retries(opts.fragment_retries)
|
|
||||||
if opts.buffersize is not None:
|
|
||||||
numeric_buffersize = FileDownloader.parse_bytes(opts.buffersize)
|
|
||||||
if numeric_buffersize is None:
|
|
||||||
parser.error('invalid buffer size specified')
|
|
||||||
opts.buffersize = numeric_buffersize
|
|
||||||
if opts.http_chunk_size is not None:
|
|
||||||
numeric_chunksize = FileDownloader.parse_bytes(opts.http_chunk_size)
|
|
||||||
if not numeric_chunksize:
|
|
||||||
parser.error('invalid http chunk size specified')
|
|
||||||
opts.http_chunk_size = numeric_chunksize
|
|
||||||
if opts.playliststart <= 0:
|
|
||||||
raise ValueError('Playlist start must be positive')
|
|
||||||
if opts.playlistend not in (-1, None) and opts.playlistend < opts.playliststart:
|
|
||||||
raise ValueError('Playlist end must be greater than playlist start')
|
|
||||||
if opts.extractaudio:
|
|
||||||
if opts.audioformat not in ['best', 'aac', 'flac', 'mp3', 'm4a', 'opus', 'vorbis', 'wav']:
|
|
||||||
parser.error('invalid audio format specified')
|
|
||||||
if opts.audioquality:
|
|
||||||
opts.audioquality = opts.audioquality.strip('k').strip('K')
|
|
||||||
if not opts.audioquality.isdigit():
|
|
||||||
parser.error('invalid audio quality specified')
|
|
||||||
if opts.recodevideo is not None:
|
|
||||||
if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg', 'mkv', 'avi']:
|
|
||||||
parser.error('invalid video recode format specified')
|
|
||||||
if opts.convertsubtitles is not None:
|
|
||||||
if opts.convertsubtitles not in ['srt', 'vtt', 'ass', 'lrc']:
|
|
||||||
parser.error('invalid subtitle format specified')
|
|
||||||
|
|
||||||
if opts.date is not None:
|
|
||||||
date = DateRange.day(opts.date)
|
|
||||||
else:
|
|
||||||
date = DateRange(opts.dateafter, opts.datebefore)
|
|
||||||
|
|
||||||
# Do not download videos when there are audio-only formats
|
|
||||||
if opts.extractaudio and not opts.keepvideo and opts.format is None:
|
|
||||||
opts.format = 'bestaudio/best'
|
|
||||||
|
|
||||||
# --all-sub automatically sets --write-sub if --write-auto-sub is not given
|
|
||||||
# this was the old behaviour if only --all-sub was given.
|
|
||||||
if opts.allsubtitles and not opts.writeautomaticsub:
|
|
||||||
opts.writesubtitles = True
|
|
||||||
|
|
||||||
outtmpl = ((opts.outtmpl is not None and opts.outtmpl) or
|
|
||||||
(opts.format == '-1' and opts.usetitle and '%(title)s-%(id)s-%(format)s.%(ext)s') or
|
|
||||||
(opts.format == '-1' and '%(id)s-%(format)s.%(ext)s') or
|
|
||||||
(opts.usetitle and opts.autonumber and '%(autonumber)s-%(title)s-%(id)s.%(ext)s') or
|
|
||||||
(opts.usetitle and '%(title)s-%(id)s.%(ext)s') or
|
|
||||||
(opts.useid and '%(id)s.%(ext)s') or
|
|
||||||
(opts.autonumber and '%(autonumber)s-%(id)s.%(ext)s') or
|
|
||||||
DEFAULT_OUTTMPL)
|
|
||||||
if not os.path.splitext(outtmpl)[1] and opts.extractaudio:
|
|
||||||
parser.error('Cannot download a video and extract audio into the same'
|
|
||||||
' file! Use "{0}.%(ext)s" instead of "{0}" as the output'
|
|
||||||
' template'.format(outtmpl))
|
|
||||||
|
|
||||||
any_getting = opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat or opts.getduration or opts.dumpjson or opts.dump_single_json
|
|
||||||
any_printing = opts.print_json
|
|
||||||
download_archive_fn = expand_path(opts.download_archive) if opts.download_archive is not None else opts.download_archive
|
|
||||||
|
|
||||||
# PostProcessors
|
|
||||||
postprocessors = []
|
|
||||||
if opts.metafromtitle:
|
|
||||||
postprocessors.append({
|
|
||||||
'key': 'MetadataFromTitle',
|
|
||||||
'titleformat': opts.metafromtitle
|
|
||||||
})
|
|
||||||
if opts.extractaudio:
|
|
||||||
postprocessors.append({
|
|
||||||
'key': 'FFmpegExtractAudio',
|
|
||||||
'preferredcodec': opts.audioformat,
|
|
||||||
'preferredquality': opts.audioquality,
|
|
||||||
'nopostoverwrites': opts.nopostoverwrites,
|
|
||||||
})
|
|
||||||
if opts.recodevideo:
|
|
||||||
postprocessors.append({
|
|
||||||
'key': 'FFmpegVideoConvertor',
|
|
||||||
'preferedformat': opts.recodevideo,
|
|
||||||
})
|
|
||||||
# FFmpegMetadataPP should be run after FFmpegVideoConvertorPP and
|
|
||||||
# FFmpegExtractAudioPP as containers before conversion may not support
|
|
||||||
# metadata (3gp, webm, etc.)
|
|
||||||
# And this post-processor should be placed before other metadata
|
|
||||||
# manipulating post-processors (FFmpegEmbedSubtitle) to prevent loss of
|
|
||||||
# extra metadata. By default ffmpeg preserves metadata applicable for both
|
|
||||||
# source and target containers. From this point the container won't change,
|
|
||||||
# so metadata can be added here.
|
|
||||||
if opts.addmetadata:
|
|
||||||
postprocessors.append({'key': 'FFmpegMetadata'})
|
|
||||||
if opts.convertsubtitles:
|
|
||||||
postprocessors.append({
|
|
||||||
'key': 'FFmpegSubtitlesConvertor',
|
|
||||||
'format': opts.convertsubtitles,
|
|
||||||
})
|
|
||||||
if opts.embedsubtitles:
|
|
||||||
postprocessors.append({
|
|
||||||
'key': 'FFmpegEmbedSubtitle',
|
|
||||||
})
|
|
||||||
if opts.embedthumbnail:
|
|
||||||
already_have_thumbnail = opts.writethumbnail or opts.write_all_thumbnails
|
|
||||||
postprocessors.append({
|
|
||||||
'key': 'EmbedThumbnail',
|
|
||||||
'already_have_thumbnail': already_have_thumbnail
|
|
||||||
})
|
|
||||||
if not already_have_thumbnail:
|
|
||||||
opts.writethumbnail = True
|
|
||||||
# XAttrMetadataPP should be run after post-processors that may change file
|
|
||||||
# contents
|
|
||||||
if opts.xattrs:
|
|
||||||
postprocessors.append({'key': 'XAttrMetadata'})
|
|
||||||
# Please keep ExecAfterDownload towards the bottom as it allows the user to modify the final file in any way.
|
|
||||||
# So if the user is able to remove the file before your postprocessor runs it might cause a few problems.
|
|
||||||
if opts.exec_cmd:
|
|
||||||
postprocessors.append({
|
|
||||||
'key': 'ExecAfterDownload',
|
|
||||||
'exec_cmd': opts.exec_cmd,
|
|
||||||
})
|
|
||||||
external_downloader_args = None
|
|
||||||
if opts.external_downloader_args:
|
|
||||||
external_downloader_args = compat_shlex_split(opts.external_downloader_args)
|
|
||||||
postprocessor_args = None
|
|
||||||
if opts.postprocessor_args:
|
|
||||||
postprocessor_args = compat_shlex_split(opts.postprocessor_args)
|
|
||||||
match_filter = (
|
|
||||||
None if opts.match_filter is None
|
|
||||||
else match_filter_func(opts.match_filter))
|
|
||||||
|
|
||||||
ydl_opts = {
|
|
||||||
'usenetrc': opts.usenetrc,
|
|
||||||
'username': opts.username,
|
|
||||||
'password': opts.password,
|
|
||||||
'twofactor': opts.twofactor,
|
|
||||||
'videopassword': opts.videopassword,
|
|
||||||
'ap_mso': opts.ap_mso,
|
|
||||||
'ap_username': opts.ap_username,
|
|
||||||
'ap_password': opts.ap_password,
|
|
||||||
'quiet': (opts.quiet or any_getting or any_printing),
|
|
||||||
'no_warnings': opts.no_warnings,
|
|
||||||
'forceurl': opts.geturl,
|
|
||||||
'forcetitle': opts.gettitle,
|
|
||||||
'forceid': opts.getid,
|
|
||||||
'forcethumbnail': opts.getthumbnail,
|
|
||||||
'forcedescription': opts.getdescription,
|
|
||||||
'forceduration': opts.getduration,
|
|
||||||
'forcefilename': opts.getfilename,
|
|
||||||
'forceformat': opts.getformat,
|
|
||||||
'forcejson': opts.dumpjson or opts.print_json,
|
|
||||||
'dump_single_json': opts.dump_single_json,
|
|
||||||
'simulate': opts.simulate or any_getting,
|
|
||||||
'skip_download': opts.skip_download,
|
|
||||||
'format': opts.format,
|
|
||||||
'listformats': opts.listformats,
|
|
||||||
'outtmpl': outtmpl,
|
|
||||||
'autonumber_size': opts.autonumber_size,
|
|
||||||
'autonumber_start': opts.autonumber_start,
|
|
||||||
'restrictfilenames': opts.restrictfilenames,
|
|
||||||
'ignoreerrors': opts.ignoreerrors,
|
|
||||||
'force_generic_extractor': opts.force_generic_extractor,
|
|
||||||
'ratelimit': opts.ratelimit,
|
|
||||||
'nooverwrites': opts.nooverwrites,
|
|
||||||
'retries': opts.retries,
|
|
||||||
'fragment_retries': opts.fragment_retries,
|
|
||||||
'skip_unavailable_fragments': opts.skip_unavailable_fragments,
|
|
||||||
'keep_fragments': opts.keep_fragments,
|
|
||||||
'buffersize': opts.buffersize,
|
|
||||||
'noresizebuffer': opts.noresizebuffer,
|
|
||||||
'http_chunk_size': opts.http_chunk_size,
|
|
||||||
'continuedl': opts.continue_dl,
|
|
||||||
'noprogress': opts.noprogress,
|
|
||||||
'progress_with_newline': opts.progress_with_newline,
|
|
||||||
'playliststart': opts.playliststart,
|
|
||||||
'playlistend': opts.playlistend,
|
|
||||||
'playlistreverse': opts.playlist_reverse,
|
|
||||||
'playlistrandom': opts.playlist_random,
|
|
||||||
'noplaylist': opts.noplaylist,
|
|
||||||
'logtostderr': opts.outtmpl == '-',
|
|
||||||
'consoletitle': opts.consoletitle,
|
|
||||||
'nopart': opts.nopart,
|
|
||||||
'updatetime': opts.updatetime,
|
|
||||||
'writedescription': opts.writedescription,
|
|
||||||
'writeannotations': opts.writeannotations,
|
|
||||||
'writeinfojson': opts.writeinfojson,
|
|
||||||
'writethumbnail': opts.writethumbnail,
|
|
||||||
'write_all_thumbnails': opts.write_all_thumbnails,
|
|
||||||
'writesubtitles': opts.writesubtitles,
|
|
||||||
'writeautomaticsub': opts.writeautomaticsub,
|
|
||||||
'allsubtitles': opts.allsubtitles,
|
|
||||||
'listsubtitles': opts.listsubtitles,
|
|
||||||
'subtitlesformat': opts.subtitlesformat,
|
|
||||||
'subtitleslangs': opts.subtitleslangs,
|
|
||||||
'matchtitle': decodeOption(opts.matchtitle),
|
|
||||||
'rejecttitle': decodeOption(opts.rejecttitle),
|
|
||||||
'max_downloads': opts.max_downloads,
|
|
||||||
'prefer_free_formats': opts.prefer_free_formats,
|
|
||||||
'verbose': opts.verbose,
|
|
||||||
'dump_intermediate_pages': opts.dump_intermediate_pages,
|
|
||||||
'write_pages': opts.write_pages,
|
|
||||||
'test': opts.test,
|
|
||||||
'keepvideo': opts.keepvideo,
|
|
||||||
'min_filesize': opts.min_filesize,
|
|
||||||
'max_filesize': opts.max_filesize,
|
|
||||||
'min_views': opts.min_views,
|
|
||||||
'max_views': opts.max_views,
|
|
||||||
'daterange': date,
|
|
||||||
'cachedir': opts.cachedir,
|
|
||||||
'youtube_print_sig_code': opts.youtube_print_sig_code,
|
|
||||||
'age_limit': opts.age_limit,
|
|
||||||
'download_archive': download_archive_fn,
|
|
||||||
'cookiefile': opts.cookiefile,
|
|
||||||
'nocheckcertificate': opts.no_check_certificate,
|
|
||||||
'prefer_insecure': opts.prefer_insecure,
|
|
||||||
'proxy': opts.proxy,
|
|
||||||
'socket_timeout': opts.socket_timeout,
|
|
||||||
'bidi_workaround': opts.bidi_workaround,
|
|
||||||
'debug_printtraffic': opts.debug_printtraffic,
|
|
||||||
'prefer_ffmpeg': opts.prefer_ffmpeg,
|
|
||||||
'include_ads': opts.include_ads,
|
|
||||||
'default_search': opts.default_search,
|
|
||||||
'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
|
|
||||||
'encoding': opts.encoding,
|
|
||||||
'extract_flat': opts.extract_flat,
|
|
||||||
'mark_watched': opts.mark_watched,
|
|
||||||
'merge_output_format': opts.merge_output_format,
|
|
||||||
'postprocessors': postprocessors,
|
|
||||||
'fixup': opts.fixup,
|
|
||||||
'source_address': opts.source_address,
|
|
||||||
'call_home': opts.call_home,
|
|
||||||
'sleep_interval': opts.sleep_interval,
|
|
||||||
'max_sleep_interval': opts.max_sleep_interval,
|
|
||||||
'external_downloader': opts.external_downloader,
|
|
||||||
'list_thumbnails': opts.list_thumbnails,
|
|
||||||
'playlist_items': opts.playlist_items,
|
|
||||||
'xattr_set_filesize': opts.xattr_set_filesize,
|
|
||||||
'match_filter': match_filter,
|
|
||||||
'no_color': opts.no_color,
|
|
||||||
'ffmpeg_location': opts.ffmpeg_location,
|
|
||||||
'hls_prefer_native': opts.hls_prefer_native,
|
|
||||||
'hls_use_mpegts': opts.hls_use_mpegts,
|
|
||||||
'external_downloader_args': external_downloader_args,
|
|
||||||
'postprocessor_args': postprocessor_args,
|
|
||||||
'cn_verification_proxy': opts.cn_verification_proxy,
|
|
||||||
'geo_verification_proxy': opts.geo_verification_proxy,
|
|
||||||
'config_location': opts.config_location,
|
|
||||||
'geo_bypass': opts.geo_bypass,
|
|
||||||
'geo_bypass_country': opts.geo_bypass_country,
|
|
||||||
'geo_bypass_ip_block': opts.geo_bypass_ip_block,
|
|
||||||
# just for deprecation check
|
|
||||||
'autonumber': opts.autonumber if opts.autonumber is True else None,
|
|
||||||
'usetitle': opts.usetitle if opts.usetitle is True else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
with YoutubeDL(ydl_opts) as ydl:
|
|
||||||
# Update version
|
|
||||||
if opts.update_self:
|
|
||||||
update_self(ydl.to_screen, opts.verbose, ydl._opener)
|
|
||||||
|
|
||||||
# Remove cache dir
|
|
||||||
if opts.rm_cachedir:
|
|
||||||
ydl.cache.remove()
|
|
||||||
|
|
||||||
# Maybe do nothing
|
|
||||||
if (len(all_urls) < 1) and (opts.load_info_filename is None):
|
|
||||||
if opts.update_self or opts.rm_cachedir:
|
|
||||||
sys.exit()
|
|
||||||
|
|
||||||
ydl.warn_if_short_id(sys.argv[1:] if argv is None else argv)
|
|
||||||
parser.error(
|
|
||||||
'You must provide at least one URL.\n'
|
|
||||||
'Type youtube-dl --help to see a list of all options.')
|
|
||||||
|
|
||||||
try:
|
|
||||||
if opts.load_info_filename is not None:
|
|
||||||
retcode = ydl.download_with_info_file(expand_path(opts.load_info_filename))
|
|
||||||
else:
|
|
||||||
retcode = ydl.download(all_urls)
|
|
||||||
except MaxDownloadsReached:
|
|
||||||
ydl.to_screen('--max-download limit reached, aborting.')
|
|
||||||
retcode = 101
|
|
||||||
|
|
||||||
sys.exit(retcode)
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
try:
|
|
||||||
_real_main(argv)
|
|
||||||
except DownloadError:
|
|
||||||
sys.exit(1)
|
|
||||||
except SameFileError:
|
|
||||||
sys.exit('ERROR: fixed output name but more than one file to download')
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
sys.exit('\nERROR: Interrupted by user')
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['main', 'YoutubeDL', 'gen_extractors', 'list_extractors']
|
|
@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
# Execute with
|
|
||||||
# $ python youtube_dl/__main__.py (2.6+)
|
|
||||||
# $ python -m youtube_dl (2.7+)
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
if __package__ is None and not hasattr(sys, 'frozen'):
|
|
||||||
# direct call of __main__.py
|
|
||||||
import os.path
|
|
||||||
path = os.path.realpath(os.path.abspath(__file__))
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(path)))
|
|
||||||
|
|
||||||
import youtube_dl
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
youtube_dl.main()
|
|
@ -1,361 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from math import ceil
|
|
||||||
|
|
||||||
from .compat import compat_b64decode
|
|
||||||
from .utils import bytes_to_intlist, intlist_to_bytes
|
|
||||||
|
|
||||||
BLOCK_SIZE_BYTES = 16
|
|
||||||
|
|
||||||
|
|
||||||
def aes_ctr_decrypt(data, key, counter):
|
|
||||||
"""
|
|
||||||
Decrypt with aes in counter mode
|
|
||||||
|
|
||||||
@param {int[]} data cipher
|
|
||||||
@param {int[]} key 16/24/32-Byte cipher key
|
|
||||||
@param {instance} counter Instance whose next_value function (@returns {int[]} 16-Byte block)
|
|
||||||
returns the next counter block
|
|
||||||
@returns {int[]} decrypted data
|
|
||||||
"""
|
|
||||||
expanded_key = key_expansion(key)
|
|
||||||
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
|
||||||
|
|
||||||
decrypted_data = []
|
|
||||||
for i in range(block_count):
|
|
||||||
counter_block = counter.next_value()
|
|
||||||
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
|
|
||||||
block += [0] * (BLOCK_SIZE_BYTES - len(block))
|
|
||||||
|
|
||||||
cipher_counter_block = aes_encrypt(counter_block, expanded_key)
|
|
||||||
decrypted_data += xor(block, cipher_counter_block)
|
|
||||||
decrypted_data = decrypted_data[:len(data)]
|
|
||||||
|
|
||||||
return decrypted_data
|
|
||||||
|
|
||||||
|
|
||||||
def aes_cbc_decrypt(data, key, iv):
|
|
||||||
"""
|
|
||||||
Decrypt with aes in CBC mode
|
|
||||||
|
|
||||||
@param {int[]} data cipher
|
|
||||||
@param {int[]} key 16/24/32-Byte cipher key
|
|
||||||
@param {int[]} iv 16-Byte IV
|
|
||||||
@returns {int[]} decrypted data
|
|
||||||
"""
|
|
||||||
expanded_key = key_expansion(key)
|
|
||||||
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
|
||||||
|
|
||||||
decrypted_data = []
|
|
||||||
previous_cipher_block = iv
|
|
||||||
for i in range(block_count):
|
|
||||||
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
|
|
||||||
block += [0] * (BLOCK_SIZE_BYTES - len(block))
|
|
||||||
|
|
||||||
decrypted_block = aes_decrypt(block, expanded_key)
|
|
||||||
decrypted_data += xor(decrypted_block, previous_cipher_block)
|
|
||||||
previous_cipher_block = block
|
|
||||||
decrypted_data = decrypted_data[:len(data)]
|
|
||||||
|
|
||||||
return decrypted_data
|
|
||||||
|
|
||||||
|
|
||||||
def aes_cbc_encrypt(data, key, iv):
|
|
||||||
"""
|
|
||||||
Encrypt with aes in CBC mode. Using PKCS#7 padding
|
|
||||||
|
|
||||||
@param {int[]} data cleartext
|
|
||||||
@param {int[]} key 16/24/32-Byte cipher key
|
|
||||||
@param {int[]} iv 16-Byte IV
|
|
||||||
@returns {int[]} encrypted data
|
|
||||||
"""
|
|
||||||
expanded_key = key_expansion(key)
|
|
||||||
block_count = int(ceil(float(len(data)) / BLOCK_SIZE_BYTES))
|
|
||||||
|
|
||||||
encrypted_data = []
|
|
||||||
previous_cipher_block = iv
|
|
||||||
for i in range(block_count):
|
|
||||||
block = data[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES]
|
|
||||||
remaining_length = BLOCK_SIZE_BYTES - len(block)
|
|
||||||
block += [remaining_length] * remaining_length
|
|
||||||
mixed_block = xor(block, previous_cipher_block)
|
|
||||||
|
|
||||||
encrypted_block = aes_encrypt(mixed_block, expanded_key)
|
|
||||||
encrypted_data += encrypted_block
|
|
||||||
|
|
||||||
previous_cipher_block = encrypted_block
|
|
||||||
|
|
||||||
return encrypted_data
|
|
||||||
|
|
||||||
|
|
||||||
def key_expansion(data):
|
|
||||||
"""
|
|
||||||
Generate key schedule
|
|
||||||
|
|
||||||
@param {int[]} data 16/24/32-Byte cipher key
|
|
||||||
@returns {int[]} 176/208/240-Byte expanded key
|
|
||||||
"""
|
|
||||||
data = data[:] # copy
|
|
||||||
rcon_iteration = 1
|
|
||||||
key_size_bytes = len(data)
|
|
||||||
expanded_key_size_bytes = (key_size_bytes // 4 + 7) * BLOCK_SIZE_BYTES
|
|
||||||
|
|
||||||
while len(data) < expanded_key_size_bytes:
|
|
||||||
temp = data[-4:]
|
|
||||||
temp = key_schedule_core(temp, rcon_iteration)
|
|
||||||
rcon_iteration += 1
|
|
||||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
|
||||||
|
|
||||||
for _ in range(3):
|
|
||||||
temp = data[-4:]
|
|
||||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
|
||||||
|
|
||||||
if key_size_bytes == 32:
|
|
||||||
temp = data[-4:]
|
|
||||||
temp = sub_bytes(temp)
|
|
||||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
|
||||||
|
|
||||||
for _ in range(3 if key_size_bytes == 32 else 2 if key_size_bytes == 24 else 0):
|
|
||||||
temp = data[-4:]
|
|
||||||
data += xor(temp, data[-key_size_bytes: 4 - key_size_bytes])
|
|
||||||
data = data[:expanded_key_size_bytes]
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def aes_encrypt(data, expanded_key):
|
|
||||||
"""
|
|
||||||
Encrypt one block with aes
|
|
||||||
|
|
||||||
@param {int[]} data 16-Byte state
|
|
||||||
@param {int[]} expanded_key 176/208/240-Byte expanded key
|
|
||||||
@returns {int[]} 16-Byte cipher
|
|
||||||
"""
|
|
||||||
rounds = len(expanded_key) // BLOCK_SIZE_BYTES - 1
|
|
||||||
|
|
||||||
data = xor(data, expanded_key[:BLOCK_SIZE_BYTES])
|
|
||||||
for i in range(1, rounds + 1):
|
|
||||||
data = sub_bytes(data)
|
|
||||||
data = shift_rows(data)
|
|
||||||
if i != rounds:
|
|
||||||
data = mix_columns(data)
|
|
||||||
data = xor(data, expanded_key[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES])
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def aes_decrypt(data, expanded_key):
|
|
||||||
"""
|
|
||||||
Decrypt one block with aes
|
|
||||||
|
|
||||||
@param {int[]} data 16-Byte cipher
|
|
||||||
@param {int[]} expanded_key 176/208/240-Byte expanded key
|
|
||||||
@returns {int[]} 16-Byte state
|
|
||||||
"""
|
|
||||||
rounds = len(expanded_key) // BLOCK_SIZE_BYTES - 1
|
|
||||||
|
|
||||||
for i in range(rounds, 0, -1):
|
|
||||||
data = xor(data, expanded_key[i * BLOCK_SIZE_BYTES: (i + 1) * BLOCK_SIZE_BYTES])
|
|
||||||
if i != rounds:
|
|
||||||
data = mix_columns_inv(data)
|
|
||||||
data = shift_rows_inv(data)
|
|
||||||
data = sub_bytes_inv(data)
|
|
||||||
data = xor(data, expanded_key[:BLOCK_SIZE_BYTES])
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def aes_decrypt_text(data, password, key_size_bytes):
|
|
||||||
"""
|
|
||||||
Decrypt text
|
|
||||||
- The first 8 Bytes of decoded 'data' are the 8 high Bytes of the counter
|
|
||||||
- The cipher key is retrieved by encrypting the first 16 Byte of 'password'
|
|
||||||
with the first 'key_size_bytes' Bytes from 'password' (if necessary filled with 0's)
|
|
||||||
- Mode of operation is 'counter'
|
|
||||||
|
|
||||||
@param {str} data Base64 encoded string
|
|
||||||
@param {str,unicode} password Password (will be encoded with utf-8)
|
|
||||||
@param {int} key_size_bytes Possible values: 16 for 128-Bit, 24 for 192-Bit or 32 for 256-Bit
|
|
||||||
@returns {str} Decrypted data
|
|
||||||
"""
|
|
||||||
NONCE_LENGTH_BYTES = 8
|
|
||||||
|
|
||||||
data = bytes_to_intlist(compat_b64decode(data))
|
|
||||||
password = bytes_to_intlist(password.encode('utf-8'))
|
|
||||||
|
|
||||||
key = password[:key_size_bytes] + [0] * (key_size_bytes - len(password))
|
|
||||||
key = aes_encrypt(key[:BLOCK_SIZE_BYTES], key_expansion(key)) * (key_size_bytes // BLOCK_SIZE_BYTES)
|
|
||||||
|
|
||||||
nonce = data[:NONCE_LENGTH_BYTES]
|
|
||||||
cipher = data[NONCE_LENGTH_BYTES:]
|
|
||||||
|
|
||||||
class Counter(object):
|
|
||||||
__value = nonce + [0] * (BLOCK_SIZE_BYTES - NONCE_LENGTH_BYTES)
|
|
||||||
|
|
||||||
def next_value(self):
|
|
||||||
temp = self.__value
|
|
||||||
self.__value = inc(self.__value)
|
|
||||||
return temp
|
|
||||||
|
|
||||||
decrypted_data = aes_ctr_decrypt(cipher, key, Counter())
|
|
||||||
plaintext = intlist_to_bytes(decrypted_data)
|
|
||||||
|
|
||||||
return plaintext
|
|
||||||
|
|
||||||
|
|
||||||
RCON = (0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36)
|
|
||||||
SBOX = (0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
|
|
||||||
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
|
|
||||||
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
|
|
||||||
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
|
|
||||||
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
|
|
||||||
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
|
|
||||||
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
|
|
||||||
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
|
|
||||||
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
|
|
||||||
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
|
|
||||||
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
|
|
||||||
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
|
|
||||||
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
|
|
||||||
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
|
|
||||||
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
|
|
||||||
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16)
|
|
||||||
SBOX_INV = (0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
|
|
||||||
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
|
|
||||||
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
|
|
||||||
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
|
|
||||||
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
|
|
||||||
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
|
|
||||||
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
|
|
||||||
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
|
|
||||||
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
|
|
||||||
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
|
|
||||||
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
|
|
||||||
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
|
|
||||||
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
|
|
||||||
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
|
|
||||||
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
|
|
||||||
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d)
|
|
||||||
MIX_COLUMN_MATRIX = ((0x2, 0x3, 0x1, 0x1),
|
|
||||||
(0x1, 0x2, 0x3, 0x1),
|
|
||||||
(0x1, 0x1, 0x2, 0x3),
|
|
||||||
(0x3, 0x1, 0x1, 0x2))
|
|
||||||
MIX_COLUMN_MATRIX_INV = ((0xE, 0xB, 0xD, 0x9),
|
|
||||||
(0x9, 0xE, 0xB, 0xD),
|
|
||||||
(0xD, 0x9, 0xE, 0xB),
|
|
||||||
(0xB, 0xD, 0x9, 0xE))
|
|
||||||
RIJNDAEL_EXP_TABLE = (0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
|
|
||||||
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
|
|
||||||
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
|
|
||||||
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
|
|
||||||
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
|
|
||||||
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
|
|
||||||
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
|
|
||||||
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
|
|
||||||
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
|
|
||||||
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
|
|
||||||
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
|
|
||||||
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
|
|
||||||
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
|
|
||||||
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
|
|
||||||
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
|
|
||||||
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01)
|
|
||||||
RIJNDAEL_LOG_TABLE = (0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1a, 0xc6, 0x4b, 0xc7, 0x1b, 0x68, 0x33, 0xee, 0xdf, 0x03,
|
|
||||||
0x64, 0x04, 0xe0, 0x0e, 0x34, 0x8d, 0x81, 0xef, 0x4c, 0x71, 0x08, 0xc8, 0xf8, 0x69, 0x1c, 0xc1,
|
|
||||||
0x7d, 0xc2, 0x1d, 0xb5, 0xf9, 0xb9, 0x27, 0x6a, 0x4d, 0xe4, 0xa6, 0x72, 0x9a, 0xc9, 0x09, 0x78,
|
|
||||||
0x65, 0x2f, 0x8a, 0x05, 0x21, 0x0f, 0xe1, 0x24, 0x12, 0xf0, 0x82, 0x45, 0x35, 0x93, 0xda, 0x8e,
|
|
||||||
0x96, 0x8f, 0xdb, 0xbd, 0x36, 0xd0, 0xce, 0x94, 0x13, 0x5c, 0xd2, 0xf1, 0x40, 0x46, 0x83, 0x38,
|
|
||||||
0x66, 0xdd, 0xfd, 0x30, 0xbf, 0x06, 0x8b, 0x62, 0xb3, 0x25, 0xe2, 0x98, 0x22, 0x88, 0x91, 0x10,
|
|
||||||
0x7e, 0x6e, 0x48, 0xc3, 0xa3, 0xb6, 0x1e, 0x42, 0x3a, 0x6b, 0x28, 0x54, 0xfa, 0x85, 0x3d, 0xba,
|
|
||||||
0x2b, 0x79, 0x0a, 0x15, 0x9b, 0x9f, 0x5e, 0xca, 0x4e, 0xd4, 0xac, 0xe5, 0xf3, 0x73, 0xa7, 0x57,
|
|
||||||
0xaf, 0x58, 0xa8, 0x50, 0xf4, 0xea, 0xd6, 0x74, 0x4f, 0xae, 0xe9, 0xd5, 0xe7, 0xe6, 0xad, 0xe8,
|
|
||||||
0x2c, 0xd7, 0x75, 0x7a, 0xeb, 0x16, 0x0b, 0xf5, 0x59, 0xcb, 0x5f, 0xb0, 0x9c, 0xa9, 0x51, 0xa0,
|
|
||||||
0x7f, 0x0c, 0xf6, 0x6f, 0x17, 0xc4, 0x49, 0xec, 0xd8, 0x43, 0x1f, 0x2d, 0xa4, 0x76, 0x7b, 0xb7,
|
|
||||||
0xcc, 0xbb, 0x3e, 0x5a, 0xfb, 0x60, 0xb1, 0x86, 0x3b, 0x52, 0xa1, 0x6c, 0xaa, 0x55, 0x29, 0x9d,
|
|
||||||
0x97, 0xb2, 0x87, 0x90, 0x61, 0xbe, 0xdc, 0xfc, 0xbc, 0x95, 0xcf, 0xcd, 0x37, 0x3f, 0x5b, 0xd1,
|
|
||||||
0x53, 0x39, 0x84, 0x3c, 0x41, 0xa2, 0x6d, 0x47, 0x14, 0x2a, 0x9e, 0x5d, 0x56, 0xf2, 0xd3, 0xab,
|
|
||||||
0x44, 0x11, 0x92, 0xd9, 0x23, 0x20, 0x2e, 0x89, 0xb4, 0x7c, 0xb8, 0x26, 0x77, 0x99, 0xe3, 0xa5,
|
|
||||||
0x67, 0x4a, 0xed, 0xde, 0xc5, 0x31, 0xfe, 0x18, 0x0d, 0x63, 0x8c, 0x80, 0xc0, 0xf7, 0x70, 0x07)
|
|
||||||
|
|
||||||
|
|
||||||
def sub_bytes(data):
|
|
||||||
return [SBOX[x] for x in data]
|
|
||||||
|
|
||||||
|
|
||||||
def sub_bytes_inv(data):
|
|
||||||
return [SBOX_INV[x] for x in data]
|
|
||||||
|
|
||||||
|
|
||||||
def rotate(data):
|
|
||||||
return data[1:] + [data[0]]
|
|
||||||
|
|
||||||
|
|
||||||
def key_schedule_core(data, rcon_iteration):
|
|
||||||
data = rotate(data)
|
|
||||||
data = sub_bytes(data)
|
|
||||||
data[0] = data[0] ^ RCON[rcon_iteration]
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def xor(data1, data2):
|
|
||||||
return [x ^ y for x, y in zip(data1, data2)]
|
|
||||||
|
|
||||||
|
|
||||||
def rijndael_mul(a, b):
|
|
||||||
if(a == 0 or b == 0):
|
|
||||||
return 0
|
|
||||||
return RIJNDAEL_EXP_TABLE[(RIJNDAEL_LOG_TABLE[a] + RIJNDAEL_LOG_TABLE[b]) % 0xFF]
|
|
||||||
|
|
||||||
|
|
||||||
def mix_column(data, matrix):
|
|
||||||
data_mixed = []
|
|
||||||
for row in range(4):
|
|
||||||
mixed = 0
|
|
||||||
for column in range(4):
|
|
||||||
# xor is (+) and (-)
|
|
||||||
mixed ^= rijndael_mul(data[column], matrix[row][column])
|
|
||||||
data_mixed.append(mixed)
|
|
||||||
return data_mixed
|
|
||||||
|
|
||||||
|
|
||||||
def mix_columns(data, matrix=MIX_COLUMN_MATRIX):
|
|
||||||
data_mixed = []
|
|
||||||
for i in range(4):
|
|
||||||
column = data[i * 4: (i + 1) * 4]
|
|
||||||
data_mixed += mix_column(column, matrix)
|
|
||||||
return data_mixed
|
|
||||||
|
|
||||||
|
|
||||||
def mix_columns_inv(data):
|
|
||||||
return mix_columns(data, MIX_COLUMN_MATRIX_INV)
|
|
||||||
|
|
||||||
|
|
||||||
def shift_rows(data):
|
|
||||||
data_shifted = []
|
|
||||||
for column in range(4):
|
|
||||||
for row in range(4):
|
|
||||||
data_shifted.append(data[((column + row) & 0b11) * 4 + row])
|
|
||||||
return data_shifted
|
|
||||||
|
|
||||||
|
|
||||||
def shift_rows_inv(data):
|
|
||||||
data_shifted = []
|
|
||||||
for column in range(4):
|
|
||||||
for row in range(4):
|
|
||||||
data_shifted.append(data[((column - row) & 0b11) * 4 + row])
|
|
||||||
return data_shifted
|
|
||||||
|
|
||||||
|
|
||||||
def inc(data):
|
|
||||||
data = data[:] # copy
|
|
||||||
for i in range(len(data) - 1, -1, -1):
|
|
||||||
if data[i] == 255:
|
|
||||||
data[i] = 0
|
|
||||||
else:
|
|
||||||
data[i] = data[i] + 1
|
|
||||||
break
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['aes_encrypt', 'key_expansion', 'aes_ctr_decrypt', 'aes_cbc_decrypt', 'aes_decrypt_text']
|
|
@ -1,96 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import io
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from .compat import compat_getenv
|
|
||||||
from .utils import (
|
|
||||||
expand_path,
|
|
||||||
write_json_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Cache(object):
|
|
||||||
def __init__(self, ydl):
|
|
||||||
self._ydl = ydl
|
|
||||||
|
|
||||||
def _get_root_dir(self):
|
|
||||||
res = self._ydl.params.get('cachedir')
|
|
||||||
if res is None:
|
|
||||||
cache_root = compat_getenv('XDG_CACHE_HOME', '~/.cache')
|
|
||||||
res = os.path.join(cache_root, 'youtube-dl')
|
|
||||||
return expand_path(res)
|
|
||||||
|
|
||||||
def _get_cache_fn(self, section, key, dtype):
|
|
||||||
assert re.match(r'^[a-zA-Z0-9_.-]+$', section), \
|
|
||||||
'invalid section %r' % section
|
|
||||||
assert re.match(r'^[a-zA-Z0-9_.-]+$', key), 'invalid key %r' % key
|
|
||||||
return os.path.join(
|
|
||||||
self._get_root_dir(), section, '%s.%s' % (key, dtype))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def enabled(self):
|
|
||||||
return self._ydl.params.get('cachedir') is not False
|
|
||||||
|
|
||||||
def store(self, section, key, data, dtype='json'):
|
|
||||||
assert dtype in ('json',)
|
|
||||||
|
|
||||||
if not self.enabled:
|
|
||||||
return
|
|
||||||
|
|
||||||
fn = self._get_cache_fn(section, key, dtype)
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
os.makedirs(os.path.dirname(fn))
|
|
||||||
except OSError as ose:
|
|
||||||
if ose.errno != errno.EEXIST:
|
|
||||||
raise
|
|
||||||
write_json_file(data, fn)
|
|
||||||
except Exception:
|
|
||||||
tb = traceback.format_exc()
|
|
||||||
self._ydl.report_warning(
|
|
||||||
'Writing cache to %r failed: %s' % (fn, tb))
|
|
||||||
|
|
||||||
def load(self, section, key, dtype='json', default=None):
|
|
||||||
assert dtype in ('json',)
|
|
||||||
|
|
||||||
if not self.enabled:
|
|
||||||
return default
|
|
||||||
|
|
||||||
cache_fn = self._get_cache_fn(section, key, dtype)
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
with io.open(cache_fn, 'r', encoding='utf-8') as cachef:
|
|
||||||
return json.load(cachef)
|
|
||||||
except ValueError:
|
|
||||||
try:
|
|
||||||
file_size = os.path.getsize(cache_fn)
|
|
||||||
except (OSError, IOError) as oe:
|
|
||||||
file_size = str(oe)
|
|
||||||
self._ydl.report_warning(
|
|
||||||
'Cache retrieval from %s failed (%s)' % (cache_fn, file_size))
|
|
||||||
except IOError:
|
|
||||||
pass # No cache available
|
|
||||||
|
|
||||||
return default
|
|
||||||
|
|
||||||
def remove(self):
|
|
||||||
if not self.enabled:
|
|
||||||
self._ydl.to_screen('Cache is disabled (Did you combine --no-cache-dir and --rm-cache-dir?)')
|
|
||||||
return
|
|
||||||
|
|
||||||
cachedir = self._get_root_dir()
|
|
||||||
if not any((term in cachedir) for term in ('cache', 'tmp')):
|
|
||||||
raise Exception('Not removing directory %s - this does not look like a cache dir' % cachedir)
|
|
||||||
|
|
||||||
self._ydl.to_screen(
|
|
||||||
'Removing cache dir %s .' % cachedir, skip_eol=True)
|
|
||||||
if os.path.exists(cachedir):
|
|
||||||
self._ydl.to_screen('.', skip_eol=True)
|
|
||||||
shutil.rmtree(cachedir)
|
|
||||||
self._ydl.to_screen('.')
|
|
3016
youtube_dl/compat.py
3016
youtube_dl/compat.py
File diff suppressed because it is too large
Load Diff
@ -1,61 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
|
||||||
from .f4m import F4mFD
|
|
||||||
from .hls import HlsFD
|
|
||||||
from .http import HttpFD
|
|
||||||
from .rtmp import RtmpFD
|
|
||||||
from .dash import DashSegmentsFD
|
|
||||||
from .rtsp import RtspFD
|
|
||||||
from .ism import IsmFD
|
|
||||||
from .external import (
|
|
||||||
get_external_downloader,
|
|
||||||
FFmpegFD,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ..utils import (
|
|
||||||
determine_protocol,
|
|
||||||
)
|
|
||||||
|
|
||||||
PROTOCOL_MAP = {
|
|
||||||
'rtmp': RtmpFD,
|
|
||||||
'm3u8_native': HlsFD,
|
|
||||||
'm3u8': FFmpegFD,
|
|
||||||
'mms': RtspFD,
|
|
||||||
'rtsp': RtspFD,
|
|
||||||
'f4m': F4mFD,
|
|
||||||
'http_dash_segments': DashSegmentsFD,
|
|
||||||
'ism': IsmFD,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_suitable_downloader(info_dict, params={}):
|
|
||||||
"""Get the downloader class that can handle the info dict."""
|
|
||||||
protocol = determine_protocol(info_dict)
|
|
||||||
info_dict['protocol'] = protocol
|
|
||||||
|
|
||||||
# if (info_dict.get('start_time') or info_dict.get('end_time')) and not info_dict.get('requested_formats') and FFmpegFD.can_download(info_dict):
|
|
||||||
# return FFmpegFD
|
|
||||||
|
|
||||||
external_downloader = params.get('external_downloader')
|
|
||||||
if external_downloader is not None:
|
|
||||||
ed = get_external_downloader(external_downloader)
|
|
||||||
if ed.can_download(info_dict):
|
|
||||||
return ed
|
|
||||||
|
|
||||||
if protocol.startswith('m3u8') and info_dict.get('is_live'):
|
|
||||||
return FFmpegFD
|
|
||||||
|
|
||||||
if protocol == 'm3u8' and params.get('hls_prefer_native') is True:
|
|
||||||
return HlsFD
|
|
||||||
|
|
||||||
if protocol == 'm3u8_native' and params.get('hls_prefer_native') is False:
|
|
||||||
return FFmpegFD
|
|
||||||
|
|
||||||
return PROTOCOL_MAP.get(protocol, HttpFD)
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'get_suitable_downloader',
|
|
||||||
'FileDownloader',
|
|
||||||
]
|
|
@ -1,389 +0,0 @@
|
|||||||
from __future__ import division, unicode_literals
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
|
|
||||||
from ..compat import compat_os_name
|
|
||||||
from ..utils import (
|
|
||||||
decodeArgument,
|
|
||||||
encodeFilename,
|
|
||||||
error_to_compat_str,
|
|
||||||
format_bytes,
|
|
||||||
shell_quote,
|
|
||||||
timeconvert,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FileDownloader(object):
|
|
||||||
"""File Downloader class.
|
|
||||||
|
|
||||||
File downloader objects are the ones responsible of downloading the
|
|
||||||
actual video file and writing it to disk.
|
|
||||||
|
|
||||||
File downloaders accept a lot of parameters. In order not to saturate
|
|
||||||
the object constructor with arguments, it receives a dictionary of
|
|
||||||
options instead.
|
|
||||||
|
|
||||||
Available options:
|
|
||||||
|
|
||||||
verbose: Print additional info to stdout.
|
|
||||||
quiet: Do not print messages to stdout.
|
|
||||||
ratelimit: Download speed limit, in bytes/sec.
|
|
||||||
retries: Number of times to retry for HTTP error 5xx
|
|
||||||
buffersize: Size of download buffer in bytes.
|
|
||||||
noresizebuffer: Do not automatically resize the download buffer.
|
|
||||||
continuedl: Try to continue downloads if possible.
|
|
||||||
noprogress: Do not print the progress bar.
|
|
||||||
logtostderr: Log messages to stderr instead of stdout.
|
|
||||||
consoletitle: Display progress in console window's titlebar.
|
|
||||||
nopart: Do not use temporary .part files.
|
|
||||||
updatetime: Use the Last-modified header to set output file timestamps.
|
|
||||||
test: Download only first bytes to test the downloader.
|
|
||||||
min_filesize: Skip files smaller than this size
|
|
||||||
max_filesize: Skip files larger than this size
|
|
||||||
xattr_set_filesize: Set ytdl.filesize user xattribute with expected size.
|
|
||||||
external_downloader_args: A list of additional command-line arguments for the
|
|
||||||
external downloader.
|
|
||||||
hls_use_mpegts: Use the mpegts container for HLS videos.
|
|
||||||
http_chunk_size: Size of a chunk for chunk-based HTTP downloading. May be
|
|
||||||
useful for bypassing bandwidth throttling imposed by
|
|
||||||
a webserver (experimental)
|
|
||||||
|
|
||||||
Subclasses of this one must re-define the real_download method.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_TEST_FILE_SIZE = 10241
|
|
||||||
params = None
|
|
||||||
|
|
||||||
def __init__(self, ydl, params):
|
|
||||||
"""Create a FileDownloader object with the given options."""
|
|
||||||
self.ydl = ydl
|
|
||||||
self._progress_hooks = []
|
|
||||||
self.params = params
|
|
||||||
self.add_progress_hook(self.report_progress)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_seconds(seconds):
|
|
||||||
(mins, secs) = divmod(seconds, 60)
|
|
||||||
(hours, mins) = divmod(mins, 60)
|
|
||||||
if hours > 99:
|
|
||||||
return '--:--:--'
|
|
||||||
if hours == 0:
|
|
||||||
return '%02d:%02d' % (mins, secs)
|
|
||||||
else:
|
|
||||||
return '%02d:%02d:%02d' % (hours, mins, secs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def calc_percent(byte_counter, data_len):
|
|
||||||
if data_len is None:
|
|
||||||
return None
|
|
||||||
return float(byte_counter) / float(data_len) * 100.0
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_percent(percent):
|
|
||||||
if percent is None:
|
|
||||||
return '---.-%'
|
|
||||||
return '%6s' % ('%3.1f%%' % percent)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def calc_eta(start, now, total, current):
|
|
||||||
if total is None:
|
|
||||||
return None
|
|
||||||
if now is None:
|
|
||||||
now = time.time()
|
|
||||||
dif = now - start
|
|
||||||
if current == 0 or dif < 0.001: # One millisecond
|
|
||||||
return None
|
|
||||||
rate = float(current) / dif
|
|
||||||
return int((float(total) - float(current)) / rate)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_eta(eta):
|
|
||||||
if eta is None:
|
|
||||||
return '--:--'
|
|
||||||
return FileDownloader.format_seconds(eta)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def calc_speed(start, now, bytes):
|
|
||||||
dif = now - start
|
|
||||||
if bytes == 0 or dif < 0.001: # One millisecond
|
|
||||||
return None
|
|
||||||
return float(bytes) / dif
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_speed(speed):
|
|
||||||
if speed is None:
|
|
||||||
return '%10s' % '---b/s'
|
|
||||||
return '%10s' % ('%s/s' % format_bytes(speed))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_retries(retries):
|
|
||||||
return 'inf' if retries == float('inf') else '%.0f' % retries
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def best_block_size(elapsed_time, bytes):
|
|
||||||
new_min = max(bytes / 2.0, 1.0)
|
|
||||||
new_max = min(max(bytes * 2.0, 1.0), 4194304) # Do not surpass 4 MB
|
|
||||||
if elapsed_time < 0.001:
|
|
||||||
return int(new_max)
|
|
||||||
rate = bytes / elapsed_time
|
|
||||||
if rate > new_max:
|
|
||||||
return int(new_max)
|
|
||||||
if rate < new_min:
|
|
||||||
return int(new_min)
|
|
||||||
return int(rate)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_bytes(bytestr):
|
|
||||||
"""Parse a string indicating a byte quantity into an integer."""
|
|
||||||
matchobj = re.match(r'(?i)^(\d+(?:\.\d+)?)([kMGTPEZY]?)$', bytestr)
|
|
||||||
if matchobj is None:
|
|
||||||
return None
|
|
||||||
number = float(matchobj.group(1))
|
|
||||||
multiplier = 1024.0 ** 'bkmgtpezy'.index(matchobj.group(2).lower())
|
|
||||||
return int(round(number * multiplier))
|
|
||||||
|
|
||||||
def to_screen(self, *args, **kargs):
|
|
||||||
self.ydl.to_screen(*args, **kargs)
|
|
||||||
|
|
||||||
def to_stderr(self, message):
|
|
||||||
self.ydl.to_screen(message)
|
|
||||||
|
|
||||||
def to_console_title(self, message):
|
|
||||||
self.ydl.to_console_title(message)
|
|
||||||
|
|
||||||
def trouble(self, *args, **kargs):
|
|
||||||
self.ydl.trouble(*args, **kargs)
|
|
||||||
|
|
||||||
def report_warning(self, *args, **kargs):
|
|
||||||
self.ydl.report_warning(*args, **kargs)
|
|
||||||
|
|
||||||
def report_error(self, *args, **kargs):
|
|
||||||
self.ydl.report_error(*args, **kargs)
|
|
||||||
|
|
||||||
def slow_down(self, start_time, now, byte_counter):
|
|
||||||
"""Sleep if the download speed is over the rate limit."""
|
|
||||||
rate_limit = self.params.get('ratelimit')
|
|
||||||
if rate_limit is None or byte_counter == 0:
|
|
||||||
return
|
|
||||||
if now is None:
|
|
||||||
now = time.time()
|
|
||||||
elapsed = now - start_time
|
|
||||||
if elapsed <= 0.0:
|
|
||||||
return
|
|
||||||
speed = float(byte_counter) / elapsed
|
|
||||||
if speed > rate_limit:
|
|
||||||
time.sleep(max((byte_counter // rate_limit) - elapsed, 0))
|
|
||||||
|
|
||||||
def temp_name(self, filename):
|
|
||||||
"""Returns a temporary filename for the given filename."""
|
|
||||||
if self.params.get('nopart', False) or filename == '-' or \
|
|
||||||
(os.path.exists(encodeFilename(filename)) and not os.path.isfile(encodeFilename(filename))):
|
|
||||||
return filename
|
|
||||||
return filename + '.part'
|
|
||||||
|
|
||||||
def undo_temp_name(self, filename):
|
|
||||||
if filename.endswith('.part'):
|
|
||||||
return filename[:-len('.part')]
|
|
||||||
return filename
|
|
||||||
|
|
||||||
def ytdl_filename(self, filename):
|
|
||||||
return filename + '.ytdl'
|
|
||||||
|
|
||||||
def try_rename(self, old_filename, new_filename):
|
|
||||||
try:
|
|
||||||
if old_filename == new_filename:
|
|
||||||
return
|
|
||||||
os.rename(encodeFilename(old_filename), encodeFilename(new_filename))
|
|
||||||
except (IOError, OSError) as err:
|
|
||||||
self.report_error('unable to rename file: %s' % error_to_compat_str(err))
|
|
||||||
|
|
||||||
def try_utime(self, filename, last_modified_hdr):
|
|
||||||
"""Try to set the last-modified time of the given file."""
|
|
||||||
if last_modified_hdr is None:
|
|
||||||
return
|
|
||||||
if not os.path.isfile(encodeFilename(filename)):
|
|
||||||
return
|
|
||||||
timestr = last_modified_hdr
|
|
||||||
if timestr is None:
|
|
||||||
return
|
|
||||||
filetime = timeconvert(timestr)
|
|
||||||
if filetime is None:
|
|
||||||
return filetime
|
|
||||||
# Ignore obviously invalid dates
|
|
||||||
if filetime == 0:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
os.utime(filename, (time.time(), filetime))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return filetime
|
|
||||||
|
|
||||||
def report_destination(self, filename):
|
|
||||||
"""Report destination filename."""
|
|
||||||
self.to_screen('[download] Destination: ' + filename)
|
|
||||||
|
|
||||||
def _report_progress_status(self, msg, is_last_line=False):
|
|
||||||
fullmsg = '[download] ' + msg
|
|
||||||
if self.params.get('progress_with_newline', False):
|
|
||||||
self.to_screen(fullmsg)
|
|
||||||
else:
|
|
||||||
if compat_os_name == 'nt':
|
|
||||||
prev_len = getattr(self, '_report_progress_prev_line_length',
|
|
||||||
0)
|
|
||||||
if prev_len > len(fullmsg):
|
|
||||||
fullmsg += ' ' * (prev_len - len(fullmsg))
|
|
||||||
self._report_progress_prev_line_length = len(fullmsg)
|
|
||||||
clear_line = '\r'
|
|
||||||
else:
|
|
||||||
clear_line = ('\r\x1b[K' if sys.stderr.isatty() else '\r')
|
|
||||||
self.to_screen(clear_line + fullmsg, skip_eol=not is_last_line)
|
|
||||||
self.to_console_title('youtube-dl ' + msg)
|
|
||||||
|
|
||||||
def report_progress(self, s):
|
|
||||||
if s['status'] == 'finished':
|
|
||||||
if self.params.get('noprogress', False):
|
|
||||||
self.to_screen('[download] Download completed')
|
|
||||||
else:
|
|
||||||
msg_template = '100%%'
|
|
||||||
if s.get('total_bytes') is not None:
|
|
||||||
s['_total_bytes_str'] = format_bytes(s['total_bytes'])
|
|
||||||
msg_template += ' of %(_total_bytes_str)s'
|
|
||||||
if s.get('elapsed') is not None:
|
|
||||||
s['_elapsed_str'] = self.format_seconds(s['elapsed'])
|
|
||||||
msg_template += ' in %(_elapsed_str)s'
|
|
||||||
self._report_progress_status(
|
|
||||||
msg_template % s, is_last_line=True)
|
|
||||||
|
|
||||||
if self.params.get('noprogress'):
|
|
||||||
return
|
|
||||||
|
|
||||||
if s['status'] != 'downloading':
|
|
||||||
return
|
|
||||||
|
|
||||||
if s.get('eta') is not None:
|
|
||||||
s['_eta_str'] = self.format_eta(s['eta'])
|
|
||||||
else:
|
|
||||||
s['_eta_str'] = 'Unknown ETA'
|
|
||||||
|
|
||||||
if s.get('total_bytes') and s.get('downloaded_bytes') is not None:
|
|
||||||
s['_percent_str'] = self.format_percent(100 * s['downloaded_bytes'] / s['total_bytes'])
|
|
||||||
elif s.get('total_bytes_estimate') and s.get('downloaded_bytes') is not None:
|
|
||||||
s['_percent_str'] = self.format_percent(100 * s['downloaded_bytes'] / s['total_bytes_estimate'])
|
|
||||||
else:
|
|
||||||
if s.get('downloaded_bytes') == 0:
|
|
||||||
s['_percent_str'] = self.format_percent(0)
|
|
||||||
else:
|
|
||||||
s['_percent_str'] = 'Unknown %'
|
|
||||||
|
|
||||||
if s.get('speed') is not None:
|
|
||||||
s['_speed_str'] = self.format_speed(s['speed'])
|
|
||||||
else:
|
|
||||||
s['_speed_str'] = 'Unknown speed'
|
|
||||||
|
|
||||||
if s.get('total_bytes') is not None:
|
|
||||||
s['_total_bytes_str'] = format_bytes(s['total_bytes'])
|
|
||||||
msg_template = '%(_percent_str)s of %(_total_bytes_str)s at %(_speed_str)s ETA %(_eta_str)s'
|
|
||||||
elif s.get('total_bytes_estimate') is not None:
|
|
||||||
s['_total_bytes_estimate_str'] = format_bytes(s['total_bytes_estimate'])
|
|
||||||
msg_template = '%(_percent_str)s of ~%(_total_bytes_estimate_str)s at %(_speed_str)s ETA %(_eta_str)s'
|
|
||||||
else:
|
|
||||||
if s.get('downloaded_bytes') is not None:
|
|
||||||
s['_downloaded_bytes_str'] = format_bytes(s['downloaded_bytes'])
|
|
||||||
if s.get('elapsed'):
|
|
||||||
s['_elapsed_str'] = self.format_seconds(s['elapsed'])
|
|
||||||
msg_template = '%(_downloaded_bytes_str)s at %(_speed_str)s (%(_elapsed_str)s)'
|
|
||||||
else:
|
|
||||||
msg_template = '%(_downloaded_bytes_str)s at %(_speed_str)s'
|
|
||||||
else:
|
|
||||||
msg_template = '%(_percent_str)s % at %(_speed_str)s ETA %(_eta_str)s'
|
|
||||||
|
|
||||||
self._report_progress_status(msg_template % s)
|
|
||||||
|
|
||||||
def report_resuming_byte(self, resume_len):
|
|
||||||
"""Report attempt to resume at given byte."""
|
|
||||||
self.to_screen('[download] Resuming download at byte %s' % resume_len)
|
|
||||||
|
|
||||||
def report_retry(self, err, count, retries):
|
|
||||||
"""Report retry in case of HTTP error 5xx"""
|
|
||||||
self.to_screen(
|
|
||||||
'[download] Got server HTTP error: %s. Retrying (attempt %d of %s)...'
|
|
||||||
% (error_to_compat_str(err), count, self.format_retries(retries)))
|
|
||||||
|
|
||||||
def report_file_already_downloaded(self, file_name):
|
|
||||||
"""Report file has already been fully downloaded."""
|
|
||||||
try:
|
|
||||||
self.to_screen('[download] %s has already been downloaded' % file_name)
|
|
||||||
except UnicodeEncodeError:
|
|
||||||
self.to_screen('[download] The file has already been downloaded')
|
|
||||||
|
|
||||||
def report_unable_to_resume(self):
|
|
||||||
"""Report it was impossible to resume download."""
|
|
||||||
self.to_screen('[download] Unable to resume')
|
|
||||||
|
|
||||||
def download(self, filename, info_dict):
|
|
||||||
"""Download to a filename using the info from info_dict
|
|
||||||
Return True on success and False otherwise
|
|
||||||
"""
|
|
||||||
|
|
||||||
nooverwrites_and_exists = (
|
|
||||||
self.params.get('nooverwrites', False) and
|
|
||||||
os.path.exists(encodeFilename(filename))
|
|
||||||
)
|
|
||||||
|
|
||||||
if not hasattr(filename, 'write'):
|
|
||||||
continuedl_and_exists = (
|
|
||||||
self.params.get('continuedl', True) and
|
|
||||||
os.path.isfile(encodeFilename(filename)) and
|
|
||||||
not self.params.get('nopart', False)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check file already present
|
|
||||||
if filename != '-' and (nooverwrites_and_exists or continuedl_and_exists):
|
|
||||||
self.report_file_already_downloaded(filename)
|
|
||||||
self._hook_progress({
|
|
||||||
'filename': filename,
|
|
||||||
'status': 'finished',
|
|
||||||
'total_bytes': os.path.getsize(encodeFilename(filename)),
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
|
|
||||||
min_sleep_interval = self.params.get('sleep_interval')
|
|
||||||
if min_sleep_interval:
|
|
||||||
max_sleep_interval = self.params.get('max_sleep_interval', min_sleep_interval)
|
|
||||||
sleep_interval = random.uniform(min_sleep_interval, max_sleep_interval)
|
|
||||||
self.to_screen(
|
|
||||||
'[download] Sleeping %s seconds...' % (
|
|
||||||
int(sleep_interval) if sleep_interval.is_integer()
|
|
||||||
else '%.2f' % sleep_interval))
|
|
||||||
time.sleep(sleep_interval)
|
|
||||||
|
|
||||||
return self.real_download(filename, info_dict)
|
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
|
||||||
"""Real download process. Redefine in subclasses."""
|
|
||||||
raise NotImplementedError('This method must be implemented by subclasses')
|
|
||||||
|
|
||||||
def _hook_progress(self, status):
|
|
||||||
for ph in self._progress_hooks:
|
|
||||||
ph(status)
|
|
||||||
|
|
||||||
def add_progress_hook(self, ph):
|
|
||||||
# See YoutubeDl.py (search for progress_hooks) for a description of
|
|
||||||
# this interface
|
|
||||||
self._progress_hooks.append(ph)
|
|
||||||
|
|
||||||
def _debug_cmd(self, args, exe=None):
|
|
||||||
if not self.params.get('verbose', False):
|
|
||||||
return
|
|
||||||
|
|
||||||
str_args = [decodeArgument(a) for a in args]
|
|
||||||
|
|
||||||
if exe is None:
|
|
||||||
exe = os.path.basename(str_args[0])
|
|
||||||
|
|
||||||
self.to_screen('[debug] %s command line: %s' % (
|
|
||||||
exe, shell_quote(str_args)))
|
|
@ -1,80 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from .fragment import FragmentFD
|
|
||||||
from ..compat import compat_urllib_error
|
|
||||||
from ..utils import (
|
|
||||||
DownloadError,
|
|
||||||
urljoin,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DashSegmentsFD(FragmentFD):
|
|
||||||
"""
|
|
||||||
Download segments in a DASH manifest
|
|
||||||
"""
|
|
||||||
|
|
||||||
FD_NAME = 'dashsegments'
|
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
|
||||||
fragment_base_url = info_dict.get('fragment_base_url')
|
|
||||||
fragments = info_dict['fragments'][:1] if self.params.get(
|
|
||||||
'test', False) else info_dict['fragments']
|
|
||||||
|
|
||||||
ctx = {
|
|
||||||
'filename': filename,
|
|
||||||
'total_frags': len(fragments),
|
|
||||||
}
|
|
||||||
|
|
||||||
self._prepare_and_start_frag_download(ctx)
|
|
||||||
|
|
||||||
fragment_retries = self.params.get('fragment_retries', 0)
|
|
||||||
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
|
|
||||||
|
|
||||||
frag_index = 0
|
|
||||||
for i, fragment in enumerate(fragments):
|
|
||||||
frag_index += 1
|
|
||||||
if frag_index <= ctx['fragment_index']:
|
|
||||||
continue
|
|
||||||
# In DASH, the first segment contains necessary headers to
|
|
||||||
# generate a valid MP4 file, so always abort for the first segment
|
|
||||||
fatal = i == 0 or not skip_unavailable_fragments
|
|
||||||
count = 0
|
|
||||||
while count <= fragment_retries:
|
|
||||||
try:
|
|
||||||
fragment_url = fragment.get('url')
|
|
||||||
if not fragment_url:
|
|
||||||
assert fragment_base_url
|
|
||||||
fragment_url = urljoin(fragment_base_url, fragment['path'])
|
|
||||||
success, frag_content = self._download_fragment(ctx, fragment_url, info_dict)
|
|
||||||
if not success:
|
|
||||||
return False
|
|
||||||
self._append_fragment(ctx, frag_content)
|
|
||||||
break
|
|
||||||
except compat_urllib_error.HTTPError as err:
|
|
||||||
# YouTube may often return 404 HTTP error for a fragment causing the
|
|
||||||
# whole download to fail. However if the same fragment is immediately
|
|
||||||
# retried with the same request data this usually succeeds (1-2 attemps
|
|
||||||
# is usually enough) thus allowing to download the whole file successfully.
|
|
||||||
# To be future-proof we will retry all fragments that fail with any
|
|
||||||
# HTTP error.
|
|
||||||
count += 1
|
|
||||||
if count <= fragment_retries:
|
|
||||||
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
|
||||||
except DownloadError:
|
|
||||||
# Don't retry fragment if error occurred during HTTP downloading
|
|
||||||
# itself since it has own retry settings
|
|
||||||
if not fatal:
|
|
||||||
self.report_skip_fragment(frag_index)
|
|
||||||
break
|
|
||||||
raise
|
|
||||||
|
|
||||||
if count > fragment_retries:
|
|
||||||
if not fatal:
|
|
||||||
self.report_skip_fragment(frag_index)
|
|
||||||
continue
|
|
||||||
self.report_error('giving up after %s fragment retries' % fragment_retries)
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._finish_frag_download(ctx)
|
|
||||||
|
|
||||||
return True
|
|
@ -1,354 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
|
||||||
from ..compat import (
|
|
||||||
compat_setenv,
|
|
||||||
compat_str,
|
|
||||||
)
|
|
||||||
from ..postprocessor.ffmpeg import FFmpegPostProcessor, EXT_TO_OUT_FORMATS
|
|
||||||
from ..utils import (
|
|
||||||
cli_option,
|
|
||||||
cli_valueless_option,
|
|
||||||
cli_bool_option,
|
|
||||||
cli_configuration_args,
|
|
||||||
encodeFilename,
|
|
||||||
encodeArgument,
|
|
||||||
handle_youtubedl_headers,
|
|
||||||
check_executable,
|
|
||||||
is_outdated_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ExternalFD(FileDownloader):
|
|
||||||
def real_download(self, filename, info_dict):
|
|
||||||
self.report_destination(filename)
|
|
||||||
tmpfilename = self.temp_name(filename)
|
|
||||||
|
|
||||||
try:
|
|
||||||
started = time.time()
|
|
||||||
retval = self._call_downloader(tmpfilename, info_dict)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
if not info_dict.get('is_live'):
|
|
||||||
raise
|
|
||||||
# Live stream downloading cancellation should be considered as
|
|
||||||
# correct and expected termination thus all postprocessing
|
|
||||||
# should take place
|
|
||||||
retval = 0
|
|
||||||
self.to_screen('[%s] Interrupted by user' % self.get_basename())
|
|
||||||
|
|
||||||
if retval == 0:
|
|
||||||
status = {
|
|
||||||
'filename': filename,
|
|
||||||
'status': 'finished',
|
|
||||||
'elapsed': time.time() - started,
|
|
||||||
}
|
|
||||||
if filename != '-':
|
|
||||||
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
|
||||||
self.to_screen('\r[%s] Downloaded %s bytes' % (self.get_basename(), fsize))
|
|
||||||
self.try_rename(tmpfilename, filename)
|
|
||||||
status.update({
|
|
||||||
'downloaded_bytes': fsize,
|
|
||||||
'total_bytes': fsize,
|
|
||||||
})
|
|
||||||
self._hook_progress(status)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.to_stderr('\n')
|
|
||||||
self.report_error('%s exited with code %d' % (
|
|
||||||
self.get_basename(), retval))
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_basename(cls):
|
|
||||||
return cls.__name__[:-2].lower()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def exe(self):
|
|
||||||
return self.params.get('external_downloader')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def available(cls):
|
|
||||||
return check_executable(cls.get_basename(), [cls.AVAILABLE_OPT])
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def supports(cls, info_dict):
|
|
||||||
return info_dict['protocol'] in ('http', 'https', 'ftp', 'ftps')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def can_download(cls, info_dict):
|
|
||||||
return cls.available() and cls.supports(info_dict)
|
|
||||||
|
|
||||||
def _option(self, command_option, param):
|
|
||||||
return cli_option(self.params, command_option, param)
|
|
||||||
|
|
||||||
def _bool_option(self, command_option, param, true_value='true', false_value='false', separator=None):
|
|
||||||
return cli_bool_option(self.params, command_option, param, true_value, false_value, separator)
|
|
||||||
|
|
||||||
def _valueless_option(self, command_option, param, expected_value=True):
|
|
||||||
return cli_valueless_option(self.params, command_option, param, expected_value)
|
|
||||||
|
|
||||||
def _configuration_args(self, default=[]):
|
|
||||||
return cli_configuration_args(self.params, 'external_downloader_args', default)
|
|
||||||
|
|
||||||
def _call_downloader(self, tmpfilename, info_dict):
|
|
||||||
""" Either overwrite this or implement _make_cmd """
|
|
||||||
cmd = [encodeArgument(a) for a in self._make_cmd(tmpfilename, info_dict)]
|
|
||||||
|
|
||||||
self._debug_cmd(cmd)
|
|
||||||
|
|
||||||
p = subprocess.Popen(
|
|
||||||
cmd, stderr=subprocess.PIPE)
|
|
||||||
_, stderr = p.communicate()
|
|
||||||
if p.returncode != 0:
|
|
||||||
self.to_stderr(stderr.decode('utf-8', 'replace'))
|
|
||||||
return p.returncode
|
|
||||||
|
|
||||||
|
|
||||||
class CurlFD(ExternalFD):
|
|
||||||
AVAILABLE_OPT = '-V'
|
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
|
||||||
cmd = [self.exe, '--location', '-o', tmpfilename]
|
|
||||||
for key, val in info_dict['http_headers'].items():
|
|
||||||
cmd += ['--header', '%s: %s' % (key, val)]
|
|
||||||
cmd += self._bool_option('--continue-at', 'continuedl', '-', '0')
|
|
||||||
cmd += self._valueless_option('--silent', 'noprogress')
|
|
||||||
cmd += self._valueless_option('--verbose', 'verbose')
|
|
||||||
cmd += self._option('--limit-rate', 'ratelimit')
|
|
||||||
cmd += self._option('--retry', 'retries')
|
|
||||||
cmd += self._option('--max-filesize', 'max_filesize')
|
|
||||||
cmd += self._option('--interface', 'source_address')
|
|
||||||
cmd += self._option('--proxy', 'proxy')
|
|
||||||
cmd += self._valueless_option('--insecure', 'nocheckcertificate')
|
|
||||||
cmd += self._configuration_args()
|
|
||||||
cmd += ['--', info_dict['url']]
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
def _call_downloader(self, tmpfilename, info_dict):
|
|
||||||
cmd = [encodeArgument(a) for a in self._make_cmd(tmpfilename, info_dict)]
|
|
||||||
|
|
||||||
self._debug_cmd(cmd)
|
|
||||||
|
|
||||||
# curl writes the progress to stderr so don't capture it.
|
|
||||||
p = subprocess.Popen(cmd)
|
|
||||||
p.communicate()
|
|
||||||
return p.returncode
|
|
||||||
|
|
||||||
|
|
||||||
class AxelFD(ExternalFD):
|
|
||||||
AVAILABLE_OPT = '-V'
|
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
|
||||||
cmd = [self.exe, '-o', tmpfilename]
|
|
||||||
for key, val in info_dict['http_headers'].items():
|
|
||||||
cmd += ['-H', '%s: %s' % (key, val)]
|
|
||||||
cmd += self._configuration_args()
|
|
||||||
cmd += ['--', info_dict['url']]
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
|
|
||||||
class WgetFD(ExternalFD):
|
|
||||||
AVAILABLE_OPT = '--version'
|
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
|
||||||
cmd = [self.exe, '-O', tmpfilename, '-nv', '--no-cookies']
|
|
||||||
for key, val in info_dict['http_headers'].items():
|
|
||||||
cmd += ['--header', '%s: %s' % (key, val)]
|
|
||||||
cmd += self._option('--bind-address', 'source_address')
|
|
||||||
cmd += self._option('--proxy', 'proxy')
|
|
||||||
cmd += self._valueless_option('--no-check-certificate', 'nocheckcertificate')
|
|
||||||
cmd += self._configuration_args()
|
|
||||||
cmd += ['--', info_dict['url']]
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
|
|
||||||
class Aria2cFD(ExternalFD):
|
|
||||||
AVAILABLE_OPT = '-v'
|
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
|
||||||
cmd = [self.exe, '-c']
|
|
||||||
cmd += self._configuration_args([
|
|
||||||
'--min-split-size', '1M', '--max-connection-per-server', '4'])
|
|
||||||
dn = os.path.dirname(tmpfilename)
|
|
||||||
if dn:
|
|
||||||
cmd += ['--dir', dn]
|
|
||||||
cmd += ['--out', os.path.basename(tmpfilename)]
|
|
||||||
for key, val in info_dict['http_headers'].items():
|
|
||||||
cmd += ['--header', '%s: %s' % (key, val)]
|
|
||||||
cmd += self._option('--interface', 'source_address')
|
|
||||||
cmd += self._option('--all-proxy', 'proxy')
|
|
||||||
cmd += self._bool_option('--check-certificate', 'nocheckcertificate', 'false', 'true', '=')
|
|
||||||
cmd += ['--', info_dict['url']]
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
|
|
||||||
class HttpieFD(ExternalFD):
|
|
||||||
@classmethod
|
|
||||||
def available(cls):
|
|
||||||
return check_executable('http', ['--version'])
|
|
||||||
|
|
||||||
def _make_cmd(self, tmpfilename, info_dict):
|
|
||||||
cmd = ['http', '--download', '--output', tmpfilename, info_dict['url']]
|
|
||||||
for key, val in info_dict['http_headers'].items():
|
|
||||||
cmd += ['%s:%s' % (key, val)]
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegFD(ExternalFD):
|
|
||||||
@classmethod
|
|
||||||
def supports(cls, info_dict):
|
|
||||||
return info_dict['protocol'] in ('http', 'https', 'ftp', 'ftps', 'm3u8', 'rtsp', 'rtmp', 'mms')
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def available(cls):
|
|
||||||
return FFmpegPostProcessor().available
|
|
||||||
|
|
||||||
def _call_downloader(self, tmpfilename, info_dict):
|
|
||||||
url = info_dict['url']
|
|
||||||
ffpp = FFmpegPostProcessor(downloader=self)
|
|
||||||
if not ffpp.available:
|
|
||||||
self.report_error('m3u8 download detected but ffmpeg or avconv could not be found. Please install one.')
|
|
||||||
return False
|
|
||||||
ffpp.check_version()
|
|
||||||
|
|
||||||
args = [ffpp.executable, '-y']
|
|
||||||
|
|
||||||
for log_level in ('quiet', 'verbose'):
|
|
||||||
if self.params.get(log_level, False):
|
|
||||||
args += ['-loglevel', log_level]
|
|
||||||
break
|
|
||||||
|
|
||||||
seekable = info_dict.get('_seekable')
|
|
||||||
if seekable is not None:
|
|
||||||
# setting -seekable prevents ffmpeg from guessing if the server
|
|
||||||
# supports seeking(by adding the header `Range: bytes=0-`), which
|
|
||||||
# can cause problems in some cases
|
|
||||||
# https://github.com/rg3/youtube-dl/issues/11800#issuecomment-275037127
|
|
||||||
# http://trac.ffmpeg.org/ticket/6125#comment:10
|
|
||||||
args += ['-seekable', '1' if seekable else '0']
|
|
||||||
|
|
||||||
args += self._configuration_args()
|
|
||||||
|
|
||||||
# start_time = info_dict.get('start_time') or 0
|
|
||||||
# if start_time:
|
|
||||||
# args += ['-ss', compat_str(start_time)]
|
|
||||||
# end_time = info_dict.get('end_time')
|
|
||||||
# if end_time:
|
|
||||||
# args += ['-t', compat_str(end_time - start_time)]
|
|
||||||
|
|
||||||
if info_dict['http_headers'] and re.match(r'^https?://', url):
|
|
||||||
# Trailing \r\n after each HTTP header is important to prevent warning from ffmpeg/avconv:
|
|
||||||
# [http @ 00000000003d2fa0] No trailing CRLF found in HTTP header.
|
|
||||||
headers = handle_youtubedl_headers(info_dict['http_headers'])
|
|
||||||
args += [
|
|
||||||
'-headers',
|
|
||||||
''.join('%s: %s\r\n' % (key, val) for key, val in headers.items())]
|
|
||||||
|
|
||||||
env = None
|
|
||||||
proxy = self.params.get('proxy')
|
|
||||||
if proxy:
|
|
||||||
if not re.match(r'^[\da-zA-Z]+://', proxy):
|
|
||||||
proxy = 'http://%s' % proxy
|
|
||||||
|
|
||||||
if proxy.startswith('socks'):
|
|
||||||
self.report_warning(
|
|
||||||
'%s does not support SOCKS proxies. Downloading is likely to fail. '
|
|
||||||
'Consider adding --hls-prefer-native to your command.' % self.get_basename())
|
|
||||||
|
|
||||||
# Since December 2015 ffmpeg supports -http_proxy option (see
|
|
||||||
# http://git.videolan.org/?p=ffmpeg.git;a=commit;h=b4eb1f29ebddd60c41a2eb39f5af701e38e0d3fd)
|
|
||||||
# We could switch to the following code if we are able to detect version properly
|
|
||||||
# args += ['-http_proxy', proxy]
|
|
||||||
env = os.environ.copy()
|
|
||||||
compat_setenv('HTTP_PROXY', proxy, env=env)
|
|
||||||
compat_setenv('http_proxy', proxy, env=env)
|
|
||||||
|
|
||||||
protocol = info_dict.get('protocol')
|
|
||||||
|
|
||||||
if protocol == 'rtmp':
|
|
||||||
player_url = info_dict.get('player_url')
|
|
||||||
page_url = info_dict.get('page_url')
|
|
||||||
app = info_dict.get('app')
|
|
||||||
play_path = info_dict.get('play_path')
|
|
||||||
tc_url = info_dict.get('tc_url')
|
|
||||||
flash_version = info_dict.get('flash_version')
|
|
||||||
live = info_dict.get('rtmp_live', False)
|
|
||||||
if player_url is not None:
|
|
||||||
args += ['-rtmp_swfverify', player_url]
|
|
||||||
if page_url is not None:
|
|
||||||
args += ['-rtmp_pageurl', page_url]
|
|
||||||
if app is not None:
|
|
||||||
args += ['-rtmp_app', app]
|
|
||||||
if play_path is not None:
|
|
||||||
args += ['-rtmp_playpath', play_path]
|
|
||||||
if tc_url is not None:
|
|
||||||
args += ['-rtmp_tcurl', tc_url]
|
|
||||||
if flash_version is not None:
|
|
||||||
args += ['-rtmp_flashver', flash_version]
|
|
||||||
if live:
|
|
||||||
args += ['-rtmp_live', 'live']
|
|
||||||
|
|
||||||
args += ['-i', url, '-c', 'copy']
|
|
||||||
|
|
||||||
if self.params.get('test', False):
|
|
||||||
args += ['-fs', compat_str(self._TEST_FILE_SIZE)]
|
|
||||||
|
|
||||||
if protocol in ('m3u8', 'm3u8_native'):
|
|
||||||
if self.params.get('hls_use_mpegts', False) or tmpfilename == '-':
|
|
||||||
args += ['-f', 'mpegts']
|
|
||||||
else:
|
|
||||||
args += ['-f', 'mp4']
|
|
||||||
if (ffpp.basename == 'ffmpeg' and is_outdated_version(ffpp._versions['ffmpeg'], '3.2', False)) and (not info_dict.get('acodec') or info_dict['acodec'].split('.')[0] in ('aac', 'mp4a')):
|
|
||||||
args += ['-bsf:a', 'aac_adtstoasc']
|
|
||||||
elif protocol == 'rtmp':
|
|
||||||
args += ['-f', 'flv']
|
|
||||||
else:
|
|
||||||
args += ['-f', EXT_TO_OUT_FORMATS.get(info_dict['ext'], info_dict['ext'])]
|
|
||||||
|
|
||||||
args = [encodeArgument(opt) for opt in args]
|
|
||||||
args.append(encodeFilename(ffpp._ffmpeg_filename_argument(tmpfilename), True))
|
|
||||||
|
|
||||||
self._debug_cmd(args)
|
|
||||||
|
|
||||||
proc = subprocess.Popen(args, stdin=subprocess.PIPE, env=env)
|
|
||||||
try:
|
|
||||||
retval = proc.wait()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
# subprocces.run would send the SIGKILL signal to ffmpeg and the
|
|
||||||
# mp4 file couldn't be played, but if we ask ffmpeg to quit it
|
|
||||||
# produces a file that is playable (this is mostly useful for live
|
|
||||||
# streams). Note that Windows is not affected and produces playable
|
|
||||||
# files (see https://github.com/rg3/youtube-dl/issues/8300).
|
|
||||||
if sys.platform != 'win32':
|
|
||||||
proc.communicate(b'q')
|
|
||||||
raise
|
|
||||||
return retval
|
|
||||||
|
|
||||||
|
|
||||||
class AVconvFD(FFmpegFD):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
_BY_NAME = dict(
|
|
||||||
(klass.get_basename(), klass)
|
|
||||||
for name, klass in globals().items()
|
|
||||||
if name.endswith('FD') and name != 'ExternalFD'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def list_external_downloaders():
|
|
||||||
return sorted(_BY_NAME.keys())
|
|
||||||
|
|
||||||
|
|
||||||
def get_external_downloader(external_downloader):
|
|
||||||
""" Given the name of the executable, see whether we support the given
|
|
||||||
downloader . """
|
|
||||||
# Drop .exe extension on Windows
|
|
||||||
bn = os.path.splitext(os.path.basename(external_downloader))[0]
|
|
||||||
return _BY_NAME[bn]
|
|
@ -1,438 +0,0 @@
|
|||||||
from __future__ import division, unicode_literals
|
|
||||||
|
|
||||||
import io
|
|
||||||
import itertools
|
|
||||||
import time
|
|
||||||
|
|
||||||
from .fragment import FragmentFD
|
|
||||||
from ..compat import (
|
|
||||||
compat_b64decode,
|
|
||||||
compat_etree_fromstring,
|
|
||||||
compat_urlparse,
|
|
||||||
compat_urllib_error,
|
|
||||||
compat_urllib_parse_urlparse,
|
|
||||||
compat_struct_pack,
|
|
||||||
compat_struct_unpack,
|
|
||||||
)
|
|
||||||
from ..utils import (
|
|
||||||
fix_xml_ampersands,
|
|
||||||
xpath_text,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DataTruncatedError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FlvReader(io.BytesIO):
|
|
||||||
"""
|
|
||||||
Reader for Flv files
|
|
||||||
The file format is documented in https://www.adobe.com/devnet/f4v.html
|
|
||||||
"""
|
|
||||||
|
|
||||||
def read_bytes(self, n):
|
|
||||||
data = self.read(n)
|
|
||||||
if len(data) < n:
|
|
||||||
raise DataTruncatedError(
|
|
||||||
'FlvReader error: need %d bytes while only %d bytes got' % (
|
|
||||||
n, len(data)))
|
|
||||||
return data
|
|
||||||
|
|
||||||
# Utility functions for reading numbers and strings
|
|
||||||
def read_unsigned_long_long(self):
|
|
||||||
return compat_struct_unpack('!Q', self.read_bytes(8))[0]
|
|
||||||
|
|
||||||
def read_unsigned_int(self):
|
|
||||||
return compat_struct_unpack('!I', self.read_bytes(4))[0]
|
|
||||||
|
|
||||||
def read_unsigned_char(self):
|
|
||||||
return compat_struct_unpack('!B', self.read_bytes(1))[0]
|
|
||||||
|
|
||||||
def read_string(self):
|
|
||||||
res = b''
|
|
||||||
while True:
|
|
||||||
char = self.read_bytes(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_bytes(4)
|
|
||||||
header_end = 8
|
|
||||||
if size == 1:
|
|
||||||
real_size = self.read_unsigned_long_long()
|
|
||||||
header_end = 16
|
|
||||||
return real_size, box_type, self.read_bytes(real_size - header_end)
|
|
||||||
|
|
||||||
def read_asrt(self):
|
|
||||||
# version
|
|
||||||
self.read_unsigned_char()
|
|
||||||
# flags
|
|
||||||
self.read_bytes(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_bytes(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_bytes(3)
|
|
||||||
|
|
||||||
self.read_unsigned_int() # BootstrapinfoVersion
|
|
||||||
# Profile,Live,Update,Reserved
|
|
||||||
flags = self.read_unsigned_char()
|
|
||||||
live = flags & 0x20 != 0
|
|
||||||
# time scale
|
|
||||||
self.read_unsigned_int()
|
|
||||||
# CurrentMediaTime
|
|
||||||
self.read_unsigned_long_long()
|
|
||||||
# SmpteTimeCodeOffset
|
|
||||||
self.read_unsigned_long_long()
|
|
||||||
|
|
||||||
self.read_string() # MovieIdentifier
|
|
||||||
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(quality_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,
|
|
||||||
'live': live,
|
|
||||||
}
|
|
||||||
|
|
||||||
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]
|
|
||||||
fragment_run_entry_table = boot_info['fragments'][0]['fragments']
|
|
||||||
first_frag_number = fragment_run_entry_table[0]['first']
|
|
||||||
fragments_counter = itertools.count(first_frag_number)
|
|
||||||
for segment, fragments_count in segment_run_table['segment_run']:
|
|
||||||
# In some live HDS streams (for example Rai), `fragments_count` is
|
|
||||||
# abnormal and causing out-of-memory errors. It's OK to change the
|
|
||||||
# number of fragments for live streams as they are updated periodically
|
|
||||||
if fragments_count == 4294967295 and boot_info['live']:
|
|
||||||
fragments_count = 2
|
|
||||||
for _ in range(fragments_count):
|
|
||||||
res.append((segment, next(fragments_counter)))
|
|
||||||
|
|
||||||
if boot_info['live']:
|
|
||||||
res = res[-2:]
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def write_unsigned_int(stream, val):
|
|
||||||
stream.write(compat_struct_pack('!I', val))
|
|
||||||
|
|
||||||
|
|
||||||
def write_unsigned_int_24(stream, val):
|
|
||||||
stream.write(compat_struct_pack('!I', val)[1:])
|
|
||||||
|
|
||||||
|
|
||||||
def write_flv_header(stream):
|
|
||||||
"""Writes the FLV header to stream"""
|
|
||||||
# FLV header
|
|
||||||
stream.write(b'FLV\x01')
|
|
||||||
stream.write(b'\x05')
|
|
||||||
stream.write(b'\x00\x00\x00\x09')
|
|
||||||
stream.write(b'\x00\x00\x00\x00')
|
|
||||||
|
|
||||||
|
|
||||||
def write_metadata_tag(stream, metadata):
|
|
||||||
"""Writes optional metadata tag to stream"""
|
|
||||||
SCRIPT_TAG = b'\x12'
|
|
||||||
FLV_TAG_HEADER_LEN = 11
|
|
||||||
|
|
||||||
if metadata:
|
|
||||||
stream.write(SCRIPT_TAG)
|
|
||||||
write_unsigned_int_24(stream, len(metadata))
|
|
||||||
stream.write(b'\x00\x00\x00\x00\x00\x00\x00')
|
|
||||||
stream.write(metadata)
|
|
||||||
write_unsigned_int(stream, FLV_TAG_HEADER_LEN + len(metadata))
|
|
||||||
|
|
||||||
|
|
||||||
def remove_encrypted_media(media):
|
|
||||||
return list(filter(lambda e: 'drmAdditionalHeaderId' not in e.attrib and
|
|
||||||
'drmAdditionalHeaderSetId' not in e.attrib,
|
|
||||||
media))
|
|
||||||
|
|
||||||
|
|
||||||
def _add_ns(prop, ver=1):
|
|
||||||
return '{http://ns.adobe.com/f4m/%d.0}%s' % (ver, prop)
|
|
||||||
|
|
||||||
|
|
||||||
def get_base_url(manifest):
|
|
||||||
base_url = xpath_text(
|
|
||||||
manifest, [_add_ns('baseURL'), _add_ns('baseURL', 2)],
|
|
||||||
'base URL', default=None)
|
|
||||||
if base_url:
|
|
||||||
base_url = base_url.strip()
|
|
||||||
return base_url
|
|
||||||
|
|
||||||
|
|
||||||
class F4mFD(FragmentFD):
|
|
||||||
"""
|
|
||||||
A downloader for f4m manifests or AdobeHDS.
|
|
||||||
"""
|
|
||||||
|
|
||||||
FD_NAME = 'f4m'
|
|
||||||
|
|
||||||
def _get_unencrypted_media(self, doc):
|
|
||||||
media = doc.findall(_add_ns('media'))
|
|
||||||
if not media:
|
|
||||||
self.report_error('No media found')
|
|
||||||
for e in (doc.findall(_add_ns('drmAdditionalHeader')) +
|
|
||||||
doc.findall(_add_ns('drmAdditionalHeaderSet'))):
|
|
||||||
# If id attribute is missing it's valid for all media nodes
|
|
||||||
# without drmAdditionalHeaderId or drmAdditionalHeaderSetId attribute
|
|
||||||
if 'id' not in e.attrib:
|
|
||||||
self.report_error('Missing ID in f4m DRM')
|
|
||||||
media = remove_encrypted_media(media)
|
|
||||||
if not media:
|
|
||||||
self.report_error('Unsupported DRM')
|
|
||||||
return media
|
|
||||||
|
|
||||||
def _get_bootstrap_from_url(self, bootstrap_url):
|
|
||||||
bootstrap = self.ydl.urlopen(bootstrap_url).read()
|
|
||||||
return read_bootstrap_info(bootstrap)
|
|
||||||
|
|
||||||
def _update_live_fragments(self, bootstrap_url, latest_fragment):
|
|
||||||
fragments_list = []
|
|
||||||
retries = 30
|
|
||||||
while (not fragments_list) and (retries > 0):
|
|
||||||
boot_info = self._get_bootstrap_from_url(bootstrap_url)
|
|
||||||
fragments_list = build_fragments_list(boot_info)
|
|
||||||
fragments_list = [f for f in fragments_list if f[1] > latest_fragment]
|
|
||||||
if not fragments_list:
|
|
||||||
# Retry after a while
|
|
||||||
time.sleep(5.0)
|
|
||||||
retries -= 1
|
|
||||||
|
|
||||||
if not fragments_list:
|
|
||||||
self.report_error('Failed to update fragments')
|
|
||||||
|
|
||||||
return fragments_list
|
|
||||||
|
|
||||||
def _parse_bootstrap_node(self, node, base_url):
|
|
||||||
# Sometimes non empty inline bootstrap info can be specified along
|
|
||||||
# with bootstrap url attribute (e.g. dummy inline bootstrap info
|
|
||||||
# contains whitespace characters in [1]). We will prefer bootstrap
|
|
||||||
# url over inline bootstrap info when present.
|
|
||||||
# 1. http://live-1-1.rutube.ru/stream/1024/HDS/SD/C2NKsS85HQNckgn5HdEmOQ/1454167650/S-s604419906/move/four/dirs/upper/1024-576p.f4m
|
|
||||||
bootstrap_url = node.get('url')
|
|
||||||
if bootstrap_url:
|
|
||||||
bootstrap_url = compat_urlparse.urljoin(
|
|
||||||
base_url, bootstrap_url)
|
|
||||||
boot_info = self._get_bootstrap_from_url(bootstrap_url)
|
|
||||||
else:
|
|
||||||
bootstrap_url = None
|
|
||||||
bootstrap = compat_b64decode(node.text)
|
|
||||||
boot_info = read_bootstrap_info(bootstrap)
|
|
||||||
return boot_info, bootstrap_url
|
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
|
||||||
man_url = info_dict['url']
|
|
||||||
requested_bitrate = info_dict.get('tbr')
|
|
||||||
self.to_screen('[%s] Downloading f4m manifest' % self.FD_NAME)
|
|
||||||
|
|
||||||
urlh = self.ydl.urlopen(self._prepare_url(info_dict, man_url))
|
|
||||||
man_url = urlh.geturl()
|
|
||||||
# Some manifests may be malformed, e.g. prosiebensat1 generated manifests
|
|
||||||
# (see https://github.com/rg3/youtube-dl/issues/6215#issuecomment-121704244
|
|
||||||
# and https://github.com/rg3/youtube-dl/issues/7823)
|
|
||||||
manifest = fix_xml_ampersands(urlh.read().decode('utf-8', 'ignore')).strip()
|
|
||||||
|
|
||||||
doc = compat_etree_fromstring(manifest)
|
|
||||||
formats = [(int(f.attrib.get('bitrate', -1)), f)
|
|
||||||
for f in self._get_unencrypted_media(doc)]
|
|
||||||
if requested_bitrate is None or len(formats) == 1:
|
|
||||||
# get the best format
|
|
||||||
formats = sorted(formats, key=lambda f: f[0])
|
|
||||||
rate, media = formats[-1]
|
|
||||||
else:
|
|
||||||
rate, media = list(filter(
|
|
||||||
lambda f: int(f[0]) == requested_bitrate, formats))[0]
|
|
||||||
|
|
||||||
# Prefer baseURL for relative URLs as per 11.2 of F4M 3.0 spec.
|
|
||||||
man_base_url = get_base_url(doc) or man_url
|
|
||||||
|
|
||||||
base_url = compat_urlparse.urljoin(man_base_url, media.attrib['url'])
|
|
||||||
bootstrap_node = doc.find(_add_ns('bootstrapInfo'))
|
|
||||||
boot_info, bootstrap_url = self._parse_bootstrap_node(
|
|
||||||
bootstrap_node, man_base_url)
|
|
||||||
live = boot_info['live']
|
|
||||||
metadata_node = media.find(_add_ns('metadata'))
|
|
||||||
if metadata_node is not None:
|
|
||||||
metadata = compat_b64decode(metadata_node.text)
|
|
||||||
else:
|
|
||||||
metadata = None
|
|
||||||
|
|
||||||
fragments_list = build_fragments_list(boot_info)
|
|
||||||
test = self.params.get('test', False)
|
|
||||||
if test:
|
|
||||||
# We only download the first fragment
|
|
||||||
fragments_list = fragments_list[:1]
|
|
||||||
total_frags = len(fragments_list)
|
|
||||||
# For some akamai manifests we'll need to add a query to the fragment url
|
|
||||||
akamai_pv = xpath_text(doc, _add_ns('pv-2.0'))
|
|
||||||
|
|
||||||
ctx = {
|
|
||||||
'filename': filename,
|
|
||||||
'total_frags': total_frags,
|
|
||||||
'live': live,
|
|
||||||
}
|
|
||||||
|
|
||||||
self._prepare_frag_download(ctx)
|
|
||||||
|
|
||||||
dest_stream = ctx['dest_stream']
|
|
||||||
|
|
||||||
if ctx['complete_frags_downloaded_bytes'] == 0:
|
|
||||||
write_flv_header(dest_stream)
|
|
||||||
if not live:
|
|
||||||
write_metadata_tag(dest_stream, metadata)
|
|
||||||
|
|
||||||
base_url_parsed = compat_urllib_parse_urlparse(base_url)
|
|
||||||
|
|
||||||
self._start_frag_download(ctx)
|
|
||||||
|
|
||||||
frag_index = 0
|
|
||||||
while fragments_list:
|
|
||||||
seg_i, frag_i = fragments_list.pop(0)
|
|
||||||
frag_index += 1
|
|
||||||
if frag_index <= ctx['fragment_index']:
|
|
||||||
continue
|
|
||||||
name = 'Seg%d-Frag%d' % (seg_i, frag_i)
|
|
||||||
query = []
|
|
||||||
if base_url_parsed.query:
|
|
||||||
query.append(base_url_parsed.query)
|
|
||||||
if akamai_pv:
|
|
||||||
query.append(akamai_pv.strip(';'))
|
|
||||||
if info_dict.get('extra_param_to_segment_url'):
|
|
||||||
query.append(info_dict['extra_param_to_segment_url'])
|
|
||||||
url_parsed = base_url_parsed._replace(path=base_url_parsed.path + name, query='&'.join(query))
|
|
||||||
try:
|
|
||||||
success, down_data = self._download_fragment(ctx, url_parsed.geturl(), info_dict)
|
|
||||||
if not success:
|
|
||||||
return False
|
|
||||||
reader = FlvReader(down_data)
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
_, box_type, box_data = reader.read_box_info()
|
|
||||||
except DataTruncatedError:
|
|
||||||
if test:
|
|
||||||
# In tests, segments may be truncated, and thus
|
|
||||||
# FlvReader may not be able to parse the whole
|
|
||||||
# chunk. If so, write the segment as is
|
|
||||||
# See https://github.com/rg3/youtube-dl/issues/9214
|
|
||||||
dest_stream.write(down_data)
|
|
||||||
break
|
|
||||||
raise
|
|
||||||
if box_type == b'mdat':
|
|
||||||
self._append_fragment(ctx, box_data)
|
|
||||||
break
|
|
||||||
except (compat_urllib_error.HTTPError, ) as err:
|
|
||||||
if live and (err.code == 404 or err.code == 410):
|
|
||||||
# We didn't keep up with the live window. Continue
|
|
||||||
# with the next available fragment.
|
|
||||||
msg = 'Fragment %d unavailable' % frag_i
|
|
||||||
self.report_warning(msg)
|
|
||||||
fragments_list = []
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
if not fragments_list and not test and live and bootstrap_url:
|
|
||||||
fragments_list = self._update_live_fragments(bootstrap_url, frag_i)
|
|
||||||
total_frags += len(fragments_list)
|
|
||||||
if fragments_list and (fragments_list[0][1] > frag_i + 1):
|
|
||||||
msg = 'Missed %d fragments' % (fragments_list[0][1] - (frag_i + 1))
|
|
||||||
self.report_warning(msg)
|
|
||||||
|
|
||||||
self._finish_frag_download(ctx)
|
|
||||||
|
|
||||||
return True
|
|
@ -1,268 +0,0 @@
|
|||||||
from __future__ import division, unicode_literals
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import json
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
|
||||||
from .http import HttpFD
|
|
||||||
from ..utils import (
|
|
||||||
error_to_compat_str,
|
|
||||||
encodeFilename,
|
|
||||||
sanitize_open,
|
|
||||||
sanitized_Request,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HttpQuietDownloader(HttpFD):
|
|
||||||
def to_screen(self, *args, **kargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FragmentFD(FileDownloader):
|
|
||||||
"""
|
|
||||||
A base file downloader class for fragmented media (e.g. f4m/m3u8 manifests).
|
|
||||||
|
|
||||||
Available options:
|
|
||||||
|
|
||||||
fragment_retries: Number of times to retry a fragment for HTTP error (DASH
|
|
||||||
and hlsnative only)
|
|
||||||
skip_unavailable_fragments:
|
|
||||||
Skip unavailable fragments (DASH and hlsnative only)
|
|
||||||
keep_fragments: Keep downloaded fragments on disk after downloading is
|
|
||||||
finished
|
|
||||||
|
|
||||||
For each incomplete fragment download youtube-dl keeps on disk a special
|
|
||||||
bookkeeping file with download state and metadata (in future such files will
|
|
||||||
be used for any incomplete download handled by youtube-dl). This file is
|
|
||||||
used to properly handle resuming, check download file consistency and detect
|
|
||||||
potential errors. The file has a .ytdl extension and represents a standard
|
|
||||||
JSON file of the following format:
|
|
||||||
|
|
||||||
extractor:
|
|
||||||
Dictionary of extractor related data. TBD.
|
|
||||||
|
|
||||||
downloader:
|
|
||||||
Dictionary of downloader related data. May contain following data:
|
|
||||||
current_fragment:
|
|
||||||
Dictionary with current (being downloaded) fragment data:
|
|
||||||
index: 0-based index of current fragment among all fragments
|
|
||||||
fragment_count:
|
|
||||||
Total count of fragments
|
|
||||||
|
|
||||||
This feature is experimental and file format may change in future.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def report_retry_fragment(self, err, frag_index, count, retries):
|
|
||||||
self.to_screen(
|
|
||||||
'[download] Got server HTTP error: %s. Retrying fragment %d (attempt %d of %s)...'
|
|
||||||
% (error_to_compat_str(err), frag_index, count, self.format_retries(retries)))
|
|
||||||
|
|
||||||
def report_skip_fragment(self, frag_index):
|
|
||||||
self.to_screen('[download] Skipping fragment %d...' % frag_index)
|
|
||||||
|
|
||||||
def _prepare_url(self, info_dict, url):
|
|
||||||
headers = info_dict.get('http_headers')
|
|
||||||
return sanitized_Request(url, None, headers) if headers else url
|
|
||||||
|
|
||||||
def _prepare_and_start_frag_download(self, ctx):
|
|
||||||
self._prepare_frag_download(ctx)
|
|
||||||
self._start_frag_download(ctx)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __do_ytdl_file(ctx):
|
|
||||||
return not ctx['live'] and not ctx['tmpfilename'] == '-'
|
|
||||||
|
|
||||||
def _read_ytdl_file(self, ctx):
|
|
||||||
assert 'ytdl_corrupt' not in ctx
|
|
||||||
stream, _ = sanitize_open(self.ytdl_filename(ctx['filename']), 'r')
|
|
||||||
try:
|
|
||||||
ctx['fragment_index'] = json.loads(stream.read())['downloader']['current_fragment']['index']
|
|
||||||
except Exception:
|
|
||||||
ctx['ytdl_corrupt'] = True
|
|
||||||
finally:
|
|
||||||
stream.close()
|
|
||||||
|
|
||||||
def _write_ytdl_file(self, ctx):
|
|
||||||
frag_index_stream, _ = sanitize_open(self.ytdl_filename(ctx['filename']), 'w')
|
|
||||||
downloader = {
|
|
||||||
'current_fragment': {
|
|
||||||
'index': ctx['fragment_index'],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if ctx.get('fragment_count') is not None:
|
|
||||||
downloader['fragment_count'] = ctx['fragment_count']
|
|
||||||
frag_index_stream.write(json.dumps({'downloader': downloader}))
|
|
||||||
frag_index_stream.close()
|
|
||||||
|
|
||||||
def _download_fragment(self, ctx, frag_url, info_dict, headers=None):
|
|
||||||
fragment_filename = '%s-Frag%d' % (ctx['tmpfilename'], ctx['fragment_index'])
|
|
||||||
success = ctx['dl'].download(fragment_filename, {
|
|
||||||
'url': frag_url,
|
|
||||||
'http_headers': headers or info_dict.get('http_headers'),
|
|
||||||
})
|
|
||||||
if not success:
|
|
||||||
return False, None
|
|
||||||
down, frag_sanitized = sanitize_open(fragment_filename, 'rb')
|
|
||||||
ctx['fragment_filename_sanitized'] = frag_sanitized
|
|
||||||
frag_content = down.read()
|
|
||||||
down.close()
|
|
||||||
return True, frag_content
|
|
||||||
|
|
||||||
def _append_fragment(self, ctx, frag_content):
|
|
||||||
try:
|
|
||||||
ctx['dest_stream'].write(frag_content)
|
|
||||||
ctx['dest_stream'].flush()
|
|
||||||
finally:
|
|
||||||
if self.__do_ytdl_file(ctx):
|
|
||||||
self._write_ytdl_file(ctx)
|
|
||||||
if not self.params.get('keep_fragments', False):
|
|
||||||
os.remove(encodeFilename(ctx['fragment_filename_sanitized']))
|
|
||||||
del ctx['fragment_filename_sanitized']
|
|
||||||
|
|
||||||
def _prepare_frag_download(self, ctx):
|
|
||||||
if 'live' not in ctx:
|
|
||||||
ctx['live'] = False
|
|
||||||
if not ctx['live']:
|
|
||||||
total_frags_str = '%d' % ctx['total_frags']
|
|
||||||
ad_frags = ctx.get('ad_frags', 0)
|
|
||||||
if ad_frags:
|
|
||||||
total_frags_str += ' (not including %d ad)' % ad_frags
|
|
||||||
else:
|
|
||||||
total_frags_str = 'unknown (live)'
|
|
||||||
self.to_screen(
|
|
||||||
'[%s] Total fragments: %s' % (self.FD_NAME, total_frags_str))
|
|
||||||
self.report_destination(ctx['filename'])
|
|
||||||
dl = HttpQuietDownloader(
|
|
||||||
self.ydl,
|
|
||||||
{
|
|
||||||
'continuedl': True,
|
|
||||||
'quiet': True,
|
|
||||||
'noprogress': True,
|
|
||||||
'ratelimit': self.params.get('ratelimit'),
|
|
||||||
'retries': self.params.get('retries', 0),
|
|
||||||
'nopart': self.params.get('nopart', False),
|
|
||||||
'test': self.params.get('test', False),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
tmpfilename = self.temp_name(ctx['filename'])
|
|
||||||
open_mode = 'wb'
|
|
||||||
resume_len = 0
|
|
||||||
|
|
||||||
# Establish possible resume length
|
|
||||||
if os.path.isfile(encodeFilename(tmpfilename)):
|
|
||||||
open_mode = 'ab'
|
|
||||||
resume_len = os.path.getsize(encodeFilename(tmpfilename))
|
|
||||||
|
|
||||||
# Should be initialized before ytdl file check
|
|
||||||
ctx.update({
|
|
||||||
'tmpfilename': tmpfilename,
|
|
||||||
'fragment_index': 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
if self.__do_ytdl_file(ctx):
|
|
||||||
if os.path.isfile(encodeFilename(self.ytdl_filename(ctx['filename']))):
|
|
||||||
self._read_ytdl_file(ctx)
|
|
||||||
is_corrupt = ctx.get('ytdl_corrupt') is True
|
|
||||||
is_inconsistent = ctx['fragment_index'] > 0 and resume_len == 0
|
|
||||||
if is_corrupt or is_inconsistent:
|
|
||||||
message = (
|
|
||||||
'.ytdl file is corrupt' if is_corrupt else
|
|
||||||
'Inconsistent state of incomplete fragment download')
|
|
||||||
self.report_warning(
|
|
||||||
'%s. Restarting from the beginning...' % message)
|
|
||||||
ctx['fragment_index'] = resume_len = 0
|
|
||||||
if 'ytdl_corrupt' in ctx:
|
|
||||||
del ctx['ytdl_corrupt']
|
|
||||||
self._write_ytdl_file(ctx)
|
|
||||||
else:
|
|
||||||
self._write_ytdl_file(ctx)
|
|
||||||
assert ctx['fragment_index'] == 0
|
|
||||||
|
|
||||||
dest_stream, tmpfilename = sanitize_open(tmpfilename, open_mode)
|
|
||||||
|
|
||||||
ctx.update({
|
|
||||||
'dl': dl,
|
|
||||||
'dest_stream': dest_stream,
|
|
||||||
'tmpfilename': tmpfilename,
|
|
||||||
# Total complete fragments downloaded so far in bytes
|
|
||||||
'complete_frags_downloaded_bytes': resume_len,
|
|
||||||
})
|
|
||||||
|
|
||||||
def _start_frag_download(self, ctx):
|
|
||||||
total_frags = ctx['total_frags']
|
|
||||||
# This dict stores the download progress, it's updated by the progress
|
|
||||||
# hook
|
|
||||||
state = {
|
|
||||||
'status': 'downloading',
|
|
||||||
'downloaded_bytes': ctx['complete_frags_downloaded_bytes'],
|
|
||||||
'fragment_index': ctx['fragment_index'],
|
|
||||||
'fragment_count': total_frags,
|
|
||||||
'filename': ctx['filename'],
|
|
||||||
'tmpfilename': ctx['tmpfilename'],
|
|
||||||
}
|
|
||||||
|
|
||||||
start = time.time()
|
|
||||||
ctx.update({
|
|
||||||
'started': start,
|
|
||||||
# Amount of fragment's bytes downloaded by the time of the previous
|
|
||||||
# frag progress hook invocation
|
|
||||||
'prev_frag_downloaded_bytes': 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
def frag_progress_hook(s):
|
|
||||||
if s['status'] not in ('downloading', 'finished'):
|
|
||||||
return
|
|
||||||
|
|
||||||
time_now = time.time()
|
|
||||||
state['elapsed'] = time_now - start
|
|
||||||
frag_total_bytes = s.get('total_bytes') or 0
|
|
||||||
if not ctx['live']:
|
|
||||||
estimated_size = (
|
|
||||||
(ctx['complete_frags_downloaded_bytes'] + frag_total_bytes) /
|
|
||||||
(state['fragment_index'] + 1) * total_frags)
|
|
||||||
state['total_bytes_estimate'] = estimated_size
|
|
||||||
|
|
||||||
if s['status'] == 'finished':
|
|
||||||
state['fragment_index'] += 1
|
|
||||||
ctx['fragment_index'] = state['fragment_index']
|
|
||||||
state['downloaded_bytes'] += frag_total_bytes - ctx['prev_frag_downloaded_bytes']
|
|
||||||
ctx['complete_frags_downloaded_bytes'] = state['downloaded_bytes']
|
|
||||||
ctx['prev_frag_downloaded_bytes'] = 0
|
|
||||||
else:
|
|
||||||
frag_downloaded_bytes = s['downloaded_bytes']
|
|
||||||
state['downloaded_bytes'] += frag_downloaded_bytes - ctx['prev_frag_downloaded_bytes']
|
|
||||||
if not ctx['live']:
|
|
||||||
state['eta'] = self.calc_eta(
|
|
||||||
start, time_now, estimated_size,
|
|
||||||
state['downloaded_bytes'])
|
|
||||||
state['speed'] = s.get('speed') or ctx.get('speed')
|
|
||||||
ctx['speed'] = state['speed']
|
|
||||||
ctx['prev_frag_downloaded_bytes'] = frag_downloaded_bytes
|
|
||||||
self._hook_progress(state)
|
|
||||||
|
|
||||||
ctx['dl'].add_progress_hook(frag_progress_hook)
|
|
||||||
|
|
||||||
return start
|
|
||||||
|
|
||||||
def _finish_frag_download(self, ctx):
|
|
||||||
ctx['dest_stream'].close()
|
|
||||||
if self.__do_ytdl_file(ctx):
|
|
||||||
ytdl_filename = encodeFilename(self.ytdl_filename(ctx['filename']))
|
|
||||||
if os.path.isfile(ytdl_filename):
|
|
||||||
os.remove(ytdl_filename)
|
|
||||||
elapsed = time.time() - ctx['started']
|
|
||||||
|
|
||||||
if ctx['tmpfilename'] == '-':
|
|
||||||
downloaded_bytes = ctx['complete_frags_downloaded_bytes']
|
|
||||||
else:
|
|
||||||
self.try_rename(ctx['tmpfilename'], ctx['filename'])
|
|
||||||
downloaded_bytes = os.path.getsize(encodeFilename(ctx['filename']))
|
|
||||||
|
|
||||||
self._hook_progress({
|
|
||||||
'downloaded_bytes': downloaded_bytes,
|
|
||||||
'total_bytes': downloaded_bytes,
|
|
||||||
'filename': ctx['filename'],
|
|
||||||
'status': 'finished',
|
|
||||||
'elapsed': elapsed,
|
|
||||||
})
|
|
@ -1,204 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import re
|
|
||||||
import binascii
|
|
||||||
try:
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
can_decrypt_frag = True
|
|
||||||
except ImportError:
|
|
||||||
can_decrypt_frag = False
|
|
||||||
|
|
||||||
from .fragment import FragmentFD
|
|
||||||
from .external import FFmpegFD
|
|
||||||
|
|
||||||
from ..compat import (
|
|
||||||
compat_urllib_error,
|
|
||||||
compat_urlparse,
|
|
||||||
compat_struct_pack,
|
|
||||||
)
|
|
||||||
from ..utils import (
|
|
||||||
parse_m3u8_attributes,
|
|
||||||
update_url_query,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HlsFD(FragmentFD):
|
|
||||||
""" A limited implementation that does not require ffmpeg """
|
|
||||||
|
|
||||||
FD_NAME = 'hlsnative'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def can_download(manifest, info_dict):
|
|
||||||
UNSUPPORTED_FEATURES = (
|
|
||||||
r'#EXT-X-KEY:METHOD=(?!NONE|AES-128)', # encrypted streams [1]
|
|
||||||
# r'#EXT-X-BYTERANGE', # playlists composed of byte ranges of media files [2]
|
|
||||||
|
|
||||||
# Live streams heuristic does not always work (e.g. geo restricted to Germany
|
|
||||||
# http://hls-geo.daserste.de/i/videoportal/Film/c_620000/622873/format,716451,716457,716450,716458,716459,.mp4.csmil/index_4_av.m3u8?null=0)
|
|
||||||
# r'#EXT-X-MEDIA-SEQUENCE:(?!0$)', # live streams [3]
|
|
||||||
|
|
||||||
# This heuristic also is not correct since segments may not be appended as well.
|
|
||||||
# Twitch vods of finished streams have EXT-X-PLAYLIST-TYPE:EVENT despite
|
|
||||||
# no segments will definitely be appended to the end of the playlist.
|
|
||||||
# r'#EXT-X-PLAYLIST-TYPE:EVENT', # media segments may be appended to the end of
|
|
||||||
# # event media playlists [4]
|
|
||||||
|
|
||||||
# 1. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.4
|
|
||||||
# 2. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.2.2
|
|
||||||
# 3. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.2
|
|
||||||
# 4. https://tools.ietf.org/html/draft-pantos-http-live-streaming-17#section-4.3.3.5
|
|
||||||
)
|
|
||||||
check_results = [not re.search(feature, manifest) for feature in UNSUPPORTED_FEATURES]
|
|
||||||
is_aes128_enc = '#EXT-X-KEY:METHOD=AES-128' in manifest
|
|
||||||
check_results.append(can_decrypt_frag or not is_aes128_enc)
|
|
||||||
check_results.append(not (is_aes128_enc and r'#EXT-X-BYTERANGE' in manifest))
|
|
||||||
check_results.append(not info_dict.get('is_live'))
|
|
||||||
return all(check_results)
|
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
|
||||||
man_url = info_dict['url']
|
|
||||||
self.to_screen('[%s] Downloading m3u8 manifest' % self.FD_NAME)
|
|
||||||
|
|
||||||
urlh = self.ydl.urlopen(self._prepare_url(info_dict, man_url))
|
|
||||||
man_url = urlh.geturl()
|
|
||||||
s = urlh.read().decode('utf-8', 'ignore')
|
|
||||||
|
|
||||||
if not self.can_download(s, info_dict):
|
|
||||||
if info_dict.get('extra_param_to_segment_url'):
|
|
||||||
self.report_error('pycrypto not found. Please install it.')
|
|
||||||
return False
|
|
||||||
self.report_warning(
|
|
||||||
'hlsnative has detected features it does not support, '
|
|
||||||
'extraction will be delegated to ffmpeg')
|
|
||||||
fd = FFmpegFD(self.ydl, self.params)
|
|
||||||
for ph in self._progress_hooks:
|
|
||||||
fd.add_progress_hook(ph)
|
|
||||||
return fd.real_download(filename, info_dict)
|
|
||||||
|
|
||||||
def is_ad_fragment(s):
|
|
||||||
return (s.startswith('#ANVATO-SEGMENT-INFO') and 'type=ad' in s or
|
|
||||||
s.startswith('#UPLYNK-SEGMENT') and s.endswith(',ad'))
|
|
||||||
|
|
||||||
media_frags = 0
|
|
||||||
ad_frags = 0
|
|
||||||
ad_frag_next = False
|
|
||||||
for line in s.splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if line.startswith('#'):
|
|
||||||
if is_ad_fragment(line):
|
|
||||||
ad_frags += 1
|
|
||||||
ad_frag_next = True
|
|
||||||
continue
|
|
||||||
if ad_frag_next:
|
|
||||||
ad_frag_next = False
|
|
||||||
continue
|
|
||||||
media_frags += 1
|
|
||||||
|
|
||||||
ctx = {
|
|
||||||
'filename': filename,
|
|
||||||
'total_frags': media_frags,
|
|
||||||
'ad_frags': ad_frags,
|
|
||||||
}
|
|
||||||
|
|
||||||
self._prepare_and_start_frag_download(ctx)
|
|
||||||
|
|
||||||
fragment_retries = self.params.get('fragment_retries', 0)
|
|
||||||
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
|
|
||||||
test = self.params.get('test', False)
|
|
||||||
|
|
||||||
extra_query = None
|
|
||||||
extra_param_to_segment_url = info_dict.get('extra_param_to_segment_url')
|
|
||||||
if extra_param_to_segment_url:
|
|
||||||
extra_query = compat_urlparse.parse_qs(extra_param_to_segment_url)
|
|
||||||
i = 0
|
|
||||||
media_sequence = 0
|
|
||||||
decrypt_info = {'METHOD': 'NONE'}
|
|
||||||
byte_range = {}
|
|
||||||
frag_index = 0
|
|
||||||
ad_frag_next = False
|
|
||||||
for line in s.splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if line:
|
|
||||||
if not line.startswith('#'):
|
|
||||||
if ad_frag_next:
|
|
||||||
ad_frag_next = False
|
|
||||||
continue
|
|
||||||
frag_index += 1
|
|
||||||
if frag_index <= ctx['fragment_index']:
|
|
||||||
continue
|
|
||||||
frag_url = (
|
|
||||||
line
|
|
||||||
if re.match(r'^https?://', line)
|
|
||||||
else compat_urlparse.urljoin(man_url, line))
|
|
||||||
if extra_query:
|
|
||||||
frag_url = update_url_query(frag_url, extra_query)
|
|
||||||
count = 0
|
|
||||||
headers = info_dict.get('http_headers', {})
|
|
||||||
if byte_range:
|
|
||||||
headers['Range'] = 'bytes=%d-%d' % (byte_range['start'], byte_range['end'])
|
|
||||||
while count <= fragment_retries:
|
|
||||||
try:
|
|
||||||
success, frag_content = self._download_fragment(
|
|
||||||
ctx, frag_url, info_dict, headers)
|
|
||||||
if not success:
|
|
||||||
return False
|
|
||||||
break
|
|
||||||
except compat_urllib_error.HTTPError as err:
|
|
||||||
# Unavailable (possibly temporary) fragments may be served.
|
|
||||||
# First we try to retry then either skip or abort.
|
|
||||||
# See https://github.com/rg3/youtube-dl/issues/10165,
|
|
||||||
# https://github.com/rg3/youtube-dl/issues/10448).
|
|
||||||
count += 1
|
|
||||||
if count <= fragment_retries:
|
|
||||||
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
|
||||||
if count > fragment_retries:
|
|
||||||
if skip_unavailable_fragments:
|
|
||||||
i += 1
|
|
||||||
media_sequence += 1
|
|
||||||
self.report_skip_fragment(frag_index)
|
|
||||||
continue
|
|
||||||
self.report_error(
|
|
||||||
'giving up after %s fragment retries' % fragment_retries)
|
|
||||||
return False
|
|
||||||
if decrypt_info['METHOD'] == 'AES-128':
|
|
||||||
iv = decrypt_info.get('IV') or compat_struct_pack('>8xq', media_sequence)
|
|
||||||
decrypt_info['KEY'] = decrypt_info.get('KEY') or self.ydl.urlopen(
|
|
||||||
self._prepare_url(info_dict, decrypt_info['URI'])).read()
|
|
||||||
frag_content = AES.new(
|
|
||||||
decrypt_info['KEY'], AES.MODE_CBC, iv).decrypt(frag_content)
|
|
||||||
self._append_fragment(ctx, frag_content)
|
|
||||||
# We only download the first fragment during the test
|
|
||||||
if test:
|
|
||||||
break
|
|
||||||
i += 1
|
|
||||||
media_sequence += 1
|
|
||||||
elif line.startswith('#EXT-X-KEY'):
|
|
||||||
decrypt_url = decrypt_info.get('URI')
|
|
||||||
decrypt_info = parse_m3u8_attributes(line[11:])
|
|
||||||
if decrypt_info['METHOD'] == 'AES-128':
|
|
||||||
if 'IV' in decrypt_info:
|
|
||||||
decrypt_info['IV'] = binascii.unhexlify(decrypt_info['IV'][2:].zfill(32))
|
|
||||||
if not re.match(r'^https?://', decrypt_info['URI']):
|
|
||||||
decrypt_info['URI'] = compat_urlparse.urljoin(
|
|
||||||
man_url, decrypt_info['URI'])
|
|
||||||
if extra_query:
|
|
||||||
decrypt_info['URI'] = update_url_query(decrypt_info['URI'], extra_query)
|
|
||||||
if decrypt_url != decrypt_info['URI']:
|
|
||||||
decrypt_info['KEY'] = None
|
|
||||||
elif line.startswith('#EXT-X-MEDIA-SEQUENCE'):
|
|
||||||
media_sequence = int(line[22:])
|
|
||||||
elif line.startswith('#EXT-X-BYTERANGE'):
|
|
||||||
splitted_byte_range = line[17:].split('@')
|
|
||||||
sub_range_start = int(splitted_byte_range[1]) if len(splitted_byte_range) == 2 else byte_range['end']
|
|
||||||
byte_range = {
|
|
||||||
'start': sub_range_start,
|
|
||||||
'end': sub_range_start + int(splitted_byte_range[0]),
|
|
||||||
}
|
|
||||||
elif is_ad_fragment(line):
|
|
||||||
ad_frag_next = True
|
|
||||||
|
|
||||||
self._finish_frag_download(ctx)
|
|
||||||
|
|
||||||
return True
|
|
@ -1,354 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import os
|
|
||||||
import socket
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
|
||||||
from ..compat import (
|
|
||||||
compat_str,
|
|
||||||
compat_urllib_error,
|
|
||||||
)
|
|
||||||
from ..utils import (
|
|
||||||
ContentTooShortError,
|
|
||||||
encodeFilename,
|
|
||||||
int_or_none,
|
|
||||||
sanitize_open,
|
|
||||||
sanitized_Request,
|
|
||||||
write_xattr,
|
|
||||||
XAttrMetadataError,
|
|
||||||
XAttrUnavailableError,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HttpFD(FileDownloader):
|
|
||||||
def real_download(self, filename, info_dict):
|
|
||||||
url = info_dict['url']
|
|
||||||
|
|
||||||
class DownloadContext(dict):
|
|
||||||
__getattr__ = dict.get
|
|
||||||
__setattr__ = dict.__setitem__
|
|
||||||
__delattr__ = dict.__delitem__
|
|
||||||
|
|
||||||
ctx = DownloadContext()
|
|
||||||
ctx.filename = filename
|
|
||||||
ctx.tmpfilename = self.temp_name(filename)
|
|
||||||
ctx.stream = None
|
|
||||||
|
|
||||||
# Do not include the Accept-Encoding header
|
|
||||||
headers = {'Youtubedl-no-compression': 'True'}
|
|
||||||
add_headers = info_dict.get('http_headers')
|
|
||||||
if add_headers:
|
|
||||||
headers.update(add_headers)
|
|
||||||
|
|
||||||
is_test = self.params.get('test', False)
|
|
||||||
chunk_size = self._TEST_FILE_SIZE if is_test else (
|
|
||||||
info_dict.get('downloader_options', {}).get('http_chunk_size') or
|
|
||||||
self.params.get('http_chunk_size') or 0)
|
|
||||||
|
|
||||||
ctx.open_mode = 'wb'
|
|
||||||
ctx.resume_len = 0
|
|
||||||
ctx.data_len = None
|
|
||||||
ctx.block_size = self.params.get('buffersize', 1024)
|
|
||||||
ctx.start_time = time.time()
|
|
||||||
ctx.chunk_size = None
|
|
||||||
|
|
||||||
if self.params.get('continuedl', True):
|
|
||||||
# Establish possible resume length
|
|
||||||
if os.path.isfile(encodeFilename(ctx.tmpfilename)):
|
|
||||||
ctx.resume_len = os.path.getsize(
|
|
||||||
encodeFilename(ctx.tmpfilename))
|
|
||||||
|
|
||||||
ctx.is_resume = ctx.resume_len > 0
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
retries = self.params.get('retries', 0)
|
|
||||||
|
|
||||||
class SucceedDownload(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class RetryDownload(Exception):
|
|
||||||
def __init__(self, source_error):
|
|
||||||
self.source_error = source_error
|
|
||||||
|
|
||||||
class NextFragment(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_range(req, start, end):
|
|
||||||
range_header = 'bytes=%d-' % start
|
|
||||||
if end:
|
|
||||||
range_header += compat_str(end)
|
|
||||||
req.add_header('Range', range_header)
|
|
||||||
|
|
||||||
def establish_connection():
|
|
||||||
ctx.chunk_size = (random.randint(int(chunk_size * 0.95), chunk_size)
|
|
||||||
if not is_test and chunk_size else chunk_size)
|
|
||||||
if ctx.resume_len > 0:
|
|
||||||
range_start = ctx.resume_len
|
|
||||||
if ctx.is_resume:
|
|
||||||
self.report_resuming_byte(ctx.resume_len)
|
|
||||||
ctx.open_mode = 'ab'
|
|
||||||
elif ctx.chunk_size > 0:
|
|
||||||
range_start = 0
|
|
||||||
else:
|
|
||||||
range_start = None
|
|
||||||
ctx.is_resume = False
|
|
||||||
range_end = range_start + ctx.chunk_size - 1 if ctx.chunk_size else None
|
|
||||||
if range_end and ctx.data_len is not None and range_end >= ctx.data_len:
|
|
||||||
range_end = ctx.data_len - 1
|
|
||||||
has_range = range_start is not None
|
|
||||||
ctx.has_range = has_range
|
|
||||||
request = sanitized_Request(url, None, headers)
|
|
||||||
if has_range:
|
|
||||||
set_range(request, range_start, range_end)
|
|
||||||
# Establish connection
|
|
||||||
try:
|
|
||||||
ctx.data = self.ydl.urlopen(request)
|
|
||||||
# When trying to resume, Content-Range HTTP header of response has to be checked
|
|
||||||
# to match the value of requested Range HTTP header. This is due to a webservers
|
|
||||||
# that don't support resuming and serve a whole file with no Content-Range
|
|
||||||
# set in response despite of requested Range (see
|
|
||||||
# https://github.com/rg3/youtube-dl/issues/6057#issuecomment-126129799)
|
|
||||||
if has_range:
|
|
||||||
content_range = ctx.data.headers.get('Content-Range')
|
|
||||||
if content_range:
|
|
||||||
content_range_m = re.search(r'bytes (\d+)-(\d+)?(?:/(\d+))?', content_range)
|
|
||||||
# Content-Range is present and matches requested Range, resume is possible
|
|
||||||
if content_range_m:
|
|
||||||
if range_start == int(content_range_m.group(1)):
|
|
||||||
content_range_end = int_or_none(content_range_m.group(2))
|
|
||||||
content_len = int_or_none(content_range_m.group(3))
|
|
||||||
accept_content_len = (
|
|
||||||
# Non-chunked download
|
|
||||||
not ctx.chunk_size or
|
|
||||||
# Chunked download and requested piece or
|
|
||||||
# its part is promised to be served
|
|
||||||
content_range_end == range_end or
|
|
||||||
content_len < range_end)
|
|
||||||
if accept_content_len:
|
|
||||||
ctx.data_len = content_len
|
|
||||||
return
|
|
||||||
# Content-Range is either not present or invalid. Assuming remote webserver is
|
|
||||||
# trying to send the whole file, resume is not possible, so wiping the local file
|
|
||||||
# and performing entire redownload
|
|
||||||
self.report_unable_to_resume()
|
|
||||||
ctx.resume_len = 0
|
|
||||||
ctx.open_mode = 'wb'
|
|
||||||
ctx.data_len = int_or_none(ctx.data.info().get('Content-length', None))
|
|
||||||
return
|
|
||||||
except (compat_urllib_error.HTTPError, ) as err:
|
|
||||||
if err.code == 416:
|
|
||||||
# Unable to resume (requested range not satisfiable)
|
|
||||||
try:
|
|
||||||
# Open the connection again without the range header
|
|
||||||
ctx.data = self.ydl.urlopen(
|
|
||||||
sanitized_Request(url, None, headers))
|
|
||||||
content_length = ctx.data.info()['Content-Length']
|
|
||||||
except (compat_urllib_error.HTTPError, ) as err:
|
|
||||||
if err.code < 500 or err.code >= 600:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
# Examine the reported length
|
|
||||||
if (content_length is not None and
|
|
||||||
(ctx.resume_len - 100 < int(content_length) < ctx.resume_len + 100)):
|
|
||||||
# The file had already been fully downloaded.
|
|
||||||
# Explanation to the above condition: in issue #175 it was revealed that
|
|
||||||
# YouTube sometimes adds or removes a few bytes from the end of the file,
|
|
||||||
# changing the file size slightly and causing problems for some users. So
|
|
||||||
# I decided to implement a suggested change and consider the file
|
|
||||||
# completely downloaded if the file size differs less than 100 bytes from
|
|
||||||
# the one in the hard drive.
|
|
||||||
self.report_file_already_downloaded(ctx.filename)
|
|
||||||
self.try_rename(ctx.tmpfilename, ctx.filename)
|
|
||||||
self._hook_progress({
|
|
||||||
'filename': ctx.filename,
|
|
||||||
'status': 'finished',
|
|
||||||
'downloaded_bytes': ctx.resume_len,
|
|
||||||
'total_bytes': ctx.resume_len,
|
|
||||||
})
|
|
||||||
raise SucceedDownload()
|
|
||||||
else:
|
|
||||||
# The length does not match, we start the download over
|
|
||||||
self.report_unable_to_resume()
|
|
||||||
ctx.resume_len = 0
|
|
||||||
ctx.open_mode = 'wb'
|
|
||||||
return
|
|
||||||
elif err.code < 500 or err.code >= 600:
|
|
||||||
# Unexpected HTTP error
|
|
||||||
raise
|
|
||||||
raise RetryDownload(err)
|
|
||||||
except socket.error as err:
|
|
||||||
if err.errno != errno.ECONNRESET:
|
|
||||||
# Connection reset is no problem, just retry
|
|
||||||
raise
|
|
||||||
raise RetryDownload(err)
|
|
||||||
|
|
||||||
def download():
|
|
||||||
data_len = ctx.data.info().get('Content-length', None)
|
|
||||||
|
|
||||||
# Range HTTP header may be ignored/unsupported by a webserver
|
|
||||||
# (e.g. extractor/scivee.py, extractor/bambuser.py).
|
|
||||||
# However, for a test we still would like to download just a piece of a file.
|
|
||||||
# To achieve this we limit data_len to _TEST_FILE_SIZE and manually control
|
|
||||||
# block size when downloading a file.
|
|
||||||
if is_test and (data_len is None or int(data_len) > self._TEST_FILE_SIZE):
|
|
||||||
data_len = self._TEST_FILE_SIZE
|
|
||||||
|
|
||||||
if data_len is not None:
|
|
||||||
data_len = int(data_len) + ctx.resume_len
|
|
||||||
min_data_len = self.params.get('min_filesize')
|
|
||||||
max_data_len = self.params.get('max_filesize')
|
|
||||||
if min_data_len is not None and data_len < min_data_len:
|
|
||||||
self.to_screen('\r[download] File is smaller than min-filesize (%s bytes < %s bytes). Aborting.' % (data_len, min_data_len))
|
|
||||||
return False
|
|
||||||
if max_data_len is not None and data_len > max_data_len:
|
|
||||||
self.to_screen('\r[download] File is larger than max-filesize (%s bytes > %s bytes). Aborting.' % (data_len, max_data_len))
|
|
||||||
return False
|
|
||||||
|
|
||||||
byte_counter = 0 + ctx.resume_len
|
|
||||||
block_size = ctx.block_size
|
|
||||||
start = time.time()
|
|
||||||
|
|
||||||
# measure time over whole while-loop, so slow_down() and best_block_size() work together properly
|
|
||||||
now = None # needed for slow_down() in the first loop run
|
|
||||||
before = start # start measuring
|
|
||||||
|
|
||||||
def retry(e):
|
|
||||||
to_stdout = ctx.tmpfilename == '-'
|
|
||||||
if not to_stdout:
|
|
||||||
ctx.stream.close()
|
|
||||||
ctx.stream = None
|
|
||||||
ctx.resume_len = byte_counter if to_stdout else os.path.getsize(encodeFilename(ctx.tmpfilename))
|
|
||||||
raise RetryDownload(e)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
# Download and write
|
|
||||||
data_block = ctx.data.read(block_size if not is_test else min(block_size, data_len - byte_counter))
|
|
||||||
# socket.timeout is a subclass of socket.error but may not have
|
|
||||||
# errno set
|
|
||||||
except socket.timeout as e:
|
|
||||||
retry(e)
|
|
||||||
except socket.error as e:
|
|
||||||
if e.errno not in (errno.ECONNRESET, errno.ETIMEDOUT):
|
|
||||||
raise
|
|
||||||
retry(e)
|
|
||||||
|
|
||||||
byte_counter += len(data_block)
|
|
||||||
|
|
||||||
# exit loop when download is finished
|
|
||||||
if len(data_block) == 0:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Open destination file just in time
|
|
||||||
if ctx.stream is None:
|
|
||||||
try:
|
|
||||||
ctx.stream, ctx.tmpfilename = sanitize_open(
|
|
||||||
ctx.tmpfilename, ctx.open_mode)
|
|
||||||
assert ctx.stream is not None
|
|
||||||
ctx.filename = self.undo_temp_name(ctx.tmpfilename)
|
|
||||||
self.report_destination(ctx.filename)
|
|
||||||
except (OSError, IOError) as err:
|
|
||||||
self.report_error('unable to open for writing: %s' % str(err))
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.params.get('xattr_set_filesize', False) and data_len is not None:
|
|
||||||
try:
|
|
||||||
write_xattr(ctx.tmpfilename, 'user.ytdl.filesize', str(data_len).encode('utf-8'))
|
|
||||||
except (XAttrUnavailableError, XAttrMetadataError) as err:
|
|
||||||
self.report_error('unable to set filesize xattr: %s' % str(err))
|
|
||||||
|
|
||||||
try:
|
|
||||||
ctx.stream.write(data_block)
|
|
||||||
except (IOError, OSError) as err:
|
|
||||||
self.to_stderr('\n')
|
|
||||||
self.report_error('unable to write data: %s' % str(err))
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Apply rate limit
|
|
||||||
self.slow_down(start, now, byte_counter - ctx.resume_len)
|
|
||||||
|
|
||||||
# end measuring of one loop run
|
|
||||||
now = time.time()
|
|
||||||
after = now
|
|
||||||
|
|
||||||
# Adjust block size
|
|
||||||
if not self.params.get('noresizebuffer', False):
|
|
||||||
block_size = self.best_block_size(after - before, len(data_block))
|
|
||||||
|
|
||||||
before = after
|
|
||||||
|
|
||||||
# Progress message
|
|
||||||
speed = self.calc_speed(start, now, byte_counter - ctx.resume_len)
|
|
||||||
if ctx.data_len is None:
|
|
||||||
eta = None
|
|
||||||
else:
|
|
||||||
eta = self.calc_eta(start, time.time(), ctx.data_len - ctx.resume_len, byte_counter - ctx.resume_len)
|
|
||||||
|
|
||||||
self._hook_progress({
|
|
||||||
'status': 'downloading',
|
|
||||||
'downloaded_bytes': byte_counter,
|
|
||||||
'total_bytes': ctx.data_len,
|
|
||||||
'tmpfilename': ctx.tmpfilename,
|
|
||||||
'filename': ctx.filename,
|
|
||||||
'eta': eta,
|
|
||||||
'speed': speed,
|
|
||||||
'elapsed': now - ctx.start_time,
|
|
||||||
})
|
|
||||||
|
|
||||||
if is_test and byte_counter == data_len:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not is_test and ctx.chunk_size and ctx.data_len is not None and byte_counter < ctx.data_len:
|
|
||||||
ctx.resume_len = byte_counter
|
|
||||||
# ctx.block_size = block_size
|
|
||||||
raise NextFragment()
|
|
||||||
|
|
||||||
if ctx.stream is None:
|
|
||||||
self.to_stderr('\n')
|
|
||||||
self.report_error('Did not get any data blocks')
|
|
||||||
return False
|
|
||||||
if ctx.tmpfilename != '-':
|
|
||||||
ctx.stream.close()
|
|
||||||
|
|
||||||
if data_len is not None and byte_counter != data_len:
|
|
||||||
err = ContentTooShortError(byte_counter, int(data_len))
|
|
||||||
if count <= retries:
|
|
||||||
retry(err)
|
|
||||||
raise err
|
|
||||||
|
|
||||||
self.try_rename(ctx.tmpfilename, ctx.filename)
|
|
||||||
|
|
||||||
# Update file modification time
|
|
||||||
if self.params.get('updatetime', True):
|
|
||||||
info_dict['filetime'] = self.try_utime(ctx.filename, ctx.data.info().get('last-modified', None))
|
|
||||||
|
|
||||||
self._hook_progress({
|
|
||||||
'downloaded_bytes': byte_counter,
|
|
||||||
'total_bytes': byte_counter,
|
|
||||||
'filename': ctx.filename,
|
|
||||||
'status': 'finished',
|
|
||||||
'elapsed': time.time() - ctx.start_time,
|
|
||||||
})
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
while count <= retries:
|
|
||||||
try:
|
|
||||||
establish_connection()
|
|
||||||
return download()
|
|
||||||
except RetryDownload as e:
|
|
||||||
count += 1
|
|
||||||
if count <= retries:
|
|
||||||
self.report_retry(e.source_error, count, retries)
|
|
||||||
continue
|
|
||||||
except NextFragment:
|
|
||||||
continue
|
|
||||||
except SucceedDownload:
|
|
||||||
return True
|
|
||||||
|
|
||||||
self.report_error('giving up after %s retries' % retries)
|
|
||||||
return False
|
|
@ -1,259 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import time
|
|
||||||
import binascii
|
|
||||||
import io
|
|
||||||
|
|
||||||
from .fragment import FragmentFD
|
|
||||||
from ..compat import (
|
|
||||||
compat_Struct,
|
|
||||||
compat_urllib_error,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
u8 = compat_Struct('>B')
|
|
||||||
u88 = compat_Struct('>Bx')
|
|
||||||
u16 = compat_Struct('>H')
|
|
||||||
u1616 = compat_Struct('>Hxx')
|
|
||||||
u32 = compat_Struct('>I')
|
|
||||||
u64 = compat_Struct('>Q')
|
|
||||||
|
|
||||||
s88 = compat_Struct('>bx')
|
|
||||||
s16 = compat_Struct('>h')
|
|
||||||
s1616 = compat_Struct('>hxx')
|
|
||||||
s32 = compat_Struct('>i')
|
|
||||||
|
|
||||||
unity_matrix = (s32.pack(0x10000) + s32.pack(0) * 3) * 2 + s32.pack(0x40000000)
|
|
||||||
|
|
||||||
TRACK_ENABLED = 0x1
|
|
||||||
TRACK_IN_MOVIE = 0x2
|
|
||||||
TRACK_IN_PREVIEW = 0x4
|
|
||||||
|
|
||||||
SELF_CONTAINED = 0x1
|
|
||||||
|
|
||||||
|
|
||||||
def box(box_type, payload):
|
|
||||||
return u32.pack(8 + len(payload)) + box_type + payload
|
|
||||||
|
|
||||||
|
|
||||||
def full_box(box_type, version, flags, payload):
|
|
||||||
return box(box_type, u8.pack(version) + u32.pack(flags)[1:] + payload)
|
|
||||||
|
|
||||||
|
|
||||||
def write_piff_header(stream, params):
|
|
||||||
track_id = params['track_id']
|
|
||||||
fourcc = params['fourcc']
|
|
||||||
duration = params['duration']
|
|
||||||
timescale = params.get('timescale', 10000000)
|
|
||||||
language = params.get('language', 'und')
|
|
||||||
height = params.get('height', 0)
|
|
||||||
width = params.get('width', 0)
|
|
||||||
is_audio = width == 0 and height == 0
|
|
||||||
creation_time = modification_time = int(time.time())
|
|
||||||
|
|
||||||
ftyp_payload = b'isml' # major brand
|
|
||||||
ftyp_payload += u32.pack(1) # minor version
|
|
||||||
ftyp_payload += b'piff' + b'iso2' # compatible brands
|
|
||||||
stream.write(box(b'ftyp', ftyp_payload)) # File Type Box
|
|
||||||
|
|
||||||
mvhd_payload = u64.pack(creation_time)
|
|
||||||
mvhd_payload += u64.pack(modification_time)
|
|
||||||
mvhd_payload += u32.pack(timescale)
|
|
||||||
mvhd_payload += u64.pack(duration)
|
|
||||||
mvhd_payload += s1616.pack(1) # rate
|
|
||||||
mvhd_payload += s88.pack(1) # volume
|
|
||||||
mvhd_payload += u16.pack(0) # reserved
|
|
||||||
mvhd_payload += u32.pack(0) * 2 # reserved
|
|
||||||
mvhd_payload += unity_matrix
|
|
||||||
mvhd_payload += u32.pack(0) * 6 # pre defined
|
|
||||||
mvhd_payload += u32.pack(0xffffffff) # next track id
|
|
||||||
moov_payload = full_box(b'mvhd', 1, 0, mvhd_payload) # Movie Header Box
|
|
||||||
|
|
||||||
tkhd_payload = u64.pack(creation_time)
|
|
||||||
tkhd_payload += u64.pack(modification_time)
|
|
||||||
tkhd_payload += u32.pack(track_id) # track id
|
|
||||||
tkhd_payload += u32.pack(0) # reserved
|
|
||||||
tkhd_payload += u64.pack(duration)
|
|
||||||
tkhd_payload += u32.pack(0) * 2 # reserved
|
|
||||||
tkhd_payload += s16.pack(0) # layer
|
|
||||||
tkhd_payload += s16.pack(0) # alternate group
|
|
||||||
tkhd_payload += s88.pack(1 if is_audio else 0) # volume
|
|
||||||
tkhd_payload += u16.pack(0) # reserved
|
|
||||||
tkhd_payload += unity_matrix
|
|
||||||
tkhd_payload += u1616.pack(width)
|
|
||||||
tkhd_payload += u1616.pack(height)
|
|
||||||
trak_payload = full_box(b'tkhd', 1, TRACK_ENABLED | TRACK_IN_MOVIE | TRACK_IN_PREVIEW, tkhd_payload) # Track Header Box
|
|
||||||
|
|
||||||
mdhd_payload = u64.pack(creation_time)
|
|
||||||
mdhd_payload += u64.pack(modification_time)
|
|
||||||
mdhd_payload += u32.pack(timescale)
|
|
||||||
mdhd_payload += u64.pack(duration)
|
|
||||||
mdhd_payload += u16.pack(((ord(language[0]) - 0x60) << 10) | ((ord(language[1]) - 0x60) << 5) | (ord(language[2]) - 0x60))
|
|
||||||
mdhd_payload += u16.pack(0) # pre defined
|
|
||||||
mdia_payload = full_box(b'mdhd', 1, 0, mdhd_payload) # Media Header Box
|
|
||||||
|
|
||||||
hdlr_payload = u32.pack(0) # pre defined
|
|
||||||
hdlr_payload += b'soun' if is_audio else b'vide' # handler type
|
|
||||||
hdlr_payload += u32.pack(0) * 3 # reserved
|
|
||||||
hdlr_payload += (b'Sound' if is_audio else b'Video') + b'Handler\0' # name
|
|
||||||
mdia_payload += full_box(b'hdlr', 0, 0, hdlr_payload) # Handler Reference Box
|
|
||||||
|
|
||||||
if is_audio:
|
|
||||||
smhd_payload = s88.pack(0) # balance
|
|
||||||
smhd_payload += u16.pack(0) # reserved
|
|
||||||
media_header_box = full_box(b'smhd', 0, 0, smhd_payload) # Sound Media Header
|
|
||||||
else:
|
|
||||||
vmhd_payload = u16.pack(0) # graphics mode
|
|
||||||
vmhd_payload += u16.pack(0) * 3 # opcolor
|
|
||||||
media_header_box = full_box(b'vmhd', 0, 1, vmhd_payload) # Video Media Header
|
|
||||||
minf_payload = media_header_box
|
|
||||||
|
|
||||||
dref_payload = u32.pack(1) # entry count
|
|
||||||
dref_payload += full_box(b'url ', 0, SELF_CONTAINED, b'') # Data Entry URL Box
|
|
||||||
dinf_payload = full_box(b'dref', 0, 0, dref_payload) # Data Reference Box
|
|
||||||
minf_payload += box(b'dinf', dinf_payload) # Data Information Box
|
|
||||||
|
|
||||||
stsd_payload = u32.pack(1) # entry count
|
|
||||||
|
|
||||||
sample_entry_payload = u8.pack(0) * 6 # reserved
|
|
||||||
sample_entry_payload += u16.pack(1) # data reference index
|
|
||||||
if is_audio:
|
|
||||||
sample_entry_payload += u32.pack(0) * 2 # reserved
|
|
||||||
sample_entry_payload += u16.pack(params.get('channels', 2))
|
|
||||||
sample_entry_payload += u16.pack(params.get('bits_per_sample', 16))
|
|
||||||
sample_entry_payload += u16.pack(0) # pre defined
|
|
||||||
sample_entry_payload += u16.pack(0) # reserved
|
|
||||||
sample_entry_payload += u1616.pack(params['sampling_rate'])
|
|
||||||
|
|
||||||
if fourcc == 'AACL':
|
|
||||||
sample_entry_box = box(b'mp4a', sample_entry_payload)
|
|
||||||
else:
|
|
||||||
sample_entry_payload += u16.pack(0) # pre defined
|
|
||||||
sample_entry_payload += u16.pack(0) # reserved
|
|
||||||
sample_entry_payload += u32.pack(0) * 3 # pre defined
|
|
||||||
sample_entry_payload += u16.pack(width)
|
|
||||||
sample_entry_payload += u16.pack(height)
|
|
||||||
sample_entry_payload += u1616.pack(0x48) # horiz resolution 72 dpi
|
|
||||||
sample_entry_payload += u1616.pack(0x48) # vert resolution 72 dpi
|
|
||||||
sample_entry_payload += u32.pack(0) # reserved
|
|
||||||
sample_entry_payload += u16.pack(1) # frame count
|
|
||||||
sample_entry_payload += u8.pack(0) * 32 # compressor name
|
|
||||||
sample_entry_payload += u16.pack(0x18) # depth
|
|
||||||
sample_entry_payload += s16.pack(-1) # pre defined
|
|
||||||
|
|
||||||
codec_private_data = binascii.unhexlify(params['codec_private_data'].encode('utf-8'))
|
|
||||||
if fourcc in ('H264', 'AVC1'):
|
|
||||||
sps, pps = codec_private_data.split(u32.pack(1))[1:]
|
|
||||||
avcc_payload = u8.pack(1) # configuration version
|
|
||||||
avcc_payload += sps[1:4] # avc profile indication + profile compatibility + avc level indication
|
|
||||||
avcc_payload += u8.pack(0xfc | (params.get('nal_unit_length_field', 4) - 1)) # complete represenation (1) + reserved (11111) + length size minus one
|
|
||||||
avcc_payload += u8.pack(1) # reserved (0) + number of sps (0000001)
|
|
||||||
avcc_payload += u16.pack(len(sps))
|
|
||||||
avcc_payload += sps
|
|
||||||
avcc_payload += u8.pack(1) # number of pps
|
|
||||||
avcc_payload += u16.pack(len(pps))
|
|
||||||
avcc_payload += pps
|
|
||||||
sample_entry_payload += box(b'avcC', avcc_payload) # AVC Decoder Configuration Record
|
|
||||||
sample_entry_box = box(b'avc1', sample_entry_payload) # AVC Simple Entry
|
|
||||||
stsd_payload += sample_entry_box
|
|
||||||
|
|
||||||
stbl_payload = full_box(b'stsd', 0, 0, stsd_payload) # Sample Description Box
|
|
||||||
|
|
||||||
stts_payload = u32.pack(0) # entry count
|
|
||||||
stbl_payload += full_box(b'stts', 0, 0, stts_payload) # Decoding Time to Sample Box
|
|
||||||
|
|
||||||
stsc_payload = u32.pack(0) # entry count
|
|
||||||
stbl_payload += full_box(b'stsc', 0, 0, stsc_payload) # Sample To Chunk Box
|
|
||||||
|
|
||||||
stco_payload = u32.pack(0) # entry count
|
|
||||||
stbl_payload += full_box(b'stco', 0, 0, stco_payload) # Chunk Offset Box
|
|
||||||
|
|
||||||
minf_payload += box(b'stbl', stbl_payload) # Sample Table Box
|
|
||||||
|
|
||||||
mdia_payload += box(b'minf', minf_payload) # Media Information Box
|
|
||||||
|
|
||||||
trak_payload += box(b'mdia', mdia_payload) # Media Box
|
|
||||||
|
|
||||||
moov_payload += box(b'trak', trak_payload) # Track Box
|
|
||||||
|
|
||||||
mehd_payload = u64.pack(duration)
|
|
||||||
mvex_payload = full_box(b'mehd', 1, 0, mehd_payload) # Movie Extends Header Box
|
|
||||||
|
|
||||||
trex_payload = u32.pack(track_id) # track id
|
|
||||||
trex_payload += u32.pack(1) # default sample description index
|
|
||||||
trex_payload += u32.pack(0) # default sample duration
|
|
||||||
trex_payload += u32.pack(0) # default sample size
|
|
||||||
trex_payload += u32.pack(0) # default sample flags
|
|
||||||
mvex_payload += full_box(b'trex', 0, 0, trex_payload) # Track Extends Box
|
|
||||||
|
|
||||||
moov_payload += box(b'mvex', mvex_payload) # Movie Extends Box
|
|
||||||
stream.write(box(b'moov', moov_payload)) # Movie Box
|
|
||||||
|
|
||||||
|
|
||||||
def extract_box_data(data, box_sequence):
|
|
||||||
data_reader = io.BytesIO(data)
|
|
||||||
while True:
|
|
||||||
box_size = u32.unpack(data_reader.read(4))[0]
|
|
||||||
box_type = data_reader.read(4)
|
|
||||||
if box_type == box_sequence[0]:
|
|
||||||
box_data = data_reader.read(box_size - 8)
|
|
||||||
if len(box_sequence) == 1:
|
|
||||||
return box_data
|
|
||||||
return extract_box_data(box_data, box_sequence[1:])
|
|
||||||
data_reader.seek(box_size - 8, 1)
|
|
||||||
|
|
||||||
|
|
||||||
class IsmFD(FragmentFD):
|
|
||||||
"""
|
|
||||||
Download segments in a ISM manifest
|
|
||||||
"""
|
|
||||||
|
|
||||||
FD_NAME = 'ism'
|
|
||||||
|
|
||||||
def real_download(self, filename, info_dict):
|
|
||||||
segments = info_dict['fragments'][:1] if self.params.get(
|
|
||||||
'test', False) else info_dict['fragments']
|
|
||||||
|
|
||||||
ctx = {
|
|
||||||
'filename': filename,
|
|
||||||
'total_frags': len(segments),
|
|
||||||
}
|
|
||||||
|
|
||||||
self._prepare_and_start_frag_download(ctx)
|
|
||||||
|
|
||||||
fragment_retries = self.params.get('fragment_retries', 0)
|
|
||||||
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
|
|
||||||
|
|
||||||
track_written = False
|
|
||||||
frag_index = 0
|
|
||||||
for i, segment in enumerate(segments):
|
|
||||||
frag_index += 1
|
|
||||||
if frag_index <= ctx['fragment_index']:
|
|
||||||
continue
|
|
||||||
count = 0
|
|
||||||
while count <= fragment_retries:
|
|
||||||
try:
|
|
||||||
success, frag_content = self._download_fragment(ctx, segment['url'], info_dict)
|
|
||||||
if not success:
|
|
||||||
return False
|
|
||||||
if not track_written:
|
|
||||||
tfhd_data = extract_box_data(frag_content, [b'moof', b'traf', b'tfhd'])
|
|
||||||
info_dict['_download_params']['track_id'] = u32.unpack(tfhd_data[4:8])[0]
|
|
||||||
write_piff_header(ctx['dest_stream'], info_dict['_download_params'])
|
|
||||||
track_written = True
|
|
||||||
self._append_fragment(ctx, frag_content)
|
|
||||||
break
|
|
||||||
except compat_urllib_error.HTTPError as err:
|
|
||||||
count += 1
|
|
||||||
if count <= fragment_retries:
|
|
||||||
self.report_retry_fragment(err, frag_index, count, fragment_retries)
|
|
||||||
if count > fragment_retries:
|
|
||||||
if skip_unavailable_fragments:
|
|
||||||
self.report_skip_fragment(frag_index)
|
|
||||||
continue
|
|
||||||
self.report_error('giving up after %s fragment retries' % fragment_retries)
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._finish_frag_download(ctx)
|
|
||||||
|
|
||||||
return True
|
|
@ -1,214 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
|
||||||
from ..compat import compat_str
|
|
||||||
from ..utils import (
|
|
||||||
check_executable,
|
|
||||||
encodeFilename,
|
|
||||||
encodeArgument,
|
|
||||||
get_exe_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def rtmpdump_version():
|
|
||||||
return get_exe_version(
|
|
||||||
'rtmpdump', ['--help'], r'(?i)RTMPDump\s*v?([0-9a-zA-Z._-]+)')
|
|
||||||
|
|
||||||
|
|
||||||
class RtmpFD(FileDownloader):
|
|
||||||
def real_download(self, filename, info_dict):
|
|
||||||
def run_rtmpdump(args):
|
|
||||||
start = time.time()
|
|
||||||
resume_percent = None
|
|
||||||
resume_downloaded_data_len = None
|
|
||||||
proc = subprocess.Popen(args, stderr=subprocess.PIPE)
|
|
||||||
cursor_in_new_line = True
|
|
||||||
proc_stderr_closed = False
|
|
||||||
try:
|
|
||||||
while not proc_stderr_closed:
|
|
||||||
# read line from stderr
|
|
||||||
line = ''
|
|
||||||
while True:
|
|
||||||
char = proc.stderr.read(1)
|
|
||||||
if not char:
|
|
||||||
proc_stderr_closed = True
|
|
||||||
break
|
|
||||||
if char in [b'\r', b'\n']:
|
|
||||||
break
|
|
||||||
line += char.decode('ascii', 'replace')
|
|
||||||
if not line:
|
|
||||||
# proc_stderr_closed is True
|
|
||||||
continue
|
|
||||||
mobj = re.search(r'([0-9]+\.[0-9]{3}) kB / [0-9]+\.[0-9]{2} sec \(([0-9]{1,2}\.[0-9])%\)', line)
|
|
||||||
if mobj:
|
|
||||||
downloaded_data_len = int(float(mobj.group(1)) * 1024)
|
|
||||||
percent = float(mobj.group(2))
|
|
||||||
if not resume_percent:
|
|
||||||
resume_percent = percent
|
|
||||||
resume_downloaded_data_len = downloaded_data_len
|
|
||||||
time_now = time.time()
|
|
||||||
eta = self.calc_eta(start, time_now, 100 - resume_percent, percent - resume_percent)
|
|
||||||
speed = self.calc_speed(start, time_now, downloaded_data_len - resume_downloaded_data_len)
|
|
||||||
data_len = None
|
|
||||||
if percent > 0:
|
|
||||||
data_len = int(downloaded_data_len * 100 / percent)
|
|
||||||
self._hook_progress({
|
|
||||||
'status': 'downloading',
|
|
||||||
'downloaded_bytes': downloaded_data_len,
|
|
||||||
'total_bytes_estimate': data_len,
|
|
||||||
'tmpfilename': tmpfilename,
|
|
||||||
'filename': filename,
|
|
||||||
'eta': eta,
|
|
||||||
'elapsed': time_now - start,
|
|
||||||
'speed': speed,
|
|
||||||
})
|
|
||||||
cursor_in_new_line = False
|
|
||||||
else:
|
|
||||||
# no percent for live streams
|
|
||||||
mobj = re.search(r'([0-9]+\.[0-9]{3}) kB / [0-9]+\.[0-9]{2} sec', line)
|
|
||||||
if mobj:
|
|
||||||
downloaded_data_len = int(float(mobj.group(1)) * 1024)
|
|
||||||
time_now = time.time()
|
|
||||||
speed = self.calc_speed(start, time_now, downloaded_data_len)
|
|
||||||
self._hook_progress({
|
|
||||||
'downloaded_bytes': downloaded_data_len,
|
|
||||||
'tmpfilename': tmpfilename,
|
|
||||||
'filename': filename,
|
|
||||||
'status': 'downloading',
|
|
||||||
'elapsed': time_now - start,
|
|
||||||
'speed': speed,
|
|
||||||
})
|
|
||||||
cursor_in_new_line = False
|
|
||||||
elif self.params.get('verbose', False):
|
|
||||||
if not cursor_in_new_line:
|
|
||||||
self.to_screen('')
|
|
||||||
cursor_in_new_line = True
|
|
||||||
self.to_screen('[rtmpdump] ' + line)
|
|
||||||
finally:
|
|
||||||
proc.wait()
|
|
||||||
if not cursor_in_new_line:
|
|
||||||
self.to_screen('')
|
|
||||||
return proc.returncode
|
|
||||||
|
|
||||||
url = info_dict['url']
|
|
||||||
player_url = info_dict.get('player_url')
|
|
||||||
page_url = info_dict.get('page_url')
|
|
||||||
app = info_dict.get('app')
|
|
||||||
play_path = info_dict.get('play_path')
|
|
||||||
tc_url = info_dict.get('tc_url')
|
|
||||||
flash_version = info_dict.get('flash_version')
|
|
||||||
live = info_dict.get('rtmp_live', False)
|
|
||||||
conn = info_dict.get('rtmp_conn')
|
|
||||||
protocol = info_dict.get('rtmp_protocol')
|
|
||||||
real_time = info_dict.get('rtmp_real_time', False)
|
|
||||||
no_resume = info_dict.get('no_resume', False)
|
|
||||||
continue_dl = self.params.get('continuedl', True)
|
|
||||||
|
|
||||||
self.report_destination(filename)
|
|
||||||
tmpfilename = self.temp_name(filename)
|
|
||||||
test = self.params.get('test', False)
|
|
||||||
|
|
||||||
# Check for rtmpdump first
|
|
||||||
if not check_executable('rtmpdump', ['-h']):
|
|
||||||
self.report_error('RTMP download detected but "rtmpdump" could not be run. Please install it.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Download using rtmpdump. rtmpdump returns exit code 2 when
|
|
||||||
# the connection was interrupted and resuming appears to be
|
|
||||||
# possible. This is part of rtmpdump's normal usage, AFAIK.
|
|
||||||
basic_args = [
|
|
||||||
'rtmpdump', '--verbose', '-r', url,
|
|
||||||
'-o', tmpfilename]
|
|
||||||
if player_url is not None:
|
|
||||||
basic_args += ['--swfVfy', player_url]
|
|
||||||
if page_url is not None:
|
|
||||||
basic_args += ['--pageUrl', page_url]
|
|
||||||
if app is not None:
|
|
||||||
basic_args += ['--app', app]
|
|
||||||
if play_path is not None:
|
|
||||||
basic_args += ['--playpath', play_path]
|
|
||||||
if tc_url is not None:
|
|
||||||
basic_args += ['--tcUrl', tc_url]
|
|
||||||
if test:
|
|
||||||
basic_args += ['--stop', '1']
|
|
||||||
if flash_version is not None:
|
|
||||||
basic_args += ['--flashVer', flash_version]
|
|
||||||
if live:
|
|
||||||
basic_args += ['--live']
|
|
||||||
if isinstance(conn, list):
|
|
||||||
for entry in conn:
|
|
||||||
basic_args += ['--conn', entry]
|
|
||||||
elif isinstance(conn, compat_str):
|
|
||||||
basic_args += ['--conn', conn]
|
|
||||||
if protocol is not None:
|
|
||||||
basic_args += ['--protocol', protocol]
|
|
||||||
if real_time:
|
|
||||||
basic_args += ['--realtime']
|
|
||||||
|
|
||||||
args = basic_args
|
|
||||||
if not no_resume and continue_dl and not live:
|
|
||||||
args += ['--resume']
|
|
||||||
if not live and continue_dl:
|
|
||||||
args += ['--skip', '1']
|
|
||||||
|
|
||||||
args = [encodeArgument(a) for a in args]
|
|
||||||
|
|
||||||
self._debug_cmd(args, exe='rtmpdump')
|
|
||||||
|
|
||||||
RD_SUCCESS = 0
|
|
||||||
RD_FAILED = 1
|
|
||||||
RD_INCOMPLETE = 2
|
|
||||||
RD_NO_CONNECT = 3
|
|
||||||
|
|
||||||
started = time.time()
|
|
||||||
|
|
||||||
try:
|
|
||||||
retval = run_rtmpdump(args)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
if not info_dict.get('is_live'):
|
|
||||||
raise
|
|
||||||
retval = RD_SUCCESS
|
|
||||||
self.to_screen('\n[rtmpdump] Interrupted by user')
|
|
||||||
|
|
||||||
if retval == RD_NO_CONNECT:
|
|
||||||
self.report_error('[rtmpdump] Could not connect to RTMP server.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
while retval in (RD_INCOMPLETE, RD_FAILED) and not test and not live:
|
|
||||||
prevsize = os.path.getsize(encodeFilename(tmpfilename))
|
|
||||||
self.to_screen('[rtmpdump] Downloaded %s bytes' % prevsize)
|
|
||||||
time.sleep(5.0) # This seems to be needed
|
|
||||||
args = basic_args + ['--resume']
|
|
||||||
if retval == RD_FAILED:
|
|
||||||
args += ['--skip', '1']
|
|
||||||
args = [encodeArgument(a) for a in args]
|
|
||||||
retval = run_rtmpdump(args)
|
|
||||||
cursize = os.path.getsize(encodeFilename(tmpfilename))
|
|
||||||
if prevsize == cursize and retval == RD_FAILED:
|
|
||||||
break
|
|
||||||
# Some rtmp streams seem abort after ~ 99.8%. Don't complain for those
|
|
||||||
if prevsize == cursize and retval == RD_INCOMPLETE and cursize > 1024:
|
|
||||||
self.to_screen('[rtmpdump] Could not download the whole video. This can happen for some advertisements.')
|
|
||||||
retval = RD_SUCCESS
|
|
||||||
break
|
|
||||||
if retval == RD_SUCCESS or (test and retval == RD_INCOMPLETE):
|
|
||||||
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
|
||||||
self.to_screen('[rtmpdump] Downloaded %s bytes' % fsize)
|
|
||||||
self.try_rename(tmpfilename, filename)
|
|
||||||
self._hook_progress({
|
|
||||||
'downloaded_bytes': fsize,
|
|
||||||
'total_bytes': fsize,
|
|
||||||
'filename': filename,
|
|
||||||
'status': 'finished',
|
|
||||||
'elapsed': time.time() - started,
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.to_stderr('\n')
|
|
||||||
self.report_error('rtmpdump exited with code %d' % retval)
|
|
||||||
return False
|
|
@ -1,47 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from .common import FileDownloader
|
|
||||||
from ..utils import (
|
|
||||||
check_executable,
|
|
||||||
encodeFilename,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RtspFD(FileDownloader):
|
|
||||||
def real_download(self, filename, info_dict):
|
|
||||||
url = info_dict['url']
|
|
||||||
self.report_destination(filename)
|
|
||||||
tmpfilename = self.temp_name(filename)
|
|
||||||
|
|
||||||
if check_executable('mplayer', ['-h']):
|
|
||||||
args = [
|
|
||||||
'mplayer', '-really-quiet', '-vo', 'null', '-vc', 'dummy',
|
|
||||||
'-dumpstream', '-dumpfile', tmpfilename, url]
|
|
||||||
elif check_executable('mpv', ['-h']):
|
|
||||||
args = [
|
|
||||||
'mpv', '-really-quiet', '--vo=null', '--stream-dump=' + tmpfilename, url]
|
|
||||||
else:
|
|
||||||
self.report_error('MMS or RTSP download detected but neither "mplayer" nor "mpv" could be run. Please install any.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
self._debug_cmd(args)
|
|
||||||
|
|
||||||
retval = subprocess.call(args)
|
|
||||||
if retval == 0:
|
|
||||||
fsize = os.path.getsize(encodeFilename(tmpfilename))
|
|
||||||
self.to_screen('\r[%s] %s bytes' % (args[0], fsize))
|
|
||||||
self.try_rename(tmpfilename, filename)
|
|
||||||
self._hook_progress({
|
|
||||||
'downloaded_bytes': fsize,
|
|
||||||
'total_bytes': fsize,
|
|
||||||
'filename': filename,
|
|
||||||
'status': 'finished',
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.to_stderr('\n')
|
|
||||||
self.report_error('%s exited with code %d' % (args[0], retval))
|
|
||||||
return False
|
|
@ -1,46 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
try:
|
|
||||||
from .lazy_extractors import *
|
|
||||||
from .lazy_extractors import _ALL_CLASSES
|
|
||||||
_LAZY_LOADER = True
|
|
||||||
except ImportError:
|
|
||||||
_LAZY_LOADER = False
|
|
||||||
from .extractors import *
|
|
||||||
|
|
||||||
_ALL_CLASSES = [
|
|
||||||
klass
|
|
||||||
for name, klass in globals().items()
|
|
||||||
if name.endswith('IE') and name != 'GenericIE'
|
|
||||||
]
|
|
||||||
#_ALL_CLASSES.append(GenericIE)
|
|
||||||
|
|
||||||
|
|
||||||
def gen_extractor_classes():
|
|
||||||
""" Return a list of supported extractors.
|
|
||||||
The order does matter; the first extractor matched is the one handling the URL.
|
|
||||||
"""
|
|
||||||
return _ALL_CLASSES
|
|
||||||
|
|
||||||
|
|
||||||
def gen_extractors():
|
|
||||||
""" Return a list of an instance of every supported extractor.
|
|
||||||
The order does matter; the first extractor matched is the one handling the URL.
|
|
||||||
"""
|
|
||||||
return [klass() for klass in gen_extractor_classes()]
|
|
||||||
|
|
||||||
|
|
||||||
def list_extractors(age_limit):
|
|
||||||
"""
|
|
||||||
Return a list of extractors that are suitable for the given age,
|
|
||||||
sorted by extractor ID.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return sorted(
|
|
||||||
filter(lambda ie: ie.is_suitable(age_limit), gen_extractors()),
|
|
||||||
key=lambda ie: ie.IE_NAME.lower())
|
|
||||||
|
|
||||||
|
|
||||||
def get_info_extractor(ie_name):
|
|
||||||
"""Returns the info extractor class with the given ie_name"""
|
|
||||||
return globals()[ie_name + 'IE']
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,50 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..utils import ExtractorError
|
|
||||||
|
|
||||||
|
|
||||||
class CommonMistakesIE(InfoExtractor):
|
|
||||||
IE_DESC = False # Do not list
|
|
||||||
_VALID_URL = r'''(?x)
|
|
||||||
(?:url|URL)$
|
|
||||||
'''
|
|
||||||
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'url',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'URL',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
msg = (
|
|
||||||
'You\'ve asked youtube-dl to download the URL "%s". '
|
|
||||||
'That doesn\'t make any sense. '
|
|
||||||
'Simply remove the parameter in your command or configuration.'
|
|
||||||
) % url
|
|
||||||
if not self._downloader.params.get('verbose'):
|
|
||||||
msg += ' Add -v to the command line to see what arguments and configuration youtube-dl got.'
|
|
||||||
raise ExtractorError(msg, expected=True)
|
|
||||||
|
|
||||||
|
|
||||||
class UnicodeBOMIE(InfoExtractor):
|
|
||||||
IE_DESC = False
|
|
||||||
_VALID_URL = r'(?P<bom>\ufeff)(?P<id>.*)$'
|
|
||||||
|
|
||||||
# Disable test for python 3.2 since BOM is broken in re in this version
|
|
||||||
# (see https://github.com/rg3/youtube-dl/issues/9751)
|
|
||||||
_TESTS = [] if (3, 0) < sys.version_info <= (3, 3) else [{
|
|
||||||
'url': '\ufeffhttp://www.youtube.com/watch?v=BaW_jenozKc',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
real_url = self._match_id(url)
|
|
||||||
self.report_warning(
|
|
||||||
'Your URL starts with a Byte Order Mark (BOM). '
|
|
||||||
'Removing the BOM and looking for "%s" ...' % real_url)
|
|
||||||
return self.url_result(real_url)
|
|
@ -1,60 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..compat import (
|
|
||||||
compat_urlparse,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RtmpIE(InfoExtractor):
|
|
||||||
IE_DESC = False # Do not list
|
|
||||||
_VALID_URL = r'(?i)rtmp[est]?://.+'
|
|
||||||
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'rtmp://cp44293.edgefcs.net/ondemand?auth=daEcTdydfdqcsb8cZcDbAaCbhamacbbawaS-bw7dBb-bWG-GqpGFqCpNCnGoyL&aifp=v001&slist=public/unsecure/audio/2c97899446428e4301471a8cb72b4b97--audio--pmg-20110908-0900a_flv_aac_med_int.mp4',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'rtmp://edge.live.hitbox.tv/live/dimak',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._generic_id(url)
|
|
||||||
title = self._generic_title(url)
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'formats': [{
|
|
||||||
'url': url,
|
|
||||||
'ext': 'flv',
|
|
||||||
'format_id': compat_urlparse.urlparse(url).scheme,
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class MmsIE(InfoExtractor):
|
|
||||||
IE_DESC = False # Do not list
|
|
||||||
_VALID_URL = r'(?i)mms://.+'
|
|
||||||
|
|
||||||
_TEST = {
|
|
||||||
# Direct MMS link
|
|
||||||
'url': 'mms://kentro.kaist.ac.kr/200907/MilesReid(0709).wmv',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'MilesReid(0709)',
|
|
||||||
'ext': 'wmv',
|
|
||||||
'title': 'MilesReid(0709)',
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True, # rtsp downloads, requiring mplayer or mpv
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._generic_id(url)
|
|
||||||
title = self._generic_title(url)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'url': url,
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
# flake8: noqa
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
from .commonmistakes import CommonMistakesIE, UnicodeBOMIE
|
|
||||||
from .commonprotocols import (
|
|
||||||
MmsIE,
|
|
||||||
RtmpIE,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .openload import OpenloadIE
|
|
||||||
|
|
||||||
from .youtube import (
|
|
||||||
YoutubeIE,
|
|
||||||
YoutubeChannelIE,
|
|
||||||
YoutubeFavouritesIE,
|
|
||||||
YoutubeHistoryIE,
|
|
||||||
YoutubeLiveIE,
|
|
||||||
YoutubePlaylistIE,
|
|
||||||
YoutubePlaylistsIE,
|
|
||||||
YoutubeRecommendedIE,
|
|
||||||
YoutubeSearchDateIE,
|
|
||||||
YoutubeSearchIE,
|
|
||||||
YoutubeSearchURLIE,
|
|
||||||
YoutubeShowIE,
|
|
||||||
YoutubeSubscriptionsIE,
|
|
||||||
YoutubeTruncatedIDIE,
|
|
||||||
YoutubeTruncatedURLIE,
|
|
||||||
YoutubeUserIE,
|
|
||||||
YoutubeWatchLaterIE,
|
|
||||||
)
|
|
File diff suppressed because it is too large
Load Diff
@ -1,379 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..compat import (
|
|
||||||
compat_urlparse,
|
|
||||||
compat_kwargs,
|
|
||||||
)
|
|
||||||
from ..utils import (
|
|
||||||
check_executable,
|
|
||||||
determine_ext,
|
|
||||||
encodeArgument,
|
|
||||||
ExtractorError,
|
|
||||||
get_element_by_id,
|
|
||||||
get_exe_version,
|
|
||||||
is_outdated_version,
|
|
||||||
std_headers,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def cookie_to_dict(cookie):
|
|
||||||
cookie_dict = {
|
|
||||||
'name': cookie.name,
|
|
||||||
'value': cookie.value,
|
|
||||||
}
|
|
||||||
if cookie.port_specified:
|
|
||||||
cookie_dict['port'] = cookie.port
|
|
||||||
if cookie.domain_specified:
|
|
||||||
cookie_dict['domain'] = cookie.domain
|
|
||||||
if cookie.path_specified:
|
|
||||||
cookie_dict['path'] = cookie.path
|
|
||||||
if cookie.expires is not None:
|
|
||||||
cookie_dict['expires'] = cookie.expires
|
|
||||||
if cookie.secure is not None:
|
|
||||||
cookie_dict['secure'] = cookie.secure
|
|
||||||
if cookie.discard is not None:
|
|
||||||
cookie_dict['discard'] = cookie.discard
|
|
||||||
try:
|
|
||||||
if (cookie.has_nonstandard_attr('httpOnly') or
|
|
||||||
cookie.has_nonstandard_attr('httponly') or
|
|
||||||
cookie.has_nonstandard_attr('HttpOnly')):
|
|
||||||
cookie_dict['httponly'] = True
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
return cookie_dict
|
|
||||||
|
|
||||||
|
|
||||||
def cookie_jar_to_list(cookie_jar):
|
|
||||||
return [cookie_to_dict(cookie) for cookie in cookie_jar]
|
|
||||||
|
|
||||||
|
|
||||||
class PhantomJSwrapper(object):
|
|
||||||
"""PhantomJS wrapper class
|
|
||||||
|
|
||||||
This class is experimental.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_TEMPLATE = r'''
|
|
||||||
phantom.onError = function(msg, trace) {{
|
|
||||||
var msgStack = ['PHANTOM ERROR: ' + msg];
|
|
||||||
if(trace && trace.length) {{
|
|
||||||
msgStack.push('TRACE:');
|
|
||||||
trace.forEach(function(t) {{
|
|
||||||
msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line
|
|
||||||
+ (t.function ? ' (in function ' + t.function +')' : ''));
|
|
||||||
}});
|
|
||||||
}}
|
|
||||||
console.error(msgStack.join('\n'));
|
|
||||||
phantom.exit(1);
|
|
||||||
}};
|
|
||||||
var page = require('webpage').create();
|
|
||||||
var fs = require('fs');
|
|
||||||
var read = {{ mode: 'r', charset: 'utf-8' }};
|
|
||||||
var write = {{ mode: 'w', charset: 'utf-8' }};
|
|
||||||
JSON.parse(fs.read("{cookies}", read)).forEach(function(x) {{
|
|
||||||
phantom.addCookie(x);
|
|
||||||
}});
|
|
||||||
page.settings.resourceTimeout = {timeout};
|
|
||||||
page.settings.userAgent = "{ua}";
|
|
||||||
page.onLoadStarted = function() {{
|
|
||||||
page.evaluate(function() {{
|
|
||||||
delete window._phantom;
|
|
||||||
delete window.callPhantom;
|
|
||||||
}});
|
|
||||||
}};
|
|
||||||
var saveAndExit = function() {{
|
|
||||||
fs.write("{html}", page.content, write);
|
|
||||||
fs.write("{cookies}", JSON.stringify(phantom.cookies), write);
|
|
||||||
phantom.exit();
|
|
||||||
}};
|
|
||||||
page.onLoadFinished = function(status) {{
|
|
||||||
if(page.url === "") {{
|
|
||||||
page.setContent(fs.read("{html}", read), "{url}");
|
|
||||||
}}
|
|
||||||
else {{
|
|
||||||
{jscode}
|
|
||||||
}}
|
|
||||||
}};
|
|
||||||
page.open("");
|
|
||||||
'''
|
|
||||||
|
|
||||||
_TMP_FILE_NAMES = ['script', 'html', 'cookies']
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _version():
|
|
||||||
return get_exe_version('phantomjs', version_re=r'([0-9.]+)')
|
|
||||||
|
|
||||||
def __init__(self, extractor, required_version=None, timeout=10000):
|
|
||||||
self._TMP_FILES = {}
|
|
||||||
|
|
||||||
self.exe = check_executable('phantomjs', ['-v'])
|
|
||||||
if not self.exe:
|
|
||||||
raise ExtractorError('PhantomJS executable not found in PATH, '
|
|
||||||
'download it from http://phantomjs.org',
|
|
||||||
expected=True)
|
|
||||||
|
|
||||||
self.extractor = extractor
|
|
||||||
|
|
||||||
if required_version:
|
|
||||||
version = self._version()
|
|
||||||
if is_outdated_version(version, required_version):
|
|
||||||
self.extractor._downloader.report_warning(
|
|
||||||
'Your copy of PhantomJS is outdated, update it to version '
|
|
||||||
'%s or newer if you encounter any errors.' % required_version)
|
|
||||||
|
|
||||||
self.options = {
|
|
||||||
'timeout': timeout,
|
|
||||||
}
|
|
||||||
for name in self._TMP_FILE_NAMES:
|
|
||||||
tmp = tempfile.NamedTemporaryFile(delete=False)
|
|
||||||
tmp.close()
|
|
||||||
self._TMP_FILES[name] = tmp
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
for name in self._TMP_FILE_NAMES:
|
|
||||||
try:
|
|
||||||
os.remove(self._TMP_FILES[name].name)
|
|
||||||
except (IOError, OSError, KeyError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _save_cookies(self, url):
|
|
||||||
cookies = cookie_jar_to_list(self.extractor._downloader.cookiejar)
|
|
||||||
for cookie in cookies:
|
|
||||||
if 'path' not in cookie:
|
|
||||||
cookie['path'] = '/'
|
|
||||||
if 'domain' not in cookie:
|
|
||||||
cookie['domain'] = compat_urlparse.urlparse(url).netloc
|
|
||||||
with open(self._TMP_FILES['cookies'].name, 'wb') as f:
|
|
||||||
f.write(json.dumps(cookies).encode('utf-8'))
|
|
||||||
|
|
||||||
def _load_cookies(self):
|
|
||||||
with open(self._TMP_FILES['cookies'].name, 'rb') as f:
|
|
||||||
cookies = json.loads(f.read().decode('utf-8'))
|
|
||||||
for cookie in cookies:
|
|
||||||
if cookie['httponly'] is True:
|
|
||||||
cookie['rest'] = {'httpOnly': None}
|
|
||||||
if 'expiry' in cookie:
|
|
||||||
cookie['expire_time'] = cookie['expiry']
|
|
||||||
self.extractor._set_cookie(**compat_kwargs(cookie))
|
|
||||||
|
|
||||||
def get(self, url, html=None, video_id=None, note=None, note2='Executing JS on webpage', headers={}, jscode='saveAndExit();'):
|
|
||||||
"""
|
|
||||||
Downloads webpage (if needed) and executes JS
|
|
||||||
|
|
||||||
Params:
|
|
||||||
url: website url
|
|
||||||
html: optional, html code of website
|
|
||||||
video_id: video id
|
|
||||||
note: optional, displayed when downloading webpage
|
|
||||||
note2: optional, displayed when executing JS
|
|
||||||
headers: custom http headers
|
|
||||||
jscode: code to be executed when page is loaded
|
|
||||||
|
|
||||||
Returns tuple with:
|
|
||||||
* downloaded website (after JS execution)
|
|
||||||
* anything you print with `console.log` (but not inside `page.execute`!)
|
|
||||||
|
|
||||||
In most cases you don't need to add any `jscode`.
|
|
||||||
It is executed in `page.onLoadFinished`.
|
|
||||||
`saveAndExit();` is mandatory, use it instead of `phantom.exit()`
|
|
||||||
It is possible to wait for some element on the webpage, for example:
|
|
||||||
var check = function() {
|
|
||||||
var elementFound = page.evaluate(function() {
|
|
||||||
return document.querySelector('#b.done') !== null;
|
|
||||||
});
|
|
||||||
if(elementFound)
|
|
||||||
saveAndExit();
|
|
||||||
else
|
|
||||||
window.setTimeout(check, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
page.evaluate(function(){
|
|
||||||
document.querySelector('#a').click();
|
|
||||||
});
|
|
||||||
check();
|
|
||||||
"""
|
|
||||||
if 'saveAndExit();' not in jscode:
|
|
||||||
raise ExtractorError('`saveAndExit();` not found in `jscode`')
|
|
||||||
if not html:
|
|
||||||
html = self.extractor._download_webpage(url, video_id, note=note, headers=headers)
|
|
||||||
with open(self._TMP_FILES['html'].name, 'wb') as f:
|
|
||||||
f.write(html.encode('utf-8'))
|
|
||||||
|
|
||||||
self._save_cookies(url)
|
|
||||||
|
|
||||||
replaces = self.options
|
|
||||||
replaces['url'] = url
|
|
||||||
user_agent = headers.get('User-Agent') or std_headers['User-Agent']
|
|
||||||
replaces['ua'] = user_agent.replace('"', '\\"')
|
|
||||||
replaces['jscode'] = jscode
|
|
||||||
|
|
||||||
for x in self._TMP_FILE_NAMES:
|
|
||||||
replaces[x] = self._TMP_FILES[x].name.replace('\\', '\\\\').replace('"', '\\"')
|
|
||||||
|
|
||||||
with open(self._TMP_FILES['script'].name, 'wb') as f:
|
|
||||||
f.write(self._TEMPLATE.format(**replaces).encode('utf-8'))
|
|
||||||
|
|
||||||
if video_id is None:
|
|
||||||
self.extractor.to_screen('%s' % (note2,))
|
|
||||||
else:
|
|
||||||
self.extractor.to_screen('%s: %s' % (video_id, note2))
|
|
||||||
|
|
||||||
p = subprocess.Popen([
|
|
||||||
self.exe, '--ssl-protocol=any',
|
|
||||||
self._TMP_FILES['script'].name
|
|
||||||
], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
out, err = p.communicate()
|
|
||||||
if p.returncode != 0:
|
|
||||||
raise ExtractorError(
|
|
||||||
'Executing JS failed\n:' + encodeArgument(err))
|
|
||||||
with open(self._TMP_FILES['html'].name, 'rb') as f:
|
|
||||||
html = f.read().decode('utf-8')
|
|
||||||
|
|
||||||
self._load_cookies()
|
|
||||||
|
|
||||||
return (html, encodeArgument(out))
|
|
||||||
|
|
||||||
|
|
||||||
class OpenloadIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'https?://(?:www\.)?(?:openload\.(?:co|io|link)|oload\.(?:tv|stream|site|xyz|win|download))/(?:f|embed)/(?P<id>[a-zA-Z0-9-_]+)'
|
|
||||||
|
|
||||||
_TESTS = [{
|
|
||||||
'url': 'https://openload.co/f/kUEfGclsU9o',
|
|
||||||
'md5': 'bf1c059b004ebc7a256f89408e65c36e',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'kUEfGclsU9o',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'skyrim_no-audio_1080.mp4',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://openload.co/embed/rjC09fkPLYs',
|
|
||||||
'info_dict': {
|
|
||||||
'id': 'rjC09fkPLYs',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'movie.mp4',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
|
||||||
'subtitles': {
|
|
||||||
'en': [{
|
|
||||||
'ext': 'vtt',
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True, # test subtitles only
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
'url': 'https://openload.co/embed/kUEfGclsU9o/skyrim_no-audio_1080.mp4',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://openload.io/f/ZAn6oz-VZGE/',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://openload.co/f/_-ztPaZtMhM/',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
# unavailable via https://openload.co/f/Sxz5sADo82g/, different layout
|
|
||||||
# for title and ext
|
|
||||||
'url': 'https://openload.co/embed/Sxz5sADo82g/',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
# unavailable via https://openload.co/embed/e-Ixz9ZR5L0/ but available
|
|
||||||
# via https://openload.co/f/e-Ixz9ZR5L0/
|
|
||||||
'url': 'https://openload.co/f/e-Ixz9ZR5L0/',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://oload.tv/embed/KnG-kKZdcfY/',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'http://www.openload.link/f/KnG-kKZdcfY',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://oload.stream/f/KnG-kKZdcfY',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://oload.xyz/f/WwRBpzW8Wtk',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://oload.win/f/kUEfGclsU9o',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
'url': 'https://oload.download/f/kUEfGclsU9o',
|
|
||||||
'only_matching': True,
|
|
||||||
}, {
|
|
||||||
# Its title has not got its extension but url has it
|
|
||||||
'url': 'https://oload.download/f/N4Otkw39VCw/Tomb.Raider.2018.HDRip.XviD.AC3-EVO.avi.mp4',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _extract_urls(webpage):
|
|
||||||
return re.findall(
|
|
||||||
r'<iframe[^>]+src=["\']((?:https?://)?(?:openload\.(?:co|io)|oload\.tv)/embed/[a-zA-Z0-9-_]+)',
|
|
||||||
webpage)
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
video_id = self._match_id(url)
|
|
||||||
url_pattern = 'https://openload.co/%%s/%s/' % video_id
|
|
||||||
headers = {
|
|
||||||
'User-Agent': self._USER_AGENT,
|
|
||||||
}
|
|
||||||
|
|
||||||
for path in ('embed', 'f'):
|
|
||||||
page_url = url_pattern % path
|
|
||||||
last = path == 'f'
|
|
||||||
webpage = self._download_webpage(
|
|
||||||
page_url, video_id, 'Downloading %s webpage' % path,
|
|
||||||
headers=headers, fatal=last)
|
|
||||||
if not webpage:
|
|
||||||
continue
|
|
||||||
if 'File not found' in webpage or 'deleted by the owner' in webpage:
|
|
||||||
if not last:
|
|
||||||
continue
|
|
||||||
raise ExtractorError('File not found', expected=True, video_id=video_id)
|
|
||||||
break
|
|
||||||
|
|
||||||
phantom = PhantomJSwrapper(self, required_version='2.0')
|
|
||||||
webpage, _ = phantom.get(page_url, html=webpage, video_id=video_id, headers=headers)
|
|
||||||
|
|
||||||
decoded_id = (get_element_by_id('streamurl', webpage) or
|
|
||||||
get_element_by_id('streamuri', webpage) or
|
|
||||||
get_element_by_id('streamurj', webpage) or
|
|
||||||
self._search_regex(
|
|
||||||
(r'>\s*([\w-]+~\d{10,}~\d+\.\d+\.0\.0~[\w-]+)\s*<',
|
|
||||||
r'>\s*([\w~-]+~\d+\.\d+\.\d+\.\d+~[\w~-]+)',
|
|
||||||
r'>\s*([\w-]+~\d{10,}~(?:[a-f\d]+:){2}:~[\w-]+)\s*<',
|
|
||||||
r'>\s*([\w~-]+~[a-f0-9:]+~[\w~-]+)\s*<',
|
|
||||||
r'>\s*([\w~-]+~[a-f0-9:]+~[\w~-]+)'), webpage,
|
|
||||||
'stream URL'))
|
|
||||||
|
|
||||||
video_url = 'https://openload.co/stream/%s?mime=true' % decoded_id
|
|
||||||
|
|
||||||
title = self._og_search_title(webpage, default=None) or self._search_regex(
|
|
||||||
r'<span[^>]+class=["\']title["\'][^>]*>([^<]+)', webpage,
|
|
||||||
'title', default=None) or self._html_search_meta(
|
|
||||||
'description', webpage, 'title', fatal=True)
|
|
||||||
|
|
||||||
entries = self._parse_html5_media_entries(page_url, webpage, video_id)
|
|
||||||
entry = entries[0] if entries else {}
|
|
||||||
subtitles = entry.get('subtitles')
|
|
||||||
|
|
||||||
info_dict = {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'thumbnail': entry.get('thumbnail') or self._og_search_thumbnail(webpage, default=None),
|
|
||||||
'url': video_url,
|
|
||||||
'ext': determine_ext(title, None) or determine_ext(url, 'mp4'),
|
|
||||||
'subtitles': subtitles,
|
|
||||||
'http_headers': headers,
|
|
||||||
}
|
|
||||||
return info_dict
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,262 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import json
|
|
||||||
import operator
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .utils import (
|
|
||||||
ExtractorError,
|
|
||||||
remove_quotes,
|
|
||||||
)
|
|
||||||
|
|
||||||
_OPERATORS = [
|
|
||||||
('|', operator.or_),
|
|
||||||
('^', operator.xor),
|
|
||||||
('&', operator.and_),
|
|
||||||
('>>', operator.rshift),
|
|
||||||
('<<', operator.lshift),
|
|
||||||
('-', operator.sub),
|
|
||||||
('+', operator.add),
|
|
||||||
('%', operator.mod),
|
|
||||||
('/', operator.truediv),
|
|
||||||
('*', operator.mul),
|
|
||||||
]
|
|
||||||
_ASSIGN_OPERATORS = [(op + '=', opfunc) for op, opfunc in _OPERATORS]
|
|
||||||
_ASSIGN_OPERATORS.append(('=', lambda cur, right: right))
|
|
||||||
|
|
||||||
_NAME_RE = r'[a-zA-Z_$][a-zA-Z_$0-9]*'
|
|
||||||
|
|
||||||
|
|
||||||
class JSInterpreter(object):
|
|
||||||
def __init__(self, code, objects=None):
|
|
||||||
if objects is None:
|
|
||||||
objects = {}
|
|
||||||
self.code = code
|
|
||||||
self._functions = {}
|
|
||||||
self._objects = objects
|
|
||||||
|
|
||||||
def interpret_statement(self, stmt, local_vars, allow_recursion=100):
|
|
||||||
if allow_recursion < 0:
|
|
||||||
raise ExtractorError('Recursion limit reached')
|
|
||||||
|
|
||||||
should_abort = False
|
|
||||||
stmt = stmt.lstrip()
|
|
||||||
stmt_m = re.match(r'var\s', stmt)
|
|
||||||
if stmt_m:
|
|
||||||
expr = stmt[len(stmt_m.group(0)):]
|
|
||||||
else:
|
|
||||||
return_m = re.match(r'return(?:\s+|$)', stmt)
|
|
||||||
if return_m:
|
|
||||||
expr = stmt[len(return_m.group(0)):]
|
|
||||||
should_abort = True
|
|
||||||
else:
|
|
||||||
# Try interpreting it as an expression
|
|
||||||
expr = stmt
|
|
||||||
|
|
||||||
v = self.interpret_expression(expr, local_vars, allow_recursion)
|
|
||||||
return v, should_abort
|
|
||||||
|
|
||||||
def interpret_expression(self, expr, local_vars, allow_recursion):
|
|
||||||
expr = expr.strip()
|
|
||||||
if expr == '': # Empty expression
|
|
||||||
return None
|
|
||||||
|
|
||||||
if expr.startswith('('):
|
|
||||||
parens_count = 0
|
|
||||||
for m in re.finditer(r'[()]', expr):
|
|
||||||
if m.group(0) == '(':
|
|
||||||
parens_count += 1
|
|
||||||
else:
|
|
||||||
parens_count -= 1
|
|
||||||
if parens_count == 0:
|
|
||||||
sub_expr = expr[1:m.start()]
|
|
||||||
sub_result = self.interpret_expression(
|
|
||||||
sub_expr, local_vars, allow_recursion)
|
|
||||||
remaining_expr = expr[m.end():].strip()
|
|
||||||
if not remaining_expr:
|
|
||||||
return sub_result
|
|
||||||
else:
|
|
||||||
expr = json.dumps(sub_result) + remaining_expr
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ExtractorError('Premature end of parens in %r' % expr)
|
|
||||||
|
|
||||||
for op, opfunc in _ASSIGN_OPERATORS:
|
|
||||||
m = re.match(r'''(?x)
|
|
||||||
(?P<out>%s)(?:\[(?P<index>[^\]]+?)\])?
|
|
||||||
\s*%s
|
|
||||||
(?P<expr>.*)$''' % (_NAME_RE, re.escape(op)), expr)
|
|
||||||
if not m:
|
|
||||||
continue
|
|
||||||
right_val = self.interpret_expression(
|
|
||||||
m.group('expr'), local_vars, allow_recursion - 1)
|
|
||||||
|
|
||||||
if m.groupdict().get('index'):
|
|
||||||
lvar = local_vars[m.group('out')]
|
|
||||||
idx = self.interpret_expression(
|
|
||||||
m.group('index'), local_vars, allow_recursion)
|
|
||||||
assert isinstance(idx, int)
|
|
||||||
cur = lvar[idx]
|
|
||||||
val = opfunc(cur, right_val)
|
|
||||||
lvar[idx] = val
|
|
||||||
return val
|
|
||||||
else:
|
|
||||||
cur = local_vars.get(m.group('out'))
|
|
||||||
val = opfunc(cur, right_val)
|
|
||||||
local_vars[m.group('out')] = val
|
|
||||||
return val
|
|
||||||
|
|
||||||
if expr.isdigit():
|
|
||||||
return int(expr)
|
|
||||||
|
|
||||||
var_m = re.match(
|
|
||||||
r'(?!if|return|true|false)(?P<name>%s)$' % _NAME_RE,
|
|
||||||
expr)
|
|
||||||
if var_m:
|
|
||||||
return local_vars[var_m.group('name')]
|
|
||||||
|
|
||||||
try:
|
|
||||||
return json.loads(expr)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
m = re.match(
|
|
||||||
r'(?P<in>%s)\[(?P<idx>.+)\]$' % _NAME_RE, expr)
|
|
||||||
if m:
|
|
||||||
val = local_vars[m.group('in')]
|
|
||||||
idx = self.interpret_expression(
|
|
||||||
m.group('idx'), local_vars, allow_recursion - 1)
|
|
||||||
return val[idx]
|
|
||||||
|
|
||||||
m = re.match(
|
|
||||||
r'(?P<var>%s)(?:\.(?P<member>[^(]+)|\[(?P<member2>[^]]+)\])\s*(?:\(+(?P<args>[^()]*)\))?$' % _NAME_RE,
|
|
||||||
expr)
|
|
||||||
if m:
|
|
||||||
variable = m.group('var')
|
|
||||||
member = remove_quotes(m.group('member') or m.group('member2'))
|
|
||||||
arg_str = m.group('args')
|
|
||||||
|
|
||||||
if variable in local_vars:
|
|
||||||
obj = local_vars[variable]
|
|
||||||
else:
|
|
||||||
if variable not in self._objects:
|
|
||||||
self._objects[variable] = self.extract_object(variable)
|
|
||||||
obj = self._objects[variable]
|
|
||||||
|
|
||||||
if arg_str is None:
|
|
||||||
# Member access
|
|
||||||
if member == 'length':
|
|
||||||
return len(obj)
|
|
||||||
return obj[member]
|
|
||||||
|
|
||||||
assert expr.endswith(')')
|
|
||||||
# Function call
|
|
||||||
if arg_str == '':
|
|
||||||
argvals = tuple()
|
|
||||||
else:
|
|
||||||
argvals = tuple([
|
|
||||||
self.interpret_expression(v, local_vars, allow_recursion)
|
|
||||||
for v in arg_str.split(',')])
|
|
||||||
|
|
||||||
if member == 'split':
|
|
||||||
assert argvals == ('',)
|
|
||||||
return list(obj)
|
|
||||||
if member == 'join':
|
|
||||||
assert len(argvals) == 1
|
|
||||||
return argvals[0].join(obj)
|
|
||||||
if member == 'reverse':
|
|
||||||
assert len(argvals) == 0
|
|
||||||
obj.reverse()
|
|
||||||
return obj
|
|
||||||
if member == 'slice':
|
|
||||||
assert len(argvals) == 1
|
|
||||||
return obj[argvals[0]:]
|
|
||||||
if member == 'splice':
|
|
||||||
assert isinstance(obj, list)
|
|
||||||
index, howMany = argvals
|
|
||||||
res = []
|
|
||||||
for i in range(index, min(index + howMany, len(obj))):
|
|
||||||
res.append(obj.pop(index))
|
|
||||||
return res
|
|
||||||
|
|
||||||
return obj[member](argvals)
|
|
||||||
|
|
||||||
for op, opfunc in _OPERATORS:
|
|
||||||
m = re.match(r'(?P<x>.+?)%s(?P<y>.+)' % re.escape(op), expr)
|
|
||||||
if not m:
|
|
||||||
continue
|
|
||||||
x, abort = self.interpret_statement(
|
|
||||||
m.group('x'), local_vars, allow_recursion - 1)
|
|
||||||
if abort:
|
|
||||||
raise ExtractorError(
|
|
||||||
'Premature left-side return of %s in %r' % (op, expr))
|
|
||||||
y, abort = self.interpret_statement(
|
|
||||||
m.group('y'), local_vars, allow_recursion - 1)
|
|
||||||
if abort:
|
|
||||||
raise ExtractorError(
|
|
||||||
'Premature right-side return of %s in %r' % (op, expr))
|
|
||||||
return opfunc(x, y)
|
|
||||||
|
|
||||||
m = re.match(
|
|
||||||
r'^(?P<func>%s)\((?P<args>[a-zA-Z0-9_$,]*)\)$' % _NAME_RE, expr)
|
|
||||||
if m:
|
|
||||||
fname = m.group('func')
|
|
||||||
argvals = tuple([
|
|
||||||
int(v) if v.isdigit() else local_vars[v]
|
|
||||||
for v in m.group('args').split(',')]) if len(m.group('args')) > 0 else tuple()
|
|
||||||
if fname not in self._functions:
|
|
||||||
self._functions[fname] = self.extract_function(fname)
|
|
||||||
return self._functions[fname](argvals)
|
|
||||||
|
|
||||||
raise ExtractorError('Unsupported JS expression %r' % expr)
|
|
||||||
|
|
||||||
def extract_object(self, objname):
|
|
||||||
_FUNC_NAME_RE = r'''(?:[a-zA-Z$0-9]+|"[a-zA-Z$0-9]+"|'[a-zA-Z$0-9]+')'''
|
|
||||||
obj = {}
|
|
||||||
obj_m = re.search(
|
|
||||||
r'''(?x)
|
|
||||||
(?<!this\.)%s\s*=\s*{\s*
|
|
||||||
(?P<fields>(%s\s*:\s*function\s*\(.*?\)\s*{.*?}(?:,\s*)?)*)
|
|
||||||
}\s*;
|
|
||||||
''' % (re.escape(objname), _FUNC_NAME_RE),
|
|
||||||
self.code)
|
|
||||||
fields = obj_m.group('fields')
|
|
||||||
# Currently, it only supports function definitions
|
|
||||||
fields_m = re.finditer(
|
|
||||||
r'''(?x)
|
|
||||||
(?P<key>%s)\s*:\s*function\s*\((?P<args>[a-z,]+)\){(?P<code>[^}]+)}
|
|
||||||
''' % _FUNC_NAME_RE,
|
|
||||||
fields)
|
|
||||||
for f in fields_m:
|
|
||||||
argnames = f.group('args').split(',')
|
|
||||||
obj[remove_quotes(f.group('key'))] = self.build_function(argnames, f.group('code'))
|
|
||||||
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def extract_function(self, funcname):
|
|
||||||
func_m = re.search(
|
|
||||||
r'''(?x)
|
|
||||||
(?:function\s+%s|[{;,]\s*%s\s*=\s*function|var\s+%s\s*=\s*function)\s*
|
|
||||||
\((?P<args>[^)]*)\)\s*
|
|
||||||
\{(?P<code>[^}]+)\}''' % (
|
|
||||||
re.escape(funcname), re.escape(funcname), re.escape(funcname)),
|
|
||||||
self.code)
|
|
||||||
if func_m is None:
|
|
||||||
raise ExtractorError('Could not find JS function %r' % funcname)
|
|
||||||
argnames = func_m.group('args').split(',')
|
|
||||||
|
|
||||||
return self.build_function(argnames, func_m.group('code'))
|
|
||||||
|
|
||||||
def call_function(self, funcname, *args):
|
|
||||||
f = self.extract_function(funcname)
|
|
||||||
return f(args)
|
|
||||||
|
|
||||||
def build_function(self, argnames, code):
|
|
||||||
def resf(args):
|
|
||||||
local_vars = dict(zip(argnames, args))
|
|
||||||
for stmt in code.split(';'):
|
|
||||||
res, abort = self.interpret_statement(stmt, local_vars)
|
|
||||||
if abort:
|
|
||||||
break
|
|
||||||
return res
|
|
||||||
return resf
|
|
@ -1,916 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
import optparse
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from .downloader.external import list_external_downloaders
|
|
||||||
from .compat import (
|
|
||||||
compat_expanduser,
|
|
||||||
compat_get_terminal_size,
|
|
||||||
compat_getenv,
|
|
||||||
compat_kwargs,
|
|
||||||
compat_shlex_split,
|
|
||||||
)
|
|
||||||
from .utils import (
|
|
||||||
preferredencoding,
|
|
||||||
write_string,
|
|
||||||
)
|
|
||||||
from .version import __version__
|
|
||||||
|
|
||||||
|
|
||||||
def _hide_login_info(opts):
|
|
||||||
PRIVATE_OPTS = set(['-p', '--password', '-u', '--username', '--video-password', '--ap-password', '--ap-username'])
|
|
||||||
eqre = re.compile('^(?P<key>' + ('|'.join(re.escape(po) for po in PRIVATE_OPTS)) + ')=.+$')
|
|
||||||
|
|
||||||
def _scrub_eq(o):
|
|
||||||
m = eqre.match(o)
|
|
||||||
if m:
|
|
||||||
return m.group('key') + '=PRIVATE'
|
|
||||||
else:
|
|
||||||
return o
|
|
||||||
|
|
||||||
opts = list(map(_scrub_eq, opts))
|
|
||||||
for idx, opt in enumerate(opts):
|
|
||||||
if opt in PRIVATE_OPTS and idx + 1 < len(opts):
|
|
||||||
opts[idx + 1] = 'PRIVATE'
|
|
||||||
return opts
|
|
||||||
|
|
||||||
|
|
||||||
def parseOpts(overrideArguments=None):
|
|
||||||
def _readOptions(filename_bytes, default=[]):
|
|
||||||
try:
|
|
||||||
optionf = open(filename_bytes)
|
|
||||||
except IOError:
|
|
||||||
return default # silently skip if file is not present
|
|
||||||
try:
|
|
||||||
# FIXME: https://github.com/rg3/youtube-dl/commit/dfe5fa49aed02cf36ba9f743b11b0903554b5e56
|
|
||||||
contents = optionf.read()
|
|
||||||
if sys.version_info < (3,):
|
|
||||||
contents = contents.decode(preferredencoding())
|
|
||||||
res = compat_shlex_split(contents, comments=True)
|
|
||||||
finally:
|
|
||||||
optionf.close()
|
|
||||||
return res
|
|
||||||
|
|
||||||
def _readUserConf():
|
|
||||||
xdg_config_home = compat_getenv('XDG_CONFIG_HOME')
|
|
||||||
if xdg_config_home:
|
|
||||||
userConfFile = os.path.join(xdg_config_home, 'youtube-dl', 'config')
|
|
||||||
if not os.path.isfile(userConfFile):
|
|
||||||
userConfFile = os.path.join(xdg_config_home, 'youtube-dl.conf')
|
|
||||||
else:
|
|
||||||
userConfFile = os.path.join(compat_expanduser('~'), '.config', 'youtube-dl', 'config')
|
|
||||||
if not os.path.isfile(userConfFile):
|
|
||||||
userConfFile = os.path.join(compat_expanduser('~'), '.config', 'youtube-dl.conf')
|
|
||||||
userConf = _readOptions(userConfFile, None)
|
|
||||||
|
|
||||||
if userConf is None:
|
|
||||||
appdata_dir = compat_getenv('appdata')
|
|
||||||
if appdata_dir:
|
|
||||||
userConf = _readOptions(
|
|
||||||
os.path.join(appdata_dir, 'youtube-dl', 'config'),
|
|
||||||
default=None)
|
|
||||||
if userConf is None:
|
|
||||||
userConf = _readOptions(
|
|
||||||
os.path.join(appdata_dir, 'youtube-dl', 'config.txt'),
|
|
||||||
default=None)
|
|
||||||
|
|
||||||
if userConf is None:
|
|
||||||
userConf = _readOptions(
|
|
||||||
os.path.join(compat_expanduser('~'), 'youtube-dl.conf'),
|
|
||||||
default=None)
|
|
||||||
if userConf is None:
|
|
||||||
userConf = _readOptions(
|
|
||||||
os.path.join(compat_expanduser('~'), 'youtube-dl.conf.txt'),
|
|
||||||
default=None)
|
|
||||||
|
|
||||||
if userConf is None:
|
|
||||||
userConf = []
|
|
||||||
|
|
||||||
return userConf
|
|
||||||
|
|
||||||
def _format_option_string(option):
|
|
||||||
''' ('-o', '--option') -> -o, --format METAVAR'''
|
|
||||||
|
|
||||||
opts = []
|
|
||||||
|
|
||||||
if option._short_opts:
|
|
||||||
opts.append(option._short_opts[0])
|
|
||||||
if option._long_opts:
|
|
||||||
opts.append(option._long_opts[0])
|
|
||||||
if len(opts) > 1:
|
|
||||||
opts.insert(1, ', ')
|
|
||||||
|
|
||||||
if option.takes_value():
|
|
||||||
opts.append(' %s' % option.metavar)
|
|
||||||
|
|
||||||
return ''.join(opts)
|
|
||||||
|
|
||||||
def _comma_separated_values_options_callback(option, opt_str, value, parser):
|
|
||||||
setattr(parser.values, option.dest, value.split(','))
|
|
||||||
|
|
||||||
# No need to wrap help messages if we're on a wide console
|
|
||||||
columns = compat_get_terminal_size().columns
|
|
||||||
max_width = columns if columns else 80
|
|
||||||
max_help_position = 80
|
|
||||||
|
|
||||||
fmt = optparse.IndentedHelpFormatter(width=max_width, max_help_position=max_help_position)
|
|
||||||
fmt.format_option_strings = _format_option_string
|
|
||||||
|
|
||||||
kw = {
|
|
||||||
'version': __version__,
|
|
||||||
'formatter': fmt,
|
|
||||||
'usage': '%prog [OPTIONS] URL [URL...]',
|
|
||||||
'conflict_handler': 'resolve',
|
|
||||||
}
|
|
||||||
|
|
||||||
parser = optparse.OptionParser(**compat_kwargs(kw))
|
|
||||||
|
|
||||||
general = optparse.OptionGroup(parser, 'General Options')
|
|
||||||
general.add_option(
|
|
||||||
'-h', '--help',
|
|
||||||
action='help',
|
|
||||||
help='Print this help text and exit')
|
|
||||||
general.add_option(
|
|
||||||
'-v', '--version',
|
|
||||||
action='version',
|
|
||||||
help='Print program version and exit')
|
|
||||||
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)')
|
|
||||||
general.add_option(
|
|
||||||
'-i', '--ignore-errors',
|
|
||||||
action='store_true', dest='ignoreerrors', default=False,
|
|
||||||
help='Continue on download errors, for example to skip unavailable videos in a playlist')
|
|
||||||
general.add_option(
|
|
||||||
'--abort-on-error',
|
|
||||||
action='store_false', dest='ignoreerrors',
|
|
||||||
help='Abort downloading of further videos (in the playlist or the command line) if an error occurs')
|
|
||||||
general.add_option(
|
|
||||||
'--dump-user-agent',
|
|
||||||
action='store_true', dest='dump_user_agent', default=False,
|
|
||||||
help='Display the current browser identification')
|
|
||||||
general.add_option(
|
|
||||||
'--list-extractors',
|
|
||||||
action='store_true', dest='list_extractors', default=False,
|
|
||||||
help='List all supported extractors')
|
|
||||||
general.add_option(
|
|
||||||
'--extractor-descriptions',
|
|
||||||
action='store_true', dest='list_extractor_descriptions', default=False,
|
|
||||||
help='Output descriptions of all supported extractors')
|
|
||||||
general.add_option(
|
|
||||||
'--force-generic-extractor',
|
|
||||||
action='store_true', dest='force_generic_extractor', default=False,
|
|
||||||
help='Force extraction to use the generic extractor')
|
|
||||||
general.add_option(
|
|
||||||
'--default-search',
|
|
||||||
dest='default_search', metavar='PREFIX',
|
|
||||||
help='Use this prefix for unqualified URLs. For example "gvsearch2:" downloads two videos from google videos for youtube-dl "large apple". Use the value "auto" to let youtube-dl guess ("auto_warning" to emit a warning when guessing). "error" just throws an error. The default value "fixup_error" repairs broken URLs, but emits an error if this is not possible instead of searching.')
|
|
||||||
general.add_option(
|
|
||||||
'--ignore-config',
|
|
||||||
action='store_true',
|
|
||||||
help='Do not read configuration files. '
|
|
||||||
'When given in the global configuration file /etc/youtube-dl.conf: '
|
|
||||||
'Do not read the user configuration in ~/.config/youtube-dl/config '
|
|
||||||
'(%APPDATA%/youtube-dl/config.txt on Windows)')
|
|
||||||
general.add_option(
|
|
||||||
'--config-location',
|
|
||||||
dest='config_location', metavar='PATH',
|
|
||||||
help='Location of the configuration file; either the path to the config or its containing directory.')
|
|
||||||
general.add_option(
|
|
||||||
'--flat-playlist',
|
|
||||||
action='store_const', dest='extract_flat', const='in_playlist',
|
|
||||||
default=False,
|
|
||||||
help='Do not extract the videos of a playlist, only list them.')
|
|
||||||
general.add_option(
|
|
||||||
'--mark-watched',
|
|
||||||
action='store_true', dest='mark_watched', default=False,
|
|
||||||
help='Mark videos watched (YouTube only)')
|
|
||||||
general.add_option(
|
|
||||||
'--no-mark-watched',
|
|
||||||
action='store_false', dest='mark_watched', default=False,
|
|
||||||
help='Do not mark videos watched (YouTube only)')
|
|
||||||
general.add_option(
|
|
||||||
'--no-color', '--no-colors',
|
|
||||||
action='store_true', dest='no_color',
|
|
||||||
default=False,
|
|
||||||
help='Do not emit color codes in output')
|
|
||||||
|
|
||||||
network = optparse.OptionGroup(parser, 'Network Options')
|
|
||||||
network.add_option(
|
|
||||||
'--proxy', dest='proxy',
|
|
||||||
default=None, metavar='URL',
|
|
||||||
help='Use the specified HTTP/HTTPS/SOCKS proxy. To enable '
|
|
||||||
'SOCKS proxy, specify a proper scheme. For example '
|
|
||||||
'socks5://127.0.0.1:1080/. Pass in an empty string (--proxy "") '
|
|
||||||
'for direct connection')
|
|
||||||
network.add_option(
|
|
||||||
'--socket-timeout',
|
|
||||||
dest='socket_timeout', type=float, default=None, metavar='SECONDS',
|
|
||||||
help='Time to wait before giving up, in seconds')
|
|
||||||
network.add_option(
|
|
||||||
'--source-address',
|
|
||||||
metavar='IP', dest='source_address', default=None,
|
|
||||||
help='Client-side IP address to bind to',
|
|
||||||
)
|
|
||||||
network.add_option(
|
|
||||||
'-4', '--force-ipv4',
|
|
||||||
action='store_const', const='0.0.0.0', dest='source_address',
|
|
||||||
help='Make all connections via IPv4',
|
|
||||||
)
|
|
||||||
network.add_option(
|
|
||||||
'-6', '--force-ipv6',
|
|
||||||
action='store_const', const='::', dest='source_address',
|
|
||||||
help='Make all connections via IPv6',
|
|
||||||
)
|
|
||||||
|
|
||||||
geo = optparse.OptionGroup(parser, 'Geo Restriction')
|
|
||||||
geo.add_option(
|
|
||||||
'--geo-verification-proxy',
|
|
||||||
dest='geo_verification_proxy', default=None, metavar='URL',
|
|
||||||
help='Use this proxy to verify the IP address for some geo-restricted sites. '
|
|
||||||
'The default proxy specified by --proxy (or none, if the option is not present) is used for the actual downloading.')
|
|
||||||
geo.add_option(
|
|
||||||
'--cn-verification-proxy',
|
|
||||||
dest='cn_verification_proxy', default=None, metavar='URL',
|
|
||||||
help=optparse.SUPPRESS_HELP)
|
|
||||||
geo.add_option(
|
|
||||||
'--geo-bypass',
|
|
||||||
action='store_true', dest='geo_bypass', default=True,
|
|
||||||
help='Bypass geographic restriction via faking X-Forwarded-For HTTP header')
|
|
||||||
geo.add_option(
|
|
||||||
'--no-geo-bypass',
|
|
||||||
action='store_false', dest='geo_bypass', default=True,
|
|
||||||
help='Do not bypass geographic restriction via faking X-Forwarded-For HTTP header')
|
|
||||||
geo.add_option(
|
|
||||||
'--geo-bypass-country', metavar='CODE',
|
|
||||||
dest='geo_bypass_country', default=None,
|
|
||||||
help='Force bypass geographic restriction with explicitly provided two-letter ISO 3166-2 country code')
|
|
||||||
geo.add_option(
|
|
||||||
'--geo-bypass-ip-block', metavar='IP_BLOCK',
|
|
||||||
dest='geo_bypass_ip_block', default=None,
|
|
||||||
help='Force bypass geographic restriction with explicitly provided IP block in CIDR notation')
|
|
||||||
|
|
||||||
selection = optparse.OptionGroup(parser, 'Video Selection')
|
|
||||||
selection.add_option(
|
|
||||||
'--playlist-start',
|
|
||||||
dest='playliststart', metavar='NUMBER', default=1, type=int,
|
|
||||||
help='Playlist video to start at (default is %default)')
|
|
||||||
selection.add_option(
|
|
||||||
'--playlist-end',
|
|
||||||
dest='playlistend', metavar='NUMBER', default=None, type=int,
|
|
||||||
help='Playlist video to end at (default is last)')
|
|
||||||
selection.add_option(
|
|
||||||
'--playlist-items',
|
|
||||||
dest='playlist_items', metavar='ITEM_SPEC', default=None,
|
|
||||||
help='Playlist video items to download. Specify indices of the videos in the playlist separated by commas like: "--playlist-items 1,2,5,8" if you want to download videos indexed 1, 2, 5, 8 in the playlist. You can specify range: "--playlist-items 1-3,7,10-13", it will download the videos at index 1, 2, 3, 7, 10, 11, 12 and 13.')
|
|
||||||
selection.add_option(
|
|
||||||
'--match-title',
|
|
||||||
dest='matchtitle', metavar='REGEX',
|
|
||||||
help='Download only matching titles (regex or caseless sub-string)')
|
|
||||||
selection.add_option(
|
|
||||||
'--reject-title',
|
|
||||||
dest='rejecttitle', metavar='REGEX',
|
|
||||||
help='Skip download for matching titles (regex or caseless sub-string)')
|
|
||||||
selection.add_option(
|
|
||||||
'--max-downloads',
|
|
||||||
dest='max_downloads', metavar='NUMBER', type=int, default=None,
|
|
||||||
help='Abort after downloading NUMBER files')
|
|
||||||
selection.add_option(
|
|
||||||
'--min-filesize',
|
|
||||||
metavar='SIZE', dest='min_filesize', default=None,
|
|
||||||
help='Do not download any videos smaller than SIZE (e.g. 50k or 44.6m)')
|
|
||||||
selection.add_option(
|
|
||||||
'--max-filesize',
|
|
||||||
metavar='SIZE', dest='max_filesize', default=None,
|
|
||||||
help='Do not download any videos larger than SIZE (e.g. 50k or 44.6m)')
|
|
||||||
selection.add_option(
|
|
||||||
'--date',
|
|
||||||
metavar='DATE', dest='date', default=None,
|
|
||||||
help='Download only videos uploaded in this date')
|
|
||||||
selection.add_option(
|
|
||||||
'--datebefore',
|
|
||||||
metavar='DATE', dest='datebefore', default=None,
|
|
||||||
help='Download only videos uploaded on or before this date (i.e. inclusive)')
|
|
||||||
selection.add_option(
|
|
||||||
'--dateafter',
|
|
||||||
metavar='DATE', dest='dateafter', default=None,
|
|
||||||
help='Download only videos uploaded on or after this date (i.e. inclusive)')
|
|
||||||
selection.add_option(
|
|
||||||
'--min-views',
|
|
||||||
metavar='COUNT', dest='min_views', default=None, type=int,
|
|
||||||
help='Do not download any videos with less than COUNT views')
|
|
||||||
selection.add_option(
|
|
||||||
'--max-views',
|
|
||||||
metavar='COUNT', dest='max_views', default=None, type=int,
|
|
||||||
help='Do not download any videos with more than COUNT views')
|
|
||||||
selection.add_option(
|
|
||||||
'--match-filter',
|
|
||||||
metavar='FILTER', dest='match_filter', default=None,
|
|
||||||
help=(
|
|
||||||
'Generic video filter. '
|
|
||||||
'Specify any key (see the "OUTPUT TEMPLATE" for a list of available keys) to '
|
|
||||||
'match if the key is present, '
|
|
||||||
'!key to check if the key is not present, '
|
|
||||||
'key > NUMBER (like "comment_count > 12", also works with '
|
|
||||||
'>=, <, <=, !=, =) to compare against a number, '
|
|
||||||
'key = \'LITERAL\' (like "uploader = \'Mike Smith\'", also works with !=) '
|
|
||||||
'to match against a string literal '
|
|
||||||
'and & to require multiple matches. '
|
|
||||||
'Values which are not known are excluded unless you '
|
|
||||||
'put a question mark (?) after the operator. '
|
|
||||||
'For example, to only match videos that have been liked more than '
|
|
||||||
'100 times and disliked less than 50 times (or the dislike '
|
|
||||||
'functionality is not available at the given service), but who '
|
|
||||||
'also have a description, use --match-filter '
|
|
||||||
'"like_count > 100 & dislike_count <? 50 & description" .'
|
|
||||||
))
|
|
||||||
selection.add_option(
|
|
||||||
'--no-playlist',
|
|
||||||
action='store_true', dest='noplaylist', default=False,
|
|
||||||
help='Download only the video, if the URL refers to a video and a playlist.')
|
|
||||||
selection.add_option(
|
|
||||||
'--yes-playlist',
|
|
||||||
action='store_false', dest='noplaylist', default=False,
|
|
||||||
help='Download the playlist, if the URL refers to a video and a playlist.')
|
|
||||||
selection.add_option(
|
|
||||||
'--age-limit',
|
|
||||||
metavar='YEARS', dest='age_limit', default=None, type=int,
|
|
||||||
help='Download only videos suitable for the given age')
|
|
||||||
selection.add_option(
|
|
||||||
'--download-archive', metavar='FILE',
|
|
||||||
dest='download_archive',
|
|
||||||
help='Download only videos not listed in the archive file. Record the IDs of all downloaded videos in it.')
|
|
||||||
selection.add_option(
|
|
||||||
'--include-ads',
|
|
||||||
dest='include_ads', action='store_true',
|
|
||||||
help='Download advertisements as well (experimental)')
|
|
||||||
|
|
||||||
authentication = optparse.OptionGroup(parser, 'Authentication Options')
|
|
||||||
authentication.add_option(
|
|
||||||
'-u', '--username',
|
|
||||||
dest='username', metavar='USERNAME',
|
|
||||||
help='Login with this account ID')
|
|
||||||
authentication.add_option(
|
|
||||||
'-p', '--password',
|
|
||||||
dest='password', metavar='PASSWORD',
|
|
||||||
help='Account password. If this option is left out, youtube-dl will ask interactively.')
|
|
||||||
authentication.add_option(
|
|
||||||
'-2', '--twofactor',
|
|
||||||
dest='twofactor', metavar='TWOFACTOR',
|
|
||||||
help='Two-factor authentication code')
|
|
||||||
authentication.add_option(
|
|
||||||
'-n', '--netrc',
|
|
||||||
action='store_true', dest='usenetrc', default=False,
|
|
||||||
help='Use .netrc authentication data')
|
|
||||||
authentication.add_option(
|
|
||||||
'--video-password',
|
|
||||||
dest='videopassword', metavar='PASSWORD',
|
|
||||||
help='Video password (vimeo, smotri, youku)')
|
|
||||||
|
|
||||||
adobe_pass = optparse.OptionGroup(parser, 'Adobe Pass Options')
|
|
||||||
adobe_pass.add_option(
|
|
||||||
'--ap-mso',
|
|
||||||
dest='ap_mso', metavar='MSO',
|
|
||||||
help='Adobe Pass multiple-system operator (TV provider) identifier, use --ap-list-mso for a list of available MSOs')
|
|
||||||
adobe_pass.add_option(
|
|
||||||
'--ap-username',
|
|
||||||
dest='ap_username', metavar='USERNAME',
|
|
||||||
help='Multiple-system operator account login')
|
|
||||||
adobe_pass.add_option(
|
|
||||||
'--ap-password',
|
|
||||||
dest='ap_password', metavar='PASSWORD',
|
|
||||||
help='Multiple-system operator account password. If this option is left out, youtube-dl will ask interactively.')
|
|
||||||
adobe_pass.add_option(
|
|
||||||
'--ap-list-mso',
|
|
||||||
action='store_true', dest='ap_list_mso', default=False,
|
|
||||||
help='List all supported multiple-system operators')
|
|
||||||
|
|
||||||
video_format = optparse.OptionGroup(parser, 'Video Format Options')
|
|
||||||
video_format.add_option(
|
|
||||||
'-f', '--format',
|
|
||||||
action='store', dest='format', metavar='FORMAT', default=None,
|
|
||||||
help='Video format code, see the "FORMAT SELECTION" for all the info')
|
|
||||||
video_format.add_option(
|
|
||||||
'--all-formats',
|
|
||||||
action='store_const', dest='format', const='all',
|
|
||||||
help='Download all available video formats')
|
|
||||||
video_format.add_option(
|
|
||||||
'--prefer-free-formats',
|
|
||||||
action='store_true', dest='prefer_free_formats', default=False,
|
|
||||||
help='Prefer free video formats unless a specific one is requested')
|
|
||||||
video_format.add_option(
|
|
||||||
'-F', '--list-formats',
|
|
||||||
action='store_true', dest='listformats',
|
|
||||||
help='List all available formats of requested videos')
|
|
||||||
video_format.add_option(
|
|
||||||
'--youtube-include-dash-manifest',
|
|
||||||
action='store_true', dest='youtube_include_dash_manifest', default=True,
|
|
||||||
help=optparse.SUPPRESS_HELP)
|
|
||||||
video_format.add_option(
|
|
||||||
'--youtube-skip-dash-manifest',
|
|
||||||
action='store_false', dest='youtube_include_dash_manifest',
|
|
||||||
help='Do not download the DASH manifests and related data on YouTube videos')
|
|
||||||
video_format.add_option(
|
|
||||||
'--merge-output-format',
|
|
||||||
action='store', dest='merge_output_format', metavar='FORMAT', default=None,
|
|
||||||
help=(
|
|
||||||
'If a merge is required (e.g. bestvideo+bestaudio), '
|
|
||||||
'output to given container format. One of mkv, mp4, ogg, webm, flv. '
|
|
||||||
'Ignored if no merge is required'))
|
|
||||||
|
|
||||||
subtitles = optparse.OptionGroup(parser, 'Subtitle Options')
|
|
||||||
subtitles.add_option(
|
|
||||||
'--write-sub', '--write-srt',
|
|
||||||
action='store_true', dest='writesubtitles', default=False,
|
|
||||||
help='Write subtitle file')
|
|
||||||
subtitles.add_option(
|
|
||||||
'--write-auto-sub', '--write-automatic-sub',
|
|
||||||
action='store_true', dest='writeautomaticsub', default=False,
|
|
||||||
help='Write automatically generated subtitle file (YouTube only)')
|
|
||||||
subtitles.add_option(
|
|
||||||
'--all-subs',
|
|
||||||
action='store_true', dest='allsubtitles', default=False,
|
|
||||||
help='Download all the available subtitles of the video')
|
|
||||||
subtitles.add_option(
|
|
||||||
'--list-subs',
|
|
||||||
action='store_true', dest='listsubtitles', default=False,
|
|
||||||
help='List all available subtitles for the video')
|
|
||||||
subtitles.add_option(
|
|
||||||
'--sub-format',
|
|
||||||
action='store', dest='subtitlesformat', metavar='FORMAT', default='best',
|
|
||||||
help='Subtitle format, accepts formats preference, for example: "srt" or "ass/srt/best"')
|
|
||||||
subtitles.add_option(
|
|
||||||
'--sub-lang', '--sub-langs', '--srt-lang',
|
|
||||||
action='callback', dest='subtitleslangs', metavar='LANGS', type='str',
|
|
||||||
default=[], callback=_comma_separated_values_options_callback,
|
|
||||||
help='Languages of the subtitles to download (optional) separated by commas, use --list-subs for available language tags')
|
|
||||||
|
|
||||||
downloader = optparse.OptionGroup(parser, 'Download Options')
|
|
||||||
downloader.add_option(
|
|
||||||
'-r', '--limit-rate', '--rate-limit',
|
|
||||||
dest='ratelimit', metavar='RATE',
|
|
||||||
help='Maximum download rate in bytes per second (e.g. 50K or 4.2M)')
|
|
||||||
downloader.add_option(
|
|
||||||
'-R', '--retries',
|
|
||||||
dest='retries', metavar='RETRIES', default=10,
|
|
||||||
help='Number of retries (default is %default), or "infinite".')
|
|
||||||
downloader.add_option(
|
|
||||||
'--fragment-retries',
|
|
||||||
dest='fragment_retries', metavar='RETRIES', default=10,
|
|
||||||
help='Number of retries for a fragment (default is %default), or "infinite" (DASH, hlsnative and ISM)')
|
|
||||||
downloader.add_option(
|
|
||||||
'--skip-unavailable-fragments',
|
|
||||||
action='store_true', dest='skip_unavailable_fragments', default=True,
|
|
||||||
help='Skip unavailable fragments (DASH, hlsnative and ISM)')
|
|
||||||
downloader.add_option(
|
|
||||||
'--abort-on-unavailable-fragment',
|
|
||||||
action='store_false', dest='skip_unavailable_fragments',
|
|
||||||
help='Abort downloading when some fragment is not available')
|
|
||||||
downloader.add_option(
|
|
||||||
'--keep-fragments',
|
|
||||||
action='store_true', dest='keep_fragments', default=False,
|
|
||||||
help='Keep downloaded fragments on disk after downloading is finished; fragments are erased by default')
|
|
||||||
downloader.add_option(
|
|
||||||
'--buffer-size',
|
|
||||||
dest='buffersize', metavar='SIZE', default='1024',
|
|
||||||
help='Size of download buffer (e.g. 1024 or 16K) (default is %default)')
|
|
||||||
downloader.add_option(
|
|
||||||
'--no-resize-buffer',
|
|
||||||
action='store_true', dest='noresizebuffer', default=False,
|
|
||||||
help='Do not automatically adjust the buffer size. By default, the buffer size is automatically resized from an initial value of SIZE.')
|
|
||||||
downloader.add_option(
|
|
||||||
'--http-chunk-size',
|
|
||||||
dest='http_chunk_size', metavar='SIZE', default=None,
|
|
||||||
help='Size of a chunk for chunk-based HTTP downloading (e.g. 10485760 or 10M) (default is disabled). '
|
|
||||||
'May be useful for bypassing bandwidth throttling imposed by a webserver (experimental)')
|
|
||||||
downloader.add_option(
|
|
||||||
'--test',
|
|
||||||
action='store_true', dest='test', default=False,
|
|
||||||
help=optparse.SUPPRESS_HELP)
|
|
||||||
downloader.add_option(
|
|
||||||
'--playlist-reverse',
|
|
||||||
action='store_true',
|
|
||||||
help='Download playlist videos in reverse order')
|
|
||||||
downloader.add_option(
|
|
||||||
'--playlist-random',
|
|
||||||
action='store_true',
|
|
||||||
help='Download playlist videos in random order')
|
|
||||||
downloader.add_option(
|
|
||||||
'--xattr-set-filesize',
|
|
||||||
dest='xattr_set_filesize', action='store_true',
|
|
||||||
help='Set file xattribute ytdl.filesize with expected file size')
|
|
||||||
downloader.add_option(
|
|
||||||
'--hls-prefer-native',
|
|
||||||
dest='hls_prefer_native', action='store_true', default=None,
|
|
||||||
help='Use the native HLS downloader instead of ffmpeg')
|
|
||||||
downloader.add_option(
|
|
||||||
'--hls-prefer-ffmpeg',
|
|
||||||
dest='hls_prefer_native', action='store_false', default=None,
|
|
||||||
help='Use ffmpeg instead of the native HLS downloader')
|
|
||||||
downloader.add_option(
|
|
||||||
'--hls-use-mpegts',
|
|
||||||
dest='hls_use_mpegts', action='store_true',
|
|
||||||
help='Use the mpegts container for HLS videos, allowing to play the '
|
|
||||||
'video while downloading (some players may not be able to play it)')
|
|
||||||
downloader.add_option(
|
|
||||||
'--external-downloader',
|
|
||||||
dest='external_downloader', metavar='COMMAND',
|
|
||||||
help='Use the specified external downloader. '
|
|
||||||
'Currently supports %s' % ','.join(list_external_downloaders()))
|
|
||||||
downloader.add_option(
|
|
||||||
'--external-downloader-args',
|
|
||||||
dest='external_downloader_args', metavar='ARGS',
|
|
||||||
help='Give these arguments to the external downloader')
|
|
||||||
|
|
||||||
workarounds = optparse.OptionGroup(parser, 'Workarounds')
|
|
||||||
workarounds.add_option(
|
|
||||||
'--encoding',
|
|
||||||
dest='encoding', metavar='ENCODING',
|
|
||||||
help='Force the specified encoding (experimental)')
|
|
||||||
workarounds.add_option(
|
|
||||||
'--no-check-certificate',
|
|
||||||
action='store_true', dest='no_check_certificate', default=False,
|
|
||||||
help='Suppress HTTPS certificate validation')
|
|
||||||
workarounds.add_option(
|
|
||||||
'--prefer-insecure',
|
|
||||||
'--prefer-unsecure', action='store_true', dest='prefer_insecure',
|
|
||||||
help='Use an unencrypted connection to retrieve information about the video. (Currently supported only for YouTube)')
|
|
||||||
workarounds.add_option(
|
|
||||||
'--user-agent',
|
|
||||||
metavar='UA', dest='user_agent',
|
|
||||||
help='Specify a custom user agent')
|
|
||||||
workarounds.add_option(
|
|
||||||
'--referer',
|
|
||||||
metavar='URL', dest='referer', default=None,
|
|
||||||
help='Specify a custom referer, use if the video access is restricted to one domain',
|
|
||||||
)
|
|
||||||
workarounds.add_option(
|
|
||||||
'--add-header',
|
|
||||||
metavar='FIELD:VALUE', dest='headers', action='append',
|
|
||||||
help='Specify a custom HTTP header and its value, separated by a colon \':\'. You can use this option multiple times',
|
|
||||||
)
|
|
||||||
workarounds.add_option(
|
|
||||||
'--bidi-workaround',
|
|
||||||
dest='bidi_workaround', action='store_true',
|
|
||||||
help='Work around terminals that lack bidirectional text support. Requires bidiv or fribidi executable in PATH')
|
|
||||||
workarounds.add_option(
|
|
||||||
'--sleep-interval', '--min-sleep-interval', metavar='SECONDS',
|
|
||||||
dest='sleep_interval', type=float,
|
|
||||||
help=(
|
|
||||||
'Number of seconds to sleep before each download when used alone '
|
|
||||||
'or a lower bound of a range for randomized sleep before each download '
|
|
||||||
'(minimum possible number of seconds to sleep) when used along with '
|
|
||||||
'--max-sleep-interval.'))
|
|
||||||
workarounds.add_option(
|
|
||||||
'--max-sleep-interval', metavar='SECONDS',
|
|
||||||
dest='max_sleep_interval', type=float,
|
|
||||||
help=(
|
|
||||||
'Upper bound of a range for randomized sleep before each download '
|
|
||||||
'(maximum possible number of seconds to sleep). Must only be used '
|
|
||||||
'along with --min-sleep-interval.'))
|
|
||||||
|
|
||||||
verbosity = optparse.OptionGroup(parser, 'Verbosity / Simulation Options')
|
|
||||||
verbosity.add_option(
|
|
||||||
'-q', '--quiet',
|
|
||||||
action='store_true', dest='quiet', default=False,
|
|
||||||
help='Activate quiet mode')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--no-warnings',
|
|
||||||
dest='no_warnings', action='store_true', default=False,
|
|
||||||
help='Ignore warnings')
|
|
||||||
verbosity.add_option(
|
|
||||||
'-s', '--simulate',
|
|
||||||
action='store_true', dest='simulate', default=False,
|
|
||||||
help='Do not download the video and do not write anything to disk')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--skip-download',
|
|
||||||
action='store_true', dest='skip_download', default=False,
|
|
||||||
help='Do not download the video')
|
|
||||||
verbosity.add_option(
|
|
||||||
'-g', '--get-url',
|
|
||||||
action='store_true', dest='geturl', default=False,
|
|
||||||
help='Simulate, quiet but print URL')
|
|
||||||
verbosity.add_option(
|
|
||||||
'-e', '--get-title',
|
|
||||||
action='store_true', dest='gettitle', default=False,
|
|
||||||
help='Simulate, quiet but print title')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--get-id',
|
|
||||||
action='store_true', dest='getid', default=False,
|
|
||||||
help='Simulate, quiet but print id')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--get-thumbnail',
|
|
||||||
action='store_true', dest='getthumbnail', default=False,
|
|
||||||
help='Simulate, quiet but print thumbnail URL')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--get-description',
|
|
||||||
action='store_true', dest='getdescription', default=False,
|
|
||||||
help='Simulate, quiet but print video description')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--get-duration',
|
|
||||||
action='store_true', dest='getduration', default=False,
|
|
||||||
help='Simulate, quiet but print video length')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--get-filename',
|
|
||||||
action='store_true', dest='getfilename', default=False,
|
|
||||||
help='Simulate, quiet but print output filename')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--get-format',
|
|
||||||
action='store_true', dest='getformat', default=False,
|
|
||||||
help='Simulate, quiet but print output format')
|
|
||||||
verbosity.add_option(
|
|
||||||
'-j', '--dump-json',
|
|
||||||
action='store_true', dest='dumpjson', default=False,
|
|
||||||
help='Simulate, quiet but print JSON information. See the "OUTPUT TEMPLATE" for a description of available keys.')
|
|
||||||
verbosity.add_option(
|
|
||||||
'-J', '--dump-single-json',
|
|
||||||
action='store_true', dest='dump_single_json', default=False,
|
|
||||||
help='Simulate, quiet but print JSON information for each command-line argument. If the URL refers to a playlist, dump the whole playlist information in a single line.')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--print-json',
|
|
||||||
action='store_true', dest='print_json', default=False,
|
|
||||||
help='Be quiet and print the video information as JSON (video is still being downloaded).',
|
|
||||||
)
|
|
||||||
verbosity.add_option(
|
|
||||||
'--newline',
|
|
||||||
action='store_true', dest='progress_with_newline', default=False,
|
|
||||||
help='Output progress bar as new lines')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--no-progress',
|
|
||||||
action='store_true', dest='noprogress', default=False,
|
|
||||||
help='Do not print progress bar')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--console-title',
|
|
||||||
action='store_true', dest='consoletitle', default=False,
|
|
||||||
help='Display progress in console titlebar')
|
|
||||||
verbosity.add_option(
|
|
||||||
'-v', '--verbose',
|
|
||||||
action='store_true', dest='verbose', default=False,
|
|
||||||
help='Print various debugging information')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--dump-pages', '--dump-intermediate-pages',
|
|
||||||
action='store_true', dest='dump_intermediate_pages', default=False,
|
|
||||||
help='Print downloaded pages encoded using base64 to debug problems (very verbose)')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--write-pages',
|
|
||||||
action='store_true', dest='write_pages', default=False,
|
|
||||||
help='Write downloaded intermediary pages to files in the current directory to debug problems')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--youtube-print-sig-code',
|
|
||||||
action='store_true', dest='youtube_print_sig_code', default=False,
|
|
||||||
help=optparse.SUPPRESS_HELP)
|
|
||||||
verbosity.add_option(
|
|
||||||
'--print-traffic', '--dump-headers',
|
|
||||||
dest='debug_printtraffic', action='store_true', default=False,
|
|
||||||
help='Display sent and read HTTP traffic')
|
|
||||||
verbosity.add_option(
|
|
||||||
'-C', '--call-home',
|
|
||||||
dest='call_home', action='store_true', default=False,
|
|
||||||
help='Contact the youtube-dl server for debugging')
|
|
||||||
verbosity.add_option(
|
|
||||||
'--no-call-home',
|
|
||||||
dest='call_home', action='store_false', default=False,
|
|
||||||
help='Do NOT contact the youtube-dl server for debugging')
|
|
||||||
|
|
||||||
filesystem = optparse.OptionGroup(parser, 'Filesystem Options')
|
|
||||||
filesystem.add_option(
|
|
||||||
'-a', '--batch-file',
|
|
||||||
dest='batchfile', metavar='FILE',
|
|
||||||
help="File containing URLs to download ('-' for stdin), one URL per line. "
|
|
||||||
"Lines starting with '#', ';' or ']' are considered as comments and ignored.")
|
|
||||||
filesystem.add_option(
|
|
||||||
'--id', default=False,
|
|
||||||
action='store_true', dest='useid', help='Use only video ID in file name')
|
|
||||||
filesystem.add_option(
|
|
||||||
'-o', '--output',
|
|
||||||
dest='outtmpl', metavar='TEMPLATE',
|
|
||||||
help=('Output filename template, see the "OUTPUT TEMPLATE" for all the info'))
|
|
||||||
filesystem.add_option(
|
|
||||||
'--autonumber-size',
|
|
||||||
dest='autonumber_size', metavar='NUMBER', type=int,
|
|
||||||
help=optparse.SUPPRESS_HELP)
|
|
||||||
filesystem.add_option(
|
|
||||||
'--autonumber-start',
|
|
||||||
dest='autonumber_start', metavar='NUMBER', default=1, type=int,
|
|
||||||
help='Specify the start value for %(autonumber)s (default is %default)')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--restrict-filenames',
|
|
||||||
action='store_true', dest='restrictfilenames', default=False,
|
|
||||||
help='Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames')
|
|
||||||
filesystem.add_option(
|
|
||||||
'-A', '--auto-number',
|
|
||||||
action='store_true', dest='autonumber', default=False,
|
|
||||||
help=optparse.SUPPRESS_HELP)
|
|
||||||
filesystem.add_option(
|
|
||||||
'-t', '--title',
|
|
||||||
action='store_true', dest='usetitle', default=False,
|
|
||||||
help=optparse.SUPPRESS_HELP)
|
|
||||||
filesystem.add_option(
|
|
||||||
'-l', '--literal', default=False,
|
|
||||||
action='store_true', dest='usetitle',
|
|
||||||
help=optparse.SUPPRESS_HELP)
|
|
||||||
filesystem.add_option(
|
|
||||||
'-w', '--no-overwrites',
|
|
||||||
action='store_true', dest='nooverwrites', default=False,
|
|
||||||
help='Do not overwrite files')
|
|
||||||
filesystem.add_option(
|
|
||||||
'-c', '--continue',
|
|
||||||
action='store_true', dest='continue_dl', default=True,
|
|
||||||
help='Force resume of partially downloaded files. By default, youtube-dl will resume downloads if possible.')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--no-continue',
|
|
||||||
action='store_false', dest='continue_dl',
|
|
||||||
help='Do not resume partially downloaded files (restart from beginning)')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--no-part',
|
|
||||||
action='store_true', dest='nopart', default=False,
|
|
||||||
help='Do not use .part files - write directly into output file')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--no-mtime',
|
|
||||||
action='store_false', dest='updatetime', default=True,
|
|
||||||
help='Do not use the Last-modified header to set the file modification time')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--write-description',
|
|
||||||
action='store_true', dest='writedescription', default=False,
|
|
||||||
help='Write video description to a .description file')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--write-info-json',
|
|
||||||
action='store_true', dest='writeinfojson', default=False,
|
|
||||||
help='Write video metadata to a .info.json file')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--write-annotations',
|
|
||||||
action='store_true', dest='writeannotations', default=False,
|
|
||||||
help='Write video annotations to a .annotations.xml file')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--load-info-json', '--load-info',
|
|
||||||
dest='load_info_filename', metavar='FILE',
|
|
||||||
help='JSON file containing the video information (created with the "--write-info-json" option)')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--cookies',
|
|
||||||
dest='cookiefile', metavar='FILE',
|
|
||||||
help='File to read cookies from and dump cookie jar in')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--cache-dir', dest='cachedir', default=None, metavar='DIR',
|
|
||||||
help='Location in the filesystem where youtube-dl can store some downloaded information permanently. By default $XDG_CACHE_HOME/youtube-dl or ~/.cache/youtube-dl . At the moment, only YouTube player files (for videos with obfuscated signatures) are cached, but that may change.')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--no-cache-dir', action='store_const', const=False, dest='cachedir',
|
|
||||||
help='Disable filesystem caching')
|
|
||||||
filesystem.add_option(
|
|
||||||
'--rm-cache-dir',
|
|
||||||
action='store_true', dest='rm_cachedir',
|
|
||||||
help='Delete all filesystem cache files')
|
|
||||||
|
|
||||||
thumbnail = optparse.OptionGroup(parser, 'Thumbnail images')
|
|
||||||
thumbnail.add_option(
|
|
||||||
'--write-thumbnail',
|
|
||||||
action='store_true', dest='writethumbnail', default=False,
|
|
||||||
help='Write thumbnail image to disk')
|
|
||||||
thumbnail.add_option(
|
|
||||||
'--write-all-thumbnails',
|
|
||||||
action='store_true', dest='write_all_thumbnails', default=False,
|
|
||||||
help='Write all thumbnail image formats to disk')
|
|
||||||
thumbnail.add_option(
|
|
||||||
'--list-thumbnails',
|
|
||||||
action='store_true', dest='list_thumbnails', default=False,
|
|
||||||
help='Simulate and list all available thumbnail formats')
|
|
||||||
|
|
||||||
postproc = optparse.OptionGroup(parser, 'Post-processing Options')
|
|
||||||
postproc.add_option(
|
|
||||||
'-x', '--extract-audio',
|
|
||||||
action='store_true', dest='extractaudio', default=False,
|
|
||||||
help='Convert video files to audio-only files (requires ffmpeg or avconv and ffprobe or avprobe)')
|
|
||||||
postproc.add_option(
|
|
||||||
'--audio-format', metavar='FORMAT', dest='audioformat', default='best',
|
|
||||||
help='Specify audio format: "best", "aac", "flac", "mp3", "m4a", "opus", "vorbis", or "wav"; "%default" by default; No effect without -x')
|
|
||||||
postproc.add_option(
|
|
||||||
'--audio-quality', metavar='QUALITY',
|
|
||||||
dest='audioquality', default='5',
|
|
||||||
help='Specify ffmpeg/avconv audio quality, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default %default)')
|
|
||||||
postproc.add_option(
|
|
||||||
'--recode-video',
|
|
||||||
metavar='FORMAT', dest='recodevideo', default=None,
|
|
||||||
help='Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm|mkv|avi)')
|
|
||||||
postproc.add_option(
|
|
||||||
'--postprocessor-args',
|
|
||||||
dest='postprocessor_args', metavar='ARGS',
|
|
||||||
help='Give these arguments to the postprocessor')
|
|
||||||
postproc.add_option(
|
|
||||||
'-k', '--keep-video',
|
|
||||||
action='store_true', dest='keepvideo', default=False,
|
|
||||||
help='Keep the video file on disk after the post-processing; the video is erased by default')
|
|
||||||
postproc.add_option(
|
|
||||||
'--no-post-overwrites',
|
|
||||||
action='store_true', dest='nopostoverwrites', default=False,
|
|
||||||
help='Do not overwrite post-processed files; the post-processed files are overwritten by default')
|
|
||||||
postproc.add_option(
|
|
||||||
'--embed-subs',
|
|
||||||
action='store_true', dest='embedsubtitles', default=False,
|
|
||||||
help='Embed subtitles in the video (only for mp4, webm and mkv videos)')
|
|
||||||
postproc.add_option(
|
|
||||||
'--embed-thumbnail',
|
|
||||||
action='store_true', dest='embedthumbnail', default=False,
|
|
||||||
help='Embed thumbnail in the audio as cover art')
|
|
||||||
postproc.add_option(
|
|
||||||
'--add-metadata',
|
|
||||||
action='store_true', dest='addmetadata', default=False,
|
|
||||||
help='Write metadata to the video file')
|
|
||||||
postproc.add_option(
|
|
||||||
'--metadata-from-title',
|
|
||||||
metavar='FORMAT', dest='metafromtitle',
|
|
||||||
help='Parse additional metadata like song title / artist from the video title. '
|
|
||||||
'The format syntax is the same as --output. Regular expression with '
|
|
||||||
'named capture groups may also be used. '
|
|
||||||
'The parsed parameters replace existing values. '
|
|
||||||
'Example: --metadata-from-title "%(artist)s - %(title)s" matches a title like '
|
|
||||||
'"Coldplay - Paradise". '
|
|
||||||
'Example (regex): --metadata-from-title "(?P<artist>.+?) - (?P<title>.+)"')
|
|
||||||
postproc.add_option(
|
|
||||||
'--xattrs',
|
|
||||||
action='store_true', dest='xattrs', default=False,
|
|
||||||
help='Write metadata to the video file\'s xattrs (using dublin core and xdg standards)')
|
|
||||||
postproc.add_option(
|
|
||||||
'--fixup',
|
|
||||||
metavar='POLICY', dest='fixup', default='detect_or_warn',
|
|
||||||
help='Automatically correct known faults of the file. '
|
|
||||||
'One of never (do nothing), warn (only emit a warning), '
|
|
||||||
'detect_or_warn (the default; fix file if we can, warn otherwise)')
|
|
||||||
postproc.add_option(
|
|
||||||
'--prefer-avconv',
|
|
||||||
action='store_false', dest='prefer_ffmpeg',
|
|
||||||
help='Prefer avconv over ffmpeg for running the postprocessors')
|
|
||||||
postproc.add_option(
|
|
||||||
'--prefer-ffmpeg',
|
|
||||||
action='store_true', dest='prefer_ffmpeg',
|
|
||||||
help='Prefer ffmpeg over avconv for running the postprocessors (default)')
|
|
||||||
postproc.add_option(
|
|
||||||
'--ffmpeg-location', '--avconv-location', metavar='PATH',
|
|
||||||
dest='ffmpeg_location',
|
|
||||||
help='Location of the ffmpeg/avconv binary; either the path to the binary or its containing directory.')
|
|
||||||
postproc.add_option(
|
|
||||||
'--exec',
|
|
||||||
metavar='CMD', dest='exec_cmd',
|
|
||||||
help='Execute a command on the file after downloading, similar to find\'s -exec syntax. Example: --exec \'adb push {} /sdcard/Music/ && rm {}\'')
|
|
||||||
postproc.add_option(
|
|
||||||
'--convert-subs', '--convert-subtitles',
|
|
||||||
metavar='FORMAT', dest='convertsubtitles', default=None,
|
|
||||||
help='Convert the subtitles to other format (currently supported: srt|ass|vtt|lrc)')
|
|
||||||
|
|
||||||
parser.add_option_group(general)
|
|
||||||
parser.add_option_group(network)
|
|
||||||
parser.add_option_group(geo)
|
|
||||||
parser.add_option_group(selection)
|
|
||||||
parser.add_option_group(downloader)
|
|
||||||
parser.add_option_group(filesystem)
|
|
||||||
parser.add_option_group(thumbnail)
|
|
||||||
parser.add_option_group(verbosity)
|
|
||||||
parser.add_option_group(workarounds)
|
|
||||||
parser.add_option_group(video_format)
|
|
||||||
parser.add_option_group(subtitles)
|
|
||||||
parser.add_option_group(authentication)
|
|
||||||
parser.add_option_group(adobe_pass)
|
|
||||||
parser.add_option_group(postproc)
|
|
||||||
|
|
||||||
if overrideArguments is not None:
|
|
||||||
opts, args = parser.parse_args(overrideArguments)
|
|
||||||
if opts.verbose:
|
|
||||||
write_string('[debug] Override config: ' + repr(overrideArguments) + '\n')
|
|
||||||
else:
|
|
||||||
def compat_conf(conf):
|
|
||||||
if sys.version_info < (3,):
|
|
||||||
return [a.decode(preferredencoding(), 'replace') for a in conf]
|
|
||||||
return conf
|
|
||||||
|
|
||||||
command_line_conf = compat_conf(sys.argv[1:])
|
|
||||||
opts, args = parser.parse_args(command_line_conf)
|
|
||||||
|
|
||||||
system_conf = user_conf = custom_conf = []
|
|
||||||
|
|
||||||
if '--config-location' in command_line_conf:
|
|
||||||
location = compat_expanduser(opts.config_location)
|
|
||||||
if os.path.isdir(location):
|
|
||||||
location = os.path.join(location, 'youtube-dl.conf')
|
|
||||||
if not os.path.exists(location):
|
|
||||||
parser.error('config-location %s does not exist.' % location)
|
|
||||||
custom_conf = _readOptions(location)
|
|
||||||
elif '--ignore-config' in command_line_conf:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
system_conf = _readOptions('/etc/youtube-dl.conf')
|
|
||||||
if '--ignore-config' not in system_conf:
|
|
||||||
user_conf = _readUserConf()
|
|
||||||
|
|
||||||
argv = system_conf + user_conf + custom_conf + command_line_conf
|
|
||||||
opts, args = parser.parse_args(argv)
|
|
||||||
if opts.verbose:
|
|
||||||
for conf_label, conf in (
|
|
||||||
('System config', system_conf),
|
|
||||||
('User config', user_conf),
|
|
||||||
('Custom config', custom_conf),
|
|
||||||
('Command-line args', command_line_conf)):
|
|
||||||
write_string('[debug] %s: %s\n' % (conf_label, repr(_hide_login_info(conf))))
|
|
||||||
|
|
||||||
return parser, opts, args
|
|
@ -1,40 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from .embedthumbnail import EmbedThumbnailPP
|
|
||||||
from .ffmpeg import (
|
|
||||||
FFmpegPostProcessor,
|
|
||||||
FFmpegEmbedSubtitlePP,
|
|
||||||
FFmpegExtractAudioPP,
|
|
||||||
FFmpegFixupStretchedPP,
|
|
||||||
FFmpegFixupM3u8PP,
|
|
||||||
FFmpegFixupM4aPP,
|
|
||||||
FFmpegMergerPP,
|
|
||||||
FFmpegMetadataPP,
|
|
||||||
FFmpegVideoConvertorPP,
|
|
||||||
FFmpegSubtitlesConvertorPP,
|
|
||||||
)
|
|
||||||
from .xattrpp import XAttrMetadataPP
|
|
||||||
from .execafterdownload import ExecAfterDownloadPP
|
|
||||||
from .metadatafromtitle import MetadataFromTitlePP
|
|
||||||
|
|
||||||
|
|
||||||
def get_postprocessor(key):
|
|
||||||
return globals()[key + 'PP']
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'EmbedThumbnailPP',
|
|
||||||
'ExecAfterDownloadPP',
|
|
||||||
'FFmpegEmbedSubtitlePP',
|
|
||||||
'FFmpegExtractAudioPP',
|
|
||||||
'FFmpegFixupM3u8PP',
|
|
||||||
'FFmpegFixupM4aPP',
|
|
||||||
'FFmpegFixupStretchedPP',
|
|
||||||
'FFmpegMergerPP',
|
|
||||||
'FFmpegMetadataPP',
|
|
||||||
'FFmpegPostProcessor',
|
|
||||||
'FFmpegSubtitlesConvertorPP',
|
|
||||||
'FFmpegVideoConvertorPP',
|
|
||||||
'MetadataFromTitlePP',
|
|
||||||
'XAttrMetadataPP',
|
|
||||||
]
|
|
@ -1,69 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ..utils import (
|
|
||||||
PostProcessingError,
|
|
||||||
cli_configuration_args,
|
|
||||||
encodeFilename,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PostProcessor(object):
|
|
||||||
"""Post Processor class.
|
|
||||||
|
|
||||||
PostProcessor objects can be added to downloaders with their
|
|
||||||
add_post_processor() method. When the downloader has finished a
|
|
||||||
successful download, it will take its internal chain of PostProcessors
|
|
||||||
and start calling the run() method on each one of them, first with
|
|
||||||
an initial argument and then with the returned value of the previous
|
|
||||||
PostProcessor.
|
|
||||||
|
|
||||||
The chain will be stopped if one of them ever returns None or the end
|
|
||||||
of the chain is reached.
|
|
||||||
|
|
||||||
PostProcessor objects follow a "mutual registration" process similar
|
|
||||||
to InfoExtractor objects.
|
|
||||||
|
|
||||||
Optionally PostProcessor can use a list of additional command-line arguments
|
|
||||||
with self._configuration_args.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_downloader = None
|
|
||||||
|
|
||||||
def __init__(self, downloader=None):
|
|
||||||
self._downloader = downloader
|
|
||||||
|
|
||||||
def set_downloader(self, downloader):
|
|
||||||
"""Sets the downloader for this PP."""
|
|
||||||
self._downloader = downloader
|
|
||||||
|
|
||||||
def run(self, information):
|
|
||||||
"""Run the PostProcessor.
|
|
||||||
|
|
||||||
The "information" argument is a dictionary like the ones
|
|
||||||
composed by InfoExtractors. The only difference is that this
|
|
||||||
one has an extra field called "filepath" that points to the
|
|
||||||
downloaded file.
|
|
||||||
|
|
||||||
This method returns a tuple, the first element is a list of the files
|
|
||||||
that can be deleted, and the second of which is the updated
|
|
||||||
information.
|
|
||||||
|
|
||||||
In addition, this method may raise a PostProcessingError
|
|
||||||
exception if post processing fails.
|
|
||||||
"""
|
|
||||||
return [], information # by default, keep file and do nothing
|
|
||||||
|
|
||||||
def try_utime(self, path, atime, mtime, errnote='Cannot update utime of file'):
|
|
||||||
try:
|
|
||||||
os.utime(encodeFilename(path), (atime, mtime))
|
|
||||||
except Exception:
|
|
||||||
self._downloader.report_warning(errnote)
|
|
||||||
|
|
||||||
def _configuration_args(self, default=[]):
|
|
||||||
return cli_configuration_args(self._downloader.params, 'postprocessor_args', default)
|
|
||||||
|
|
||||||
|
|
||||||
class AudioConversionError(PostProcessingError):
|
|
||||||
pass
|
|
@ -1,93 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from .ffmpeg import FFmpegPostProcessor
|
|
||||||
|
|
||||||
from ..utils import (
|
|
||||||
check_executable,
|
|
||||||
encodeArgument,
|
|
||||||
encodeFilename,
|
|
||||||
PostProcessingError,
|
|
||||||
prepend_extension,
|
|
||||||
shell_quote
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EmbedThumbnailPPError(PostProcessingError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EmbedThumbnailPP(FFmpegPostProcessor):
|
|
||||||
def __init__(self, downloader=None, already_have_thumbnail=False):
|
|
||||||
super(EmbedThumbnailPP, self).__init__(downloader)
|
|
||||||
self._already_have_thumbnail = already_have_thumbnail
|
|
||||||
|
|
||||||
def run(self, info):
|
|
||||||
filename = info['filepath']
|
|
||||||
temp_filename = prepend_extension(filename, 'temp')
|
|
||||||
|
|
||||||
if not info.get('thumbnails'):
|
|
||||||
self._downloader.to_screen('[embedthumbnail] There aren\'t any thumbnails to embed')
|
|
||||||
return [], info
|
|
||||||
|
|
||||||
thumbnail_filename = info['thumbnails'][-1]['filename']
|
|
||||||
|
|
||||||
if not os.path.exists(encodeFilename(thumbnail_filename)):
|
|
||||||
self._downloader.report_warning(
|
|
||||||
'Skipping embedding the thumbnail because the file is missing.')
|
|
||||||
return [], info
|
|
||||||
|
|
||||||
if info['ext'] == 'mp3':
|
|
||||||
options = [
|
|
||||||
'-c', 'copy', '-map', '0', '-map', '1',
|
|
||||||
'-metadata:s:v', 'title="Album cover"', '-metadata:s:v', 'comment="Cover (Front)"']
|
|
||||||
|
|
||||||
self._downloader.to_screen('[ffmpeg] Adding thumbnail to "%s"' % filename)
|
|
||||||
|
|
||||||
self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)
|
|
||||||
|
|
||||||
if not self._already_have_thumbnail:
|
|
||||||
os.remove(encodeFilename(thumbnail_filename))
|
|
||||||
os.remove(encodeFilename(filename))
|
|
||||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
|
||||||
|
|
||||||
elif info['ext'] in ['m4a', 'mp4']:
|
|
||||||
if not check_executable('AtomicParsley', ['-v']):
|
|
||||||
raise EmbedThumbnailPPError('AtomicParsley was not found. Please install.')
|
|
||||||
|
|
||||||
cmd = [encodeFilename('AtomicParsley', True),
|
|
||||||
encodeFilename(filename, True),
|
|
||||||
encodeArgument('--artwork'),
|
|
||||||
encodeFilename(thumbnail_filename, True),
|
|
||||||
encodeArgument('-o'),
|
|
||||||
encodeFilename(temp_filename, True)]
|
|
||||||
|
|
||||||
self._downloader.to_screen('[atomicparsley] Adding thumbnail to "%s"' % filename)
|
|
||||||
|
|
||||||
if self._downloader.params.get('verbose', False):
|
|
||||||
self._downloader.to_screen('[debug] AtomicParsley command line: %s' % shell_quote(cmd))
|
|
||||||
|
|
||||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
|
|
||||||
if p.returncode != 0:
|
|
||||||
msg = stderr.decode('utf-8', 'replace').strip()
|
|
||||||
raise EmbedThumbnailPPError(msg)
|
|
||||||
|
|
||||||
if not self._already_have_thumbnail:
|
|
||||||
os.remove(encodeFilename(thumbnail_filename))
|
|
||||||
# for formats that don't support thumbnails (like 3gp) AtomicParsley
|
|
||||||
# won't create to the temporary file
|
|
||||||
if b'No changes' in stdout:
|
|
||||||
self._downloader.report_warning('The file format doesn\'t support embedding a thumbnail')
|
|
||||||
else:
|
|
||||||
os.remove(encodeFilename(filename))
|
|
||||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
|
||||||
else:
|
|
||||||
raise EmbedThumbnailPPError('Only mp3 and m4a/mp4 are supported for thumbnail embedding for now.')
|
|
||||||
|
|
||||||
return [], info
|
|
@ -1,31 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from .common import PostProcessor
|
|
||||||
from ..compat import compat_shlex_quote
|
|
||||||
from ..utils import (
|
|
||||||
encodeArgument,
|
|
||||||
PostProcessingError,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ExecAfterDownloadPP(PostProcessor):
|
|
||||||
def __init__(self, downloader, exec_cmd):
|
|
||||||
super(ExecAfterDownloadPP, self).__init__(downloader)
|
|
||||||
self.exec_cmd = exec_cmd
|
|
||||||
|
|
||||||
def run(self, information):
|
|
||||||
cmd = self.exec_cmd
|
|
||||||
if '{}' not in cmd:
|
|
||||||
cmd += ' {}'
|
|
||||||
|
|
||||||
cmd = cmd.replace('{}', compat_shlex_quote(information['filepath']))
|
|
||||||
|
|
||||||
self._downloader.to_screen('[exec] Executing command: %s' % cmd)
|
|
||||||
retCode = subprocess.call(encodeArgument(cmd), shell=True)
|
|
||||||
if retCode != 0:
|
|
||||||
raise PostProcessingError(
|
|
||||||
'Command returned error code %d' % retCode)
|
|
||||||
|
|
||||||
return [], information
|
|
@ -1,613 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
from .common import AudioConversionError, PostProcessor
|
|
||||||
|
|
||||||
from ..compat import (
|
|
||||||
compat_subprocess_get_DEVNULL,
|
|
||||||
)
|
|
||||||
from ..utils import (
|
|
||||||
encodeArgument,
|
|
||||||
encodeFilename,
|
|
||||||
get_exe_version,
|
|
||||||
is_outdated_version,
|
|
||||||
PostProcessingError,
|
|
||||||
prepend_extension,
|
|
||||||
shell_quote,
|
|
||||||
subtitles_filename,
|
|
||||||
dfxp2srt,
|
|
||||||
ISO639Utils,
|
|
||||||
replace_extension,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
EXT_TO_OUT_FORMATS = {
|
|
||||||
'aac': 'adts',
|
|
||||||
'flac': 'flac',
|
|
||||||
'm4a': 'ipod',
|
|
||||||
'mka': 'matroska',
|
|
||||||
'mkv': 'matroska',
|
|
||||||
'mpg': 'mpeg',
|
|
||||||
'ogv': 'ogg',
|
|
||||||
'ts': 'mpegts',
|
|
||||||
'wma': 'asf',
|
|
||||||
'wmv': 'asf',
|
|
||||||
}
|
|
||||||
ACODECS = {
|
|
||||||
'mp3': 'libmp3lame',
|
|
||||||
'aac': 'aac',
|
|
||||||
'flac': 'flac',
|
|
||||||
'm4a': 'aac',
|
|
||||||
'opus': 'libopus',
|
|
||||||
'vorbis': 'libvorbis',
|
|
||||||
'wav': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegPostProcessorError(PostProcessingError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegPostProcessor(PostProcessor):
|
|
||||||
def __init__(self, downloader=None):
|
|
||||||
PostProcessor.__init__(self, downloader)
|
|
||||||
self._determine_executables()
|
|
||||||
|
|
||||||
def check_version(self):
|
|
||||||
if not self.available:
|
|
||||||
raise FFmpegPostProcessorError('ffmpeg or avconv not found. Please install one.')
|
|
||||||
|
|
||||||
required_version = '10-0' if self.basename == 'avconv' else '1.0'
|
|
||||||
if is_outdated_version(
|
|
||||||
self._versions[self.basename], required_version):
|
|
||||||
warning = 'Your copy of %s is outdated, update %s to version %s or newer if you encounter any errors.' % (
|
|
||||||
self.basename, self.basename, required_version)
|
|
||||||
if self._downloader:
|
|
||||||
self._downloader.report_warning(warning)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_versions(downloader=None):
|
|
||||||
return FFmpegPostProcessor(downloader)._versions
|
|
||||||
|
|
||||||
def _determine_executables(self):
|
|
||||||
programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe']
|
|
||||||
prefer_ffmpeg = True
|
|
||||||
|
|
||||||
self.basename = None
|
|
||||||
self.probe_basename = None
|
|
||||||
|
|
||||||
self._paths = None
|
|
||||||
self._versions = None
|
|
||||||
if self._downloader:
|
|
||||||
prefer_ffmpeg = self._downloader.params.get('prefer_ffmpeg', True)
|
|
||||||
location = self._downloader.params.get('ffmpeg_location')
|
|
||||||
if location is not None:
|
|
||||||
if not os.path.exists(location):
|
|
||||||
self._downloader.report_warning(
|
|
||||||
'ffmpeg-location %s does not exist! '
|
|
||||||
'Continuing without avconv/ffmpeg.' % (location))
|
|
||||||
self._versions = {}
|
|
||||||
return
|
|
||||||
elif not os.path.isdir(location):
|
|
||||||
basename = os.path.splitext(os.path.basename(location))[0]
|
|
||||||
if basename not in programs:
|
|
||||||
self._downloader.report_warning(
|
|
||||||
'Cannot identify executable %s, its basename should be one of %s. '
|
|
||||||
'Continuing without avconv/ffmpeg.' %
|
|
||||||
(location, ', '.join(programs)))
|
|
||||||
self._versions = {}
|
|
||||||
return None
|
|
||||||
location = os.path.dirname(os.path.abspath(location))
|
|
||||||
if basename in ('ffmpeg', 'ffprobe'):
|
|
||||||
prefer_ffmpeg = True
|
|
||||||
|
|
||||||
self._paths = dict(
|
|
||||||
(p, os.path.join(location, p)) for p in programs)
|
|
||||||
self._versions = dict(
|
|
||||||
(p, get_exe_version(self._paths[p], args=['-version']))
|
|
||||||
for p in programs)
|
|
||||||
if self._versions is None:
|
|
||||||
self._versions = dict(
|
|
||||||
(p, get_exe_version(p, args=['-version'])) for p in programs)
|
|
||||||
self._paths = dict((p, p) for p in programs)
|
|
||||||
|
|
||||||
if prefer_ffmpeg is False:
|
|
||||||
prefs = ('avconv', 'ffmpeg')
|
|
||||||
else:
|
|
||||||
prefs = ('ffmpeg', 'avconv')
|
|
||||||
for p in prefs:
|
|
||||||
if self._versions[p]:
|
|
||||||
self.basename = p
|
|
||||||
break
|
|
||||||
|
|
||||||
if prefer_ffmpeg is False:
|
|
||||||
prefs = ('avprobe', 'ffprobe')
|
|
||||||
else:
|
|
||||||
prefs = ('ffprobe', 'avprobe')
|
|
||||||
for p in prefs:
|
|
||||||
if self._versions[p]:
|
|
||||||
self.probe_basename = p
|
|
||||||
break
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self):
|
|
||||||
return self.basename is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def executable(self):
|
|
||||||
return self._paths[self.basename]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def probe_available(self):
|
|
||||||
return self.probe_basename is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def probe_executable(self):
|
|
||||||
return self._paths[self.probe_basename]
|
|
||||||
|
|
||||||
def get_audio_codec(self, path):
|
|
||||||
if not self.probe_available:
|
|
||||||
raise PostProcessingError('ffprobe or avprobe not found. Please install one.')
|
|
||||||
try:
|
|
||||||
cmd = [
|
|
||||||
encodeFilename(self.probe_executable, True),
|
|
||||||
encodeArgument('-show_streams'),
|
|
||||||
encodeFilename(self._ffmpeg_filename_argument(path), True)]
|
|
||||||
if self._downloader.params.get('verbose', False):
|
|
||||||
self._downloader.to_screen('[debug] %s command line: %s' % (self.basename, shell_quote(cmd)))
|
|
||||||
handle = subprocess.Popen(cmd, stderr=compat_subprocess_get_DEVNULL(), stdout=subprocess.PIPE, stdin=subprocess.PIPE)
|
|
||||||
output = handle.communicate()[0]
|
|
||||||
if handle.wait() != 0:
|
|
||||||
return None
|
|
||||||
except (IOError, OSError):
|
|
||||||
return None
|
|
||||||
audio_codec = None
|
|
||||||
for line in output.decode('ascii', 'ignore').split('\n'):
|
|
||||||
if line.startswith('codec_name='):
|
|
||||||
audio_codec = line.split('=')[1].strip()
|
|
||||||
elif line.strip() == 'codec_type=audio' and audio_codec is not None:
|
|
||||||
return audio_codec
|
|
||||||
return None
|
|
||||||
|
|
||||||
def run_ffmpeg_multiple_files(self, input_paths, out_path, opts):
|
|
||||||
self.check_version()
|
|
||||||
|
|
||||||
oldest_mtime = min(
|
|
||||||
os.stat(encodeFilename(path)).st_mtime for path in input_paths)
|
|
||||||
|
|
||||||
opts += self._configuration_args()
|
|
||||||
|
|
||||||
files_cmd = []
|
|
||||||
for path in input_paths:
|
|
||||||
files_cmd.extend([
|
|
||||||
encodeArgument('-i'),
|
|
||||||
encodeFilename(self._ffmpeg_filename_argument(path), True)
|
|
||||||
])
|
|
||||||
cmd = ([encodeFilename(self.executable, True), encodeArgument('-y')] +
|
|
||||||
files_cmd +
|
|
||||||
[encodeArgument(o) for o in opts] +
|
|
||||||
[encodeFilename(self._ffmpeg_filename_argument(out_path), True)])
|
|
||||||
|
|
||||||
if self._downloader.params.get('verbose', False):
|
|
||||||
self._downloader.to_screen('[debug] ffmpeg command line: %s' % shell_quote(cmd))
|
|
||||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
if p.returncode != 0:
|
|
||||||
stderr = stderr.decode('utf-8', 'replace')
|
|
||||||
msg = stderr.strip().split('\n')[-1]
|
|
||||||
raise FFmpegPostProcessorError(msg)
|
|
||||||
self.try_utime(out_path, oldest_mtime, oldest_mtime)
|
|
||||||
|
|
||||||
def run_ffmpeg(self, path, out_path, opts):
|
|
||||||
self.run_ffmpeg_multiple_files([path], out_path, opts)
|
|
||||||
|
|
||||||
def _ffmpeg_filename_argument(self, fn):
|
|
||||||
# Always use 'file:' because the filename may contain ':' (ffmpeg
|
|
||||||
# interprets that as a protocol) or can start with '-' (-- is broken in
|
|
||||||
# ffmpeg, see https://ffmpeg.org/trac/ffmpeg/ticket/2127 for details)
|
|
||||||
# Also leave '-' intact in order not to break streaming to stdout.
|
|
||||||
return 'file:' + fn if fn != '-' else fn
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegExtractAudioPP(FFmpegPostProcessor):
|
|
||||||
def __init__(self, downloader=None, preferredcodec=None, preferredquality=None, nopostoverwrites=False):
|
|
||||||
FFmpegPostProcessor.__init__(self, downloader)
|
|
||||||
if preferredcodec is None:
|
|
||||||
preferredcodec = 'best'
|
|
||||||
self._preferredcodec = preferredcodec
|
|
||||||
self._preferredquality = preferredquality
|
|
||||||
self._nopostoverwrites = nopostoverwrites
|
|
||||||
|
|
||||||
def run_ffmpeg(self, path, out_path, codec, more_opts):
|
|
||||||
if codec is None:
|
|
||||||
acodec_opts = []
|
|
||||||
else:
|
|
||||||
acodec_opts = ['-acodec', codec]
|
|
||||||
opts = ['-vn'] + acodec_opts + more_opts
|
|
||||||
try:
|
|
||||||
FFmpegPostProcessor.run_ffmpeg(self, path, out_path, opts)
|
|
||||||
except FFmpegPostProcessorError as err:
|
|
||||||
raise AudioConversionError(err.msg)
|
|
||||||
|
|
||||||
def run(self, information):
|
|
||||||
path = information['filepath']
|
|
||||||
|
|
||||||
filecodec = self.get_audio_codec(path)
|
|
||||||
if filecodec is None:
|
|
||||||
raise PostProcessingError('WARNING: unable to obtain file audio codec with ffprobe')
|
|
||||||
|
|
||||||
more_opts = []
|
|
||||||
if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
|
|
||||||
if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']:
|
|
||||||
# Lossless, but in another container
|
|
||||||
acodec = 'copy'
|
|
||||||
extension = 'm4a'
|
|
||||||
more_opts = ['-bsf:a', 'aac_adtstoasc']
|
|
||||||
elif filecodec in ['aac', 'flac', 'mp3', 'vorbis', 'opus']:
|
|
||||||
# Lossless if possible
|
|
||||||
acodec = 'copy'
|
|
||||||
extension = filecodec
|
|
||||||
if filecodec == 'aac':
|
|
||||||
more_opts = ['-f', 'adts']
|
|
||||||
if filecodec == 'vorbis':
|
|
||||||
extension = 'ogg'
|
|
||||||
else:
|
|
||||||
# MP3 otherwise.
|
|
||||||
acodec = 'libmp3lame'
|
|
||||||
extension = 'mp3'
|
|
||||||
more_opts = []
|
|
||||||
if self._preferredquality is not None:
|
|
||||||
if int(self._preferredquality) < 10:
|
|
||||||
more_opts += ['-q:a', self._preferredquality]
|
|
||||||
else:
|
|
||||||
more_opts += ['-b:a', self._preferredquality + 'k']
|
|
||||||
else:
|
|
||||||
# We convert the audio (lossy if codec is lossy)
|
|
||||||
acodec = ACODECS[self._preferredcodec]
|
|
||||||
extension = self._preferredcodec
|
|
||||||
more_opts = []
|
|
||||||
if self._preferredquality is not None:
|
|
||||||
# The opus codec doesn't support the -aq option
|
|
||||||
if int(self._preferredquality) < 10 and extension != 'opus':
|
|
||||||
more_opts += ['-q:a', self._preferredquality]
|
|
||||||
else:
|
|
||||||
more_opts += ['-b:a', self._preferredquality + 'k']
|
|
||||||
if self._preferredcodec == 'aac':
|
|
||||||
more_opts += ['-f', 'adts']
|
|
||||||
if self._preferredcodec == 'm4a':
|
|
||||||
more_opts += ['-bsf:a', 'aac_adtstoasc']
|
|
||||||
if self._preferredcodec == 'vorbis':
|
|
||||||
extension = 'ogg'
|
|
||||||
if self._preferredcodec == 'wav':
|
|
||||||
extension = 'wav'
|
|
||||||
more_opts += ['-f', 'wav']
|
|
||||||
|
|
||||||
prefix, sep, ext = path.rpartition('.') # not os.path.splitext, since the latter does not work on unicode in all setups
|
|
||||||
new_path = prefix + sep + extension
|
|
||||||
|
|
||||||
information['filepath'] = new_path
|
|
||||||
information['ext'] = extension
|
|
||||||
|
|
||||||
# If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
|
|
||||||
if (new_path == path or
|
|
||||||
(self._nopostoverwrites and os.path.exists(encodeFilename(new_path)))):
|
|
||||||
self._downloader.to_screen('[ffmpeg] Post-process file %s exists, skipping' % new_path)
|
|
||||||
return [], information
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._downloader.to_screen('[ffmpeg] Destination: ' + new_path)
|
|
||||||
self.run_ffmpeg(path, new_path, acodec, more_opts)
|
|
||||||
except AudioConversionError as e:
|
|
||||||
raise PostProcessingError(
|
|
||||||
'audio conversion failed: ' + e.msg)
|
|
||||||
except Exception:
|
|
||||||
raise PostProcessingError('error running ' + self.basename)
|
|
||||||
|
|
||||||
# Try to update the date time for extracted audio file.
|
|
||||||
if information.get('filetime') is not None:
|
|
||||||
self.try_utime(
|
|
||||||
new_path, time.time(), information['filetime'],
|
|
||||||
errnote='Cannot update utime of audio file')
|
|
||||||
|
|
||||||
return [path], information
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegVideoConvertorPP(FFmpegPostProcessor):
|
|
||||||
def __init__(self, downloader=None, preferedformat=None):
|
|
||||||
super(FFmpegVideoConvertorPP, self).__init__(downloader)
|
|
||||||
self._preferedformat = preferedformat
|
|
||||||
|
|
||||||
def run(self, information):
|
|
||||||
path = information['filepath']
|
|
||||||
if information['ext'] == self._preferedformat:
|
|
||||||
self._downloader.to_screen('[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
|
|
||||||
return [], information
|
|
||||||
options = []
|
|
||||||
if self._preferedformat == 'avi':
|
|
||||||
options.extend(['-c:v', 'libxvid', '-vtag', 'XVID'])
|
|
||||||
prefix, sep, ext = path.rpartition('.')
|
|
||||||
outpath = prefix + sep + self._preferedformat
|
|
||||||
self._downloader.to_screen('[' + 'ffmpeg' + '] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) + outpath)
|
|
||||||
self.run_ffmpeg(path, outpath, options)
|
|
||||||
information['filepath'] = outpath
|
|
||||||
information['format'] = self._preferedformat
|
|
||||||
information['ext'] = self._preferedformat
|
|
||||||
return [path], information
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
|
|
||||||
def run(self, information):
|
|
||||||
if information['ext'] not in ('mp4', 'webm', 'mkv'):
|
|
||||||
self._downloader.to_screen('[ffmpeg] Subtitles can only be embedded in mp4, webm or mkv files')
|
|
||||||
return [], information
|
|
||||||
subtitles = information.get('requested_subtitles')
|
|
||||||
if not subtitles:
|
|
||||||
self._downloader.to_screen('[ffmpeg] There aren\'t any subtitles to embed')
|
|
||||||
return [], information
|
|
||||||
|
|
||||||
filename = information['filepath']
|
|
||||||
|
|
||||||
ext = information['ext']
|
|
||||||
sub_langs = []
|
|
||||||
sub_filenames = []
|
|
||||||
webm_vtt_warn = False
|
|
||||||
|
|
||||||
for lang, sub_info in subtitles.items():
|
|
||||||
sub_ext = sub_info['ext']
|
|
||||||
if ext != 'webm' or ext == 'webm' and sub_ext == 'vtt':
|
|
||||||
sub_langs.append(lang)
|
|
||||||
sub_filenames.append(subtitles_filename(filename, lang, sub_ext))
|
|
||||||
else:
|
|
||||||
if not webm_vtt_warn and ext == 'webm' and sub_ext != 'vtt':
|
|
||||||
webm_vtt_warn = True
|
|
||||||
self._downloader.to_screen('[ffmpeg] Only WebVTT subtitles can be embedded in webm files')
|
|
||||||
|
|
||||||
if not sub_langs:
|
|
||||||
return [], information
|
|
||||||
|
|
||||||
input_files = [filename] + sub_filenames
|
|
||||||
|
|
||||||
opts = [
|
|
||||||
'-map', '0',
|
|
||||||
'-c', 'copy',
|
|
||||||
# Don't copy the existing subtitles, we may be running the
|
|
||||||
# postprocessor a second time
|
|
||||||
'-map', '-0:s',
|
|
||||||
]
|
|
||||||
if information['ext'] == 'mp4':
|
|
||||||
opts += ['-c:s', 'mov_text']
|
|
||||||
for (i, lang) in enumerate(sub_langs):
|
|
||||||
opts.extend(['-map', '%d:0' % (i + 1)])
|
|
||||||
lang_code = ISO639Utils.short2long(lang)
|
|
||||||
if lang_code is not None:
|
|
||||||
opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code])
|
|
||||||
|
|
||||||
temp_filename = prepend_extension(filename, 'temp')
|
|
||||||
self._downloader.to_screen('[ffmpeg] Embedding subtitles in \'%s\'' % filename)
|
|
||||||
self.run_ffmpeg_multiple_files(input_files, temp_filename, opts)
|
|
||||||
os.remove(encodeFilename(filename))
|
|
||||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
|
||||||
|
|
||||||
return sub_filenames, information
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegMetadataPP(FFmpegPostProcessor):
|
|
||||||
def run(self, info):
|
|
||||||
metadata = {}
|
|
||||||
|
|
||||||
def add(meta_list, info_list=None):
|
|
||||||
if not info_list:
|
|
||||||
info_list = meta_list
|
|
||||||
if not isinstance(meta_list, (list, tuple)):
|
|
||||||
meta_list = (meta_list,)
|
|
||||||
if not isinstance(info_list, (list, tuple)):
|
|
||||||
info_list = (info_list,)
|
|
||||||
for info_f in info_list:
|
|
||||||
if info.get(info_f) is not None:
|
|
||||||
for meta_f in meta_list:
|
|
||||||
metadata[meta_f] = info[info_f]
|
|
||||||
break
|
|
||||||
|
|
||||||
add('title', ('track', 'title'))
|
|
||||||
add('date', 'upload_date')
|
|
||||||
add(('description', 'comment'), 'description')
|
|
||||||
add('purl', 'webpage_url')
|
|
||||||
add('track', 'track_number')
|
|
||||||
add('artist', ('artist', 'creator', 'uploader', 'uploader_id'))
|
|
||||||
add('genre')
|
|
||||||
add('album')
|
|
||||||
add('album_artist')
|
|
||||||
add('disc', 'disc_number')
|
|
||||||
|
|
||||||
if not metadata:
|
|
||||||
self._downloader.to_screen('[ffmpeg] There isn\'t any metadata to add')
|
|
||||||
return [], info
|
|
||||||
|
|
||||||
filename = info['filepath']
|
|
||||||
temp_filename = prepend_extension(filename, 'temp')
|
|
||||||
in_filenames = [filename]
|
|
||||||
options = []
|
|
||||||
|
|
||||||
if info['ext'] == 'm4a':
|
|
||||||
options.extend(['-vn', '-acodec', 'copy'])
|
|
||||||
else:
|
|
||||||
options.extend(['-c', 'copy'])
|
|
||||||
|
|
||||||
for (name, value) in metadata.items():
|
|
||||||
options.extend(['-metadata', '%s=%s' % (name, value)])
|
|
||||||
|
|
||||||
chapters = info.get('chapters', [])
|
|
||||||
if chapters:
|
|
||||||
metadata_filename = replace_extension(filename, 'meta')
|
|
||||||
with io.open(metadata_filename, 'wt', encoding='utf-8') as f:
|
|
||||||
def ffmpeg_escape(text):
|
|
||||||
return re.sub(r'(=|;|#|\\|\n)', r'\\\1', text)
|
|
||||||
|
|
||||||
metadata_file_content = ';FFMETADATA1\n'
|
|
||||||
for chapter in chapters:
|
|
||||||
metadata_file_content += '[CHAPTER]\nTIMEBASE=1/1000\n'
|
|
||||||
metadata_file_content += 'START=%d\n' % (chapter['start_time'] * 1000)
|
|
||||||
metadata_file_content += 'END=%d\n' % (chapter['end_time'] * 1000)
|
|
||||||
chapter_title = chapter.get('title')
|
|
||||||
if chapter_title:
|
|
||||||
metadata_file_content += 'title=%s\n' % ffmpeg_escape(chapter_title)
|
|
||||||
f.write(metadata_file_content)
|
|
||||||
in_filenames.append(metadata_filename)
|
|
||||||
options.extend(['-map_metadata', '1'])
|
|
||||||
|
|
||||||
self._downloader.to_screen('[ffmpeg] Adding metadata to \'%s\'' % filename)
|
|
||||||
self.run_ffmpeg_multiple_files(in_filenames, temp_filename, options)
|
|
||||||
if chapters:
|
|
||||||
os.remove(metadata_filename)
|
|
||||||
os.remove(encodeFilename(filename))
|
|
||||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
|
||||||
return [], info
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegMergerPP(FFmpegPostProcessor):
|
|
||||||
def run(self, info):
|
|
||||||
filename = info['filepath']
|
|
||||||
temp_filename = prepend_extension(filename, 'temp')
|
|
||||||
args = ['-c', 'copy', '-map', '0:v:0', '-map', '1:a:0']
|
|
||||||
self._downloader.to_screen('[ffmpeg] Merging formats into "%s"' % filename)
|
|
||||||
self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args)
|
|
||||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
|
||||||
return info['__files_to_merge'], info
|
|
||||||
|
|
||||||
def can_merge(self):
|
|
||||||
# TODO: figure out merge-capable ffmpeg version
|
|
||||||
if self.basename != 'avconv':
|
|
||||||
return True
|
|
||||||
|
|
||||||
required_version = '10-0'
|
|
||||||
if is_outdated_version(
|
|
||||||
self._versions[self.basename], required_version):
|
|
||||||
warning = ('Your copy of %s is outdated and unable to properly mux separate video and audio files, '
|
|
||||||
'youtube-dl will download single file media. '
|
|
||||||
'Update %s to version %s or newer to fix this.') % (
|
|
||||||
self.basename, self.basename, required_version)
|
|
||||||
if self._downloader:
|
|
||||||
self._downloader.report_warning(warning)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegFixupStretchedPP(FFmpegPostProcessor):
|
|
||||||
def run(self, info):
|
|
||||||
stretched_ratio = info.get('stretched_ratio')
|
|
||||||
if stretched_ratio is None or stretched_ratio == 1:
|
|
||||||
return [], info
|
|
||||||
|
|
||||||
filename = info['filepath']
|
|
||||||
temp_filename = prepend_extension(filename, 'temp')
|
|
||||||
|
|
||||||
options = ['-c', 'copy', '-aspect', '%f' % stretched_ratio]
|
|
||||||
self._downloader.to_screen('[ffmpeg] Fixing aspect ratio in "%s"' % filename)
|
|
||||||
self.run_ffmpeg(filename, temp_filename, options)
|
|
||||||
|
|
||||||
os.remove(encodeFilename(filename))
|
|
||||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
|
||||||
|
|
||||||
return [], info
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegFixupM4aPP(FFmpegPostProcessor):
|
|
||||||
def run(self, info):
|
|
||||||
if info.get('container') != 'm4a_dash':
|
|
||||||
return [], info
|
|
||||||
|
|
||||||
filename = info['filepath']
|
|
||||||
temp_filename = prepend_extension(filename, 'temp')
|
|
||||||
|
|
||||||
options = ['-c', 'copy', '-f', 'mp4']
|
|
||||||
self._downloader.to_screen('[ffmpeg] Correcting container in "%s"' % filename)
|
|
||||||
self.run_ffmpeg(filename, temp_filename, options)
|
|
||||||
|
|
||||||
os.remove(encodeFilename(filename))
|
|
||||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
|
||||||
|
|
||||||
return [], info
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegFixupM3u8PP(FFmpegPostProcessor):
|
|
||||||
def run(self, info):
|
|
||||||
filename = info['filepath']
|
|
||||||
if self.get_audio_codec(filename) == 'aac':
|
|
||||||
temp_filename = prepend_extension(filename, 'temp')
|
|
||||||
|
|
||||||
options = ['-c', 'copy', '-f', 'mp4', '-bsf:a', 'aac_adtstoasc']
|
|
||||||
self._downloader.to_screen('[ffmpeg] Fixing malformed AAC bitstream in "%s"' % filename)
|
|
||||||
self.run_ffmpeg(filename, temp_filename, options)
|
|
||||||
|
|
||||||
os.remove(encodeFilename(filename))
|
|
||||||
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
|
|
||||||
return [], info
|
|
||||||
|
|
||||||
|
|
||||||
class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
|
|
||||||
def __init__(self, downloader=None, format=None):
|
|
||||||
super(FFmpegSubtitlesConvertorPP, self).__init__(downloader)
|
|
||||||
self.format = format
|
|
||||||
|
|
||||||
def run(self, info):
|
|
||||||
subs = info.get('requested_subtitles')
|
|
||||||
filename = info['filepath']
|
|
||||||
new_ext = self.format
|
|
||||||
new_format = new_ext
|
|
||||||
if new_format == 'vtt':
|
|
||||||
new_format = 'webvtt'
|
|
||||||
if subs is None:
|
|
||||||
self._downloader.to_screen('[ffmpeg] There aren\'t any subtitles to convert')
|
|
||||||
return [], info
|
|
||||||
self._downloader.to_screen('[ffmpeg] Converting subtitles')
|
|
||||||
sub_filenames = []
|
|
||||||
for lang, sub in subs.items():
|
|
||||||
ext = sub['ext']
|
|
||||||
if ext == new_ext:
|
|
||||||
self._downloader.to_screen(
|
|
||||||
'[ffmpeg] Subtitle file for %s is already in the requested format' % new_ext)
|
|
||||||
continue
|
|
||||||
old_file = subtitles_filename(filename, lang, ext)
|
|
||||||
sub_filenames.append(old_file)
|
|
||||||
new_file = subtitles_filename(filename, lang, new_ext)
|
|
||||||
|
|
||||||
if ext in ('dfxp', 'ttml', 'tt'):
|
|
||||||
self._downloader.report_warning(
|
|
||||||
'You have requested to convert dfxp (TTML) subtitles into another format, '
|
|
||||||
'which results in style information loss')
|
|
||||||
|
|
||||||
dfxp_file = old_file
|
|
||||||
srt_file = subtitles_filename(filename, lang, 'srt')
|
|
||||||
|
|
||||||
with open(dfxp_file, 'rb') as f:
|
|
||||||
srt_data = dfxp2srt(f.read())
|
|
||||||
|
|
||||||
with io.open(srt_file, 'wt', encoding='utf-8') as f:
|
|
||||||
f.write(srt_data)
|
|
||||||
old_file = srt_file
|
|
||||||
|
|
||||||
subs[lang] = {
|
|
||||||
'ext': 'srt',
|
|
||||||
'data': srt_data
|
|
||||||
}
|
|
||||||
|
|
||||||
if new_ext == 'srt':
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
sub_filenames.append(srt_file)
|
|
||||||
|
|
||||||
self.run_ffmpeg(old_file, new_file, ['-f', new_format])
|
|
||||||
|
|
||||||
with io.open(new_file, 'rt', encoding='utf-8') as f:
|
|
||||||
subs[lang] = {
|
|
||||||
'ext': new_ext,
|
|
||||||
'data': f.read(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return sub_filenames, info
|
|
@ -1,48 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import PostProcessor
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataFromTitlePP(PostProcessor):
|
|
||||||
def __init__(self, downloader, titleformat):
|
|
||||||
super(MetadataFromTitlePP, self).__init__(downloader)
|
|
||||||
self._titleformat = titleformat
|
|
||||||
self._titleregex = (self.format_to_regex(titleformat)
|
|
||||||
if re.search(r'%\(\w+\)s', titleformat)
|
|
||||||
else titleformat)
|
|
||||||
|
|
||||||
def format_to_regex(self, fmt):
|
|
||||||
r"""
|
|
||||||
Converts a string like
|
|
||||||
'%(title)s - %(artist)s'
|
|
||||||
to a regex like
|
|
||||||
'(?P<title>.+)\ \-\ (?P<artist>.+)'
|
|
||||||
"""
|
|
||||||
lastpos = 0
|
|
||||||
regex = ''
|
|
||||||
# replace %(..)s with regex group and escape other string parts
|
|
||||||
for match in re.finditer(r'%\((\w+)\)s', fmt):
|
|
||||||
regex += re.escape(fmt[lastpos:match.start()])
|
|
||||||
regex += r'(?P<' + match.group(1) + '>.+)'
|
|
||||||
lastpos = match.end()
|
|
||||||
if lastpos < len(fmt):
|
|
||||||
regex += re.escape(fmt[lastpos:])
|
|
||||||
return regex
|
|
||||||
|
|
||||||
def run(self, info):
|
|
||||||
title = info['title']
|
|
||||||
match = re.match(self._titleregex, title)
|
|
||||||
if match is None:
|
|
||||||
self._downloader.to_screen(
|
|
||||||
'[fromtitle] Could not interpret title of video as "%s"'
|
|
||||||
% self._titleformat)
|
|
||||||
return [], info
|
|
||||||
for attribute, value in match.groupdict().items():
|
|
||||||
info[attribute] = value
|
|
||||||
self._downloader.to_screen(
|
|
||||||
'[fromtitle] parsed %s: %s'
|
|
||||||
% (attribute, value if value is not None else 'NA'))
|
|
||||||
|
|
||||||
return [], info
|
|
@ -1,79 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from .common import PostProcessor
|
|
||||||
from ..compat import compat_os_name
|
|
||||||
from ..utils import (
|
|
||||||
hyphenate_date,
|
|
||||||
write_xattr,
|
|
||||||
XAttrMetadataError,
|
|
||||||
XAttrUnavailableError,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class XAttrMetadataPP(PostProcessor):
|
|
||||||
|
|
||||||
#
|
|
||||||
# More info about extended attributes for media:
|
|
||||||
# http://freedesktop.org/wiki/CommonExtendedAttributes/
|
|
||||||
# http://www.freedesktop.org/wiki/PhreedomDraft/
|
|
||||||
# http://dublincore.org/documents/usageguide/elements.shtml
|
|
||||||
#
|
|
||||||
# TODO:
|
|
||||||
# * capture youtube keywords and put them in 'user.dublincore.subject' (comma-separated)
|
|
||||||
# * figure out which xattrs can be used for 'duration', 'thumbnail', 'resolution'
|
|
||||||
#
|
|
||||||
|
|
||||||
def run(self, info):
|
|
||||||
""" Set extended attributes on downloaded file (if xattr support is found). """
|
|
||||||
|
|
||||||
# Write the metadata to the file's xattrs
|
|
||||||
self._downloader.to_screen('[metadata] Writing metadata to file\'s xattrs')
|
|
||||||
|
|
||||||
filename = info['filepath']
|
|
||||||
|
|
||||||
try:
|
|
||||||
xattr_mapping = {
|
|
||||||
'user.xdg.referrer.url': 'webpage_url',
|
|
||||||
# 'user.xdg.comment': 'description',
|
|
||||||
'user.dublincore.title': 'title',
|
|
||||||
'user.dublincore.date': 'upload_date',
|
|
||||||
'user.dublincore.description': 'description',
|
|
||||||
'user.dublincore.contributor': 'uploader',
|
|
||||||
'user.dublincore.format': 'format',
|
|
||||||
}
|
|
||||||
|
|
||||||
num_written = 0
|
|
||||||
for xattrname, infoname in xattr_mapping.items():
|
|
||||||
|
|
||||||
value = info.get(infoname)
|
|
||||||
|
|
||||||
if value:
|
|
||||||
if infoname == 'upload_date':
|
|
||||||
value = hyphenate_date(value)
|
|
||||||
|
|
||||||
byte_value = value.encode('utf-8')
|
|
||||||
write_xattr(filename, xattrname, byte_value)
|
|
||||||
num_written += 1
|
|
||||||
|
|
||||||
return [], info
|
|
||||||
|
|
||||||
except XAttrUnavailableError as e:
|
|
||||||
self._downloader.report_error(str(e))
|
|
||||||
return [], info
|
|
||||||
|
|
||||||
except XAttrMetadataError as e:
|
|
||||||
if e.reason == 'NO_SPACE':
|
|
||||||
self._downloader.report_warning(
|
|
||||||
'There\'s no disk space left, disk quota exceeded or filesystem xattr limit exceeded. ' +
|
|
||||||
(('Some ' if num_written else '') + 'extended attributes are not written.').capitalize())
|
|
||||||
elif e.reason == 'VALUE_TOO_LONG':
|
|
||||||
self._downloader.report_warning(
|
|
||||||
'Unable to write extended attributes due to too long values.')
|
|
||||||
else:
|
|
||||||
msg = 'This filesystem doesn\'t support extended attributes. '
|
|
||||||
if compat_os_name == 'nt':
|
|
||||||
msg += 'You need to use NTFS.'
|
|
||||||
else:
|
|
||||||
msg += '(You may have to enable them in your /etc/fstab)'
|
|
||||||
self._downloader.report_error(msg)
|
|
||||||
return [], info
|
|
@ -1,273 +0,0 @@
|
|||||||
# Public Domain SOCKS proxy protocol implementation
|
|
||||||
# Adapted from https://gist.github.com/bluec0re/cafd3764412967417fd3
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
# References:
|
|
||||||
# SOCKS4 protocol http://www.openssh.com/txt/socks4.protocol
|
|
||||||
# SOCKS4A protocol http://www.openssh.com/txt/socks4a.protocol
|
|
||||||
# SOCKS5 protocol https://tools.ietf.org/html/rfc1928
|
|
||||||
# SOCKS5 username/password authentication https://tools.ietf.org/html/rfc1929
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import socket
|
|
||||||
|
|
||||||
from .compat import (
|
|
||||||
compat_ord,
|
|
||||||
compat_struct_pack,
|
|
||||||
compat_struct_unpack,
|
|
||||||
)
|
|
||||||
|
|
||||||
__author__ = 'Timo Schmid <coding@timoschmid.de>'
|
|
||||||
|
|
||||||
SOCKS4_VERSION = 4
|
|
||||||
SOCKS4_REPLY_VERSION = 0x00
|
|
||||||
# Excerpt from SOCKS4A protocol:
|
|
||||||
# if the client cannot resolve the destination host's domain name to find its
|
|
||||||
# IP address, it should set the first three bytes of DSTIP to NULL and the last
|
|
||||||
# byte to a non-zero value.
|
|
||||||
SOCKS4_DEFAULT_DSTIP = compat_struct_pack('!BBBB', 0, 0, 0, 0xFF)
|
|
||||||
|
|
||||||
SOCKS5_VERSION = 5
|
|
||||||
SOCKS5_USER_AUTH_VERSION = 0x01
|
|
||||||
SOCKS5_USER_AUTH_SUCCESS = 0x00
|
|
||||||
|
|
||||||
|
|
||||||
class Socks4Command(object):
|
|
||||||
CMD_CONNECT = 0x01
|
|
||||||
CMD_BIND = 0x02
|
|
||||||
|
|
||||||
|
|
||||||
class Socks5Command(Socks4Command):
|
|
||||||
CMD_UDP_ASSOCIATE = 0x03
|
|
||||||
|
|
||||||
|
|
||||||
class Socks5Auth(object):
|
|
||||||
AUTH_NONE = 0x00
|
|
||||||
AUTH_GSSAPI = 0x01
|
|
||||||
AUTH_USER_PASS = 0x02
|
|
||||||
AUTH_NO_ACCEPTABLE = 0xFF # For server response
|
|
||||||
|
|
||||||
|
|
||||||
class Socks5AddressType(object):
|
|
||||||
ATYP_IPV4 = 0x01
|
|
||||||
ATYP_DOMAINNAME = 0x03
|
|
||||||
ATYP_IPV6 = 0x04
|
|
||||||
|
|
||||||
|
|
||||||
class ProxyError(socket.error):
|
|
||||||
ERR_SUCCESS = 0x00
|
|
||||||
|
|
||||||
def __init__(self, code=None, msg=None):
|
|
||||||
if code is not None and msg is None:
|
|
||||||
msg = self.CODES.get(code) or 'unknown error'
|
|
||||||
super(ProxyError, self).__init__(code, msg)
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidVersionError(ProxyError):
|
|
||||||
def __init__(self, expected_version, got_version):
|
|
||||||
msg = ('Invalid response version from server. Expected {0:02x} got '
|
|
||||||
'{1:02x}'.format(expected_version, got_version))
|
|
||||||
super(InvalidVersionError, self).__init__(0, msg)
|
|
||||||
|
|
||||||
|
|
||||||
class Socks4Error(ProxyError):
|
|
||||||
ERR_SUCCESS = 90
|
|
||||||
|
|
||||||
CODES = {
|
|
||||||
91: 'request rejected or failed',
|
|
||||||
92: 'request rejected because SOCKS server cannot connect to identd on the client',
|
|
||||||
93: 'request rejected because the client program and identd report different user-ids'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Socks5Error(ProxyError):
|
|
||||||
ERR_GENERAL_FAILURE = 0x01
|
|
||||||
|
|
||||||
CODES = {
|
|
||||||
0x01: 'general SOCKS server failure',
|
|
||||||
0x02: 'connection not allowed by ruleset',
|
|
||||||
0x03: 'Network unreachable',
|
|
||||||
0x04: 'Host unreachable',
|
|
||||||
0x05: 'Connection refused',
|
|
||||||
0x06: 'TTL expired',
|
|
||||||
0x07: 'Command not supported',
|
|
||||||
0x08: 'Address type not supported',
|
|
||||||
0xFE: 'unknown username or invalid password',
|
|
||||||
0xFF: 'all offered authentication methods were rejected'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ProxyType(object):
|
|
||||||
SOCKS4 = 0
|
|
||||||
SOCKS4A = 1
|
|
||||||
SOCKS5 = 2
|
|
||||||
|
|
||||||
|
|
||||||
Proxy = collections.namedtuple('Proxy', (
|
|
||||||
'type', 'host', 'port', 'username', 'password', 'remote_dns'))
|
|
||||||
|
|
||||||
|
|
||||||
class sockssocket(socket.socket):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._proxy = None
|
|
||||||
super(sockssocket, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def setproxy(self, proxytype, addr, port, rdns=True, username=None, password=None):
|
|
||||||
assert proxytype in (ProxyType.SOCKS4, ProxyType.SOCKS4A, ProxyType.SOCKS5)
|
|
||||||
|
|
||||||
self._proxy = Proxy(proxytype, addr, port, username, password, rdns)
|
|
||||||
|
|
||||||
def recvall(self, cnt):
|
|
||||||
data = b''
|
|
||||||
while len(data) < cnt:
|
|
||||||
cur = self.recv(cnt - len(data))
|
|
||||||
if not cur:
|
|
||||||
raise EOFError('{0} bytes missing'.format(cnt - len(data)))
|
|
||||||
data += cur
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _recv_bytes(self, cnt):
|
|
||||||
data = self.recvall(cnt)
|
|
||||||
return compat_struct_unpack('!{0}B'.format(cnt), data)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _len_and_data(data):
|
|
||||||
return compat_struct_pack('!B', len(data)) + data
|
|
||||||
|
|
||||||
def _check_response_version(self, expected_version, got_version):
|
|
||||||
if got_version != expected_version:
|
|
||||||
self.close()
|
|
||||||
raise InvalidVersionError(expected_version, got_version)
|
|
||||||
|
|
||||||
def _resolve_address(self, destaddr, default, use_remote_dns):
|
|
||||||
try:
|
|
||||||
return socket.inet_aton(destaddr)
|
|
||||||
except socket.error:
|
|
||||||
if use_remote_dns and self._proxy.remote_dns:
|
|
||||||
return default
|
|
||||||
else:
|
|
||||||
return socket.inet_aton(socket.gethostbyname(destaddr))
|
|
||||||
|
|
||||||
def _setup_socks4(self, address, is_4a=False):
|
|
||||||
destaddr, port = address
|
|
||||||
|
|
||||||
ipaddr = self._resolve_address(destaddr, SOCKS4_DEFAULT_DSTIP, use_remote_dns=is_4a)
|
|
||||||
|
|
||||||
packet = compat_struct_pack('!BBH', SOCKS4_VERSION, Socks4Command.CMD_CONNECT, port) + ipaddr
|
|
||||||
|
|
||||||
username = (self._proxy.username or '').encode('utf-8')
|
|
||||||
packet += username + b'\x00'
|
|
||||||
|
|
||||||
if is_4a and self._proxy.remote_dns:
|
|
||||||
packet += destaddr.encode('utf-8') + b'\x00'
|
|
||||||
|
|
||||||
self.sendall(packet)
|
|
||||||
|
|
||||||
version, resp_code, dstport, dsthost = compat_struct_unpack('!BBHI', self.recvall(8))
|
|
||||||
|
|
||||||
self._check_response_version(SOCKS4_REPLY_VERSION, version)
|
|
||||||
|
|
||||||
if resp_code != Socks4Error.ERR_SUCCESS:
|
|
||||||
self.close()
|
|
||||||
raise Socks4Error(resp_code)
|
|
||||||
|
|
||||||
return (dsthost, dstport)
|
|
||||||
|
|
||||||
def _setup_socks4a(self, address):
|
|
||||||
self._setup_socks4(address, is_4a=True)
|
|
||||||
|
|
||||||
def _socks5_auth(self):
|
|
||||||
packet = compat_struct_pack('!B', SOCKS5_VERSION)
|
|
||||||
|
|
||||||
auth_methods = [Socks5Auth.AUTH_NONE]
|
|
||||||
if self._proxy.username and self._proxy.password:
|
|
||||||
auth_methods.append(Socks5Auth.AUTH_USER_PASS)
|
|
||||||
|
|
||||||
packet += compat_struct_pack('!B', len(auth_methods))
|
|
||||||
packet += compat_struct_pack('!{0}B'.format(len(auth_methods)), *auth_methods)
|
|
||||||
|
|
||||||
self.sendall(packet)
|
|
||||||
|
|
||||||
version, method = self._recv_bytes(2)
|
|
||||||
|
|
||||||
self._check_response_version(SOCKS5_VERSION, version)
|
|
||||||
|
|
||||||
if method == Socks5Auth.AUTH_NO_ACCEPTABLE or (
|
|
||||||
method == Socks5Auth.AUTH_USER_PASS and (not self._proxy.username or not self._proxy.password)):
|
|
||||||
self.close()
|
|
||||||
raise Socks5Error(Socks5Auth.AUTH_NO_ACCEPTABLE)
|
|
||||||
|
|
||||||
if method == Socks5Auth.AUTH_USER_PASS:
|
|
||||||
username = self._proxy.username.encode('utf-8')
|
|
||||||
password = self._proxy.password.encode('utf-8')
|
|
||||||
packet = compat_struct_pack('!B', SOCKS5_USER_AUTH_VERSION)
|
|
||||||
packet += self._len_and_data(username) + self._len_and_data(password)
|
|
||||||
self.sendall(packet)
|
|
||||||
|
|
||||||
version, status = self._recv_bytes(2)
|
|
||||||
|
|
||||||
self._check_response_version(SOCKS5_USER_AUTH_VERSION, version)
|
|
||||||
|
|
||||||
if status != SOCKS5_USER_AUTH_SUCCESS:
|
|
||||||
self.close()
|
|
||||||
raise Socks5Error(Socks5Error.ERR_GENERAL_FAILURE)
|
|
||||||
|
|
||||||
def _setup_socks5(self, address):
|
|
||||||
destaddr, port = address
|
|
||||||
|
|
||||||
ipaddr = self._resolve_address(destaddr, None, use_remote_dns=True)
|
|
||||||
|
|
||||||
self._socks5_auth()
|
|
||||||
|
|
||||||
reserved = 0
|
|
||||||
packet = compat_struct_pack('!BBB', SOCKS5_VERSION, Socks5Command.CMD_CONNECT, reserved)
|
|
||||||
if ipaddr is None:
|
|
||||||
destaddr = destaddr.encode('utf-8')
|
|
||||||
packet += compat_struct_pack('!B', Socks5AddressType.ATYP_DOMAINNAME)
|
|
||||||
packet += self._len_and_data(destaddr)
|
|
||||||
else:
|
|
||||||
packet += compat_struct_pack('!B', Socks5AddressType.ATYP_IPV4) + ipaddr
|
|
||||||
packet += compat_struct_pack('!H', port)
|
|
||||||
|
|
||||||
self.sendall(packet)
|
|
||||||
|
|
||||||
version, status, reserved, atype = self._recv_bytes(4)
|
|
||||||
|
|
||||||
self._check_response_version(SOCKS5_VERSION, version)
|
|
||||||
|
|
||||||
if status != Socks5Error.ERR_SUCCESS:
|
|
||||||
self.close()
|
|
||||||
raise Socks5Error(status)
|
|
||||||
|
|
||||||
if atype == Socks5AddressType.ATYP_IPV4:
|
|
||||||
destaddr = self.recvall(4)
|
|
||||||
elif atype == Socks5AddressType.ATYP_DOMAINNAME:
|
|
||||||
alen = compat_ord(self.recv(1))
|
|
||||||
destaddr = self.recvall(alen)
|
|
||||||
elif atype == Socks5AddressType.ATYP_IPV6:
|
|
||||||
destaddr = self.recvall(16)
|
|
||||||
destport = compat_struct_unpack('!H', self.recvall(2))[0]
|
|
||||||
|
|
||||||
return (destaddr, destport)
|
|
||||||
|
|
||||||
def _make_proxy(self, connect_func, address):
|
|
||||||
if not self._proxy:
|
|
||||||
return connect_func(self, address)
|
|
||||||
|
|
||||||
result = connect_func(self, (self._proxy.host, self._proxy.port))
|
|
||||||
if result != 0 and result is not None:
|
|
||||||
return result
|
|
||||||
setup_funcs = {
|
|
||||||
ProxyType.SOCKS4: self._setup_socks4,
|
|
||||||
ProxyType.SOCKS4A: self._setup_socks4a,
|
|
||||||
ProxyType.SOCKS5: self._setup_socks5,
|
|
||||||
}
|
|
||||||
setup_funcs[self._proxy.type](address)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def connect(self, address):
|
|
||||||
self._make_proxy(socket.socket.connect, address)
|
|
||||||
|
|
||||||
def connect_ex(self, address):
|
|
||||||
return self._make_proxy(socket.socket.connect_ex, address)
|
|
@ -1,834 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import io
|
|
||||||
import zlib
|
|
||||||
|
|
||||||
from .compat import (
|
|
||||||
compat_str,
|
|
||||||
compat_struct_unpack,
|
|
||||||
)
|
|
||||||
from .utils import (
|
|
||||||
ExtractorError,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_tags(file_contents):
|
|
||||||
if file_contents[1:3] != b'WS':
|
|
||||||
raise ExtractorError(
|
|
||||||
'Not an SWF file; header is %r' % file_contents[:3])
|
|
||||||
if file_contents[:1] == b'C':
|
|
||||||
content = zlib.decompress(file_contents[8:])
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Unsupported compression format %r' %
|
|
||||||
file_contents[:1])
|
|
||||||
|
|
||||||
# Determine number of bits in framesize rectangle
|
|
||||||
framesize_nbits = compat_struct_unpack('!B', content[:1])[0] >> 3
|
|
||||||
framesize_len = (5 + 4 * framesize_nbits + 7) // 8
|
|
||||||
|
|
||||||
pos = framesize_len + 2 + 2
|
|
||||||
while pos < len(content):
|
|
||||||
header16 = compat_struct_unpack('<H', content[pos:pos + 2])[0]
|
|
||||||
pos += 2
|
|
||||||
tag_code = header16 >> 6
|
|
||||||
tag_len = header16 & 0x3f
|
|
||||||
if tag_len == 0x3f:
|
|
||||||
tag_len = compat_struct_unpack('<I', content[pos:pos + 4])[0]
|
|
||||||
pos += 4
|
|
||||||
assert pos + tag_len <= len(content), \
|
|
||||||
('Tag %d ends at %d+%d - that\'s longer than the file (%d)'
|
|
||||||
% (tag_code, pos, tag_len, len(content)))
|
|
||||||
yield (tag_code, content[pos:pos + tag_len])
|
|
||||||
pos += tag_len
|
|
||||||
|
|
||||||
|
|
||||||
class _AVMClass_Object(object):
|
|
||||||
def __init__(self, avm_class):
|
|
||||||
self.avm_class = avm_class
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '%s#%x' % (self.avm_class.name, id(self))
|
|
||||||
|
|
||||||
|
|
||||||
class _ScopeDict(dict):
|
|
||||||
def __init__(self, avm_class):
|
|
||||||
super(_ScopeDict, self).__init__()
|
|
||||||
self.avm_class = avm_class
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '%s__Scope(%s)' % (
|
|
||||||
self.avm_class.name,
|
|
||||||
super(_ScopeDict, self).__repr__())
|
|
||||||
|
|
||||||
|
|
||||||
class _AVMClass(object):
|
|
||||||
def __init__(self, name_idx, name, static_properties=None):
|
|
||||||
self.name_idx = name_idx
|
|
||||||
self.name = name
|
|
||||||
self.method_names = {}
|
|
||||||
self.method_idxs = {}
|
|
||||||
self.methods = {}
|
|
||||||
self.method_pyfunctions = {}
|
|
||||||
self.static_properties = static_properties if static_properties else {}
|
|
||||||
|
|
||||||
self.variables = _ScopeDict(self)
|
|
||||||
self.constants = {}
|
|
||||||
|
|
||||||
def make_object(self):
|
|
||||||
return _AVMClass_Object(self)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '_AVMClass(%s)' % (self.name)
|
|
||||||
|
|
||||||
def register_methods(self, methods):
|
|
||||||
self.method_names.update(methods.items())
|
|
||||||
self.method_idxs.update(dict(
|
|
||||||
(idx, name)
|
|
||||||
for name, idx in methods.items()))
|
|
||||||
|
|
||||||
|
|
||||||
class _Multiname(object):
|
|
||||||
def __init__(self, kind):
|
|
||||||
self.kind = kind
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '[MULTINAME kind: 0x%x]' % self.kind
|
|
||||||
|
|
||||||
|
|
||||||
def _read_int(reader):
|
|
||||||
res = 0
|
|
||||||
shift = 0
|
|
||||||
for _ in range(5):
|
|
||||||
buf = reader.read(1)
|
|
||||||
assert len(buf) == 1
|
|
||||||
b = compat_struct_unpack('<B', buf)[0]
|
|
||||||
res = res | ((b & 0x7f) << shift)
|
|
||||||
if b & 0x80 == 0:
|
|
||||||
break
|
|
||||||
shift += 7
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def _u30(reader):
|
|
||||||
res = _read_int(reader)
|
|
||||||
assert res & 0xf0000000 == 0
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
_u32 = _read_int
|
|
||||||
|
|
||||||
|
|
||||||
def _s32(reader):
|
|
||||||
v = _read_int(reader)
|
|
||||||
if v & 0x80000000 != 0:
|
|
||||||
v = - ((v ^ 0xffffffff) + 1)
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
def _s24(reader):
|
|
||||||
bs = reader.read(3)
|
|
||||||
assert len(bs) == 3
|
|
||||||
last_byte = b'\xff' if (ord(bs[2:3]) >= 0x80) else b'\x00'
|
|
||||||
return compat_struct_unpack('<i', bs + last_byte)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def _read_string(reader):
|
|
||||||
slen = _u30(reader)
|
|
||||||
resb = reader.read(slen)
|
|
||||||
assert len(resb) == slen
|
|
||||||
return resb.decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
def _read_bytes(count, reader):
|
|
||||||
assert count >= 0
|
|
||||||
resb = reader.read(count)
|
|
||||||
assert len(resb) == count
|
|
||||||
return resb
|
|
||||||
|
|
||||||
|
|
||||||
def _read_byte(reader):
|
|
||||||
resb = _read_bytes(1, reader=reader)
|
|
||||||
res = compat_struct_unpack('<B', resb)[0]
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
StringClass = _AVMClass('(no name idx)', 'String')
|
|
||||||
ByteArrayClass = _AVMClass('(no name idx)', 'ByteArray')
|
|
||||||
TimerClass = _AVMClass('(no name idx)', 'Timer')
|
|
||||||
TimerEventClass = _AVMClass('(no name idx)', 'TimerEvent', {'TIMER': 'timer'})
|
|
||||||
_builtin_classes = {
|
|
||||||
StringClass.name: StringClass,
|
|
||||||
ByteArrayClass.name: ByteArrayClass,
|
|
||||||
TimerClass.name: TimerClass,
|
|
||||||
TimerEventClass.name: TimerEventClass,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class _Undefined(object):
|
|
||||||
def __bool__(self):
|
|
||||||
return False
|
|
||||||
__nonzero__ = __bool__
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'undefined'
|
|
||||||
__repr__ = __str__
|
|
||||||
|
|
||||||
|
|
||||||
undefined = _Undefined()
|
|
||||||
|
|
||||||
|
|
||||||
class SWFInterpreter(object):
|
|
||||||
def __init__(self, file_contents):
|
|
||||||
self._patched_functions = {
|
|
||||||
(TimerClass, 'addEventListener'): lambda params: undefined,
|
|
||||||
}
|
|
||||||
code_tag = next(tag
|
|
||||||
for tag_code, tag in _extract_tags(file_contents)
|
|
||||||
if tag_code == 82)
|
|
||||||
p = code_tag.index(b'\0', 4) + 1
|
|
||||||
code_reader = io.BytesIO(code_tag[p:])
|
|
||||||
|
|
||||||
# Parse ABC (AVM2 ByteCode)
|
|
||||||
|
|
||||||
# Define a couple convenience methods
|
|
||||||
u30 = lambda *args: _u30(*args, reader=code_reader)
|
|
||||||
s32 = lambda *args: _s32(*args, reader=code_reader)
|
|
||||||
u32 = lambda *args: _u32(*args, reader=code_reader)
|
|
||||||
read_bytes = lambda *args: _read_bytes(*args, reader=code_reader)
|
|
||||||
read_byte = lambda *args: _read_byte(*args, reader=code_reader)
|
|
||||||
|
|
||||||
# minor_version + major_version
|
|
||||||
read_bytes(2 + 2)
|
|
||||||
|
|
||||||
# Constant pool
|
|
||||||
int_count = u30()
|
|
||||||
self.constant_ints = [0]
|
|
||||||
for _c in range(1, int_count):
|
|
||||||
self.constant_ints.append(s32())
|
|
||||||
self.constant_uints = [0]
|
|
||||||
uint_count = u30()
|
|
||||||
for _c in range(1, uint_count):
|
|
||||||
self.constant_uints.append(u32())
|
|
||||||
double_count = u30()
|
|
||||||
read_bytes(max(0, (double_count - 1)) * 8)
|
|
||||||
string_count = u30()
|
|
||||||
self.constant_strings = ['']
|
|
||||||
for _c in range(1, string_count):
|
|
||||||
s = _read_string(code_reader)
|
|
||||||
self.constant_strings.append(s)
|
|
||||||
namespace_count = u30()
|
|
||||||
for _c in range(1, namespace_count):
|
|
||||||
read_bytes(1) # kind
|
|
||||||
u30() # name
|
|
||||||
ns_set_count = u30()
|
|
||||||
for _c in range(1, ns_set_count):
|
|
||||||
count = u30()
|
|
||||||
for _c2 in range(count):
|
|
||||||
u30()
|
|
||||||
multiname_count = u30()
|
|
||||||
MULTINAME_SIZES = {
|
|
||||||
0x07: 2, # QName
|
|
||||||
0x0d: 2, # QNameA
|
|
||||||
0x0f: 1, # RTQName
|
|
||||||
0x10: 1, # RTQNameA
|
|
||||||
0x11: 0, # RTQNameL
|
|
||||||
0x12: 0, # RTQNameLA
|
|
||||||
0x09: 2, # Multiname
|
|
||||||
0x0e: 2, # MultinameA
|
|
||||||
0x1b: 1, # MultinameL
|
|
||||||
0x1c: 1, # MultinameLA
|
|
||||||
}
|
|
||||||
self.multinames = ['']
|
|
||||||
for _c in range(1, multiname_count):
|
|
||||||
kind = u30()
|
|
||||||
assert kind in MULTINAME_SIZES, 'Invalid multiname kind %r' % kind
|
|
||||||
if kind == 0x07:
|
|
||||||
u30() # namespace_idx
|
|
||||||
name_idx = u30()
|
|
||||||
self.multinames.append(self.constant_strings[name_idx])
|
|
||||||
elif kind == 0x09:
|
|
||||||
name_idx = u30()
|
|
||||||
u30()
|
|
||||||
self.multinames.append(self.constant_strings[name_idx])
|
|
||||||
else:
|
|
||||||
self.multinames.append(_Multiname(kind))
|
|
||||||
for _c2 in range(MULTINAME_SIZES[kind]):
|
|
||||||
u30()
|
|
||||||
|
|
||||||
# Methods
|
|
||||||
method_count = u30()
|
|
||||||
MethodInfo = collections.namedtuple(
|
|
||||||
'MethodInfo',
|
|
||||||
['NEED_ARGUMENTS', 'NEED_REST'])
|
|
||||||
method_infos = []
|
|
||||||
for method_id in range(method_count):
|
|
||||||
param_count = u30()
|
|
||||||
u30() # return type
|
|
||||||
for _ in range(param_count):
|
|
||||||
u30() # param type
|
|
||||||
u30() # name index (always 0 for youtube)
|
|
||||||
flags = read_byte()
|
|
||||||
if flags & 0x08 != 0:
|
|
||||||
# Options present
|
|
||||||
option_count = u30()
|
|
||||||
for c in range(option_count):
|
|
||||||
u30() # val
|
|
||||||
read_bytes(1) # kind
|
|
||||||
if flags & 0x80 != 0:
|
|
||||||
# Param names present
|
|
||||||
for _ in range(param_count):
|
|
||||||
u30() # param name
|
|
||||||
mi = MethodInfo(flags & 0x01 != 0, flags & 0x04 != 0)
|
|
||||||
method_infos.append(mi)
|
|
||||||
|
|
||||||
# Metadata
|
|
||||||
metadata_count = u30()
|
|
||||||
for _c in range(metadata_count):
|
|
||||||
u30() # name
|
|
||||||
item_count = u30()
|
|
||||||
for _c2 in range(item_count):
|
|
||||||
u30() # key
|
|
||||||
u30() # value
|
|
||||||
|
|
||||||
def parse_traits_info():
|
|
||||||
trait_name_idx = u30()
|
|
||||||
kind_full = read_byte()
|
|
||||||
kind = kind_full & 0x0f
|
|
||||||
attrs = kind_full >> 4
|
|
||||||
methods = {}
|
|
||||||
constants = None
|
|
||||||
if kind == 0x00: # Slot
|
|
||||||
u30() # Slot id
|
|
||||||
u30() # type_name_idx
|
|
||||||
vindex = u30()
|
|
||||||
if vindex != 0:
|
|
||||||
read_byte() # vkind
|
|
||||||
elif kind == 0x06: # Const
|
|
||||||
u30() # Slot id
|
|
||||||
u30() # type_name_idx
|
|
||||||
vindex = u30()
|
|
||||||
vkind = 'any'
|
|
||||||
if vindex != 0:
|
|
||||||
vkind = read_byte()
|
|
||||||
if vkind == 0x03: # Constant_Int
|
|
||||||
value = self.constant_ints[vindex]
|
|
||||||
elif vkind == 0x04: # Constant_UInt
|
|
||||||
value = self.constant_uints[vindex]
|
|
||||||
else:
|
|
||||||
return {}, None # Ignore silently for now
|
|
||||||
constants = {self.multinames[trait_name_idx]: value}
|
|
||||||
elif kind in (0x01, 0x02, 0x03): # Method / Getter / Setter
|
|
||||||
u30() # disp_id
|
|
||||||
method_idx = u30()
|
|
||||||
methods[self.multinames[trait_name_idx]] = method_idx
|
|
||||||
elif kind == 0x04: # Class
|
|
||||||
u30() # slot_id
|
|
||||||
u30() # classi
|
|
||||||
elif kind == 0x05: # Function
|
|
||||||
u30() # slot_id
|
|
||||||
function_idx = u30()
|
|
||||||
methods[function_idx] = self.multinames[trait_name_idx]
|
|
||||||
else:
|
|
||||||
raise ExtractorError('Unsupported trait kind %d' % kind)
|
|
||||||
|
|
||||||
if attrs & 0x4 != 0: # Metadata present
|
|
||||||
metadata_count = u30()
|
|
||||||
for _c3 in range(metadata_count):
|
|
||||||
u30() # metadata index
|
|
||||||
|
|
||||||
return methods, constants
|
|
||||||
|
|
||||||
# Classes
|
|
||||||
class_count = u30()
|
|
||||||
classes = []
|
|
||||||
for class_id in range(class_count):
|
|
||||||
name_idx = u30()
|
|
||||||
|
|
||||||
cname = self.multinames[name_idx]
|
|
||||||
avm_class = _AVMClass(name_idx, cname)
|
|
||||||
classes.append(avm_class)
|
|
||||||
|
|
||||||
u30() # super_name idx
|
|
||||||
flags = read_byte()
|
|
||||||
if flags & 0x08 != 0: # Protected namespace is present
|
|
||||||
u30() # protected_ns_idx
|
|
||||||
intrf_count = u30()
|
|
||||||
for _c2 in range(intrf_count):
|
|
||||||
u30()
|
|
||||||
u30() # iinit
|
|
||||||
trait_count = u30()
|
|
||||||
for _c2 in range(trait_count):
|
|
||||||
trait_methods, trait_constants = parse_traits_info()
|
|
||||||
avm_class.register_methods(trait_methods)
|
|
||||||
if trait_constants:
|
|
||||||
avm_class.constants.update(trait_constants)
|
|
||||||
|
|
||||||
assert len(classes) == class_count
|
|
||||||
self._classes_by_name = dict((c.name, c) for c in classes)
|
|
||||||
|
|
||||||
for avm_class in classes:
|
|
||||||
avm_class.cinit_idx = u30()
|
|
||||||
trait_count = u30()
|
|
||||||
for _c2 in range(trait_count):
|
|
||||||
trait_methods, trait_constants = parse_traits_info()
|
|
||||||
avm_class.register_methods(trait_methods)
|
|
||||||
if trait_constants:
|
|
||||||
avm_class.constants.update(trait_constants)
|
|
||||||
|
|
||||||
# Scripts
|
|
||||||
script_count = u30()
|
|
||||||
for _c in range(script_count):
|
|
||||||
u30() # init
|
|
||||||
trait_count = u30()
|
|
||||||
for _c2 in range(trait_count):
|
|
||||||
parse_traits_info()
|
|
||||||
|
|
||||||
# Method bodies
|
|
||||||
method_body_count = u30()
|
|
||||||
Method = collections.namedtuple('Method', ['code', 'local_count'])
|
|
||||||
self._all_methods = []
|
|
||||||
for _c in range(method_body_count):
|
|
||||||
method_idx = u30()
|
|
||||||
u30() # max_stack
|
|
||||||
local_count = u30()
|
|
||||||
u30() # init_scope_depth
|
|
||||||
u30() # max_scope_depth
|
|
||||||
code_length = u30()
|
|
||||||
code = read_bytes(code_length)
|
|
||||||
m = Method(code, local_count)
|
|
||||||
self._all_methods.append(m)
|
|
||||||
for avm_class in classes:
|
|
||||||
if method_idx in avm_class.method_idxs:
|
|
||||||
avm_class.methods[avm_class.method_idxs[method_idx]] = m
|
|
||||||
exception_count = u30()
|
|
||||||
for _c2 in range(exception_count):
|
|
||||||
u30() # from
|
|
||||||
u30() # to
|
|
||||||
u30() # target
|
|
||||||
u30() # exc_type
|
|
||||||
u30() # var_name
|
|
||||||
trait_count = u30()
|
|
||||||
for _c2 in range(trait_count):
|
|
||||||
parse_traits_info()
|
|
||||||
|
|
||||||
assert p + code_reader.tell() == len(code_tag)
|
|
||||||
|
|
||||||
def patch_function(self, avm_class, func_name, f):
|
|
||||||
self._patched_functions[(avm_class, func_name)] = f
|
|
||||||
|
|
||||||
def extract_class(self, class_name, call_cinit=True):
|
|
||||||
try:
|
|
||||||
res = self._classes_by_name[class_name]
|
|
||||||
except KeyError:
|
|
||||||
raise ExtractorError('Class %r not found' % class_name)
|
|
||||||
|
|
||||||
if call_cinit and hasattr(res, 'cinit_idx'):
|
|
||||||
res.register_methods({'$cinit': res.cinit_idx})
|
|
||||||
res.methods['$cinit'] = self._all_methods[res.cinit_idx]
|
|
||||||
cinit = self.extract_function(res, '$cinit')
|
|
||||||
cinit([])
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
def extract_function(self, avm_class, func_name):
|
|
||||||
p = self._patched_functions.get((avm_class, func_name))
|
|
||||||
if p:
|
|
||||||
return p
|
|
||||||
if func_name in avm_class.method_pyfunctions:
|
|
||||||
return avm_class.method_pyfunctions[func_name]
|
|
||||||
if func_name in self._classes_by_name:
|
|
||||||
return self._classes_by_name[func_name].make_object()
|
|
||||||
if func_name not in avm_class.methods:
|
|
||||||
raise ExtractorError('Cannot find function %s.%s' % (
|
|
||||||
avm_class.name, func_name))
|
|
||||||
m = avm_class.methods[func_name]
|
|
||||||
|
|
||||||
def resfunc(args):
|
|
||||||
# Helper functions
|
|
||||||
coder = io.BytesIO(m.code)
|
|
||||||
s24 = lambda: _s24(coder)
|
|
||||||
u30 = lambda: _u30(coder)
|
|
||||||
|
|
||||||
registers = [avm_class.variables] + list(args) + [None] * m.local_count
|
|
||||||
stack = []
|
|
||||||
scopes = collections.deque([
|
|
||||||
self._classes_by_name, avm_class.constants, avm_class.variables])
|
|
||||||
while True:
|
|
||||||
opcode = _read_byte(coder)
|
|
||||||
if opcode == 9: # label
|
|
||||||
pass # Spec says: "Do nothing."
|
|
||||||
elif opcode == 16: # jump
|
|
||||||
offset = s24()
|
|
||||||
coder.seek(coder.tell() + offset)
|
|
||||||
elif opcode == 17: # iftrue
|
|
||||||
offset = s24()
|
|
||||||
value = stack.pop()
|
|
||||||
if value:
|
|
||||||
coder.seek(coder.tell() + offset)
|
|
||||||
elif opcode == 18: # iffalse
|
|
||||||
offset = s24()
|
|
||||||
value = stack.pop()
|
|
||||||
if not value:
|
|
||||||
coder.seek(coder.tell() + offset)
|
|
||||||
elif opcode == 19: # ifeq
|
|
||||||
offset = s24()
|
|
||||||
value2 = stack.pop()
|
|
||||||
value1 = stack.pop()
|
|
||||||
if value2 == value1:
|
|
||||||
coder.seek(coder.tell() + offset)
|
|
||||||
elif opcode == 20: # ifne
|
|
||||||
offset = s24()
|
|
||||||
value2 = stack.pop()
|
|
||||||
value1 = stack.pop()
|
|
||||||
if value2 != value1:
|
|
||||||
coder.seek(coder.tell() + offset)
|
|
||||||
elif opcode == 21: # iflt
|
|
||||||
offset = s24()
|
|
||||||
value2 = stack.pop()
|
|
||||||
value1 = stack.pop()
|
|
||||||
if value1 < value2:
|
|
||||||
coder.seek(coder.tell() + offset)
|
|
||||||
elif opcode == 32: # pushnull
|
|
||||||
stack.append(None)
|
|
||||||
elif opcode == 33: # pushundefined
|
|
||||||
stack.append(undefined)
|
|
||||||
elif opcode == 36: # pushbyte
|
|
||||||
v = _read_byte(coder)
|
|
||||||
stack.append(v)
|
|
||||||
elif opcode == 37: # pushshort
|
|
||||||
v = u30()
|
|
||||||
stack.append(v)
|
|
||||||
elif opcode == 38: # pushtrue
|
|
||||||
stack.append(True)
|
|
||||||
elif opcode == 39: # pushfalse
|
|
||||||
stack.append(False)
|
|
||||||
elif opcode == 40: # pushnan
|
|
||||||
stack.append(float('NaN'))
|
|
||||||
elif opcode == 42: # dup
|
|
||||||
value = stack[-1]
|
|
||||||
stack.append(value)
|
|
||||||
elif opcode == 44: # pushstring
|
|
||||||
idx = u30()
|
|
||||||
stack.append(self.constant_strings[idx])
|
|
||||||
elif opcode == 48: # pushscope
|
|
||||||
new_scope = stack.pop()
|
|
||||||
scopes.append(new_scope)
|
|
||||||
elif opcode == 66: # construct
|
|
||||||
arg_count = u30()
|
|
||||||
args = list(reversed(
|
|
||||||
[stack.pop() for _ in range(arg_count)]))
|
|
||||||
obj = stack.pop()
|
|
||||||
res = obj.avm_class.make_object()
|
|
||||||
stack.append(res)
|
|
||||||
elif opcode == 70: # callproperty
|
|
||||||
index = u30()
|
|
||||||
mname = self.multinames[index]
|
|
||||||
arg_count = u30()
|
|
||||||
args = list(reversed(
|
|
||||||
[stack.pop() for _ in range(arg_count)]))
|
|
||||||
obj = stack.pop()
|
|
||||||
|
|
||||||
if obj == StringClass:
|
|
||||||
if mname == 'String':
|
|
||||||
assert len(args) == 1
|
|
||||||
assert isinstance(args[0], (
|
|
||||||
int, compat_str, _Undefined))
|
|
||||||
if args[0] == undefined:
|
|
||||||
res = 'undefined'
|
|
||||||
else:
|
|
||||||
res = compat_str(args[0])
|
|
||||||
stack.append(res)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Function String.%s is not yet implemented'
|
|
||||||
% mname)
|
|
||||||
elif isinstance(obj, _AVMClass_Object):
|
|
||||||
func = self.extract_function(obj.avm_class, mname)
|
|
||||||
res = func(args)
|
|
||||||
stack.append(res)
|
|
||||||
continue
|
|
||||||
elif isinstance(obj, _AVMClass):
|
|
||||||
func = self.extract_function(obj, mname)
|
|
||||||
res = func(args)
|
|
||||||
stack.append(res)
|
|
||||||
continue
|
|
||||||
elif isinstance(obj, _ScopeDict):
|
|
||||||
if mname in obj.avm_class.method_names:
|
|
||||||
func = self.extract_function(obj.avm_class, mname)
|
|
||||||
res = func(args)
|
|
||||||
else:
|
|
||||||
res = obj[mname]
|
|
||||||
stack.append(res)
|
|
||||||
continue
|
|
||||||
elif isinstance(obj, compat_str):
|
|
||||||
if mname == 'split':
|
|
||||||
assert len(args) == 1
|
|
||||||
assert isinstance(args[0], compat_str)
|
|
||||||
if args[0] == '':
|
|
||||||
res = list(obj)
|
|
||||||
else:
|
|
||||||
res = obj.split(args[0])
|
|
||||||
stack.append(res)
|
|
||||||
continue
|
|
||||||
elif mname == 'charCodeAt':
|
|
||||||
assert len(args) <= 1
|
|
||||||
idx = 0 if len(args) == 0 else args[0]
|
|
||||||
assert isinstance(idx, int)
|
|
||||||
res = ord(obj[idx])
|
|
||||||
stack.append(res)
|
|
||||||
continue
|
|
||||||
elif isinstance(obj, list):
|
|
||||||
if mname == 'slice':
|
|
||||||
assert len(args) == 1
|
|
||||||
assert isinstance(args[0], int)
|
|
||||||
res = obj[args[0]:]
|
|
||||||
stack.append(res)
|
|
||||||
continue
|
|
||||||
elif mname == 'join':
|
|
||||||
assert len(args) == 1
|
|
||||||
assert isinstance(args[0], compat_str)
|
|
||||||
res = args[0].join(obj)
|
|
||||||
stack.append(res)
|
|
||||||
continue
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Unsupported property %r on %r'
|
|
||||||
% (mname, obj))
|
|
||||||
elif opcode == 71: # returnvoid
|
|
||||||
res = undefined
|
|
||||||
return res
|
|
||||||
elif opcode == 72: # returnvalue
|
|
||||||
res = stack.pop()
|
|
||||||
return res
|
|
||||||
elif opcode == 73: # constructsuper
|
|
||||||
# Not yet implemented, just hope it works without it
|
|
||||||
arg_count = u30()
|
|
||||||
args = list(reversed(
|
|
||||||
[stack.pop() for _ in range(arg_count)]))
|
|
||||||
obj = stack.pop()
|
|
||||||
elif opcode == 74: # constructproperty
|
|
||||||
index = u30()
|
|
||||||
arg_count = u30()
|
|
||||||
args = list(reversed(
|
|
||||||
[stack.pop() for _ in range(arg_count)]))
|
|
||||||
obj = stack.pop()
|
|
||||||
|
|
||||||
mname = self.multinames[index]
|
|
||||||
assert isinstance(obj, _AVMClass)
|
|
||||||
|
|
||||||
# We do not actually call the constructor for now;
|
|
||||||
# we just pretend it does nothing
|
|
||||||
stack.append(obj.make_object())
|
|
||||||
elif opcode == 79: # callpropvoid
|
|
||||||
index = u30()
|
|
||||||
mname = self.multinames[index]
|
|
||||||
arg_count = u30()
|
|
||||||
args = list(reversed(
|
|
||||||
[stack.pop() for _ in range(arg_count)]))
|
|
||||||
obj = stack.pop()
|
|
||||||
if isinstance(obj, _AVMClass_Object):
|
|
||||||
func = self.extract_function(obj.avm_class, mname)
|
|
||||||
res = func(args)
|
|
||||||
assert res is undefined
|
|
||||||
continue
|
|
||||||
if isinstance(obj, _ScopeDict):
|
|
||||||
assert mname in obj.avm_class.method_names
|
|
||||||
func = self.extract_function(obj.avm_class, mname)
|
|
||||||
res = func(args)
|
|
||||||
assert res is undefined
|
|
||||||
continue
|
|
||||||
if mname == 'reverse':
|
|
||||||
assert isinstance(obj, list)
|
|
||||||
obj.reverse()
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Unsupported (void) property %r on %r'
|
|
||||||
% (mname, obj))
|
|
||||||
elif opcode == 86: # newarray
|
|
||||||
arg_count = u30()
|
|
||||||
arr = []
|
|
||||||
for i in range(arg_count):
|
|
||||||
arr.append(stack.pop())
|
|
||||||
arr = arr[::-1]
|
|
||||||
stack.append(arr)
|
|
||||||
elif opcode == 93: # findpropstrict
|
|
||||||
index = u30()
|
|
||||||
mname = self.multinames[index]
|
|
||||||
for s in reversed(scopes):
|
|
||||||
if mname in s:
|
|
||||||
res = s
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
res = scopes[0]
|
|
||||||
if mname not in res and mname in _builtin_classes:
|
|
||||||
stack.append(_builtin_classes[mname])
|
|
||||||
else:
|
|
||||||
stack.append(res[mname])
|
|
||||||
elif opcode == 94: # findproperty
|
|
||||||
index = u30()
|
|
||||||
mname = self.multinames[index]
|
|
||||||
for s in reversed(scopes):
|
|
||||||
if mname in s:
|
|
||||||
res = s
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
res = avm_class.variables
|
|
||||||
stack.append(res)
|
|
||||||
elif opcode == 96: # getlex
|
|
||||||
index = u30()
|
|
||||||
mname = self.multinames[index]
|
|
||||||
for s in reversed(scopes):
|
|
||||||
if mname in s:
|
|
||||||
scope = s
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
scope = avm_class.variables
|
|
||||||
|
|
||||||
if mname in scope:
|
|
||||||
res = scope[mname]
|
|
||||||
elif mname in _builtin_classes:
|
|
||||||
res = _builtin_classes[mname]
|
|
||||||
else:
|
|
||||||
# Assume uninitialized
|
|
||||||
# TODO warn here
|
|
||||||
res = undefined
|
|
||||||
stack.append(res)
|
|
||||||
elif opcode == 97: # setproperty
|
|
||||||
index = u30()
|
|
||||||
value = stack.pop()
|
|
||||||
idx = self.multinames[index]
|
|
||||||
if isinstance(idx, _Multiname):
|
|
||||||
idx = stack.pop()
|
|
||||||
obj = stack.pop()
|
|
||||||
obj[idx] = value
|
|
||||||
elif opcode == 98: # getlocal
|
|
||||||
index = u30()
|
|
||||||
stack.append(registers[index])
|
|
||||||
elif opcode == 99: # setlocal
|
|
||||||
index = u30()
|
|
||||||
value = stack.pop()
|
|
||||||
registers[index] = value
|
|
||||||
elif opcode == 102: # getproperty
|
|
||||||
index = u30()
|
|
||||||
pname = self.multinames[index]
|
|
||||||
if pname == 'length':
|
|
||||||
obj = stack.pop()
|
|
||||||
assert isinstance(obj, (compat_str, list))
|
|
||||||
stack.append(len(obj))
|
|
||||||
elif isinstance(pname, compat_str): # Member access
|
|
||||||
obj = stack.pop()
|
|
||||||
if isinstance(obj, _AVMClass):
|
|
||||||
res = obj.static_properties[pname]
|
|
||||||
stack.append(res)
|
|
||||||
continue
|
|
||||||
|
|
||||||
assert isinstance(obj, (dict, _ScopeDict)),\
|
|
||||||
'Accessing member %r on %r' % (pname, obj)
|
|
||||||
res = obj.get(pname, undefined)
|
|
||||||
stack.append(res)
|
|
||||||
else: # Assume attribute access
|
|
||||||
idx = stack.pop()
|
|
||||||
assert isinstance(idx, int)
|
|
||||||
obj = stack.pop()
|
|
||||||
assert isinstance(obj, list)
|
|
||||||
stack.append(obj[idx])
|
|
||||||
elif opcode == 104: # initproperty
|
|
||||||
index = u30()
|
|
||||||
value = stack.pop()
|
|
||||||
idx = self.multinames[index]
|
|
||||||
if isinstance(idx, _Multiname):
|
|
||||||
idx = stack.pop()
|
|
||||||
obj = stack.pop()
|
|
||||||
obj[idx] = value
|
|
||||||
elif opcode == 115: # convert_
|
|
||||||
value = stack.pop()
|
|
||||||
intvalue = int(value)
|
|
||||||
stack.append(intvalue)
|
|
||||||
elif opcode == 128: # coerce
|
|
||||||
u30()
|
|
||||||
elif opcode == 130: # coerce_a
|
|
||||||
value = stack.pop()
|
|
||||||
# um, yes, it's any value
|
|
||||||
stack.append(value)
|
|
||||||
elif opcode == 133: # coerce_s
|
|
||||||
assert isinstance(stack[-1], (type(None), compat_str))
|
|
||||||
elif opcode == 147: # decrement
|
|
||||||
value = stack.pop()
|
|
||||||
assert isinstance(value, int)
|
|
||||||
stack.append(value - 1)
|
|
||||||
elif opcode == 149: # typeof
|
|
||||||
value = stack.pop()
|
|
||||||
return {
|
|
||||||
_Undefined: 'undefined',
|
|
||||||
compat_str: 'String',
|
|
||||||
int: 'Number',
|
|
||||||
float: 'Number',
|
|
||||||
}[type(value)]
|
|
||||||
elif opcode == 160: # add
|
|
||||||
value2 = stack.pop()
|
|
||||||
value1 = stack.pop()
|
|
||||||
res = value1 + value2
|
|
||||||
stack.append(res)
|
|
||||||
elif opcode == 161: # subtract
|
|
||||||
value2 = stack.pop()
|
|
||||||
value1 = stack.pop()
|
|
||||||
res = value1 - value2
|
|
||||||
stack.append(res)
|
|
||||||
elif opcode == 162: # multiply
|
|
||||||
value2 = stack.pop()
|
|
||||||
value1 = stack.pop()
|
|
||||||
res = value1 * value2
|
|
||||||
stack.append(res)
|
|
||||||
elif opcode == 164: # modulo
|
|
||||||
value2 = stack.pop()
|
|
||||||
value1 = stack.pop()
|
|
||||||
res = value1 % value2
|
|
||||||
stack.append(res)
|
|
||||||
elif opcode == 168: # bitand
|
|
||||||
value2 = stack.pop()
|
|
||||||
value1 = stack.pop()
|
|
||||||
assert isinstance(value1, int)
|
|
||||||
assert isinstance(value2, int)
|
|
||||||
res = value1 & value2
|
|
||||||
stack.append(res)
|
|
||||||
elif opcode == 171: # equals
|
|
||||||
value2 = stack.pop()
|
|
||||||
value1 = stack.pop()
|
|
||||||
result = value1 == value2
|
|
||||||
stack.append(result)
|
|
||||||
elif opcode == 175: # greaterequals
|
|
||||||
value2 = stack.pop()
|
|
||||||
value1 = stack.pop()
|
|
||||||
result = value1 >= value2
|
|
||||||
stack.append(result)
|
|
||||||
elif opcode == 192: # increment_i
|
|
||||||
value = stack.pop()
|
|
||||||
assert isinstance(value, int)
|
|
||||||
stack.append(value + 1)
|
|
||||||
elif opcode == 208: # getlocal_0
|
|
||||||
stack.append(registers[0])
|
|
||||||
elif opcode == 209: # getlocal_1
|
|
||||||
stack.append(registers[1])
|
|
||||||
elif opcode == 210: # getlocal_2
|
|
||||||
stack.append(registers[2])
|
|
||||||
elif opcode == 211: # getlocal_3
|
|
||||||
stack.append(registers[3])
|
|
||||||
elif opcode == 212: # setlocal_0
|
|
||||||
registers[0] = stack.pop()
|
|
||||||
elif opcode == 213: # setlocal_1
|
|
||||||
registers[1] = stack.pop()
|
|
||||||
elif opcode == 214: # setlocal_2
|
|
||||||
registers[2] = stack.pop()
|
|
||||||
elif opcode == 215: # setlocal_3
|
|
||||||
registers[3] = stack.pop()
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Unsupported opcode %d' % opcode)
|
|
||||||
|
|
||||||
avm_class.method_pyfunctions[func_name] = resfunc
|
|
||||||
return resfunc
|
|
@ -1,187 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import io
|
|
||||||
import json
|
|
||||||
import traceback
|
|
||||||
import hashlib
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from zipimport import zipimporter
|
|
||||||
|
|
||||||
from .utils import encode_compat_str
|
|
||||||
|
|
||||||
from .version import __version__
|
|
||||||
|
|
||||||
|
|
||||||
def rsa_verify(message, signature, key):
|
|
||||||
from hashlib import sha256
|
|
||||||
assert isinstance(message, bytes)
|
|
||||||
byte_size = (len(bin(key[0])) - 2 + 8 - 1) // 8
|
|
||||||
signature = ('%x' % pow(int(signature, 16), key[1], key[0])).encode()
|
|
||||||
signature = (byte_size * 2 - len(signature)) * b'0' + signature
|
|
||||||
asn1 = b'3031300d060960864801650304020105000420'
|
|
||||||
asn1 += sha256(message).hexdigest().encode()
|
|
||||||
if byte_size < len(asn1) // 2 + 11:
|
|
||||||
return False
|
|
||||||
expected = b'0001' + (byte_size - len(asn1) // 2 - 3) * b'ff' + b'00' + asn1
|
|
||||||
return expected == signature
|
|
||||||
|
|
||||||
|
|
||||||
def update_self(to_screen, verbose, opener):
|
|
||||||
"""Update the program file with the latest version from the repository"""
|
|
||||||
|
|
||||||
UPDATE_URL = 'https://rg3.github.io/youtube-dl/update/'
|
|
||||||
VERSION_URL = UPDATE_URL + 'LATEST_VERSION'
|
|
||||||
JSON_URL = UPDATE_URL + 'versions.json'
|
|
||||||
UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537)
|
|
||||||
|
|
||||||
if not isinstance(globals().get('__loader__'), zipimporter) and not hasattr(sys, 'frozen'):
|
|
||||||
to_screen('It looks like you installed youtube-dl with a package manager, pip, setup.py or a tarball. Please use that to update.')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if there is a new version
|
|
||||||
try:
|
|
||||||
newversion = opener.open(VERSION_URL).read().decode('utf-8').strip()
|
|
||||||
except Exception:
|
|
||||||
if verbose:
|
|
||||||
to_screen(encode_compat_str(traceback.format_exc()))
|
|
||||||
to_screen('ERROR: can\'t find the current version. Please try again later.')
|
|
||||||
return
|
|
||||||
if newversion == __version__:
|
|
||||||
to_screen('youtube-dl is up-to-date (' + __version__ + ')')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Download and check versions info
|
|
||||||
try:
|
|
||||||
versions_info = opener.open(JSON_URL).read().decode('utf-8')
|
|
||||||
versions_info = json.loads(versions_info)
|
|
||||||
except Exception:
|
|
||||||
if verbose:
|
|
||||||
to_screen(encode_compat_str(traceback.format_exc()))
|
|
||||||
to_screen('ERROR: can\'t obtain versions info. Please try again later.')
|
|
||||||
return
|
|
||||||
if 'signature' not in versions_info:
|
|
||||||
to_screen('ERROR: the versions file is not signed or corrupted. Aborting.')
|
|
||||||
return
|
|
||||||
signature = versions_info['signature']
|
|
||||||
del versions_info['signature']
|
|
||||||
if not rsa_verify(json.dumps(versions_info, sort_keys=True).encode('utf-8'), signature, UPDATES_RSA_KEY):
|
|
||||||
to_screen('ERROR: the versions file signature is invalid. Aborting.')
|
|
||||||
return
|
|
||||||
|
|
||||||
version_id = versions_info['latest']
|
|
||||||
|
|
||||||
def version_tuple(version_str):
|
|
||||||
return tuple(map(int, version_str.split('.')))
|
|
||||||
if version_tuple(__version__) >= version_tuple(version_id):
|
|
||||||
to_screen('youtube-dl is up to date (%s)' % __version__)
|
|
||||||
return
|
|
||||||
|
|
||||||
to_screen('Updating to version ' + version_id + ' ...')
|
|
||||||
version = versions_info['versions'][version_id]
|
|
||||||
|
|
||||||
print_notes(to_screen, versions_info['versions'])
|
|
||||||
|
|
||||||
# sys.executable is set to the full pathname of the exe-file for py2exe
|
|
||||||
filename = sys.executable if hasattr(sys, 'frozen') else sys.argv[0]
|
|
||||||
|
|
||||||
if not os.access(filename, os.W_OK):
|
|
||||||
to_screen('ERROR: no write permissions on %s' % filename)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Py2EXE
|
|
||||||
if hasattr(sys, 'frozen'):
|
|
||||||
exe = filename
|
|
||||||
directory = os.path.dirname(exe)
|
|
||||||
if not os.access(directory, os.W_OK):
|
|
||||||
to_screen('ERROR: no write permissions on %s' % directory)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
urlh = opener.open(version['exe'][0])
|
|
||||||
newcontent = urlh.read()
|
|
||||||
urlh.close()
|
|
||||||
except (IOError, OSError):
|
|
||||||
if verbose:
|
|
||||||
to_screen(encode_compat_str(traceback.format_exc()))
|
|
||||||
to_screen('ERROR: unable to download latest version')
|
|
||||||
return
|
|
||||||
|
|
||||||
newcontent_hash = hashlib.sha256(newcontent).hexdigest()
|
|
||||||
if newcontent_hash != version['exe'][1]:
|
|
||||||
to_screen('ERROR: the downloaded file hash does not match. Aborting.')
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(exe + '.new', 'wb') as outf:
|
|
||||||
outf.write(newcontent)
|
|
||||||
except (IOError, OSError):
|
|
||||||
if verbose:
|
|
||||||
to_screen(encode_compat_str(traceback.format_exc()))
|
|
||||||
to_screen('ERROR: unable to write the new version')
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
bat = os.path.join(directory, 'youtube-dl-updater.bat')
|
|
||||||
with io.open(bat, 'w') as batfile:
|
|
||||||
batfile.write('''
|
|
||||||
@echo off
|
|
||||||
echo Waiting for file handle to be closed ...
|
|
||||||
ping 127.0.0.1 -n 5 -w 1000 > NUL
|
|
||||||
move /Y "%s.new" "%s" > NUL
|
|
||||||
echo Updated youtube-dl to version %s.
|
|
||||||
start /b "" cmd /c del "%%~f0"&exit /b"
|
|
||||||
\n''' % (exe, exe, version_id))
|
|
||||||
|
|
||||||
subprocess.Popen([bat]) # Continues to run in the background
|
|
||||||
return # Do not show premature success messages
|
|
||||||
except (IOError, OSError):
|
|
||||||
if verbose:
|
|
||||||
to_screen(encode_compat_str(traceback.format_exc()))
|
|
||||||
to_screen('ERROR: unable to overwrite current version')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Zip unix package
|
|
||||||
elif isinstance(globals().get('__loader__'), zipimporter):
|
|
||||||
try:
|
|
||||||
urlh = opener.open(version['bin'][0])
|
|
||||||
newcontent = urlh.read()
|
|
||||||
urlh.close()
|
|
||||||
except (IOError, OSError):
|
|
||||||
if verbose:
|
|
||||||
to_screen(encode_compat_str(traceback.format_exc()))
|
|
||||||
to_screen('ERROR: unable to download latest version')
|
|
||||||
return
|
|
||||||
|
|
||||||
newcontent_hash = hashlib.sha256(newcontent).hexdigest()
|
|
||||||
if newcontent_hash != version['bin'][1]:
|
|
||||||
to_screen('ERROR: the downloaded file hash does not match. Aborting.')
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(filename, 'wb') as outf:
|
|
||||||
outf.write(newcontent)
|
|
||||||
except (IOError, OSError):
|
|
||||||
if verbose:
|
|
||||||
to_screen(encode_compat_str(traceback.format_exc()))
|
|
||||||
to_screen('ERROR: unable to overwrite current version')
|
|
||||||
return
|
|
||||||
|
|
||||||
to_screen('Updated youtube-dl. Restart youtube-dl to use the new version.')
|
|
||||||
|
|
||||||
|
|
||||||
def get_notes(versions, fromVersion):
|
|
||||||
notes = []
|
|
||||||
for v, vdata in sorted(versions.items()):
|
|
||||||
if v > fromVersion:
|
|
||||||
notes.extend(vdata.get('notes', []))
|
|
||||||
return notes
|
|
||||||
|
|
||||||
|
|
||||||
def print_notes(to_screen, versions, fromVersion=__version__):
|
|
||||||
notes = get_notes(versions, fromVersion)
|
|
||||||
if notes:
|
|
||||||
to_screen('PLEASE NOTE:')
|
|
||||||
for note in notes:
|
|
||||||
to_screen(note)
|
|
3990
youtube_dl/utils.py
3990
youtube_dl/utils.py
File diff suppressed because it is too large
Load Diff
@ -1,3 +0,0 @@
|
|||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
__version__ = '2018.07.10'
|
|
Loading…
x
Reference in New Issue
Block a user