fix line endings
This commit is contained in:
parent
2696fb30c2
commit
79937c1c82
16
.gitignore
vendored
16
.gitignore
vendored
@ -1,9 +1,9 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
youtube_dl/
|
youtube_dl/
|
||||||
banned_addresses.txt
|
banned_addresses.txt
|
||||||
youtube/common_old.py
|
youtube/common_old.py
|
||||||
youtube/common_older.py
|
youtube/common_older.py
|
||||||
youtube/watch_old.py
|
youtube/watch_old.py
|
||||||
youtube/watch_later.txt
|
youtube/watch_later.txt
|
@ -1,252 +1,253 @@
|
|||||||
import base64
|
import base64
|
||||||
import youtube.common as common
|
import youtube.common as common
|
||||||
from youtube.common import default_multi_get, URL_ORIGIN, get_thumbnail_url, video_id
|
from youtube.common import default_multi_get, URL_ORIGIN, get_thumbnail_url, video_id
|
||||||
import urllib
|
import urllib
|
||||||
import json
|
import json
|
||||||
from string import Template
|
from string import Template
|
||||||
import youtube.proto as proto
|
import youtube.proto as proto
|
||||||
import html
|
import html
|
||||||
import math
|
import math
|
||||||
import gevent
|
import gevent
|
||||||
import re
|
import re
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
with open("yt_channel_items_template.html", "r") as file:
|
with open("yt_channel_items_template.html", "r") as file:
|
||||||
yt_channel_items_template = Template(file.read())
|
yt_channel_items_template = Template(file.read())
|
||||||
|
|
||||||
with open("yt_channel_about_template.html", "r") as file:
|
with open("yt_channel_about_template.html", "r") as file:
|
||||||
yt_channel_about_template = Template(file.read())
|
yt_channel_about_template = Template(file.read())
|
||||||
|
|
||||||
'''continuation = Proto(
|
'''continuation = Proto(
|
||||||
Field('optional', 'continuation', 80226972, Proto(
|
Field('optional', 'continuation', 80226972, Proto(
|
||||||
Field('optional', 'browse_id', 2, String),
|
Field('optional', 'browse_id', 2, String),
|
||||||
Field('optional', 'params', 3, Base64(Proto(
|
Field('optional', 'params', 3, Base64(Proto(
|
||||||
Field('optional', 'channel_tab', 2, String),
|
Field('optional', 'channel_tab', 2, String),
|
||||||
Field('optional', 'sort', 3, ENUM
|
Field('optional', 'sort', 3, ENUM
|
||||||
Field('optional', 'page', 15, String),
|
Field('optional', 'page', 15, String),
|
||||||
)))
|
)))
|
||||||
))
|
))
|
||||||
)'''
|
)'''
|
||||||
|
|
||||||
|
|
||||||
'''channel_continuation = Proto(
|
'''channel_continuation = Proto(
|
||||||
Field('optional', 'pointless_nest', 80226972, Proto(
|
Field('optional', 'pointless_nest', 80226972, Proto(
|
||||||
Field('optional', 'channel_id', 2, String),
|
Field('optional', 'channel_id', 2, String),
|
||||||
Field('optional', 'continuation_info', 3, Base64(Proto(
|
Field('optional', 'continuation_info', 3, Base64(Proto(
|
||||||
Field('optional', 'channel_tab', 2, String),
|
Field('optional', 'channel_tab', 2, String),
|
||||||
Field('optional', 'sort', 3, ENUM
|
Field('optional', 'sort', 3, ENUM
|
||||||
Field('optional', 'page', 15, String),
|
Field('optional', 'page', 15, String),
|
||||||
)))
|
)))
|
||||||
))
|
))
|
||||||
)'''
|
)'''
|
||||||
|
|
||||||
headers_1 = (
|
headers_1 = (
|
||||||
('Accept', '*/*'),
|
('Accept', '*/*'),
|
||||||
('Accept-Language', 'en-US,en;q=0.5'),
|
('Accept-Language', 'en-US,en;q=0.5'),
|
||||||
('X-YouTube-Client-Name', '1'),
|
('X-YouTube-Client-Name', '1'),
|
||||||
('X-YouTube-Client-Version', '2.20180614'),
|
('X-YouTube-Client-Version', '2.20180614'),
|
||||||
)
|
)
|
||||||
# https://www.youtube.com/browse_ajax?action_continuation=1&direct_render=1&continuation=4qmFsgJAEhhVQzdVY3M0MkZaeTN1WXpqcnF6T0lIc3caJEVnWjJhV1JsYjNNZ0FEZ0JZQUZxQUhvQk1yZ0JBQSUzRCUzRA%3D%3D
|
# https://www.youtube.com/browse_ajax?action_continuation=1&direct_render=1&continuation=4qmFsgJAEhhVQzdVY3M0MkZaeTN1WXpqcnF6T0lIc3caJEVnWjJhV1JsYjNNZ0FEZ0JZQUZxQUhvQk1yZ0JBQSUzRCUzRA%3D%3D
|
||||||
# https://www.youtube.com/browse_ajax?ctoken=4qmFsgJAEhhVQzdVY3M0MkZaeTN1WXpqcnF6T0lIc3caJEVnWjJhV1JsYjNNZ0FEZ0JZQUZxQUhvQk1yZ0JBQSUzRCUzRA%3D%3D&continuation=4qmFsgJAEhhVQzdVY3M0MkZaeTN1WXpqcnF6T0lIc3caJEVnWjJhV1JsYjNNZ0FEZ0JZQUZxQUhvQk1yZ0JBQSUzRCUzRA%3D%3D&itct=CDsQybcCIhMIhZi1krTc2wIVjMicCh2HXQnhKJsc
|
# https://www.youtube.com/browse_ajax?ctoken=4qmFsgJAEhhVQzdVY3M0MkZaeTN1WXpqcnF6T0lIc3caJEVnWjJhV1JsYjNNZ0FEZ0JZQUZxQUhvQk1yZ0JBQSUzRCUzRA%3D%3D&continuation=4qmFsgJAEhhVQzdVY3M0MkZaeTN1WXpqcnF6T0lIc3caJEVnWjJhV1JsYjNNZ0FEZ0JZQUZxQUhvQk1yZ0JBQSUzRCUzRA%3D%3D&itct=CDsQybcCIhMIhZi1krTc2wIVjMicCh2HXQnhKJsc
|
||||||
|
|
||||||
# grid view: 4qmFsgJAEhhVQzdVY3M0MkZaeTN1WXpqcnF6T0lIc3caJEVnWjJhV1JsYjNNZ0FEZ0JZQUZxQUhvQk1yZ0JBQSUzRCUzRA
|
# grid view: 4qmFsgJAEhhVQzdVY3M0MkZaeTN1WXpqcnF6T0lIc3caJEVnWjJhV1JsYjNNZ0FEZ0JZQUZxQUhvQk1yZ0JBQSUzRCUzRA
|
||||||
# list view: 4qmFsgJCEhhVQzdVY3M0MkZaeTN1WXpqcnF6T0lIc3caJkVnWjJhV1JsYjNNWUF5QUFNQUk0QVdBQmFnQjZBVEs0QVFBJTNE
|
# list view: 4qmFsgJCEhhVQzdVY3M0MkZaeTN1WXpqcnF6T0lIc3caJkVnWjJhV1JsYjNNWUF5QUFNQUk0QVdBQmFnQjZBVEs0QVFBJTNE
|
||||||
# SORT:
|
# SORT:
|
||||||
# Popular - 1
|
# Popular - 1
|
||||||
# Oldest - 2
|
# Oldest - 2
|
||||||
# Newest - 3
|
# Newest - 3
|
||||||
|
|
||||||
# view:
|
# view:
|
||||||
# grid: 0 or 1
|
# grid: 0 or 1
|
||||||
# list: 2
|
# list: 2
|
||||||
def channel_ctoken(channel_id, page, sort, tab, view=1):
|
def channel_ctoken(channel_id, page, sort, tab, view=1):
|
||||||
|
|
||||||
tab = proto.string(2, tab )
|
tab = proto.string(2, tab )
|
||||||
sort = proto.uint(3, int(sort))
|
sort = proto.uint(3, int(sort))
|
||||||
page = proto.string(15, str(page) )
|
page = proto.string(15, str(page) )
|
||||||
view = proto.uint(6, int(view))
|
view = proto.uint(6, int(view))
|
||||||
continuation_info = proto.string( 3, proto.percent_b64encode(tab + view + sort + page) )
|
continuation_info = proto.string( 3, proto.percent_b64encode(tab + view + sort + page) )
|
||||||
|
|
||||||
channel_id = proto.string(2, channel_id )
|
channel_id = proto.string(2, channel_id )
|
||||||
pointless_nest = proto.string(80226972, channel_id + continuation_info)
|
pointless_nest = proto.string(80226972, channel_id + continuation_info)
|
||||||
|
|
||||||
return base64.urlsafe_b64encode(pointless_nest).decode('ascii')
|
return base64.urlsafe_b64encode(pointless_nest).decode('ascii')
|
||||||
|
|
||||||
def get_channel_tab(channel_id, page="1", sort=3, tab='videos', view=1):
|
def get_channel_tab(channel_id, page="1", sort=3, tab='videos', view=1):
|
||||||
ctoken = channel_ctoken(channel_id, page, sort, tab, view).replace('=', '%3D')
|
ctoken = channel_ctoken(channel_id, page, sort, tab, view).replace('=', '%3D')
|
||||||
url = "https://www.youtube.com/browse_ajax?ctoken=" + ctoken
|
url = "https://www.youtube.com/browse_ajax?ctoken=" + ctoken
|
||||||
|
|
||||||
print("Sending channel tab ajax request")
|
print("Sending channel tab ajax request")
|
||||||
content = common.fetch_url(url, headers_1)
|
content = common.fetch_url(url, headers_1)
|
||||||
print("Finished recieving channel tab response")
|
print("Finished recieving channel tab response")
|
||||||
|
|
||||||
info = json.loads(content)
|
info = json.loads(content)
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
grid_video_item_template = Template('''
|
grid_video_item_template = Template('''
|
||||||
<div class="small-item-box">
|
<div class="small-item-box">
|
||||||
<div class="small-item">
|
<div class="small-item">
|
||||||
<a class="video-thumbnail-box" href="$url" title="$title">
|
<a class="video-thumbnail-box" href="$url" title="$title">
|
||||||
<img class="video-thumbnail-img" src="$thumbnail">
|
<img class="video-thumbnail-img" src="$thumbnail">
|
||||||
<span class="video-duration">$duration</span>
|
<span class="video-duration">$duration</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="title" href="$url" title="$title">$title</a>
|
<a class="title" href="$url" title="$title">$title</a>
|
||||||
|
|
||||||
<span class="views">$views</span>
|
<span class="views">$views</span>
|
||||||
<time datetime="$datetime">Uploaded $published</time>
|
<time datetime="$datetime">Uploaded $published</time>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<input class="item-checkbox" type="checkbox" name="video_info_list" value="$video_info" form="playlist-add">
|
<input class="item-checkbox" type="checkbox" name="video_info_list" value="$video_info" form="playlist-add">
|
||||||
</div>
|
</div>
|
||||||
''')
|
''')
|
||||||
|
|
||||||
def grid_video_item_info(grid_video_renderer, author):
|
def grid_video_item_info(grid_video_renderer, author):
|
||||||
renderer = grid_video_renderer
|
renderer = grid_video_renderer
|
||||||
return {
|
return {
|
||||||
"title": renderer['title']['simpleText'],
|
"title": renderer['title']['simpleText'],
|
||||||
"id": renderer['videoId'],
|
"id": renderer['videoId'],
|
||||||
"views": renderer['viewCountText'].get('simpleText', None) or renderer['viewCountText']['runs'][0]['text'],
|
"views": renderer['viewCountText'].get('simpleText', None) or renderer['viewCountText']['runs'][0]['text'],
|
||||||
"author": author,
|
"author": author,
|
||||||
"duration": default_multi_get(renderer, 'lengthText', 'simpleText', default=''), # livestreams dont have a length
|
"duration": default_multi_get(renderer, 'lengthText', 'simpleText', default=''), # livestreams dont have a length
|
||||||
"published": default_multi_get(renderer, 'publishedTimeText', 'simpleText', default=''),
|
"published": default_multi_get(renderer, 'publishedTimeText', 'simpleText', default=''),
|
||||||
}
|
}
|
||||||
|
|
||||||
def grid_video_item_html(item):
|
def grid_video_item_html(item):
|
||||||
video_info = json.dumps({key: item[key] for key in ('id', 'title', 'author', 'duration')})
|
video_info = json.dumps({key: item[key] for key in ('id', 'title', 'author', 'duration')})
|
||||||
return grid_video_item_template.substitute(
|
return grid_video_item_template.substitute(
|
||||||
title = html.escape(item["title"]),
|
title = html.escape(item["title"]),
|
||||||
views = item["views"],
|
views = item["views"],
|
||||||
duration = item["duration"],
|
duration = item["duration"],
|
||||||
url = URL_ORIGIN + "/watch?v=" + item["id"],
|
url = URL_ORIGIN + "/watch?v=" + item["id"],
|
||||||
thumbnail = get_thumbnail_url(item['id']),
|
thumbnail = get_thumbnail_url(item['id']),
|
||||||
video_info = html.escape(json.dumps(video_info)),
|
video_info = html.escape(json.dumps(video_info)),
|
||||||
published = item["published"],
|
published = item["published"],
|
||||||
datetime = '', # TODO
|
datetime = '', # TODO
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_number_of_videos(channel_id):
|
def get_number_of_videos(channel_id):
|
||||||
# Uploads playlist
|
# Uploads playlist
|
||||||
playlist_id = 'UU' + channel_id[2:]
|
playlist_id = 'UU' + channel_id[2:]
|
||||||
url = 'https://m.youtube.com/playlist?list=' + playlist_id + '&ajax=1&disable_polymer=true'
|
url = 'https://m.youtube.com/playlist?list=' + playlist_id + '&ajax=1&disable_polymer=true'
|
||||||
print("Getting number of videos")
|
print("Getting number of videos")
|
||||||
response = common.fetch_url(url, common.mobile_ua + headers_1)
|
response = common.fetch_url(url, common.mobile_ua + headers_1)
|
||||||
with open('playlist_debug_metadata', 'wb') as f:
|
with open('playlist_debug_metadata', 'wb') as f:
|
||||||
f.write(response)
|
f.write(response)
|
||||||
response = response.decode('utf-8')
|
response = response.decode('utf-8')
|
||||||
print("Got response for number of videos")
|
print("Got response for number of videos")
|
||||||
return int(re.search(r'"num_videos_text":\s*{(?:"item_type":\s*"formatted_string",)?\s*"runs":\s*\[{"text":\s*"([\d,]*) videos"', response).group(1).replace(',',''))
|
return int(re.search(r'"num_videos_text":\s*{(?:"item_type":\s*"formatted_string",)?\s*"runs":\s*\[{"text":\s*"([\d,]*) videos"', response).group(1).replace(',',''))
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=128)
|
@functools.lru_cache(maxsize=128)
|
||||||
def get_channel_id(username):
|
def get_channel_id(username):
|
||||||
# method that gives the smallest possible response at ~10 kb
|
# method that gives the smallest possible response at ~10 kb
|
||||||
# needs to be as fast as possible
|
# needs to be as fast as possible
|
||||||
url = 'https://m.youtube.com/user/' + username + '/about?ajax=1&disable_polymer=true'
|
url = 'https://m.youtube.com/user/' + username + '/about?ajax=1&disable_polymer=true'
|
||||||
response = common.fetch_url(url, common.mobile_ua + headers_1).decode('utf-8')
|
response = common.fetch_url(url, common.mobile_ua + headers_1).decode('utf-8')
|
||||||
return re.search(r'"channel_id":\s*"([a-zA-Z0-9_-]*)"', response).group(1)
|
return re.search(r'"channel_id":\s*"([a-zA-Z0-9_-]*)"', response).group(1)
|
||||||
|
|
||||||
|
|
||||||
def channel_videos_html(polymer_json, current_page=1, number_of_videos = 1000, current_query_string=''):
|
def channel_videos_html(polymer_json, current_page=1, number_of_videos = 1000, current_query_string=''):
|
||||||
microformat = polymer_json[1]['response']['microformat']['microformatDataRenderer']
|
microformat = polymer_json[1]['response']['microformat']['microformatDataRenderer']
|
||||||
channel_url = microformat['urlCanonical'].rstrip('/')
|
channel_url = microformat['urlCanonical'].rstrip('/')
|
||||||
channel_id = channel_url[channel_url.rfind('/')+1:]
|
channel_id = channel_url[channel_url.rfind('/')+1:]
|
||||||
try:
|
try:
|
||||||
items = polymer_json[1]['response']['continuationContents']['gridContinuation']['items']
|
items = polymer_json[1]['response']['continuationContents']['gridContinuation']['items']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
items = polymer_json[1]['response']['contents']['twoColumnBrowseResultsRenderer']['tabs'][1]['tabRenderer']['content']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]['gridRenderer']['items']
|
items = polymer_json[1]['response']['contents']['twoColumnBrowseResultsRenderer']['tabs'][1]['tabRenderer']['content']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]['gridRenderer']['items']
|
||||||
items_html = ''
|
items_html = ''
|
||||||
for video in items:
|
for video in items:
|
||||||
items_html += grid_video_item_html(grid_video_item_info(video['gridVideoRenderer'], microformat['title']))
|
items_html += grid_video_item_html(grid_video_item_info(video['gridVideoRenderer'], microformat['title']))
|
||||||
|
|
||||||
return yt_channel_items_template.substitute(
|
return yt_channel_items_template.substitute(
|
||||||
channel_title = microformat['title'],
|
channel_title = microformat['title'],
|
||||||
channel_about_url = URL_ORIGIN + "/channel/" + channel_id + "/about",
|
channel_about_url = URL_ORIGIN + "/channel/" + channel_id + "/about",
|
||||||
avatar = '/' + microformat['thumbnail']['thumbnails'][0]['url'],
|
avatar = '/' + microformat['thumbnail']['thumbnails'][0]['url'],
|
||||||
page_title = microformat['title'] + ' - Channel',
|
page_title = microformat['title'] + ' - Channel',
|
||||||
items = items_html,
|
items = items_html,
|
||||||
page_buttons = common.page_buttons_html(current_page, math.ceil(number_of_videos/30), URL_ORIGIN + "/channel/" + channel_id + "/videos", current_query_string)
|
page_buttons = common.page_buttons_html(current_page, math.ceil(number_of_videos/30), URL_ORIGIN + "/channel/" + channel_id + "/videos", current_query_string),
|
||||||
)
|
number_of_results = '{:,}'.format(number_of_videos) + " videos",
|
||||||
|
)
|
||||||
channel_link_template = Template('''
|
|
||||||
<a href="$url">$text</a>''')
|
channel_link_template = Template('''
|
||||||
stat_template = Template('''
|
<a href="$url">$text</a>''')
|
||||||
<li>$stat_value</li>''')
|
stat_template = Template('''
|
||||||
def channel_about_page(polymer_json):
|
<li>$stat_value</li>''')
|
||||||
avatar = '/' + polymer_json[1]['response']['microformat']['microformatDataRenderer']['thumbnail']['thumbnails'][0]['url']
|
def channel_about_page(polymer_json):
|
||||||
# my goodness...
|
avatar = '/' + polymer_json[1]['response']['microformat']['microformatDataRenderer']['thumbnail']['thumbnails'][0]['url']
|
||||||
channel_metadata = polymer_json[1]['response']['contents']['twoColumnBrowseResultsRenderer']['tabs'][5]['tabRenderer']['content']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]['channelAboutFullMetadataRenderer']
|
# my goodness...
|
||||||
channel_links = ''
|
channel_metadata = polymer_json[1]['response']['contents']['twoColumnBrowseResultsRenderer']['tabs'][5]['tabRenderer']['content']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]['channelAboutFullMetadataRenderer']
|
||||||
for link_json in channel_metadata['primaryLinks']:
|
channel_links = ''
|
||||||
channel_links += channel_link_template.substitute(
|
for link_json in channel_metadata['primaryLinks']:
|
||||||
url = html.escape(link_json['navigationEndpoint']['urlEndpoint']['url']),
|
channel_links += channel_link_template.substitute(
|
||||||
text = common.get_plain_text(link_json['title']),
|
url = html.escape(link_json['navigationEndpoint']['urlEndpoint']['url']),
|
||||||
)
|
text = common.get_plain_text(link_json['title']),
|
||||||
|
)
|
||||||
stats = ''
|
|
||||||
for stat_name in ('subscriberCountText', 'joinedDateText', 'viewCountText', 'country'):
|
stats = ''
|
||||||
try:
|
for stat_name in ('subscriberCountText', 'joinedDateText', 'viewCountText', 'country'):
|
||||||
stat_value = common.get_plain_text(channel_metadata[stat_name])
|
try:
|
||||||
except KeyError:
|
stat_value = common.get_plain_text(channel_metadata[stat_name])
|
||||||
continue
|
except KeyError:
|
||||||
else:
|
continue
|
||||||
stats += stat_template.substitute(stat_value=stat_value)
|
else:
|
||||||
try:
|
stats += stat_template.substitute(stat_value=stat_value)
|
||||||
description = common.format_text_runs(common.get_formatted_text(channel_metadata['description']))
|
try:
|
||||||
except KeyError:
|
description = common.format_text_runs(common.get_formatted_text(channel_metadata['description']))
|
||||||
description = ''
|
except KeyError:
|
||||||
return yt_channel_about_template.substitute(
|
description = ''
|
||||||
page_title = common.get_plain_text(channel_metadata['title']) + ' - About',
|
return yt_channel_about_template.substitute(
|
||||||
channel_title = common.get_plain_text(channel_metadata['title']),
|
page_title = common.get_plain_text(channel_metadata['title']) + ' - About',
|
||||||
avatar = html.escape(avatar),
|
channel_title = common.get_plain_text(channel_metadata['title']),
|
||||||
description = description,
|
avatar = html.escape(avatar),
|
||||||
links = channel_links,
|
description = description,
|
||||||
stats = stats,
|
links = channel_links,
|
||||||
channel_videos_url = common.URL_ORIGIN + '/channel/' + channel_metadata['channelId'] + '/videos',
|
stats = stats,
|
||||||
)
|
channel_videos_url = common.URL_ORIGIN + '/channel/' + channel_metadata['channelId'] + '/videos',
|
||||||
|
)
|
||||||
def get_channel_page(url, query_string=''):
|
|
||||||
path_components = url.rstrip('/').lstrip('/').split('/')
|
def get_channel_page(url, query_string=''):
|
||||||
channel_id = path_components[0]
|
path_components = url.rstrip('/').lstrip('/').split('/')
|
||||||
try:
|
channel_id = path_components[0]
|
||||||
tab = path_components[1]
|
try:
|
||||||
except IndexError:
|
tab = path_components[1]
|
||||||
tab = 'videos'
|
except IndexError:
|
||||||
|
tab = 'videos'
|
||||||
parameters = urllib.parse.parse_qs(query_string)
|
|
||||||
page_number = int(common.default_multi_get(parameters, 'page', 0, default='1'))
|
parameters = urllib.parse.parse_qs(query_string)
|
||||||
sort = common.default_multi_get(parameters, 'sort', 0, default='3')
|
page_number = int(common.default_multi_get(parameters, 'page', 0, default='1'))
|
||||||
view = common.default_multi_get(parameters, 'view', 0, default='1')
|
sort = common.default_multi_get(parameters, 'sort', 0, default='3')
|
||||||
|
view = common.default_multi_get(parameters, 'view', 0, default='1')
|
||||||
if tab == 'videos':
|
|
||||||
tasks = (
|
if tab == 'videos':
|
||||||
gevent.spawn(get_number_of_videos, channel_id ),
|
tasks = (
|
||||||
gevent.spawn(get_channel_tab, channel_id, page_number, sort, 'videos', view)
|
gevent.spawn(get_number_of_videos, channel_id ),
|
||||||
)
|
gevent.spawn(get_channel_tab, channel_id, page_number, sort, 'videos', view)
|
||||||
gevent.joinall(tasks)
|
)
|
||||||
number_of_videos, polymer_json = tasks[0].value, tasks[1].value
|
gevent.joinall(tasks)
|
||||||
|
number_of_videos, polymer_json = tasks[0].value, tasks[1].value
|
||||||
return channel_videos_html(polymer_json, page_number, number_of_videos, query_string)
|
|
||||||
elif tab == 'about':
|
return channel_videos_html(polymer_json, page_number, number_of_videos, query_string)
|
||||||
polymer_json = common.fetch_url('https://www.youtube.com/channel/' + channel_id + '/about?pbj=1', headers_1)
|
elif tab == 'about':
|
||||||
polymer_json = json.loads(polymer_json)
|
polymer_json = common.fetch_url('https://www.youtube.com/channel/' + channel_id + '/about?pbj=1', headers_1)
|
||||||
return channel_about_page(polymer_json)
|
polymer_json = json.loads(polymer_json)
|
||||||
else:
|
return channel_about_page(polymer_json)
|
||||||
raise ValueError('Unknown channel tab: ' + tab)
|
else:
|
||||||
|
raise ValueError('Unknown channel tab: ' + tab)
|
||||||
def get_user_page(url, query_string=''):
|
|
||||||
path_components = url.rstrip('/').lstrip('/').split('/')
|
def get_user_page(url, query_string=''):
|
||||||
username = path_components[0]
|
path_components = url.rstrip('/').lstrip('/').split('/')
|
||||||
try:
|
username = path_components[0]
|
||||||
page = path_components[1]
|
try:
|
||||||
except IndexError:
|
page = path_components[1]
|
||||||
page = 'videos'
|
except IndexError:
|
||||||
if page == 'videos':
|
page = 'videos'
|
||||||
polymer_json = common.fetch_url('https://www.youtube.com/user/' + username + '/videos?pbj=1', headers_1)
|
if page == 'videos':
|
||||||
polymer_json = json.loads(polymer_json)
|
polymer_json = common.fetch_url('https://www.youtube.com/user/' + username + '/videos?pbj=1', headers_1)
|
||||||
return channel_videos_html(polymer_json)
|
polymer_json = json.loads(polymer_json)
|
||||||
elif page == 'about':
|
return channel_videos_html(polymer_json)
|
||||||
polymer_json = common.fetch_url('https://www.youtube.com/user/' + username + '/about?pbj=1', headers_1)
|
elif page == 'about':
|
||||||
polymer_json = json.loads(polymer_json)
|
polymer_json = common.fetch_url('https://www.youtube.com/user/' + username + '/about?pbj=1', headers_1)
|
||||||
return channel_about_page(polymer_json)
|
polymer_json = json.loads(polymer_json)
|
||||||
else:
|
return channel_about_page(polymer_json)
|
||||||
|
else:
|
||||||
raise ValueError('Unknown channel page: ' + page)
|
raise ValueError('Unknown channel page: ' + page)
|
@ -1,59 +1,59 @@
|
|||||||
.comments{
|
.comments{
|
||||||
grid-row-gap: 10px;
|
grid-row-gap: 10px;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content:start;
|
align-content:start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment{
|
.comment{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 0fr 0fr 1fr;
|
grid-template-columns: 0fr 0fr 1fr;
|
||||||
grid-template-rows: 0fr 0fr 0fr 0fr;
|
grid-template-rows: 0fr 0fr 0fr 0fr;
|
||||||
background-color: #dadada;
|
background-color: #dadada;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment .author-avatar{
|
.comment .author-avatar{
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1 / span 3;
|
grid-row: 1 / span 3;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment address{
|
.comment address{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
margin-right:15px;
|
margin-right:15px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment .text{
|
.comment .text{
|
||||||
grid-column: 2 / span 2;
|
grid-column: 2 / span 2;
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment time{
|
.comment time{
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.comment .likes{
|
.comment .likes{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
grid-row:3;
|
grid-row:3;
|
||||||
font-weight:bold;
|
font-weight:bold;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment .replies{
|
.comment .replies{
|
||||||
grid-column:2 / span 2;
|
grid-column:2 / span 2;
|
||||||
grid-row:4;
|
grid-row:4;
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-comments{
|
.more-comments{
|
||||||
justify-self:center;
|
justify-self:center;
|
||||||
|
|
||||||
}
|
}
|
@ -1,166 +1,166 @@
|
|||||||
import json
|
import json
|
||||||
import youtube.proto as proto
|
import youtube.proto as proto
|
||||||
import base64
|
import base64
|
||||||
from youtube.common import uppercase_escape, default_multi_get, format_text_runs, URL_ORIGIN, fetch_url
|
from youtube.common import uppercase_escape, default_multi_get, format_text_runs, URL_ORIGIN, fetch_url
|
||||||
from string import Template
|
from string import Template
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib
|
import urllib
|
||||||
import html
|
import html
|
||||||
comment_template = Template('''
|
comment_template = Template('''
|
||||||
<div class="comment-container">
|
<div class="comment-container">
|
||||||
<div class="comment">
|
<div class="comment">
|
||||||
<a class="author-avatar" href="$author_url" title="$author">
|
<a class="author-avatar" href="$author_url" title="$author">
|
||||||
<img class="author-avatar-img" src="$author_avatar">
|
<img class="author-avatar-img" src="$author_avatar">
|
||||||
</a>
|
</a>
|
||||||
<address>
|
<address>
|
||||||
<a class="author" href="$author_url" title="$author">$author</a>
|
<a class="author" href="$author_url" title="$author">$author</a>
|
||||||
</address>
|
</address>
|
||||||
<span class="text">$text</span>
|
<span class="text">$text</span>
|
||||||
<time datetime="$datetime">$published</time>
|
<time datetime="$datetime">$published</time>
|
||||||
<span class="likes">$likes</span>
|
<span class="likes">$likes</span>
|
||||||
$replies
|
$replies
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
''')
|
''')
|
||||||
reply_link_template = Template('''
|
reply_link_template = Template('''
|
||||||
<a href="$url" class="replies">View replies</a>
|
<a href="$url" class="replies">View replies</a>
|
||||||
''')
|
''')
|
||||||
with open("yt_comments_template.html", "r") as file:
|
with open("yt_comments_template.html", "r") as file:
|
||||||
yt_comments_template = Template(file.read())
|
yt_comments_template = Template(file.read())
|
||||||
|
|
||||||
|
|
||||||
# <a class="replies-link" href="$replies_url">$replies_link_text</a>
|
# <a class="replies-link" href="$replies_url">$replies_link_text</a>
|
||||||
|
|
||||||
|
|
||||||
# Here's what I know about the secret key (starting with ASJN_i)
|
# Here's what I know about the secret key (starting with ASJN_i)
|
||||||
# *The secret key definitely contains the following information (or perhaps the information is stored at youtube's servers):
|
# *The secret key definitely contains the following information (or perhaps the information is stored at youtube's servers):
|
||||||
# -Video id
|
# -Video id
|
||||||
# -Offset
|
# -Offset
|
||||||
# -Sort
|
# -Sort
|
||||||
# *If the video id or sort in the ctoken contradicts the ASJN, the response is an error. The offset encoded outside the ASJN is ignored entirely.
|
# *If the video id or sort in the ctoken contradicts the ASJN, the response is an error. The offset encoded outside the ASJN is ignored entirely.
|
||||||
# *The ASJN is base64 encoded data, indicated by the fact that the character after "ASJN_i" is one of ("0", "1", "2", "3")
|
# *The ASJN is base64 encoded data, indicated by the fact that the character after "ASJN_i" is one of ("0", "1", "2", "3")
|
||||||
# *The encoded data is not valid protobuf
|
# *The encoded data is not valid protobuf
|
||||||
# *The encoded data (after the 5 or so bytes that are always the same) is indistinguishable from random data according to a battery of randomness tests
|
# *The encoded data (after the 5 or so bytes that are always the same) is indistinguishable from random data according to a battery of randomness tests
|
||||||
# *The ASJN in the ctoken provided by a response changes in regular intervals of about a second or two.
|
# *The ASJN in the ctoken provided by a response changes in regular intervals of about a second or two.
|
||||||
# *Old ASJN's continue to work, and start at the same comment even if new comments have been posted since
|
# *Old ASJN's continue to work, and start at the same comment even if new comments have been posted since
|
||||||
# *The ASJN has no relation with any of the data in the response it came from
|
# *The ASJN has no relation with any of the data in the response it came from
|
||||||
|
|
||||||
def make_comment_ctoken(video_id, sort=0, offset=0, secret_key=''):
|
def make_comment_ctoken(video_id, sort=0, offset=0, secret_key=''):
|
||||||
video_id = proto.as_bytes(video_id)
|
video_id = proto.as_bytes(video_id)
|
||||||
secret_key = proto.as_bytes(secret_key)
|
secret_key = proto.as_bytes(secret_key)
|
||||||
|
|
||||||
|
|
||||||
page_info = proto.string(4,video_id) + proto.uint(6, sort)
|
page_info = proto.string(4,video_id) + proto.uint(6, sort)
|
||||||
offset_information = proto.nested(4, page_info) + proto.uint(5, offset)
|
offset_information = proto.nested(4, page_info) + proto.uint(5, offset)
|
||||||
if secret_key:
|
if secret_key:
|
||||||
offset_information = proto.string(1, secret_key) + offset_information
|
offset_information = proto.string(1, secret_key) + offset_information
|
||||||
|
|
||||||
result = proto.nested(2, proto.string(2, video_id)) + proto.uint(3,6) + proto.nested(6, offset_information)
|
result = proto.nested(2, proto.string(2, video_id)) + proto.uint(3,6) + proto.nested(6, offset_information)
|
||||||
return base64.urlsafe_b64encode(result).decode('ascii')
|
return base64.urlsafe_b64encode(result).decode('ascii')
|
||||||
|
|
||||||
mobile_headers = {
|
mobile_headers = {
|
||||||
'Host': 'm.youtube.com',
|
'Host': 'm.youtube.com',
|
||||||
'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',
|
'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',
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
'Accept-Language': 'en-US,en;q=0.5',
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
'X-YouTube-Client-Name': '2',
|
'X-YouTube-Client-Name': '2',
|
||||||
'X-YouTube-Client-Version': '1.20180613',
|
'X-YouTube-Client-Version': '1.20180613',
|
||||||
}
|
}
|
||||||
def request_comments(ctoken, replies=False):
|
def request_comments(ctoken, replies=False):
|
||||||
if replies: # let's make it use different urls for no reason despite all the data being encoded
|
if replies: # let's make it use different urls for no reason despite all the data being encoded
|
||||||
base_url = "https://m.youtube.com/watch_comment?action_get_comment_replies=1&ctoken="
|
base_url = "https://m.youtube.com/watch_comment?action_get_comment_replies=1&ctoken="
|
||||||
else:
|
else:
|
||||||
base_url = "https://m.youtube.com/watch_comment?action_get_comments=1&ctoken="
|
base_url = "https://m.youtube.com/watch_comment?action_get_comments=1&ctoken="
|
||||||
url = base_url + ctoken.replace("=", "%3D") + "&pbj=1"
|
url = base_url + ctoken.replace("=", "%3D") + "&pbj=1"
|
||||||
print("Sending comments ajax request")
|
print("Sending comments ajax request")
|
||||||
for i in range(0,8): # don't retry more than 8 times
|
for i in range(0,8): # don't retry more than 8 times
|
||||||
content = fetch_url(url, headers=mobile_headers)
|
content = fetch_url(url, headers=mobile_headers)
|
||||||
if content[0:4] == b")]}'": # random closing characters included at beginning of response for some reason
|
if content[0:4] == b")]}'": # random closing characters included at beginning of response for some reason
|
||||||
content = content[4:]
|
content = content[4:]
|
||||||
elif content[0:10] == b'\n<!DOCTYPE': # occasionally returns html instead of json for no reason
|
elif content[0:10] == b'\n<!DOCTYPE': # occasionally returns html instead of json for no reason
|
||||||
content = b''
|
content = b''
|
||||||
print("got <!DOCTYPE>, retrying")
|
print("got <!DOCTYPE>, retrying")
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
'''with open('comments_debug', 'wb') as f:
|
'''with open('comments_debug', 'wb') as f:
|
||||||
f.write(content)'''
|
f.write(content)'''
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def parse_comments(content, replies=False):
|
def parse_comments(content, replies=False):
|
||||||
try:
|
try:
|
||||||
content = json.loads(uppercase_escape(content.decode('utf-8')))
|
content = json.loads(uppercase_escape(content.decode('utf-8')))
|
||||||
#print(content)
|
#print(content)
|
||||||
comments_raw = content['content']['continuation_contents']['contents']
|
comments_raw = content['content']['continuation_contents']['contents']
|
||||||
ctoken = default_multi_get(content, 'content', 'continuation_contents', 'continuations', 0, 'continuation', default='')
|
ctoken = default_multi_get(content, 'content', 'continuation_contents', 'continuations', 0, 'continuation', default='')
|
||||||
|
|
||||||
comments = []
|
comments = []
|
||||||
for comment_raw in comments_raw:
|
for comment_raw in comments_raw:
|
||||||
replies_url = ''
|
replies_url = ''
|
||||||
if not replies:
|
if not replies:
|
||||||
if comment_raw['replies'] is not None:
|
if comment_raw['replies'] is not None:
|
||||||
ctoken = comment_raw['replies']['continuations'][0]['continuation']
|
ctoken = comment_raw['replies']['continuations'][0]['continuation']
|
||||||
replies_url = URL_ORIGIN + '/comments?ctoken=' + ctoken + "&replies=1"
|
replies_url = URL_ORIGIN + '/comments?ctoken=' + ctoken + "&replies=1"
|
||||||
comment_raw = comment_raw['comment']
|
comment_raw = comment_raw['comment']
|
||||||
comment = {
|
comment = {
|
||||||
'author': comment_raw['author']['runs'][0]['text'],
|
'author': comment_raw['author']['runs'][0]['text'],
|
||||||
'author_url': comment_raw['author_endpoint']['url'],
|
'author_url': comment_raw['author_endpoint']['url'],
|
||||||
'author_avatar': comment_raw['author_thumbnail']['url'],
|
'author_avatar': comment_raw['author_thumbnail']['url'],
|
||||||
'likes': comment_raw['like_count'],
|
'likes': comment_raw['like_count'],
|
||||||
'published': comment_raw['published_time']['runs'][0]['text'],
|
'published': comment_raw['published_time']['runs'][0]['text'],
|
||||||
'text': comment_raw['content']['runs'],
|
'text': comment_raw['content']['runs'],
|
||||||
'reply_count': '',
|
'reply_count': '',
|
||||||
'replies_url': replies_url,
|
'replies_url': replies_url,
|
||||||
}
|
}
|
||||||
comments.append(comment)
|
comments.append(comment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print('Error parsing comments: ' + str(e))
|
print('Error parsing comments: ' + str(e))
|
||||||
comments = ()
|
comments = ()
|
||||||
ctoken = ''
|
ctoken = ''
|
||||||
else:
|
else:
|
||||||
print("Finished getting and parsing comments")
|
print("Finished getting and parsing comments")
|
||||||
return {'ctoken': ctoken, 'comments': comments}
|
return {'ctoken': ctoken, 'comments': comments}
|
||||||
|
|
||||||
def get_comments_html(result):
|
def get_comments_html(result):
|
||||||
html_result = ''
|
html_result = ''
|
||||||
for comment in result['comments']:
|
for comment in result['comments']:
|
||||||
replies = ''
|
replies = ''
|
||||||
if comment['replies_url']:
|
if comment['replies_url']:
|
||||||
replies = reply_link_template.substitute(url=comment['replies_url'])
|
replies = reply_link_template.substitute(url=comment['replies_url'])
|
||||||
html_result += comment_template.substitute(
|
html_result += comment_template.substitute(
|
||||||
author=html.escape(comment['author']),
|
author=html.escape(comment['author']),
|
||||||
author_url = URL_ORIGIN + comment['author_url'],
|
author_url = URL_ORIGIN + comment['author_url'],
|
||||||
author_avatar = '/' + comment['author_avatar'],
|
author_avatar = '/' + comment['author_avatar'],
|
||||||
likes = str(comment['likes']) + ' likes' if str(comment['likes']) != '0' else '',
|
likes = str(comment['likes']) + ' likes' if str(comment['likes']) != '0' else '',
|
||||||
published = comment['published'],
|
published = comment['published'],
|
||||||
text = format_text_runs(comment['text']),
|
text = format_text_runs(comment['text']),
|
||||||
datetime = '', #TODO
|
datetime = '', #TODO
|
||||||
replies=replies,
|
replies=replies,
|
||||||
#replies='',
|
#replies='',
|
||||||
)
|
)
|
||||||
return html_result, result['ctoken']
|
return html_result, result['ctoken']
|
||||||
|
|
||||||
def video_comments(video_id, sort=0, offset=0, secret_key=''):
|
def video_comments(video_id, sort=0, offset=0, secret_key=''):
|
||||||
result = parse_comments(request_comments(make_comment_ctoken(video_id, sort, offset, secret_key)))
|
result = parse_comments(request_comments(make_comment_ctoken(video_id, sort, offset, secret_key)))
|
||||||
return get_comments_html(result)
|
return get_comments_html(result)
|
||||||
|
|
||||||
more_comments_template = Template('''<a class="page-button more-comments" href="$url">More comments</a>''')
|
more_comments_template = Template('''<a class="page-button more-comments" href="$url">More comments</a>''')
|
||||||
|
|
||||||
def get_comments_page(query_string):
|
def get_comments_page(query_string):
|
||||||
parameters = urllib.parse.parse_qs(query_string)
|
parameters = urllib.parse.parse_qs(query_string)
|
||||||
ctoken = parameters['ctoken'][0]
|
ctoken = parameters['ctoken'][0]
|
||||||
replies = default_multi_get(parameters, 'replies', 0, default="0") == "1"
|
replies = default_multi_get(parameters, 'replies', 0, default="0") == "1"
|
||||||
|
|
||||||
result = parse_comments(request_comments(ctoken, replies), replies)
|
result = parse_comments(request_comments(ctoken, replies), replies)
|
||||||
comments_html, ctoken = get_comments_html(result)
|
comments_html, ctoken = get_comments_html(result)
|
||||||
if ctoken == '':
|
if ctoken == '':
|
||||||
more_comments_button = ''
|
more_comments_button = ''
|
||||||
else:
|
else:
|
||||||
more_comments_button = more_comments_template.substitute(url = URL_ORIGIN + '/comments?ctoken=' + ctoken)
|
more_comments_button = more_comments_template.substitute(url = URL_ORIGIN + '/comments?ctoken=' + ctoken)
|
||||||
|
|
||||||
return yt_comments_template.substitute(
|
return yt_comments_template.substitute(
|
||||||
comments = comments_html,
|
comments = comments_html,
|
||||||
page_title = 'Comments',
|
page_title = 'Comments',
|
||||||
more_comments_button=more_comments_button,
|
more_comments_button=more_comments_button,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
1278
youtube/common.py
1278
youtube/common.py
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,11 @@
|
|||||||
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
|
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
|
||||||
<ShortName>Youtube local</ShortName>
|
<ShortName>Youtube local</ShortName>
|
||||||
<Description>no CIA shit in the background</Description>
|
<Description>no CIA shit in the background</Description>
|
||||||
<InputEncoding>UTF-8</InputEncoding>
|
<InputEncoding>UTF-8</InputEncoding>
|
||||||
<Image width="16" height="16">data:image/x-icon;base64,R0lGODlhEAAQAJECAP8AAAAAAP///wAAACH5BAEAAAIALAAAAAAQABAAAAIplI+py+0NogQuyBDEnEd2kHkfFWUamEzmpZSfmaIHPHrRguUm/fT+UwAAOw==</Image>
|
<Image width="16" height="16">data:image/x-icon;base64,R0lGODlhEAAQAJECAP8AAAAAAP///wAAACH5BAEAAAIALAAAAAAQABAAAAIplI+py+0NogQuyBDEnEd2kHkfFWUamEzmpZSfmaIHPHrRguUm/fT+UwAAOw==</Image>
|
||||||
|
|
||||||
<Url type="text/html" method="GET" template="http://localhost/youtube.com/search">
|
<Url type="text/html" method="GET" template="http://localhost/youtube.com/search">
|
||||||
<Param name="query" value="{searchTerms}"/>
|
<Param name="query" value="{searchTerms}"/>
|
||||||
</Url>
|
</Url>
|
||||||
<SearchForm>http://localhost/youtube.com/search</SearchForm>
|
<SearchForm>http://localhost/youtube.com/search</SearchForm>
|
||||||
</SearchPlugin>
|
</SearchPlugin>
|
@ -1,243 +1,243 @@
|
|||||||
import base64
|
import base64
|
||||||
import youtube.common as common
|
import youtube.common as common
|
||||||
import urllib
|
import urllib
|
||||||
import json
|
import json
|
||||||
from string import Template
|
from string import Template
|
||||||
import youtube.proto as proto
|
import youtube.proto as proto
|
||||||
import gevent
|
import gevent
|
||||||
import math
|
import math
|
||||||
|
|
||||||
with open("yt_playlist_template.html", "r") as file:
|
with open("yt_playlist_template.html", "r") as file:
|
||||||
yt_playlist_template = Template(file.read())
|
yt_playlist_template = Template(file.read())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def youtube_obfuscated_endian(offset):
|
def youtube_obfuscated_endian(offset):
|
||||||
if offset < 128:
|
if offset < 128:
|
||||||
return bytes((offset,))
|
return bytes((offset,))
|
||||||
first_byte = 255 & offset
|
first_byte = 255 & offset
|
||||||
second_byte = 255 & (offset >> 7)
|
second_byte = 255 & (offset >> 7)
|
||||||
second_byte = second_byte | 1
|
second_byte = second_byte | 1
|
||||||
|
|
||||||
# The next 2 bytes encode the offset in little endian order,
|
# The next 2 bytes encode the offset in little endian order,
|
||||||
# BUT, it's done in a strange way. The least significant bit (LSB) of the second byte is not part
|
# BUT, it's done in a strange way. The least significant bit (LSB) of the second byte is not part
|
||||||
# of the offset. Instead, to get the number which the two bytes encode, that LSB
|
# of the offset. Instead, to get the number which the two bytes encode, that LSB
|
||||||
# of the second byte is combined with the most significant bit (MSB) of the first byte
|
# of the second byte is combined with the most significant bit (MSB) of the first byte
|
||||||
# in a logical AND. Replace the two bits with the result of the AND to get the two little endian
|
# in a logical AND. Replace the two bits with the result of the AND to get the two little endian
|
||||||
# bytes that represent the offset.
|
# bytes that represent the offset.
|
||||||
|
|
||||||
return bytes((first_byte, second_byte))
|
return bytes((first_byte, second_byte))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# just some garbage that's required, don't know what it means, if it means anything.
|
# just some garbage that's required, don't know what it means, if it means anything.
|
||||||
ctoken_header = b'\xe2\xa9\x85\xb2\x02' # e2 a9 85 b2 02
|
ctoken_header = b'\xe2\xa9\x85\xb2\x02' # e2 a9 85 b2 02
|
||||||
|
|
||||||
def byte(x):
|
def byte(x):
|
||||||
return bytes((x,))
|
return bytes((x,))
|
||||||
|
|
||||||
# TL;DR: the offset is hidden inside 3 nested base 64 encodes with random junk data added on the side periodically
|
# TL;DR: the offset is hidden inside 3 nested base 64 encodes with random junk data added on the side periodically
|
||||||
def create_ctoken(playlist_id, offset):
|
def create_ctoken(playlist_id, offset):
|
||||||
obfuscated_offset = b'\x08' + youtube_obfuscated_endian(offset) # 0x08 slapped on for no apparent reason
|
obfuscated_offset = b'\x08' + youtube_obfuscated_endian(offset) # 0x08 slapped on for no apparent reason
|
||||||
obfuscated_offset = b'PT:' + base64.urlsafe_b64encode(obfuscated_offset).replace(b'=', b'')
|
obfuscated_offset = b'PT:' + base64.urlsafe_b64encode(obfuscated_offset).replace(b'=', b'')
|
||||||
obfuscated_offset = b'z' + byte(len(obfuscated_offset)) + obfuscated_offset
|
obfuscated_offset = b'z' + byte(len(obfuscated_offset)) + obfuscated_offset
|
||||||
obfuscated_offset = base64.urlsafe_b64encode(obfuscated_offset).replace(b'=', b'%3D')
|
obfuscated_offset = base64.urlsafe_b64encode(obfuscated_offset).replace(b'=', b'%3D')
|
||||||
|
|
||||||
playlist_bytes = b'VL' + bytes(playlist_id, 'ascii')
|
playlist_bytes = b'VL' + bytes(playlist_id, 'ascii')
|
||||||
main_info = b'\x12' + byte(len(playlist_bytes)) + playlist_bytes + b'\x1a' + byte(len(obfuscated_offset)) + obfuscated_offset
|
main_info = b'\x12' + byte(len(playlist_bytes)) + playlist_bytes + b'\x1a' + byte(len(obfuscated_offset)) + obfuscated_offset
|
||||||
|
|
||||||
ctoken = base64.urlsafe_b64encode(ctoken_header + byte(len(main_info)) + main_info)
|
ctoken = base64.urlsafe_b64encode(ctoken_header + byte(len(main_info)) + main_info)
|
||||||
|
|
||||||
return ctoken.decode('ascii')
|
return ctoken.decode('ascii')
|
||||||
|
|
||||||
def playlist_ctoken(playlist_id, offset):
|
def playlist_ctoken(playlist_id, offset):
|
||||||
|
|
||||||
offset = proto.uint(1, offset)
|
offset = proto.uint(1, offset)
|
||||||
# this is just obfuscation as far as I can tell. It doesn't even follow protobuf
|
# this is just obfuscation as far as I can tell. It doesn't even follow protobuf
|
||||||
offset = b'PT:' + proto.unpadded_b64encode(offset)
|
offset = b'PT:' + proto.unpadded_b64encode(offset)
|
||||||
offset = proto.string(15, offset)
|
offset = proto.string(15, offset)
|
||||||
|
|
||||||
continuation_info = proto.string( 3, proto.percent_b64encode(offset) )
|
continuation_info = proto.string( 3, proto.percent_b64encode(offset) )
|
||||||
|
|
||||||
playlist_id = proto.string(2, 'VL' + playlist_id )
|
playlist_id = proto.string(2, 'VL' + playlist_id )
|
||||||
pointless_nest = proto.string(80226972, playlist_id + continuation_info)
|
pointless_nest = proto.string(80226972, playlist_id + continuation_info)
|
||||||
|
|
||||||
return base64.urlsafe_b64encode(pointless_nest).decode('ascii')
|
return base64.urlsafe_b64encode(pointless_nest).decode('ascii')
|
||||||
|
|
||||||
# initial request types:
|
# initial request types:
|
||||||
# polymer_json: https://m.youtube.com/playlist?list=PLv3TTBr1W_9tppikBxAE_G6qjWdBljBHJ&pbj=1&lact=0
|
# polymer_json: https://m.youtube.com/playlist?list=PLv3TTBr1W_9tppikBxAE_G6qjWdBljBHJ&pbj=1&lact=0
|
||||||
# ajax json: https://m.youtube.com/playlist?list=PLv3TTBr1W_9tppikBxAE_G6qjWdBljBHJ&pbj=1&lact=0 with header X-YouTube-Client-Version: 1.20180418
|
# ajax json: https://m.youtube.com/playlist?list=PLv3TTBr1W_9tppikBxAE_G6qjWdBljBHJ&pbj=1&lact=0 with header X-YouTube-Client-Version: 1.20180418
|
||||||
|
|
||||||
|
|
||||||
# continuation request types:
|
# continuation request types:
|
||||||
# polymer_json: https://m.youtube.com/playlist?&ctoken=[...]&pbj=1
|
# polymer_json: https://m.youtube.com/playlist?&ctoken=[...]&pbj=1
|
||||||
# ajax json: https://m.youtube.com/playlist?action_continuation=1&ajax=1&ctoken=[...]
|
# ajax json: https://m.youtube.com/playlist?action_continuation=1&ajax=1&ctoken=[...]
|
||||||
|
|
||||||
|
|
||||||
headers_1 = (
|
headers_1 = (
|
||||||
('Accept', '*/*'),
|
('Accept', '*/*'),
|
||||||
('Accept-Language', 'en-US,en;q=0.5'),
|
('Accept-Language', 'en-US,en;q=0.5'),
|
||||||
('X-YouTube-Client-Name', '1'),
|
('X-YouTube-Client-Name', '1'),
|
||||||
('X-YouTube-Client-Version', '2.20180614'),
|
('X-YouTube-Client-Version', '2.20180614'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def playlist_first_page(playlist_id):
|
def playlist_first_page(playlist_id):
|
||||||
url = 'https://m.youtube.com/playlist?list=' + playlist_id + '&ajax=1&disable_polymer=true'
|
url = 'https://m.youtube.com/playlist?list=' + playlist_id + '&ajax=1&disable_polymer=true'
|
||||||
content = common.fetch_url(url, common.mobile_ua + headers_1)
|
content = common.fetch_url(url, common.mobile_ua + headers_1)
|
||||||
if content[0:4] == b")]}'":
|
if content[0:4] == b")]}'":
|
||||||
content = content[4:]
|
content = content[4:]
|
||||||
content = json.loads(common.uppercase_escape(content.decode('utf-8')))
|
content = json.loads(common.uppercase_escape(content.decode('utf-8')))
|
||||||
return content
|
return content
|
||||||
|
|
||||||
ajax_info_dispatch = {
|
ajax_info_dispatch = {
|
||||||
'view_count_text': ('views', common.get_text),
|
'view_count_text': ('views', common.get_text),
|
||||||
'num_videos_text': ('size', lambda node: common.get_text(node).split(' ')[0]),
|
'num_videos_text': ('size', lambda node: common.get_text(node).split(' ')[0]),
|
||||||
'thumbnail': ('thumbnail', lambda node: node.url),
|
'thumbnail': ('thumbnail', lambda node: node.url),
|
||||||
'title': ('title', common.get_text),
|
'title': ('title', common.get_text),
|
||||||
'owner_text': ('author', common.get_text),
|
'owner_text': ('author', common.get_text),
|
||||||
'owner_endpoint': ('author_url', lambda node: node.url),
|
'owner_endpoint': ('author_url', lambda node: node.url),
|
||||||
'description': ('description', common.get_formatted_text),
|
'description': ('description', common.get_formatted_text),
|
||||||
|
|
||||||
}
|
}
|
||||||
def metadata_info(ajax_json):
|
def metadata_info(ajax_json):
|
||||||
info = {}
|
info = {}
|
||||||
try:
|
try:
|
||||||
for key, node in ajax_json.items():
|
for key, node in ajax_json.items():
|
||||||
try:
|
try:
|
||||||
simple_key, function = dispatch[key]
|
simple_key, function = dispatch[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
continue
|
continue
|
||||||
info[simple_key] = function(node)
|
info[simple_key] = function(node)
|
||||||
return info
|
return info
|
||||||
except (KeyError,IndexError):
|
except (KeyError,IndexError):
|
||||||
print(ajax_json)
|
print(ajax_json)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#https://m.youtube.com/playlist?itct=CBMQybcCIhMIptj9xJaJ2wIV2JKcCh3Idwu-&ctoken=4qmFsgI2EiRWTFBMT3kwajlBdmxWWlB0bzZJa2pLZnB1MFNjeC0tN1BHVEMaDmVnWlFWRHBEUWxFJTNE&pbj=1
|
#https://m.youtube.com/playlist?itct=CBMQybcCIhMIptj9xJaJ2wIV2JKcCh3Idwu-&ctoken=4qmFsgI2EiRWTFBMT3kwajlBdmxWWlB0bzZJa2pLZnB1MFNjeC0tN1BHVEMaDmVnWlFWRHBEUWxFJTNE&pbj=1
|
||||||
def get_videos_ajax(playlist_id, page):
|
def get_videos_ajax(playlist_id, page):
|
||||||
|
|
||||||
url = "https://m.youtube.com/playlist?action_continuation=1&ajax=1&ctoken=" + playlist_ctoken(playlist_id, (int(page)-1)*20)
|
url = "https://m.youtube.com/playlist?action_continuation=1&ajax=1&ctoken=" + playlist_ctoken(playlist_id, (int(page)-1)*20)
|
||||||
headers = {
|
headers = {
|
||||||
'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',
|
'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',
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
'Accept-Language': 'en-US,en;q=0.5',
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
'X-YouTube-Client-Name': '2',
|
'X-YouTube-Client-Name': '2',
|
||||||
'X-YouTube-Client-Version': '1.20180508',
|
'X-YouTube-Client-Version': '1.20180508',
|
||||||
}
|
}
|
||||||
print("Sending playlist ajax request")
|
print("Sending playlist ajax request")
|
||||||
content = common.fetch_url(url, headers)
|
content = common.fetch_url(url, headers)
|
||||||
with open('playlist_debug', 'wb') as f:
|
with open('playlist_debug', 'wb') as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
content = content[4:]
|
content = content[4:]
|
||||||
print("Finished recieving playlist response")
|
print("Finished recieving playlist response")
|
||||||
|
|
||||||
info = json.loads(common.uppercase_escape(content.decode('utf-8')))
|
info = json.loads(common.uppercase_escape(content.decode('utf-8')))
|
||||||
return info
|
return info
|
||||||
|
|
||||||
def get_playlist_videos(ajax_json):
|
def get_playlist_videos(ajax_json):
|
||||||
videos = []
|
videos = []
|
||||||
#info = get_bloated_playlist_videos(playlist_id, page)
|
#info = get_bloated_playlist_videos(playlist_id, page)
|
||||||
#print(info)
|
#print(info)
|
||||||
video_list = ajax_json['content']['continuation_contents']['contents']
|
video_list = ajax_json['content']['continuation_contents']['contents']
|
||||||
|
|
||||||
|
|
||||||
for video_json_crap in video_list:
|
for video_json_crap in video_list:
|
||||||
try:
|
try:
|
||||||
videos.append({
|
videos.append({
|
||||||
"title": video_json_crap["title"]['runs'][0]['text'],
|
"title": video_json_crap["title"]['runs'][0]['text'],
|
||||||
"id": video_json_crap["video_id"],
|
"id": video_json_crap["video_id"],
|
||||||
"views": "",
|
"views": "",
|
||||||
"duration": common.default_multi_get(video_json_crap, 'length', 'runs', 0, 'text', default=''), # livestreams dont have a length
|
"duration": common.default_multi_get(video_json_crap, 'length', 'runs', 0, 'text', default=''), # livestreams dont have a length
|
||||||
"author": video_json_crap['short_byline']['runs'][0]['text'],
|
"author": video_json_crap['short_byline']['runs'][0]['text'],
|
||||||
"author_url": '',
|
"author_url": '',
|
||||||
"published": '',
|
"published": '',
|
||||||
'playlist_index': '',
|
'playlist_index': '',
|
||||||
|
|
||||||
})
|
})
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
print(video_json_crap)
|
print(video_json_crap)
|
||||||
raise
|
raise
|
||||||
return videos
|
return videos
|
||||||
|
|
||||||
def get_playlist_videos_format2(playlist_id, page):
|
def get_playlist_videos_format2(playlist_id, page):
|
||||||
videos = []
|
videos = []
|
||||||
info = get_bloated_playlist_videos(playlist_id, page)
|
info = get_bloated_playlist_videos(playlist_id, page)
|
||||||
video_list = info['response']['continuationContents']['playlistVideoListContinuation']['contents']
|
video_list = info['response']['continuationContents']['playlistVideoListContinuation']['contents']
|
||||||
|
|
||||||
for video_json_crap in video_list:
|
for video_json_crap in video_list:
|
||||||
|
|
||||||
video_json_crap = video_json_crap['videoRenderer']
|
video_json_crap = video_json_crap['videoRenderer']
|
||||||
|
|
||||||
try:
|
try:
|
||||||
videos.append({
|
videos.append({
|
||||||
"title": video_json_crap["title"]['runs'][0]['text'],
|
"title": video_json_crap["title"]['runs'][0]['text'],
|
||||||
"video_id": video_json_crap["videoId"],
|
"video_id": video_json_crap["videoId"],
|
||||||
"views": "",
|
"views": "",
|
||||||
"duration": common.default_multi_get(video_json_crap, 'lengthText', 'runs', 0, 'text', default=''), # livestreams dont have a length
|
"duration": common.default_multi_get(video_json_crap, 'lengthText', 'runs', 0, 'text', default=''), # livestreams dont have a length
|
||||||
"uploader": video_json_crap['shortBylineText']['runs'][0]['text'],
|
"uploader": video_json_crap['shortBylineText']['runs'][0]['text'],
|
||||||
"uploader_url": common.ORIGIN_URL + video_json_crap['shortBylineText']['runs'][0]['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url'],
|
"uploader_url": common.ORIGIN_URL + video_json_crap['shortBylineText']['runs'][0]['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url'],
|
||||||
"published": common.default_multi_get(video_json_crap, 'publishedTimeText', 'simpleText', default=''),
|
"published": common.default_multi_get(video_json_crap, 'publishedTimeText', 'simpleText', default=''),
|
||||||
'playlist_index': video_json_crap['index']['runs'][0]['text'],
|
'playlist_index': video_json_crap['index']['runs'][0]['text'],
|
||||||
|
|
||||||
})
|
})
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
print(video_json_crap)
|
print(video_json_crap)
|
||||||
raise
|
raise
|
||||||
return videos
|
return videos
|
||||||
|
|
||||||
|
|
||||||
def playlist_videos_html(ajax_json):
|
def playlist_videos_html(ajax_json):
|
||||||
result = ''
|
result = ''
|
||||||
for info in get_playlist_videos(ajax_json):
|
for info in get_playlist_videos(ajax_json):
|
||||||
result += common.small_video_item_html(info)
|
result += common.small_video_item_html(info)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
playlist_stat_template = Template('''
|
playlist_stat_template = Template('''
|
||||||
<div>$stat</div>''')
|
<div>$stat</div>''')
|
||||||
def get_playlist_page(query_string):
|
def get_playlist_page(query_string):
|
||||||
parameters = urllib.parse.parse_qs(query_string)
|
parameters = urllib.parse.parse_qs(query_string)
|
||||||
playlist_id = parameters['list'][0]
|
playlist_id = parameters['list'][0]
|
||||||
page = parameters.get("page", "1")[0]
|
page = parameters.get("page", "1")[0]
|
||||||
if page == "1":
|
if page == "1":
|
||||||
first_page_json = playlist_first_page(playlist_id)
|
first_page_json = playlist_first_page(playlist_id)
|
||||||
this_page_json = first_page_json
|
this_page_json = first_page_json
|
||||||
else:
|
else:
|
||||||
tasks = (
|
tasks = (
|
||||||
gevent.spawn(playlist_first_page, playlist_id ),
|
gevent.spawn(playlist_first_page, playlist_id ),
|
||||||
gevent.spawn(get_videos_ajax, playlist_id, page)
|
gevent.spawn(get_videos_ajax, playlist_id, page)
|
||||||
)
|
)
|
||||||
gevent.joinall(tasks)
|
gevent.joinall(tasks)
|
||||||
first_page_json, this_page_json = tasks[0].value, tasks[1].value
|
first_page_json, this_page_json = tasks[0].value, tasks[1].value
|
||||||
|
|
||||||
try:
|
try:
|
||||||
video_list = this_page_json['content']['section_list']['contents'][0]['contents'][0]['contents']
|
video_list = this_page_json['content']['section_list']['contents'][0]['contents'][0]['contents']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
video_list = this_page_json['content']['continuation_contents']['contents']
|
video_list = this_page_json['content']['continuation_contents']['contents']
|
||||||
videos_html = ''
|
videos_html = ''
|
||||||
for video_json in video_list:
|
for video_json in video_list:
|
||||||
info = common.ajax_info(video_json)
|
info = common.ajax_info(video_json)
|
||||||
videos_html += common.video_item_html(info, common.small_video_item_template)
|
videos_html += common.video_item_html(info, common.small_video_item_template)
|
||||||
|
|
||||||
|
|
||||||
metadata = common.ajax_info(first_page_json['content']['playlist_header'])
|
metadata = common.ajax_info(first_page_json['content']['playlist_header'])
|
||||||
video_count = int(metadata['size'].replace(',', ''))
|
video_count = int(metadata['size'].replace(',', ''))
|
||||||
page_buttons = common.page_buttons_html(int(page), math.ceil(video_count/20), common.URL_ORIGIN + "/playlist", query_string)
|
page_buttons = common.page_buttons_html(int(page), math.ceil(video_count/20), common.URL_ORIGIN + "/playlist", query_string)
|
||||||
|
|
||||||
html_ready = common.get_html_ready(metadata)
|
html_ready = common.get_html_ready(metadata)
|
||||||
html_ready['page_title'] = html_ready['title'] + ' - Page ' + str(page)
|
html_ready['page_title'] = html_ready['title'] + ' - Page ' + str(page)
|
||||||
|
|
||||||
stats = ''
|
stats = ''
|
||||||
stats += playlist_stat_template.substitute(stat=html_ready['size'] + ' videos')
|
stats += playlist_stat_template.substitute(stat=html_ready['size'] + ' videos')
|
||||||
stats += playlist_stat_template.substitute(stat=html_ready['views'])
|
stats += playlist_stat_template.substitute(stat=html_ready['views'])
|
||||||
return yt_playlist_template.substitute(
|
return yt_playlist_template.substitute(
|
||||||
videos = videos_html,
|
videos = videos_html,
|
||||||
page_buttons = page_buttons,
|
page_buttons = page_buttons,
|
||||||
stats = stats,
|
stats = stats,
|
||||||
**html_ready
|
**html_ready
|
||||||
)
|
)
|
128
youtube/proto.py
128
youtube/proto.py
@ -1,65 +1,65 @@
|
|||||||
from math import ceil
|
from math import ceil
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
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.
|
||||||
encoded_bytes = bytearray(needed_bytes)
|
encoded_bytes = bytearray(needed_bytes)
|
||||||
for i in range(0, needed_bytes - 1):
|
for i in range(0, needed_bytes - 1):
|
||||||
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)
|
||||||
nested = string
|
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'')
|
||||||
|
|
||||||
def as_bytes(value):
|
def as_bytes(value):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return value.encode('ascii')
|
return value.encode('ascii')
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
@ -1,231 +1,231 @@
|
|||||||
import json
|
import json
|
||||||
import urllib
|
import urllib
|
||||||
import html
|
import html
|
||||||
from string import Template
|
from string import Template
|
||||||
import base64
|
import base64
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from youtube.common import default_multi_get, get_thumbnail_url, URL_ORIGIN
|
from youtube.common import default_multi_get, get_thumbnail_url, URL_ORIGIN
|
||||||
import youtube.common as common
|
import youtube.common as common
|
||||||
|
|
||||||
with open("yt_search_results_template.html", "r") as file:
|
with open("yt_search_results_template.html", "r") as file:
|
||||||
yt_search_results_template = file.read()
|
yt_search_results_template = file.read()
|
||||||
|
|
||||||
with open("yt_search_template.html", "r") as file:
|
with open("yt_search_template.html", "r") as file:
|
||||||
yt_search_template = file.read()
|
yt_search_template = file.read()
|
||||||
|
|
||||||
page_button_template = Template('''<a class="page-button" href="$href">$page</a>''')
|
page_button_template = Template('''<a class="page-button" href="$href">$page</a>''')
|
||||||
current_page_button_template = Template('''<div class="page-button">$page</div>''')
|
current_page_button_template = Template('''<div class="page-button">$page</div>''')
|
||||||
video_result_template = '''
|
video_result_template = '''
|
||||||
<div class="medium-item">
|
<div class="medium-item">
|
||||||
<a class="video-thumbnail-box" href="$video_url" title="$video_title">
|
<a class="video-thumbnail-box" href="$video_url" title="$video_title">
|
||||||
<img class="video-thumbnail-img" src="$thumbnail_url">
|
<img class="video-thumbnail-img" src="$thumbnail_url">
|
||||||
<span class="video-duration">$length</span>
|
<span class="video-duration">$length</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="title" href="$video_url">$video_title</a>
|
<a class="title" href="$video_url">$video_title</a>
|
||||||
|
|
||||||
<address>Uploaded by <a href="$uploader_channel_url">$uploader</a></address>
|
<address>Uploaded by <a href="$uploader_channel_url">$uploader</a></address>
|
||||||
<span class="views">$views</span>
|
<span class="views">$views</span>
|
||||||
|
|
||||||
|
|
||||||
<time datetime="$datetime">Uploaded $upload_date</time>
|
<time datetime="$datetime">Uploaded $upload_date</time>
|
||||||
|
|
||||||
<span class="description">$description</span>
|
<span class="description">$description</span>
|
||||||
</div>
|
</div>
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Sort: 1
|
# Sort: 1
|
||||||
# Upload date: 2
|
# Upload date: 2
|
||||||
# View count: 3
|
# View count: 3
|
||||||
# Rating: 1
|
# Rating: 1
|
||||||
# Offset: 9
|
# Offset: 9
|
||||||
# Filters: 2
|
# Filters: 2
|
||||||
# Upload date: 1
|
# Upload date: 1
|
||||||
# Type: 2
|
# Type: 2
|
||||||
# Duration: 3
|
# Duration: 3
|
||||||
|
|
||||||
|
|
||||||
features = {
|
features = {
|
||||||
'4k': 14,
|
'4k': 14,
|
||||||
'hd': 4,
|
'hd': 4,
|
||||||
'hdr': 25,
|
'hdr': 25,
|
||||||
'subtitles': 5,
|
'subtitles': 5,
|
||||||
'creative_commons': 6,
|
'creative_commons': 6,
|
||||||
'3d': 7,
|
'3d': 7,
|
||||||
'live': 8,
|
'live': 8,
|
||||||
'purchased': 9,
|
'purchased': 9,
|
||||||
'360': 15,
|
'360': 15,
|
||||||
'location': 23,
|
'location': 23,
|
||||||
}
|
}
|
||||||
|
|
||||||
def page_number_to_sp_parameter(page):
|
def page_number_to_sp_parameter(page):
|
||||||
offset = (int(page) - 1)*20 # 20 results per page
|
offset = (int(page) - 1)*20 # 20 results per page
|
||||||
first_byte = 255 & offset
|
first_byte = 255 & offset
|
||||||
second_byte = 255 & (offset >> 7)
|
second_byte = 255 & (offset >> 7)
|
||||||
second_byte = second_byte | 1
|
second_byte = second_byte | 1
|
||||||
|
|
||||||
# 0b01001000 is required, and is always the same.
|
# 0b01001000 is required, and is always the same.
|
||||||
# The next 2 bytes encode the offset in little endian order,
|
# The next 2 bytes encode the offset in little endian order,
|
||||||
# BUT, it's done in a strange way. The least significant bit (LSB) of the second byte is not part
|
# BUT, it's done in a strange way. The least significant bit (LSB) of the second byte is not part
|
||||||
# of the offset. Instead, to get the number which the two bytes encode, that LSB
|
# of the offset. Instead, to get the number which the two bytes encode, that LSB
|
||||||
# of the second byte is combined with the most significant bit (MSB) of the first byte
|
# of the second byte is combined with the most significant bit (MSB) of the first byte
|
||||||
# in a logical AND. Replace the two bits with the result of the AND to get the two little endian
|
# in a logical AND. Replace the two bits with the result of the AND to get the two little endian
|
||||||
# bytes that represent the offset.
|
# bytes that represent the offset.
|
||||||
# I figured this out by trial and error on the sp parameter. I don't know why it's done like this;
|
# I figured this out by trial and error on the sp parameter. I don't know why it's done like this;
|
||||||
# perhaps it's just obfuscation.
|
# perhaps it's just obfuscation.
|
||||||
param_bytes = bytes((0b01001000, first_byte, second_byte))
|
param_bytes = bytes((0b01001000, first_byte, second_byte))
|
||||||
param_encoded = urllib.parse.quote(base64.urlsafe_b64encode(param_bytes))
|
param_encoded = urllib.parse.quote(base64.urlsafe_b64encode(param_bytes))
|
||||||
return param_encoded
|
return param_encoded
|
||||||
|
|
||||||
def get_search_json(query, page):
|
def get_search_json(query, page):
|
||||||
url = "https://www.youtube.com/results?search_query=" + urllib.parse.quote_plus(query)
|
url = "https://www.youtube.com/results?search_query=" + urllib.parse.quote_plus(query)
|
||||||
headers = {
|
headers = {
|
||||||
'Host': 'www.youtube.com',
|
'Host': 'www.youtube.com',
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)',
|
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)',
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
'Accept-Language': 'en-US,en;q=0.5',
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
'X-YouTube-Client-Name': '1',
|
'X-YouTube-Client-Name': '1',
|
||||||
'X-YouTube-Client-Version': '2.20180418',
|
'X-YouTube-Client-Version': '2.20180418',
|
||||||
}
|
}
|
||||||
url += "&pbj=1&sp=" + page_number_to_sp_parameter(page)
|
url += "&pbj=1&sp=" + page_number_to_sp_parameter(page)
|
||||||
content = common.fetch_url(url, headers=headers)
|
content = common.fetch_url(url, headers=headers)
|
||||||
info = json.loads(content)
|
info = json.loads(content)
|
||||||
return info
|
return info
|
||||||
|
|
||||||
"""def get_search_info(query, page):
|
"""def get_search_info(query, page):
|
||||||
result_info = dict()
|
result_info = dict()
|
||||||
info = get_bloated_search_info(query, page)
|
info = get_bloated_search_info(query, page)
|
||||||
|
|
||||||
estimated_results = int(info[1]['response']['estimatedResults'])
|
estimated_results = int(info[1]['response']['estimatedResults'])
|
||||||
estimated_pages = ceil(estimated_results/20)
|
estimated_pages = ceil(estimated_results/20)
|
||||||
result_info['estimated_results'] = estimated_results
|
result_info['estimated_results'] = estimated_results
|
||||||
result_info['estimated_pages'] = estimated_pages
|
result_info['estimated_pages'] = estimated_pages
|
||||||
|
|
||||||
result_info['results'] = []
|
result_info['results'] = []
|
||||||
# this is what you get when you hire H-1B's
|
# this is what you get when you hire H-1B's
|
||||||
video_list = info[1]['response']['contents']['twoColumnSearchResultsRenderer']['primaryContents']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents']
|
video_list = info[1]['response']['contents']['twoColumnSearchResultsRenderer']['primaryContents']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents']
|
||||||
|
|
||||||
|
|
||||||
for video_json_crap in video_list:
|
for video_json_crap in video_list:
|
||||||
# they have a dictionary whose only content is another dictionary...
|
# they have a dictionary whose only content is another dictionary...
|
||||||
try:
|
try:
|
||||||
type = list(video_json_crap.keys())[0]
|
type = list(video_json_crap.keys())[0]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
continue #channelRenderer or playlistRenderer
|
continue #channelRenderer or playlistRenderer
|
||||||
'''description = ""
|
'''description = ""
|
||||||
for text_run in video_json_crap["descriptionSnippet"]["runs"]:
|
for text_run in video_json_crap["descriptionSnippet"]["runs"]:
|
||||||
if text_run.get("bold", False):
|
if text_run.get("bold", False):
|
||||||
description += "<b>" + html.escape'''
|
description += "<b>" + html.escape'''
|
||||||
try:
|
try:
|
||||||
result_info['results'].append({
|
result_info['results'].append({
|
||||||
"title": video_json_crap["title"]["simpleText"],
|
"title": video_json_crap["title"]["simpleText"],
|
||||||
"video_id": video_json_crap["videoId"],
|
"video_id": video_json_crap["videoId"],
|
||||||
"description": video_json_crap.get("descriptionSnippet",dict()).get('runs',[]), # a list of text runs (formmated), rather than plain text
|
"description": video_json_crap.get("descriptionSnippet",dict()).get('runs',[]), # a list of text runs (formmated), rather than plain text
|
||||||
"thumbnail": get_thumbnail_url(video_json_crap["videoId"]),
|
"thumbnail": get_thumbnail_url(video_json_crap["videoId"]),
|
||||||
"views_text": video_json_crap['viewCountText'].get('simpleText', None) or video_json_crap['viewCountText']['runs'][0]['text'],
|
"views_text": video_json_crap['viewCountText'].get('simpleText', None) or video_json_crap['viewCountText']['runs'][0]['text'],
|
||||||
"length_text": default_multi_get(video_json_crap, 'lengthText', 'simpleText', default=''), # livestreams dont have a length
|
"length_text": default_multi_get(video_json_crap, 'lengthText', 'simpleText', default=''), # livestreams dont have a length
|
||||||
"uploader": video_json_crap['longBylineText']['runs'][0]['text'],
|
"uploader": video_json_crap['longBylineText']['runs'][0]['text'],
|
||||||
"uploader_url": URL_ORIGIN + video_json_crap['longBylineText']['runs'][0]['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url'],
|
"uploader_url": URL_ORIGIN + video_json_crap['longBylineText']['runs'][0]['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url'],
|
||||||
"published_time_text": default_multi_get(video_json_crap, 'publishedTimeText', 'simpleText', default=''),
|
"published_time_text": default_multi_get(video_json_crap, 'publishedTimeText', 'simpleText', default=''),
|
||||||
|
|
||||||
})
|
})
|
||||||
except KeyError:
|
except KeyError:
|
||||||
print(video_json_crap)
|
print(video_json_crap)
|
||||||
raise
|
raise
|
||||||
return result_info"""
|
return result_info"""
|
||||||
|
|
||||||
|
|
||||||
def page_buttons_html(page_start, page_end, current_page, query):
|
def page_buttons_html(page_start, page_end, current_page, query):
|
||||||
result = ""
|
result = ""
|
||||||
for page in range(page_start, page_end+1):
|
for page in range(page_start, page_end+1):
|
||||||
if page == current_page:
|
if page == current_page:
|
||||||
template = current_page_button_template
|
template = current_page_button_template
|
||||||
else:
|
else:
|
||||||
template = page_button_template
|
template = page_button_template
|
||||||
result += template.substitute(page=page, href=URL_ORIGIN + "/search?query=" + urllib.parse.quote_plus(query) + "&page=" + str(page))
|
result += template.substitute(page=page, href=URL_ORIGIN + "/search?query=" + urllib.parse.quote_plus(query) + "&page=" + str(page))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
showing_results_for = Template('''
|
showing_results_for = Template('''
|
||||||
<div>Showing results for <a>$corrected_query</a></div>
|
<div>Showing results for <a>$corrected_query</a></div>
|
||||||
<div>Search instead for <a href="$original_query_url">$original_query</a></div>
|
<div>Search instead for <a href="$original_query_url">$original_query</a></div>
|
||||||
''')
|
''')
|
||||||
did_you_mean = Template('''
|
did_you_mean = Template('''
|
||||||
<div>Did you mean <a href="$corrected_query_url">$corrected_query</a></div>
|
<div>Did you mean <a href="$corrected_query_url">$corrected_query</a></div>
|
||||||
''')
|
''')
|
||||||
def get_search_page(query_string, parameters=()):
|
def get_search_page(query_string, parameters=()):
|
||||||
qs_query = urllib.parse.parse_qs(query_string)
|
qs_query = urllib.parse.parse_qs(query_string)
|
||||||
if len(qs_query) == 0:
|
if len(qs_query) == 0:
|
||||||
return yt_search_template
|
return yt_search_template
|
||||||
query = qs_query["query"][0]
|
query = qs_query["query"][0]
|
||||||
page = qs_query.get("page", "1")[0]
|
page = qs_query.get("page", "1")[0]
|
||||||
|
|
||||||
info = get_search_json(query, page)
|
info = get_search_json(query, page)
|
||||||
|
|
||||||
estimated_results = int(info[1]['response']['estimatedResults'])
|
estimated_results = int(info[1]['response']['estimatedResults'])
|
||||||
estimated_pages = ceil(estimated_results/20)
|
estimated_pages = ceil(estimated_results/20)
|
||||||
results = info[1]['response']['contents']['twoColumnSearchResultsRenderer']['primaryContents']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents']
|
results = info[1]['response']['contents']['twoColumnSearchResultsRenderer']['primaryContents']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents']
|
||||||
|
|
||||||
corrections = ''
|
corrections = ''
|
||||||
result_list_html = ""
|
result_list_html = ""
|
||||||
for renderer in results:
|
for renderer in results:
|
||||||
type = list(renderer.keys())[0]
|
type = list(renderer.keys())[0]
|
||||||
if type == 'shelfRenderer':
|
if type == 'shelfRenderer':
|
||||||
continue
|
continue
|
||||||
if type == 'didYouMeanRenderer':
|
if type == 'didYouMeanRenderer':
|
||||||
renderer = renderer[type]
|
renderer = renderer[type]
|
||||||
corrected_query_string = urllib.parse.parse_qs(query_string)
|
corrected_query_string = urllib.parse.parse_qs(query_string)
|
||||||
corrected_query_string['query'] = [renderer['correctedQueryEndpoint']['searchEndpoint']['query']]
|
corrected_query_string['query'] = [renderer['correctedQueryEndpoint']['searchEndpoint']['query']]
|
||||||
corrected_query_url = URL_ORIGIN + '/search?' + common.make_query_string(corrected_query_string)
|
corrected_query_url = URL_ORIGIN + '/search?' + common.make_query_string(corrected_query_string)
|
||||||
corrections = did_you_mean.substitute(
|
corrections = did_you_mean.substitute(
|
||||||
corrected_query_url = corrected_query_url,
|
corrected_query_url = corrected_query_url,
|
||||||
corrected_query = common.format_text_runs(renderer['correctedQuery']['runs']),
|
corrected_query = common.format_text_runs(renderer['correctedQuery']['runs']),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
if type == 'showingResultsForRenderer':
|
if type == 'showingResultsForRenderer':
|
||||||
renderer = renderer[type]
|
renderer = renderer[type]
|
||||||
no_autocorrect_query_string = urllib.parse.parse_qs(query_string)
|
no_autocorrect_query_string = urllib.parse.parse_qs(query_string)
|
||||||
no_autocorrect_query_string['autocorrect'] = ['0']
|
no_autocorrect_query_string['autocorrect'] = ['0']
|
||||||
no_autocorrect_query_url = URL_ORIGIN + '/search?' + common.make_query_string(no_autocorrect_query_string)
|
no_autocorrect_query_url = URL_ORIGIN + '/search?' + common.make_query_string(no_autocorrect_query_string)
|
||||||
corrections = showing_results_for.substitute(
|
corrections = showing_results_for.substitute(
|
||||||
corrected_query = common.format_text_runs(renderer['correctedQuery']['runs']),
|
corrected_query = common.format_text_runs(renderer['correctedQuery']['runs']),
|
||||||
original_query_url = no_autocorrect_query_url,
|
original_query_url = no_autocorrect_query_url,
|
||||||
original_query = html.escape(renderer['originalQuery']['simpleText']),
|
original_query = html.escape(renderer['originalQuery']['simpleText']),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
result_list_html += common.renderer_html(renderer, current_query_string=query_string)
|
result_list_html += common.renderer_html(renderer, current_query_string=query_string)
|
||||||
'''type = list(result.keys())[0]
|
'''type = list(result.keys())[0]
|
||||||
result = result[type]
|
result = result[type]
|
||||||
if type == "showingResultsForRenderer":
|
if type == "showingResultsForRenderer":
|
||||||
url = URL_ORIGIN + "/search"
|
url = URL_ORIGIN + "/search"
|
||||||
if len(parameters) > 0:
|
if len(parameters) > 0:
|
||||||
url += ';' + ';'.join(parameters)
|
url += ';' + ';'.join(parameters)
|
||||||
url += '?' + '&'.join(key + '=' + ','.join(values) for key,values in qs_query.items())
|
url += '?' + '&'.join(key + '=' + ','.join(values) for key,values in qs_query.items())
|
||||||
|
|
||||||
result_list_html += showing_results_for_template.substitute(
|
result_list_html += showing_results_for_template.substitute(
|
||||||
corrected_query=common.format_text_runs(result['correctedQuery']['runs']),
|
corrected_query=common.format_text_runs(result['correctedQuery']['runs']),
|
||||||
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result_list_html += common.html_functions[type](result)'''
|
result_list_html += common.html_functions[type](result)'''
|
||||||
|
|
||||||
page = int(page)
|
page = int(page)
|
||||||
if page <= 5:
|
if page <= 5:
|
||||||
page_start = 1
|
page_start = 1
|
||||||
page_end = min(9, estimated_pages)
|
page_end = min(9, estimated_pages)
|
||||||
else:
|
else:
|
||||||
page_start = page - 4
|
page_start = page - 4
|
||||||
page_end = min(page + 4, estimated_pages)
|
page_end = min(page + 4, estimated_pages)
|
||||||
|
|
||||||
|
|
||||||
result = Template(yt_search_results_template).substitute(
|
result = Template(yt_search_results_template).substitute(
|
||||||
results = result_list_html,
|
results = result_list_html,
|
||||||
page_title = query + " - Search",
|
page_title = query + " - Search",
|
||||||
search_box_value = html.escape(query),
|
search_box_value = html.escape(query),
|
||||||
number_of_results = '{:,}'.format(estimated_results),
|
number_of_results = '{:,}'.format(estimated_results),
|
||||||
number_of_pages = '{:,}'.format(estimated_pages),
|
number_of_pages = '{:,}'.format(estimated_pages),
|
||||||
page_buttons = page_buttons_html(page_start, page_end, page, query),
|
page_buttons = page_buttons_html(page_start, page_end, page, query),
|
||||||
corrections = corrections
|
corrections = corrections
|
||||||
)
|
)
|
||||||
return result
|
return result
|
@ -1,271 +1,271 @@
|
|||||||
h1, h2, h3, h4, h5, h6, div{
|
h1, h2, h3, h4, h5, h6, div{
|
||||||
margin:0;
|
margin:0;
|
||||||
padding:0;
|
padding:0;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
body{
|
body{
|
||||||
margin:0;
|
margin:0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color:#222;
|
color:#222;
|
||||||
|
|
||||||
|
|
||||||
background-color:#cccccc;
|
background-color:#cccccc;
|
||||||
|
|
||||||
min-height:100vh;
|
min-height:100vh;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-rows: 50px 1fr;
|
grid-template-rows: 50px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
header{
|
header{
|
||||||
background-color:#333333;
|
background-color:#333333;
|
||||||
|
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
main{
|
main{
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
button{
|
button{
|
||||||
padding:0; /* Fuck browser-specific styling. Fix your shit mozilla */
|
padding:0; /* Fuck browser-specific styling. Fix your shit mozilla */
|
||||||
}
|
}
|
||||||
address{
|
address{
|
||||||
font-style:normal;
|
font-style:normal;
|
||||||
}
|
}
|
||||||
#site-search{
|
#site-search{
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 0fr;
|
grid-template-columns: 1fr 0fr;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-search .search-box{
|
#site-search .search-box{
|
||||||
align-self:center;
|
align-self:center;
|
||||||
height:25px;
|
height:25px;
|
||||||
border:0;
|
border:0;
|
||||||
|
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
}
|
}
|
||||||
#site-search .search-button{
|
#site-search .search-button{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
align-self:center;
|
align-self:center;
|
||||||
height:25px;
|
height:25px;
|
||||||
|
|
||||||
border-style:solid;
|
border-style:solid;
|
||||||
border-width:1px;
|
border-width:1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.full-item{
|
.full-item{
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 0fr 0fr 0fr 0fr 0fr;
|
grid-template-rows: 0fr 0fr 0fr 0fr 0fr;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
|
||||||
}
|
}
|
||||||
.full-item video{
|
.full-item video{
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
}
|
}
|
||||||
.full-item .title{
|
.full-item .title{
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
grid-row:2;
|
grid-row:2;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.full-item address{
|
.full-item address{
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 3;
|
grid-row: 3;
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
.full-item .views{
|
.full-item .views{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 3;
|
grid-row: 3;
|
||||||
justify-self:end;
|
justify-self:end;
|
||||||
}
|
}
|
||||||
.full-item time{
|
.full-item time{
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 4;
|
grid-row: 4;
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
}
|
}
|
||||||
.full-item .likes-dislikes{
|
.full-item .likes-dislikes{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 4;
|
grid-row: 4;
|
||||||
justify-self:end;
|
justify-self:end;
|
||||||
}
|
}
|
||||||
.full-item .description{
|
.full-item .description{
|
||||||
background-color:#d0d0d0;
|
background-color:#d0d0d0;
|
||||||
margin-top:8px;
|
margin-top:8px;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
grid-row: 5;
|
grid-row: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.medium-item{
|
.medium-item{
|
||||||
background-color:#bcbcbc;
|
background-color:#bcbcbc;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
grid-template-columns: 246px 1fr 0fr;
|
grid-template-columns: 246px 1fr 0fr;
|
||||||
grid-template-rows: 0fr 0fr 0fr 0fr 0fr 1fr;
|
grid-template-rows: 0fr 0fr 0fr 0fr 0fr 1fr;
|
||||||
}
|
}
|
||||||
.medium-item .title{
|
.medium-item .title{
|
||||||
grid-column:2 / span 2;
|
grid-column:2 / span 2;
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.medium-item address{
|
.medium-item address{
|
||||||
display:inline;
|
display:inline;
|
||||||
}
|
}
|
||||||
/*.medium-item .views{
|
/*.medium-item .views{
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
justify-self:end;
|
justify-self:end;
|
||||||
}
|
}
|
||||||
.medium-item time{
|
.medium-item time{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 3;
|
grid-row: 3;
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
}*/
|
}*/
|
||||||
.medium-item .stats{
|
.medium-item .stats{
|
||||||
grid-column: 2 / span 2;
|
grid-column: 2 / span 2;
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.medium-item .description{
|
.medium-item .description{
|
||||||
grid-column: 2 / span 2;
|
grid-column: 2 / span 2;
|
||||||
grid-row: 4;
|
grid-row: 4;
|
||||||
}
|
}
|
||||||
.medium-item .badges{
|
.medium-item .badges{
|
||||||
grid-column: 2 / span 2;
|
grid-column: 2 / span 2;
|
||||||
grid-row: 5;
|
grid-row: 5;
|
||||||
}
|
}
|
||||||
/* thumbnail size */
|
/* thumbnail size */
|
||||||
.medium-item img{
|
.medium-item img{
|
||||||
/*height:138px;
|
/*height:138px;
|
||||||
width:246px;*/
|
width:246px;*/
|
||||||
height:100%;
|
height:100%;
|
||||||
justify-self:center;
|
justify-self:center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.small-item-box{
|
.small-item-box{
|
||||||
color: #767676;
|
color: #767676;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 1fr 0fr;
|
grid-template-columns: 1fr 0fr;
|
||||||
grid-template-rows: 94px;
|
grid-template-rows: 94px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.small-item{
|
.small-item{
|
||||||
background-color:#bcbcbc;
|
background-color:#bcbcbc;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
text-decoration:none;
|
text-decoration:none;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 168px 1fr;
|
grid-template-columns: 168px 1fr;
|
||||||
grid-column-gap: 5px;
|
grid-column-gap: 5px;
|
||||||
grid-template-rows: 0fr 0fr 0fr 1fr;
|
grid-template-rows: 0fr 0fr 0fr 1fr;
|
||||||
}
|
}
|
||||||
.small-item .title{
|
.small-item .title{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
margin:0;
|
margin:0;
|
||||||
|
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-decoration:initial;
|
text-decoration:initial;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.small-item address{
|
.small-item address{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
justify-self: start;
|
justify-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.small-item .views{
|
.small-item .views{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 3;
|
grid-row: 3;
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
}
|
}
|
||||||
/* thumbnail size */
|
/* thumbnail size */
|
||||||
.small-item img{
|
.small-item img{
|
||||||
/*height:94px;
|
/*height:94px;
|
||||||
width:168px;*/
|
width:168px;*/
|
||||||
height:100%;
|
height:100%;
|
||||||
justify-self:center;
|
justify-self:center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-checkbox{
|
.item-checkbox{
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
align-self:center;
|
align-self:center;
|
||||||
height:30px;
|
height:30px;
|
||||||
width:30px;
|
width:30px;
|
||||||
|
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---Thumbnails for videos---- */
|
/* ---Thumbnails for videos---- */
|
||||||
.video-thumbnail-box{
|
.video-thumbnail-box{
|
||||||
grid-column:1;
|
grid-column:1;
|
||||||
grid-row:1 / span 6;
|
grid-row:1 / span 6;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 1fr 0fr;
|
grid-template-columns: 1fr 0fr;
|
||||||
}
|
}
|
||||||
.video-thumbnail-img{
|
.video-thumbnail-img{
|
||||||
grid-column:1 / span 2;
|
grid-column:1 / span 2;
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
}
|
}
|
||||||
.video-duration{
|
.video-duration{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
align-self: end;
|
align-self: end;
|
||||||
opacity: .8;
|
opacity: .8;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---Thumbnails for playlists---- */
|
/* ---Thumbnails for playlists---- */
|
||||||
.playlist-thumbnail-box{
|
.playlist-thumbnail-box{
|
||||||
grid-column:1;
|
grid-column:1;
|
||||||
grid-row:1 / span 5;
|
grid-row:1 / span 5;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 2fr;
|
grid-template-columns: 3fr 2fr;
|
||||||
}
|
}
|
||||||
.playlist-thumbnail-img{
|
.playlist-thumbnail-img{
|
||||||
grid-column:1 / span 2;
|
grid-column:1 / span 2;
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
}
|
}
|
||||||
.playlist-thumbnail-info{
|
.playlist-thumbnail-info{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
|
|
||||||
text-align:center;
|
text-align:center;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
opacity: .8;
|
opacity: .8;
|
||||||
color: #cfcfcf;
|
color: #cfcfcf;
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-button-row{
|
.page-button-row{
|
||||||
justify-self:center;
|
justify-self: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;
|
||||||
}
|
}
|
||||||
.page-button{
|
.page-button{
|
||||||
background-color: #e9e9e9;
|
background-color: #e9e9e9;
|
||||||
border-style: outset;
|
border-style: outset;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
@ -1,18 +1,18 @@
|
|||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
with open("subscriptions.txt", 'r', encoding='utf-8') as file:
|
with open("subscriptions.txt", 'r', encoding='utf-8') as file:
|
||||||
subscriptions = file.read()
|
subscriptions = file.read()
|
||||||
|
|
||||||
# Line format: "channel_id channel_name"
|
# Line format: "channel_id channel_name"
|
||||||
# Example:
|
# Example:
|
||||||
# UCYO_jab_esuFRV4b17AJtAw 3Blue1Brown
|
# UCYO_jab_esuFRV4b17AJtAw 3Blue1Brown
|
||||||
|
|
||||||
subscriptions = ((line[0:24], line[25: ]) for line in subscriptions.splitlines())
|
subscriptions = ((line[0:24], line[25: ]) for line in subscriptions.splitlines())
|
||||||
|
|
||||||
def get_new_videos():
|
def get_new_videos():
|
||||||
for channel_id, channel_name in subscriptions:
|
for channel_id, channel_name in subscriptions:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_subscriptions_page():
|
def get_subscriptions_page():
|
||||||
|
@ -1,132 +1,132 @@
|
|||||||
|
|
||||||
import re as _re
|
import re as _re
|
||||||
from collections import ChainMap as _ChainMap
|
from collections import ChainMap as _ChainMap
|
||||||
|
|
||||||
class _TemplateMetaclass(type):
|
class _TemplateMetaclass(type):
|
||||||
pattern = r"""
|
pattern = r"""
|
||||||
%(delim)s(?:
|
%(delim)s(?:
|
||||||
(?P<escaped>%(delim)s) | # Escape sequence of two delimiters
|
(?P<escaped>%(delim)s) | # Escape sequence of two delimiters
|
||||||
(?P<named>%(id)s) | # delimiter and a Python identifier
|
(?P<named>%(id)s) | # delimiter and a Python identifier
|
||||||
{(?P<braced>%(id)s)} | # delimiter and a braced identifier
|
{(?P<braced>%(id)s)} | # delimiter and a braced identifier
|
||||||
(?P<invalid>) # Other ill-formed delimiter exprs
|
(?P<invalid>) # Other ill-formed delimiter exprs
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(cls, name, bases, dct):
|
def __init__(cls, name, bases, dct):
|
||||||
super(_TemplateMetaclass, cls).__init__(name, bases, dct)
|
super(_TemplateMetaclass, cls).__init__(name, bases, dct)
|
||||||
if 'pattern' in dct:
|
if 'pattern' in dct:
|
||||||
pattern = cls.pattern
|
pattern = cls.pattern
|
||||||
else:
|
else:
|
||||||
pattern = _TemplateMetaclass.pattern % {
|
pattern = _TemplateMetaclass.pattern % {
|
||||||
'delim' : _re.escape(cls.delimiter),
|
'delim' : _re.escape(cls.delimiter),
|
||||||
'id' : cls.idpattern,
|
'id' : cls.idpattern,
|
||||||
}
|
}
|
||||||
cls.pattern = _re.compile(pattern, cls.flags | _re.VERBOSE)
|
cls.pattern = _re.compile(pattern, cls.flags | _re.VERBOSE)
|
||||||
|
|
||||||
|
|
||||||
class Template(metaclass=_TemplateMetaclass):
|
class Template(metaclass=_TemplateMetaclass):
|
||||||
"""A string class for supporting $-substitutions."""
|
"""A string class for supporting $-substitutions."""
|
||||||
|
|
||||||
delimiter = '$'
|
delimiter = '$'
|
||||||
idpattern = r'[_a-z][_a-z0-9]*'
|
idpattern = r'[_a-z][_a-z0-9]*'
|
||||||
flags = _re.IGNORECASE
|
flags = _re.IGNORECASE
|
||||||
|
|
||||||
def __init__(self, template):
|
def __init__(self, template):
|
||||||
self.template = template
|
self.template = template
|
||||||
|
|
||||||
# Search for $$, $identifier, ${identifier}, and any bare $'s
|
# Search for $$, $identifier, ${identifier}, and any bare $'s
|
||||||
|
|
||||||
def _invalid(self, mo):
|
def _invalid(self, mo):
|
||||||
i = mo.start('invalid')
|
i = mo.start('invalid')
|
||||||
lines = self.template[:i].splitlines(keepends=True)
|
lines = self.template[:i].splitlines(keepends=True)
|
||||||
if not lines:
|
if not lines:
|
||||||
colno = 1
|
colno = 1
|
||||||
lineno = 1
|
lineno = 1
|
||||||
else:
|
else:
|
||||||
colno = i - len(''.join(lines[:-1]))
|
colno = i - len(''.join(lines[:-1]))
|
||||||
lineno = len(lines)
|
lineno = len(lines)
|
||||||
raise ValueError('Invalid placeholder in string: line %d, col %d' %
|
raise ValueError('Invalid placeholder in string: line %d, col %d' %
|
||||||
(lineno, colno))
|
(lineno, colno))
|
||||||
|
|
||||||
def substitute(*args, **kws):
|
def substitute(*args, **kws):
|
||||||
if not args:
|
if not args:
|
||||||
raise TypeError("descriptor 'substitute' of 'Template' object "
|
raise TypeError("descriptor 'substitute' of 'Template' object "
|
||||||
"needs an argument")
|
"needs an argument")
|
||||||
self, *args = args # allow the "self" keyword be passed
|
self, *args = args # allow the "self" keyword be passed
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
raise TypeError('Too many positional arguments')
|
raise TypeError('Too many positional arguments')
|
||||||
if not args:
|
if not args:
|
||||||
mapping = kws
|
mapping = kws
|
||||||
elif kws:
|
elif kws:
|
||||||
mapping = _ChainMap(kws, args[0])
|
mapping = _ChainMap(kws, args[0])
|
||||||
else:
|
else:
|
||||||
mapping = args[0]
|
mapping = args[0]
|
||||||
# Helper function for .sub()
|
# Helper function for .sub()
|
||||||
def convert(mo):
|
def convert(mo):
|
||||||
# Check the most common path first.
|
# Check the most common path first.
|
||||||
named = mo.group('named') or mo.group('braced')
|
named = mo.group('named') or mo.group('braced')
|
||||||
if named is not None:
|
if named is not None:
|
||||||
return str(mapping.get(named,''))
|
return str(mapping.get(named,''))
|
||||||
if mo.group('escaped') is not None:
|
if mo.group('escaped') is not None:
|
||||||
return self.delimiter
|
return self.delimiter
|
||||||
if mo.group('invalid') is not None:
|
if mo.group('invalid') is not None:
|
||||||
self._invalid(mo)
|
self._invalid(mo)
|
||||||
raise ValueError('Unrecognized named group in pattern',
|
raise ValueError('Unrecognized named group in pattern',
|
||||||
self.pattern)
|
self.pattern)
|
||||||
return self.pattern.sub(convert, self.template)
|
return self.pattern.sub(convert, self.template)
|
||||||
|
|
||||||
def strict_substitute(*args, **kws):
|
def strict_substitute(*args, **kws):
|
||||||
if not args:
|
if not args:
|
||||||
raise TypeError("descriptor 'substitute' of 'Template' object "
|
raise TypeError("descriptor 'substitute' of 'Template' object "
|
||||||
"needs an argument")
|
"needs an argument")
|
||||||
self, *args = args # allow the "self" keyword be passed
|
self, *args = args # allow the "self" keyword be passed
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
raise TypeError('Too many positional arguments')
|
raise TypeError('Too many positional arguments')
|
||||||
if not args:
|
if not args:
|
||||||
mapping = kws
|
mapping = kws
|
||||||
elif kws:
|
elif kws:
|
||||||
mapping = _ChainMap(kws, args[0])
|
mapping = _ChainMap(kws, args[0])
|
||||||
else:
|
else:
|
||||||
mapping = args[0]
|
mapping = args[0]
|
||||||
# Helper function for .sub()
|
# Helper function for .sub()
|
||||||
def convert(mo):
|
def convert(mo):
|
||||||
# Check the most common path first.
|
# Check the most common path first.
|
||||||
named = mo.group('named') or mo.group('braced')
|
named = mo.group('named') or mo.group('braced')
|
||||||
if named is not None:
|
if named is not None:
|
||||||
return str(mapping[named])
|
return str(mapping[named])
|
||||||
if mo.group('escaped') is not None:
|
if mo.group('escaped') is not None:
|
||||||
return self.delimiter
|
return self.delimiter
|
||||||
if mo.group('invalid') is not None:
|
if mo.group('invalid') is not None:
|
||||||
self._invalid(mo)
|
self._invalid(mo)
|
||||||
raise ValueError('Unrecognized named group in pattern',
|
raise ValueError('Unrecognized named group in pattern',
|
||||||
self.pattern)
|
self.pattern)
|
||||||
return self.pattern.sub(convert, self.template)
|
return self.pattern.sub(convert, self.template)
|
||||||
|
|
||||||
def safe_substitute(*args, **kws):
|
def safe_substitute(*args, **kws):
|
||||||
if not args:
|
if not args:
|
||||||
raise TypeError("descriptor 'safe_substitute' of 'Template' object "
|
raise TypeError("descriptor 'safe_substitute' of 'Template' object "
|
||||||
"needs an argument")
|
"needs an argument")
|
||||||
self, *args = args # allow the "self" keyword be passed
|
self, *args = args # allow the "self" keyword be passed
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
raise TypeError('Too many positional arguments')
|
raise TypeError('Too many positional arguments')
|
||||||
if not args:
|
if not args:
|
||||||
mapping = kws
|
mapping = kws
|
||||||
elif kws:
|
elif kws:
|
||||||
mapping = _ChainMap(kws, args[0])
|
mapping = _ChainMap(kws, args[0])
|
||||||
else:
|
else:
|
||||||
mapping = args[0]
|
mapping = args[0]
|
||||||
# Helper function for .sub()
|
# Helper function for .sub()
|
||||||
def convert(mo):
|
def convert(mo):
|
||||||
named = mo.group('named') or mo.group('braced')
|
named = mo.group('named') or mo.group('braced')
|
||||||
if named is not None:
|
if named is not None:
|
||||||
try:
|
try:
|
||||||
return str(mapping[named])
|
return str(mapping[named])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return mo.group()
|
return mo.group()
|
||||||
if mo.group('escaped') is not None:
|
if mo.group('escaped') is not None:
|
||||||
return self.delimiter
|
return self.delimiter
|
||||||
if mo.group('invalid') is not None:
|
if mo.group('invalid') is not None:
|
||||||
return mo.group()
|
return mo.group()
|
||||||
raise ValueError('Unrecognized named group in pattern',
|
raise ValueError('Unrecognized named group in pattern',
|
||||||
self.pattern)
|
self.pattern)
|
||||||
return self.pattern.sub(convert, self.template)
|
return self.pattern.sub(convert, self.template)
|
586
youtube/watch.py
586
youtube/watch.py
@ -1,294 +1,294 @@
|
|||||||
from youtube_dl.YoutubeDL import YoutubeDL
|
from youtube_dl.YoutubeDL import YoutubeDL
|
||||||
import json
|
import json
|
||||||
import urllib
|
import urllib
|
||||||
from string import Template
|
from string import Template
|
||||||
import html
|
import html
|
||||||
import youtube.common as common
|
import youtube.common as common
|
||||||
from youtube.common import default_multi_get, get_thumbnail_url, video_id, URL_ORIGIN
|
from youtube.common import default_multi_get, get_thumbnail_url, video_id, URL_ORIGIN
|
||||||
import youtube.comments as comments
|
import youtube.comments as comments
|
||||||
import gevent
|
import gevent
|
||||||
|
|
||||||
video_height_priority = (360, 480, 240, 720, 1080)
|
video_height_priority = (360, 480, 240, 720, 1080)
|
||||||
|
|
||||||
|
|
||||||
_formats = {
|
_formats = {
|
||||||
'5': {'ext': 'flv', 'width': 400, 'height': 240, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
|
'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'},
|
'6': {'ext': 'flv', 'width': 450, 'height': 270, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
|
||||||
'13': {'ext': '3gp', 'acodec': 'aac', 'vcodec': 'mp4v'},
|
'13': {'ext': '3gp', 'acodec': 'aac', 'vcodec': 'mp4v'},
|
||||||
'17': {'ext': '3gp', 'width': 176, 'height': 144, 'acodec': 'aac', 'abr': 24, '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'},
|
'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'},
|
'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'},
|
'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'},
|
'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
|
# itag 36 videos are either 320x180 (BaW_jenozKc) or 320x240 (__2ABJjxzNo), abr varies as well
|
||||||
'36': {'ext': '3gp', 'width': 320, 'acodec': 'aac', 'vcodec': 'mp4v'},
|
'36': {'ext': '3gp', 'width': 320, 'acodec': 'aac', 'vcodec': 'mp4v'},
|
||||||
'37': {'ext': 'mp4', 'width': 1920, 'height': 1080, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
|
'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'},
|
'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'},
|
'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'},
|
'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'},
|
'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'},
|
'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'},
|
'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'},
|
'78': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
|
||||||
|
|
||||||
|
|
||||||
# 3D videos
|
# 3D videos
|
||||||
'82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
|
'82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
|
||||||
'83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
|
'83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
|
||||||
'84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
|
'84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
|
||||||
'85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
|
'85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
|
||||||
'100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8', 'preference': -20},
|
'100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8', 'preference': -20},
|
||||||
'101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
|
'101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
|
||||||
'102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
|
'102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
|
||||||
|
|
||||||
# Apple HTTP Live Streaming
|
# Apple HTTP Live Streaming
|
||||||
'91': {'ext': 'mp4', 'height': 144, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
|
'91': {'ext': 'mp4', 'height': 144, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
|
||||||
'92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
|
'92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
|
||||||
'93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
|
'93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
|
||||||
'94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
|
'94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
|
||||||
'95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
|
'95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
|
||||||
'96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
|
'96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
|
||||||
'132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
|
'132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
|
||||||
'151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 24, 'vcodec': 'h264', 'preference': -10},
|
'151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 24, 'vcodec': 'h264', 'preference': -10},
|
||||||
|
|
||||||
# DASH mp4 video
|
# DASH mp4 video
|
||||||
'133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
'133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
'134': {'ext': 'mp4', 'height': 360, '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'},
|
'135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
'136': {'ext': 'mp4', 'height': 720, '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'},
|
'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/rg3/youtube-dl/issues/4559)
|
'138': {'ext': 'mp4', 'format_note': 'DASH video', 'vcodec': 'h264'}, # Height can vary (https://github.com/rg3/youtube-dl/issues/4559)
|
||||||
'160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
'160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
'212': {'ext': 'mp4', 'height': 480, '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'},
|
'264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
'298': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
|
'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},
|
'299': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
|
||||||
'266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
'266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'h264'},
|
||||||
|
|
||||||
# Dash mp4 audio
|
# Dash mp4 audio
|
||||||
'139': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 48, 'container': 'm4a_dash'},
|
'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'},
|
'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'},
|
'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'},
|
'256': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
|
||||||
'258': {'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'},
|
'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'},
|
'328': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'ec-3', 'container': 'm4a_dash'},
|
||||||
|
|
||||||
# Dash webm
|
# Dash webm
|
||||||
'167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
|
'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'},
|
'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'},
|
'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'},
|
'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'},
|
'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'},
|
'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'},
|
'278': {'ext': 'webm', 'height': 144, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp9'},
|
||||||
'242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
'242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
'243': {'ext': 'webm', 'height': 360, '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'},
|
'244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
'245': {'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'},
|
'246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
'247': {'ext': 'webm', 'height': 720, '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'},
|
'248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
'271': {'ext': 'webm', 'height': 1440, '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)
|
# itag 272 videos are either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
|
||||||
'272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
'272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
'302': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
|
'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},
|
'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},
|
'308': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
|
||||||
'313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
'313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
|
||||||
'315': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
|
'315': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
|
||||||
|
|
||||||
# Dash webm audio
|
# Dash webm audio
|
||||||
'171': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 128},
|
'171': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 128},
|
||||||
'172': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 256},
|
'172': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 256},
|
||||||
|
|
||||||
# Dash webm audio with opus inside
|
# Dash webm audio with opus inside
|
||||||
'249': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50},
|
'249': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50},
|
||||||
'250': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70},
|
'250': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70},
|
||||||
'251': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160},
|
'251': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160},
|
||||||
|
|
||||||
# RTMP (unnamed)
|
# RTMP (unnamed)
|
||||||
'_rtmp': {'protocol': 'rtmp'},
|
'_rtmp': {'protocol': 'rtmp'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
source_tag_template = Template('''
|
source_tag_template = Template('''
|
||||||
<source src="$src" type="$type">''')
|
<source src="$src" type="$type">''')
|
||||||
|
|
||||||
with open("yt_watch_template.html", "r") as file:
|
with open("yt_watch_template.html", "r") as file:
|
||||||
yt_watch_template = Template(file.read())
|
yt_watch_template = Template(file.read())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# example:
|
# example:
|
||||||
#https://www.youtube.com/related_ajax?ctoken=CBQSJhILVGNxV29rOEF1YkXAAQDIAQDgAQGiAg0o____________AUAAGAAq0gEInJOqsOyB1tAaCNeMgaD4spLIKQioxdHSu8SF9JgBCLr27tnaioDpXwj1-L_R3s7r2wcIv8TnueeUo908CMXSganIrvHDJgiVuMirrqbgqYABCJDsu8PBzdGW8wEI_-WI2t-c-IlQCOK_m_KB_rP5wAEIl7S4serqnq5YCNSs55mMt8qLyQEImvutmp-x9LaCAQiVg96VpY_pqJMBCOPsgdTflsGRsQEI7ZfYleKIub0tCIrcsb7a_uu95gEIi9Gz6_bC76zEAQjo1c_W8JzlkhI%3D&continuation=CBQSJhILVGNxV29rOEF1YkXAAQDIAQDgAQGiAg0o____________AUAAGAAq0gEInJOqsOyB1tAaCNeMgaD4spLIKQioxdHSu8SF9JgBCLr27tnaioDpXwj1-L_R3s7r2wcIv8TnueeUo908CMXSganIrvHDJgiVuMirrqbgqYABCJDsu8PBzdGW8wEI_-WI2t-c-IlQCOK_m_KB_rP5wAEIl7S4serqnq5YCNSs55mMt8qLyQEImvutmp-x9LaCAQiVg96VpY_pqJMBCOPsgdTflsGRsQEI7ZfYleKIub0tCIrcsb7a_uu95gEIi9Gz6_bC76zEAQjo1c_W8JzlkhI%3D&itct=CCkQybcCIhMIg8PShInX2gIVgdvBCh15WA0ZKPgd
|
#https://www.youtube.com/related_ajax?ctoken=CBQSJhILVGNxV29rOEF1YkXAAQDIAQDgAQGiAg0o____________AUAAGAAq0gEInJOqsOyB1tAaCNeMgaD4spLIKQioxdHSu8SF9JgBCLr27tnaioDpXwj1-L_R3s7r2wcIv8TnueeUo908CMXSganIrvHDJgiVuMirrqbgqYABCJDsu8PBzdGW8wEI_-WI2t-c-IlQCOK_m_KB_rP5wAEIl7S4serqnq5YCNSs55mMt8qLyQEImvutmp-x9LaCAQiVg96VpY_pqJMBCOPsgdTflsGRsQEI7ZfYleKIub0tCIrcsb7a_uu95gEIi9Gz6_bC76zEAQjo1c_W8JzlkhI%3D&continuation=CBQSJhILVGNxV29rOEF1YkXAAQDIAQDgAQGiAg0o____________AUAAGAAq0gEInJOqsOyB1tAaCNeMgaD4spLIKQioxdHSu8SF9JgBCLr27tnaioDpXwj1-L_R3s7r2wcIv8TnueeUo908CMXSganIrvHDJgiVuMirrqbgqYABCJDsu8PBzdGW8wEI_-WI2t-c-IlQCOK_m_KB_rP5wAEIl7S4serqnq5YCNSs55mMt8qLyQEImvutmp-x9LaCAQiVg96VpY_pqJMBCOPsgdTflsGRsQEI7ZfYleKIub0tCIrcsb7a_uu95gEIi9Gz6_bC76zEAQjo1c_W8JzlkhI%3D&itct=CCkQybcCIhMIg8PShInX2gIVgdvBCh15WA0ZKPgd
|
||||||
def get_bloated_more_related_videos(video_url, related_videos_token, id_token):
|
def get_bloated_more_related_videos(video_url, related_videos_token, id_token):
|
||||||
related_videos_token = urllib.parse.quote(related_videos_token)
|
related_videos_token = urllib.parse.quote(related_videos_token)
|
||||||
url = "https://www.youtube.com/related_ajax?ctoken=" + related_videos_token + "&continuation=" + related_videos_token
|
url = "https://www.youtube.com/related_ajax?ctoken=" + related_videos_token + "&continuation=" + related_videos_token
|
||||||
headers = {
|
headers = {
|
||||||
'Host': 'www.youtube.com',
|
'Host': 'www.youtube.com',
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)',
|
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)',
|
||||||
'Accept': '*/*',
|
'Accept': '*/*',
|
||||||
'Accept-Language': 'en-US,en;q=0.5',
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
'Referer': video_url,
|
'Referer': video_url,
|
||||||
'X-YouTube-Client-Name': '1',
|
'X-YouTube-Client-Name': '1',
|
||||||
'X-YouTube-Client-Version': '2.20180418',
|
'X-YouTube-Client-Version': '2.20180418',
|
||||||
'X-Youtube-Identity-Token': id_token,
|
'X-Youtube-Identity-Token': id_token,
|
||||||
|
|
||||||
}
|
}
|
||||||
#print(url)
|
#print(url)
|
||||||
req = urllib.request.Request(url, headers=headers)
|
req = urllib.request.Request(url, headers=headers)
|
||||||
response = urllib.request.urlopen(req, timeout = 5)
|
response = urllib.request.urlopen(req, timeout = 5)
|
||||||
content = response.read()
|
content = response.read()
|
||||||
info = json.loads(content)
|
info = json.loads(content)
|
||||||
return info
|
return info
|
||||||
|
|
||||||
def get_more_related_videos_info(video_url, related_videos_token, id_token):
|
def get_more_related_videos_info(video_url, related_videos_token, id_token):
|
||||||
results = []
|
results = []
|
||||||
info = get_bloated_more_related_videos(video_url, related_videos_token, id_token)
|
info = get_bloated_more_related_videos(video_url, related_videos_token, id_token)
|
||||||
bloated_results = info[1]['response']['continuationContents']['watchNextSecondaryResultsContinuation']['results']
|
bloated_results = info[1]['response']['continuationContents']['watchNextSecondaryResultsContinuation']['results']
|
||||||
for bloated_result in bloated_results:
|
for bloated_result in bloated_results:
|
||||||
bloated_result = bloated_result['compactVideoRenderer']
|
bloated_result = bloated_result['compactVideoRenderer']
|
||||||
results.append({
|
results.append({
|
||||||
"title": bloated_result['title']['simpleText'],
|
"title": bloated_result['title']['simpleText'],
|
||||||
"video_id": bloated_result['videoId'],
|
"video_id": bloated_result['videoId'],
|
||||||
"views_text": bloated_result['viewCountText']['simpleText'],
|
"views_text": bloated_result['viewCountText']['simpleText'],
|
||||||
"length_text": default_multi_get(bloated_result, 'lengthText', 'simpleText', default=''), # livestreams dont have a length
|
"length_text": default_multi_get(bloated_result, 'lengthText', 'simpleText', default=''), # livestreams dont have a length
|
||||||
"length_text": bloated_result['lengthText']['simpleText'],
|
"length_text": bloated_result['lengthText']['simpleText'],
|
||||||
"uploader_name": bloated_result['longBylineText']['runs'][0]['text'],
|
"uploader_name": bloated_result['longBylineText']['runs'][0]['text'],
|
||||||
"uploader_url": bloated_result['longBylineText']['runs'][0]['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url'],
|
"uploader_url": bloated_result['longBylineText']['runs'][0]['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url'],
|
||||||
})
|
})
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def more_related_videos_html(video_info):
|
def more_related_videos_html(video_info):
|
||||||
related_videos = get_related_videos(url, 1, video_info['related_videos_token'], video_info['id_token'])
|
related_videos = get_related_videos(url, 1, video_info['related_videos_token'], video_info['id_token'])
|
||||||
|
|
||||||
related_videos_html = ""
|
related_videos_html = ""
|
||||||
for video in related_videos:
|
for video in related_videos:
|
||||||
related_videos_html += Template(video_related_template).substitute(
|
related_videos_html += Template(video_related_template).substitute(
|
||||||
video_title=html.escape(video["title"]),
|
video_title=html.escape(video["title"]),
|
||||||
views=video["views_text"],
|
views=video["views_text"],
|
||||||
uploader=html.escape(video["uploader_name"]),
|
uploader=html.escape(video["uploader_name"]),
|
||||||
uploader_channel_url=video["uploader_url"],
|
uploader_channel_url=video["uploader_url"],
|
||||||
length=video["length_text"],
|
length=video["length_text"],
|
||||||
video_url = "/youtube.com/watch?v=" + video["video_id"],
|
video_url = "/youtube.com/watch?v=" + video["video_id"],
|
||||||
thumbnail_url= get_thumbnail_url(video['video_id']),
|
thumbnail_url= get_thumbnail_url(video['video_id']),
|
||||||
)
|
)
|
||||||
return related_videos_html
|
return related_videos_html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_related_items_html(info):
|
def get_related_items_html(info):
|
||||||
result = ""
|
result = ""
|
||||||
for item in info['related_vids']:
|
for item in info['related_vids']:
|
||||||
if 'list' in item: # playlist:
|
if 'list' in item: # playlist:
|
||||||
result += common.small_playlist_item_html(watch_page_related_playlist_info(item))
|
result += common.small_playlist_item_html(watch_page_related_playlist_info(item))
|
||||||
else:
|
else:
|
||||||
result += common.small_video_item_html(watch_page_related_video_info(item))
|
result += common.small_video_item_html(watch_page_related_video_info(item))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# json of related items retrieved directly from the watch page has different names for everything
|
# json of related items retrieved directly from the watch page has different names for everything
|
||||||
# converts these to standard names
|
# converts these to standard names
|
||||||
def watch_page_related_video_info(item):
|
def watch_page_related_video_info(item):
|
||||||
result = {key: item[key] for key in ('id', 'title', 'author')}
|
result = {key: item[key] for key in ('id', 'title', 'author')}
|
||||||
result['duration'] = common.seconds_to_timestamp(item['length_seconds'])
|
result['duration'] = common.seconds_to_timestamp(item['length_seconds'])
|
||||||
try:
|
try:
|
||||||
result['views'] = item['short_view_count_text']
|
result['views'] = item['short_view_count_text']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
result['views'] = ''
|
result['views'] = ''
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def watch_page_related_playlist_info(item):
|
def watch_page_related_playlist_info(item):
|
||||||
return {
|
return {
|
||||||
'size': item['playlist_length'] if item['playlist_length'] != "0" else "50+",
|
'size': item['playlist_length'] if item['playlist_length'] != "0" else "50+",
|
||||||
'title': item['playlist_title'],
|
'title': item['playlist_title'],
|
||||||
'id': item['list'],
|
'id': item['list'],
|
||||||
'first_video_id': item['video_id'],
|
'first_video_id': item['video_id'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def sort_formats(info):
|
def sort_formats(info):
|
||||||
info['formats'].sort(key=lambda x: default_multi_get(_formats, x['format_id'], 'height', default=0))
|
info['formats'].sort(key=lambda x: default_multi_get(_formats, x['format_id'], 'height', default=0))
|
||||||
for index, format in enumerate(info['formats']):
|
for index, format in enumerate(info['formats']):
|
||||||
if default_multi_get(_formats, format['format_id'], 'height', default=0) >= 360:
|
if default_multi_get(_formats, format['format_id'], 'height', default=0) >= 360:
|
||||||
break
|
break
|
||||||
info['formats'] = info['formats'][index:] + info['formats'][0:index]
|
info['formats'] = info['formats'][index:] + info['formats'][0:index]
|
||||||
info['formats'] = [format for format in info['formats'] if format['acodec'] != 'none' and format['vcodec'] != 'none']
|
info['formats'] = [format for format in info['formats'] if format['acodec'] != 'none' and format['vcodec'] != 'none']
|
||||||
|
|
||||||
def formats_html(info):
|
def formats_html(info):
|
||||||
result = ''
|
result = ''
|
||||||
for format in info['formats']:
|
for format in info['formats']:
|
||||||
result += source_tag_template.substitute(
|
result += source_tag_template.substitute(
|
||||||
src=format['url'],
|
src=format['url'],
|
||||||
type='audio/' + format['ext'] if format['vcodec'] == "none" else 'video/' + format['ext'],
|
type='audio/' + format['ext'] if format['vcodec'] == "none" else 'video/' + format['ext'],
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def choose_format(info):
|
def choose_format(info):
|
||||||
suitable_formats = []
|
suitable_formats = []
|
||||||
with open('teste.txt', 'w', encoding='utf-8') as f:
|
with open('teste.txt', 'w', encoding='utf-8') as f:
|
||||||
f.write(json.dumps(info['formats']))
|
f.write(json.dumps(info['formats']))
|
||||||
for format in info['formats']:
|
for format in info['formats']:
|
||||||
if (format["ext"] in ("mp4", "webm")
|
if (format["ext"] in ("mp4", "webm")
|
||||||
and format["acodec"] != "none"
|
and format["acodec"] != "none"
|
||||||
and format["vcodec"] != "none"
|
and format["vcodec"] != "none"
|
||||||
and format.get("height","none") in video_height_priority):
|
and format.get("height","none") in video_height_priority):
|
||||||
suitable_formats.append(format)
|
suitable_formats.append(format)
|
||||||
|
|
||||||
current_best = (suitable_formats[0],video_height_priority.index(suitable_formats[0]["height"]))
|
current_best = (suitable_formats[0],video_height_priority.index(suitable_formats[0]["height"]))
|
||||||
for format in suitable_formats:
|
for format in suitable_formats:
|
||||||
video_priority_index = video_height_priority.index(format["height"])
|
video_priority_index = video_height_priority.index(format["height"])
|
||||||
if video_priority_index < current_best[1]:
|
if video_priority_index < current_best[1]:
|
||||||
current_best = (format, video_priority_index)
|
current_best = (format, video_priority_index)
|
||||||
return current_best[0]
|
return current_best[0]
|
||||||
|
|
||||||
more_comments_template = Template('''<a class="page-button more-comments" href="$url">More comments</a>''')
|
more_comments_template = Template('''<a class="page-button more-comments" href="$url">More comments</a>''')
|
||||||
def get_watch_page(query_string):
|
def get_watch_page(query_string):
|
||||||
id = urllib.parse.parse_qs(query_string)['v'][0]
|
id = urllib.parse.parse_qs(query_string)['v'][0]
|
||||||
tasks = (
|
tasks = (
|
||||||
gevent.spawn(comments.video_comments, id ),
|
gevent.spawn(comments.video_comments, id ),
|
||||||
gevent.spawn(YoutubeDL(params={'youtube_include_dash_manifest':False}).extract_info, "https://www.youtube.com/watch?v=" + id, download=False)
|
gevent.spawn(YoutubeDL(params={'youtube_include_dash_manifest':False}).extract_info, "https://www.youtube.com/watch?v=" + id, download=False)
|
||||||
)
|
)
|
||||||
gevent.joinall(tasks)
|
gevent.joinall(tasks)
|
||||||
comments_info, info = tasks[0].value, tasks[1].value
|
comments_info, info = tasks[0].value, tasks[1].value
|
||||||
comments_html, ctoken = comments_info
|
comments_html, ctoken = comments_info
|
||||||
|
|
||||||
if ctoken == '':
|
if ctoken == '':
|
||||||
more_comments_button = ''
|
more_comments_button = ''
|
||||||
else:
|
else:
|
||||||
more_comments_button = more_comments_template.substitute(url = URL_ORIGIN + '/comments?ctoken=' + ctoken)
|
more_comments_button = more_comments_template.substitute(url = URL_ORIGIN + '/comments?ctoken=' + ctoken)
|
||||||
#comments_html = comments.comments_html(video_id(url))
|
#comments_html = comments.comments_html(video_id(url))
|
||||||
#info = YoutubeDL().extract_info(url, download=False)
|
#info = YoutubeDL().extract_info(url, download=False)
|
||||||
|
|
||||||
#chosen_format = choose_format(info)
|
#chosen_format = choose_format(info)
|
||||||
sort_formats(info)
|
sort_formats(info)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
upload_year = info["upload_date"][0:4]
|
upload_year = info["upload_date"][0:4]
|
||||||
upload_month = info["upload_date"][4:6]
|
upload_month = info["upload_date"][4:6]
|
||||||
upload_day = info["upload_date"][6:8]
|
upload_day = info["upload_date"][6:8]
|
||||||
upload_date = upload_month + "/" + upload_day + "/" + upload_year
|
upload_date = upload_month + "/" + upload_day + "/" + upload_year
|
||||||
|
|
||||||
related_videos_html = get_related_items_html(info)
|
related_videos_html = get_related_items_html(info)
|
||||||
|
|
||||||
page = yt_watch_template.substitute(
|
page = yt_watch_template.substitute(
|
||||||
video_title=html.escape(info["title"]),
|
video_title=html.escape(info["title"]),
|
||||||
page_title=html.escape(info["title"]),
|
page_title=html.escape(info["title"]),
|
||||||
uploader=html.escape(info["uploader"]),
|
uploader=html.escape(info["uploader"]),
|
||||||
uploader_channel_url='/' + info["uploader_url"],
|
uploader_channel_url='/' + info["uploader_url"],
|
||||||
#upload_date=datetime.datetime.fromtimestamp(info["timestamp"]).strftime("%d %b %Y %H:%M:%S"),
|
#upload_date=datetime.datetime.fromtimestamp(info["timestamp"]).strftime("%d %b %Y %H:%M:%S"),
|
||||||
upload_date = upload_date,
|
upload_date = upload_date,
|
||||||
views='{:,}'.format(info["view_count"]),
|
views='{:,}'.format(info["view_count"]),
|
||||||
likes=(lambda x: '{:,}'.format(x) if x is not None else "")(info["like_count"]),
|
likes=(lambda x: '{:,}'.format(x) if x is not None else "")(info["like_count"]),
|
||||||
dislikes=(lambda x: '{:,}'.format(x) if x is not None else "")(info["dislike_count"]),
|
dislikes=(lambda x: '{:,}'.format(x) if x is not None else "")(info["dislike_count"]),
|
||||||
description=html.escape(info["description"]),
|
description=html.escape(info["description"]),
|
||||||
video_sources=formats_html(info),
|
video_sources=formats_html(info),
|
||||||
related = related_videos_html,
|
related = related_videos_html,
|
||||||
comments=comments_html,
|
comments=comments_html,
|
||||||
more_comments_button = more_comments_button,
|
more_comments_button = more_comments_button,
|
||||||
)
|
)
|
||||||
return page
|
return page
|
@ -1,11 +1,11 @@
|
|||||||
import os.path
|
import os.path
|
||||||
import json
|
import json
|
||||||
watch_later_file = os.path.normpath("youtube/watch_later.txt")
|
watch_later_file = os.path.normpath("youtube/watch_later.txt")
|
||||||
def add_to_watch_later(video_info_list):
|
def add_to_watch_later(video_info_list):
|
||||||
with open(watch_later_file, "a", encoding='utf-8') as file:
|
with open(watch_later_file, "a", encoding='utf-8') as file:
|
||||||
for info in video_info_list:
|
for info in video_info_list:
|
||||||
file.write(info + "\n")
|
file.write(info + "\n")
|
||||||
|
|
||||||
|
|
||||||
def get_watch_later_page():
|
def get_watch_later_page():
|
||||||
pass
|
pass
|
@ -1,60 +1,60 @@
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from youtube import watch_later, watch, search, playlist, channel, comments
|
from youtube import watch_later, watch, search, playlist, channel, comments
|
||||||
YOUTUBE_FILES = (
|
YOUTUBE_FILES = (
|
||||||
"/shared.css",
|
"/shared.css",
|
||||||
"/opensearch.xml",
|
"/opensearch.xml",
|
||||||
'/comments.css',
|
'/comments.css',
|
||||||
)
|
)
|
||||||
|
|
||||||
def youtube(env, start_response):
|
def youtube(env, start_response):
|
||||||
path, method, query_string = env['PATH_INFO'], env['REQUEST_METHOD'], env['QUERY_STRING']
|
path, method, query_string = env['PATH_INFO'], env['REQUEST_METHOD'], env['QUERY_STRING']
|
||||||
if method == "GET":
|
if method == "GET":
|
||||||
if path in YOUTUBE_FILES:
|
if path in YOUTUBE_FILES:
|
||||||
with open("youtube" + path, 'rb') as f:
|
with open("youtube" + path, 'rb') as f:
|
||||||
mime_type = mimetypes.guess_type(path)[0] or 'application/octet-stream'
|
mime_type = mimetypes.guess_type(path)[0] or 'application/octet-stream'
|
||||||
start_response('200 OK', (('Content-type',mime_type),) )
|
start_response('200 OK', (('Content-type',mime_type),) )
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
elif path == "/comments":
|
elif path == "/comments":
|
||||||
start_response('200 OK', (('Content-type','text/html'),) )
|
start_response('200 OK', (('Content-type','text/html'),) )
|
||||||
return comments.get_comments_page(query_string).encode()
|
return comments.get_comments_page(query_string).encode()
|
||||||
|
|
||||||
elif path == "/watch":
|
elif path == "/watch":
|
||||||
start_response('200 OK', (('Content-type','text/html'),) )
|
start_response('200 OK', (('Content-type','text/html'),) )
|
||||||
return watch.get_watch_page(query_string).encode()
|
return watch.get_watch_page(query_string).encode()
|
||||||
|
|
||||||
elif path == "/search":
|
elif path == "/search":
|
||||||
start_response('200 OK', (('Content-type','text/html'),) )
|
start_response('200 OK', (('Content-type','text/html'),) )
|
||||||
return search.get_search_page(query_string).encode()
|
return search.get_search_page(query_string).encode()
|
||||||
|
|
||||||
elif path == "/playlist":
|
elif path == "/playlist":
|
||||||
start_response('200 OK', (('Content-type','text/html'),) )
|
start_response('200 OK', (('Content-type','text/html'),) )
|
||||||
return playlist.get_playlist_page(query_string).encode()
|
return playlist.get_playlist_page(query_string).encode()
|
||||||
|
|
||||||
elif path.startswith("/channel/"):
|
elif path.startswith("/channel/"):
|
||||||
start_response('200 OK', (('Content-type','text/html'),) )
|
start_response('200 OK', (('Content-type','text/html'),) )
|
||||||
return channel.get_channel_page(path[9:], query_string=query_string).encode()
|
return channel.get_channel_page(path[9:], query_string=query_string).encode()
|
||||||
|
|
||||||
elif path.startswith("/user/"):
|
elif path.startswith("/user/"):
|
||||||
start_response('200 OK', (('Content-type','text/html'),) )
|
start_response('200 OK', (('Content-type','text/html'),) )
|
||||||
return channel.get_user_page(path[6:], query_string=query_string).encode()
|
return channel.get_user_page(path[6:], query_string=query_string).encode()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
start_response('404 Not Found', () )
|
start_response('404 Not Found', () )
|
||||||
return b'404 Not Found'
|
return b'404 Not Found'
|
||||||
|
|
||||||
elif method == "POST":
|
elif method == "POST":
|
||||||
if path == "/edit_playlist":
|
if path == "/edit_playlist":
|
||||||
fields = urllib.parse.parse_qs(env['wsgi.input'].read().decode())
|
fields = urllib.parse.parse_qs(env['wsgi.input'].read().decode())
|
||||||
if fields['action'][0] == 'add' and fields['playlist_name'][0] == 'watch_later':
|
if fields['action'][0] == 'add' and fields['playlist_name'][0] == 'watch_later':
|
||||||
watch_later.add_to_watch_later(fields['video_info_list'])
|
watch_later.add_to_watch_later(fields['video_info_list'])
|
||||||
|
|
||||||
start_response('204 No Content', ())
|
start_response('204 No Content', ())
|
||||||
else:
|
else:
|
||||||
start_response('404 Not Found', ())
|
start_response('404 Not Found', ())
|
||||||
return b'404 Not Found'
|
return b'404 Not Found'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
start_response('501 Not Implemented', ())
|
start_response('501 Not Implemented', ())
|
||||||
return b'501 Not Implemented'
|
return b'501 Not Implemented'
|
@ -1,128 +1,128 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>$page_title</title>
|
<title>$page_title</title>
|
||||||
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
header{
|
header{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 2fr;
|
grid-template-columns: 3fr 2fr;
|
||||||
}
|
}
|
||||||
#header-left{
|
#header-left{
|
||||||
grid-column:1;
|
grid-column:1;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 1fr 640px;
|
grid-template-columns: 1fr 640px;
|
||||||
}
|
}
|
||||||
#site-search{
|
#site-search{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
#header-right{
|
#header-right{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns:40px 400px 100px 1fr;
|
grid-template-columns:40px 400px 100px 1fr;
|
||||||
grid-template-rows: 1fr 1fr;
|
grid-template-rows: 1fr 1fr;
|
||||||
}
|
}
|
||||||
#playlist-add{
|
#playlist-add{
|
||||||
display:contents;
|
display:contents;
|
||||||
}
|
}
|
||||||
#playlist-name-selection{
|
#playlist-name-selection{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
}
|
}
|
||||||
#playlist-add-button{
|
#playlist-add-button{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
grid-row:2;
|
grid-row:2;
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
}
|
}
|
||||||
#item-selection-reset{
|
#item-selection-reset{
|
||||||
grid-column:3;
|
grid-column:3;
|
||||||
grid-row:2;
|
grid-row:2;
|
||||||
justify-self:center;
|
justify-self:center;
|
||||||
}
|
}
|
||||||
main{
|
main{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-rows: 0fr 0fr 1fr;
|
grid-template-rows: 0fr 0fr 1fr;
|
||||||
grid-template-columns: 0fr 1fr;
|
grid-template-columns: 0fr 1fr;
|
||||||
}
|
}
|
||||||
main .avatar{
|
main .avatar{
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
grid-column:1;
|
grid-column:1;
|
||||||
}
|
}
|
||||||
main .title{
|
main .title{
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
}
|
}
|
||||||
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: #bcbcbc;
|
background-color: #bcbcbc;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
main .channel-info{
|
main .channel-info{
|
||||||
grid-row: 3;
|
grid-row: 3;
|
||||||
grid-column: 1 / span 3;
|
grid-column: 1 / span 3;
|
||||||
}
|
}
|
||||||
.tab{
|
.tab{
|
||||||
padding: 5px 75px;
|
padding: 5px 75px;
|
||||||
}
|
}
|
||||||
.description{
|
.description{
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div id="header-left">
|
<div id="header-left">
|
||||||
<form id="site-search" action="/youtube.com/search">
|
<form id="site-search" action="/youtube.com/search">
|
||||||
<input type="search" name="query" class="search-box">
|
<input type="search" name="query" class="search-box">
|
||||||
<button type="submit" value="Search" class="search-button">Search</button>
|
<button type="submit" value="Search" class="search-button">Search</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="header-right">
|
<div id="header-right">
|
||||||
<form id="playlist-add" action="/youtube.com/edit_playlist" method="post" target="_self">
|
<form id="playlist-add" action="/youtube.com/edit_playlist" method="post" target="_self">
|
||||||
<input type="hidden" name="action" value="add">
|
<input type="hidden" name="action" value="add">
|
||||||
<select name="playlist_name" id="playlist-name-selection">
|
<select name="playlist_name" id="playlist-name-selection">
|
||||||
<option value="watch_later">watch_later</option>
|
<option value="watch_later">watch_later</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" id="playlist-add-button">Add to playlist</button>
|
<button type="submit" id="playlist-add-button">Add to playlist</button>
|
||||||
<button type="reset" id="item-selection-reset">Clear selection</button>
|
<button type="reset" id="item-selection-reset">Clear selection</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<img class="avatar" src="$avatar">
|
<img class="avatar" src="$avatar">
|
||||||
<h2 class="title">$channel_title</h2>
|
<h2 class="title">$channel_title</h2>
|
||||||
<nav class="channel-tabs">
|
<nav class="channel-tabs">
|
||||||
<a class="tab page-button" href="$channel_videos_url">Videos</a>
|
<a class="tab page-button" href="$channel_videos_url">Videos</a>
|
||||||
<a class="tab page-button">About</a>
|
<a class="tab page-button">About</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="channel-info">
|
<div class="channel-info">
|
||||||
<ul>
|
<ul>
|
||||||
$stats
|
$stats
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
<hr>
|
<hr>
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
<span class="description">$description</span>
|
<span class="description">$description</span>
|
||||||
<hr>
|
<hr>
|
||||||
$links
|
$links
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,134 +1,134 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>$page_title</title>
|
<title>$page_title</title>
|
||||||
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
header{
|
header{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 2fr;
|
grid-template-columns: 3fr 2fr;
|
||||||
}
|
}
|
||||||
#header-left{
|
#header-left{
|
||||||
grid-column:1;
|
grid-column:1;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 1fr 640px;
|
grid-template-columns: 1fr 640px;
|
||||||
}
|
}
|
||||||
#site-search{
|
#site-search{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
#header-right{
|
#header-right{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns:40px 400px 100px 1fr;
|
grid-template-columns:40px 400px 100px 1fr;
|
||||||
grid-template-rows: 1fr 1fr;
|
grid-template-rows: 1fr 1fr;
|
||||||
}
|
}
|
||||||
#playlist-add{
|
#playlist-add{
|
||||||
display:contents;
|
display:contents;
|
||||||
}
|
}
|
||||||
#playlist-name-selection{
|
#playlist-name-selection{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
}
|
}
|
||||||
#playlist-add-button{
|
#playlist-add-button{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
grid-row:2;
|
grid-row:2;
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
}
|
}
|
||||||
#item-selection-reset{
|
#item-selection-reset{
|
||||||
grid-column:3;
|
grid-column:3;
|
||||||
grid-row:2;
|
grid-row:2;
|
||||||
justify-self:center;
|
justify-self:center;
|
||||||
}
|
}
|
||||||
main{
|
main{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-rows: 0fr 0fr 0fr 1fr;
|
grid-template-rows: 0fr 0fr 0fr 1fr;
|
||||||
grid-template-columns: 0fr 1fr;
|
grid-template-columns: 0fr 1fr;
|
||||||
}
|
}
|
||||||
main .avatar{
|
main .avatar{
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
grid-column:1;
|
grid-column:1;
|
||||||
height:200px;
|
height:200px;
|
||||||
width:200px;
|
width:200px;
|
||||||
}
|
}
|
||||||
main .title{
|
main .title{
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
}
|
}
|
||||||
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: #bcbcbc;
|
background-color: #bcbcbc;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
main .item-grid{
|
main .item-grid{
|
||||||
grid-row:4;
|
grid-row:4;
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: repeat(auto-fill, 400px);
|
grid-template-columns: repeat(auto-fill, 400px);
|
||||||
grid-auto-rows: 94px;
|
grid-auto-rows: 94px;
|
||||||
grid-row-gap: 10px;
|
grid-row-gap: 10px;
|
||||||
|
|
||||||
}
|
}
|
||||||
.page-button-row{
|
.page-button-row{
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
}
|
}
|
||||||
.tab{
|
.tab{
|
||||||
padding: 5px 75px;
|
padding: 5px 75px;
|
||||||
}
|
}
|
||||||
#number_of_results{
|
#number_of_results{
|
||||||
font-weight:bold;
|
font-weight:bold;
|
||||||
grid-row:3;
|
grid-row:3;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div id="header-left">
|
<div id="header-left">
|
||||||
<form id="site-search" action="/youtube.com/search">
|
<form id="site-search" action="/youtube.com/search">
|
||||||
<input type="search" name="query" class="search-box">
|
<input type="search" name="query" class="search-box">
|
||||||
<button type="submit" value="Search" class="search-button">Search</button>
|
<button type="submit" value="Search" class="search-button">Search</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="header-right">
|
<div id="header-right">
|
||||||
<form id="playlist-add" action="/youtube.com/edit_playlist" method="post" target="_self">
|
<form id="playlist-add" action="/youtube.com/edit_playlist" method="post" target="_self">
|
||||||
<input type="hidden" name="action" value="add">
|
<input type="hidden" name="action" value="add">
|
||||||
<select name="playlist_name" id="playlist-name-selection">
|
<select name="playlist_name" id="playlist-name-selection">
|
||||||
<option value="watch_later">watch_later</option>
|
<option value="watch_later">watch_later</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" id="playlist-add-button">Add to playlist</button>
|
<button type="submit" id="playlist-add-button">Add to playlist</button>
|
||||||
<button type="reset" id="item-selection-reset">Clear selection</button>
|
<button type="reset" id="item-selection-reset">Clear selection</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<img class="avatar" src="$avatar">
|
<img class="avatar" src="$avatar">
|
||||||
<h2 class="title">$channel_title</h2>
|
<h2 class="title">$channel_title</h2>
|
||||||
<nav class="channel-tabs">
|
<nav class="channel-tabs">
|
||||||
<a class="tab page-button">Videos</a>
|
<a class="tab page-button">Videos</a>
|
||||||
<a class="tab page-button" href="$channel_about_url">About</a>
|
<a class="tab page-button" href="$channel_about_url">About</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div id="number-of-results">$number_of_results</div>
|
<div id="number-of-results">$number_of_results</div>
|
||||||
<nav class="item-grid">
|
<nav class="item-grid">
|
||||||
$items
|
$items
|
||||||
</nav>
|
</nav>
|
||||||
<nav class="page-button-row">
|
<nav class="page-button-row">
|
||||||
$page_buttons
|
$page_buttons
|
||||||
</nav>
|
</nav>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,62 +1,62 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>$page_title</title>
|
<title>$page_title</title>
|
||||||
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
||||||
<link href="/youtube.com/comments.css" type="text/css" rel="stylesheet">
|
<link href="/youtube.com/comments.css" type="text/css" rel="stylesheet">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
main{
|
main{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 2fr;
|
grid-template-columns: 3fr 2fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
header{
|
header{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 2fr;
|
grid-template-columns: 3fr 2fr;
|
||||||
}
|
}
|
||||||
#header-left{
|
#header-left{
|
||||||
grid-column:1;
|
grid-column:1;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 1fr 640px;
|
grid-template-columns: 1fr 640px;
|
||||||
}
|
}
|
||||||
#site-search{
|
#site-search{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
#left{
|
#left{
|
||||||
background-color:#bcbcbc;
|
background-color:#bcbcbc;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
grid-template-columns: 1fr 640px;
|
grid-template-columns: 1fr 640px;
|
||||||
grid-template-rows: 0fr 0fr;
|
grid-template-rows: 0fr 0fr;
|
||||||
}
|
}
|
||||||
.comments{
|
.comments{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
}
|
}
|
||||||
#left .page-button{
|
#left .page-button{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div id="header-left">
|
<div id="header-left">
|
||||||
<form id="site-search" action="/youtube.com/search">
|
<form id="site-search" action="/youtube.com/search">
|
||||||
<input type="search" name="query" class="search-box">
|
<input type="search" name="query" class="search-box">
|
||||||
<button type="submit" value="Search" class="search-button">Search</button>
|
<button type="submit" value="Search" class="search-button">Search</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div id="left">
|
<div id="left">
|
||||||
<section class="comments">
|
<section class="comments">
|
||||||
$comments
|
$comments
|
||||||
</section>
|
</section>
|
||||||
$more_comments_button
|
$more_comments_button
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,132 +1,132 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>$page_title</title>
|
<title>$page_title</title>
|
||||||
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
main{
|
main{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 1fr;
|
grid-template-columns: 3fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
header{
|
header{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 1fr;
|
grid-template-columns: 3fr 1fr;
|
||||||
}
|
}
|
||||||
#header-left{
|
#header-left{
|
||||||
grid-column:1;
|
grid-column:1;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 1fr 800px;
|
grid-template-columns: 1fr 800px;
|
||||||
}
|
}
|
||||||
#site-search{
|
#site-search{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#left{
|
#left{
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 800px;
|
grid-template-columns: 1fr 800px;
|
||||||
grid-template-rows: 0fr 1fr 0fr;
|
grid-template-rows: 0fr 1fr 0fr;
|
||||||
}
|
}
|
||||||
.playlist-metadata{
|
.playlist-metadata{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 0fr 1fr;
|
grid-template-columns: 0fr 1fr;
|
||||||
grid-template-rows: 0fr 0fr 0fr 0fr 1fr;
|
grid-template-rows: 0fr 0fr 0fr 0fr 1fr;
|
||||||
}
|
}
|
||||||
.playlist-thumbnail{
|
.playlist-thumbnail{
|
||||||
grid-row: 1 / span 5;
|
grid-row: 1 / span 5;
|
||||||
grid-column:1;
|
grid-column:1;
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
width:250px;
|
width:250px;
|
||||||
}
|
}
|
||||||
.playlist-title{
|
.playlist-title{
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
}
|
}
|
||||||
.playlist-author{
|
.playlist-author{
|
||||||
grid-row:2;
|
grid-row:2;
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
}
|
}
|
||||||
.playlist-stats{
|
.playlist-stats{
|
||||||
grid-row:3;
|
grid-row:3;
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playlist-description{
|
.playlist-description{
|
||||||
grid-row:4;
|
grid-row:4;
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
min-width:0px;
|
min-width:0px;
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
.page-button-row{
|
.page-button-row{
|
||||||
grid-row: 3;
|
grid-row: 3;
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#right{
|
#right{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
|
||||||
}
|
}
|
||||||
#results{
|
#results{
|
||||||
|
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
|
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-rows: 0fr;
|
grid-auto-rows: 0fr;
|
||||||
grid-row-gap: 10px;
|
grid-row-gap: 10px;
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div id="header-left">
|
<div id="header-left">
|
||||||
<form id="site-search" action="/youtube.com/search">
|
<form id="site-search" action="/youtube.com/search">
|
||||||
<input type="search" name="query" class="search-box">
|
<input type="search" name="query" class="search-box">
|
||||||
<button type="submit" value="Search" class="search-button">Search</button>
|
<button type="submit" value="Search" class="search-button">Search</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div id="left">
|
<div id="left">
|
||||||
<div class="playlist-metadata">
|
<div class="playlist-metadata">
|
||||||
<img class="playlist-thumbnail" src="$thumbnail">
|
<img class="playlist-thumbnail" src="$thumbnail">
|
||||||
<h2 class="playlist-title">$title</h2>
|
<h2 class="playlist-title">$title</h2>
|
||||||
<a class="playlist-author" href="$author_url">$author</a>
|
<a class="playlist-author" href="$author_url">$author</a>
|
||||||
<div class="playlist-stats">
|
<div class="playlist-stats">
|
||||||
$stats
|
$stats
|
||||||
</div>
|
</div>
|
||||||
<div class="playlist-description">$description</div>
|
<div class="playlist-description">$description</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="results">
|
<div id="results">
|
||||||
$videos
|
$videos
|
||||||
</div>
|
</div>
|
||||||
<nav class="page-button-row">
|
<nav class="page-button-row">
|
||||||
$page_buttons
|
$page_buttons
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="right">
|
<div id="right">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,105 +1,105 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>$page_title</title>
|
<title>$page_title</title>
|
||||||
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
main{
|
main{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 1fr;
|
grid-template-columns: 3fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
header{
|
header{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 1fr;
|
grid-template-columns: 3fr 1fr;
|
||||||
}
|
}
|
||||||
#header-left{
|
#header-left{
|
||||||
grid-column:1;
|
grid-column:1;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 1fr 800px;
|
grid-template-columns: 1fr 800px;
|
||||||
}
|
}
|
||||||
#site-search{
|
#site-search{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#left{
|
#left{
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 800px;
|
grid-template-columns: 1fr 800px;
|
||||||
grid-row-gap: 20px;
|
grid-row-gap: 20px;
|
||||||
align-content:start;
|
align-content:start;
|
||||||
}
|
}
|
||||||
#number-of-results{
|
#number-of-results{
|
||||||
font-weight:bold;
|
font-weight:bold;
|
||||||
}
|
}
|
||||||
#result-info{
|
#result-info{
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
align-self:center;
|
align-self:center;
|
||||||
}
|
}
|
||||||
.page-button-row{
|
.page-button-row{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#right{
|
#right{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
|
||||||
}
|
}
|
||||||
#results{
|
#results{
|
||||||
|
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
|
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-rows: 138px;
|
grid-auto-rows: 138px;
|
||||||
grid-row-gap: 10px;
|
grid-row-gap: 10px;
|
||||||
|
|
||||||
}
|
}
|
||||||
.badge{
|
.badge{
|
||||||
background-color:#cccccc;
|
background-color:#cccccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div id="header-left">
|
<div id="header-left">
|
||||||
<form id="site-search" action="/youtube.com/search">
|
<form id="site-search" action="/youtube.com/search">
|
||||||
<input type="search" name="query" class="search-box" value="$search_box_value">
|
<input type="search" name="query" class="search-box" value="$search_box_value">
|
||||||
<button type="submit" value="Search" class="search-button">Search</button>
|
<button type="submit" value="Search" class="search-button">Search</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div id="left">
|
<div id="left">
|
||||||
<div id="result-info">
|
<div id="result-info">
|
||||||
<div id="number-of-results">Approximately $number_of_results results ($number_of_pages pages)</div>
|
<div id="number-of-results">Approximately $number_of_results results ($number_of_pages pages)</div>
|
||||||
$corrections
|
$corrections
|
||||||
</div>
|
</div>
|
||||||
<div id="results">
|
<div id="results">
|
||||||
$results
|
$results
|
||||||
</div>
|
</div>
|
||||||
<nav class="page-button-row">
|
<nav class="page-button-row">
|
||||||
$page_buttons
|
$page_buttons
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="right">
|
<div id="right">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,108 +1,108 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Search</title>
|
<title>Search</title>
|
||||||
<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">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
|
|
||||||
body{
|
body{
|
||||||
margin:0;
|
margin:0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
color:#222;
|
color:#222;
|
||||||
//background-color:#888888;
|
//background-color:#888888;
|
||||||
background-color:#cccccc;
|
background-color:#cccccc;
|
||||||
|
|
||||||
min-height:100vh;
|
min-height:100vh;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 1fr;
|
grid-template-columns: 3fr 1fr;
|
||||||
grid-template-rows: 50px 1fr;
|
grid-template-rows: 50px 1fr;
|
||||||
}
|
}
|
||||||
h3{
|
h3{
|
||||||
margin:0;
|
margin:0;
|
||||||
}
|
}
|
||||||
#header{
|
#header{
|
||||||
background-color:#333333;
|
background-color:#333333;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-column: 1 / span 3;
|
grid-column: 1 / span 3;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
|
||||||
grid-template-columns: 3fr 1fr;
|
grid-template-columns: 3fr 1fr;
|
||||||
|
|
||||||
}
|
}
|
||||||
#header-left{
|
#header-left{
|
||||||
display:grid;
|
display:grid;
|
||||||
|
|
||||||
grid-template-columns: 1fr 800px;
|
grid-template-columns: 1fr 800px;
|
||||||
|
|
||||||
}
|
}
|
||||||
#search-form{
|
#search-form{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 0fr;
|
grid-template-columns: 1fr 0fr;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#search-box{
|
#search-box{
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
align-self:center;
|
align-self:center;
|
||||||
height:25px;
|
height:25px;
|
||||||
padding:0;
|
padding:0;
|
||||||
margin:0;
|
margin:0;
|
||||||
border:0;
|
border:0;
|
||||||
|
|
||||||
}
|
}
|
||||||
#search-button{
|
#search-button{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
align-self:center;
|
align-self:center;
|
||||||
height:25px;
|
height:25px;
|
||||||
|
|
||||||
padding-top:0;
|
padding-top:0;
|
||||||
padding-bottom:0;
|
padding-bottom:0;
|
||||||
border-style:solid;
|
border-style:solid;
|
||||||
border-width:1px;
|
border-width:1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#left{
|
#left{
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
|
||||||
#right{
|
#right{
|
||||||
|
|
||||||
|
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<div id="header-left">
|
<div id="header-left">
|
||||||
<form id="search-form" action="/youtube.com/search">
|
<form id="search-form" action="/youtube.com/search">
|
||||||
<input type="text" name="query" id="search-box">
|
<input type="text" name="query" id="search-box">
|
||||||
<input type="submit" value="Search" id="search-button">
|
<input type="submit" value="Search" id="search-button">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="left">
|
<div id="left">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="right">
|
<div id="right">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -1,148 +1,148 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>$page_title</title>
|
<title>$page_title</title>
|
||||||
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
||||||
<link href="/youtube.com/comments.css" type="text/css" rel="stylesheet">
|
<link href="/youtube.com/comments.css" type="text/css" rel="stylesheet">
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
main{
|
main{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 2fr;
|
grid-template-columns: 3fr 2fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
header{
|
header{
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 3fr 2fr;
|
grid-template-columns: 3fr 2fr;
|
||||||
}
|
}
|
||||||
#header-left{
|
#header-left{
|
||||||
grid-column:1;
|
grid-column:1;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns: 1fr 640px;
|
grid-template-columns: 1fr 640px;
|
||||||
}
|
}
|
||||||
#site-search{
|
#site-search{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
#header-right{
|
#header-right{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
|
|
||||||
display:grid;
|
display:grid;
|
||||||
grid-template-columns:40px 400px 100px 1fr;
|
grid-template-columns:40px 400px 100px 1fr;
|
||||||
grid-template-rows: 1fr 1fr;
|
grid-template-rows: 1fr 1fr;
|
||||||
}
|
}
|
||||||
#playlist-add{
|
#playlist-add{
|
||||||
display:contents;
|
display:contents;
|
||||||
}
|
}
|
||||||
#playlist-name-selection{
|
#playlist-name-selection{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
grid-row:1;
|
grid-row:1;
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
}
|
}
|
||||||
#playlist-add-button{
|
#playlist-add-button{
|
||||||
grid-column:2;
|
grid-column:2;
|
||||||
grid-row:2;
|
grid-row:2;
|
||||||
justify-self:start;
|
justify-self:start;
|
||||||
}
|
}
|
||||||
#item-selection-reset{
|
#item-selection-reset{
|
||||||
grid-column:3;
|
grid-column:3;
|
||||||
grid-row:2;
|
grid-row:2;
|
||||||
justify-self:center;
|
justify-self:center;
|
||||||
}
|
}
|
||||||
#left{
|
#left{
|
||||||
background-color:#bcbcbc;
|
background-color:#bcbcbc;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
grid-template-columns: 1fr 640px;
|
grid-template-columns: 1fr 640px;
|
||||||
}
|
}
|
||||||
.full-item{
|
.full-item{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
.comments{
|
.comments{
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
grid-row: 6;
|
grid-row: 6;
|
||||||
margin-top:10px;
|
margin-top:10px;
|
||||||
}
|
}
|
||||||
.more-comments{
|
.more-comments{
|
||||||
grid-column: 1 / span 2;
|
grid-column: 1 / span 2;
|
||||||
}
|
}
|
||||||
#right{
|
#right{
|
||||||
background-color:#cccccc;
|
background-color:#cccccc;
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 40px 500px 1fr;
|
grid-template-columns: 40px 500px 1fr;
|
||||||
}
|
}
|
||||||
#related{
|
#related{
|
||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-rows: 90px;
|
grid-auto-rows: 90px;
|
||||||
grid-row-gap: 10px;
|
grid-row-gap: 10px;
|
||||||
}
|
}
|
||||||
#related .medium-item{
|
#related .medium-item{
|
||||||
grid-template-columns: 160px 1fr 0fr;
|
grid-template-columns: 160px 1fr 0fr;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div id="header-left">
|
<div id="header-left">
|
||||||
<form id="site-search" action="/youtube.com/search">
|
<form id="site-search" action="/youtube.com/search">
|
||||||
<input type="search" name="query" class="search-box">
|
<input type="search" name="query" class="search-box">
|
||||||
<button type="submit" value="Search" class="search-button">Search</button>
|
<button type="submit" value="Search" class="search-button">Search</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="header-right">
|
<div id="header-right">
|
||||||
<form id="playlist-add" action="/youtube.com/edit_playlist" method="post" target="_self">
|
<form id="playlist-add" action="/youtube.com/edit_playlist" method="post" target="_self">
|
||||||
<input type="hidden" name="action" value="add">
|
<input type="hidden" name="action" value="add">
|
||||||
<select name="playlist_name" id="playlist-name-selection">
|
<select name="playlist_name" id="playlist-name-selection">
|
||||||
<option value="watch_later">watch_later</option>
|
<option value="watch_later">watch_later</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" id="playlist-add-button">Add to playlist</button>
|
<button type="submit" id="playlist-add-button">Add to playlist</button>
|
||||||
<button type="reset" id="item-selection-reset">Clear selection</button>
|
<button type="reset" id="item-selection-reset">Clear selection</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div id="left">
|
<div id="left">
|
||||||
<article class="full-item">
|
<article class="full-item">
|
||||||
|
|
||||||
<video width="640" height="360" controls autofocus>
|
<video width="640" height="360" controls autofocus>
|
||||||
$video_sources
|
$video_sources
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
<h2 class="title">$video_title</h2>
|
<h2 class="title">$video_title</h2>
|
||||||
|
|
||||||
<address>Uploaded by <a href="$uploader_channel_url">$uploader</a></address>
|
<address>Uploaded by <a href="$uploader_channel_url">$uploader</a></address>
|
||||||
<span class="views">$views views</span>
|
<span class="views">$views views</span>
|
||||||
|
|
||||||
|
|
||||||
<time datetime="$upload_date">Published on $upload_date</time>
|
<time datetime="$upload_date">Published on $upload_date</time>
|
||||||
<span class="likes-dislikes">$likes likes $dislikes dislikes</span>
|
<span class="likes-dislikes">$likes likes $dislikes dislikes</span>
|
||||||
|
|
||||||
<span class="description">$description</span>
|
<span class="description">$description</span>
|
||||||
|
|
||||||
<section class="comments">
|
<section class="comments">
|
||||||
$comments
|
$comments
|
||||||
</section>
|
</section>
|
||||||
$more_comments_button
|
$more_comments_button
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="right">
|
<div id="right">
|
||||||
<nav id="related">
|
<nav id="related">
|
||||||
$related
|
$related
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
Loading…
x
Reference in New Issue
Block a user