diff options
Diffstat (limited to 'youtube/templates')
-rw-r--r-- | youtube/templates/base.html | 179 | ||||
-rw-r--r-- | youtube/templates/channel.html | 133 | ||||
-rw-r--r-- | youtube/templates/comments.html | 65 | ||||
-rw-r--r-- | youtube/templates/comments_page.html | 47 | ||||
-rw-r--r-- | youtube/templates/common_elements.html | 140 | ||||
-rw-r--r-- | youtube/templates/embed.html | 74 | ||||
-rw-r--r-- | youtube/templates/error.html | 36 | ||||
-rw-r--r-- | youtube/templates/home.html | 13 | ||||
-rw-r--r-- | youtube/templates/licenses.html | 64 | ||||
-rw-r--r-- | youtube/templates/local_playlist.html | 49 | ||||
-rw-r--r-- | youtube/templates/local_playlists_list.html | 14 | ||||
-rw-r--r-- | youtube/templates/playlist.html | 41 | ||||
-rw-r--r-- | youtube/templates/search.html | 34 | ||||
-rw-r--r-- | youtube/templates/settings.html | 47 | ||||
-rw-r--r-- | youtube/templates/shared.css | 5 | ||||
-rw-r--r-- | youtube/templates/status.html | 6 | ||||
-rw-r--r-- | youtube/templates/subscription_manager.html | 78 | ||||
-rw-r--r-- | youtube/templates/subscriptions.html | 83 | ||||
-rw-r--r-- | youtube/templates/subscriptions.xml | 9 | ||||
-rw-r--r-- | youtube/templates/unsubscribe_verify.html | 21 | ||||
-rw-r--r-- | youtube/templates/watch.html | 263 |
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 (< 4 minutes)</label> + </div> + <div class="option"> + <input type="radio" id="duration_long" name="duration" value="2"> + <label for="duration_long">Long (> 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=" " data-src="{{ info['thumbnail'] }}"> + {% elif info['type'] == 'channel' %} + <img class="thumbnail-img channel" alt=" " src="{{ info['thumbnail'] }}"> + {% else %} + <img class="thumbnail-img" alt=" " 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 %} |