aboutsummaryrefslogtreecommitdiffstats
path: root/youtube/templates
diff options
context:
space:
mode:
Diffstat (limited to 'youtube/templates')
-rw-r--r--youtube/templates/base.html80
-rw-r--r--youtube/templates/channel.html39
-rw-r--r--youtube/templates/comments.html8
-rw-r--r--youtube/templates/comments_page.html2
-rw-r--r--youtube/templates/common_elements.html43
-rw-r--r--youtube/templates/embed.html97
-rw-r--r--youtube/templates/error.html26
-rw-r--r--youtube/templates/home.html2
-rw-r--r--youtube/templates/licenses.html50
-rw-r--r--youtube/templates/local_playlist.html28
-rw-r--r--youtube/templates/playlist.html12
-rw-r--r--youtube/templates/search.html6
-rw-r--r--youtube/templates/settings.html26
-rw-r--r--youtube/templates/shared.css5
-rw-r--r--youtube/templates/subscription_manager.html25
-rw-r--r--youtube/templates/subscriptions.html4
-rw-r--r--youtube/templates/subscriptions.xml9
-rw-r--r--youtube/templates/unsubscribe_verify.html8
-rw-r--r--youtube/templates/watch.html366
19 files changed, 527 insertions, 309 deletions
diff --git a/youtube/templates/base.html b/youtube/templates/base.html
index 7b32d76..a67e745 100644
--- a/youtube/templates/base.html
+++ b/youtube/templates/base.html
@@ -1,77 +1,97 @@
+{% if settings.app_public %}
+ {% set app_url = settings.app_url|string %}
+{% else %}
+ {% set app_url = settings.app_url|string + ':' + settings.port_number|string %}
+{% endif %}
<!DOCTYPE html>
<html lang="en">
<head>
- <meta charset="UTF-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1"/>
- <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' https://*.googlevideo.com; {{ "img-src 'self' https://*.googleusercontent.com https://*.ggpht.com https://*.ytimg.com;" if not settings.proxy_images else "" }}"/>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval' blob:; media-src 'self' blob: {{ app_url }}/* data: https://*.googlevideo.com; img-src 'self' https://*.googleusercontent.com https://*.ggpht.com https://*.ytimg.com; connect-src 'self' https://*.googlevideo.com; font-src 'self' data:; worker-src 'self' blob:;">
<title>{{ page_title }}</title>
- <link title="Youtube local" href="/youtube.com/opensearch.xml" rel="search" type="application/opensearchdescription+xml"/>
- <link href="/youtube.com/static/favicon.ico" type="image/x-icon" rel="icon"/>
- <link href="/youtube.com/static/normalize.css" rel="stylesheet"/>
- <link href="{{ theme_path }}" rel="stylesheet"/>
+ <link title="YT Local" href="/youtube.com/opensearch.xml" rel="search" type="application/opensearchdescription+xml">
+ <link href="/youtube.com/static/favicon.ico" type="image/x-icon" rel="icon">
+ <link href="/youtube.com/static/normalize.css" rel="stylesheet">
+ <link href="{{ theme_path }}" rel="stylesheet">
+ <link href="/youtube.com/shared.css" rel="stylesheet">
{% block style %}
{{ style }}
{% endblock %}
+
+ {% if js_data %}
+ <script>
+ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+ data = {{ js_data|tojson }};
+ // @license-end
+ </script>
+ {% endif %}
+ <script>
+ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+ // Image prefix for thumbnails
+ let settings_img_prefix = "{{ settings.img_prefix or '' }}";
+ // @license-end
+ </script>
</head>
<body>
<header class="header">
<nav class="home">
- <a href="/youtube.com" id="home-link">YouTube Local</a>
+ <a href="/youtube.com" id="home-link">YT Local</a>
</nav>
- <form class="form" id="site-search" action="/youtube.com/search">
- <input type="search" name="query" class="search-box" value="{{ search_box_value }}"
- {{ "autofocus" if request.path == "/" else "" }} placeholder="Type to search...">
- <button type="submit" value="Search" class="search-button">Search</button>
+ <form class="form" id="site-search" action="/youtube.com/results">
+ <input type="search" name="search_query" class="search-box" value="{{ search_box_value }}"
+ {{ "autofocus" if (request.path in ("/", "/results") or error_message) else "" }} required placeholder="{{ _('Type to search...') }}">
+ <button type="submit" value="Search" class="search-button">{{ _('Search') }}</button>
<!-- options -->
<div class="dropdown">
<!-- hidden box -->
- <input id="options-toggle-cbox" class="opt-box" role="button" type="checkbox">
+ <input id="options-toggle-cbox" class="opt-box" type="checkbox">
<!-- end hidden box -->
- <label class="dropdown-label" for="options-toggle-cbox">Options</label>
+ <label class="dropdown-label" for="options-toggle-cbox">{{ _('Options') }}</label>
<div class="dropdown-content">
- <h3>Sort by</h3>
+ <h3>{{ _('Sort by') }}</h3>
<div class="option">
<input type="radio" id="sort_relevance" name="sort" value="0">
- <label for="sort_relevance">Relevance</label>
+ <label for="sort_relevance">{{ _('Relevance') }}</label>
</div>
<div class="option">
<input type="radio" id="sort_upload_date" name="sort" value="2">
- <label for="sort_upload_date">Upload date</label>
+ <label for="sort_upload_date">{{ _('Upload date') }}</label>
</div>
<div class="option">
<input type="radio" id="sort_view_count" name="sort" value="3">
- <label for="sort_view_count">View count</label>
+ <label for="sort_view_count">{{ _('View count') }}</label>
</div>
<div class="option">
<input type="radio" id="sort_rating" name="sort" value="1">
- <label for="sort_rating">Rating</label>
+ <label for="sort_rating">{{ _('Rating') }}</label>
</div>
- <h3>Upload date</h3>
+ <h3>{{ _('Upload date') }}</h3>
<div class="option">
<input type="radio" id="time_any" name="time" value="0">
- <label for="time_any">Any</label>
+ <label for="time_any">{{ _('Any') }}</label>
</div>
<div class="option">
<input type="radio" id="time_last_hour" name="time" value="1">
- <label for="time_last_hour">Last hour</label>
+ <label for="time_last_hour">{{ _('Last hour') }}</label>
</div>
<div class="option">
<input type="radio" id="time_today" name="time" value="2">
- <label for="time_today">Today</label>
+ <label for="time_today">{{ _('Today') }}</label>
</div>
<div class="option">
<input type="radio" id="time_this_week" name="time" value="3">
- <label for="time_this_week">This week</label>
+ <label for="time_this_week">{{ _('This week') }}</label>
</div>
<div class="option">
<input type="radio" id="time_this_month" name="time" value="4">
- <label for="time_this_month">This month</label>
+ <label for="time_this_month">{{ _('This month') }}</label>
</div>
<div class="option">
<input type="radio" id="time_this_year" name="time" value="5">
- <label for="time_this_year">This year</label>
+ <label for="time_this_year">{{ _('This year') }}</label>
</div>
<h3>Type</h3>
@@ -119,7 +139,7 @@
{% if header_playlist_names is defined %}
<form class="playlist" id="playlist-edit" action="/youtube.com/edit_playlist" method="post" target="_self">
- <input class="play-box" name="playlist_name" id="playlist-name-selection" list="playlist-options" type="search" placeholder="I added your playlist...">
+ <input class="play-box" name="playlist_name" id="playlist-name-selection" list="playlist-options" type="search" placeholder="Add name of your playlist...">
<datalist class="play-hidden" id="playlist-options">
{% for playlist_name in header_playlist_names %}
<option value="{{ playlist_name }}">{{ playlist_name }}</option>
@@ -127,7 +147,7 @@
</datalist>
<button class="play-add" type="submit" id="playlist-add-button" name="action" value="add">+List</button>
<div class="play-clean">
- <button type="reset" id="item-selection-reset">Clear selection</button>
+ <button type="reset" id="item-selection-reset">Clear</button>
</div>
</form>
<script src="/youtube.com/static/js/playlistadd.js"></script>
@@ -150,8 +170,8 @@
</div>
<div>
<p>This site is Free/Libre Software</p>
- {% if current_commit and current_version %}
- <p>Current version: {{ current_version }}-{{ current_commit }} @ {{ current_branch }}</p>
+ {% if current_commit != None %}
+ <p>Current version: {{ current_commit }} @ {{ current_branch }}</p>
{% else %}
<p>Current version: {{ current_version }}</p>
{% endif %}
diff --git a/youtube/templates/channel.html b/youtube/templates/channel.html
index 294f1df..274b727 100644
--- a/youtube/templates/channel.html
+++ b/youtube/templates/channel.html
@@ -1,21 +1,21 @@
{% if current_tab == 'search' %}
{% set page_title = search_box_value + ' - Page ' + page_number|string %}
{% else %}
- {% set page_title = channel_name + ' - Channel' %}
+ {% set page_title = channel_name|string + ' - Channel' %}
{% endif %}
{% extends "base.html" %}
{% import "common_elements.html" as common_elements %}
{% block style %}
- <link href="/youtube.com/static/message_box.css" rel="stylesheet"/>
- <link href="/youtube.com/static/channel.css" rel="stylesheet"/>
+ <link href="/youtube.com/static/message_box.css" rel="stylesheet">
+ <link href="/youtube.com/static/channel.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<div class="author-container">
<div class="author">
- <img alt="{{ channel_name }}" src="{{ avatar }}"/>
+ <img alt="{{ channel_name }}" src="{{ avatar }}">
<h2>{{ channel_name }}</h2>
</div>
<div class="summary">
@@ -33,7 +33,7 @@
<hr/>
<nav class="channel-tabs">
- {% for tab_name in ('Videos', 'Playlists', 'About') %}
+ {% for tab_name in ('Videos', 'Shorts', 'Streams', 'Playlists', 'About') %}
{% if tab_name.lower() == current_tab %}
<a class="tab page-button">{{ tab_name }}</a>
{% else %}
@@ -51,8 +51,11 @@
<ul>
{% for (before_text, stat, after_text) in [
('Joined ', date_joined, ''),
- ('', view_count|commatize, ' views'),
+ ('', approx_view_count, ' views'),
('', approx_subscriber_count, ' subscribers'),
+ ('', approx_video_count, ' videos'),
+ ('Country: ', country, ''),
+ ('Canonical Url: ', canonical_url, ''),
] %}
{% if stat %}
<li>{{ before_text + stat|string + after_text }}</li>
@@ -65,7 +68,11 @@
<hr>
<ul>
{% for text, url in links %}
- <li><a href="{{ url }}">{{ text }}</a></li>
+ {% if url %}
+ <li><a href="{{ url }}">{{ text }}</a></li>
+ {% else %}
+ <li>{{ text }}</li>
+ {% endif %}
{% endfor %}
</ul>
</div>
@@ -73,11 +80,15 @@
<!-- new-->
<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>
+ {% if current_tab in ('videos', 'shorts', 'streams') %}
+ {% set sorts = [('3', 'newest'), ('4', 'newest - no shorts')] %}
+ {% if current_tab in ('shorts', 'streams') and not is_last_page %}
+ <div id="number-of-results">{{ number_of_videos }}+ videos</div>
+ {% else %}
+ <div id="number-of-results">{{ number_of_videos }} videos</div>
+ {% endif %}
{% elif current_tab == 'playlists' %}
- {% set sorts = [('2', 'oldest'), ('3', 'newest'), ('4', 'last video added')] %}
+ {% set sorts = [('3', 'newest'), ('4', 'last video added')] %}
{% if items %}
<h2 class="page-number">Page {{ page_number }}</h2>
{% else %}
@@ -110,13 +121,13 @@
<hr/>
<footer class="pagination-container">
- {% if current_tab == 'videos' and current_sort.__str__() == '2' %}
+ {% if current_tab in ('shorts', 'streams') %}
<nav class="next-previous-button-row">
- {{ common_elements.next_previous_ctoken_buttons(None, ctoken, channel_url + '/' + current_tab, parameters_dictionary) }}
+ {{ common_elements.next_previous_buttons(is_last_page, channel_url + '/' + current_tab, parameters_dictionary) }}
</nav>
{% elif current_tab == 'videos' %}
<nav class="pagination-list">
- {{ common_elements.page_buttons(number_of_pages, channel_url + '/' + current_tab, parameters_dictionary, include_ends=(current_sort.__str__() == '3')) }}
+ {{ common_elements.page_buttons(number_of_pages, channel_url + '/' + current_tab, parameters_dictionary, include_ends=(current_sort.__str__() in '34')) }}
</nav>
{% elif current_tab == 'playlists' or current_tab == 'search' %}
<nav class="next-previous-button-row">
diff --git a/youtube/templates/comments.html b/youtube/templates/comments.html
index 7d0ef85..dac0d23 100644
--- a/youtube/templates/comments.html
+++ b/youtube/templates/comments.html
@@ -3,13 +3,13 @@
{% macro render_comment(comment, include_avatar, timestamp_links=False) %}
<div class="comment-container">
<div class="comment">
- <a class="author-avatar" href="{{ comment['author_url'] }}" title="{{ comment['author'] }}">
+ <a class="author-avatar" href="{{ comment['author_url'] or '#' }}" title="{{ comment['author'] }}">
{% if include_avatar %}
<img class="author-avatar-img" alt="{{ comment['author'] }}" src="{{ comment['author_avatar'] }}">
{% endif %}
</a>
<address class="author-name">
- <a class="author" href="{{ comment['author_url'] }}" title="{{ comment['author'] }}">{{ comment['author'] }}</a>
+ <a class="author" href="{{ comment['author_url'] or '#' }}" title="{{ comment['author'] }}">{{ comment['author'] }}</a>
</address>
<a class="permalink" href="{{ comment['permalink'] }}" title="permalink">
<span>{{ comment['time_published'] }}</span>
@@ -21,7 +21,7 @@
<span class="comment-text">{{ common_elements.text_runs(comment['text']) }}</span>
{% endif %}
- <span class="comment-likes">{{ comment['likes_text'] if comment['like_count'] else ''}}</span>
+ <span class="comment-likes">{{ comment['likes_text'] if comment['approx_like_count'] else ''}}</span>
<div class="button-row">
{% if comment['reply_count'] %}
{% if settings.use_comments_js and comment['replies_url'] %}
@@ -58,7 +58,7 @@
{% endfor %}
</div>
{% if 'more_comments_url' is in comments_info %}
- <a class="page-button more-comments" href="{{ comments_info['more_comments_url'] }}">More comments</a>
+ <a class="page-button more-comments" href="{{ comments_info['more_comments_url'] }}">{{ _('More comments') }}</a>
{% endif %}
{% endif %}
diff --git a/youtube/templates/comments_page.html b/youtube/templates/comments_page.html
index 09230f5..3764b10 100644
--- a/youtube/templates/comments_page.html
+++ b/youtube/templates/comments_page.html
@@ -1,4 +1,4 @@
-{% set page_title = ('Replies' if comments_info['is_replies'] else 'Comments page ' + comments_info['page_number']) %}
+{% set page_title = ('Replies' if comments_info['is_replies'] else 'Comments page ' + comments_info['page_number']|string) %}
{% import "comments.html" as comments with context %}
{% if not slim %}
diff --git a/youtube/templates/common_elements.html b/youtube/templates/common_elements.html
index 94554d4..1b4b08c 100644
--- a/youtube/templates/common_elements.html
+++ b/youtube/templates/common_elements.html
@@ -20,14 +20,14 @@
{{ info['error'] }}
{% else %}
<div class="item-video {{ info['type'] + '-item' }}">
- <a class="thumbnail-box" href="{{ info['url'] }}" title="{{ info['title'] }}">
+ <a class="thumbnail-box" href="{{ info['url'] or '#' }}" title="{{ info['title'] }}">
<div class="thumbnail {% if info['type'] == 'channel' %} channel {% endif %}">
{% if lazy_load %}
- <img class="thumbnail-img lazy" alt="&#x20;" data-src="{{ info['thumbnail'] }}">
+ <img class="thumbnail-img lazy" alt="&#x20;" data-src="{{ info['thumbnail'] }}" onerror="thumbnail_fallback(this)">
{% elif info['type'] == 'channel' %}
- <img class="thumbnail-img channel" alt="&#x20;" src="{{ info['thumbnail'] }}">
+ <img class="thumbnail-img channel" alt="&#x20;" src="{{ info['thumbnail'] }}" onerror="thumbnail_fallback(this)">
{% else %}
- <img class="thumbnail-img" alt="&#x20;" src="{{ info['thumbnail'] }}">
+ <img class="thumbnail-img" alt="&#x20;" src="{{ info['thumbnail'] }}" onerror="thumbnail_fallback(this)">
{% endif %}
{% if info['type'] != 'channel' %}
@@ -35,19 +35,32 @@
{% endif %}
</div>
</a>
- <h4 class="title"><a href="{{ info['url'] }}" title="{{ info['title'] }}">{{ info['title'] }}</a></h4>
+ <h4 class="title"><a href="{{ info['url'] or '#' }}" title="{{ info['title'] }}">{{ info['title'] }}</a></h4>
{% if include_author %}
+ {% set author_description = info['author'] %}
+ {% set AUTHOR_DESC_LENGTH = 35 %}
+ {% if author_description != None %}
+ {% if author_description|length >= AUTHOR_DESC_LENGTH %}
+ {% set author_description = author_description[:AUTHOR_DESC_LENGTH].split(' ')[:-1]|join(' ') %}
+ {% if not author_description[-1] in ['.', '?', ':', '!'] %}
+ {% set author_more = author_description + '…' %}
+ {% set author_description = author_more|replace('"','') %}
+ {% endif %}
+ {% endif %}
+ {% endif %}
{% if info.get('author_url') %}
- <address title="{{ info['author'] }}"><b><a href="{{ info['author_url'] }}">{{ info['author'] }}</a></b></address>
+ <address title="{{ info['author'] }}"><b><a href="{{ info['author_url'] }}">{{ author_description }}</a></b></address>
{% else %}
- <address title="{{ info['author'] }}"><b>{{ info['author'] }}</b></address>
+ <address title="{{ info['author'] }}"><b>{{ author_description }}</b></address>
{% endif %}
{% endif %}
<div class="stats {{'horizontal-stats' if horizontal else 'vertical-stats'}}">
{% if info['type'] == 'channel' %}
- <div>{{ info['approx_subscriber_count'] }} subscribers</div>
+ {% if info.get('approx_subscriber_count') %}
+ <div>{{ info['approx_subscriber_count'] }} subscribers</div>
+ {% endif %}
<div>{{ info['video_count']|commatize }} videos</div>
{% else %}
{% if info.get('time_published') %}
@@ -104,17 +117,18 @@
{% set parameters_dictionary = parameters_dictionary.to_dict() %}
{% if current_page != 1 %}
- {% set _ = parameters_dictionary.__setitem__('page', current_page - 1) %}
- <a class="page-link previous-page" href="{{ url + '?' + parameters_dictionary|urlencode }}">Previous page</a>
+ {% set _ = parameters_dictionary.__setitem__('page', current_page - 1) %}
+ <a class="page-link previous-page" href="{{ url + '?' + parameters_dictionary|urlencode }}">Previous page</a>
{% endif %}
{% if not is_last_page %}
- {% set _ = parameters_dictionary.__setitem__('page', current_page + 1) %}
- <a class="page-link next-page" href="{{ url + '?' + parameters_dictionary|urlencode }}">Next page</a>
+ {% set _ = parameters_dictionary.__setitem__('page', current_page + 1) %}
+ <a class="page-link next-page" href="{{ url + '?' + parameters_dictionary|urlencode }}">Next page</a>
{% endif %}
+{% endmacro %}
- {% macro next_previous_ctoken_buttons(prev_ctoken, next_ctoken, url, parameters_dictionary) %}
- {% set parameters_dictionary = parameters_dictionary.to_dict() %}
+{% macro next_previous_ctoken_buttons(prev_ctoken, next_ctoken, url, parameters_dictionary) %}
+ {% set parameters_dictionary = parameters_dictionary.to_dict() %}
{% if prev_ctoken %}
{% set _ = parameters_dictionary.__setitem__('ctoken', prev_ctoken) %}
@@ -125,5 +139,4 @@
{% set _ = parameters_dictionary.__setitem__('ctoken', next_ctoken) %}
<a class="page-link next-page" href="{{ url + '?' + parameters_dictionary|urlencode }}">Next page</a>
{% endif %}
- {% endmacro %}
{% endmacro %}
diff --git a/youtube/templates/embed.html b/youtube/templates/embed.html
index 728791b..b1ad63f 100644
--- a/youtube/templates/embed.html
+++ b/youtube/templates/embed.html
@@ -1,11 +1,16 @@
<!DOCTYPE html>
-<html lang="es">
+<html lang="en">
<head>
- <meta charset="UTF-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1"/>
- <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; media-src 'self' https://*.googlevideo.com; {{ "img-src 'self' https://*.googleusercontent.com https://*.ggpht.com https://*.ytimg.com;" if not settings.proxy_images else "" }}"/>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' blob: https://*.googlevideo.com; img-src 'self' https://*.googleusercontent.com https://*.ggpht.com https://*.ytimg.com; connect-src 'self' https://*.googlevideo.com; font-src 'self' data:;">
<title>{{ title }}</title>
- <link href="/youtube.com/static/favicon.ico" type="image/x-icon" rel="icon"/>
+ <link href="/youtube.com/static/favicon.ico" type="image/x-icon" rel="icon">
+ {% if settings.use_video_player == 2 %}
+ <!-- plyr -->
+ <link href="/youtube.com/static/modules/plyr/plyr.css" rel="stylesheet">
+ <!-- /plyr -->
+ {% endif %}
<style>
body {
margin: 0rem;
@@ -15,14 +20,23 @@
width: 100%;
height: auto;
}
+ /* Prevent this div from blocking right-click menu for video
+ e.g. Firefox playback speed options */
+ .plyr__poster {
+ display: none !important;
+ }
</style>
+ {% if js_data %}
+ <script>
+ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+ data = {{ js_data|tojson }};
+ // @license-end
+ </script>
+ {% endif %}
</head>
<body>
- <video controls autofocus onmouseleave="{{ title }}"
+ <video id="js-video-player" controls autofocus onmouseleave="{{ title }}"
oncontextmenu="{{ title }}" onmouseenter="{{ title }}" title="{{ title }}">
- {% for video_source in video_sources %}
- <source src="{{ video_source['src'] }}" type="{{ video_source['type'] }}">
- {% endfor %}
{% for source in subtitle_sources %}
{% if source['on'] %}
<track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}" default>
@@ -30,6 +44,71 @@
<track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}">
{% endif %}
{% endfor %}
+
+ {% if uni_sources %}
+ {% for source in uni_sources %}
+ <source src="{{ source['url'] }}" type="{{ source['type'] }}" title="{{ source['quality_string'] }}">
+ {% endfor %}
+ {% endif %}
</video>
+
+ <script>
+ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+ let storyboard_url = {{ storyboard_url | tojson }};
+ let hls_manifest_url = {{ hls_manifest_url | tojson }};
+ let hls_unavailable = {{ hls_unavailable | tojson }};
+ let playback_mode = {{ playback_mode | tojson }};
+ let pair_sources = {{ pair_sources | tojson }};
+ let pair_idx = {{ pair_idx | tojson }};
+ // @license-end
+ </script>
+
+ {% set hls_should_work = (playback_mode == 'hls' or playback_mode == 'auto') and not hls_unavailable %}
+ {% set use_dash = not hls_should_work %}
+
+ {% if not use_dash %}
+ <script src="/youtube.com/static/js/hls.min.js"
+ integrity="sha512-CSVqc4a7tn+tizDNt+eDoVn2fXYAwMDpCLrwGlWrOktNfZQ9gp4dKKScElMeRlrIifhliXs0a06BLaUgmMlCUw=="
+ crossorigin="anonymous"></script>
+ {% endif %}
+
+ <script src="/youtube.com/static/js/common.js"></script>
+
+ {% if settings.use_video_player == 0 %}
+ <!-- Native player -->
+ {% if use_dash %}
+ <script src="/youtube.com/static/js/watch.dash.js"></script>
+ {% else %}
+ <script src="/youtube.com/static/js/watch.hls.js"></script>
+ {% endif %}
+ {% elif settings.use_video_player == 1 %}
+ <!-- Native player with hotkeys -->
+ <script src="/youtube.com/static/js/hotkeys.js"></script>
+ {% if use_dash %}
+ <script src="/youtube.com/static/js/watch.dash.js"></script>
+ {% else %}
+ <script src="/youtube.com/static/js/watch.hls.js"></script>
+ {% endif %}
+ {% elif settings.use_video_player == 2 %}
+ <!-- plyr -->
+ <script src="/youtube.com/static/modules/plyr/plyr.min.js"
+ integrity="sha512-l6ZzdXpfMHRfifqaR79wbYCEWjLDMI9DnROvb+oLkKq6d7MGroGpMbI7HFpicvmAH/2aQO+vJhewq8rhysrImw=="
+ crossorigin="anonymous"></script>
+ {% if use_dash %}
+ <script src="/youtube.com/static/js/plyr.dash.start.js"></script>
+ {% else %}
+ <script src="/youtube.com/static/js/plyr.hls.start.js"></script>
+ {% endif %}
+ <!-- /plyr -->
+ {% endif %}
+
+ {% if use_dash %}
+ <script src="/youtube.com/static/js/av-merge.js"></script>
+ {% endif %}
+
+ <!-- Storyboard Preview Thumbnails (native players only; Plyr handles this internally) -->
+ {% if settings.use_video_player != 2 and settings.native_player_storyboard %}
+ <script src="/youtube.com/static/js/storyboard-preview.js"></script>
+ {% endif %}
</body>
</html>
diff --git a/youtube/templates/error.html b/youtube/templates/error.html
index 7256aee..97f8ca9 100644
--- a/youtube/templates/error.html
+++ b/youtube/templates/error.html
@@ -1,22 +1,36 @@
-{% set page_title = 'Error' %}
+{% if error_code %}
+ {% set page_title = 'Error: ' ~ error_code %}
+{% else %}
+ {% set page_title = 'Error' %}
+{% endif %}
{% if not slim %}
{% extends "base.html" %}
{% endif %}
-{% block style %}
- <link href="/youtube.com/static/home.css" rel="stylesheet">
-{% endblock style %}
+{% if traceback %}
+ {% block style %}
+ <link href="/youtube.com/static/home.css" rel="stylesheet">
+ {% endblock style %}
+{% endif %}
{% block main %}
{% if traceback %}
<div class="code-error" id="error-box">
<h1>500 Uncaught exception:</h1>
<div class="code-box"><code>{{ traceback }}</code></div>
- <p>Please report this issue at <a href="https://libregit.org/heckyel/yt-local/issues" target="_blank" rel="noopener noreferrer">https://libregit.org/heckyel/yt-local/issues</a></p>
+ <p>Please report this issue at <a href="https://todo.sr.ht/~heckyel/yt-local" target="_blank" rel="noopener noreferrer">https://todo.sr.ht/~heckyel/yt-local</a></p>
<p>Remember to include the traceback in your issue and redact any information in it you do not want to share</p>
</div>
{% else %}
- <div id="error-message">{{ error_message }}</div>
+ <section id="error-message" class="comments-area">
+ <div class="comments">
+ <div class="comment-container">
+ <div class="comment">
+ <span class="comment-text">{{ error_message }}</span>
+ </div>
+ </div>
+ </div>
+ </section>
{% endif %}
{% endblock %}
diff --git a/youtube/templates/home.html b/youtube/templates/home.html
index ef1f029..0adac56 100644
--- a/youtube/templates/home.html
+++ b/youtube/templates/home.html
@@ -1,7 +1,7 @@
{% set page_title = title %}
{% extends "base.html" %}
{% block style %}
- <link href="/youtube.com/static/home.css" rel="stylesheet"/>
+ <link href="/youtube.com/static/home.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<ul>
diff --git a/youtube/templates/licenses.html b/youtube/templates/licenses.html
index dc883a8..e2af1bf 100644
--- a/youtube/templates/licenses.html
+++ b/youtube/templates/licenses.html
@@ -1,7 +1,7 @@
{% set page_title = title %}
{% extends "base.html" %}
{% block style %}
- <link href="/youtube.com/static/license.css" rel="stylesheet"/>
+ <link href="/youtube.com/static/license.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<table id="jslicense-labels1" class="table">
@@ -15,6 +15,11 @@
</thead>
<tbody>
<tr>
+ <td data-label="File"><a href="/youtube.com/static/js/av-merge.js">av-merge.js</a></td>
+ <td data-label="License"><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0 or later</a></td>
+ <td data-label="Source"><a href="/youtube.com/static/js/av-merge.js">av-merge.js</a></td>
+ </tr>
+ <tr>
<td data-label="File"><a href="/youtube.com/static/js/comments.js">comments.js</a></td>
<td data-label="License"><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0 or later</a></td>
<td data-label="Source"><a href="/youtube.com/static/js/comments.js">comments.js</a></td>
@@ -25,6 +30,11 @@
<td data-label="Source"><a href="/youtube.com/static/js/common.js">common.js</a></td>
</tr>
<tr>
+ <td data-label="File"><a href="/youtube.com/static/js/hls.min.js">hls.min.js</a></td>
+ <td data-label="License"><a href="https://spdx.org/licenses/BSD-3-Clause.html">BSD-3-Clause</a></td>
+ <td data-label="Source"><a href="https://github.com/video-dev/hls.js/tree/v1.6.15/src">hls.js v1.6.15 source</a></td>
+ </tr>
+ <tr>
<td data-label="File"><a href="/youtube.com/static/js/hotkeys.js">hotkeys.js</a></td>
<td data-label="License"><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0 or later</a></td>
<td data-label="Source"><a href="/youtube.com/static/js/hotkeys.js">hotkeys.js</a></td>
@@ -35,15 +45,45 @@
<td data-label="Source"><a href="/youtube.com/static/js/playlistadd.js">playlistadd.js</a></td>
</tr>
<tr>
- <td data-label="File"><a href="/youtube.com/static/js/speedyplay.js">speedyplay.js</a></td>
+ <td data-label="File"><a href="/youtube.com/static/js/plyr.dash.start.js">plyr.dash.start.js</a></td>
+ <td data-label="License"><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0 or later</a></td>
+ <td data-label="Source"><a href="/youtube.com/static/js/plyr.dash.start.js">plyr.dash.start.js</a></td>
+ </tr>
+ <tr>
+ <td data-label="File"><a href="/youtube.com/static/js/plyr.hls.start.js">plyr.hls.start.js</a></td>
+ <td data-label="License"><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0 or later</a></td>
+ <td data-label="Source"><a href="/youtube.com/static/js/plyr.hls.start.js">plyr.hls.start.js</a></td>
+ </tr>
+ <tr>
+ <td data-label="File"><a href="/youtube.com/static/js/sponsorblock.js">sponsorblock.js</a></td>
+ <td data-label="License"><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0 or later</a></td>
+ <td data-label="Source"><a href="/youtube.com/static/js/sponsorblock.js">sponsorblock.js</a></td>
+ </tr>
+ <tr>
+ <td data-label="File"><a href="/youtube.com/static/js/storyboard-preview.js">storyboard-preview.js</a></td>
<td data-label="License"><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0 or later</a></td>
- <td data-label="Source"><a href="/youtube.com/static/js/speedyplay.js">speedyplay.js</a></td>
+ <td data-label="Source"><a href="/youtube.com/static/js/storyboard-preview.js">storyboard-preview.js</a></td>
+ </tr>
+ <tr>
+ <td data-label="File"><a href="/youtube.com/static/modules/plyr/plyr.min.js">plyr.min.js</a></td>
+ <td data-label="License"><a href="https://spdx.org/licenses/MIT.html">Expat</a></td>
+ <td data-label="Source"><a href="/youtube.com/static/modules/plyr/plyr.js">plyr.js</a></td>
</tr>
- <tr>
+ <tr>
<td data-label="File"><a href="/youtube.com/static/js/transcript-table.js">transcript-table.js</a></td>
<td data-label="License"><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0 or later</a></td>
<td data-label="Source"><a href="/youtube.com/static/js/transcript-table.js">transcript-table.js</a></td>
- </tr>
+ </tr>
+ <tr>
+ <td data-label="File"><a href="/youtube.com/static/js/watch.dash.js">watch.dash.js</a></td>
+ <td data-label="License"><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0 or later</a></td>
+ <td data-label="Source"><a href="/youtube.com/static/js/watch.dash.js">watch.dash.js</a></td>
+ </tr>
+ <tr>
+ <td data-label="File"><a href="/youtube.com/static/js/watch.hls.js">watch.hls.js</a></td>
+ <td data-label="License"><a href="http://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0 or later</a></td>
+ <td data-label="Source"><a href="/youtube.com/static/js/watch.hls.js">watch.hls.js</a></td>
+ </tr>
</tbody>
</table>
{% endblock main %}
diff --git a/youtube/templates/local_playlist.html b/youtube/templates/local_playlist.html
index 765ac66..3286f67 100644
--- a/youtube/templates/local_playlist.html
+++ b/youtube/templates/local_playlist.html
@@ -2,13 +2,29 @@
{% extends "base.html" %}
{% import "common_elements.html" as common_elements %}
{% block style %}
- <link href="/youtube.com/static/message_box.css" rel="stylesheet"/>
- <link href="/youtube.com/static/local_playlist.css" rel="stylesheet"/>
+ <link href="/youtube.com/static/message_box.css" rel="stylesheet">
+ <link href="/youtube.com/static/local_playlist.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<div class="playlist-metadata">
<h2 class="play-title">{{ playlist_name }}</h2>
+
+ <div id="export-options">
+ <form id="playlist-export" method="post">
+ <select id="export-type" name="export_format">
+ <option value="json">JSON</option>
+ <option value="ids">Video id list (txt)</option>
+ <option value="urls">Video url list (txt)</option>
+ </select>
+ <button type="submit" id="playlist-export-button" name="action" value="export">Export</button>
+ </form>
+ </div>
+ </div>
+
+ <form id="playlist-remove" action="/youtube.com/edit_playlist" method="post" target="_self"></form>
+ <div class="playlist-metadata" id="video-remove-container">
+ <button id="removePlayList" type="submit" name="action" value="remove_playlist" form="playlist-remove" formaction="">Remove playlist</button>
<input type="hidden" name="playlist_page" value="{{ playlist_name }}" form="playlist-edit">
<button class="play-action" type="submit" id="playlist-remove-button" name="action" value="remove" form="playlist-edit" formaction="">Remove from playlist</button>
</div>
@@ -17,6 +33,14 @@
{{ common_elements.item(video_info) }}
{% endfor %}
</div>
+ <script>
+ // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
+ const deletePlayList = document.getElementById('removePlayList');
+ deletePlayList.addEventListener('click', (event) => {
+ return confirm('You are about to permanently delete {{ playlist_name }}\n\nOnce a playlist is permanently deleted, it cannot be recovered.')
+ });
+ // @license-end
+ </script>
<footer class="pagination-container">
<nav class="pagination-list">
{{ common_elements.page_buttons(num_pages, '/https://www.youtube.com/playlists/' + playlist_name, parameters_dictionary) }}
diff --git a/youtube/templates/playlist.html b/youtube/templates/playlist.html
index c154207..dc9c37a 100644
--- a/youtube/templates/playlist.html
+++ b/youtube/templates/playlist.html
@@ -2,19 +2,25 @@
{% extends "base.html" %}
{% import "common_elements.html" as common_elements %}
{% block style %}
- <link href="/youtube.com/static/message_box.css" rel="stylesheet"/>
- <link href="/youtube.com/static/playlist.css" rel="stylesheet"/>
+ <link href="/youtube.com/static/message_box.css" rel="stylesheet">
+ <link href="/youtube.com/static/playlist.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<div class="playlist-metadata">
<div class="author">
- <img alt="{{ title }}" src="{{ thumbnail }}"/>
+ {% if thumbnail %}
+ <img alt="{{ title }}" src="{{ thumbnail }}">
+ {% endif %}
<h2>{{ title }}</h2>
</div>
<div class="summary">
+ {% if author_url %}
<a class="playlist-author" href="{{ author_url }}">{{ author }}</a>
+ {% else %}
+ <span class="playlist-author">{{ author }}</span>
+ {% endif %}
</div>
<div class="playlist-stats">
<div>{{ video_count|commatize }} videos</div>
diff --git a/youtube/templates/search.html b/youtube/templates/search.html
index 4238d70..af87c90 100644
--- a/youtube/templates/search.html
+++ b/youtube/templates/search.html
@@ -3,8 +3,8 @@
{% extends "base.html" %}
{% import "common_elements.html" as common_elements %}
{% block style %}
- <link href="/youtube.com/static/message_box.css" rel="stylesheet"/>
- <link href="/youtube.com/static/search.css" rel="stylesheet"/>
+ <link href="/youtube.com/static/message_box.css" rel="stylesheet">
+ <link href="/youtube.com/static/search.css" rel="stylesheet">
{% endblock style %}
{% block main %}
@@ -28,7 +28,7 @@
<!-- /video item -->
<footer class="pagination-container">
<nav class="pagination-list">
- {{ common_elements.page_buttons(estimated_pages, '/https://www.youtube.com/search', parameters_dictionary) }}
+ {{ common_elements.page_buttons(estimated_pages, '/https://www.youtube.com/results', parameters_dictionary) }}
</nav>
</footer>
{% endblock main %}
diff --git a/youtube/templates/settings.html b/youtube/templates/settings.html
index 7a7ce9e..940eeae 100644
--- a/youtube/templates/settings.html
+++ b/youtube/templates/settings.html
@@ -1,21 +1,21 @@
{% set page_title = 'Settings' %}
{% extends "base.html" %}
{% block style %}
- <link href="/youtube.com/static/settings.css" rel="stylesheet"/>
+ <link href="/youtube.com/static/settings.css" rel="stylesheet">
{% endblock style %}
{% block main %}
<form method="POST" class="settings-form">
{% for categ in categories %}
- <h2>{{ categ|capitalize }}</h2>
+ <h2>{{ _(categ|capitalize) }}</h2>
<ul class="settings-list">
{% for setting_name, setting_info, value in settings_by_category[categ] %}
{% if not setting_info.get('hidden', false) %}
<li class="setting-item">
{% if 'label' is in(setting_info) %}
- <label for="{{ 'setting_' + setting_name }}">{{ setting_info['label'] }}</label>
+ <label for="{{ 'setting_' + setting_name }}" {% if 'comment' is in(setting_info) %}title="{{ setting_info['comment'] }}" {% endif %}>{{ _(setting_info['label']) }}</label>
{% else %}
- <label for="{{ 'setting_' + setting_name }}">{{ setting_name.replace('_', ' ')|capitalize }}</label>
+ <label for="{{ 'setting_' + setting_name }}" {% if 'comment' is in(setting_info) %}title="{{ setting_info['comment'] }}" {% endif %}>{{ _(setting_name.replace('_', ' ')|capitalize) }}</label>
{% endif %}
{% if setting_info['type'].__name__ == 'bool' %}
@@ -24,24 +24,32 @@
{% if 'options' is in(setting_info) %}
<select id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}">
{% for option in setting_info['options'] %}
- <option value="{{ option[0] }}" {{ 'selected' if option[0] == value else '' }}>{{ option[1] }}</option>
+ <option value="{{ option[0] }}" {{ 'selected' if option[0] == value else '' }}>{{ _(option[1]) }}</option>
{% endfor %}
</select>
{% else %}
<input type="number" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" value="{{ value }}" step="1">
{% endif %}
{% elif setting_info['type'].__name__ == 'float' %}
-
+ <input type="number" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" value="{{ value }}" step="0.01">
{% elif setting_info['type'].__name__ == 'str' %}
- <input type="text" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" value="{{ value }}">
+ {% if 'options' is in(setting_info) %}
+ <select id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}">
+ {% for option in setting_info['options'] %}
+ <option value="{{ option[0] }}" {{ 'selected' if option[0] == value else '' }}>{{ _(option[1]) }}</option>
+ {% endfor %}
+ </select>
+ {% else %}
+ <input type="text" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" value="{{ value }}">
+ {% endif %}
{% else %}
- <span>Error: Unknown setting type: setting_info['type'].__name__</span>
+ <span>Error: Unknown setting type: {{ setting_info['type'].__name__ }}</span>
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
{% endfor %}
- <input type="submit" value="Save settings">
+ <input type="submit" value="{{ _('Save settings') }}">
</form>
{% endblock main %}
diff --git a/youtube/templates/shared.css b/youtube/templates/shared.css
new file mode 100644
index 0000000..8f12651
--- /dev/null
+++ b/youtube/templates/shared.css
@@ -0,0 +1,5 @@
+html {
+ font-family: {{ font_family }};
+ background: var(--background);
+ color: var(--text);
+}
diff --git a/youtube/templates/subscription_manager.html b/youtube/templates/subscription_manager.html
index 92cd024..96082c3 100644
--- a/youtube/templates/subscription_manager.html
+++ b/youtube/templates/subscription_manager.html
@@ -1,7 +1,7 @@
{% set page_title = 'Subscription Manager' %}
{% extends "base.html" %}
{% block style %}
- <link href="/youtube.com/static/subscription_manager.css" rel="stylesheet"/>
+ <link href="/youtube.com/static/subscription_manager.css" rel="stylesheet">
{% endblock style %}
@@ -19,14 +19,25 @@
<div class="import-export">
<form class="subscriptions-import-form" enctype="multipart/form-data" action="/youtube.com/import_subscriptions" method="POST">
<h2>Import subscriptions</h2>
- <input type="file" id="subscriptions-import" accept="application/json, application/xml, text/x-opml" name="subscriptions_file">
- <input type="submit" value="Import" class="import-submit-button">
+ <div class="subscriptions-import-options">
+ <input type="file" id="subscriptions-import" accept="application/json, application/xml, text/x-opml, text/csv" name="subscriptions_file" required>
+ <input type="submit" value="Import">
+ </div>
</form>
- <!--<ul class="subscriptions-export-links">
- <li><a href="/youtube.com/subscriptions.opml">Export subscriptions (OPML)</a></li>
- <li><a href="/youtube.com/subscriptions.xml">Export subscriptions (RSS)</a></li>
- </ul>-->
+ <form class="subscriptions-export-form" action="/youtube.com/export_subscriptions" method="POST">
+ <h2>Export subscriptions</h2>
+ <div class="subscriptions-export-options">
+ <select id="export-type" name="export_format" title="Export format">
+ <option value="json_newpipe">JSON (NewPipe)</option>
+ <option value="json_google_takeout">JSON (Old Google Takeout Format)</option>
+ <option value="opml">OPML (RSS, no tags)</option>
+ </select>
+ <label for="include-muted">Include muted</label>
+ <input id="include-muted" type="checkbox" name="include_muted" checked>
+ <input type="submit" value="Export">
+ </div>
+ </form>
</div>
<hr>
diff --git a/youtube/templates/subscriptions.html b/youtube/templates/subscriptions.html
index b528e5c..2823e8d 100644
--- a/youtube/templates/subscriptions.html
+++ b/youtube/templates/subscriptions.html
@@ -7,8 +7,8 @@
{% import "common_elements.html" as common_elements %}
{% block style %}
- <link href="/youtube.com/static/message_box.css" rel="stylesheet"/>
- <link href="/youtube.com/static/subscription.css" rel="stylesheet"/>
+ <link href="/youtube.com/static/message_box.css" rel="stylesheet">
+ <link href="/youtube.com/static/subscription.css" rel="stylesheet">
{% endblock style %}
{% block main %}
diff --git a/youtube/templates/subscriptions.xml b/youtube/templates/subscriptions.xml
new file mode 100644
index 0000000..5365da1
--- /dev/null
+++ b/youtube/templates/subscriptions.xml
@@ -0,0 +1,9 @@
+<opml version="1.1">
+ <body>
+ <outline text="YouTube Subscriptions" title="YouTube Subscriptions">
+ {% for sub in sub_list %}
+ <outline text="{{sub['channel_name']}}" title="{{sub['channel_name']}}" type="rss" xmlUrl="https://www.youtube.com/feeds/videos.xml?channel_id={{sub['channel_id']}}" />
+ {%- endfor %}
+ </outline>
+ </body>
+</opml>
diff --git a/youtube/templates/unsubscribe_verify.html b/youtube/templates/unsubscribe_verify.html
index 98581c0..e899783 100644
--- a/youtube/templates/unsubscribe_verify.html
+++ b/youtube/templates/unsubscribe_verify.html
@@ -1,17 +1,19 @@
{% set page_title = 'Unsubscribe?' %}
{% extends "base.html" %}
+{% block style %}
+ <link href="/youtube.com/static/unsubscribe.css" rel="stylesheet"/>
+{% endblock style %}
{% block main %}
- <span>Are you sure you want to unsubscribe from these channels?</span>
+ <p>Are you sure you want to unsubscribe from these channels?</p>
<form class="subscriptions-import-form" action="/youtube.com/subscription_manager" method="POST">
{% for channel_id, channel_name in unsubscribe_list %}
<input type="hidden" name="channel_ids" value="{{ channel_id }}">
{% endfor %}
-
<input type="hidden" name="action" value="unsubscribe">
<input type="submit" value="Yes, unsubscribe">
</form>
- <ul>
+ <ul class="list-channel">
{% for channel_id, channel_name in unsubscribe_list %}
<li><a href="{{ '/https://www.youtube.com/channel/' + channel_id }}" title="{{ channel_name }}">{{ channel_name }}</a></li>
{% endfor %}
diff --git a/youtube/templates/watch.html b/youtube/templates/watch.html
index 7184872..6b4f48f 100644
--- a/youtube/templates/watch.html
+++ b/youtube/templates/watch.html
@@ -3,8 +3,14 @@
{% import "common_elements.html" as common_elements %}
{% import "comments.html" as comments with context %}
{% block style %}
- <link href="/youtube.com/static/message_box.css" rel="stylesheet"/>
- <link href="/youtube.com/static/watch.css" rel="stylesheet"/>
+ <link href="/youtube.com/static/message_box.css" rel="stylesheet">
+ <link href="/youtube.com/static/watch.css" rel="stylesheet">
+ {% if settings.use_video_player == 2 %}
+ <!-- plyr -->
+ <link href="/youtube.com/static/modules/plyr/plyr.css" rel="stylesheet">
+ <link href="/youtube.com/static/modules/plyr/custom_plyr.css" rel="stylesheet">
+ <!-- /plyr -->
+ {% endif %}
{% endblock style %}
{% block main %}
@@ -17,22 +23,9 @@
{% endif %}
</span>
</div>
- {% elif (video_sources.__len__() == 0 or live) and hls_formats.__len__() != 0 %}
- <div class="live-url-choices">
- <span>Copy a url into your video player:</span>
- <ol>
- {% for fmt in hls_formats %}
- <li class="url-choice"><div class="url-choice-label">{{ fmt['video_quality'] }}: </div><input class="url-choice-copy" value="{{ fmt['url'] }}" readonly onclick="this.select();"></li>
- {% endfor %}
- </ol>
- </div>
{% else %}
<figure class="sc-video">
- <video id="js-video-player" playsinline controls>
- {% for video_source in video_sources %}
- <source src="{{ video_source['src'] }}" type="{{ video_source['type'] }}">
- {% endfor %}
-
+ <video id="js-video-player" playsinline controls {{ 'autoplay' if settings.autoplay_videos }}>
{% for source in subtitle_sources %}
{% if source['on'] %}
<track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}" default>
@@ -40,14 +33,19 @@
<track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}">
{% endif %}
{% endfor %}
+
+ {% if uni_sources %}
+ {% for source in uni_sources %}
+ <source src="{{ source['url'] }}" type="{{ source['type'] }}" title="{{ source['quality_string'] }}">
+ {% endfor %}
+ {% endif %}
</video>
+ {% if hls_unavailable and not uni_sources %}
+ <div class="playability-error">
+ <span>Error: HLS streams unavailable. Video may not play without JavaScript fallback.</span>
+ </div>
+ {% endif %}
</figure>
-
- {% if time_start != 0 %}
- <script>
- document.getElementById('js-video-player').currentTime = {{ time_start|tojson }};
- </script>
- {% endif %}
{% endif %}
<div class="sc-info">
@@ -72,34 +70,73 @@
<address class="v-uploaded">Uploaded by <a href="{{ uploader_channel_url }}">{{ uploader }}</a></address>
<span class="v-views">{{ view_count }} views</span>
<time class="v-published" datetime="{{ time_published_utc }}">Published on {{ time_published }}</time>
- <span class="v-likes-dislikes">{{ like_count }} likes {{ dislike_count }} dislikes</span>
+ <span class="v-likes-dislikes">{{ like_count }} likes</span>
<div class="external-player-controls">
<input class="speed" id="speed-control" type="text" title="Video speed">
- <script src="/youtube.com/static/js/speedyplay.js"></script>
+ {% if settings.use_video_player < 2 %}
+ <!-- Quality selector (populated by JS: HLS adds Auto+levels, DASH adds discrete sources) -->
+ <select id="quality-select" autocomplete="off">
+ </select>
+ {% else %}
+ <select id="quality-select" autocomplete="off" style="display: none;">
+ </select>
+ {% endif %}
+ {% if settings.use_video_player != 2 %}
+ {% if audio_tracks|length > 1 %}
+ <select id="audio-track-select" autocomplete="off">
+ {% for track in audio_tracks %}
+ <option value="{{ track['id'] }}" {{ 'selected' if track['is_default'] else '' }}>{{ track['name'] }}</option>
+ {% endfor %}
+ </select>
+ {% endif %}
+ {% endif %}
</div>
-
<input class="v-checkbox" name="video_info_list" value="{{ video_info }}" form="playlist-edit" type="checkbox">
+ <span class="v-direct-link"><a href="https://youtu.be/{{ video_id }}" rel="noopener noreferrer" target="_blank">{{ _('Direct Link') }}</a></span>
+
+ {% if settings.use_video_download != 0 %}
<details class="v-download">
- <summary class="download-dropdown-label">Download</summary>
- <ul class="download-dropdown-content">
- {% for format in download_formats %}
- <li class="download-format">
- <a class="download-link" href="{{ format['url'] }}" download="{{ title }}.{{ format['ext'] }}">
- {{ format['ext'] }} {{ format['video_quality'] }} {{ format['audio_quality'] }} {{ format['file_size'] }} {{ format['codecs'] }}
- </a>
- </li>
- {% endfor %}
- {% for download in other_downloads %}
- <li class="download-format">
- <a href="{{ download['url'] }}" download>
- {{ download['ext'] }} {{ download['label'] }}
- </a>
- </li>
- {% endfor %}
- </ul>
+ <summary class="download-dropdown-label">{{ _('Download') }}</summary>
+ <div class="download-table-container">
+ <table class="download-table" aria-label="Download formats">
+ <thead>
+ <tr>
+ <th scope="col">{{ _('Ext') }}</th>
+ <th scope="col">{{ _('Video') }}</th>
+ <th scope="col">{{ _('Audio') }}</th>
+ <th scope="col">{{ _('Size') }}</th>
+ <th scope="col">{{ _('Codecs') }}</th>
+ <th scope="col">{{ _('Link') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for format in download_formats %}
+ <tr>
+ <td data-label="{{ _('Ext') }}">{{ format['ext'] }}</td>
+ <td data-label="{{ _('Video') }}">{{ format['video_quality'] }}</td>
+ <td data-label="{{ _('Audio') }}">{{ format['audio_quality'] }}</td>
+ <td data-label="{{ _('Size') }}">{{ format['file_size'] }}</td>
+ <td data-label="{{ _('Codecs') }}">{{ format['codecs'] }}</td>
+ <td data-label="{{ _('Link') }}"><a class="download-link" href="{{ format['url'] }}" download="{{ title }}.{{ format['ext'] }}" aria-label="{{ _('Download') }} {{ format['ext'] }} {{ format['video_quality'] }} {{ format['audio_quality'] }}">{{ _('Download') }}</a></td>
+ </tr>
+ {% endfor %}
+ {% for download in other_downloads %}
+ <tr>
+ <td data-label="{{ _('Ext') }}">{{ download['ext'] }}</td>
+ <td data-label="{{ _('Video') }}" colspan="3">{{ download['label'] }}</td>
+ <td data-label="{{ _('Codecs') }}">{{ download.get('codecs', 'N/A') }}</td>
+ <td data-label="{{ _('Link') }}"><a class="download-link" href="{{ download['url'] }}" download aria-label="{{ _('Download') }} {{ download['label'] }}">{{ _('Download') }}</a></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
</details>
+ {% else %}
+ <span class="v-download"></span>
+ {% endif %}
<span class="v-description">{{ common_elements.text_runs(description)|escape|urlize|timestamps|safe }}</span>
<div class="v-music-list">
@@ -115,7 +152,11 @@
{% for track in music_list %}
<tr>
{% for attribute in music_attributes %}
- <td>{{ track.get(attribute.lower(), '') }}</td>
+ {% if attribute.lower() == 'title' and track['url'] is not none %}
+ <td><a href="{{ track['url'] }}">{{ track.get(attribute.lower(), '') }}</a></td>
+ {% else %}
+ <td>{{ track.get(attribute.lower(), '') }}</td>
+ {% endif %}
{% endfor %}
</tr>
{% endfor %}
@@ -123,7 +164,7 @@
{% endif %}
</div>
<details class="v-more-info">
- <summary>More info</summary>
+ <summary>{{ _('More info') }}</summary>
<div class="more-info-content">
<p>Tor exit node: {{ ip_address }}</p>
{% if invidious_used %}
@@ -143,154 +184,39 @@
<!-- playlist -->
{% if playlist %}
- <div class="site-playlist">
- <div class="playlist-header">
- <a href="{{ playlist['url'] }}" title="{{ playlist['title'] }}"><h3>{{ playlist['title'] }}</h3></a>
- <ul class="playlist-metadata">
- <li>Autoplay: <input type="checkbox" id="autoplay-toggle"></li>
- {% if playlist['current_index'] is none %}
- <li>[Error!]/{{ playlist['video_count'] }}</li>
- {% else %}
- <li>{{ playlist['current_index']+1 }}/{{ playlist['video_count'] }}</li>
- {% endif %}
- <li><a href="{{ playlist['author_url'] }}" title="{{ playlist['author'] }}">{{ playlist['author'] }}</a></li>
- </ul>
+ <div class="site-playlist">
+ <div class="playlist-header">
+ <a href="{{ playlist['url'] }}" title="{{ playlist['title'] }}"><h3>{{ playlist['title'] }}</h3></a>
+ <ul class="playlist-metadata">
+ <li><label for="playlist-autoplay-toggle">{{ _('AutoNext') }}: </label><input id="playlist-autoplay-toggle" type="checkbox" class="autoplay-toggle"></li>
+ {% if playlist['current_index'] is none %}
+ <li>[Error!]/{{ playlist['video_count'] }}</li>
+ {% else %}
+ <li>{{ playlist['current_index']+1 }}/{{ playlist['video_count'] }}</li>
+ {% endif %}
+ {% if playlist['author_url'] %}
+ <li><a href="{{ playlist['author_url'] }}" title="{{ playlist['author'] }}">{{ playlist['author'] }}</a></li>
+ {% elif playlist['author'] %}
+ <li>{{ playlist['author'] }}</li>
+ {% endif %}
+ </ul>
+ </div>
+ <nav class="playlist-videos">
+ {% for info in playlist['items'] %}
+ {# non-lazy load for 5 videos surrounding current video #}
+ {# for non-js browsers or old such that IntersectionObserver doesn't work #}
+ {# -10 is sentinel to not load anything if there's no current_index for some reason #}
+ {% if (playlist.get('current_index', -10) - loop.index0)|abs is lt(5) %}
+ {{ common_elements.item(info, include_badges=false, lazy_load=false) }}
+ {% else %}
+ {{ common_elements.item(info, include_badges=false, lazy_load=true) }}
+ {% endif %}
+ {% endfor %}
+ </nav>
</div>
- <nav class="playlist-videos">
- {% for info in playlist['items'] %}
- {# non-lazy load for 5 videos surrounding current video #}
- {# for non-js browsers or old such that IntersectionObserver doesn't work #}
- {# -10 is sentinel to not load anything if there's no current_index for some reason #}
- {% if (playlist.get('current_index', -10) - loop.index0)|abs is lt(5) %}
- {{ common_elements.item(info, include_badges=false, lazy_load=false) }}
- {% else %}
- {{ common_elements.item(info, include_badges=false, lazy_load=true) }}
- {% endif %}
- {% endfor %}
- </nav>
- {% if playlist['current_index'] is not none %}
- <script>
- // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
- (function main() {
- // from https://stackoverflow.com/a/6969486
- function escapeRegExp(string) {
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
- }
- let playability_error = {{ 'true' if playability_error else 'false' }};
- let playlist_id = {{ playlist['id']|tojson }};
- playlist_id = escapeRegExp(playlist_id);
-
- // read cookies on whether to autoplay thru playlist
- // pain in the ass:
- // https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie
- let cookieValue = document.cookie.replace(new RegExp(
- '(?:(?:^|.*;\\s*)autoplay_' + playlist_id + '\\s*\\=\\s*([^;]*).*$)|^.*$'), '$1');
- let autoplayEnabled = 0;
- if(cookieValue.length === 0){
- autoplayEnabled = 0;
- } else {
- autoplayEnabled = Number(cookieValue);
- }
-
- // check the checkbox if autoplay is on
- let checkbox = document.querySelector('#autoplay-toggle');
- if(autoplayEnabled){
- checkbox.checked = true;
- }
-
- // listen for checkbox to turn autoplay on and off
- checkbox.addEventListener( 'change', function() {
- if(this.checked) {
- autoplayEnabled = 1;
- document.cookie = 'autoplay_' + playlist_id + '=1';
- } else {
- autoplayEnabled = 0;
- document.cookie = 'autoplay_' + playlist_id + '=0';
- }
- });
-
- const vid = document.getElementById('js-video-player');
- if(!playability_error){
- if(autoplayEnabled){
- vid.play();
- }
- }
-
- let currentIndex = {{ playlist['current_index']|tojson }};
- {% if playlist['current_index']+1 == playlist['items']|length %}
- let nextVideoUrl = null;
- {% else %}
- let nextVideoUrl = {{ (playlist['items'][playlist['current_index']+1]['url'])|tojson }};
- {% endif %}
- let nextVideoDelay = 1000;
-
- // scroll playlist to proper position
- let pl = document.querySelector('.playlist-videos');
- // item height + gap == 100
- pl.scrollTop = 100*currentIndex;
-
- // go to next video when video ends
- // https://stackoverflow.com/a/2880950
- if(nextVideoUrl){
- if(playability_error){
- videoEnded();
- } else {
- vid.addEventListener('ended', videoEnded, false);
- }
- function nextVideo(){
- if(autoplayEnabled){
- window.location.href = nextVideoUrl;
- }
- }
- function videoEnded(e) {
- window.setTimeout(nextVideo, nextVideoDelay);
- }
- }
- }());
- // @license-end
- </script>
- {% endif %}
- {% if playlist['id'] is not none %}
- <script>
- // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
- (function main() {
- // lazy load playlist images
- // copied almost verbatim from
- // https://css-tricks.com/tips-for-rolling-your-own-lazy-loading/
- // IntersectionObserver isn't supported in pre-quantum
- // firefox versions, but the alternative of making it
- // manually is a performance drain, so oh well
- let observer = new IntersectionObserver(lazyLoad, {
- // where in relation to the edge of the viewport, we are observing
- rootMargin: "100px",
- // how much of the element needs to have intersected
- // in order to fire our loading function
- threshold: 1.0
- });
-
- function lazyLoad(elements) {
- elements.forEach(item => {
- if (item.intersectionRatio > 0) {
- // set the src attribute to trigger a load
- item.target.src = item.target.dataset.src;
- // stop observing this element. Our work here is done!
- observer.unobserve(item.target);
- };
- });
- };
-
- // Tell our observer to observe all img elements with a "lazy" class
- let lazyImages = document.querySelectorAll('img.lazy');
- lazyImages.forEach(img => {
- observer.observe(img);
- });
- }());
- // @license-end
- </script>
- {% endif %}
- </div>
+ {% elif settings.related_videos_mode != 0 %}
+ <div class="related-autoplay"><label for="related-autoplay-toggle">{{ _('AutoNext') }}: </label><input id="related-autoplay-toggle" type="checkbox" class="autoplay-toggle"></div>
{% endif %}
- <!-- /playlist -->
{% if subtitle_sources %}
<details id="transcript-details">
@@ -311,7 +237,7 @@
{% if settings.related_videos_mode != 0 %}
<details class="related-videos-outer" {{'open' if settings.related_videos_mode == 1 else ''}}>
- <summary>Related Videos</summary>
+ <summary>{{ _('Related Videos') }}</summary>
<nav class="related-videos-inner">
{% for info in related %}
{{ common_elements.item(info, include_badges=false) }}
@@ -325,10 +251,10 @@
<!-- comments -->
{% if settings.comments_mode != 0 %}
{% if comments_disabled %}
- <div class="comments-area-outer comments-disabled">Comments disabled</div>
+ <div class="comments-area-outer comments-disabled">{{ _('Comments disabled') }}</div>
{% else %}
<details class="comments-area-outer" {{'open' if settings.comments_mode == 1 else ''}}>
- <summary>{{ comment_count|commatize }} comment{{'s' if comment_count != 1 else ''}}</summary>
+ <summary>{{ comment_count|commatize }} {{ _('Comment') }}{{'s' if comment_count != '1' else ''}}</summary>
<div class="comments-area-inner comments-area">
{% if comments_info %}
{{ comments.video_comments(comments_info) }}
@@ -342,12 +268,62 @@
<script>
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
- data = {{ js_data|tojson }};
+ let storyboard_url = {{ storyboard_url | tojson }};
+ let hls_manifest_url = {{ hls_manifest_url | tojson }};
+ let hls_unavailable = {{ hls_unavailable | tojson }};
+ let playback_mode = {{ playback_mode | tojson }};
+ let pair_sources = {{ pair_sources | tojson }};
+ let pair_idx = {{ pair_idx | tojson }};
// @license-end
</script>
+
<script src="/youtube.com/static/js/common.js"></script>
<script src="/youtube.com/static/js/transcript-table.js"></script>
- {% if settings.use_video_hotkeys %} <script src="/youtube.com/static/js/hotkeys.js"></script> {% endif %}
+
+ {% set hls_should_work = (playback_mode == 'hls' or playback_mode == 'auto') and not hls_unavailable %}
+ {% set use_dash = not hls_should_work %}
+
+ {% if use_dash %}
+ <script src="/youtube.com/static/js/av-merge.js"></script>
+ {% else %}
+ <script src="/youtube.com/static/js/hls.min.js"
+ integrity="sha512-CSVqc4a7tn+tizDNt+eDoVn2fXYAwMDpCLrwGlWrOktNfZQ9gp4dKKScElMeRlrIifhliXs0a06BLaUgmMlCUw=="
+ crossorigin="anonymous"></script>
+ {% endif %}
+
+ {% if settings.use_video_player == 0 %}
+ <!-- Native player (no hotkeys) -->
+ {% if use_dash %}
+ <script src="/youtube.com/static/js/watch.dash.js"></script>
+ {% else %}
+ <script src="/youtube.com/static/js/watch.hls.js"></script>
+ {% endif %}
+ {% elif settings.use_video_player == 1 %}
+ <!-- Native player with hotkeys -->
+ <script src="/youtube.com/static/js/hotkeys.js"></script>
+ {% if use_dash %}
+ <script src="/youtube.com/static/js/watch.dash.js"></script>
+ {% else %}
+ <script src="/youtube.com/static/js/watch.hls.js"></script>
+ {% endif %}
+ {% elif settings.use_video_player == 2 %}
+ <!-- plyr -->
+ <script src="/youtube.com/static/modules/plyr/plyr.min.js"
+ integrity="sha512-l6ZzdXpfMHRfifqaR79wbYCEWjLDMI9DnROvb+oLkKq6d7MGroGpMbI7HFpicvmAH/2aQO+vJhewq8rhysrImw=="
+ crossorigin="anonymous"></script>
+ {% if use_dash %}
+ <script src="/youtube.com/static/js/plyr.dash.start.js"></script>
+ {% else %}
+ <script src="/youtube.com/static/js/plyr.hls.start.js"></script>
+ {% endif %}
+ <!-- /plyr -->
+ {% endif %}
+
+ <!-- Storyboard Preview Thumbnails (native players only; Plyr handles this internally) -->
+ {% if settings.use_video_player != 2 and settings.native_player_storyboard %}
+ <script src="/youtube.com/static/js/storyboard-preview.js"></script>
+ {% endif %}
+
{% if settings.use_comments_js %} <script src="/youtube.com/static/js/comments.js"></script> {% endif %}
{% if settings.use_sponsorblock_js %} <script src="/youtube.com/static/js/sponsorblock.js"></script> {% endif %}
{% endblock main %}