From f54fbd057ebb8d1e9631782b6d182b403598648f Mon Sep 17 00:00:00 2001 From: psvenk <45520974+psvenk@users.noreply.github.com> Date: Thu, 15 Aug 2019 16:29:55 +0000 Subject: [PATCH 01/10] Add prefers-color-scheme support (#601) * Add prefers-color-scheme support This should fix . The cookie storage format has been changed from boolean ("true"/"false") to tri-state ("dark"/"light"/""), so that users without a cookie set will get dark mode if they have enabled the dark theme in their operating system. The code for handling the cookie state, along with the user's operating system theme, has been factored out into a new function `update_mode`, which is called both at window load and at the "storage" event listener, because the "storage" event listener is only trigerred when a change is made to the localStorage from another tab/window (for more info - see ). --- assets/js/themes.js | 37 ++++++++++--- locales/ar.json | 4 ++ locales/de.json | 4 ++ locales/el.json | 4 ++ locales/en-US.json | 4 ++ locales/eo.json | 4 ++ locales/es.json | 4 ++ locales/eu.json | 4 ++ locales/fr.json | 4 ++ locales/is.json | 4 ++ locales/it.json | 4 ++ locales/nb_NO.json | 4 ++ locales/nl.json | 4 ++ locales/pl.json | 4 ++ locales/ru.json | 4 ++ locales/uk.json | 4 ++ locales/zh-CN.json | 4 ++ src/invidious.cr | 30 ++++++++--- src/invidious/helpers/helpers.cr | 74 ++++++++++++++++++++++++-- src/invidious/helpers/patch_mapping.cr | 2 +- src/invidious/helpers/utils.cr | 13 +++++ src/invidious/users.cr | 62 ++------------------- src/invidious/views/preferences.ecr | 8 ++- src/invidious/views/template.ecr | 9 ++-- 24 files changed, 215 insertions(+), 84 deletions(-) diff --git a/assets/js/themes.js b/assets/js/themes.js index 683aea397..90a05c36e 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -1,8 +1,8 @@ -var toggle_theme = document.getElementById('toggle_theme') +var toggle_theme = document.getElementById('toggle_theme'); toggle_theme.href = 'javascript:void(0);'; toggle_theme.addEventListener('click', function () { - var dark_mode = document.getElementById('dark_theme').media == 'none'; + var dark_mode = document.getElementById('dark_theme').media === 'none'; var url = '/toggle_theme?redirect=false'; var xhr = new XMLHttpRequest(); @@ -11,19 +11,24 @@ toggle_theme.addEventListener('click', function () { xhr.open('GET', url, true); set_mode(dark_mode); - localStorage.setItem('dark_mode', dark_mode); + window.localStorage.setItem('dark_mode', dark_mode ? 'dark' : 'light'); xhr.send(); }); window.addEventListener('storage', function (e) { - if (e.key == 'dark_mode') { - var dark_mode = e.newValue === 'true'; - set_mode(dark_mode); + if (e.key === 'dark_mode') { + update_mode(e.newValue); } }); -function set_mode(bool) { +window.addEventListener('load', function () { + window.localStorage.setItem('dark_mode', document.getElementById('dark_mode_pref').textContent); + // Update localStorage if dark mode preference changed on preferences page + update_mode(window.localStorage.dark_mode); +}); + +function set_mode (bool) { document.getElementById('dark_theme').media = !bool ? 'none' : ''; document.getElementById('light_theme').media = bool ? 'none' : ''; @@ -33,3 +38,21 @@ function set_mode(bool) { toggle_theme.children[0].setAttribute('class', 'icon ion-ios-moon'); } } + +function update_mode (mode) { + if (mode === 'true' /* for backwards compatibility */ || mode === 'dark') { + // If preference for dark mode indicated + set_mode(true); + } + else if (mode === 'false' /* for backwards compaibility */ || mode === 'light') { + // If preference for light mode indicated + set_mode(false); + } + else if (document.getElementById('dark_mode_pref').textContent === '' && window.matchMedia('(prefers-color-scheme: dark)').matches) { + // If no preference indicated here and no preference indicated on the preferences page (backend), but the browser tells us that the operating system has a dark theme + set_mode(true); + } + // else do nothing, falling back to the mode defined by the `dark_mode` preference on the preferences page (backend) +} + + diff --git a/locales/ar.json b/locales/ar.json index e3716008f..f2ed450c2 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -68,7 +68,11 @@ "Show related videos: ": "عرض مقاطع الفيديو ذات الصلة؟", "Show annotations by default: ": "عرض الملاحظات فى الفيديو تلقائيا ؟", "Visual preferences": "التفضيلات المرئية", + "Player style: ": "", "Dark mode: ": "الوضع الليلى: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "الوضع الخفيف: ", "Subscription preferences": "تفضيلات الإشتراك", "Show annotations by default for subscribed channels: ": "عرض الملاحظات فى الفيديوهات تلقائيا فى القنوات المشترك بها فقط ؟", diff --git a/locales/de.json b/locales/de.json index 33edb706c..e0ef9d679 100644 --- a/locales/de.json +++ b/locales/de.json @@ -68,7 +68,11 @@ "Show related videos: ": "Ähnliche Videos anzeigen? ", "Show annotations by default: ": "Standardmäßig Anmerkungen anzeigen? ", "Visual preferences": "Anzeigeeinstellungen", + "Player style: ": "", "Dark mode: ": "Nachtmodus: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Schlanker Modus: ", "Subscription preferences": "Abonnementeinstellungen", "Show annotations by default for subscribed channels: ": "Anmerkungen für abonnierte Kanäle standardmäßig anzeigen? ", diff --git a/locales/el.json b/locales/el.json index 03d255335..32f154a15 100644 --- a/locales/el.json +++ b/locales/el.json @@ -74,7 +74,11 @@ "Show related videos: ": "Προβολή σχετικών βίντεο; ", "Show annotations by default: ": "Αυτόματη προβολή σημειώσεων; :", "Visual preferences": "Προτιμήσεις εμφάνισης", + "Player style: ": "", "Dark mode: ": "Σκοτεινή λειτουργία: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Ελαφριά λειτουργία: ", "Subscription preferences": "Προτιμήσεις συνδρομών", "Show annotations by default for subscribed channels: ": "Προβολή σημειώσεων μόνο για κανάλια στα οποία είστε συνδρομητής; ", diff --git a/locales/en-US.json b/locales/en-US.json index 16301b1d9..580d9ead5 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -74,7 +74,11 @@ "Show related videos: ": "Show related videos: ", "Show annotations by default: ": "Show annotations by default: ", "Visual preferences": "Visual preferences", + "Player style: ": "", "Dark mode: ": "Dark mode: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Thin mode: ", "Subscription preferences": "Subscription preferences", "Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ", diff --git a/locales/eo.json b/locales/eo.json index fe85edf21..2c22af59b 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -68,7 +68,11 @@ "Show related videos: ": "Ĉu montri rilatajn videojn? ", "Show annotations by default: ": "Ĉu montri prinotojn defaŭlte? ", "Visual preferences": "Vidaj preferoj", + "Player style: ": "", "Dark mode: ": "Malhela reĝimo: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Maldika reĝimo: ", "Subscription preferences": "Abonaj agordoj", "Show annotations by default for subscribed channels: ": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ", diff --git a/locales/es.json b/locales/es.json index fdbf2fb25..5860b882f 100644 --- a/locales/es.json +++ b/locales/es.json @@ -68,7 +68,11 @@ "Show related videos: ": "¿Mostrar vídeos relacionados? ", "Show annotations by default: ": "¿Mostrar anotaciones por defecto? ", "Visual preferences": "Preferencias visuales", + "Player style: ": "", "Dark mode: ": "Modo oscuro: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Modo compacto: ", "Subscription preferences": "Preferencias de la suscripción", "Show annotations by default for subscribed channels: ": "¿Mostrar anotaciones por defecto para los canales suscritos? ", diff --git a/locales/eu.json b/locales/eu.json index 5a756154d..cbdbbefc0 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -68,7 +68,11 @@ "Show related videos: ": "", "Show annotations by default: ": "", "Visual preferences": "", + "Player style: ": "", "Dark mode: ": "", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "", "Subscription preferences": "", "Show annotations by default for subscribed channels: ": "", diff --git a/locales/fr.json b/locales/fr.json index 37a773f17..af561a0cc 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -68,7 +68,11 @@ "Show related videos: ": "Voir les vidéos liées : ", "Show annotations by default: ": "Voir les annotations par défaut : ", "Visual preferences": "Préférences du site", + "Player style: ": "", "Dark mode: ": "Mode Sombre : ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Mode Simplifié : ", "Subscription preferences": "Préférences de la page d'abonnements", "Show annotations by default for subscribed channels: ": "Voir les annotations par défaut sur les chaînes suivies : ", diff --git a/locales/is.json b/locales/is.json index 43ba26e9c..808063c42 100644 --- a/locales/is.json +++ b/locales/is.json @@ -68,7 +68,11 @@ "Show related videos: ": "Sýna tengd myndbönd? ", "Show annotations by default: ": "Á að sýna glósur sjálfgefið? ", "Visual preferences": "Sjónrænar stillingar", + "Player style: ": "", "Dark mode: ": "Myrkur ham: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Þunnt ham: ", "Subscription preferences": "Áskriftarstillingar", "Show annotations by default for subscribed channels: ": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ", diff --git a/locales/it.json b/locales/it.json index bf028c91a..7f532d0d3 100644 --- a/locales/it.json +++ b/locales/it.json @@ -68,7 +68,11 @@ "Show related videos: ": "Mostra video correlati? ", "Show annotations by default: ": "Mostra le annotazioni per impostazione predefinita? ", "Visual preferences": "Preferenze grafiche", + "Player style: ": "", "Dark mode: ": "Tema scuro: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Modalità per connessioni lente: ", "Subscription preferences": "Preferenze iscrizioni", "Show annotations by default for subscribed channels: ": "", diff --git a/locales/nb_NO.json b/locales/nb_NO.json index 589512d45..f50e22900 100644 --- a/locales/nb_NO.json +++ b/locales/nb_NO.json @@ -68,7 +68,11 @@ "Show related videos: ": "Vis relaterte videoer? ", "Show annotations by default: ": "Vis merknader som forvalg? ", "Visual preferences": "Visuelle innstillinger", + "Player style: ": "", "Dark mode: ": "Mørk drakt: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Tynt modus: ", "Subscription preferences": "Abonnementsinnstillinger", "Show annotations by default for subscribed channels: ": "Vis merknader som forvalg for kanaler det abonneres på? ", diff --git a/locales/nl.json b/locales/nl.json index b24b13b84..3e2c6c642 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -68,7 +68,11 @@ "Show related videos: ": "Gerelateerde video's tonen? ", "Show annotations by default: ": "Standaard annotaties tonen? ", "Visual preferences": "Visuele instellingen", + "Player style: ": "", "Dark mode: ": "Donkere modus: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Smalle modus: ", "Subscription preferences": "Abonnementsinstellingen", "Show annotations by default for subscribed channels: ": "Standaard annotaties tonen voor geabonneerde kanalen? ", diff --git a/locales/pl.json b/locales/pl.json index 550e664b1..1e3a2068c 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -68,7 +68,11 @@ "Show related videos: ": "Pokaż powiązane filmy? ", "Show annotations by default: ": "", "Visual preferences": "Preferencje Wizualne", + "Player style: ": "", "Dark mode: ": "Ciemny motyw: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Tryb minimalny: ", "Subscription preferences": "Preferencje subskrybcji", "Show annotations by default for subscribed channels: ": "", diff --git a/locales/ru.json b/locales/ru.json index f9cc1e2d9..90aa4a3b5 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -68,7 +68,11 @@ "Show related videos: ": "Показывать похожие видео? ", "Show annotations by default: ": "Всегда показывать аннотации? ", "Visual preferences": "Настройки сайта", + "Player style: ": "", "Dark mode: ": "Тёмное оформление: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Облегчённое оформление: ", "Subscription preferences": "Настройки подписок", "Show annotations by default for subscribed channels: ": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ", diff --git a/locales/uk.json b/locales/uk.json index 95e5798df..e537008cb 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -68,7 +68,11 @@ "Show related videos: ": "Показувати схожі відео? ", "Show annotations by default: ": "Завжди показувати анотації? ", "Visual preferences": "Налаштування сайту", + "Player style: ": "", "Dark mode: ": "Темне оформлення: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Полегшене оформлення: ", "Subscription preferences": "Налаштування підписок", "Show annotations by default for subscribed channels: ": "Завжди показувати анотації у відео каналів, на які ви підписані? ", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 0a3f53d97..23617d049 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -68,7 +68,11 @@ "Show related videos: ": "显示相关视频?", "Show annotations by default: ": "默认显示视频注释?", "Visual preferences": "视觉选项", + "Player style: ": "", "Dark mode: ": "暗色模式:", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "窄页模式:", "Subscription preferences": "订阅设置", "Show annotations by default for subscribed channels: ": "在订阅频道的视频默认显示注释?", diff --git a/src/invidious.cr b/src/invidious.cr index 16695c5fd..712a408fb 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -267,8 +267,7 @@ before_all do |env| end end - dark_mode = env.params.query["dark_mode"]? || preferences.dark_mode.to_s - dark_mode = dark_mode == "true" + dark_mode = convert_theme(env.params.query["dark_mode"]?) || preferences.dark_mode.to_s thin_mode = env.params.query["thin_mode"]? || preferences.thin_mode.to_s thin_mode = thin_mode == "true" @@ -1528,8 +1527,7 @@ post "/preferences" do |env| locale ||= CONFIG.default_user_preferences.locale dark_mode = env.params.body["dark_mode"]?.try &.as(String) - dark_mode ||= "off" - dark_mode = dark_mode == "on" + dark_mode ||= CONFIG.default_user_preferences.dark_mode thin_mode = env.params.body["thin_mode"]?.try &.as(String) thin_mode ||= "off" @@ -1553,6 +1551,7 @@ post "/preferences" do |env| notifications_only ||= "off" notifications_only = notifications_only == "on" + # Convert to JSON and back again to take advantage of converters used for compatability preferences = Preferences.from_json({ annotations: annotations, annotations_subscribed: annotations_subscribed, @@ -1648,12 +1647,27 @@ get "/toggle_theme" do |env| if user = env.get? "user" user = user.as(User) preferences = user.preferences - preferences.dark_mode = !preferences.dark_mode - PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences.to_json, user.email) + case preferences.dark_mode + when "dark" + preferences.dark_mode = "light" + else + preferences.dark_mode = "dark" + end + + preferences = preferences.to_json + + PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email) else preferences = env.get("preferences").as(Preferences) - preferences.dark_mode = !preferences.dark_mode + + case preferences.dark_mode + when "dark" + preferences.dark_mode = "light" + else + preferences.dark_mode = "dark" + end + preferences = preferences.to_json if Kemal.config.ssl || config.https_only @@ -2026,7 +2040,7 @@ post "/data_control" do |env| env.response.puts %() env.response.puts %() env.response.puts %() - if env.get("preferences").as(Preferences).dark_mode + if env.get("preferences").as(Preferences).dark_mode == "dark" env.response.puts %() else env.response.puts %() diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index ce0ded325..03c1654cf 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -24,6 +24,27 @@ end struct ConfigPreferences module StringToArray + def self.to_json(value : Array(String), json : JSON::Builder) + json.array do + value.each do |element| + json.string element + end + end + end + + def self.from_json(value : JSON::PullParser) : Array(String) + begin + result = [] of String + value.read_array do + result << HTML.escape(value.read_string[0, 100]) + end + rescue ex + result = [HTML.escape(value.read_string[0, 100]), ""] + end + + result + end + def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder) yaml.sequence do value.each do |element| @@ -44,11 +65,11 @@ struct ConfigPreferences node.raise "Expected scalar, not #{item.class}" end - result << item.value + result << HTML.escape(item.value[0, 100]) end rescue ex if node.is_a?(YAML::Nodes::Scalar) - result = [node.value, ""] + result = [HTML.escape(node.value[0, 100]), ""] else result = ["", ""] end @@ -58,6 +79,53 @@ struct ConfigPreferences end end + module BoolToString + def self.to_json(value : String, json : JSON::Builder) + json.string value + end + + def self.from_json(value : JSON::PullParser) : String + begin + result = value.read_string + + if result.empty? + CONFIG.default_user_preferences.dark_mode + else + result + end + rescue ex + result = value.read_bool + + if result + "dark" + else + "light" + end + end + end + + def self.to_yaml(value : String, yaml : YAML::Nodes::Builder) + yaml.scalar value + end + + def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String + unless node.is_a?(YAML::Nodes::Scalar) + node.raise "Expected sequence, not #{node.class}" + end + + case node.value + when "true" + "dark" + when "false" + "light" + when "" + CONFIG.default_user_preferences.dark_mode + else + node.value + end + end + end + yaml_mapping({ annotations: {type: Bool, default: false}, annotations_subscribed: {type: Bool, default: false}, @@ -66,7 +134,7 @@ struct ConfigPreferences comments: {type: Array(String), default: ["youtube", ""], converter: StringToArray}, continue: {type: Bool, default: false}, continue_autoplay: {type: Bool, default: true}, - dark_mode: {type: Bool, default: false}, + dark_mode: {type: String, default: "", converter: BoolToString}, latest_only: {type: Bool, default: false}, listen: {type: Bool, default: false}, local: {type: Bool, default: false}, diff --git a/src/invidious/helpers/patch_mapping.cr b/src/invidious/helpers/patch_mapping.cr index 8360caa61..e138aa1c9 100644 --- a/src/invidious/helpers/patch_mapping.cr +++ b/src/invidious/helpers/patch_mapping.cr @@ -4,7 +4,7 @@ def Object.from_json(string_or_io, default) : self new parser, default end -# Adds configurable 'default' to +# Adds configurable 'default' macro patched_json_mapping(_properties_, strict = false) {% for key, value in _properties_ %} {% _properties_[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %} diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 69aae839d..b39f65c59 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -356,3 +356,16 @@ def parse_range(range) return 0_i64, nil end + +def convert_theme(theme) + case theme + when "true" + "dark" + when "false" + "light" + when "", nil + nil + else + theme + end +end diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 35d8a49e1..8bd82bf14 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -31,62 +31,6 @@ struct User end struct Preferences - module StringToArray - def self.to_json(value : Array(String), json : JSON::Builder) - json.array do - value.each do |element| - json.string element - end - end - end - - def self.from_json(value : JSON::PullParser) : Array(String) - begin - result = [] of String - value.read_array do - result << HTML.escape(value.read_string[0, 100]) - end - rescue ex - result = [HTML.escape(value.read_string[0, 100]), ""] - end - - result - end - - def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder) - yaml.sequence do - value.each do |element| - yaml.scalar element - end - end - end - - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Array(String) - begin - unless node.is_a?(YAML::Nodes::Sequence) - node.raise "Expected sequence, not #{node.class}" - end - - result = [] of String - node.nodes.each do |item| - unless item.is_a?(YAML::Nodes::Scalar) - node.raise "Expected scalar, not #{item.class}" - end - - result << HTML.escape(item.value[0, 100]) - end - rescue ex - if node.is_a?(YAML::Nodes::Scalar) - result = [HTML.escape(node.value[0, 100]), ""] - else - result = ["", ""] - end - end - - result - end - end - module ProcessString def self.to_json(value : String, json : JSON::Builder) json.string value @@ -127,11 +71,11 @@ struct Preferences annotations: {type: Bool, default: CONFIG.default_user_preferences.annotations}, annotations_subscribed: {type: Bool, default: CONFIG.default_user_preferences.annotations_subscribed}, autoplay: {type: Bool, default: CONFIG.default_user_preferences.autoplay}, - captions: {type: Array(String), default: CONFIG.default_user_preferences.captions, converter: StringToArray}, - comments: {type: Array(String), default: CONFIG.default_user_preferences.comments, converter: StringToArray}, + captions: {type: Array(String), default: CONFIG.default_user_preferences.captions, converter: ConfigPreferences::StringToArray}, + comments: {type: Array(String), default: CONFIG.default_user_preferences.comments, converter: ConfigPreferences::StringToArray}, continue: {type: Bool, default: CONFIG.default_user_preferences.continue}, continue_autoplay: {type: Bool, default: CONFIG.default_user_preferences.continue_autoplay}, - dark_mode: {type: Bool, default: CONFIG.default_user_preferences.dark_mode}, + dark_mode: {type: String, default: CONFIG.default_user_preferences.dark_mode, converter: ConfigPreferences::BoolToString}, latest_only: {type: Bool, default: CONFIG.default_user_preferences.latest_only}, listen: {type: Bool, default: CONFIG.default_user_preferences.listen}, local: {type: Bool, default: CONFIG.default_user_preferences.local}, diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index b04bcd4d4..6ea01fba9 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -122,8 +122,12 @@ function update_value(element) {
- - checked<% end %>> + +
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 6272d2be7..8d8cec885 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -18,13 +18,14 @@ - media="none"<% end %>> - media="none"<% end %>> + media="none"<% end %>> + media="none"<% end %>> <% locale = LOCALES[env.get("preferences").as(Preferences).locale]? %> +
@@ -43,7 +44,7 @@ <% if env.get? "user" %>
" class="pure-menu-heading"> - <% if env.get("preferences").as(Preferences).dark_mode %> + <% if env.get("preferences").as(Preferences).dark_mode == "dark" %> <% else %> @@ -76,7 +77,7 @@ <% else %>
" class="pure-menu-heading"> - <% if env.get("preferences").as(Preferences).dark_mode %> + <% if env.get("preferences").as(Preferences).dark_mode == "dark" %> <% else %> From a19cdb5e7251cb5b51d08e0c844119d8267440b3 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 16 Aug 2019 15:46:37 -0500 Subject: [PATCH 02/10] Fix season playlists --- src/invidious.cr | 40 +++++++++++++++++++++++-- src/invidious/helpers/helpers.cr | 30 +++++++------------ src/invidious/search.cr | 14 ++++----- src/invidious/views/components/item.ecr | 4 +-- 4 files changed, 57 insertions(+), 31 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 712a408fb..21d6544de 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -3047,8 +3047,7 @@ get "/channel/:ucid" do |env| item.author end end - items.select! { |item| item.responds_to?(:thumbnail_id) && item.thumbnail_id } - items = items.map { |item| item.as(SearchPlaylist) } + items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) } items.each { |item| item.author = "" } else sort_options = {"newest", "oldest", "popular"} @@ -5086,6 +5085,43 @@ get "/sb/:id/:storyboard/:index" do |env| end end +get "/s_p/:id/:name" do |env| + id = env.params.url["id"] + name = env.params.url["name"] + + host = "https://i9.ytimg.com" + client = make_client(URI.parse(host)) + url = env.request.resource + + headers = HTTP::Headers.new + REQUEST_HEADERS_WHITELIST.each do |header| + if env.request.headers[header]? + headers[header] = env.request.headers[header] + end + end + + begin + client.get(url, headers) do |response| + env.response.status_code = response.status_code + response.headers.each do |key, value| + if !RESPONSE_HEADERS_BLACKLIST.includes? key + env.response.headers[key] = value + end + end + + env.response.headers["Access-Control-Allow-Origin"] = "*" + + if response.status_code >= 300 && response.status_code != 404 + env.response.headers.delete("Transfer-Encoding") + break + end + + proxy_file(response, env) + end + rescue ex + end +end + get "/vi/:id/:name" do |env| id = env.params.url["id"] name = env.params.url["name"] diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 03c1654cf..e018e5670 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -312,8 +312,7 @@ end def extract_videos(nodeset, ucid = nil, author_name = nil) videos = extract_items(nodeset, ucid, author_name) - videos.select! { |item| !item.is_a?(SearchChannel | SearchPlaylist) } - videos.map { |video| video.as(SearchVideo) } + videos.select { |item| item.is_a?(SearchVideo) }.map { |video| video.as(SearchVideo) } end def extract_items(nodeset, ucid = nil, author_name = nil) @@ -361,14 +360,14 @@ def extract_items(nodeset, ucid = nil, author_name = nil) anchor = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/a)) end - video_count = node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b)) + video_count = node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b)) || + node.xpath_node(%q(.//span[@class="formatted-video-count-label"])) if video_count video_count = video_count.content if video_count == "50+" author = "YouTube" author_id = "UC-9-kyTW8ZkZNDHQJ6FgpwQ" - video_count = video_count.rchop("+") end video_count = video_count.gsub(/\D/, "").to_i? @@ -400,11 +399,6 @@ def extract_items(nodeset, ucid = nil, author_name = nil) playlist_thumbnail = node.xpath_node(%q(.//div/span/img)).try &.["data-thumb"]? playlist_thumbnail ||= node.xpath_node(%q(.//div/span/img)).try &.["src"] - if !playlist_thumbnail || playlist_thumbnail.empty? - thumbnail_id = videos[0]?.try &.id - else - thumbnail_id = playlist_thumbnail.match(/\/vi\/(?[a-zA-Z0-9_-]{11})\/\w+\.jpg/).try &.["video_id"] - end items << SearchPlaylist.new( title, @@ -413,7 +407,7 @@ def extract_items(nodeset, ucid = nil, author_name = nil) author_id, video_count, videos, - thumbnail_id + playlist_thumbnail ) when .includes? "yt-lockup-channel" author = title.strip @@ -586,15 +580,11 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) playlist_thumbnail = child_node.xpath_node(%q(.//span/img)).try &.["data-thumb"]? playlist_thumbnail ||= child_node.xpath_node(%q(.//span/img)).try &.["src"] - if !playlist_thumbnail || playlist_thumbnail.empty? - thumbnail_id = videos[0]?.try &.id - else - thumbnail_id = playlist_thumbnail.match(/\/vi\/(?[a-zA-Z0-9_-]{11})\/\w+\.jpg/).try &.["video_id"] - end - video_count_label = child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"])) - if video_count_label - video_count = video_count_label.content.gsub(/\D/, "").to_i? + video_count = child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b)) || + child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"])) + if video_count + video_count = video_count.content.gsub(/\D/, "").to_i? end video_count ||= 50 @@ -605,7 +595,7 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) ucid, video_count, Array(SearchPlaylistVideo).new, - thumbnail_id + playlist_thumbnail ) end end @@ -620,7 +610,7 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) ucid, videos.size, videos, - videos[0].try &.id + "/vi/#{videos[0].id}/mqdefault.jpg" ) end end diff --git a/src/invidious/search.cr b/src/invidious/search.cr index 1d4805bf5..784e38973 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -152,13 +152,13 @@ struct SearchPlaylist end db_mapping({ - title: String, - id: String, - author: String, - ucid: String, - video_count: Int32, - videos: Array(SearchPlaylistVideo), - thumbnail_id: String?, + title: String, + id: String, + author: String, + ucid: String, + video_count: Int32, + videos: Array(SearchPlaylistVideo), + thumbnail: String?, }) end diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 28e700581..71ae70df7 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -15,7 +15,7 @@
<%= item.description_html %>
<% when SearchPlaylist %> <% if item.id.starts_with? "RD" %> - <% url = "/mix?list=#{item.id}&continuation=#{item.thumbnail_id}" %> + <% url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").full_path.split("/")[2]}" %> <% else %> <% url = "/playlist?list=#{item.id}" %> <% end %> @@ -23,7 +23,7 @@
<% if !env.get("preferences").as(Preferences).thin_mode %>
- + "/>

<%= number_with_separator(item.video_count) %> videos

<% end %> From 7eaac995bd549b03f53930fbbfc5d77e2b051362 Mon Sep 17 00:00:00 2001 From: Dragnucs Date: Fri, 16 Aug 2019 20:59:05 +0000 Subject: [PATCH 03/10] Change font family to better native selection (#679) --- assets/css/default.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 4b81d9f5e..451fbc8ce 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -1,3 +1,8 @@ +html, +body { + font-family: BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif; +} + .deleted { background-color: rgb(255, 0, 0, 0.5); } @@ -113,7 +118,6 @@ img.thumbnail { border-radius: 2px; padding: 2px; font-size: 16px; - font-family: sans-serif; right: 0.25em; bottom: -0.75em; } @@ -126,7 +130,6 @@ img.thumbnail { border-radius: 2px; padding: 4px 8px 4px 8px; font-size: 16px; - font-family: sans-serif; left: 0.2em; top: -0.7em; } From e6b4e1268945777c5d07dfca4362a1af23f6d970 Mon Sep 17 00:00:00 2001 From: leonklingele <5585491+leonklingele@users.noreply.github.com> Date: Fri, 16 Aug 2019 23:01:14 +0200 Subject: [PATCH 04/10] js: add support for keydown events (#678) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * js: add support for keydown events This will modify the player behavior even if the player element is unfocused. Based on the YouTube key bindings, allow to - toggle playback with space and 'k' key - increase and decrease player volume with up / down arrow key - mute and unmute player with 'm' key - jump forwards and backwards by 5 seconds with right / left arrow key - jump forwards and backwards by 10 seconds with 'l' / 'j' key - set video progress with number keys 0–9 - toggle captions with 'c' key - toggle fullscreen mode with 'f' key - play next video with 'N' key - increase and decrease playback speed with '>' / '<' key * js: remove unused dependency 'videojs.hotkeys.min.js' Support for controlling the player volume by scrolling over it is still retained by copying over the relevant code part from the aforementioned library. --- assets/js/player.js | 332 ++++++++++++++---- assets/js/videojs.hotkeys.min.js | 2 - assets/js/watch.js | 40 ++- .../views/components/player_sources.ecr | 1 - src/invidious/views/licenses.ecr | 14 - 5 files changed, 291 insertions(+), 98 deletions(-) delete mode 100644 assets/js/videojs.hotkeys.min.js diff --git a/assets/js/player.js b/assets/js/player.js index 25cbb18b3..4a61258cc 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -38,69 +38,7 @@ var shareOptions = { embedCode: "" } -var player = videojs('player', options, function () { - this.hotkeys({ - volumeStep: 0.1, - seekStep: 5, - enableModifiersForNumbers: false, - enableHoverScroll: true, - customKeys: { - // Toggle play with K Key - play: { - key: function (e) { - return e.which === 75; - }, - handler: function (player, options, e) { - if (player.paused()) { - player.play(); - } else { - player.pause(); - } - } - }, - // Go backward 10 seconds - backward: { - key: function (e) { - return e.which === 74; - }, - handler: function (player, options, e) { - player.currentTime(player.currentTime() - 10); - } - }, - // Go forward 10 seconds - forward: { - key: function (e) { - return e.which === 76; - }, - handler: function (player, options, e) { - player.currentTime(player.currentTime() + 10); - } - }, - // Increase speed - increase_speed: { - key: function (e) { - return (e.which === 190 && e.shiftKey); - }, - handler: function (player, _, e) { - size = options.playbackRates.length; - index = options.playbackRates.indexOf(player.playbackRate()); - player.playbackRate(options.playbackRates[(index + 1) % size]); - } - }, - // Decrease speed - decrease_speed: { - key: function (e) { - return (e.which === 188 && e.shiftKey); - }, - handler: function (player, _, e) { - size = options.playbackRates.length; - index = options.playbackRates.indexOf(player.playbackRate()); - player.playbackRate(options.playbackRates[(size + index - 1) % size]); - } - } - } - }); -}); +var player = videojs('player', options); if (location.pathname.startsWith('/embed/')) { player.overlay({ @@ -254,5 +192,273 @@ if (!video_data.params.listen && video_data.params.annotations) { xhr.send(); } +function increase_volume(delta) { + const curVolume = player.volume(); + let newVolume = curVolume + delta; + if (newVolume > 1) { + newVolume = 1; + } else if (newVolume < 0) { + newVolume = 0; + } + player.volume(newVolume); +} + +function toggle_muted() { + const isMuted = player.muted(); + player.muted(!isMuted); +} + +function skip_seconds(delta) { + const duration = player.duration(); + const curTime = player.currentTime(); + let newTime = curTime + delta; + if (newTime > duration) { + newTime = duration; + } else if (newTime < 0) { + newTime = 0; + } + player.currentTime(newTime); +} + +function set_time_percent(percent) { + const duration = player.duration(); + const newTime = duration * (percent / 100); + player.currentTime(newTime); +} + +function toggle_play() { + if (player.paused()) { + player.play(); + } else { + player.pause(); + } +} + +const toggle_captions = (function() { + let toggledTrack = null; + const onChange = function(e) { + toggledTrack = null; + }; + const bindChange = function(onOrOff) { + player.textTracks()[onOrOff]('change', onChange); + }; + // Wrapper function to ignore our own emitted events and only listen + // to events emitted by Video.js on click on the captions menu items. + const setMode = function(track, mode) { + bindChange('off'); + track.mode = mode; + window.setTimeout(function() { + bindChange('on'); + }, 0); + }; + bindChange('on'); + return function() { + if (toggledTrack !== null) { + if (toggledTrack.mode !== 'showing') { + setMode(toggledTrack, 'showing'); + } else { + setMode(toggledTrack, 'disabled'); + } + toggledTrack = null; + return; + } + + // Used as a fallback if no captions are currently active. + // TODO: Make this more intelligent by e.g. relying on browser language. + let fallbackCaptionsTrack = null; + + const tracks = player.textTracks(); + for (let i = 0; i < tracks.length; i++) { + const track = tracks[i]; + if (track.kind !== 'captions') { + continue; + } + + if (fallbackCaptionsTrack === null) { + fallbackCaptionsTrack = track; + } + if (track.mode === 'showing') { + setMode(track, 'disabled'); + toggledTrack = track; + return; + } + } + + // Fallback if no captions are currently active. + if (fallbackCaptionsTrack !== null) { + setMode(fallbackCaptionsTrack, 'showing'); + toggledTrack = fallbackCaptionsTrack; + } + }; +})(); + +function toggle_fullscreen() { + if (player.isFullscreen()) { + player.exitFullscreen(); + } else { + player.requestFullscreen(); + } +} + +function increase_playback_rate(steps) { + const maxIndex = options.playbackRates.length - 1; + const curIndex = options.playbackRates.indexOf(player.playbackRate()); + let newIndex = curIndex + steps; + if (newIndex > maxIndex) { + newIndex = maxIndex; + } else if (newIndex < 0) { + newIndex = 0; + } + player.playbackRate(options.playbackRates[newIndex]); +} + +window.addEventListener('keydown', e => { + if (e.target.tagName.toLowerCase() === 'input') { + // Ignore input when focus is on certain elements, e.g. form fields. + return; + } + // See https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L310-L313 + const isPlayerFocused = false + || e.target === document.querySelector('.video-js') + || e.target === document.querySelector('.vjs-tech') + || e.target === document.querySelector('.iframeblocker') + || e.target === document.querySelector('.vjs-control-bar') + ; + let action = null; + + const code = e.keyCode; + const key = e.key; + switch (key) { + case ' ': + case 'k': + action = toggle_play; + break; + + case 'ArrowUp': + if (isPlayerFocused) { + action = increase_volume.bind(this, 0.1); + } + break; + case 'ArrowDown': + if (isPlayerFocused) { + action = increase_volume.bind(this, -0.1); + } + break; + + case 'm': + action = toggle_muted; + break; + + case 'ArrowRight': + action = skip_seconds.bind(this, 5); + break; + case 'ArrowLeft': + action = skip_seconds.bind(this, -5); + break; + case 'l': + action = skip_seconds.bind(this, 10); + break; + case 'j': + action = skip_seconds.bind(this, -10); + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + const percent = (code - 48) * 10; + action = set_time_percent.bind(this, percent); + break; + + case 'c': + action = toggle_captions; + break; + case 'f': + action = toggle_fullscreen; + break; + + case 'N': + action = next_video; + break; + case 'P': + // TODO: Add support to play back previous video. + break; + + case '.': + // TODO: Add support for next-frame-stepping. + break; + case ',': + // TODO: Add support for previous-frame-stepping. + break; + + case '>': + action = increase_playback_rate.bind(this, 1); + break; + case '<': + action = increase_playback_rate.bind(this, -1); + break; + + default: + console.info('Unhandled key down event: %s:', key, e); + break; + } + + if (action) { + e.preventDefault(); + action(); + } +}, false); + +// Add support for controlling the player volume by scrolling over it. Adapted from +// https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L292-L328 +(function() { + const volumeStep = 0.05; + const enableVolumeScroll = true; + const enableHoverScroll = true; + const doc = document; + const pEl = document.getElementById('player'); + + var volumeHover = false; + var volumeSelector = pEl.querySelector('.vjs-volume-menu-button') || pEl.querySelector('.vjs-volume-panel'); + if (volumeSelector != null) { + volumeSelector.onmouseover = function() { volumeHover = true; }; + volumeSelector.onmouseout = function() { volumeHover = false; }; + } + + var mouseScroll = function mouseScroll(event) { + var activeEl = doc.activeElement; + if (enableHoverScroll) { + // If we leave this undefined then it can match non-existent elements below + activeEl = 0; + } + + // When controls are disabled, hotkeys will be disabled as well + if (player.controls()) { + if (volumeHover) { + if (enableVolumeScroll) { + event = window.event || event; + var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail))); + event.preventDefault(); + + if (delta == 1) { + increase_volume(volumeStep); + } else if (delta == -1) { + increase_volume(-volumeStep); + } + } + } + } + }; + + player.on('mousewheel', mouseScroll); + player.on("DOMMouseScroll", mouseScroll); +}()); + // Since videojs-share can sometimes be blocked, we defer it until last player.share(shareOptions); diff --git a/assets/js/videojs.hotkeys.min.js b/assets/js/videojs.hotkeys.min.js deleted file mode 100644 index a6cfe6e2c..000000000 --- a/assets/js/videojs.hotkeys.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/* videojs-hotkeys v0.2.25 - https://github.com/ctd1500/videojs-hotkeys */ -!function(e,n){"undefined"!=typeof window&&window.videojs?n(window.videojs):"function"==typeof define&&define.amd?define("videojs-hotkeys",["video.js"],function(e){return n(e.default||e)}):"undefined"!=typeof module&&module.exports&&(module.exports=n(require("video.js")))}(0,function(e){"use strict";"undefined"!=typeof window&&(window.videojs_hotkeys={version:"0.2.25"});(e.registerPlugin||e.plugin)("hotkeys",function(n){function t(e){return"function"==typeof s?s(e):s}function r(e){null!=e&&"function"==typeof e.then&&e.then(null,function(e){})}var o=this,u=o.el(),l=document,i={volumeStep:.1,seekStep:5,enableMute:!0,enableVolumeScroll:!0,enableHoverScroll:!1,enableFullscreen:!0,enableNumbers:!0,enableJogStyle:!1,alwaysCaptureHotkeys:!1,enableModifiersForNumbers:!0,enableInactiveFocus:!0,skipInitialFocus:!1,playPauseKey:function(e){return 32===e.which||179===e.which},rewindKey:function(e){return 37===e.which||177===e.which},forwardKey:function(e){return 39===e.which||176===e.which},volumeUpKey:function(e){return 38===e.which},volumeDownKey:function(e){return 40===e.which},muteKey:function(e){return 77===e.which},fullscreenKey:function(e){return 70===e.which},customKeys:{}},c=e.mergeOptions||e.util.mergeOptions,a=(n=c(i,n||{})).volumeStep,s=n.seekStep,m=n.enableMute,f=n.enableVolumeScroll,y=n.enableHoverScroll,v=n.enableFullscreen,d=n.enableNumbers,p=n.enableJogStyle,b=n.alwaysCaptureHotkeys,h=n.enableModifiersForNumbers,w=n.enableInactiveFocus,k=n.skipInitialFocus,S=e.VERSION;u.hasAttribute("tabIndex")||u.setAttribute("tabIndex","-1"),u.style.outline="none",!b&&o.autoplay()||k||o.one("play",function(){u.focus()}),w&&o.on("userinactive",function(){var e=function(){clearTimeout(n)},n=setTimeout(function(){o.off("useractive",e);var n=l.activeElement,t=u.querySelector(".vjs-control-bar");n&&n.parentElement==t&&u.focus()},10);o.one("useractive",e)}),o.on("play",function(){var e=u.querySelector(".iframeblocker");e&&""===e.style.display&&(e.style.display="block",e.style.bottom="39px")});var K=!1,q=u.querySelector(".vjs-volume-menu-button")||u.querySelector(".vjs-volume-panel");null!=q&&(q.onmouseover=function(){K=!0},q.onmouseout=function(){K=!1});var j=function(e){if(y)n=0;else var n=l.activeElement;if(o.controls()&&(b||n==u||n==u.querySelector(".vjs-tech")||n==u.querySelector(".iframeblocker")||n==u.querySelector(".vjs-control-bar")||K)&&f){e=window.event||e;var t=Math.max(-1,Math.min(1,e.wheelDelta||-e.detail));e.preventDefault(),1==t?o.volume(o.volume()+a):-1==t&&o.volume(o.volume()-a)}},F=function(e,t){return n.playPauseKey(e,t)?1:n.rewindKey(e,t)?2:n.forwardKey(e,t)?3:n.volumeUpKey(e,t)?4:n.volumeDownKey(e,t)?5:n.muteKey(e,t)?6:n.fullscreenKey(e,t)?7:void 0};return o.on("keydown",function(e){var i,c,s=e.which,f=e.preventDefault,y=o.duration();if(o.controls()){var w=l.activeElement;if(b||w==u||w==u.querySelector(".vjs-tech")||w==u.querySelector(".vjs-control-bar")||w==u.querySelector(".iframeblocker"))switch(F(e,o)){case 1:f(),b&&e.stopPropagation(),o.paused()?r(o.play()):o.pause();break;case 2:i=!o.paused(),f(),i&&o.pause(),(c=o.currentTime()-t(e))<=0&&(c=0),o.currentTime(c),i&&r(o.play());break;case 3:i=!o.paused(),f(),i&&o.pause(),(c=o.currentTime()+t(e))>=y&&(c=i?y-.001:y),o.currentTime(c),i&&r(o.play());break;case 5:f(),p?(c=o.currentTime()-1,o.currentTime()<=1&&(c=0),o.currentTime(c)):o.volume(o.volume()-a);break;case 4:f(),p?((c=o.currentTime()+1)>=y&&(c=y),o.currentTime(c)):o.volume(o.volume()+a);break;case 6:m&&o.muted(!o.muted());break;case 7:v&&(o.isFullscreen()?o.exitFullscreen():o.requestFullscreen());break;default:if((s>47&&s<59||s>95&&s<106)&&(h||!(e.metaKey||e.ctrlKey||e.altKey))&&d){var k=48;s>95&&(k=96);var S=s-k;f(),o.currentTime(o.duration()*S*.1)}for(var K in n.customKeys){var q=n.customKeys[K];q&&q.key&&q.handler&&q.key(e)&&(f(),q.handler(o,n,e))}}}}),o.on("dblclick",function(e){if(null!=S&&S<="7.1.0"&&o.controls()){var n=e.relatedTarget||e.toElement||l.activeElement;n!=u&&n!=u.querySelector(".vjs-tech")&&n!=u.querySelector(".iframeblocker")||v&&(o.isFullscreen()?o.exitFullscreen():o.requestFullscreen())}}),o.on("mousewheel",j),o.on("DOMMouseScroll",j),this})}); \ No newline at end of file diff --git a/assets/js/watch.js b/assets/js/watch.js index 05e3b7e26..0f3e81232 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -73,29 +73,33 @@ if (continue_button) { continue_button.onclick = continue_autoplay; } -function continue_autoplay(event) { - if (event.target.checked) { - player.on('ended', function () { - var url = new URL('https://example.com/watch?v=' + video_data.next_video); +function next_video() { + var url = new URL('https://example.com/watch?v=' + video_data.next_video); - if (video_data.params.autoplay || video_data.params.continue_autoplay) { - url.searchParams.set('autoplay', '1'); - } + if (video_data.params.autoplay || video_data.params.continue_autoplay) { + url.searchParams.set('autoplay', '1'); + } - if (video_data.params.listen !== video_data.preferences.listen) { - url.searchParams.set('listen', video_data.params.listen); - } + if (video_data.params.listen !== video_data.preferences.listen) { + url.searchParams.set('listen', video_data.params.listen); + } - if (video_data.params.speed !== video_data.preferences.speed) { - url.searchParams.set('speed', video_data.params.speed); - } + if (video_data.params.speed !== video_data.preferences.speed) { + url.searchParams.set('speed', video_data.params.speed); + } - if (video_data.params.local !== video_data.preferences.local) { - url.searchParams.set('local', video_data.params.local); - } + if (video_data.params.local !== video_data.preferences.local) { + url.searchParams.set('local', video_data.params.local); + } + + url.searchParams.set('continue', '1'); + location.assign(url.pathname + url.search); +} - url.searchParams.set('continue', '1'); - location.assign(url.pathname + url.search); +function continue_autoplay(event) { + if (event.target.checked) { + player.on('ended', function () { + next_video(); }); } else { player.off('ended'); diff --git a/src/invidious/views/components/player_sources.ecr b/src/invidious/views/components/player_sources.ecr index 003d2c3ac..d950e0da7 100644 --- a/src/invidious/views/components/player_sources.ecr +++ b/src/invidious/views/components/player_sources.ecr @@ -6,7 +6,6 @@ - diff --git a/src/invidious/views/licenses.ecr b/src/invidious/views/licenses.ecr index 7cffb7fc0..aae8bb19e 100644 --- a/src/invidious/views/licenses.ecr +++ b/src/invidious/views/licenses.ecr @@ -135,20 +135,6 @@ - - -
videojs.hotkeys.min.js - - - - Apache-2.0-only - - - - <%= translate(locale, "source") %> - - - videojs-http-source-selector.min.js From 2b94975345a3bdd085a99081de521d1c5d3ada48 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 16 Aug 2019 20:06:21 -0500 Subject: [PATCH 05/10] Fix playlist_thumbnail extractor --- assets/css/default.css | 11 +++++++---- src/invidious.cr | 5 ++--- src/invidious/helpers/helpers.cr | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 451fbc8ce..4daa9f16b 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -1,6 +1,8 @@ html, body { - font-family: BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif; + font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, + Arial, sans-serif; } .deleted { @@ -108,6 +110,7 @@ img.thumbnail { height: 100%; left: 0; top: 0; + object-fit: cover; } .length { @@ -442,8 +445,8 @@ video.video-js { } .video-js.player-style-youtube .vjs-control-bar { - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; } .video-js.player-style-youtube .vjs-big-play-button { /* @@ -452,6 +455,6 @@ video.video-js { */ top: 50%; left: 50%; - margin-top: -.81666em; + margin-top: -0.81666em; margin-left: -1.5em; } diff --git a/src/invidious.cr b/src/invidious.cr index 21d6544de..2fa1d66f0 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -3003,7 +3003,7 @@ get "/user/:user/about" do |env| env.redirect "/channel/#{user}" end -get "/channel:ucid/about" do |env| +get "/channel/:ucid/about" do |env| ucid = env.params.url["ucid"] env.redirect "/channel/#{ucid}" end @@ -3107,8 +3107,7 @@ get "/channel/:ucid/playlists" do |env| end items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) - items.select! { |item| item.is_a?(SearchPlaylist) && !item.videos.empty? } - items = items.map { |item| item.as(SearchPlaylist) } + items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) } items.each { |item| item.author = "" } env.set "search", "channel:#{channel.ucid} " diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index e018e5670..1b232072a 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -397,8 +397,8 @@ def extract_items(nodeset, ucid = nil, author_name = nil) ) end - playlist_thumbnail = node.xpath_node(%q(.//div/span/img)).try &.["data-thumb"]? - playlist_thumbnail ||= node.xpath_node(%q(.//div/span/img)).try &.["src"] + playlist_thumbnail = node.xpath_node(%q(.//span/img)).try &.["data-thumb"]? + playlist_thumbnail ||= node.xpath_node(%q(.//span/img)).try &.["src"] items << SearchPlaylist.new( title, From acaf7b969add62bdcc32f4bc193bcd5280413cc3 Mon Sep 17 00:00:00 2001 From: leonklingele Date: Mon, 19 Aug 2019 06:22:39 +0200 Subject: [PATCH 06/10] js: add support to detect alt, meta and control key in keydown handler (#704) This fixes a quite severe user experience issue where pressing the 'alt', 'meta' and/or 'ctrl' key along with one of the supported keys (e.g. 'f' to enter video fullscreen mode) would overwrite the default browser behavior. In the case of 'f+meta' we would enter fullscreen mode, and not open the browser search panel as one might expect. This change is required to stay consistent with the way YouTube handles keydown events. --- assets/js/player.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index 4a61258cc..0d0ecebd9 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -327,8 +327,13 @@ window.addEventListener('keydown', e => { let action = null; const code = e.keyCode; - const key = e.key; - switch (key) { + const decoratedKey = + e.key + + (e.altKey ? '+alt' : '') + + (e.ctrlKey ? '+ctrl' : '') + + (e.metaKey ? '+meta' : '') + ; + switch (decoratedKey) { case ' ': case 'k': action = toggle_play; @@ -405,7 +410,7 @@ window.addEventListener('keydown', e => { break; default: - console.info('Unhandled key down event: %s:', key, e); + console.info('Unhandled key down event: %s:', decoratedKey, e); break; } From e768e1e27757805d0dbb2c1f3dbaf788c8c6cbfd Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 19 Aug 2019 09:00:37 -0500 Subject: [PATCH 07/10] Fix allowed_regions for globally blocked videos --- src/invidious/videos.cr | 55 +++++++++++++---------------------------- 1 file changed, 17 insertions(+), 38 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 7fd06dd53..03fe9a264 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -108,33 +108,7 @@ CAPTION_LANGUAGES = { "Zulu", } -REGIONS = {"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"} -BYPASS_REGIONS = { - "GB", - "DE", - "FR", - "IN", - "CN", - "RU", - "CA", - "JP", - "IT", - "TH", - "ES", - "AE", - "KR", - "IR", - "BR", - "PK", - "ID", - "BD", - "MX", - "PH", - "EG", - "VN", - "CD", - "TR", -} +REGIONS = {"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"} # See https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L380-#L476 VIDEO_FORMATS = { @@ -1133,19 +1107,24 @@ def fetch_video(id, region) info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ") allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).try &.["content"].split(",") - allowed_regions ||= [] of String + if !allowed_regions || allowed_regions == [""] + allowed_regions = [] of String + end # Check for region-blocks - if info["reason"]? && info["reason"].includes? "your country" - region = allowed_regions.sample(1)[0]? - client = make_client(YT_URL, region) - response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999") - - html = XML.parse_html(response.body) - info = extract_player_config(response.body, html) - - info["region"] = region if region - info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ") + if info["reason"]? && info["reason"].includes?("your country") + bypass_regions = PROXY_LIST.keys & allowed_regions + if !bypass_regions.empty? + region = bypass_regions[rand(bypass_regions.size)] + client = make_client(YT_URL, region) + response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999") + + html = XML.parse_html(response.body) + info = extract_player_config(response.body, html) + + info["region"] = region if region + info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ") + end end # Try to pull streams from embed URL From 9f9cc1ffb5abdb9c0b4331a1f69039f0e77ae667 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 21 Aug 2019 18:23:20 -0500 Subject: [PATCH 08/10] Refactor search extractor --- src/invidious.cr | 2 +- src/invidious/channels.cr | 15 +++--- src/invidious/helpers/helpers.cr | 90 ++++++++++++++------------------ 3 files changed, 48 insertions(+), 59 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 2fa1d66f0..143be96dd 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -5167,7 +5167,7 @@ get "/vi/:id/:name" do |env| end end -# Undocumented, creates anonymous playlist with specified 'video_ids' +# Undocumented, creates anonymous playlist with specified 'video_ids', max 50 videos get "/watch_videos" do |env| client = make_client(YT_URL) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 5e01cef27..107039a64 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -387,14 +387,15 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by) html = XML.parse_html(json["content_html"].as_s) nodeset = html.xpath_nodes(%q(//li[contains(@class, "feed-item-container")])) - else - url = "/channel/#{ucid}/playlists?disable_polymer=1&flow=list" + elsif auto_generated + url = "/channel/#{ucid}" - if auto_generated - url += "&view=50" - else - url += "&view=1" - end + response = client.get(url) + html = XML.parse_html(response.body) + + nodeset = html.xpath_nodes(%q(//ul[@id="browse-items-primary"]/li[contains(@class, "feed-item-container")])) + else + url = "/channel/#{ucid}/playlists?disable_polymer=1&flow=list&view=1" case sort_by when "last", "last_added" diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 1b232072a..20ff2de6c 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -442,47 +442,20 @@ def extract_items(nodeset, ucid = nil, author_name = nil) else id = id.lchop("/watch?v=") - metadata = node.xpath_nodes(%q(.//div[contains(@class,"yt-lockup-meta")]/ul/li)) + metadata = node.xpath_node(%q(.//div[contains(@class,"yt-lockup-meta")]/ul)) - begin - published = decode_date(metadata[0].content.lchop("Streamed ").lchop("Starts ")) - rescue ex - end - begin - published ||= Time.unix(metadata[0].xpath_node(%q(.//span)).not_nil!["data-timestamp"].to_i64) - rescue ex - end + published = metadata.try &.xpath_node(%q(.//li[contains(text(), " ago")])).try { |node| decode_date(node.content.sub(/^[a-zA-Z]+ /, "")) } + published ||= metadata.try &.xpath_node(%q(.//span[@data-timestamp])).try { |node| Time.unix(node["data-timestamp"].to_i64) } published ||= Time.utc - begin - view_count = metadata[0].content.rchop(" watching").delete(",").try &.to_i64? - rescue ex - end - begin - view_count ||= metadata.try &.[1].content.delete("No views,").try &.to_i64? - rescue ex - end + view_count = metadata.try &.xpath_node(%q(.//li[contains(text(), " views")])).try &.content.gsub(/\D/, "").to_i64? view_count ||= 0_i64 - length_seconds = node.xpath_node(%q(.//span[@class="video-time"])) - if length_seconds - length_seconds = decode_length_seconds(length_seconds.content) - else - length_seconds = -1 - end + length_seconds = node.xpath_node(%q(.//span[@class="video-time"])).try { |node| decode_length_seconds(node.content) } + length_seconds ||= -1 - live_now = node.xpath_node(%q(.//span[contains(@class, "yt-badge-live")])) - if live_now - live_now = true - else - live_now = false - end - - if node.xpath_node(%q(.//span[text()="Premium"])) - premium = true - else - premium = false - end + live_now = node.xpath_node(%q(.//span[contains(@class, "yt-badge-live")])) ? true : false + premium = node.xpath_node(%q(.//span[text()="Premium"])) ? true : false if !premium || node.xpath_node(%q(.//span[contains(text(), "Free episode")])) paid = false @@ -520,26 +493,18 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) nodeset.each do |shelf| shelf_anchor = shelf.xpath_node(%q(.//h2[contains(@class, "branded-page-module-title")])) + next if !shelf_anchor - if !shelf_anchor - next - end - - title = shelf_anchor.xpath_node(%q(.//span[contains(@class, "branded-page-module-title-text")])) - if title - title = title.content.strip - end + title = shelf_anchor.xpath_node(%q(.//span[contains(@class, "branded-page-module-title-text")])).try &.content.strip title ||= "" id = shelf_anchor.xpath_node(%q(.//a)).try &.["href"] - if !id - next - end + next if !id - is_playlist = false + shelf_is_playlist = false videos = [] of SearchPlaylistVideo - shelf.xpath_nodes(%q(.//ul[contains(@class, "yt-uix-shelfslider-list")]/li)).each do |child_node| + shelf.xpath_nodes(%q(.//ul[contains(@class, "yt-uix-shelfslider-list") or contains(@class, "expanded-shelf-content-list")]/li)).each do |child_node| type = child_node.xpath_node(%q(./div)) if !type next @@ -547,7 +512,7 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) case type["class"] when .includes? "yt-lockup-video" - is_playlist = true + shelf_is_playlist = true anchor = child_node.xpath_node(%q(.//h3[contains(@class, "yt-lockup-title")]/a)) if anchor @@ -588,19 +553,42 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) end video_count ||= 50 + videos = [] of SearchPlaylistVideo + child_node.xpath_nodes(%q(.//*[contains(@class, "yt-lockup-playlist-items")]/li)).each do |video| + anchor = video.xpath_node(%q(.//a)) + if anchor + video_title = anchor.content.strip + id = HTTP::Params.parse(URI.parse(anchor["href"]).query.not_nil!)["v"] + end + video_title ||= "" + id ||= "" + + anchor = video.xpath_node(%q(.//span/span)) + if anchor + length_seconds = decode_length_seconds(anchor.content) + end + length_seconds ||= 0 + + videos << SearchPlaylistVideo.new( + video_title, + id, + length_seconds + ) + end + items << SearchPlaylist.new( playlist_title, plid, author_name, ucid, video_count, - Array(SearchPlaylistVideo).new, + videos, playlist_thumbnail ) end end - if is_playlist + if shelf_is_playlist plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"] items << SearchPlaylist.new( From 4c9975a7d90ec9b3821befb07af02a293925196a Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 21 Aug 2019 18:35:54 -0500 Subject: [PATCH 09/10] Use accurate sub count when available --- src/invidious/views/channel.ecr | 2 +- src/invidious/views/community.ecr | 2 +- src/invidious/views/playlists.ecr | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index b5eb46ea3..1074598dc 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -34,7 +34,7 @@
<% ucid = channel.ucid %> <% author = channel.author %> - <% sub_count_text = number_to_short_text(channel.sub_count) %> + <% sub_count_text = channel.sub_count.format %> <%= rendered "components/subscribe_widget" %>
diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr index 218cc2d41..9d086b5d6 100644 --- a/src/invidious/views/community.ecr +++ b/src/invidious/views/community.ecr @@ -33,7 +33,7 @@
<% ucid = channel.ucid %> <% author = channel.author %> - <% sub_count_text = number_to_short_text(channel.sub_count) %> + <% sub_count_text = channel.sub_count.format %> <%= rendered "components/subscribe_widget" %>
diff --git a/src/invidious/views/playlists.ecr b/src/invidious/views/playlists.ecr index a32192b5a..400922ff9 100644 --- a/src/invidious/views/playlists.ecr +++ b/src/invidious/views/playlists.ecr @@ -33,7 +33,7 @@
<% ucid = channel.ucid %> <% author = channel.author %> - <% sub_count_text = number_to_short_text(channel.sub_count) %> + <% sub_count_text = channel.sub_count.format %> <%= rendered "components/subscribe_widget" %>
From 059f50dad477daab63246314198ec7e274493ece Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 21 Aug 2019 19:08:11 -0500 Subject: [PATCH 10/10] Add 'playlistThumbnail' to playlist objects --- src/invidious.cr | 2 ++ src/invidious/helpers/helpers.cr | 56 +++++++++++++------------------- src/invidious/playlists.cr | 18 +++++----- src/invidious/search.cr | 1 + 4 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 143be96dd..0203e3b87 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -4101,8 +4101,10 @@ get "/api/v1/playlists/:plid" do |env| response = JSON.build do |json| json.object do + json.field "type", "playlist" json.field "title", playlist.title json.field "playlistId", playlist.id + json.field "playlistThumbnail", playlist.thumbnail json.field "author", playlist.author json.field "authorId", playlist.ucid diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 20ff2de6c..331f63607 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -331,18 +331,8 @@ def extract_items(nodeset, ucid = nil, author_name = nil) next end - anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)) - if anchor - author = anchor.content.strip - author_id = anchor["href"].split("/")[-1] - end - - author ||= author_name - author_id ||= ucid - - author ||= "" - author_id ||= "" - + author_id = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)).try &.["href"].split("/")[-1] || ucid || "" + author = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)).try &.content.strip || author_name || "" description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")])).try &.to_s || "" tile = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-tile")])) @@ -401,13 +391,13 @@ def extract_items(nodeset, ucid = nil, author_name = nil) playlist_thumbnail ||= node.xpath_node(%q(.//span/img)).try &.["src"] items << SearchPlaylist.new( - title, - plid, - author, - author_id, - video_count, - videos, - playlist_thumbnail + title: title, + id: plid, + author: author, + ucid: author_id, + video_count: video_count, + videos: videos, + thumbnail: playlist_thumbnail ) when .includes? "yt-lockup-channel" author = title.strip @@ -577,13 +567,13 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) end items << SearchPlaylist.new( - playlist_title, - plid, - author_name, - ucid, - video_count, - videos, - playlist_thumbnail + title: playlist_title, + id: plid, + author: author_name, + ucid: ucid, + video_count: video_count, + videos: videos, + thumbnail: playlist_thumbnail ) end end @@ -592,13 +582,13 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"] items << SearchPlaylist.new( - title, - plid, - author_name, - ucid, - videos.size, - videos, - "/vi/#{videos[0].id}/mqdefault.jpg" + title: title, + id: plid, + author: author_name, + ucid: ucid, + video_count: videos.size, + videos: videos, + thumbnail: "https://i.ytimg.com/vi/#{videos[0].id}/mqdefault.jpg" ) end end diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index d28a41497..7965d9905 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -51,6 +51,7 @@ struct Playlist video_count: Int32, views: Int64, updated: Time, + thumbnail: String?, }) end @@ -223,6 +224,9 @@ def fetch_playlist(plid, locale) description_html = document.xpath_node(%q(//span[@class="pl-header-description-text"]/div/div[1])).try &.to_s || document.xpath_node(%q(//span[@class="pl-header-description-text"])).try &.to_s || "" + playlist_thumbnail = document.xpath_node(%q(//div[@class="pl-header-thumb"]/img)).try &.["data-thumb"]? || + document.xpath_node(%q(//div[@class="pl-header-thumb"]/img)).try &.["src"] + # YouTube allows anonymous playlists, so most of this can be empty or optional anchor = document.xpath_node(%q(//ul[@class="pl-header-details"])) author = anchor.try &.xpath_node(%q(.//li[1]/a)).try &.content @@ -234,15 +238,12 @@ def fetch_playlist(plid, locale) video_count = anchor.try &.xpath_node(%q(.//li[2])).try &.content.gsub(/\D/, "").to_i? video_count ||= 0 - views = anchor.try &.xpath_node(%q(.//li[3])).try &.content.delete("No views, ").to_i64? + + views = anchor.try &.xpath_node(%q(.//li[3])).try &.content.gsub(/\D/, "").to_i64? views ||= 0_i64 - updated = anchor.try &.xpath_node(%q(.//li[4])).try &.content.lchop("Last updated on ").lchop("Updated ") - if updated - updated = decode_date(updated) - else - updated = Time.utc - end + updated = anchor.try &.xpath_node(%q(.//li[4])).try &.content.lchop("Last updated on ").lchop("Updated ").try { |date| decode_date(date) } + updated ||= Time.utc playlist = Playlist.new( title: title, @@ -253,7 +254,8 @@ def fetch_playlist(plid, locale) description_html: description_html, video_count: video_count, views: views, - updated: updated + updated: updated, + thumbnail: playlist_thumbnail, ) return playlist diff --git a/src/invidious/search.cr b/src/invidious/search.cr index 784e38973..7a36f32ec 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -117,6 +117,7 @@ struct SearchPlaylist json.field "type", "playlist" json.field "title", self.title json.field "playlistId", self.id + json.field "playlistThumbnail", self.thumbnail json.field "author", self.author json.field "authorId", self.ucid