fix line endings

This commit is contained in:
James Taylor 2018-07-02 17:45:25 -07:00
parent 2696fb30c2
commit 79937c1c82
22 changed files with 3260 additions and 3259 deletions

16
.gitignore vendored
View File

@ -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

View File

@ -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)

View File

@ -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;
} }

View File

@ -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,
) )

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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
) )

View File

@ -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

View File

@ -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

View File

@ -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;
} }

View File

@ -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():

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>