Merge branch 'master' into add_sponsorblock

This commit is contained in:
James Taylor 2020-10-21 18:53:12 -07:00 committed by GitHub
commit aa52c7a42e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 163 additions and 106 deletions

View File

@ -119,7 +119,7 @@ wine_run(['./python/python.exe', '-I', 'get-pip.py'])
## Isolated mode ## Isolated mode
We want to run in what is called isolated mode, given by the switch -I. We want to run in what is called isolated mode, given by the switch -I.
This mode prevents the embedded python distribution from searching in This mode prevents the embedded python distribution from searching in
global directories for imports global directories for imports
For example, if a user has `C:\Python37` and the embedded distribution is For example, if a user has `C:\Python37` and the embedded distribution is

View File

@ -160,6 +160,19 @@ For security reasons, enabling this is not recommended.''',
], ],
}), }),
('font', {
'type': int,
'default': 1,
'comment': '',
'options': [
(0, 'Browser default'),
(1, 'Arial'),
(2, 'Liberation Serif'),
(3, 'Verdana'),
(4, 'Tahoma'),
],
}),
('autocheck_subscriptions', { ('autocheck_subscriptions', {
'type': bool, 'type': bool,
'default': 0, 'default': 0,
@ -319,12 +332,6 @@ else:
globals().update(current_settings_dict) globals().update(current_settings_dict)
if proxy_images:
img_prefix = "/"
else:
img_prefix = ""
if route_tor: if route_tor:
print("Tor routing is ON") print("Tor routing is ON")
@ -343,6 +350,19 @@ def add_setting_changed_hook(setting, func):
hooks[setting] = [func] hooks[setting] = [func]
def set_img_prefix(old_value=None, value=None):
global img_prefix
if value is None:
value = proxy_images
if value:
img_prefix = '/'
else:
img_prefix = ''
set_img_prefix()
add_setting_changed_hook('proxy_images', set_img_prefix)
def settings_page(): def settings_page():
if request.method == 'GET': if request.method == 'GET':
return flask.render_template('settings.html', return flask.render_template('settings.html',

View File

@ -1,5 +1,6 @@
from youtube import util from youtube import util
import flask import flask
from flask import request
import settings import settings
import traceback import traceback
import re import re
@ -59,6 +60,7 @@ def timestamps(text):
@yt_app.errorhandler(500) @yt_app.errorhandler(500)
def error_page(e): def error_page(e):
slim = request.args.get('slim', False) # whether it was an ajax request
if (exc_info()[0] == util.FetchError if (exc_info()[0] == util.FetchError
and exc_info()[1].code == '429' and exc_info()[1].code == '429'
and settings.route_tor and settings.route_tor
@ -68,5 +70,22 @@ def error_page(e):
' using the New Identity button in the Tor Browser.') ' using the New Identity button in the Tor Browser.')
if exc_info()[1].ip: if exc_info()[1].ip:
error_message += ' Exit node IP address: ' + exc_info()[1].ip error_message += ' Exit node IP address: ' + exc_info()[1].ip
return flask.render_template('error.html', error_message=error_message), 502 return flask.render_template('error.html', error_message=error_message, slim=slim), 502
return flask.render_template('error.html', traceback=traceback.format_exc()), 500 return flask.render_template('error.html', traceback=traceback.format_exc(), slim=slim), 500
font_choices = {
0: 'initial',
1: 'arial, "liberation sans", sans-serif',
2: '"liberation serif", "times new roman", calibri, carlito, serif',
3: 'verdana, sans-serif',
4: 'tahoma, sans-serif',
}
@yt_app.route('/shared.css')
def get_css():
return flask.Response(
flask.render_template('shared.css',
font_family = font_choices[settings.font]
),
mimetype='text/css',
)

View File

@ -90,7 +90,7 @@ def single_comment_ctoken(video_id, comment_id):
def post_process_comments_info(comments_info): def post_process_comments_info(comments_info):
for comment in comments_info['comments']: for comment in comments_info['comments']:
comment['author_url'] = concat_or_none( comment['author_url'] = concat_or_none(
util.URL_ORIGIN, comment['author_url']) '/', comment['author_url'])
comment['author_avatar'] = concat_or_none( comment['author_avatar'] = concat_or_none(
settings.img_prefix, comment['author_avatar']) settings.img_prefix, comment['author_avatar'])

View File

@ -155,7 +155,7 @@ def get_delete_comment_page():
def get_post_comment_page(): def get_post_comment_page():
video_id = request.args['video_id'] video_id = request.args['video_id']
parent_id = request.args.get('parent_id', '') parent_id = request.args.get('parent_id', '')
if parent_id: # comment reply if parent_id: # comment reply
form_action = util.URL_ORIGIN + '/comments?parent_id=' + parent_id + "&video_id=" + video_id form_action = util.URL_ORIGIN + '/comments?parent_id=' + parent_id + "&video_id=" + video_id
replying = True replying = True

View File

@ -5,13 +5,13 @@ import io
def byte(n): def byte(n):
return bytes((n,)) return bytes((n,))
def varint_encode(offset): def varint_encode(offset):
'''In this encoding system, for each 8-bit byte, the first bit is 1 if there are more bytes, and 0 is this is the last one. '''In this encoding system, for each 8-bit byte, the first bit is 1 if there are more bytes, and 0 is this is the last one.
The next 7 bits are data. These 7-bit sections represent the data in Little endian order. For example, suppose the data is The next 7 bits are data. These 7-bit sections represent the data in Little endian order. For example, suppose the data is
aaaaaaabbbbbbbccccccc (each of these sections is 7 bits). It will be encoded as: aaaaaaabbbbbbbccccccc (each of these sections is 7 bits). It will be encoded as:
1ccccccc 1bbbbbbb 0aaaaaaa 1ccccccc 1bbbbbbb 0aaaaaaa
This encoding is used in youtube parameters to encode offsets and to encode the length for length-prefixed data. This encoding is used in youtube parameters to encode offsets and to encode the length for length-prefixed data.
See https://developers.google.com/protocol-buffers/docs/encoding#varints for more info.''' See https://developers.google.com/protocol-buffers/docs/encoding#varints for more info.'''
needed_bytes = ceil(offset.bit_length()/7) or 1 # (0).bit_length() returns 0, but we need 1 in that case. needed_bytes = ceil(offset.bit_length()/7) or 1 # (0).bit_length() returns 0, but we need 1 in that case.
@ -20,20 +20,20 @@ def varint_encode(offset):
encoded_bytes[i] = (offset & 127) | 128 # 7 least significant bits encoded_bytes[i] = (offset & 127) | 128 # 7 least significant bits
offset = offset >> 7 offset = offset >> 7
encoded_bytes[-1] = offset & 127 # leave first bit as zero for last byte encoded_bytes[-1] = offset & 127 # leave first bit as zero for last byte
return bytes(encoded_bytes) return bytes(encoded_bytes)
def varint_decode(encoded): def varint_decode(encoded):
decoded = 0 decoded = 0
for i, byte in enumerate(encoded): for i, byte in enumerate(encoded):
decoded |= (byte & 127) << 7*i decoded |= (byte & 127) << 7*i
if not (byte & 128): if not (byte & 128):
break break
return decoded return decoded
def string(field_number, data): def string(field_number, data):
data = as_bytes(data) data = as_bytes(data)
return _proto_field(2, field_number, varint_encode(len(data)) + data) return _proto_field(2, field_number, varint_encode(len(data)) + data)
@ -41,20 +41,20 @@ nested = string
def uint(field_number, value): def uint(field_number, value):
return _proto_field(0, field_number, varint_encode(value)) return _proto_field(0, field_number, varint_encode(value))
def _proto_field(wire_type, field_number, data): def _proto_field(wire_type, field_number, data):
''' See https://developers.google.com/protocol-buffers/docs/encoding#structure ''' ''' See https://developers.google.com/protocol-buffers/docs/encoding#structure '''
return varint_encode( (field_number << 3) | wire_type) + data return varint_encode( (field_number << 3) | wire_type) + data
def percent_b64encode(data): def percent_b64encode(data):
return base64.urlsafe_b64encode(data).replace(b'=', b'%3D') return base64.urlsafe_b64encode(data).replace(b'=', b'%3D')
def unpadded_b64encode(data): def unpadded_b64encode(data):
return base64.urlsafe_b64encode(data).replace(b'=', b'') return base64.urlsafe_b64encode(data).replace(b'=', b'')
@ -81,7 +81,7 @@ def read_varint(data):
i += 1 i += 1
return result return result
def read_group(data, end_sequence): def read_group(data, end_sequence):
start = data.tell() start = data.tell()
index = data.original.find(end_sequence, start) index = data.original.find(end_sequence, start)
@ -101,7 +101,7 @@ def read_protobuf(data):
break break
wire_type = tag & 7 wire_type = tag & 7
field_number = tag >> 3 field_number = tag >> 3
if wire_type == 0: if wire_type == 0:
value = read_varint(data) value = read_varint(data)
elif wire_type == 1: elif wire_type == 1:

View File

@ -1,4 +1,4 @@
.video-metadata{ .video-metadata{
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
grid-template-rows: auto auto 1fr auto; grid-template-rows: auto auto 1fr auto;
@ -124,6 +124,23 @@
grid-column-gap: 10px; grid-column-gap: 10px;
} }
details.replies > summary{
background-color: var(--interface-color);
border-style: outset;
border-width: 1px;
font-weight: bold;
padding-bottom: 0px;
}
.replies-open-new-tab{
display: inline-block;
margin-top: 5px;
}
details.replies .comment{
width: 600px;
}
.more-comments{ .more-comments{
justify-self:center; justify-self:center;
margin-top:10px; margin-top:10px;

View File

@ -41,9 +41,7 @@ function doXhr(url, callback=null) {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open("GET", url); xhr.open("GET", url);
xhr.onload = (e) => { xhr.onload = (e) => {
let ok = xhr.status >= 200 && xhr.status < 300; callback(e.currentTarget.response);
if (ok) callback(e.currentTarget.response);
else alert(`${xhr.responseURL} status code: ${xhr.status}`);
} }
xhr.send(); xhr.send();
return xhr; return xhr;

View File

@ -4,12 +4,9 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>{{ page_title }}</title> <title>{{ page_title }}</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; media-src 'self' https://*.googlevideo.com; <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; media-src 'self' https://*.googlevideo.com;
{% if not settings.proxy_images %} {{ "img-src 'self' https://*.googleusercontent.com https://*.ggpht.com https://*.ytimg.com;" if not settings.proxy_images else "" }}">
img-src https://*.googleusercontent.com https://*.ggpht.com https://*.ytimg.com;
{% endif %}">
<link href="{{ theme_path }}" type="text/css" rel="stylesheet"> <link href="{{ theme_path }}" type="text/css" rel="stylesheet">
<link href="/youtube.com/static/shared.css" type="text/css" rel="stylesheet"> <link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
<link href="/youtube.com/static/comments.css" type="text/css" rel="stylesheet"> <link href="/youtube.com/static/comments.css" type="text/css" rel="stylesheet">
<link href="/youtube.com/static/favicon.ico" type="image/x-icon" rel="icon"> <link href="/youtube.com/static/favicon.ico" type="image/x-icon" rel="icon">
<link title="Youtube local" href="/youtube.com/opensearch.xml" rel="search" type="application/opensearchdescription+xml"> <link title="Youtube local" href="/youtube.com/opensearch.xml" rel="search" type="application/opensearchdescription+xml">

View File

@ -34,11 +34,11 @@
main .channel-tabs{ main .channel-tabs{
grid-row:2; grid-row:2;
grid-column: 1 / span 2; grid-column: 1 / span 2;
display:grid; display:grid;
grid-auto-flow: column; grid-auto-flow: column;
justify-content:start; justify-content:start;
background-color: var(--interface-color); background-color: var(--interface-color);
padding: 3px; padding: 3px;
padding-left: 6px; padding-left: 6px;
@ -103,7 +103,7 @@
} }
{% endblock style %} {% endblock style %}
{% block main %} {% block main %}
<img class="avatar" src="{{ avatar }}"> <img class="avatar" src="{{ avatar }}">
<div class="summary"> <div class="summary">
<h2 class="title">{{ channel_name }}</h2> <h2 class="title">{{ channel_name }}</h2>

View File

@ -25,6 +25,7 @@
{% if settings.use_comments_js and comment['reply_count'] %} {% if settings.use_comments_js and comment['reply_count'] %}
<details class="replies" src="{{ comment['replies_url'] }}"> <details class="replies" src="{{ comment['replies_url'] }}">
<summary>{{ comment['view_replies_text'] }}</summary> <summary>{{ comment['view_replies_text'] }}</summary>
<a href="{{ comment['replies_url'] }}" class="replies-open-new-tab" target="_blank">Open in new tab</a>
<div class="comment_page">loading..</div> <div class="comment_page">loading..</div>
</details> </details>
{% else %} {% else %}

View File

@ -1,29 +1,8 @@
{% set page_title = 'Error' %} {% set page_title = 'Error' %}
{% extends "base.html" %}
{% block style %} {% if not slim %}
h1{ {% extends "base.html" %}
font-size: 2rem; {% endif %}
font-weight: normal;
}
#error-box, #error-message{
background-color: var(--interface-color);
width: 80%;
margin: auto;
margin-top: 20px;
padding: 5px;
}
#error-box > div, #error-box > p, #error-box > h1{
white-space: pre-wrap;
margin-bottom: 10px;
}
.code-box{
padding: 5px;
border-style:solid;
border-width:1px;
border-radius:5px;
}
{% endblock style %}
{% block main %} {% block main %}
{% if traceback %} {% if traceback %}

View File

@ -25,7 +25,7 @@
} }
{% endblock style %} {% endblock style %}
{% block main %} {% block main %}
<div class="playlist-metadata"> <div class="playlist-metadata">
<h2 class="playlist-title">{{ playlist_name }}</h2> <h2 class="playlist-title">{{ playlist_name }}</h2>
<input type="hidden" name="playlist_page" value="{{ playlist_name }}" form="playlist-edit"> <input type="hidden" name="playlist_page" value="{{ playlist_name }}" form="playlist-edit">

View File

@ -6,7 +6,7 @@
width: 800px; width: 800px;
margin:auto; margin:auto;
} }
.playlist-metadata{ .playlist-metadata{
display:grid; display:grid;
grid-template-columns: 0fr 1fr; grid-template-columns: 0fr 1fr;
@ -44,7 +44,7 @@
display: grid; display: grid;
grid-auto-rows: 0fr; grid-auto-rows: 0fr;
grid-row-gap: 10px; grid-row-gap: 10px;
} }
{% endblock style %} {% endblock style %}
@ -61,7 +61,7 @@
<div class="playlist-description">{{ common_elements.text_runs(description) }}</div> <div class="playlist-description">{{ common_elements.text_runs(description) }}</div>
</div> </div>
<div id="results"> <div id="results">
{% for info in video_list %} {% for info in video_list %}
{{ common_elements.item(info) }} {{ common_elements.item(info) }}
{% endfor %} {% endfor %}

View File

@ -12,17 +12,17 @@ address{
} }
html{ html{
font-family: "liberation serif", "times new roman", calibri, carlito, serif; font-family: {{ font_family }};
} }
body{ body{
margin:0; margin:0;
padding: 0; padding: 0;
color:var(--text-color); color:var(--text-color);
background-color:var(--background-color); background-color:var(--background-color);
min-height:100vh; min-height:100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -141,7 +141,7 @@ body{
.item-list{ .item-list{
display: grid; display: grid;
grid-row-gap: 10px; grid-row-gap: 10px;
} }
@ -164,7 +164,7 @@ body{
.item-box{ .item-box{
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;
/* prevent overflow due to long titles with no spaces: /* prevent overflow due to long titles with no spaces:
https://stackoverflow.com/a/43312314 */ https://stackoverflow.com/a/43312314 */
min-width: 0; min-width: 0;
} }
@ -185,7 +185,7 @@ body{
align-content: start; align-content: start;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
grid-template-rows: auto auto auto auto 1fr; grid-template-rows: auto auto auto auto 1fr;
/* prevent overflow due to long titles with no spaces: /* prevent overflow due to long titles with no spaces:
https://stackoverflow.com/a/43312314 */ https://stackoverflow.com/a/43312314 */
min-width: 0; min-width: 0;
} }
@ -308,7 +308,7 @@ body{
justify-content: center; justify-content: center;
display: grid; display: grid;
grid-auto-columns: 40px; grid-auto-columns: 40px;
grid-auto-flow: column; grid-auto-flow: column;
height: 40px; height: 40px;
} }
.next-previous-button-row{ .next-previous-button-row{
@ -334,3 +334,26 @@ body{
padding: 2px; padding: 2px;
justify-self: start; justify-self: start;
} }
/* error page stuff */
h1{
font-size: 2rem;
font-weight: normal;
}
#error-box, #error-message{
background-color: var(--interface-color);
width: 80%;
margin: auto;
margin-top: 20px;
padding: 5px;
}
#error-box > div, #error-box > p, #error-box > h1{
white-space: pre-wrap;
margin-bottom: 10px;
}
.code-box{
padding: 5px;
border-style:solid;
border-width:1px;
border-radius:5px;
}

View File

@ -14,18 +14,6 @@
text-decoration: underline; text-decoration: underline;
} }
details.replies > summary{
background-color: var(--interface-color);
border-style: outset;
border-width: 1px;
font-weight: bold;
padding-bottom: 0px;
}
details.replies .comment{
width: 600px;
}
.playability-error{ .playability-error{
height: 360px; height: 360px;
width: 640px; width: 640px;

View File

@ -226,15 +226,19 @@ def extract_info(video_id, use_invidious, playlist_id=None, index=None):
return {'error': 'Failed to parse json response'} return {'error': 'Failed to parse json response'}
info = yt_data_extract.extract_watch_info(polymer_json) info = yt_data_extract.extract_watch_info(polymer_json)
# age restriction bypass # request player if it's missing
if info['age_restricted']: # see https://github.com/user234683/youtube-local/issues/22#issuecomment-706395160
print('Fetching age restriction bypass page') if info['age_restricted'] or info['player_response_missing']:
if info['age_restricted']:
print('Age restricted video. Fetching get_video_info page')
else:
print('Missing player. Fetching get_video_info page')
data = { data = {
'video_id': video_id, 'video_id': video_id,
'eurl': 'https://youtube.googleapis.com/v/' + video_id, 'eurl': 'https://youtube.googleapis.com/v/' + video_id,
} }
url = 'https://www.youtube.com/get_video_info?' + urllib.parse.urlencode(data) url = 'https://www.youtube.com/get_video_info?' + urllib.parse.urlencode(data)
video_info_page = util.fetch_url(url, debug_name='get_video_info', report_text='Fetched age restriction bypass page').decode('utf-8') video_info_page = util.fetch_url(url, debug_name='get_video_info', report_text='Fetched get_video_info page').decode('utf-8')
yt_data_extract.update_with_age_restricted_info(info, video_info_page) yt_data_extract.update_with_age_restricted_info(info, video_info_page)
# signature decryption # signature decryption

View File

@ -90,15 +90,20 @@ def remove_redirect(url):
return urllib.parse.parse_qs(query_string)['q'][0] return urllib.parse.parse_qs(query_string)['q'][0]
return url return url
youtube_url_re = re.compile(r'^(?:(?:(?:https?:)?//)?(?:www\.)?youtube\.com)?(/.*)$') norm_url_re = re.compile(r'^(?:(?:https?:)?//)?((?:[\w-]+\.)+[\w-]+)?(/.*)$')
def normalize_url(url): def normalize_url(url):
'''Insert https, resolve relative paths for youtube.com, and put www. infront of youtube.com'''
if url is None: if url is None:
return None return None
match = youtube_url_re.fullmatch(url) match = norm_url_re.fullmatch(url)
if match is None: if match is None:
raise Exception() raise Exception(url)
return 'https://www.youtube.com' + match.group(1) domain = match.group(1) or 'www.youtube.com'
if domain == 'youtube.com':
domain = 'www.youtube.com'
return 'https://' + domain + match.group(2)
def _recover_urls(runs): def _recover_urls(runs):
for run in runs: for run in runs:
@ -240,11 +245,11 @@ def extract_item_info(item, additional_info={}):
)) ))
info['author_url'] = ('https://www.youtube.com/channel/' + info['author_id']) if info['author_id'] else None info['author_url'] = ('https://www.youtube.com/channel/' + info['author_id']) if info['author_id'] else None
info['description'] = extract_formatted_text(multi_get(item, 'descriptionSnippet', 'descriptionText')) info['description'] = extract_formatted_text(multi_get(item, 'descriptionSnippet', 'descriptionText'))
info['thumbnail'] = multi_deep_get(item, info['thumbnail'] = normalize_url(multi_deep_get(item,
['thumbnail', 'thumbnails', 0, 'url'], # videos ['thumbnail', 'thumbnails', 0, 'url'], # videos
['thumbnails', 0, 'thumbnails', 0, 'url'], # playlists ['thumbnails', 0, 'thumbnails', 0, 'url'], # playlists
['thumbnailRenderer', 'showCustomThumbnailRenderer', 'thumbnail', 'thumbnails', 0, 'url'], # shows ['thumbnailRenderer', 'showCustomThumbnailRenderer', 'thumbnail', 'thumbnails', 0, 'url'], # shows
) ))
info['badges'] = [] info['badges'] = []
for badge_node in multi_get(item, 'badges', 'ownerBadges', default=()): for badge_node in multi_get(item, 'badges', 'ownerBadges', default=()):
@ -290,7 +295,7 @@ def extract_item_info(item, additional_info={}):
info['duration'] = extract_str(item.get('lengthText')) info['duration'] = extract_str(item.get('lengthText'))
# if it's an item in a playlist, get its index # if it's an item in a playlist, get its index
if 'index' in item: # url has wrong index on playlist page if 'index' in item: # url has wrong index on playlist page
info['index'] = extract_int(item.get('index')) info['index'] = extract_int(item.get('index'))
elif 'indexText' in item: elif 'indexText' in item:
# Current item in playlist has ▶ instead of the actual index, must # Current item in playlist has ▶ instead of the actual index, must

View File

@ -49,10 +49,10 @@ def extract_channel_info(polymer_json, tab):
if info['short_description'] and len(info['short_description']) > 730: if info['short_description'] and len(info['short_description']) > 730:
info['short_description'] = info['short_description'][0:730] + '...' info['short_description'] = info['short_description'][0:730] + '...'
info['channel_name'] = metadata.get('title') info['channel_name'] = metadata.get('title')
info['avatar'] = multi_deep_get(metadata, info['avatar'] = normalize_url(multi_deep_get(metadata,
['avatar', 'thumbnails', 0, 'url'], ['avatar', 'thumbnails', 0, 'url'],
['thumbnail', 'thumbnails', 0, 'url'], ['thumbnail', 'thumbnails', 0, 'url'],
) ))
channel_url = multi_get(metadata, 'urlCanonical', 'channelUrl') channel_url = multi_get(metadata, 'urlCanonical', 'channelUrl')
if channel_url: if channel_url:
channel_id = get(channel_url.rstrip('/').split('/'), -1) channel_id = get(channel_url.rstrip('/').split('/'), -1)
@ -164,7 +164,7 @@ def extract_playlist_metadata(polymer_json):
metadata['video_count'] = extract_int(header.get('numVideosText')) metadata['video_count'] = extract_int(header.get('numVideosText'))
metadata['description'] = extract_str(header.get('descriptionText'), default='') metadata['description'] = extract_str(header.get('descriptionText'), default='')
metadata['author'] = extract_str(header.get('ownerText')) metadata['author'] = extract_str(header.get('ownerText'))
metadata['author_id'] = multi_deep_get(header, metadata['author_id'] = multi_deep_get(header,
['ownerText', 'runs', 0, 'navigationEndpoint', 'browseEndpoint', 'browseId'], ['ownerText', 'runs', 0, 'navigationEndpoint', 'browseEndpoint', 'browseId'],
['ownerEndpoint', 'browseEndpoint', 'browseId']) ['ownerEndpoint', 'browseEndpoint', 'browseId'])
if metadata['author_id']: if metadata['author_id']:
@ -263,13 +263,13 @@ def extract_comments_info(polymer_json):
# These 3 are sometimes absent, likely because the channel was deleted # These 3 are sometimes absent, likely because the channel was deleted
comment_info['author'] = extract_str(comment_renderer.get('authorText')) comment_info['author'] = extract_str(comment_renderer.get('authorText'))
comment_info['author_url'] = deep_get(comment_renderer, comment_info['author_url'] = normalize_url(deep_get(comment_renderer,
'authorEndpoint', 'commandMetadata', 'webCommandMetadata', 'url') 'authorEndpoint', 'commandMetadata', 'webCommandMetadata', 'url'))
comment_info['author_id'] = deep_get(comment_renderer, comment_info['author_id'] = deep_get(comment_renderer,
'authorEndpoint', 'browseEndpoint', 'browseId') 'authorEndpoint', 'browseEndpoint', 'browseId')
comment_info['author_avatar'] = deep_get(comment_renderer, comment_info['author_avatar'] = normalize_url(deep_get(
'authorThumbnail', 'thumbnails', 0, 'url') comment_renderer, 'authorThumbnail', 'thumbnails', 0, 'url'))
comment_info['id'] = comment_renderer.get('commentId') comment_info['id'] = comment_renderer.get('commentId')
comment_info['text'] = extract_formatted_text(comment_renderer.get('contentText')) comment_info['text'] = extract_formatted_text(comment_renderer.get('contentText'))
comment_info['time_published'] = extract_str(comment_renderer.get('publishedTimeText')) comment_info['time_published'] = extract_str(comment_renderer.get('publishedTimeText'))

View File

@ -172,7 +172,7 @@ def _extract_watch_info_mobile(top_level):
else: else:
info['playlist'] = {} info['playlist'] = {}
info['playlist']['title'] = playlist.get('title') info['playlist']['title'] = playlist.get('title')
info['playlist']['author'] = extract_str(multi_get(playlist, info['playlist']['author'] = extract_str(multi_get(playlist,
'ownerName', 'longBylineText', 'shortBylineText', 'ownerText')) 'ownerName', 'longBylineText', 'shortBylineText', 'ownerText'))
author_id = deep_get(playlist, 'longBylineText', 'runs', 0, author_id = deep_get(playlist, 'longBylineText', 'runs', 0,
'navigationEndpoint', 'browseEndpoint', 'browseId') 'navigationEndpoint', 'browseEndpoint', 'browseId')
@ -447,7 +447,8 @@ def _extract_playability_error(info, player_response, error_prefix=''):
SUBTITLE_FORMATS = ('srv1', 'srv2', 'srv3', 'ttml', 'vtt') SUBTITLE_FORMATS = ('srv1', 'srv2', 'srv3', 'ttml', 'vtt')
def extract_watch_info(polymer_json): def extract_watch_info(polymer_json):
info = {'playability_error': None, 'error': None} info = {'playability_error': None, 'error': None,
'player_response_missing': None}
if isinstance(polymer_json, dict): if isinstance(polymer_json, dict):
top_level = polymer_json top_level = polymer_json
@ -477,6 +478,10 @@ def extract_watch_info(polymer_json):
else: else:
embedded_player_response = {} embedded_player_response = {}
# see https://github.com/user234683/youtube-local/issues/22#issuecomment-706395160
info['player_response_missing'] = not (
player_response or embedded_player_response)
# captions # captions
info['automatic_caption_languages'] = [] info['automatic_caption_languages'] = []
info['manual_caption_languages'] = [] info['manual_caption_languages'] = []
@ -580,7 +585,8 @@ def get_caption_url(info, language, format, automatic=False, translation_languag
return url return url
def update_with_age_restricted_info(info, video_info_page): def update_with_age_restricted_info(info, video_info_page):
ERROR_PREFIX = 'Error bypassing age-restriction: ' '''Inserts urls from 'player_response' in get_video_info page'''
ERROR_PREFIX = 'Error getting missing player or bypassing age-restriction: '
video_info = urllib.parse.parse_qs(video_info_page) video_info = urllib.parse.parse_qs(video_info_page)
player_response = deep_get(video_info, 'player_response', 0) player_response = deep_get(video_info, 'player_response', 0)