summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md6
-rw-r--r--.github/workflows/build-nightly-container.yml13
-rw-r--r--.github/workflows/build-stable-container.yml13
-rw-r--r--.github/workflows/ci.yml6
-rw-r--r--assets/js/handlers.js4
-rw-r--r--assets/js/pagination.js93
-rw-r--r--assets/js/playlist_widget.js6
-rw-r--r--assets/js/subscribe_widget.js4
-rw-r--r--assets/js/watch.js4
-rw-r--r--assets/js/watched_widget.js4
-rw-r--r--config/config.example.yml10
-rw-r--r--locales/de.json1
-rw-r--r--locales/en-US.json1
-rw-r--r--locales/ru.json1
-rw-r--r--src/invidious/config.cr12
-rw-r--r--src/invidious/frontend/pagination.cr32
-rw-r--r--src/invidious/frontend/watch_page.cr2
-rw-r--r--src/invidious/helpers/crystal_class_overrides.cr34
-rw-r--r--src/invidious/helpers/errors.cr4
-rw-r--r--src/invidious/helpers/handlers.cr1
-rw-r--r--src/invidious/helpers/serialized_yt_data.cr21
-rw-r--r--src/invidious/jsonify/api_v1/video_json.cr6
-rw-r--r--src/invidious/mixes.cr4
-rw-r--r--src/invidious/playlists.cr4
-rw-r--r--src/invidious/routes/account.cr14
-rw-r--r--src/invidious/routes/api/manifest.cr40
-rw-r--r--src/invidious/routes/api/v1/misc.cr14
-rw-r--r--src/invidious/routes/embed.cr6
-rw-r--r--src/invidious/routes/feeds.cr1
-rw-r--r--src/invidious/routes/playlists.cr31
-rw-r--r--src/invidious/routes/subscriptions.cr14
-rw-r--r--src/invidious/routes/video_playback.cr9
-rw-r--r--src/invidious/routes/watch.cr20
-rw-r--r--src/invidious/search/filters.cr2
-rw-r--r--src/invidious/search/query.cr2
-rw-r--r--src/invidious/user/imports.cr57
-rw-r--r--src/invidious/videos.cr2
-rw-r--r--src/invidious/videos/parser.cr8
-rw-r--r--src/invidious/videos/storyboard.cr2
-rw-r--r--src/invidious/views/channel.ecr6
-rw-r--r--src/invidious/views/components/item.ecr6
-rw-r--r--src/invidious/views/components/items_paginated.ecr10
-rw-r--r--src/invidious/views/components/subscribe_widget.ecr4
-rw-r--r--src/invidious/views/feeds/history.ecr2
-rw-r--r--src/invidious/views/user/subscription_manager.ecr2
-rw-r--r--src/invidious/views/user/token_manager.ecr2
-rw-r--r--src/invidious/views/watch.ecr3
-rw-r--r--src/invidious/yt_backend/extractors.cr7
-rw-r--r--src/invidious/yt_backend/youtube_api.cr12
49 files changed, 326 insertions, 236 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 4c1a6330..02bc3795 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -10,8 +10,10 @@ assignees: ''
<!--
BEFORE TRYING TO REPORT A BUG:
- * Read the FAQ!
- * Use the search function to check if there is already an issue open for your problem!
+ * Read the FAQ: https://docs.invidious.io/faq/!
+ * Use the search function to check if there is already an issue open for your problem: https://github.com/search?q=repo%3Aiv-org%2Finvidious+replace+me+with+your+bug&type=issues!
+
+ MAKE SURE TO FOLLOW THE TWO STEPS ABOVE BEFORE REPORTING A BUG. A BUG THAT ALREADY EXIST WILL IMMEDIATELY CLOSED.
If you want to suggest a new feature please use "Feature request" instead
If you want to suggest an enhancement to an existing feature please use "Enhancement" instead
diff --git a/.github/workflows/build-nightly-container.yml b/.github/workflows/build-nightly-container.yml
index bee27600..5ff3322f 100644
--- a/.github/workflows/build-nightly-container.yml
+++ b/.github/workflows/build-nightly-container.yml
@@ -23,19 +23,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- - name: Install Crystal
- uses: crystal-lang/install-crystal@v1.8.2
- with:
- crystal: 1.12.2
-
- - name: Run lint
- run: |
- if ! crystal tool format --check; then
- crystal tool format
- git diff
- exit 1
- fi
-
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
diff --git a/.github/workflows/build-stable-container.yml b/.github/workflows/build-stable-container.yml
index d2d106b6..25571ed6 100644
--- a/.github/workflows/build-stable-container.yml
+++ b/.github/workflows/build-stable-container.yml
@@ -14,19 +14,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- - name: Install Crystal
- uses: crystal-lang/install-crystal@v1.8.2
- with:
- crystal: 1.12.2
-
- - name: Run lint
- run: |
- if ! crystal tool format --check; then
- crystal tool format
- git diff
- exit 1
- fi
-
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dd472d1a..5f859613 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,11 +38,10 @@ jobs:
matrix:
stable: [true]
crystal:
- - 1.10.1
- - 1.11.2
- 1.12.1
- 1.13.2
- 1.14.0
+ - 1.15.0
include:
- crystal: nightly
stable: false
@@ -136,6 +135,7 @@ jobs:
submodules: true
- name: Install Crystal
+ id: lint_step_install_crystal
uses: crystal-lang/install-crystal@v1.8.0
with:
crystal: latest
@@ -146,7 +146,7 @@ jobs:
path: |
./lib
./bin
- key: shards-${{ hashFiles('shard.lock') }}
+ key: shards-${{ hashFiles('shard.lock') }}-${{ steps.lint_step_install_crystal.outputs.crystal }}
- name: Install Shards
run: |
diff --git a/assets/js/handlers.js b/assets/js/handlers.js
index 539974fb..67cd9081 100644
--- a/assets/js/handlers.js
+++ b/assets/js/handlers.js
@@ -91,7 +91,7 @@
var count = document.getElementById('count');
count.textContent--;
- var url = '/token_ajax?action_revoke_token=1&redirect=false' +
+ var url = '/token_ajax?action=revoke_token&redirect=false' +
'&referer=' + encodeURIComponent(location.href) +
'&session=' + target.getAttribute('data-session');
@@ -111,7 +111,7 @@
var count = document.getElementById('count');
count.textContent--;
- var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
+ var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
'&referer=' + encodeURIComponent(location.href) +
'&c=' + target.getAttribute('data-ucid');
diff --git a/assets/js/pagination.js b/assets/js/pagination.js
new file mode 100644
index 00000000..2e560a34
--- /dev/null
+++ b/assets/js/pagination.js
@@ -0,0 +1,93 @@
+'use strict';
+
+const CURRENT_CONTINUATION = (new URL(document.location)).searchParams.get("continuation");
+const CONT_CACHE_KEY = `continuation_cache_${encodeURIComponent(window.location.pathname)}`;
+
+function get_data(){
+ return JSON.parse(sessionStorage.getItem(CONT_CACHE_KEY)) || [];
+}
+
+function save_data(){
+ const prev_data = get_data();
+ prev_data.push(CURRENT_CONTINUATION);
+
+ sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data));
+}
+
+function button_press(){
+ let prev_data = get_data();
+ if (!prev_data.length) return null;
+
+ // Sanity check. Nowhere should the current continuation token exist in the cache
+ // but it can happen when using the browser's back feature. As such we'd need to travel
+ // back to the point where the current continuation token first appears in order to
+ // account for the rewind.
+ const conflict_at = prev_data.indexOf(CURRENT_CONTINUATION);
+ if (conflict_at != -1) {
+ prev_data.length = conflict_at;
+ }
+
+ const prev_ctoken = prev_data.pop();
+
+ // On the first page, the stored continuation token is null.
+ if (prev_ctoken === null) {
+ sessionStorage.removeItem(CONT_CACHE_KEY);
+ let url = set_continuation();
+ window.location.href = url;
+
+ return;
+ }
+
+ sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data));
+ let url = set_continuation(prev_ctoken);
+
+ window.location.href = url;
+};
+
+// Method to set the current page's continuation token
+// Removes the continuation parameter when a continuation token is not given
+function set_continuation(prev_ctoken = null){
+ let url = window.location.href.split('?')[0];
+ let params = window.location.href.split('?')[1];
+ let url_params = new URLSearchParams(params);
+
+ if (prev_ctoken) {
+ url_params.set("continuation", prev_ctoken);
+ } else {
+ url_params.delete('continuation');
+ };
+
+ if(Array.from(url_params).length > 0){
+ return `${url}?${url_params.toString()}`;
+ } else {
+ return url;
+ }
+}
+
+addEventListener('DOMContentLoaded', function(){
+ const pagination_data = JSON.parse(document.getElementById('pagination-data').textContent);
+ const next_page_containers = document.getElementsByClassName("page-next-container");
+
+ for (let container of next_page_containers){
+ const next_page_button = container.getElementsByClassName("pure-button")
+
+ // exists?
+ if (next_page_button.length > 0){
+ next_page_button[0].addEventListener("click", save_data);
+ }
+ }
+
+ // Only add previous page buttons when not on the first page
+ if (CURRENT_CONTINUATION) {
+ const prev_page_containers = document.getElementsByClassName("page-prev-container")
+
+ for (let container of prev_page_containers) {
+ if (pagination_data.is_rtl) {
+ container.innerHTML = `<button class="pure-button pure-button-secondary">${pagination_data.prev_page}&nbsp;&nbsp;<i class="icon ion-ios-arrow-forward"></i></button>`
+ } else {
+ container.innerHTML = `<button class="pure-button pure-button-secondary"><i class="icon ion-ios-arrow-back"></i>&nbsp;&nbsp;${pagination_data.prev_page}</button>`
+ }
+ container.getElementsByClassName("pure-button")[0].addEventListener("click", button_press);
+ }
+ }
+});
diff --git a/assets/js/playlist_widget.js b/assets/js/playlist_widget.js
index c92592ac..96a51d70 100644
--- a/assets/js/playlist_widget.js
+++ b/assets/js/playlist_widget.js
@@ -6,7 +6,7 @@ function add_playlist_video(target) {
var select = target.parentNode.children[0].children[1];
var option = select.children[select.selectedIndex];
- var url = '/playlist_ajax?action_add_video=1&redirect=false' +
+ var url = '/playlist_ajax?action=add_video&redirect=false' +
'&video_id=' + target.getAttribute('data-id') +
'&playlist_id=' + option.getAttribute('data-plid');
@@ -21,7 +21,7 @@ function add_playlist_item(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
- var url = '/playlist_ajax?action_add_video=1&redirect=false' +
+ var url = '/playlist_ajax?action=add_video&redirect=false' +
'&video_id=' + target.getAttribute('data-id') +
'&playlist_id=' + target.getAttribute('data-plid');
@@ -36,7 +36,7 @@ function remove_playlist_item(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
- var url = '/playlist_ajax?action_remove_video=1&redirect=false' +
+ var url = '/playlist_ajax?action=remove_video&redirect=false' +
'&set_video_id=' + target.getAttribute('data-index') +
'&playlist_id=' + target.getAttribute('data-plid');
diff --git a/assets/js/subscribe_widget.js b/assets/js/subscribe_widget.js
index 7665a00b..d462e848 100644
--- a/assets/js/subscribe_widget.js
+++ b/assets/js/subscribe_widget.js
@@ -16,7 +16,7 @@ function subscribe() {
subscribe_button.onclick = unsubscribe;
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
- var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' +
+ var url = '/subscription_ajax?action=create_subscription_to_channel&redirect=false' +
'&c=' + subscribe_data.ucid;
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, {
@@ -32,7 +32,7 @@ function unsubscribe() {
subscribe_button.onclick = subscribe;
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
- var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
+ var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
'&c=' + subscribe_data.ucid;
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, {
diff --git a/assets/js/watch.js b/assets/js/watch.js
index 26ad138f..d869d40d 100644
--- a/assets/js/watch.js
+++ b/assets/js/watch.js
@@ -67,6 +67,10 @@ function get_playlist(plid) {
'&format=html&hl=' + video_data.preferences.locale;
}
+ if (video_data.params.listen) {
+ plid_url += '&listen=1'
+ }
+
helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, {
on200: function (response) {
playlist.innerHTML = response.playlistHtml;
diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js
index f1ac9cb4..06af62cc 100644
--- a/assets/js/watched_widget.js
+++ b/assets/js/watched_widget.js
@@ -6,7 +6,7 @@ function mark_watched(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
- var url = '/watch_ajax?action_mark_watched=1&redirect=false' +
+ var url = '/watch_ajax?action=mark_watched&redirect=false' +
'&id=' + target.getAttribute('data-id');
helpers.xhr('POST', url, {payload: payload}, {
@@ -22,7 +22,7 @@ function mark_unwatched(target) {
var count = document.getElementById('count');
count.textContent--;
- var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' +
+ var url = '/watch_ajax?action=mark_unwatched&redirect=false' +
'&id=' + target.getAttribute('data-id');
helpers.xhr('POST', url, {payload: payload}, {
diff --git a/config/config.example.yml b/config/config.example.yml
index a3a2eeb7..bc2deda5 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -178,11 +178,11 @@ https_only: false
##
## If unset, then no HTTP proxy will be used.
##
-http_proxy:
- user:
- password:
- host:
- port:
+#http_proxy:
+# user:
+# password:
+# host:
+# port:
##
diff --git a/locales/de.json b/locales/de.json
index 151f2abe..a9a62619 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -11,6 +11,7 @@
"last": "neueste",
"Next page": "Nächste Seite",
"Previous page": "Vorherige Seite",
+ "First page": "Erste Seite",
"Clear watch history?": "Verlauf löschen?",
"New password": "Neues Passwort",
"New passwords must match": "Neue Passwörter müssen übereinstimmen",
diff --git a/locales/en-US.json b/locales/en-US.json
index c23f6bc3..381bcab5 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -33,6 +33,7 @@
"last": "last",
"Next page": "Next page",
"Previous page": "Previous page",
+ "First page": "First page",
"Clear watch history?": "Clear watch history?",
"New password": "New password",
"New passwords must match": "New passwords must match",
diff --git a/locales/ru.json b/locales/ru.json
index 80c98de8..31ef1a33 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -11,6 +11,7 @@
"last": "последние",
"Next page": "Следующая страница",
"Previous page": "Предыдущая страница",
+ "First page": "Первая страница",
"Clear watch history?": "Очистить историю просмотров?",
"New password": "Новый пароль",
"New passwords must match": "Новые пароли не совпадают",
diff --git a/src/invidious/config.cr b/src/invidious/config.cr
index c4ca622f..4b3bdafc 100644
--- a/src/invidious/config.cr
+++ b/src/invidious/config.cr
@@ -184,6 +184,9 @@ class Config
config = Config.from_yaml(config_yaml)
# Update config from env vars (upcased and prefixed with "INVIDIOUS_")
+ #
+ # Also checks if any top-level config options are set to "CHANGE_ME!!"
+ # TODO: Support non-top-level config options such as the ones in DBConfig
{% for ivar in Config.instance_vars %}
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
@@ -220,6 +223,12 @@ class Config
exit(1)
end
end
+
+ # Warn when any config attribute is set to "CHANGE_ME!!"
+ if config.{{ivar.id}} == "CHANGE_ME!!"
+ puts "Config: The value of '#{ {{ivar.stringify}} }' needs to be changed!!"
+ exit(1)
+ end
{% end %}
# HMAC_key is mandatory
@@ -227,9 +236,6 @@ class Config
if config.hmac_key.empty?
puts "Config: 'hmac_key' is required/can't be empty"
exit(1)
- elsif config.hmac_key == "CHANGE_ME!!"
- puts "Config: The value of 'hmac_key' needs to be changed!!"
- exit(1)
end
# Build database_url from db.* if it's not set directly
diff --git a/src/invidious/frontend/pagination.cr b/src/invidious/frontend/pagination.cr
index 3f931f4e..a29f5936 100644
--- a/src/invidious/frontend/pagination.cr
+++ b/src/invidious/frontend/pagination.cr
@@ -3,6 +3,24 @@ require "uri"
module Invidious::Frontend::Pagination
extend self
+ private def first_page(str : String::Builder, locale : String?, url : String)
+ str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
+
+ if locale_is_rtl?(locale)
+ # Inverted arrow ("first" points to the right)
+ str << translate(locale, "First page")
+ str << "&nbsp;&nbsp;"
+ str << %(<i class="icon ion-ios-arrow-forward"></i>)
+ else
+ # Regular arrow ("first" points to the left)
+ str << %(<i class="icon ion-ios-arrow-back"></i>)
+ str << "&nbsp;&nbsp;"
+ str << translate(locale, "First page")
+ end
+
+ str << "</a>"
+ end
+
private def previous_page(str : String::Builder, locale : String?, url : String)
# Link
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
@@ -72,18 +90,24 @@ module Invidious::Frontend::Pagination
end
end
- def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?)
+ def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?, first_page : Bool, params : URI::Params)
return String.build do |str|
str << %(<div class="h-box">\n)
str << %(<div class="page-nav-container flexible">\n)
- str << %(<div class="page-prev-container flex-left"></div>\n)
+ str << %(<div class="page-prev-container flex-left">)
+
+ if !first_page
+ self.first_page(str, locale, base_url.to_s)
+ end
+
+ str << %(</div>\n)
str << %(<div class="page-next-container flex-right">)
if !ctoken.nil?
- params_next = URI::Params{"continuation" => ctoken}
- url_next = HttpServer::Utils.add_params_to_url(base_url, params_next)
+ params["continuation"] = ctoken
+ url_next = HttpServer::Utils.add_params_to_url(base_url, params)
self.next_page(str, locale, url_next.to_s)
end
diff --git a/src/invidious/frontend/watch_page.cr b/src/invidious/frontend/watch_page.cr
index c8cb7110..2e2f6ad0 100644
--- a/src/invidious/frontend/watch_page.cr
+++ b/src/invidious/frontend/watch_page.cr
@@ -13,7 +13,7 @@ module Invidious::Frontend::WatchPage
@full_videos,
@video_streams,
@audio_streams,
- @captions
+ @captions,
)
end
end
diff --git a/src/invidious/helpers/crystal_class_overrides.cr b/src/invidious/helpers/crystal_class_overrides.cr
index 3040d7a0..fec3f62c 100644
--- a/src/invidious/helpers/crystal_class_overrides.cr
+++ b/src/invidious/helpers/crystal_class_overrides.cr
@@ -18,40 +18,6 @@ end
class HTTP::Client
property family : Socket::Family = Socket::Family::UNSPEC
- # Override stdlib to automatically initialize proxy if configured
- #
- # Accurate as of crystal 1.12.1
-
- def initialize(@host : String, port = nil, tls : TLSContext = nil)
- check_host_only(@host)
-
- {% if flag?(:without_openssl) %}
- if tls
- raise "HTTP::Client TLS is disabled because `-D without_openssl` was passed at compile time"
- end
- @tls = nil
- {% else %}
- @tls = case tls
- when true
- OpenSSL::SSL::Context::Client.new
- when OpenSSL::SSL::Context::Client
- tls
- when false, nil
- nil
- end
- {% end %}
-
- @port = (port || (@tls ? 443 : 80)).to_i
-
- self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
- end
-
- def initialize(@io : IO, @host = "", @port = 80)
- @reconnect = false
-
- self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
- end
-
private def io
io = @io
return io if io
diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr
index b7643194..900cb0c6 100644
--- a/src/invidious/helpers/errors.cr
+++ b/src/invidious/helpers/errors.cr
@@ -130,7 +130,7 @@ def error_json_helper(
env : HTTP::Server::Context,
status_code : Int32,
exception : Exception,
- additional_fields : Hash(String, Object) | Nil = nil
+ additional_fields : Hash(String, Object) | Nil = nil,
)
if exception.is_a?(InfoException)
return error_json_helper(env, status_code, exception.message || "", additional_fields)
@@ -152,7 +152,7 @@ def error_json_helper(
env : HTTP::Server::Context,
status_code : Int32,
message : String,
- additional_fields : Hash(String, Object) | Nil = nil
+ additional_fields : Hash(String, Object) | Nil = nil,
)
env.response.content_type = "application/json"
env.response.status_code = status_code
diff --git a/src/invidious/helpers/handlers.cr b/src/invidious/helpers/handlers.cr
index f3e3b951..13ea9fe9 100644
--- a/src/invidious/helpers/handlers.cr
+++ b/src/invidious/helpers/handlers.cr
@@ -27,6 +27,7 @@ class Kemal::RouteHandler
# Processes the route if it's a match. Otherwise renders 404.
private def process_request(context)
raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found?
+ return if context.response.closed?
content = context.route.handler.call(context)
if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(context.response.status_code) && exclude_match?(context)
diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr
index 1fef5f93..f8e8f187 100644
--- a/src/invidious/helpers/serialized_yt_data.cr
+++ b/src/invidious/helpers/serialized_yt_data.cr
@@ -24,6 +24,7 @@ struct SearchVideo
property length_seconds : Int32
property premiere_timestamp : Time?
property author_verified : Bool
+ property author_thumbnail : String?
property badges : VideoBadges
def to_xml(auto_generated, query_params, xml : XML::Builder)
@@ -88,6 +89,24 @@ struct SearchVideo
json.field "authorUrl", "/channel/#{self.ucid}"
json.field "authorVerified", self.author_verified
+ author_thumbnail = self.author_thumbnail
+
+ if author_thumbnail
+ json.field "authorThumbnails" do
+ json.array do
+ qualities = {32, 48, 76, 100, 176, 512}
+
+ qualities.each do |quality|
+ json.object do
+ json.field "url", author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
+ json.field "width", quality
+ json.field "height", quality
+ end
+ end
+ end
+ end
+ end
+
json.field "videoThumbnails" do
Invidious::JSONify::APIv1.thumbnails(json, self.id)
end
@@ -223,7 +242,7 @@ struct SearchChannel
qualities.each do |quality|
json.object do
- json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
+ json.field "url", self.author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
json.field "width", quality
json.field "height", quality
end
diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr
index 08cd533f..3439ae60 100644
--- a/src/invidious/jsonify/api_v1/video_json.cr
+++ b/src/invidious/jsonify/api_v1/video_json.cr
@@ -267,6 +267,12 @@ module Invidious::JSONify::APIv1
json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i
json.field "viewCountText", rv["short_view_count"]?
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
+ json.field "published", rv["published"]?
+ if !rv["published"]?.nil?
+ json.field "publishedText", translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))
+ else
+ json.field "publishedText", ""
+ end
end
end
end
diff --git a/src/invidious/mixes.cr b/src/invidious/mixes.cr
index 823ca85b..28ff0ff6 100644
--- a/src/invidious/mixes.cr
+++ b/src/invidious/mixes.cr
@@ -81,7 +81,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
})
end
-def template_mix(mix)
+def template_mix(mix, listen)
html = <<-END_HTML
<h3>
<a href="/mix?list=#{mix["mixId"]}">
@@ -95,7 +95,7 @@ def template_mix(mix)
mix["videos"].as_a.each do |video|
html += <<-END_HTML
<li class="pure-menu-item">
- <a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}">
+ <a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}#{listen ? "&listen=1" : ""}">
<div class="thumbnail">
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr
index a51e88b4..b670c009 100644
--- a/src/invidious/playlists.cr
+++ b/src/invidious/playlists.cr
@@ -505,7 +505,7 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
return videos
end
-def template_playlist(playlist)
+def template_playlist(playlist, listen)
html = <<-END_HTML
<h3>
<a href="/playlist?list=#{playlist["playlistId"]}">
@@ -519,7 +519,7 @@ def template_playlist(playlist)
playlist["videos"].as_a.each do |video|
html += <<-END_HTML
<li class="pure-menu-item" id="#{video["videoId"]}">
- <a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}">
+ <a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}#{listen ? "&listen=1" : ""}">
<div class="thumbnail">
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr
index dd65e7a6..c8db207c 100644
--- a/src/invidious/routes/account.cr
+++ b/src/invidious/routes/account.cr
@@ -328,17 +328,9 @@ module Invidious::Routes::Account
end
end
- if env.params.query["action_revoke_token"]?
- action = "action_revoke_token"
- else
- return env.redirect referer
- end
-
- session = env.params.query["session"]?
- session ||= ""
-
- case action
- when .starts_with? "action_revoke_token"
+ case action = env.params.query["action"]?
+ when "revoke_token"
+ session = env.params.query["session"]
Invidious::Database::SessionIDs.delete(sid: session, email: user.email)
else
return error_json(400, "Unsupported action #{action}")
diff --git a/src/invidious/routes/api/manifest.cr b/src/invidious/routes/api/manifest.cr
index d89e752c..6c4225e5 100644
--- a/src/invidious/routes/api/manifest.cr
+++ b/src/invidious/routes/api/manifest.cr
@@ -27,28 +27,21 @@ module Invidious::Routes::API::Manifest
haltf env, status_code: response.status_code
end
- manifest = response.body
-
- manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
- url = baseurl.lchop("<BaseURL>")
- url = url.rchop("</BaseURL>")
-
- if local
- uri = URI.parse(url)
- url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/"
- end
-
+ # Proxy URLs for video playback on invidious.
+ # Other API clients can get the original URLs by omiting `local=true`.
+ manifest = response.body.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
+ url = baseurl.lchop("<BaseURL>").rchop("</BaseURL>")
+ url = HttpServer::Utils.proxy_video_url(url, absolute: true) if local
"<BaseURL>#{url}</BaseURL>"
end
return manifest
end
- adaptive_fmts = video.adaptive_fmts
-
+ # Ditto, only proxify URLs if `local=true` is used
if local
- adaptive_fmts.each do |fmt|
- fmt["url"] = JSON::Any.new("#{HOST_URL}#{URI.parse(fmt["url"].as_s).request_target}")
+ video.adaptive_fmts.each do |fmt|
+ fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s, absolute: true))
end
end
@@ -70,17 +63,23 @@ module Invidious::Routes::API::Manifest
# OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415)
next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange"))
+ audio_track = fmt["audioTrack"]?.try &.as_h? || {} of String => JSON::Any
+ lang = audio_track["id"]?.try &.as_s.split('.')[0] || "und"
+ is_default = audio_track.has_key?("audioIsDefault") ? audio_track["audioIsDefault"].as_bool : i == 0
+ displayname = audio_track["displayName"]?.try &.as_s || "Unknown"
+ bitrate = fmt["bitrate"]
+
# Different representations of the same audio should be groupped into one AdaptationSet.
# However, most players don't support auto quality switching, so we have to trick them
# into providing a quality selector.
# See https://github.com/iv-org/invidious/issues/3074 for more details.
- xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: fmt["bitrate"].to_s + "k") do
+ xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: "#{displayname} [#{bitrate}k]", lang: lang) do
codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
bandwidth = fmt["bitrate"].as_i
itag = fmt["itag"].as_i
url = fmt["url"].as_s
- xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: i == 0 ? "main" : "alternate")
+ xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: is_default ? "main" : "alternate")
xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do
xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
@@ -177,8 +176,9 @@ module Invidious::Routes::API::Manifest
manifest = response.body
if local
- manifest = manifest.gsub(/^https:\/\/\w+---.{11}\.c\.youtube\.com[^\n]*/m) do |match|
- path = URI.parse(match).path
+ manifest = manifest.gsub(/https:\/\/[^\n"]*/m) do |match|
+ uri = URI.parse(match)
+ path = uri.path
path = path.lchop("/videoplayback/")
path = path.rchop("/")
@@ -207,7 +207,7 @@ module Invidious::Routes::API::Manifest
raw_params["fvip"] = fvip["fvip"]
end
- raw_params["local"] = "true"
+ raw_params["host"] = uri.host.not_nil!
"#{HOST_URL}/videoplayback?#{raw_params}"
end
diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr
index 093669fe..4f5b58da 100644
--- a/src/invidious/routes/api/v1/misc.cr
+++ b/src/invidious/routes/api/v1/misc.cr
@@ -42,6 +42,9 @@ module Invidious::Routes::API::V1::Misc
format = env.params.query["format"]?
format ||= "json"
+ listen_param = env.params.query["listen"]?
+ listen = (listen_param == "true" || listen_param == "1")
+
if plid.starts_with? "RD"
return env.redirect "/api/v1/mixes/#{plid}"
end
@@ -85,7 +88,7 @@ module Invidious::Routes::API::V1::Misc
end
if format == "html"
- playlist_html = template_playlist(json_response)
+ playlist_html = template_playlist(json_response, listen)
index, next_video = json_response["videos"].as_a.skip(1 + lookback).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
response = {
@@ -111,6 +114,9 @@ module Invidious::Routes::API::V1::Misc
format = env.params.query["format"]?
format ||= "json"
+ listen_param = env.params.query["listen"]?
+ listen = (listen_param == "true" || listen_param == "1")
+
begin
mix = fetch_mix(rdid, continuation, locale: locale)
@@ -141,9 +147,7 @@ module Invidious::Routes::API::V1::Misc
json.field "authorUrl", "/channel/#{video.ucid}"
json.field "videoThumbnails" do
- json.array do
- Invidious::JSONify::APIv1.thumbnails(json, video.id)
- end
+ Invidious::JSONify::APIv1.thumbnails(json, video.id)
end
json.field "index", video.index
@@ -157,7 +161,7 @@ module Invidious::Routes::API::V1::Misc
if format == "html"
response = JSON.parse(response)
- playlist_html = template_mix(response)
+ playlist_html = template_mix(response, listen)
next_video = response["videos"].as_a.select { |video| !video["author"].as_s.empty? }[0]?.try &.["videoId"]
response = {
diff --git a/src/invidious/routes/embed.cr b/src/invidious/routes/embed.cr
index 266f7ba4..00f24159 100644
--- a/src/invidious/routes/embed.cr
+++ b/src/invidious/routes/embed.cr
@@ -157,10 +157,12 @@ module Invidious::Routes::Embed
adaptive_fmts = video.adaptive_fmts
if params.local
- fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
- adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
+ fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
end
+ # Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
+ adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
+
video_streams = video.video_streams
audio_streams = video.audio_streams
diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr
index ca448991..fa5065fb 100644
--- a/src/invidious/routes/feeds.cr
+++ b/src/invidious/routes/feeds.cr
@@ -187,6 +187,7 @@ module Invidious::Routes::Feeds
length_seconds: 0,
premiere_timestamp: nil,
author_verified: false,
+ author_thumbnail: nil,
badges: VideoBadges::None,
})
end
diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr
index 9c6843e9..f2213da4 100644
--- a/src/invidious/routes/playlists.cr
+++ b/src/invidious/routes/playlists.cr
@@ -304,23 +304,6 @@ module Invidious::Routes::Playlists
end
end
- if env.params.query["action_create_playlist"]?
- action = "action_create_playlist"
- elsif env.params.query["action_delete_playlist"]?
- action = "action_delete_playlist"
- elsif env.params.query["action_edit_playlist"]?
- action = "action_edit_playlist"
- elsif env.params.query["action_add_video"]?
- action = "action_add_video"
- video_id = env.params.query["video_id"]
- elsif env.params.query["action_remove_video"]?
- action = "action_remove_video"
- elsif env.params.query["action_move_video_before"]?
- action = "action_move_video_before"
- else
- return env.redirect referer
- end
-
begin
playlist_id = env.params.query["playlist_id"]
playlist = get_playlist(playlist_id).as(InvidiousPlaylist)
@@ -335,12 +318,8 @@ module Invidious::Routes::Playlists
end
end
- email = user.email
-
- case action
- when "action_edit_playlist"
- # TODO: Playlist stub
- when "action_add_video"
+ case action = env.params.query["action"]?
+ when "add_video"
if playlist.index.size >= CONFIG.playlist_length_limit
if redirect
return error_template(400, "Playlist cannot have more than #{CONFIG.playlist_length_limit} videos")
@@ -377,12 +356,14 @@ module Invidious::Routes::Playlists
Invidious::Database::PlaylistVideos.insert(playlist_video)
Invidious::Database::Playlists.update_video_added(playlist_id, playlist_video.index)
- when "action_remove_video"
+ when "remove_video"
index = env.params.query["set_video_id"]
Invidious::Database::PlaylistVideos.delete(index)
Invidious::Database::Playlists.update_video_removed(playlist_id, index)
- when "action_move_video_before"
+ when "move_video_before"
# TODO: Playlist stub
+ when nil
+ return error_json(400, "Missing action")
else
return error_json(400, "Unsupported action #{action}")
end
diff --git a/src/invidious/routes/subscriptions.cr b/src/invidious/routes/subscriptions.cr
index 7f9ec592..1de655d2 100644
--- a/src/invidious/routes/subscriptions.cr
+++ b/src/invidious/routes/subscriptions.cr
@@ -32,24 +32,16 @@ module Invidious::Routes::Subscriptions
end
end
- if env.params.query["action_create_subscription_to_channel"]?.try &.to_i?.try &.== 1
- action = "action_create_subscription_to_channel"
- elsif env.params.query["action_remove_subscriptions"]?.try &.to_i?.try &.== 1
- action = "action_remove_subscriptions"
- else
- return env.redirect referer
- end
-
channel_id = env.params.query["c"]?
channel_id ||= ""
- case action
- when "action_create_subscription_to_channel"
+ case action = env.params.query["action"]?
+ when "create_subscription_to_channel"
if !user.subscriptions.includes? channel_id
get_channel(channel_id)
Invidious::Database::Users.subscribe_channel(user, channel_id)
end
- when "action_remove_subscriptions"
+ when "remove_subscriptions"
Invidious::Database::Users.unsubscribe_channel(user, channel_id)
else
return error_json(400, "Unsupported action #{action}")
diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr
index 26852d06..a8f9f665 100644
--- a/src/invidious/routes/video_playback.cr
+++ b/src/invidious/routes/video_playback.cr
@@ -164,10 +164,13 @@ module Invidious::Routes::VideoPlayback
env.response.headers["Access-Control-Allow-Origin"] = "*"
if location = resp.headers["Location"]?
- location = URI.parse(location)
- location = "#{location.request_target}&host=#{location.host}#{region ? "&region=#{region}" : ""}"
+ url = Invidious::HttpServer::Utils.proxy_video_url(location, region: region)
- env.redirect location
+ if title = query_params["title"]?
+ url = "#{url}&title=#{URI.encode_www_form(title)}"
+ end
+
+ env.redirect url
break
end
diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr
index aabe8dfc..1f384546 100644
--- a/src/invidious/routes/watch.cr
+++ b/src/invidious/routes/watch.cr
@@ -121,10 +121,12 @@ module Invidious::Routes::Watch
adaptive_fmts = video.adaptive_fmts
if params.local
- fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
- adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
+ fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
end
+ # Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
+ adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
+
video_streams = video.video_streams
audio_streams = video.audio_streams
@@ -241,18 +243,10 @@ module Invidious::Routes::Watch
end
end
- if env.params.query["action_mark_watched"]?
- action = "action_mark_watched"
- elsif env.params.query["action_mark_unwatched"]?
- action = "action_mark_unwatched"
- else
- return env.redirect referer
- end
-
- case action
- when "action_mark_watched"
+ case action = env.params.query["action"]?
+ when "mark_watched"
Invidious::Database::Users.mark_watched(user, id)
- when "action_mark_unwatched"
+ when "mark_unwatched"
Invidious::Database::Users.mark_unwatched(user, id)
else
return error_json(400, "Unsupported action #{action}")
diff --git a/src/invidious/search/filters.cr b/src/invidious/search/filters.cr
index bf968734..bc2715cf 100644
--- a/src/invidious/search/filters.cr
+++ b/src/invidious/search/filters.cr
@@ -75,7 +75,7 @@ module Invidious::Search
@type : Type = Type::All,
@duration : Duration = Duration::None,
@features : Features = Features::None,
- @sort : Sort = Sort::Relevance
+ @sort : Sort = Sort::Relevance,
)
end
diff --git a/src/invidious/search/query.cr b/src/invidious/search/query.cr
index c8e8cf7f..94a92e23 100644
--- a/src/invidious/search/query.cr
+++ b/src/invidious/search/query.cr
@@ -47,7 +47,7 @@ module Invidious::Search
def initialize(
params : HTTP::Params,
@type : Type = Type::Regular,
- @region : String? = nil
+ @region : String? = nil,
)
# Get the raw search query string (common to all search types). In
# Regular search mode, also look for the `search_query` URL parameter
diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr
index 533c18d9..007eb666 100644
--- a/src/invidious/user/imports.cr
+++ b/src/invidious/user/imports.cr
@@ -290,42 +290,39 @@ struct Invidious::User
end
def from_newpipe(user : User, body : String) : Bool
- io = IO::Memory.new(body)
+ Compress::Zip::File.open(IO::Memory.new(body), true) do |file|
+ entry = file.entries.find { |file_entry| file_entry.filename == "newpipe.db" }
+ return false if entry.nil?
+ entry.open do |file_io|
+ # Ensure max size of 4MB
+ io_sized = IO::Sized.new(file_io, 0x400000)
- Compress::Zip::File.open(io) do |file|
- file.entries.each do |entry|
- entry.open do |file_io|
- # Ensure max size of 4MB
- io_sized = IO::Sized.new(file_io, 0x400000)
-
- next if entry.filename != "newpipe.db"
-
- tempfile = File.tempfile(".db")
-
- begin
- File.write(tempfile.path, io_sized.gets_to_end)
- rescue
- return false
- end
-
- db = DB.open("sqlite3://" + tempfile.path)
-
- user.watched += db.query_all("SELECT url FROM streams", as: String)
- .map(&.lchop("https://www.youtube.com/watch?v="))
+ begin
+ temp = File.tempfile(".db") do |tempfile|
+ begin
+ File.write(tempfile.path, io_sized.gets_to_end)
+ rescue
+ return false
+ end
- user.watched.uniq!
- Invidious::Database::Users.update_watch_history(user)
+ DB.open("sqlite3://" + tempfile.path) do |db|
+ user.watched += db.query_all("SELECT url FROM streams", as: String)
+ .map(&.lchop("https://www.youtube.com/watch?v="))
- user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String)
- .map(&.lchop("https://www.youtube.com/channel/"))
+ user.watched.uniq!
+ Invidious::Database::Users.update_watch_history(user)
- user.subscriptions.uniq!
- user.subscriptions = get_batch_channels(user.subscriptions)
+ user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String)
+ .map(&.lchop("https://www.youtube.com/channel/"))
- Invidious::Database::Users.update_subscriptions(user)
+ user.subscriptions.uniq!
+ user.subscriptions = get_batch_channels(user.subscriptions)
- db.close
- tempfile.delete
+ Invidious::Database::Users.update_subscriptions(user)
+ end
+ end
+ ensure
+ temp.delete if !temp.nil?
end
end
end
diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr
index ae09e736..962f87bd 100644
--- a/src/invidious/videos.cr
+++ b/src/invidious/videos.cr
@@ -106,7 +106,7 @@ struct Video
if formats = info.dig?("streamingData", "adaptiveFormats")
return formats
.as_a.map(&.as_h)
- .sort_by! { |f| f["width"]?.try &.as_i || 0 }
+ .sort_by! { |f| f["width"]?.try &.as_i || f["audioTrack"]?.try { |a| a["audioIsDefault"]?.try { |v| v.as_bool ? -1 : 0 } } || 0 }
else
return [] of Hash(String, JSON::Any)
end
diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr
index 915c9baf..5ca4bdb2 100644
--- a/src/invidious/videos/parser.cr
+++ b/src/invidious/videos/parser.cr
@@ -36,6 +36,13 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
LOGGER.trace("parse_related_video: Found \"watchNextEndScreenRenderer\" container")
+ if published_time_text = related["publishedTimeText"]?
+ decoded_time = decode_date(published_time_text["simpleText"].to_s)
+ published = decoded_time.to_rfc3339.to_s
+ else
+ published = nil
+ end
+
# TODO: when refactoring video types, make a struct for related videos
# or reuse an existing type, if that fits.
return {
@@ -47,6 +54,7 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
"view_count" => JSON::Any.new(view_count || "0"),
"short_view_count" => JSON::Any.new(short_view_count || "0"),
"author_verified" => JSON::Any.new(author_verified),
+ "published" => JSON::Any.new(published || ""),
}
end
diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr
index a72c2f55..bd0eef59 100644
--- a/src/invidious/videos/storyboard.cr
+++ b/src/invidious/videos/storyboard.cr
@@ -20,7 +20,7 @@ module Invidious::Videos
def initialize(
*, @url, @width, @height, @count, @interval,
- @rows, @columns, @images_count
+ @rows, @columns, @images_count,
)
authority = /(i\d?).ytimg.com/.match!(@url.host.not_nil!)[1]?
diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr
index a84e44bc..1fe8ab7e 100644
--- a/src/invidious/views/channel.ecr
+++ b/src/invidious/views/channel.ecr
@@ -20,7 +20,9 @@
page_nav_html = IV::Frontend::Pagination.nav_ctoken(locale,
base_url: relative_url,
- ctoken: next_continuation
+ ctoken: next_continuation,
+ first_page: continuation.nil?,
+ params: env.params.query,
)
%>
@@ -40,6 +42,8 @@
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
<%- end -%>
+<script src="/js/pagination.js?v=<%= ASSET_COMMIT %>"></script>
+
<link rel="alternate" href="<%= youtube_url %>">
<title><%= author %> - Invidious</title>
<% end %>
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index 6d227cfc..c966a926 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -128,7 +128,7 @@
<div class="top-left-overlay">
<%- if env.get? "show_watched" -%>
- <form data-onsubmit="return_false" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
+ <form data-onsubmit="return_false" action="/watch_ajax?action=mark_watched&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button type="submit" class="pure-button pure-button-secondary low-profile"
data-onclick="mark_watched" data-id="<%= item.id %>">
@@ -138,14 +138,14 @@
<%- end -%>
<%- if plid_form = env.get?("add_playlist_items") -%>
- <%- form_parameters = "action_add_video=1&video_id=#{item.id}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
+ <%- form_parameters = "action=add_video&video_id=#{item.id}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
<form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button type="submit" class="pure-button pure-button-secondary low-profile"
data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>"><i class="icon ion-md-add"></i></button>
</form>
<%- elsif item.is_a?(PlaylistVideo) && (plid_form = env.get?("remove_playlist_items")) -%>
- <%- form_parameters = "action_remove_video=1&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
+ <%- form_parameters = "action=remove_video&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
<form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button type="submit" class="pure-button pure-button-secondary low-profile"
diff --git a/src/invidious/views/components/items_paginated.ecr b/src/invidious/views/components/items_paginated.ecr
index 4534a0a3..f69df3fe 100644
--- a/src/invidious/views/components/items_paginated.ecr
+++ b/src/invidious/views/components/items_paginated.ecr
@@ -8,4 +8,14 @@
<%= page_nav_html %>
+<script id="pagination-data" type="application/json">
+<%=
+{
+ "next_page" => translate(locale, "Next page"),
+ "prev_page" => translate(locale, "Previous page"),
+ "is_rtl" => locale_is_rtl?(locale)
+}.to_pretty_json
+%>
+</script>
+
<script src="/js/watched_indicator.js"></script>
diff --git a/src/invidious/views/components/subscribe_widget.ecr b/src/invidious/views/components/subscribe_widget.ecr
index 05e4e253..3cfcb0eb 100644
--- a/src/invidious/views/components/subscribe_widget.ecr
+++ b/src/invidious/views/components/subscribe_widget.ecr
@@ -1,13 +1,13 @@
<% if user %>
<% if subscriptions.includes? ucid %>
- <form action="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
+ <form action="/subscription_ajax?action=remove_subscriptions&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button data-type="unsubscribe" id="subscribe" class="pure-button pure-button-primary">
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b>
</button>
</form>
<% else %>
- <form action="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
+ <form action="/subscription_ajax?action=create_subscription_to_channel&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button data-type="subscribe" id="subscribe" class="pure-button pure-button-primary">
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b>
diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr
index bda4e1f3..13fe4147 100644
--- a/src/invidious/views/feeds/history.ecr
+++ b/src/invidious/views/feeds/history.ecr
@@ -37,7 +37,7 @@
</a>
<div class="top-left-overlay"><div class="watched">
- <form data-onsubmit="return_false" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post">
+ <form data-onsubmit="return_false" action="/watch_ajax?action=mark_unwatched&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<button type="submit" class="pure-button pure-button-secondary low-profile"
data-onclick="mark_unwatched" data-id="<%= item %>"><i class="icon ion-md-trash"></i></button>
diff --git a/src/invidious/views/user/subscription_manager.ecr b/src/invidious/views/user/subscription_manager.ecr
index c9801f09..d566e228 100644
--- a/src/invidious/views/user/subscription_manager.ecr
+++ b/src/invidious/views/user/subscription_manager.ecr
@@ -37,7 +37,7 @@
<div class="pure-u-2-5"></div>
<div class="pure-u-1-5" style="text-align:right">
<h3 style="padding-right:0.5em">
- <form data-onsubmit="return_false" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
+ <form data-onsubmit="return_false" action="/subscription_ajax?action=remove_subscriptions&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input style="all:unset" type="submit" data-onclick="remove_subscription" data-ucid="<%= channel.id %>" value="<%= translate(locale, "unsubscribe") %>">
</form>
diff --git a/src/invidious/views/user/token_manager.ecr b/src/invidious/views/user/token_manager.ecr
index a73fa048..8431deb0 100644
--- a/src/invidious/views/user/token_manager.ecr
+++ b/src/invidious/views/user/token_manager.ecr
@@ -29,7 +29,7 @@
</div>
<div class="pure-u-1-5" style="text-align:right">
<h3 style="padding-right:0.5em">
- <form data-onsubmit="return_false" action="/token_ajax?action_revoke_token=1&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post">
+ <form data-onsubmit="return_false" action="/token_ajax?action=revoke_token&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input style="all:unset" type="submit" data-onclick="revoke_token" data-session="<%= token[:session] %>" value="<%= translate(locale, "revoke") %>">
</form>
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr
index 45c58a16..6f9ced6f 100644
--- a/src/invidious/views/watch.ecr
+++ b/src/invidious/views/watch.ecr
@@ -158,7 +158,7 @@ we're going to need to do it here in order to allow for translations.
<% if user %>
<% playlists = Invidious::Database::Playlists.select_user_created_playlists(user.email) %>
<% if !playlists.empty? %>
- <form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax" method="post" target="_blank">
+ <form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax?action=add_video" method="post" target="_blank">
<div class="pure-control-group">
<label for="playlist_id"><%= translate(locale, "Add to playlist: ") %></label>
<select style="width:100%" name="playlist_id" id="playlist_id">
@@ -169,7 +169,6 @@ we're going to need to do it here in order to allow for translations.
</div>
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
- <input type="hidden" name="action_add_video" value="1">
<input type="hidden" name="video_id" value="<%= video.id %>">
<button data-onclick="add_playlist_video" data-id="<%= video.id %>" type="submit" class="pure-button pure-button-primary">
<b><%= translate(locale, "Add to playlist") %></b>
diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr
index 2631b62a..edd7bf1b 100644
--- a/src/invidious/yt_backend/extractors.cr
+++ b/src/invidious/yt_backend/extractors.cr
@@ -67,6 +67,8 @@ private module Parsers
author_id = author_fallback.id
end
+ author_thumbnail = item_contents.dig?("channelThumbnailSupportedRenderers", "channelThumbnailWithLinkRenderer", "thumbnail", "thumbnails", 0, "url").try &.as_s
+
author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
# For live videos (and possibly recently premiered videos) there is no published information.
@@ -148,6 +150,7 @@ private module Parsers
length_seconds: length_seconds,
premiere_timestamp: premiere_timestamp,
author_verified: author_verified,
+ author_thumbnail: author_thumbnail,
badges: badges,
})
end
@@ -579,6 +582,7 @@ private module Parsers
length_seconds: duration,
premiere_timestamp: Time.unix(0),
author_verified: false,
+ author_thumbnail: nil,
badges: VideoBadges::None,
})
end
@@ -708,6 +712,7 @@ private module Parsers
length_seconds: duration,
premiere_timestamp: Time.unix(0),
author_verified: false,
+ author_thumbnail: nil,
badges: VideoBadges::None,
})
end
@@ -1024,7 +1029,7 @@ end
def extract_items(
initial_data : InitialData,
author_fallback : String? = nil,
- author_id_fallback : String? = nil
+ author_id_fallback : String? = nil,
) : {Array(SearchItem), String?}
items = [] of SearchItem
continuation = nil
diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr
index 8f5aa61d..ec080d8c 100644
--- a/src/invidious/yt_backend/youtube_api.cr
+++ b/src/invidious/yt_backend/youtube_api.cr
@@ -211,7 +211,7 @@ module YoutubeAPI
def initialize(
*,
@client_type = ClientType::Web,
- @region = "US"
+ @region = "US",
)
end
@@ -370,7 +370,7 @@ module YoutubeAPI
browse_id : String,
*, # Force the following parameters to be passed by name
params : String,
- client_config : ClientConfig | Nil = nil
+ client_config : ClientConfig | Nil = nil,
)
# JSON Request data, required by the API
data = {
@@ -464,7 +464,7 @@ module YoutubeAPI
video_id : String,
*, # Force the following parameters to be passed by name
params : String,
- client_config : ClientConfig | Nil = nil
+ client_config : ClientConfig | Nil = nil,
)
# Playback context, separate because it can be different between clients
playback_ctx = {
@@ -557,7 +557,7 @@ module YoutubeAPI
def search(
search_query : String,
params : String,
- client_config : ClientConfig | Nil = nil
+ client_config : ClientConfig | Nil = nil,
)
# JSON Request data, required by the API
data = {
@@ -583,7 +583,7 @@ module YoutubeAPI
def get_transcript(
params : String,
- client_config : ClientConfig | Nil = nil
+ client_config : ClientConfig | Nil = nil,
) : Hash(String, JSON::Any)
data = {
"context" => self.make_context(client_config),
@@ -605,7 +605,7 @@ module YoutubeAPI
def _post_json(
endpoint : String,
data : Hash,
- client_config : ClientConfig | Nil
+ client_config : ClientConfig | Nil,
) : Hash(String, JSON::Any)
# Use the default client config if nil is passed
client_config ||= DEFAULT_CLIENT_CONFIG