Convert channel page to flask framework
This commit is contained in:
parent
24553bfb4f
commit
64434b02ca
@ -6,7 +6,7 @@ from youtube import yt_app
|
|||||||
from youtube import util
|
from youtube import util
|
||||||
|
|
||||||
# these are just so the files get run - they import yt_app and add routes to it
|
# these are just so the files get run - they import yt_app and add routes to it
|
||||||
from youtube import watch, search, playlist
|
from youtube import watch, search, playlist, channel
|
||||||
|
|
||||||
import settings
|
import settings
|
||||||
|
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
import flask
|
import flask
|
||||||
yt_app = flask.Flask(__name__)
|
yt_app = flask.Flask(__name__)
|
||||||
|
yt_app.url_map.strict_slashes = False
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import base64
|
import base64
|
||||||
from youtube import util, yt_data_extract, html_common
|
from youtube import util, yt_data_extract, html_common
|
||||||
|
from youtube import yt_app
|
||||||
|
|
||||||
import http_errors
|
import http_errors
|
||||||
import urllib
|
import urllib
|
||||||
@ -12,11 +13,8 @@ import gevent
|
|||||||
import re
|
import re
|
||||||
import functools
|
import functools
|
||||||
|
|
||||||
with open("yt_channel_items_template.html", "r") as file:
|
import flask
|
||||||
yt_channel_items_template = Template(file.read())
|
from flask import request
|
||||||
|
|
||||||
with open("yt_channel_about_template.html", "r") as file:
|
|
||||||
yt_channel_about_template = Template(file.read())
|
|
||||||
|
|
||||||
'''continuation = Proto(
|
'''continuation = Proto(
|
||||||
Field('optional', 'continuation', 80226972, Proto(
|
Field('optional', 'continuation', 80226972, Proto(
|
||||||
@ -96,11 +94,7 @@ def get_channel_tab(channel_id, page="1", sort=3, tab='videos', view=1):
|
|||||||
|
|
||||||
'''with open('debug/channel_debug', 'wb') as f:
|
'''with open('debug/channel_debug', 'wb') as f:
|
||||||
f.write(content)'''
|
f.write(content)'''
|
||||||
info = json.loads(content)
|
return content
|
||||||
return info
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_number_of_videos(channel_id):
|
def get_number_of_videos(channel_id):
|
||||||
# Uploads playlist
|
# Uploads playlist
|
||||||
@ -136,231 +130,6 @@ def get_channel_id(username):
|
|||||||
response = util.fetch_url(url, util.mobile_ua + headers_1).decode('utf-8')
|
response = util.fetch_url(url, util.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 grid_items_html(items, additional_info={}):
|
|
||||||
result = ''' <nav class="item-grid">\n'''
|
|
||||||
for item in items:
|
|
||||||
result += html_common.renderer_html(item, additional_info)
|
|
||||||
result += '''\n</nav>'''
|
|
||||||
return result
|
|
||||||
|
|
||||||
def list_items_html(items, additional_info={}):
|
|
||||||
result = ''' <nav class="item-list">'''
|
|
||||||
for item in items:
|
|
||||||
result += html_common.renderer_html(item, additional_info)
|
|
||||||
result += '''\n</nav>'''
|
|
||||||
return result
|
|
||||||
|
|
||||||
channel_tab_template = Template('''\n<a class="tab page-button"$href_attribute>$tab_name</a>''')
|
|
||||||
channel_search_template = Template('''
|
|
||||||
<form class="channel-search" action="$action">
|
|
||||||
<input type="search" name="query" class="search-box" value="$search_box_value">
|
|
||||||
<button type="submit" value="Search" class="search-button">Search</button>
|
|
||||||
</form>''')
|
|
||||||
|
|
||||||
tabs = ('Videos', 'Playlists', 'About')
|
|
||||||
def channel_tabs_html(channel_id, current_tab, search_box_value=''):
|
|
||||||
result = ''
|
|
||||||
for tab_name in tabs:
|
|
||||||
if tab_name == current_tab:
|
|
||||||
result += channel_tab_template.substitute(
|
|
||||||
href_attribute = '',
|
|
||||||
tab_name = tab_name,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
result += channel_tab_template.substitute(
|
|
||||||
href_attribute = ' href="' + util.URL_ORIGIN + '/channel/' + channel_id + '/' + tab_name.lower() + '"',
|
|
||||||
tab_name = tab_name,
|
|
||||||
)
|
|
||||||
result += channel_search_template.substitute(
|
|
||||||
action = util.URL_ORIGIN + "/channel/" + channel_id + "/search",
|
|
||||||
search_box_value = html.escape(search_box_value),
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
channel_sort_button_template = Template('''\n<a class="sort-button"$href_attribute>$text</a>''')
|
|
||||||
sorts = {
|
|
||||||
"videos": (('1', 'views'), ('2', 'oldest'), ('3', 'newest'),),
|
|
||||||
"playlists": (('2', 'oldest'), ('3', 'newest'), ('4', 'last video added'),),
|
|
||||||
}
|
|
||||||
def channel_sort_buttons_html(channel_id, tab, current_sort):
|
|
||||||
result = ''
|
|
||||||
for sort_number, sort_name in sorts[tab]:
|
|
||||||
if sort_number == str(current_sort):
|
|
||||||
result += channel_sort_button_template.substitute(
|
|
||||||
href_attribute='',
|
|
||||||
text = 'Sorted by ' + sort_name
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
result += channel_sort_button_template.substitute(
|
|
||||||
href_attribute=' href="' + util.URL_ORIGIN + '/channel/' + channel_id + '/' + tab + '?sort=' + sort_number + '"',
|
|
||||||
text = 'Sort by ' + sort_name
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def get_microformat(response):
|
|
||||||
try:
|
|
||||||
return response['microformat']['microformatDataRenderer']
|
|
||||||
|
|
||||||
# channel doesn't exist or was terminated
|
|
||||||
# example terminated channel: https://www.youtube.com/channel/UCnKJeK_r90jDdIuzHXC0Org
|
|
||||||
except KeyError:
|
|
||||||
if 'alerts' in response and len(response['alerts']) > 0:
|
|
||||||
result = ''
|
|
||||||
for alert in response['alerts']:
|
|
||||||
result += alert['alertRenderer']['text']['simpleText'] + '\n'
|
|
||||||
raise http_errors.Code200(result)
|
|
||||||
elif 'errors' in response['responseContext']:
|
|
||||||
for error in response['responseContext']['errors']['error']:
|
|
||||||
if error['code'] == 'INVALID_VALUE' and error['location'] == 'browse_id':
|
|
||||||
raise http_errors.Error404('This channel does not exist')
|
|
||||||
raise
|
|
||||||
|
|
||||||
# example channel with no videos: https://www.youtube.com/user/jungleace
|
|
||||||
def get_grid_items(response):
|
|
||||||
try:
|
|
||||||
return response['continuationContents']['gridContinuation']['items']
|
|
||||||
except KeyError:
|
|
||||||
try:
|
|
||||||
contents = response['contents']
|
|
||||||
except KeyError:
|
|
||||||
return []
|
|
||||||
|
|
||||||
item_section = tab_with_content(contents['twoColumnBrowseResultsRenderer']['tabs'])['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]
|
|
||||||
try:
|
|
||||||
return item_section['gridRenderer']['items']
|
|
||||||
except KeyError:
|
|
||||||
if "messageRenderer" in item_section:
|
|
||||||
return []
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def channel_videos_html(polymer_json, current_page=1, current_sort=3, number_of_videos = 1000, current_query_string=''):
|
|
||||||
response = polymer_json[1]['response']
|
|
||||||
microformat = get_microformat(response)
|
|
||||||
channel_url = microformat['urlCanonical'].rstrip('/')
|
|
||||||
channel_id = channel_url[channel_url.rfind('/')+1:]
|
|
||||||
|
|
||||||
items = get_grid_items(response)
|
|
||||||
items_html = grid_items_html(items, {'author': microformat['title']})
|
|
||||||
|
|
||||||
return yt_channel_items_template.substitute(
|
|
||||||
header = html_common.get_header(),
|
|
||||||
channel_title = microformat['title'],
|
|
||||||
channel_tabs = channel_tabs_html(channel_id, 'Videos'),
|
|
||||||
sort_buttons = channel_sort_buttons_html(channel_id, 'videos', current_sort),
|
|
||||||
avatar = '/' + microformat['thumbnail']['thumbnails'][0]['url'],
|
|
||||||
page_title = microformat['title'] + ' - Channel',
|
|
||||||
items = items_html,
|
|
||||||
page_buttons = html_common.page_buttons_html(current_page, math.ceil(number_of_videos/30), util.URL_ORIGIN + "/channel/" + channel_id + "/videos", current_query_string),
|
|
||||||
number_of_results = '{:,}'.format(number_of_videos) + " videos",
|
|
||||||
)
|
|
||||||
|
|
||||||
def channel_playlists_html(polymer_json, current_sort=3):
|
|
||||||
response = polymer_json[1]['response']
|
|
||||||
microformat = get_microformat(response)
|
|
||||||
channel_url = microformat['urlCanonical'].rstrip('/')
|
|
||||||
channel_id = channel_url[channel_url.rfind('/')+1:]
|
|
||||||
|
|
||||||
items = get_grid_items(response)
|
|
||||||
items_html = grid_items_html(items, {'author': microformat['title']})
|
|
||||||
|
|
||||||
return yt_channel_items_template.substitute(
|
|
||||||
header = html_common.get_header(),
|
|
||||||
channel_title = microformat['title'],
|
|
||||||
channel_tabs = channel_tabs_html(channel_id, 'Playlists'),
|
|
||||||
sort_buttons = channel_sort_buttons_html(channel_id, 'playlists', current_sort),
|
|
||||||
avatar = '/' + microformat['thumbnail']['thumbnails'][0]['url'],
|
|
||||||
page_title = microformat['title'] + ' - Channel',
|
|
||||||
items = items_html,
|
|
||||||
page_buttons = '',
|
|
||||||
number_of_results = '',
|
|
||||||
)
|
|
||||||
|
|
||||||
# Example channel where tabs do not have definite index: https://www.youtube.com/channel/UC4gQ8i3FD7YbhOgqUkeQEJg
|
|
||||||
def tab_with_content(tabs):
|
|
||||||
for tab in tabs:
|
|
||||||
try:
|
|
||||||
renderer = tab['tabRenderer']
|
|
||||||
except KeyError:
|
|
||||||
renderer = tab['expandableTabRenderer']
|
|
||||||
try:
|
|
||||||
return renderer['content']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise Exception("No tabs found with content")
|
|
||||||
|
|
||||||
channel_link_template = Template('''
|
|
||||||
<li><a href="$url">$text</a></li>''')
|
|
||||||
stat_template = Template('''
|
|
||||||
<li>$stat_value</li>''')
|
|
||||||
def channel_about_page(polymer_json):
|
|
||||||
microformat = get_microformat(polymer_json[1]['response'])
|
|
||||||
avatar = '/' + microformat['thumbnail']['thumbnails'][0]['url']
|
|
||||||
# my goodness...
|
|
||||||
channel_metadata = tab_with_content(polymer_json[1]['response']['contents']['twoColumnBrowseResultsRenderer']['tabs'])['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]['channelAboutFullMetadataRenderer']
|
|
||||||
channel_links = ''
|
|
||||||
for link_json in channel_metadata.get('primaryLinks', ()):
|
|
||||||
url = link_json['navigationEndpoint']['urlEndpoint']['url']
|
|
||||||
if url.startswith("/redirect"):
|
|
||||||
query_string = url[url.find('?')+1: ]
|
|
||||||
url = urllib.parse.parse_qs(query_string)['q'][0]
|
|
||||||
|
|
||||||
channel_links += channel_link_template.substitute(
|
|
||||||
url = html.escape(url),
|
|
||||||
text = yt_data_extract.get_plain_text(link_json['title']),
|
|
||||||
)
|
|
||||||
|
|
||||||
stats = ''
|
|
||||||
for stat_name in ('subscriberCountText', 'joinedDateText', 'viewCountText', 'country'):
|
|
||||||
try:
|
|
||||||
stat_value = yt_data_extract.get_plain_text(channel_metadata[stat_name])
|
|
||||||
except KeyError:
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
stats += stat_template.substitute(stat_value=stat_value)
|
|
||||||
try:
|
|
||||||
description = yt_data_extract.format_text_runs(yt_data_extract.get_formatted_text(channel_metadata['description']))
|
|
||||||
except KeyError:
|
|
||||||
description = ''
|
|
||||||
return yt_channel_about_template.substitute(
|
|
||||||
header = html_common.get_header(),
|
|
||||||
page_title = yt_data_extract.get_plain_text(channel_metadata['title']) + ' - About',
|
|
||||||
channel_title = yt_data_extract.get_plain_text(channel_metadata['title']),
|
|
||||||
avatar = html.escape(avatar),
|
|
||||||
description = description,
|
|
||||||
links = channel_links,
|
|
||||||
stats = stats,
|
|
||||||
channel_tabs = channel_tabs_html(channel_metadata['channelId'], 'About'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def channel_search_page(polymer_json, query, current_page=1, number_of_videos = 1000, current_query_string=''):
|
|
||||||
response = polymer_json[1]['response']
|
|
||||||
microformat = get_microformat(response)
|
|
||||||
channel_url = microformat['urlCanonical'].rstrip('/')
|
|
||||||
channel_id = channel_url[channel_url.rfind('/')+1:]
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
items = tab_with_content(response['contents']['twoColumnBrowseResultsRenderer']['tabs'])['sectionListRenderer']['contents']
|
|
||||||
except KeyError:
|
|
||||||
items = response['continuationContents']['sectionListContinuation']['contents']
|
|
||||||
|
|
||||||
items_html = list_items_html(items)
|
|
||||||
|
|
||||||
return yt_channel_items_template.substitute(
|
|
||||||
header = html_common.get_header(),
|
|
||||||
channel_title = html.escape(microformat['title']),
|
|
||||||
channel_tabs = channel_tabs_html(channel_id, '', query),
|
|
||||||
avatar = '/' + microformat['thumbnail']['thumbnails'][0]['url'],
|
|
||||||
page_title = html.escape(query + ' - Channel search'),
|
|
||||||
items = items_html,
|
|
||||||
page_buttons = html_common.page_buttons_html(current_page, math.ceil(number_of_videos/29), util.URL_ORIGIN + "/channel/" + channel_id + "/search", current_query_string),
|
|
||||||
number_of_results = '',
|
|
||||||
sort_buttons = '',
|
|
||||||
)
|
|
||||||
def get_channel_search_json(channel_id, query, page):
|
def get_channel_search_json(channel_id, query, page):
|
||||||
params = proto.string(2, 'search') + proto.string(15, str(page))
|
params = proto.string(2, 'search') + proto.string(15, str(page))
|
||||||
params = proto.percent_b64encode(params)
|
params = proto.percent_b64encode(params)
|
||||||
@ -370,24 +139,148 @@ def get_channel_search_json(channel_id, query, page):
|
|||||||
polymer_json = util.fetch_url("https://www.youtube.com/browse_ajax?ctoken=" + ctoken, util.desktop_ua + headers_1)
|
polymer_json = util.fetch_url("https://www.youtube.com/browse_ajax?ctoken=" + ctoken, util.desktop_ua + headers_1)
|
||||||
'''with open('debug/channel_search_debug', 'wb') as f:
|
'''with open('debug/channel_search_debug', 'wb') as f:
|
||||||
f.write(polymer_json)'''
|
f.write(polymer_json)'''
|
||||||
polymer_json = json.loads(polymer_json)
|
|
||||||
|
|
||||||
return polymer_json
|
return polymer_json
|
||||||
|
|
||||||
playlist_sort_codes = {'2': "da", '3': "dd", '4': "lad"}
|
def extract_info(polymer_json, tab, html_prepare=True):
|
||||||
def get_channel_page(env, start_response):
|
response = polymer_json[1]['response']
|
||||||
path_parts = env['path_parts']
|
|
||||||
channel_id = path_parts[1]
|
|
||||||
try:
|
try:
|
||||||
tab = path_parts[2]
|
microformat = response['microformat']['microformatDataRenderer']
|
||||||
except IndexError:
|
|
||||||
tab = 'videos'
|
# channel doesn't exist or was terminated
|
||||||
|
# example terminated channel: https://www.youtube.com/channel/UCnKJeK_r90jDdIuzHXC0Org
|
||||||
|
except KeyError:
|
||||||
|
if 'alerts' in response and len(response['alerts']) > 0:
|
||||||
|
result = ''
|
||||||
|
for alert in response['alerts']:
|
||||||
|
result += alert['alertRenderer']['text']['simpleText'] + '\n'
|
||||||
|
flask.abort(200, result)
|
||||||
|
elif 'errors' in response['responseContext']:
|
||||||
|
for error in response['responseContext']['errors']['error']:
|
||||||
|
if error['code'] == 'INVALID_VALUE' and error['location'] == 'browse_id':
|
||||||
|
flask.abort(404, 'This channel does not exist')
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
info = {}
|
||||||
|
info['current_tab'] = tab
|
||||||
|
|
||||||
|
|
||||||
|
# stuff from microformat (info given by youtube for every page on channel)
|
||||||
|
info['description'] = microformat['description']
|
||||||
|
info['channel_name'] = microformat['title']
|
||||||
|
info['avatar'] = microformat['thumbnail']['thumbnails'][0]['url']
|
||||||
|
channel_url = microformat['urlCanonical'].rstrip('/')
|
||||||
|
channel_id = channel_url[channel_url.rfind('/')+1:]
|
||||||
|
info['channel_id'] = channel_id
|
||||||
|
info['channel_url'] = 'https://www.youtube.com/channel/' + channel_id
|
||||||
|
|
||||||
|
|
||||||
|
# empty channel
|
||||||
|
if 'contents' not in response and 'continuationContents' not in response:
|
||||||
|
info['items'] = []
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
# find the tab with content
|
||||||
|
# example channel where tabs do not have definite index: https://www.youtube.com/channel/UC4gQ8i3FD7YbhOgqUkeQEJg
|
||||||
|
# TODO: maybe use the 'selected' attribute for this?
|
||||||
|
if 'continuationContents' not in response:
|
||||||
|
tab_renderer = None
|
||||||
|
tab_content = None
|
||||||
|
for tab_json in response['contents']['twoColumnBrowseResultsRenderer']['tabs']:
|
||||||
|
try:
|
||||||
|
tab_renderer = tab_json['tabRenderer']
|
||||||
|
except KeyError:
|
||||||
|
tab_renderer = tab_json['expandableTabRenderer']
|
||||||
|
try:
|
||||||
|
tab_content = tab_renderer['content']
|
||||||
|
break
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else: # didn't break
|
||||||
|
raise Exception("No tabs found with content")
|
||||||
|
assert tab == tab_renderer['title'].lower()
|
||||||
|
|
||||||
|
|
||||||
|
# extract tab-specific info
|
||||||
|
if tab in ('videos', 'playlists', 'search'): # find the list of items
|
||||||
|
if 'continuationContents' in response:
|
||||||
|
try:
|
||||||
|
items = response['continuationContents']['gridContinuation']['items']
|
||||||
|
except KeyError:
|
||||||
|
items = response['continuationContents']['sectionListContinuation']['contents'] # for search
|
||||||
|
else:
|
||||||
|
contents = tab_content['sectionListRenderer']['contents']
|
||||||
|
if 'itemSectionRenderer' in contents[0]:
|
||||||
|
item_section = contents[0]['itemSectionRenderer']['contents'][0]
|
||||||
|
try:
|
||||||
|
items = item_section['gridRenderer']['items']
|
||||||
|
except KeyError:
|
||||||
|
if "messageRenderer" in item_section:
|
||||||
|
items = []
|
||||||
|
else:
|
||||||
|
raise Exception('gridRenderer missing but messageRenderer not found')
|
||||||
|
else:
|
||||||
|
items = contents # for search
|
||||||
|
|
||||||
|
# TODO: Fix this URL prefixing shit
|
||||||
|
additional_info = {'author': info['channel_name'], 'author_url': '/channel/' + channel_id}
|
||||||
|
if html_prepare:
|
||||||
|
info['items'] = [yt_data_extract.parse_info_prepare_for_html(renderer, additional_info) for renderer in items]
|
||||||
|
elif items is not None:
|
||||||
|
info['items'] = [yt_data_extract.renderer_info(renderer, additional_info) for renderer in items]
|
||||||
|
|
||||||
|
elif tab == 'about':
|
||||||
|
channel_metadata = tab_content['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]['channelAboutFullMetadataRenderer']
|
||||||
|
|
||||||
|
|
||||||
|
info['links'] = []
|
||||||
|
for link_json in channel_metadata.get('primaryLinks', ()):
|
||||||
|
url = link_json['navigationEndpoint']['urlEndpoint']['url']
|
||||||
|
if url.startswith('/redirect'): # youtube puts these on external links to do tracking
|
||||||
|
query_string = url[url.find('?')+1: ]
|
||||||
|
url = urllib.parse.parse_qs(query_string)['q'][0]
|
||||||
|
|
||||||
|
text = yt_data_extract.get_plain_text(link_json['title'])
|
||||||
|
|
||||||
|
info['links'].append( (text, url) )
|
||||||
|
|
||||||
|
|
||||||
|
info['stats'] = []
|
||||||
|
for stat_name in ('subscriberCountText', 'joinedDateText', 'viewCountText', 'country'):
|
||||||
|
try:
|
||||||
|
stat = channel_metadata[stat_name]
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
info['stats'].append(yt_data_extract.get_plain_text(stat))
|
||||||
|
|
||||||
|
|
||||||
|
info['description'] = yt_data_extract.get_text(channel_metadata['description'])
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise NotImplementedError('Unknown or unsupported channel tab: ' + tab)
|
||||||
|
|
||||||
|
|
||||||
|
if html_prepare:
|
||||||
|
info['avatar'] = '/' + info['avatar']
|
||||||
|
info['channel_url'] = '/' + info['channel_url']
|
||||||
|
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
playlist_sort_codes = {'2': "da", '3': "dd", '4': "lad"}
|
||||||
|
|
||||||
|
@yt_app.route('/channel/<channel_id>/')
|
||||||
|
@yt_app.route('/channel/<channel_id>/<tab>')
|
||||||
|
def get_channel_page(channel_id, tab='videos'):
|
||||||
|
|
||||||
|
page_number = int(request.args.get('page', 1))
|
||||||
|
sort = request.args.get('sort', '3')
|
||||||
|
view = request.args.get('view', '1')
|
||||||
|
query = request.args.get('query', '')
|
||||||
|
|
||||||
parameters = env['parameters']
|
|
||||||
page_number = int(util.default_multi_get(parameters, 'page', 0, default='1'))
|
|
||||||
sort = util.default_multi_get(parameters, 'sort', 0, default='3')
|
|
||||||
view = util.default_multi_get(parameters, 'view', 0, default='1')
|
|
||||||
query = util.default_multi_get(parameters, 'query', 0, default='')
|
|
||||||
|
|
||||||
if tab == 'videos':
|
if tab == 'videos':
|
||||||
tasks = (
|
tasks = (
|
||||||
@ -397,17 +290,10 @@ def get_channel_page(env, start_response):
|
|||||||
gevent.joinall(tasks)
|
gevent.joinall(tasks)
|
||||||
number_of_videos, polymer_json = tasks[0].value, tasks[1].value
|
number_of_videos, polymer_json = tasks[0].value, tasks[1].value
|
||||||
|
|
||||||
result = channel_videos_html(polymer_json, page_number, sort, number_of_videos, env['QUERY_STRING'])
|
|
||||||
elif tab == 'about':
|
elif tab == 'about':
|
||||||
polymer_json = util.fetch_url('https://www.youtube.com/channel/' + channel_id + '/about?pbj=1', util.desktop_ua + headers_1)
|
polymer_json = util.fetch_url('https://www.youtube.com/channel/' + channel_id + '/about?pbj=1', util.desktop_ua + headers_1)
|
||||||
polymer_json = json.loads(polymer_json)
|
|
||||||
result = channel_about_page(polymer_json)
|
|
||||||
elif tab == 'playlists':
|
elif tab == 'playlists':
|
||||||
polymer_json = util.fetch_url('https://www.youtube.com/channel/' + channel_id + '/playlists?pbj=1&view=1&sort=' + playlist_sort_codes[sort], util.desktop_ua + headers_1)
|
polymer_json = util.fetch_url('https://www.youtube.com/channel/' + channel_id + '/playlists?pbj=1&view=1&sort=' + playlist_sort_codes[sort], util.desktop_ua + headers_1)
|
||||||
'''with open('debug/channel_playlists_debug', 'wb') as f:
|
|
||||||
f.write(polymer_json)'''
|
|
||||||
polymer_json = json.loads(polymer_json)
|
|
||||||
result = channel_playlists_html(polymer_json, sort)
|
|
||||||
elif tab == 'search':
|
elif tab == 'search':
|
||||||
tasks = (
|
tasks = (
|
||||||
gevent.spawn(get_number_of_videos, channel_id ),
|
gevent.spawn(get_number_of_videos, channel_id ),
|
||||||
@ -416,54 +302,78 @@ def get_channel_page(env, start_response):
|
|||||||
gevent.joinall(tasks)
|
gevent.joinall(tasks)
|
||||||
number_of_videos, polymer_json = tasks[0].value, tasks[1].value
|
number_of_videos, polymer_json = tasks[0].value, tasks[1].value
|
||||||
|
|
||||||
result = channel_search_page(polymer_json, query, page_number, number_of_videos, env['QUERY_STRING'])
|
|
||||||
else:
|
else:
|
||||||
start_response('404 Not Found', [('Content-type', 'text/plain'),])
|
flask.abort(404, 'Unknown channel tab: ' + tab)
|
||||||
return b'Unknown channel tab: ' + tab.encode('utf-8')
|
|
||||||
|
|
||||||
start_response('200 OK', [('Content-type','text/html'),])
|
|
||||||
return result.encode('utf-8')
|
|
||||||
|
|
||||||
# youtube.com/user/[username]/[page]
|
info = extract_info(json.loads(polymer_json), tab)
|
||||||
# youtube.com/c/[custom]/[page]
|
if tab in ('videos', 'search'):
|
||||||
# youtube.com/[custom]/[page]
|
info['number_of_videos'] = number_of_videos
|
||||||
def get_channel_page_general_url(env, start_response):
|
info['number_of_pages'] = math.ceil(number_of_videos/30)
|
||||||
path_parts = env['path_parts']
|
if tab in ('videos', 'playlists'):
|
||||||
|
info['current_sort'] = sort
|
||||||
|
elif tab == 'search':
|
||||||
|
info['search_box_value'] = query
|
||||||
|
|
||||||
is_toplevel = not path_parts[0] in ('user', 'c')
|
|
||||||
|
|
||||||
if len(path_parts) + int(is_toplevel) == 3: # has /[page] after it
|
return flask.render_template('channel.html',
|
||||||
page = path_parts[2]
|
parameters_dictionary = request.args,
|
||||||
base_url = 'https://www.youtube.com/' + '/'.join(path_parts[0:-1])
|
**info
|
||||||
elif len(path_parts) + int(is_toplevel) == 2: # does not have /[page] after it, use /videos by default
|
)
|
||||||
page = 'videos'
|
|
||||||
base_url = 'https://www.youtube.com/' + '/'.join(path_parts)
|
|
||||||
else:
|
|
||||||
start_response('404 Not Found', [('Content-type', 'text/plain'),])
|
|
||||||
return b'Invalid channel url'
|
|
||||||
|
|
||||||
if page == 'videos':
|
|
||||||
|
# youtube.com/user/[username]/[tab]
|
||||||
|
# youtube.com/c/[custom]/[tab]
|
||||||
|
# youtube.com/[custom]/[tab]
|
||||||
|
def get_channel_page_general_url(base_url, tab, request):
|
||||||
|
|
||||||
|
page_number = int(request.args.get('page', 1))
|
||||||
|
sort = request.args.get('sort', '3')
|
||||||
|
view = request.args.get('view', '1')
|
||||||
|
query = request.args.get('query', '')
|
||||||
|
|
||||||
|
if tab == 'videos':
|
||||||
polymer_json = util.fetch_url(base_url + '/videos?pbj=1&view=0', util.desktop_ua + headers_1)
|
polymer_json = util.fetch_url(base_url + '/videos?pbj=1&view=0', util.desktop_ua + headers_1)
|
||||||
'''with open('debug/user_page_videos', 'wb') as f:
|
with open('debug/channel_debug', 'wb') as f:
|
||||||
f.write(polymer_json)'''
|
f.write(polymer_json)
|
||||||
polymer_json = json.loads(polymer_json)
|
elif tab == 'about':
|
||||||
result = channel_videos_html(polymer_json)
|
|
||||||
elif page == 'about':
|
|
||||||
polymer_json = util.fetch_url(base_url + '/about?pbj=1', util.desktop_ua + headers_1)
|
polymer_json = util.fetch_url(base_url + '/about?pbj=1', util.desktop_ua + headers_1)
|
||||||
polymer_json = json.loads(polymer_json)
|
elif tab == 'playlists':
|
||||||
result = channel_about_page(polymer_json)
|
|
||||||
elif page == 'playlists':
|
|
||||||
polymer_json = util.fetch_url(base_url+ '/playlists?pbj=1&view=1', util.desktop_ua + headers_1)
|
polymer_json = util.fetch_url(base_url+ '/playlists?pbj=1&view=1', util.desktop_ua + headers_1)
|
||||||
polymer_json = json.loads(polymer_json)
|
elif tab == 'search':
|
||||||
result = channel_playlists_html(polymer_json)
|
|
||||||
elif page == 'search':
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
'''polymer_json = util.fetch_url('https://www.youtube.com/user' + username + '/search?pbj=1&' + query_string, util.desktop_ua + headers_1)
|
|
||||||
polymer_json = json.loads(polymer_json)
|
|
||||||
return channel_search_page('''
|
|
||||||
else:
|
else:
|
||||||
start_response('404 Not Found', [('Content-type', 'text/plain'),])
|
flask.abort(404, 'Unknown channel tab: ' + tab)
|
||||||
return b'Unknown channel page: ' + page.encode('utf-8')
|
|
||||||
|
|
||||||
|
info = extract_info(json.loads(polymer_json), tab)
|
||||||
|
if tab in ('videos', 'search'):
|
||||||
|
info['number_of_videos'] = 1000
|
||||||
|
info['number_of_pages'] = math.ceil(1000/30)
|
||||||
|
if tab in ('videos', 'playlists'):
|
||||||
|
info['current_sort'] = sort
|
||||||
|
elif tab == 'search':
|
||||||
|
info['search_box_value'] = query
|
||||||
|
|
||||||
|
|
||||||
|
return flask.render_template('channel.html',
|
||||||
|
parameters_dictionary = request.args,
|
||||||
|
**info
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@yt_app.route('/user/<username>/')
|
||||||
|
@yt_app.route('/user/<username>/<tab>')
|
||||||
|
def get_user_page(username, tab='videos'):
|
||||||
|
return get_channel_page_general_url('https://www.youtube.com/user/' + username, tab, request)
|
||||||
|
|
||||||
|
@yt_app.route('/c/<custom>/')
|
||||||
|
@yt_app.route('/c/<custom>/<tab>')
|
||||||
|
def get_custom_c_page(custom, tab='videos'):
|
||||||
|
return get_channel_page_general_url('https://www.youtube.com/c/' + custom, tab, request)
|
||||||
|
|
||||||
|
@yt_app.route('/<custom>')
|
||||||
|
@yt_app.route('/<custom>/<tab>')
|
||||||
|
def get_toplevel_custom_page(custom, tab='videos'):
|
||||||
|
return get_channel_page_general_url('https://www.youtube.com/' + custom, tab, request)
|
||||||
|
|
||||||
start_response('200 OK', [('Content-type','text/html'),])
|
|
||||||
return result.encode('utf-8')
|
|
||||||
|
144
youtube/templates/channel.html
Normal file
144
youtube/templates/channel.html
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block page_title %}{{ channel_name + ' - Channel' }}{% endblock %}
|
||||||
|
{% import "common_elements.html" as common_elements %}
|
||||||
|
{% block style %}
|
||||||
|
main{
|
||||||
|
display:grid;
|
||||||
|
{% if current_tab == 'about' %}
|
||||||
|
grid-template-rows: 0fr 0fr 1fr;
|
||||||
|
grid-template-columns: 0fr 1fr;
|
||||||
|
{% else %}
|
||||||
|
grid-template-rows: repeat(5, 0fr);
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
main .avatar{
|
||||||
|
grid-row:1;
|
||||||
|
grid-column:1;
|
||||||
|
height:200px;
|
||||||
|
width:200px;
|
||||||
|
}
|
||||||
|
main .title{
|
||||||
|
grid-row:1;
|
||||||
|
grid-column:2;
|
||||||
|
}
|
||||||
|
main .channel-tabs{
|
||||||
|
grid-row:2;
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
|
||||||
|
display:grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
justify-content:start;
|
||||||
|
|
||||||
|
background-color: #aaaaaa;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
#links-metadata{
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-column-gap: 10px;
|
||||||
|
grid-column: 1/span 2;
|
||||||
|
justify-content: start;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
background-color: #bababa;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
#number-of-results{
|
||||||
|
font-weight:bold;
|
||||||
|
}
|
||||||
|
.item-grid{
|
||||||
|
grid-row:4;
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
|
.item-list{
|
||||||
|
width:1000px;
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
|
.page-button-row{
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
|
.tab{
|
||||||
|
padding: 5px 75px;
|
||||||
|
}
|
||||||
|
main .channel-info{
|
||||||
|
grid-row: 3;
|
||||||
|
grid-column: 1 / span 3;
|
||||||
|
}
|
||||||
|
.description{
|
||||||
|
white-space: pre-wrap;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
{% endblock style %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<img class="avatar" src="{{ avatar }}">
|
||||||
|
<h2 class="title">{{ channel_name }}</h2>
|
||||||
|
<nav class="channel-tabs">
|
||||||
|
{% for tab_name in ('Videos', 'Playlists', 'About') %}
|
||||||
|
{% if tab_name.lower() == current_tab %}
|
||||||
|
<a class="tab page-button">{{ tab_name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="tab page-button" href="{{ channel_url + '/' + tab_name.lower() }}">{{ tab_name }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<form class="channel-search" action="{{ channel_url + '/search' }}">
|
||||||
|
<input type="search" name="query" class="search-box" value="{{ search_box_value }}">
|
||||||
|
<button type="submit" value="Search" class="search-button">Search</button>
|
||||||
|
</form>
|
||||||
|
</nav>
|
||||||
|
{% if current_tab == 'about' %}
|
||||||
|
<div class="channel-info">
|
||||||
|
<ul>
|
||||||
|
{% for stat in stats %}
|
||||||
|
<li>{{ stat }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<h3>Description</h3>
|
||||||
|
<span class="description">{{ common_elements.text_runs(description) }}</span>
|
||||||
|
<hr>
|
||||||
|
<ul>
|
||||||
|
{% for text, url in links %}
|
||||||
|
<li><a href="{{ url }}">{{ text }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div id="links-metadata">
|
||||||
|
{% if current_tab == 'videos' %}
|
||||||
|
{% set sorts = [('1', 'views'), ('2', 'oldest'), ('3', 'newest')] %}
|
||||||
|
<div id="number-of-results">{{ number_of_videos }} videos</div>
|
||||||
|
{% elif current_tab == 'playlists' %}
|
||||||
|
{% set sorts = [('2', 'oldest'), ('3', 'newest'), ('4', 'last video added')] %}
|
||||||
|
{% else %}
|
||||||
|
{% set sorts = [] %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for sort_number, sort_name in sorts %}
|
||||||
|
{% if sort_number == current_sort.__str__() %}
|
||||||
|
<a class="sort-button">{{ 'Sorted by ' + sort_name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="sort-button" href="{{ channel_url + '/' + current_tab + '?sort=' + sort_number }}">{{ 'Sort by ' + sort_name }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if current_tab != 'about' %}
|
||||||
|
<nav class="{{ 'item-list' if current_tab == 'search' else 'item-grid' }}">
|
||||||
|
{% for item_info in items %}
|
||||||
|
{{ common_elements.item(item_info, include_author=false) }}
|
||||||
|
{% endfor %}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{% if current_tab != 'playlists' %}
|
||||||
|
<nav class="page-button-row">
|
||||||
|
{{ common_elements.page_buttons(number_of_pages, channel_url + '/' + current_tab, parameters_dictionary) }}
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
{% endblock main %}
|
@ -14,7 +14,7 @@
|
|||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro small_item(info) %}
|
{% macro small_item(info, include_author=true) %}
|
||||||
<div class="small-item-box">
|
<div class="small-item-box">
|
||||||
<div class="small-item">
|
<div class="small-item">
|
||||||
{% if info['type'] == 'video' %}
|
{% if info['type'] == 'video' %}
|
||||||
@ -47,12 +47,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro get_stats(info) %}
|
{% macro get_stats(info, include_author=true) %}
|
||||||
|
{% if include_author %}
|
||||||
{% if 'author_url' is in(info) %}
|
{% if 'author_url' is in(info) %}
|
||||||
<address>By <a href="{{ info['author_url'] }}">{{ info['author'] }}</a></address>
|
<address>By <a href="{{ info['author_url'] }}">{{ info['author'] }}</a></address>
|
||||||
{% else %}
|
{% else %}
|
||||||
<address><b>{{ info['author'] }}</b></address>
|
<address><b>{{ info['author'] }}</b></address>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% if 'views' is in(info) %}
|
{% if 'views' is in(info) %}
|
||||||
<span class="views">{{ info['views'] }}</span>
|
<span class="views">{{ info['views'] }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -63,7 +65,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% macro medium_item(info) %}
|
{% macro medium_item(info, include_author=true) %}
|
||||||
<div class="medium-item-box">
|
<div class="medium-item-box">
|
||||||
<div class="medium-item">
|
<div class="medium-item">
|
||||||
{% if info['type'] == 'video' %}
|
{% if info['type'] == 'video' %}
|
||||||
@ -75,7 +77,7 @@
|
|||||||
<a class="title" href="{{ info['url'] }}" title="{{ info['title'] }}">{{ info['title'] }}</a>
|
<a class="title" href="{{ info['url'] }}" title="{{ info['title'] }}">{{ info['title'] }}</a>
|
||||||
|
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
{{ get_stats(info) }}
|
{{ get_stats(info, include_author) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="description">{{ text_runs(info.get('description', '')) }}</span>
|
<span class="description">{{ text_runs(info.get('description', '')) }}</span>
|
||||||
@ -91,7 +93,7 @@
|
|||||||
<a class="title" href="{{ info['url'] }}" title="{{ info['title'] }}">{{ info['title'] }}</a>
|
<a class="title" href="{{ info['url'] }}" title="{{ info['title'] }}">{{ info['title'] }}</a>
|
||||||
|
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
{{ get_stats(info) }}
|
{{ get_stats(info, include_author) }}
|
||||||
</div>
|
</div>
|
||||||
{% elif info['type'] == 'channel' %}
|
{% elif info['type'] == 'channel' %}
|
||||||
<a class="video-thumbnail-box" href="{{ info['url'] }}" title="{{ info['title'] }}">
|
<a class="video-thumbnail-box" href="{{ info['url'] }}" title="{{ info['title'] }}">
|
||||||
@ -115,11 +117,11 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro item(info) %}
|
{% macro item(info, include_author=true) %}
|
||||||
{% if info['item_size'] == 'small' %}
|
{% if info['item_size'] == 'small' %}
|
||||||
{{ small_item(info) }}
|
{{ small_item(info, include_author) }}
|
||||||
{% elif info['item_size'] == 'medium' %}
|
{% elif info['item_size'] == 'medium' %}
|
||||||
{{ medium_item(info) }}
|
{{ medium_item(info, include_author) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
Error: Unknown item size
|
Error: Unknown item size
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -36,19 +36,11 @@ import json
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_plain_text(node):
|
def get_plain_text(node):
|
||||||
try:
|
try:
|
||||||
return html.escape(node['simpleText'])
|
return node['simpleText']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return unformmated_text_runs(node['runs'])
|
return ''.join(text_run['text'] for text_run in node['runs'])
|
||||||
|
|
||||||
def unformmated_text_runs(runs):
|
|
||||||
result = ''
|
|
||||||
for text_run in runs:
|
|
||||||
result += html.escape(text_run["text"])
|
|
||||||
return result
|
|
||||||
|
|
||||||
def format_text_runs(runs):
|
def format_text_runs(runs):
|
||||||
if isinstance(runs, str):
|
if isinstance(runs, str):
|
||||||
@ -78,6 +70,8 @@ def get_url(node):
|
|||||||
|
|
||||||
|
|
||||||
def get_text(node):
|
def get_text(node):
|
||||||
|
if node == {}:
|
||||||
|
return ''
|
||||||
try:
|
try:
|
||||||
return node['simpleText']
|
return node['simpleText']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -86,6 +80,9 @@ def get_text(node):
|
|||||||
return node['runs'][0]['text']
|
return node['runs'][0]['text']
|
||||||
except IndexError: # empty text runs
|
except IndexError: # empty text runs
|
||||||
return ''
|
return ''
|
||||||
|
except KeyError:
|
||||||
|
print(node)
|
||||||
|
raise
|
||||||
|
|
||||||
def get_formatted_text(node):
|
def get_formatted_text(node):
|
||||||
try:
|
try:
|
||||||
@ -200,7 +197,7 @@ def renderer_info(renderer, additional_info={}):
|
|||||||
|
|
||||||
info.update(additional_info)
|
info.update(additional_info)
|
||||||
|
|
||||||
if type.startswith('compact') or type.startswith('playlist') or type.startswith('grid'):
|
if type.startswith('compact') or type.startswith('playlist'):
|
||||||
info['item_size'] = 'small'
|
info['item_size'] = 'small'
|
||||||
else:
|
else:
|
||||||
info['item_size'] = 'medium'
|
info['item_size'] = 'medium'
|
||||||
@ -271,13 +268,8 @@ def renderer_info(renderer, additional_info={}):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def parse_info_prepare_for_html(renderer, additional_info={}):
|
||||||
#print(renderer)
|
item = renderer_info(renderer, additional_info)
|
||||||
#raise NotImplementedError('Unknown renderer type: ' + type)
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def parse_info_prepare_for_html(renderer):
|
|
||||||
item = renderer_info(renderer)
|
|
||||||
prefix_urls(item)
|
prefix_urls(item)
|
||||||
add_extra_html_info(item)
|
add_extra_html_info(item)
|
||||||
|
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>$page_title</title>
|
|
||||||
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
|
||||||
<link href="/youtube.com/favicon.ico" type="image/x-icon" rel="icon">
|
|
||||||
<link title="Youtube local" href="/youtube.com/opensearch.xml" rel="search" type="application/opensearchdescription+xml">
|
|
||||||
<style type="text/css">
|
|
||||||
main{
|
|
||||||
display:grid;
|
|
||||||
grid-template-rows: 0fr 0fr 1fr;
|
|
||||||
grid-template-columns: 0fr 1fr;
|
|
||||||
}
|
|
||||||
main .avatar{
|
|
||||||
grid-row:1;
|
|
||||||
grid-column:1;
|
|
||||||
height:200px;
|
|
||||||
width:200px;
|
|
||||||
}
|
|
||||||
main .title{
|
|
||||||
grid-row:1;
|
|
||||||
grid-column:2;
|
|
||||||
}
|
|
||||||
main .channel-tabs{
|
|
||||||
grid-row:2;
|
|
||||||
grid-column: 1 / span 2;
|
|
||||||
|
|
||||||
display:grid;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
justify-content:start;
|
|
||||||
|
|
||||||
background-color: #aaaaaa;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
main .channel-info{
|
|
||||||
grid-row: 3;
|
|
||||||
grid-column: 1 / span 3;
|
|
||||||
}
|
|
||||||
.tab{
|
|
||||||
padding: 5px 75px;
|
|
||||||
}
|
|
||||||
.description{
|
|
||||||
white-space: pre-wrap;
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
$header
|
|
||||||
<main>
|
|
||||||
<img class="avatar" src="$avatar">
|
|
||||||
<h2 class="title">$channel_title</h2>
|
|
||||||
<nav class="channel-tabs">
|
|
||||||
$channel_tabs
|
|
||||||
</nav>
|
|
||||||
<div class="channel-info">
|
|
||||||
<ul>
|
|
||||||
$stats
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
<hr>
|
|
||||||
<h3>Description</h3>
|
|
||||||
<span class="description">$description</span>
|
|
||||||
<hr>
|
|
||||||
<ul>
|
|
||||||
$links
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,90 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>$page_title</title>
|
|
||||||
<link href="/youtube.com/shared.css" type="text/css" rel="stylesheet">
|
|
||||||
<link href="/youtube.com/favicon.ico" type="image/x-icon" rel="icon">
|
|
||||||
<link title="Youtube local" href="/youtube.com/opensearch.xml" rel="search" type="application/opensearchdescription+xml">
|
|
||||||
<style type="text/css">
|
|
||||||
main{
|
|
||||||
display:grid;
|
|
||||||
grid-template-rows: repeat(5, 0fr);
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
}
|
|
||||||
main .avatar{
|
|
||||||
grid-row:1;
|
|
||||||
grid-column:1;
|
|
||||||
height:200px;
|
|
||||||
width:200px;
|
|
||||||
}
|
|
||||||
main .title{
|
|
||||||
grid-row:1;
|
|
||||||
grid-column:2;
|
|
||||||
}
|
|
||||||
main .channel-tabs{
|
|
||||||
grid-row:2;
|
|
||||||
grid-column: 1 / span 2;
|
|
||||||
|
|
||||||
display:grid;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
justify-content:start;
|
|
||||||
|
|
||||||
background-color: #aaaaaa;
|
|
||||||
padding: 3px;
|
|
||||||
}
|
|
||||||
#links-metadata{
|
|
||||||
display: grid;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
grid-column-gap: 10px;
|
|
||||||
grid-column: 1/span 2;
|
|
||||||
justify-content: start;
|
|
||||||
padding-top: 8px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
background-color: #bababa;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
#number-of-results{
|
|
||||||
font-weight:bold;
|
|
||||||
}
|
|
||||||
.item-grid{
|
|
||||||
grid-row:4;
|
|
||||||
grid-column: 1 / span 2;
|
|
||||||
}
|
|
||||||
.item-list{
|
|
||||||
width:1000px;
|
|
||||||
grid-column: 1 / span 2;
|
|
||||||
}
|
|
||||||
.page-button-row{
|
|
||||||
grid-column: 1 / span 2;
|
|
||||||
}
|
|
||||||
.tab{
|
|
||||||
padding: 5px 75px;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
$header
|
|
||||||
<main>
|
|
||||||
<img class="avatar" src="$avatar">
|
|
||||||
<h2 class="title">$channel_title</h2>
|
|
||||||
<nav class="channel-tabs">
|
|
||||||
$channel_tabs
|
|
||||||
</nav>
|
|
||||||
<div id="links-metadata">
|
|
||||||
<div id="number-of-results">$number_of_results</div>
|
|
||||||
$sort_buttons
|
|
||||||
</div>
|
|
||||||
$items
|
|
||||||
<nav class="page-button-row">
|
|
||||||
$page_buttons
|
|
||||||
</nav>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
x
Reference in New Issue
Block a user