aboutsummaryrefslogtreecommitdiffstats
path: root/youtube/templates
diff options
context:
space:
mode:
authorLibravatarLibravatar Biswakalyan Bhuyan <biswa@surgot.in> 2024-09-19 15:33:11 +0530
committerLibravatarLibravatar Biswakalyan Bhuyan <biswa@surgot.in> 2024-09-19 15:33:11 +0530
commita4e01da27c08e43a67b2618ad1e71c1f8f86d5cd (patch)
tree5b8f407dbb7e9d1ab2106ac0cc8564897e7a2098 /youtube/templates
downloadyt-local-a4e01da27c08e43a67b2618ad1e71c1f8f86d5cd.tar.gz
yt-local-a4e01da27c08e43a67b2618ad1e71c1f8f86d5cd.tar.bz2
yt-local-a4e01da27c08e43a67b2618ad1e71c1f8f86d5cd.zip
youtube fronendHEADmaster
Diffstat (limited to 'youtube/templates')
-rw-r--r--youtube/templates/base.html179
-rw-r--r--youtube/templates/channel.html133
-rw-r--r--youtube/templates/comments.html65
-rw-r--r--youtube/templates/comments_page.html47
-rw-r--r--youtube/templates/common_elements.html140
-rw-r--r--youtube/templates/embed.html74
-rw-r--r--youtube/templates/error.html36
-rw-r--r--youtube/templates/home.html13
-rw-r--r--youtube/templates/licenses.html64
-rw-r--r--youtube/templates/local_playlist.html49
-rw-r--r--youtube/templates/local_playlists_list.html14
-rw-r--r--youtube/templates/playlist.html41
-rw-r--r--youtube/templates/search.html34
-rw-r--r--youtube/templates/settings.html47
-rw-r--r--youtube/templates/shared.css5
-rw-r--r--youtube/templates/status.html6
-rw-r--r--youtube/templates/subscription_manager.html78
-rw-r--r--youtube/templates/subscriptions.html83
-rw-r--r--youtube/templates/subscriptions.xml9
-rw-r--r--youtube/templates/unsubscribe_verify.html21
-rw-r--r--youtube/templates/watch.html263
21 files changed, 1401 insertions, 0 deletions
diff --git a/youtube/templates/base.html b/youtube/templates/base.html
new file mode 100644
index 0000000..393cc52
--- /dev/null
+++ b/youtube/templates/base.html
@@ -0,0 +1,179 @@
+{% 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' blob: {{ app_url }}/* data: https://*.googlevideo.com; {{ "img-src 'self' https://*.googleusercontent.com https://*.ggpht.com https://*.ytimg.com;" if not settings.proxy_images else "" }}">
+ <title>{{ page_title }}</title>
+ <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 %}
+ </head>
+
+ <body>
+ <header class="header">
+ <nav class="home">
+ <a href="/youtube.com" id="home-link">YT Local</a>
+ </nav>
+ <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" type="checkbox">
+ <!-- end hidden box -->
+ <label class="dropdown-label" for="options-toggle-cbox">Options</label>
+ <div class="dropdown-content">
+ <h3>Sort by</h3>
+ <div class="option">
+ <input type="radio" id="sort_relevance" name="sort" value="0">
+ <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>
+ </div>
+ <div class="option">
+ <input type="radio" id="sort_view_count" name="sort" value="3">
+ <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>
+ </div>
+
+ <h3>Upload date</h3>
+ <div class="option">
+ <input type="radio" id="time_any" name="time" value="0">
+ <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>
+ </div>
+ <div class="option">
+ <input type="radio" id="time_today" name="time" value="2">
+ <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>
+ </div>
+ <div class="option">
+ <input type="radio" id="time_this_month" name="time" value="4">
+ <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>
+ </div>
+
+ <h3>Type</h3>
+ <div class="option">
+ <input type="radio" id="type_any" name="type" value="0">
+ <label for="type_any">Any</label>
+ </div>
+ <div class="option">
+ <input type="radio" id="type_video" name="type" value="1">
+ <label for="type_video">Video</label>
+ </div>
+ <div class="option">
+ <input type="radio" id="type_channel" name="type" value="2">
+ <label for="type_channel">Channel</label>
+ </div>
+ <div class="option">
+ <input type="radio" id="type_playlist" name="type" value="3">
+ <label for="type_playlist">Playlist</label>
+ </div>
+ <div class="option">
+ <input type="radio" id="type_movie" name="type" value="4">
+ <label for="type_movie">Movie</label>
+ </div>
+ <div class="option">
+ <input type="radio" id="type_show" name="type" value="5">
+ <label for="type_show">Show</label>
+ </div>
+
+ <h3>Duration</h3>
+ <div class="option">
+ <input type="radio" id="duration_any" name="duration" value="0">
+ <label for="duration_any">Any</label>
+ </div>
+ <div class="option">
+ <input type="radio" id="duration_short" name="duration" value="1">
+ <label for="duration_short">Short (&lt; 4 minutes)</label>
+ </div>
+ <div class="option">
+ <input type="radio" id="duration_long" name="duration" value="2">
+ <label for="duration_long">Long (&gt; 20 minutes)</label>
+ </div>
+ </div>
+ </div>
+ </form>
+
+ {% 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="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>
+ {% endfor %}
+ </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</button>
+ </div>
+ </form>
+ <script src="/youtube.com/static/js/playlistadd.js"></script>
+ {% endif %}
+
+ </header>
+ <main class="main">
+
+ {% block main %}
+ {{ main }}
+ {% endblock %}
+
+ </main>
+ <footer class="footer">
+ <div>
+ <a href="https://git.sr.ht/~heckyel/yt-local"
+ rel="noopener noreferrer" target="_blank">
+ Released under the AGPLv3 or later
+ </a>
+ </div>
+ <div>
+ <p>This site is Free/Libre Software</p>
+ {% if current_commit != None %}
+ <p>Current version: {{ current_commit }} @ {{ current_branch }}</p>
+ {% else %}
+ <p>Current version: {{ current_version }}</p>
+ {% endif %}
+ </div>
+ <div>
+ <a href="/youtube.com/licenses" data-jslicense="1" rel="noopener noreferrer" target="_blank">JavaScript licenses</a>
+ </div>
+ </footer>
+ </body>
+
+</html>
diff --git a/youtube/templates/channel.html b/youtube/templates/channel.html
new file mode 100644
index 0000000..c43f488
--- /dev/null
+++ b/youtube/templates/channel.html
@@ -0,0 +1,133 @@
+{% if current_tab == 'search' %}
+ {% set page_title = search_box_value + ' - Page ' + page_number|string %}
+{% else %}
+ {% 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">
+{% endblock style %}
+
+{% block main %}
+
+ <div class="author-container">
+ <div class="author">
+ <img alt="{{ channel_name }}" src="{{ avatar }}">
+ <h2>{{ channel_name }}</h2>
+ </div>
+ <div class="summary">
+ <p>{{ short_description }}</p>
+ </div>
+ <div class="subscribe">
+ <form method="POST" action="/youtube.com/subscriptions" class="subscribe-unsubscribe">
+ <input class="btn-subscribe" type="submit" value="{{ 'Unsubscribe' if subscribed else 'Subscribe' }}">
+ <input type="hidden" name="channel_id" value="{{ channel_id }}">
+ <input type="hidden" name="channel_name" value="{{ channel_name }}">
+ <input type="hidden" name="action" value="{{ 'unsubscribe' if subscribed else 'subscribe' }}">
+ </form>
+ </div>
+ </div>
+ <hr/>
+
+ <nav class="channel-tabs">
+ {% for tab_name in ('Videos', 'Shorts', 'Streams', '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 (before_text, stat, after_text) in [
+ ('Joined ', date_joined, ''),
+ ('', 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>
+ {% endif %}
+ {% endfor %}
+ </ul>
+ <hr>
+ <h3>Description</h3>
+ <div class="description">{{ common_elements.text_runs(description) }}</div>
+ <hr>
+ <ul>
+ {% for text, url in links %}
+ {% if url %}
+ <li><a href="{{ url }}">{{ text }}</a></li>
+ {% else %}
+ <li>{{ text }}</li>
+ {% endif %}
+ {% endfor %}
+ </ul>
+ </div>
+ {% else %}
+
+ <!-- new-->
+ <div id="links-metadata">
+ {% if current_tab in ('videos', 'shorts', 'streams') %}
+ {% set sorts = [('1', 'views'), ('2', 'oldest'), ('3', 'newest'), ('4', 'newest - no shorts'),] %}
+ <div id="number-of-results">{{ number_of_videos }} videos</div>
+ {% elif current_tab == 'playlists' %}
+ {% set sorts = [('2', 'oldest'), ('3', 'newest'), ('4', 'last video added')] %}
+ {% if items %}
+ <h2 class="page-number">Page {{ page_number }}</h2>
+ {% else %}
+ <h2 class="page-number">No items</h2>
+ {% endif %}
+ {% elif current_tab == 'search' %}
+ {% if items %}
+ <h2 class="page-number">Page {{ page_number }}</h2>
+ {% else %}
+ <h2 class="page-number">No results</h2>
+ {% endif %}
+ {% 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>
+
+ <div class="video-container {{ current_tab + '-content'}}">
+ {% for item_info in items %}
+ {{ common_elements.item(item_info, include_author=false) }}
+ {% endfor %}
+ </div>
+ <hr/>
+
+ <footer class="pagination-container">
+ {% if current_tab in ('videos', 'shorts', 'streams') %}
+ <nav class="pagination-list">
+ {{ 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">
+ {{ common_elements.next_previous_buttons(is_last_page, channel_url + '/' + current_tab, parameters_dictionary) }}
+ </nav>
+ {% endif %}
+ </footer>
+ <!-- /new-->
+ {% endif %}
+
+{% endblock main %}
diff --git a/youtube/templates/comments.html b/youtube/templates/comments.html
new file mode 100644
index 0000000..7bd75e5
--- /dev/null
+++ b/youtube/templates/comments.html
@@ -0,0 +1,65 @@
+{% import "common_elements.html" as common_elements %}
+
+{% 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'] }}">
+ {% 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>
+ </address>
+ <a class="permalink" href="{{ comment['permalink'] }}" title="permalink">
+ <span>{{ comment['time_published'] }}</span>
+ </a>
+
+ {% if timestamp_links %}
+ <span class="comment-text">{{ common_elements.text_runs(comment['text'])|timestamps|safe }}</span>
+ {% else %}
+ <span class="comment-text">{{ common_elements.text_runs(comment['text']) }}</span>
+ {% endif %}
+
+ <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'] %}
+ <details class="replies" data-src="{{ comment['replies_url'] }}">
+ <summary>{{ comment['view_replies_text'] }}</summary>
+ <a href="{{ comment['replies_url'] }}" class="replies-open-new-tab" target="_blank">Open in new tab</a>
+ <div class="comment_page">loading...</div>
+ </details>
+ {% elif comment['replies_url'] %}
+ <a href="{{ comment['replies_url'] }}" class="replies">{{ comment['view_replies_text'] }}</a>
+ {% else %}
+ <a class="replies">{{ comment['view_replies_text'] }} (error constructing url)</a>
+ {% endif %}
+ {% endif %}
+ </div>
+ </div>
+ </div>
+{% endmacro %}
+
+{% macro video_comments(comments_info) %}
+ <div class="comment-links">
+ {% for link_text, link_url in comments_info['comment_links'] %}
+ <a class="sort-button" href="{{ link_url }}">{{ link_text }}</a>
+ {% endfor %}
+ </div>
+ {% if comments_info['error'] %}
+ <div class="comments">
+ <div class="code-box"><code>{{ comments_info['error'] }}</code></div>
+ </div>
+ {% else %}
+ <div class="comments">
+ {% for comment in comments_info['comments'] %}
+ {{ render_comment(comment, comments_info['include_avatars'], True) }}
+ {% 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>
+ {% endif %}
+ {% endif %}
+
+{% endmacro %}
diff --git a/youtube/templates/comments_page.html b/youtube/templates/comments_page.html
new file mode 100644
index 0000000..3764b10
--- /dev/null
+++ b/youtube/templates/comments_page.html
@@ -0,0 +1,47 @@
+{% 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 %}
+ {% extends "base.html" %}
+ {% block style %}
+ <link href="/youtube.com/static/comments.css" rel="stylesheet">
+ {% endblock style %}
+{% endif %}
+
+{% block main %}
+ <section class="comments-area">
+ {% if not comments_info['is_replies'] %}
+ <section class="video-metadata">
+ <a class="video-metadata-thumbnail-box" href="{{ comments_info['video_url'] }}" title="{{ comments_info['video_title'] }}">
+ <img class="video-metadata-thumbnail-img" src="{{ comments_info['video_thumbnail'] }}" height="180px" width="320px">
+ </a>
+ <a class="title" href="{{ comments_info['video_url'] }}" title="{{ comments_info['video_title'] }}">{{ comments_info['video_title'] }}</a>
+
+ <h2>Comments page {{ comments_info['page_number'] }}</h2>
+ <span>Sorted by {{ comments_info['sort_text'] }}</span>
+ </section>
+ {% endif %}
+
+ {% if not comments_info['is_replies'] %}
+ <div class="comment-links">
+ {% for link_text, link_url in comments_info['comment_links'] %}
+ <a class="sort-button" href="{{ link_url }}">{{ link_text }}</a>
+ {% endfor %}
+ </div>
+ {% endif %}
+
+ <div class="comments">
+ {% for comment in comments_info['comments'] %}
+ {{ comments.render_comment(comment, comments_info['include_avatars'], slim) }}
+ {% 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>
+ {% endif %}
+ </section>
+
+ {% if settings.use_comments_js %}
+ <script src="/youtube.com/static/js/common.js"></script>
+ <script src="/youtube.com/static/js/comments.js"></script>
+ {% endif %}
+{% endblock main %}
diff --git a/youtube/templates/common_elements.html b/youtube/templates/common_elements.html
new file mode 100644
index 0000000..bacc513
--- /dev/null
+++ b/youtube/templates/common_elements.html
@@ -0,0 +1,140 @@
+{% macro text_runs(runs) %}
+ {%- if runs[0] is mapping -%}
+ {%- for text_run in runs -%}
+ {%- if text_run.get("bold", false) -%}
+ <b>{{ text_run["text"] }}</b>
+ {%- elif text_run.get('italics', false) -%}
+ <i>{{ text_run["text"] }}</i>
+ {%- else -%}
+ {{ text_run["text"] }}
+ {%- endif -%}
+ {%- endfor -%}
+ {%- elif runs -%}
+ {{ runs }}
+ {%- endif -%}
+{% endmacro %}
+
+{% macro item(info, description=false, horizontal=true, include_author=true, include_badges=true, lazy_load=false) %}
+ <article class="item-box">
+ {% if info['error'] %}
+ {{ info['error'] }}
+ {% else %}
+ <div class="item-video {{ info['type'] + '-item' }}">
+ <a class="thumbnail-box" href="{{ info['url'] }}" 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'] }}">
+ {% elif info['type'] == 'channel' %}
+ <img class="thumbnail-img channel" alt="&#x20;" src="{{ info['thumbnail'] }}">
+ {% else %}
+ <img class="thumbnail-img" alt="&#x20;" src="{{ info['thumbnail'] }}">
+ {% endif %}
+
+ {% if info['type'] != 'channel' %}
+ <p class="length">{{ (info['video_count']|commatize + ' videos') if info['type'] == 'playlist' else info['duration'] }}</p>
+ {% endif %}
+ </div>
+ </a>
+ <h4 class="title"><a href="{{ info['url'] }}" 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'] }}">{{ author_description }}</a></b></address>
+ {% else %}
+ <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>
+ <div>{{ info['video_count']|commatize }} videos</div>
+ {% else %}
+ {% if info.get('time_published') %}
+ <span>{{ info['time_published'] }}</span>
+ {% endif %}
+ {% if info.get('approx_view_count') %}
+ <div class="views">{{ info['approx_view_count'] }} views</div>
+ {% endif %}
+ {% endif %}
+ </div>
+ </div>
+ {% if info['type'] == 'video' %}
+ <input class="item-checkbox" type="checkbox" name="video_info_list" value="{{ info['video_info'] }}" form="playlist-edit">
+ {% endif %}
+ {% endif %}
+ </article>
+{% endmacro %}
+
+{% macro page_buttons(estimated_pages, url, parameters_dictionary, include_ends=false) %}
+ {% set current_page = parameters_dictionary.get('page', 1)|int %}
+ {% set parameters_dictionary = parameters_dictionary.to_dict() %}
+ {% if current_page is le(5) %}
+ {% set page_start = 1 %}
+ {% set page_end = [9, estimated_pages]|min %}
+ {% else %}
+ {% set page_start = current_page - 4 %}
+ {% set page_end = [current_page + 4, estimated_pages]|min %}
+ {% endif %}
+
+ {% if include_ends and page_start is gt(1) %}
+ {% set _ = parameters_dictionary.__setitem__('page', 1) %}
+ <a class="page-link first-page-button" href="{{ url + '?' + parameters_dictionary|urlencode }}">{{ 1 }}</a>
+ {% endif %}
+
+ {% for page in range(page_start, page_end+1) %}
+ {% if page == current_page %}
+ <a class="page-link is-current">{{ page }}</a>
+ {% else %}
+ {# https://stackoverflow.com/questions/36886650/how-to-add-a-new-entry-into-a-dictionary-object-while-using-jinja2 #}
+ {% set _ = parameters_dictionary.__setitem__('page', page) %}
+ <a class="page-link" href="{{ url + '?' + parameters_dictionary|urlencode }}">{{ page }}</a>
+ {% endif %}
+ {% endfor %}
+
+ {% if include_ends and page_end is lt(estimated_pages) %}
+ {% set _ = parameters_dictionary.__setitem__('page', estimated_pages) %}
+ <a class="page-link last-page-button" href="{{ url + '?' + parameters_dictionary|urlencode }}">{{ estimated_pages }}</a>
+ {% endif %}
+
+{% endmacro %}
+
+{% macro next_previous_buttons(is_last_page, url, parameters_dictionary) %}
+ {% set current_page = parameters_dictionary.get('page', 1)|int %}
+ {% 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>
+ {% 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>
+ {% endif %}
+{% endmacro %}
+
+{% 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) %}
+ <a class="page-link previous-page" href="{{ url + '?' + parameters_dictionary|urlencode }}">Previous page</a>
+ {% endif %}
+
+ {% if next_ctoken %}
+ {% set _ = parameters_dictionary.__setitem__('ctoken', next_ctoken) %}
+ <a class="page-link next-page" href="{{ url + '?' + parameters_dictionary|urlencode }}">Next page</a>
+ {% endif %}
+{% endmacro %}
diff --git a/youtube/templates/embed.html b/youtube/templates/embed.html
new file mode 100644
index 0000000..85d2d78
--- /dev/null
+++ b/youtube/templates/embed.html
@@ -0,0 +1,74 @@
+<!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'; media-src 'self' https://*.googlevideo.com; {{ "img-src 'self' https://*.googleusercontent.com https://*.ggpht.com https://*.ytimg.com;" if not settings.proxy_images else "" }}">
+ <title>{{ title }}</title>
+ <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;
+ padding: 0rem;
+ }
+ video {
+ 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 id="js-video-player" controls autofocus onmouseleave="{{ title }}"
+ oncontextmenu="{{ title }}" onmouseenter="{{ title }}" title="{{ title }}">
+ {% if uni_sources %}
+ <source src="{{ uni_sources[uni_idx]['url'] }}" type="{{ uni_sources[uni_idx]['type'] }}" data-res="{{ uni_sources[uni_idx]['quality'] }}">
+ {% endif %}
+ {% for source in subtitle_sources %}
+ {% if source['on'] %}
+ <track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}" default>
+ {% else %}
+ <track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}">
+ {% endif %}
+ {% endfor %}
+ </video>
+ {% 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
+ let storyboard_url = {{ storyboard_url | tojson }};
+ // @license-end
+ </script>
+ {% if 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>
+ <script src="/youtube.com/static/js/plyr-start.js"></script>
+ <!-- /plyr -->
+ {% elif settings.use_video_player == 1 %}
+ <script src="/youtube.com/static/js/hotkeys.js"></script>
+ {% endif %}
+ </body>
+</html>
diff --git a/youtube/templates/error.html b/youtube/templates/error.html
new file mode 100644
index 0000000..97f8ca9
--- /dev/null
+++ b/youtube/templates/error.html
@@ -0,0 +1,36 @@
+{% if error_code %}
+ {% set page_title = 'Error: ' ~ error_code %}
+{% else %}
+ {% set page_title = 'Error' %}
+{% endif %}
+
+{% if not slim %}
+ {% extends "base.html" %}
+{% endif %}
+
+{% 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://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 %}
+ <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
new file mode 100644
index 0000000..0adac56
--- /dev/null
+++ b/youtube/templates/home.html
@@ -0,0 +1,13 @@
+{% set page_title = title %}
+{% extends "base.html" %}
+{% block style %}
+ <link href="/youtube.com/static/home.css" rel="stylesheet">
+{% endblock style %}
+{% block main %}
+ <ul>
+ <li><a href="/youtube.com/playlists">Local playlists</a></li>
+ <li><a href="/youtube.com/subscriptions">Subscriptions</a></li>
+ <li><a href="/youtube.com/subscription_manager">Subscription Manager</a></li>
+ <li><a href="/youtube.com/settings">Settings</a></li>
+ </ul>
+{% endblock main %}
diff --git a/youtube/templates/licenses.html b/youtube/templates/licenses.html
new file mode 100644
index 0000000..dc73bfb
--- /dev/null
+++ b/youtube/templates/licenses.html
@@ -0,0 +1,64 @@
+{% set page_title = title %}
+{% extends "base.html" %}
+{% block style %}
+ <link href="/youtube.com/static/license.css" rel="stylesheet">
+{% endblock style %}
+{% block main %}
+ <table id="jslicense-labels1" class="table">
+ <caption>JavaScript Licensing Table</caption>
+ <thead>
+ <tr>
+ <th>File</th>
+ <th>License</th>
+ <th>Source</th>
+ </tr>
+ </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>
+ </tr>
+ <tr>
+ <td data-label="File"><a href="/youtube.com/static/js/common.js">common.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/common.js">common.js</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>
+ </tr>
+ <tr>
+ <td data-label="File"><a href="/youtube.com/static/js/playlistadd.js">playlistadd.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/playlistadd.js">playlistadd.js</a></td>
+ </tr>
+ <tr>
+ <td data-label="File"><a href="/youtube.com/static/js/plyr-start.js">plyr-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-start.js">plyr-start.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>
+ <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>
+ <td data-label="File"><a href="/youtube.com/static/js/watch.js">watch.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.js">watch.js</a></td>
+ </tr>
+ </tbody>
+ </table>
+{% endblock main %}
diff --git a/youtube/templates/local_playlist.html b/youtube/templates/local_playlist.html
new file mode 100644
index 0000000..3286f67
--- /dev/null
+++ b/youtube/templates/local_playlist.html
@@ -0,0 +1,49 @@
+{% set page_title = playlist_name + ' - Local playlist' %}
+{% 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">
+{% 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>
+ <div id="results" class="video-container">
+ {% for video_info in videos %}
+ {{ 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) }}
+ </nav>
+ </footer>
+{% endblock main %}
diff --git a/youtube/templates/local_playlists_list.html b/youtube/templates/local_playlists_list.html
new file mode 100644
index 0000000..61a6888
--- /dev/null
+++ b/youtube/templates/local_playlists_list.html
@@ -0,0 +1,14 @@
+{% set page_title = 'Local playlists' %}
+{% extends "base.html" %}
+
+{% block style %}
+ <link href="/youtube.com/static/home.css" rel="stylesheet"/>
+{% endblock style %}
+
+{% block main %}
+ <ul>
+ {% for playlist_name, playlist_url in playlists %}
+ <li><a href="{{ playlist_url }}">{{ playlist_name }}</a></li>
+ {% endfor %}
+ </ul>
+{% endblock main %}
diff --git a/youtube/templates/playlist.html b/youtube/templates/playlist.html
new file mode 100644
index 0000000..994523e
--- /dev/null
+++ b/youtube/templates/playlist.html
@@ -0,0 +1,41 @@
+{% set page_title = title|string + ' - Page ' + parameters_dictionary.get('page', '1') %}
+{% 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">
+{% endblock style %}
+
+{% block main %}
+
+ <div class="playlist-metadata">
+ <div class="author">
+ <img alt="{{ title }}" src="{{ thumbnail }}">
+ <h2>{{ title }}</h2>
+ </div>
+ <div class="summary">
+ <a class="playlist-author" href="{{ author_url }}">{{ author }}</a>
+ </div>
+ <div class="playlist-stats">
+ <div>{{ video_count|commatize }} videos</div>
+ <div>{{ view_count|commatize }} views</div>
+ <div>Last updated {{ time_published }}</div>
+ </div>
+ </div>
+ <hr/>
+
+
+ <div id="results" class="video-container">
+ {% for info in video_list %}
+ {{ common_elements.item(info) }}
+ {% endfor %}
+ </div>
+ <hr/>
+
+ <footer class="pagination-container">
+ <nav class="pagination-list">
+ {{ common_elements.page_buttons(num_pages, '/https://www.youtube.com/playlist', parameters_dictionary) }}
+ </nav>
+ </footer>
+
+{% endblock main %}
diff --git a/youtube/templates/search.html b/youtube/templates/search.html
new file mode 100644
index 0000000..af87c90
--- /dev/null
+++ b/youtube/templates/search.html
@@ -0,0 +1,34 @@
+{% set search_box_value = query %}
+{% set page_title = query + ' - Search' %}
+{% 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">
+{% endblock style %}
+
+{% block main %}
+ <div class="result-info" id="result-info">
+ <div id="number-of-results">Approximately {{ '{:,}'.format(estimated_results) }} results ({{ '{:,}'.format(estimated_pages) }} pages)</div>
+ {% if corrections['type'] == 'showing_results_for' %}
+ <div>Showing results for <a>{{ common_elements.text_runs(corrections['corrected_query_text']) }}</a></div>
+ <div>Search instead for <a href="{{ corrections['original_query_url'] }}">{{ corrections['original_query_text'] }}</a></div>
+ {% elif corrections['type'] == 'did_you_mean' %}
+ <div>Did you mean <a href="{{ corrections['corrected_query_url'] }}">{{ common_elements.text_runs(corrections['corrected_query_text']) }}</a></div>
+ {% endif %}
+ </div>
+
+ <!-- video item -->
+ <div class="video-container">
+ {% for info in results %}
+ {{ common_elements.item(info, description=true) }}
+ {% endfor %}
+ </div>
+ <hr/>
+ <!-- /video item -->
+ <footer class="pagination-container">
+ <nav class="pagination-list">
+ {{ 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
new file mode 100644
index 0000000..a4ebabf
--- /dev/null
+++ b/youtube/templates/settings.html
@@ -0,0 +1,47 @@
+{% set page_title = 'Settings' %}
+{% extends "base.html" %}
+{% block style %}
+ <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>
+ <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 }}" {% if 'comment' is in(setting_info) %}title="{{ setting_info['comment'] }}" {% endif %}>{{ setting_info['label'] }}</label>
+ {% else %}
+ <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' %}
+ <input type="checkbox" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" {{ 'checked' if value else '' }}>
+ {% elif setting_info['type'].__name__ == 'int' %}
+ {% 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="number" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" value="{{ value }}" step="1">
+ {% endif %}
+ {% elif setting_info['type'].__name__ == 'float' %}
+
+ {% elif setting_info['type'].__name__ == 'str' %}
+ <input type="text" id="{{ 'setting_' + setting_name }}" name="{{ setting_name }}" value="{{ value }}">
+ {% else %}
+ <span>Error: Unknown setting type: setting_info['type'].__name__</span>
+ {% endif %}
+ </li>
+ {% endif %}
+ {% endfor %}
+ </ul>
+ {% endfor %}
+ <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/status.html b/youtube/templates/status.html
new file mode 100644
index 0000000..97e2ed4
--- /dev/null
+++ b/youtube/templates/status.html
@@ -0,0 +1,6 @@
+{% set page_title = (title if (title is defined) else 'Status') %}
+{% extends "base.html" %}
+
+{% block main %}
+ {{ message }}
+{% endblock %}
diff --git a/youtube/templates/subscription_manager.html b/youtube/templates/subscription_manager.html
new file mode 100644
index 0000000..96082c3
--- /dev/null
+++ b/youtube/templates/subscription_manager.html
@@ -0,0 +1,78 @@
+{% set page_title = 'Subscription Manager' %}
+{% extends "base.html" %}
+{% block style %}
+ <link href="/youtube.com/static/subscription_manager.css" rel="stylesheet">
+{% endblock style %}
+
+
+{% macro subscription_list(sub_list) %}
+ {% for subscription in sub_list %}
+ <li class="sub-list-item {{ 'muted' if subscription['muted'] else '' }}">
+ <input class="sub-list-checkbox" name="channel_ids" value="{{ subscription['channel_id'] }}" form="subscription-manager-form" type="checkbox">
+ <a href="{{ subscription['channel_url'] }}" class="sub-list-item-name" title="{{ subscription['channel_name'] }}">{{ subscription['channel_name'] }}</a>
+ <span class="tag-list">{{ ', '.join(subscription['tags']) }}</span>
+ </li>
+ {% endfor %}
+{% endmacro %}
+
+{% block main %}
+ <div class="import-export">
+ <form class="subscriptions-import-form" enctype="multipart/form-data" action="/youtube.com/import_subscriptions" method="POST">
+ <h2>Import subscriptions</h2>
+ <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>
+
+ <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>
+
+ <form id="subscription-manager-form" class="sub-list-controls" method="POST">
+ {% if group_by_tags %}
+ <a class="sort-button" href="/https://www.youtube.com/subscription_manager?group_by_tags=0">Don't group</a>
+ {% else %}
+ <a class="sort-button" href="/https://www.youtube.com/subscription_manager?group_by_tags=1">Group by tags</a>
+ {% endif %}
+ <input type="text" name="tags">
+ <button type="submit" name="action" value="add_tags">Add tags</button>
+ <button type="submit" name="action" value="remove_tags">Remove tags</button>
+ <button type="submit" name="action" value="unsubscribe_verify">Unsubscribe</button>
+ <button type="submit" name="action" value="mute">Mute</button>
+ <button type="submit" name="action" value="unmute">Unmute</button>
+ <input type="reset" value="Clear Selection">
+ </form>
+
+
+ {% if group_by_tags %}
+ <ul class="tag-group-list">
+ {% for tag_name, sub_list in tag_groups %}
+ <li class="tag-group">
+ <h2 class="tag-group-name">{{ tag_name }}</h2>
+ <ol class="sub-list">
+ {{ subscription_list(sub_list) }}
+ </ol>
+ </li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <ol class="sub-list">
+ {{ subscription_list(sub_list) }}
+ </ol>
+ {% endif %}
+
+{% endblock main %}
diff --git a/youtube/templates/subscriptions.html b/youtube/templates/subscriptions.html
new file mode 100644
index 0000000..2823e8d
--- /dev/null
+++ b/youtube/templates/subscriptions.html
@@ -0,0 +1,83 @@
+{% if current_tag %}
+ {% set page_title = 'Subscriptions - ' + current_tag %}
+{% else %}
+ {% set page_title = 'Subscriptions' %}
+{% 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/subscription.css" rel="stylesheet">
+{% endblock style %}
+
+{% block main %}
+
+ <div class="subscriptions-sidebar">
+ <div class="sidebar-links">
+ <a class="sidebar-title" href="/youtube.com/subscription_manager" class="sub-manager-link">Subscription Manager</a>
+ <form class="sidebar-action" method="POST" class="refresh-all">
+ <input type="submit" value="Check All">
+ <input type="hidden" name="action" value="refresh">
+ <input type="hidden" name="type" value="all">
+ </form>
+ </div>
+
+ <ol class="sidebar-list tags">
+ {% if current_tag %}
+ <li class="sidebar-list-item">
+ <a href="/youtube.com/subscriptions" class="sidebar-item-name">Any tag</a>
+ </li>
+ {% endif %}
+
+ {% for tag in tags %}
+ <li class="sidebar-list-item">
+ {% if tag == current_tag %}
+ <span class="sidebar-item-name">{{ tag }}</span>
+ {% else %}
+ <a href="?tag={{ tag|urlencode }}" class="sidebar-item-name">{{ tag }}</a>
+ {% endif %}
+ <form method="POST" class="sidebar-item-refresh">
+ <input type="submit" value="Check">
+ <input type="hidden" name="action" value="refresh">
+ <input type="hidden" name="type" value="tag">
+ <input type="hidden" name="tag_name" value="{{ tag }}">
+ </form>
+ </li>
+ {% endfor %}
+ </ol>
+
+ <hr>
+ <ol class="sidebar-list sub-refresh-list">
+ {% for subscription in subscription_list %}
+ <li class="sidebar-list-item {{ 'muted' if subscription['muted'] else '' }}">
+ <a href="{{ subscription['channel_url'] }}" class="sidebar-item-name" title="{{ subscription['channel_name'] }}">{{ subscription['channel_name'] }}</a>
+ <form method="POST" class="sidebar-item-refresh">
+ <input type="submit" value="Check">
+ <input type="hidden" name="action" value="refresh">
+ <input type="hidden" name="type" value="channel">
+ <input type="hidden" name="channel_id" value="{{ subscription['channel_id'] }}">
+ </form>
+ </li>
+ {% endfor %}
+ </ol>
+ </div>
+
+ {% if current_tag %}
+ <h2 class="current-tag">{{ current_tag }}</h2>
+ {% endif %}
+
+ <div class="video-container">
+ {% for video_info in videos %}
+ {{ common_elements.item(video_info) }}
+ {% endfor %}
+ </div>
+ <hr/>
+
+ <footer class="pagination-container">
+ <nav class="pagination-list">
+ {{ common_elements.page_buttons(num_pages, '/youtube.com/subscriptions', parameters_dictionary) }}
+ </nav>
+ </footer>
+
+{% endblock 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
new file mode 100644
index 0000000..e899783
--- /dev/null
+++ b/youtube/templates/unsubscribe_verify.html
@@ -0,0 +1,21 @@
+{% set page_title = 'Unsubscribe?' %}
+{% extends "base.html" %}
+{% block style %}
+ <link href="/youtube.com/static/unsubscribe.css" rel="stylesheet"/>
+{% endblock style %}
+
+{% block main %}
+ <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 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 %}
+ </ul>
+{% endblock main %}
diff --git a/youtube/templates/watch.html b/youtube/templates/watch.html
new file mode 100644
index 0000000..0991457
--- /dev/null
+++ b/youtube/templates/watch.html
@@ -0,0 +1,263 @@
+{% set page_title = title %}
+{% extends "base.html" %}
+{% 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">
+ {% 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 %}
+ {% if playability_error %}
+ <div class="playability-error">
+ <span>{{ 'Error: ' + playability_error }}
+ {% if invidious_reload_button %}
+ <a href="{{ video_url }}&use_invidious=0"><br>
+ Reload without invidious (for usage of new identity button).</a>
+ {% endif %}
+ </span>
+ </div>
+ {% elif (uni_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 {{ 'autoplay' if settings.autoplay_videos }}>
+ {% if uni_sources %}
+ <source src="{{ uni_sources[uni_idx]['url'] }}" type="{{ uni_sources[uni_idx]['type'] }}" data-res="{{ uni_sources[uni_idx]['quality'] }}">
+ {% endif %}
+
+ {% for source in subtitle_sources %}
+ {% if source['on'] %}
+ <track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}" default>
+ {% else %}
+ <track label="{{ source['label'] }}" src="{{ source['url'] }}" kind="subtitles" srclang="{{ source['srclang'] }}">
+ {% endif %}
+ {% endfor %}
+ </video>
+ </figure>
+ {% endif %}
+
+ <div class="sc-info">
+ <div class="video-info">
+ <h1 class="v-title">{{ title }}</h1>
+
+ <ul class="labels">
+ {%- if unlisted -%}
+ <li class="is-unlisted">Unlisted</li>
+ {%- endif -%}
+ {%- if age_restricted -%}
+ <li class="age-restricted">Age-restricted</li>
+ {%- endif -%}
+ {%- if limited_state -%}
+ <li>Limited state</li>
+ {%- endif -%}
+ {%- if live -%}
+ <li>Live</li>
+ {%- endif -%}
+ </ul>
+
+ <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</span>
+
+ <div class="external-player-controls">
+ <input class="speed" id="speed-control" type="text" title="Video speed">
+ {% if settings.use_video_player != 2 %}
+ <select id="quality-select" autocomplete="off">
+ {% for src in uni_sources %}
+ <option value='{"type": "uni", "index": {{ loop.index0 }}}' {{ 'selected' if loop.index0 == uni_idx and not using_pair_sources else '' }} >{{ src['quality_string'] }}</option>
+ {% endfor %}
+ {% for src_pair in pair_sources %}
+ <option value='{"type": "pair", "index": {{ loop.index0}}}' {{ 'selected' if loop.index0 == pair_idx and using_pair_sources else '' }} >{{ src_pair['quality_string'] }}</option>
+ {% endfor %}
+ </select>
+ {% 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>
+ </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">
+ {% if music_list.__len__() != 0 %}
+ <hr>
+ <table>
+ <caption>Music</caption>
+ <tr>
+ {% for attribute in music_attributes %}
+ <th>{{ attribute }}</th>
+ {% endfor %}
+ </tr>
+ {% for track in music_list %}
+ <tr>
+ {% for attribute in music_attributes %}
+ {% 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 %}
+ </table>
+ {% endif %}
+ </div>
+ <details class="v-more-info">
+ <summary>More info</summary>
+ <div class="more-info-content">
+ <p>Tor exit node: {{ ip_address }}</p>
+ {% if invidious_used %}
+ <p>Used Invidious as fallback.</p>
+ {% endif %}
+ <p class="allowed-countries">Allowed countries: {{ allowed_countries|join(', ') }}</p>
+ {% if settings.use_sponsorblock_js %}
+ <ul class="more-actions">
+ <li><label><input type=checkbox id=skip_sponsors checked>skip sponsors</label> <span id=skip_n></span>
+ </ul>
+ {% endif %}
+ </div>
+ </details>
+ </div>
+
+ <div class="side-videos">
+
+ <!-- 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><label for="playlist-autoplay-toggle">Autoplay: </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 %}
+ <li><a href="{{ playlist['author_url'] }}" title="{{ playlist['author'] }}">{{ playlist['author'] }}</a></li>
+ </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>
+ {% elif settings.related_videos_mode != 0 %}
+ <div class="related-autoplay"><label for="related-autoplay-toggle">Autoplay: </label><input id="related-autoplay-toggle" type="checkbox" class="autoplay-toggle"></div>
+ {% endif %}
+
+ {% if subtitle_sources %}
+ <details id="transcript-details">
+ <summary>Transcript</summary>
+ <div id="transcript-div">
+ <select id="select-tt">
+ {% for source in subtitle_sources %}
+ <option>{{ source['label'] }}</option>
+ {% endfor %}
+ </select>
+ <label for="transcript-use-table">Table view</label>
+ <input id="transcript-use-table" type="checkbox">
+ <table id="transcript-table"></table>
+ </div>
+ </details>
+ {% endif %}
+
+
+ {% if settings.related_videos_mode != 0 %}
+ <details class="related-videos-outer" {{'open' if settings.related_videos_mode == 1 else ''}}>
+ <summary>Related Videos</summary>
+ <nav class="related-videos-inner">
+ {% for info in related %}
+ {{ common_elements.item(info, include_badges=false) }}
+ {% endfor %}
+ </nav>
+ </details>
+ {% endif %}
+
+ </div>
+
+ <!-- comments -->
+ {% if settings.comments_mode != 0 %}
+ {% if comments_disabled %}
+ <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>
+ <div class="comments-area-inner comments-area">
+ {% if comments_info %}
+ {{ comments.video_comments(comments_info) }}
+ {% endif %}
+ </div>
+ </details>
+ {% endif %}
+ {% endif %}
+
+ </div>
+
+ <script src="/youtube.com/static/js/av-merge.js"></script>
+ <script src="/youtube.com/static/js/watch.js"></script>
+ <script>
+ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+ let storyboard_url = {{ storyboard_url | 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_player == 2 %}
+ <!-- plyr -->
+ <script src="/youtube.com/static/modules/plyr/plyr.min.js"
+ integrity="sha512-l6ZzdXpfMHRfifqaR79wbYCEWjLDMI9DnROvb+oLkKq6d7MGroGpMbI7HFpicvmAH/2aQO+vJhewq8rhysrImw=="
+ crossorigin="anonymous"></script>
+ <script src="/youtube.com/static/js/plyr-start.js"></script>
+ <!-- /plyr -->
+ {% elif settings.use_video_player == 1 %}
+ <script src="/youtube.com/static/js/hotkeys.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 %}