From 7dd699370fae20c69119a4117468b1d999a2752a Mon Sep 17 00:00:00 2001 From: meow Date: Fri, 6 May 2022 04:46:59 +0300 Subject: [PATCH 01/42] js code rewrite. Created _helpers.js with XHR and storage wrapper --- assets/js/_helpers.js | 218 +++++++++ assets/js/community.js | 86 ++-- assets/js/embed.js | 97 ++-- assets/js/handlers.js | 133 +++--- assets/js/notifications.js | 92 ++-- assets/js/player.js | 306 +++++------- assets/js/playlist_widget.js | 52 +- assets/js/subscribe_widget.js | 92 +--- assets/js/themes.js | 48 +- assets/js/watch.js | 446 +++++++----------- assets/js/watched_widget.js | 39 +- src/invidious/comments.cr | 2 +- src/invidious/views/add_playlist_items.ecr | 1 + src/invidious/views/community.ecr | 1 + src/invidious/views/components/player.ecr | 1 + .../views/components/subscribe_widget.ecr | 1 + src/invidious/views/embed.ecr | 1 + src/invidious/views/feeds/history.ecr | 1 + src/invidious/views/feeds/subscriptions.ecr | 1 + src/invidious/views/licenses.ecr | 14 + src/invidious/views/playlist.ecr | 1 + src/invidious/views/template.ecr | 1 + src/invidious/views/watch.ecr | 2 + 23 files changed, 735 insertions(+), 901 deletions(-) create mode 100644 assets/js/_helpers.js diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js new file mode 100644 index 00000000..04576348 --- /dev/null +++ b/assets/js/_helpers.js @@ -0,0 +1,218 @@ +'use strict'; +// Contains only auxiliary methods +// May be included and executed unlimited number of times without any consequences + +// Polyfills for IE11 +Array.prototype.find = Array.prototype.find || function (condition) { + return this.filter(condition)[0]; +}; +Array.from = Array.from || function (source) { + return Array.prototype.slice.call(source); +}; +NodeList.prototype.forEach = NodeList.prototype.forEach || function (callback) { + Array.from(this).forEach(callback); +}; +String.prototype.includes = String.prototype.includes || function (searchString) { + return this.indexOf(searchString) >= 0; +}; +String.prototype.startsWith = String.prototype.startsWith || function (prefix) { + return this.substr(0, prefix.length) === prefix; +}; +Math.sign = Math.sign || function(x) { + x = +x; + if (!x) return x; // 0 and NaN + return x > 0 ? 1 : -1; +}; + +// Monstrous global variable for handy code +helpers = helpers || { + /** + * https://en.wikipedia.org/wiki/Clamping_(graphics) + * @param {Number} num Source number + * @param {Number} min Low border + * @param {Number} max High border + * @returns {Number} Clamped value + */ + clamp: function (num, min, max) { + if (max < min) { + var t = max; max = min; min = t; // swap max and min + } + + if (max > num) + return max; + if (min < num) + return min; + return num; + }, + + /** @private */ + _xhr: function (method, url, options, callbacks) { + const xhr = new XMLHttpRequest(); + xhr.open(method, url); + + // Default options + xhr.responseType = 'json'; + xhr.timeout = 10000; + // Default options redefining + if (options.responseType) + xhr.responseType = options.responseType; + if (options.timeout) + xhr.timeout = options.timeout; + + if (method === 'POST') + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) + if (callbacks.on200) + callbacks.on200(xhr.response); + else + if (callbacks.onNon200) + callbacks.onNon200(xhr); + } + }; + + xhr.ontimeout = function () { + if (callbacks.onTimeout) + callbacks.onTimeout(xhr); + }; + + xhr.onerror = function () { + if (callbacks.onError) + callbacks.onError(xhr); + }; + + if (options.payload) + xhr.send(options.payload); + else + xhr.send(); + }, + /** @private */ + _xhrRetry(method, url, options, callbacks) { + if (options.retries <= 0) { + console.warn('Failed to pull', options.entity_name); + if (callbacks.onTotalFail) + callbacks.onTotalFail(); + return; + } + helpers.xhr(method, url, options, callbacks); + }, + /** + * @callback callbackXhrOn200 + * @param {Object} response - xhr.response + */ + /** + * @callback callbackXhrError + * @param {XMLHttpRequest} xhr + */ + /** + * @param {'GET'|'POST'} method - 'GET' or 'POST' + * @param {String} url - URL to send request to + * @param {Object} options - other XHR options + * @param {XMLHttpRequestBodyInit} [options.payload=null] - payload for POST-requests + * @param {'arraybuffer'|'blob'|'document'|'json'|'text'} [options.responseType=json] + * @param {Number} [options.timeout=10000] + * @param {Number} [options.retries=1] + * @param {String} [options.entity_name='unknown'] - string to log + * @param {Number} [options.retry_timeout=1000] + * @param {Object} callbacks - functions to execute on events fired + * @param {callbackXhrOn200} [callbacks.on200] + * @param {callbackXhrError} [callbacks.onNon200] + * @param {callbackXhrError} [callbacks.onTimeout] + * @param {callbackXhrError} [callbacks.onError] + * @param {callbackXhrError} [callbacks.onTotalFail] - if failed after all retries + */ + xhr(method, url, options, callbacks) { + if (options.retries > 1) { + helpers._xhr(method, url, options, callbacks); + return; + } + + if (!options.entity_name) options.entity_name = 'unknown'; + if (!options.retry_timeout) options.retry_timeout = 1; + const retries_total = options.retries; + + const retry = function () { + console.warn('Pulling ' + options.entity_name + ' failed... ' + options.retries + '/' + retries_total); + setTimeout(function () { + options.retries--; + helpers._xhrRetry(method, url, options, callbacks); + }, options.retry_timeout); + }; + + if (callbacks.onError) + callbacks._onError = callbacks.onError; + callbacks.onError = function (xhr) { + if (callbacks._onError) + callbacks._onError(); + retry(); + }; + + if (callbacks.onTimeout) + callbacks._onTimeout = callbacks.onTimeout; + callbacks.onTimeout = function (xhr) { + if (callbacks._onTimeout) + callbacks._onTimeout(); + retry(); + }; + helpers._xhrRetry(method, url, options, callbacks); + }, + + /** + * @typedef {Object} invidiousStorage + * @property {(key:String) => Object|null} get + * @property {(key:String, value:Object) => null} set + * @property {(key:String) => null} remove + */ + + /** + * Universal storage proxy. Uses inside localStorage or cookies + * @type {invidiousStorage} + */ + storage: (function () { + // access to localStorage throws exception in Tor Browser, so try is needed + let localStorageIsUsable = false; + try{localStorageIsUsable = !!localStorage.setItem;}catch(e){} + + if (localStorageIsUsable) { + return { + get: function (key) { return localStorage[key]; }, + set: function (key, value) { localStorage[key] = value; }, + remove: function (key) { localStorage.removeItem(key); } + }; + } + + console.info('Storage: localStorage is disabled or unaccessible trying cookies'); + return { + get: function (key) { + const cookiePrefix = key + '='; + function findCallback(cookie) {return cookie.startsWith(cookiePrefix);} + const matchedCookie = document.cookie.split(';').find(findCallback); + if (matchedCookie) + return matchedCookie.replace(cookiePrefix, ''); + return null; + }, + set: function (key, value) { + const cookie_data = encodeURIComponent(JSON.stringify(value)); + + // Set expiration in 2 year + const date = new Date(); + date.setTime(date.getTime() + 2*365.25*24*60*60); + + const ip_regex = /^((\d+\.){3}\d+|[A-Fa-f0-9]*:[A-Fa-f0-9:]*:[A-Fa-f0-9:]+)$/; + let domain_used = location.hostname; + + // Fix for a bug in FF where the leading dot in the FQDN is not ignored + if (domain_used.charAt(0) !== '.' && !ip_regex.test(domain_used) && domain_used !== 'localhost') + domain_used = '.' + location.hostname; + + document.cookie = key + '=' + cookie_data + '; SameSite=Strict; path=/; domain=' + + domain_used + '; expires=' + date.toGMTString() + ';'; + }, + remove: function (key) { + document.cookie = key + '=; Max-Age=0'; + } + }; + })() +}; diff --git a/assets/js/community.js b/assets/js/community.js index 44066a58..33e2e3ed 100644 --- a/assets/js/community.js +++ b/assets/js/community.js @@ -1,13 +1,6 @@ 'use strict'; var community_data = JSON.parse(document.getElementById('community_data').textContent); -String.prototype.supplant = function (o) { - return this.replace(/{([^{}]*)}/g, function (a, b) { - var r = o[b]; - return typeof r === 'string' || typeof r === 'number' ? r : a; - }); -}; - function hide_youtube_replies(event) { var target = event.target; @@ -38,13 +31,6 @@ function show_youtube_replies(event) { target.setAttribute('data-sub-text', sub_text); } -function number_with_separator(val) { - while (/(\d+)(\d{3})/.test(val.toString())) { - val = val.toString().replace(/(\d+)(\d{3})/, '$1' + ',' + '$2'); - } - return val; -} - function get_youtube_replies(target, load_more) { var continuation = target.getAttribute('data-continuation'); @@ -52,53 +38,45 @@ function get_youtube_replies(target, load_more) { var fallback = body.innerHTML; body.innerHTML = '

'; - + var url = '/api/v1/channels/comments/' + community_data.ucid + '?format=html' + '&hl=' + community_data.preferences.locale + '&thin_mode=' + community_data.preferences.thin_mode + '&continuation=' + continuation; - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('GET', url, true); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - if (load_more) { - body = body.parentNode.parentNode; - body.removeChild(body.lastElementChild); - body.innerHTML += xhr.response.contentHtml; - } else { - body.removeChild(body.lastElementChild); - - var p = document.createElement('p'); - var a = document.createElement('a'); - p.appendChild(a); - - a.href = 'javascript:void(0)'; - a.onclick = hide_youtube_replies; - a.setAttribute('data-sub-text', community_data.hide_replies_text); - a.setAttribute('data-inner-text', community_data.show_replies_text); - a.innerText = community_data.hide_replies_text; - - var div = document.createElement('div'); - div.innerHTML = xhr.response.contentHtml; - - body.appendChild(p); - body.appendChild(div); - } + helpers.xhr('GET', url, {}, { + on200: function (response) { + if (load_more) { + body = body.parentNode.parentNode; + body.removeChild(body.lastElementChild); + body.innerHTML += response.contentHtml; } else { - body.innerHTML = fallback; + body.removeChild(body.lastElementChild); + + var p = document.createElement('p'); + var a = document.createElement('a'); + p.appendChild(a); + + a.href = 'javascript:void(0)'; + a.onclick = hide_youtube_replies; + a.setAttribute('data-sub-text', community_data.hide_replies_text); + a.setAttribute('data-inner-text', community_data.show_replies_text); + a.innerText = community_data.hide_replies_text; + + var div = document.createElement('div'); + div.innerHTML = response.contentHtml; + + body.appendChild(p); + body.appendChild(div); } + }, + onNon200: function (xhr) { + body.innerHTML = fallback; + }, + onTimeout: function (xhr) { + console.warn('Pulling comments failed'); + body.innerHTML = fallback; } - }; - - xhr.ontimeout = function () { - console.warn('Pulling comments failed.'); - body.innerHTML = fallback; - }; - - xhr.send(); + }); } diff --git a/assets/js/embed.js b/assets/js/embed.js index 7e9ac605..b11b5e5a 100644 --- a/assets/js/embed.js +++ b/assets/js/embed.js @@ -1,14 +1,7 @@ 'use strict'; var video_data = JSON.parse(document.getElementById('video_data').textContent); -function get_playlist(plid, retries) { - if (retries === undefined) retries = 5; - - if (retries <= 0) { - console.warn('Failed to pull playlist'); - return; - } - +function get_playlist(plid) { var plid_url; if (plid.startsWith('RD')) { plid_url = '/api/v1/mixes/' + plid + @@ -21,85 +14,49 @@ function get_playlist(plid, retries) { '&format=html&hl=' + video_data.preferences.locale; } - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('GET', plid_url, true); + helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, { + on200: function (response) { + if (!response.nextVideo) + return; - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - if (xhr.response.nextVideo) { - player.on('ended', function () { - var url = new URL('https://example.com/embed/' + xhr.response.nextVideo); + player.on('ended', function () { + var url = new URL('https://example.com/embed/' + response.nextVideo); - url.searchParams.set('list', plid); - if (!plid.startsWith('RD')) { - url.searchParams.set('index', xhr.response.index); - } + url.searchParams.set('list', plid); + if (!plid.startsWith('RD')) + url.searchParams.set('index', response.index); + 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.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.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.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); - } - - location.assign(url.pathname + url.search); - }); - } - } + location.assign(url.pathname + url.search); + }); } - }; - - xhr.onerror = function () { - console.warn('Pulling playlist failed... ' + retries + '/5'); - setTimeout(function () { get_playlist(plid, retries - 1); }, 1000); - }; - - xhr.ontimeout = function () { - console.warn('Pulling playlist failed... ' + retries + '/5'); - get_playlist(plid, retries - 1); - }; - - xhr.send(); + }); } -window.addEventListener('load', function (e) { +addEventListener('load', function (e) { if (video_data.plid) { get_playlist(video_data.plid); } else if (video_data.video_series) { player.on('ended', function () { var url = new URL('https://example.com/embed/' + video_data.video_series.shift()); - if (video_data.params.autoplay || video_data.params.continue_autoplay) { + if (video_data.params.autoplay || video_data.params.continue_autoplay) url.searchParams.set('autoplay', '1'); - } - - if (video_data.params.listen !== video_data.preferences.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) { + 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) { + if (video_data.params.local !== video_data.preferences.local) url.searchParams.set('local', video_data.params.local); - } - - if (video_data.video_series.length !== 0) { + if (video_data.video_series.length !== 0) url.searchParams.set('playlist', video_data.video_series.join(',')); - } location.assign(url.pathname + url.search); }); diff --git a/assets/js/handlers.js b/assets/js/handlers.js index f6617b60..438832b1 100644 --- a/assets/js/handlers.js +++ b/assets/js/handlers.js @@ -1,8 +1,6 @@ 'use strict'; (function () { - var n2a = function (n) { return Array.prototype.slice.call(n); }; - var video_player = document.getElementById('player_html5_api'); if (video_player) { video_player.onmouseenter = function () { video_player['data-title'] = video_player['title']; video_player['title'] = ''; }; @@ -11,8 +9,8 @@ } // For dynamically inserted elements - document.addEventListener('click', function (e) { - if (!e || !e.target) { return; } + addEventListener('click', function (e) { + if (!e || !e.target) return; var t = e.target; var handler_name = t.getAttribute('data-onclick'); @@ -29,6 +27,7 @@ get_youtube_replies(t, load_more, load_replies); break; case 'toggle_parent': + e.preventDefault(); toggle_parent(t); break; default: @@ -36,118 +35,98 @@ } }); - n2a(document.querySelectorAll('[data-mouse="switch_classes"]')).forEach(function (e) { - var classes = e.getAttribute('data-switch-classes').split(','); - var ec = classes[0]; - var lc = classes[1]; - var onoff = function (on, off) { - var cs = e.getAttribute('class'); - cs = cs.split(off).join(on); - e.setAttribute('class', cs); - }; - e.onmouseenter = function () { onoff(ec, lc); }; - e.onmouseleave = function () { onoff(lc, ec); }; + document.querySelectorAll('[data-mouse="switch_classes"]').forEach(function (el) { + var classes = el.getAttribute('data-switch-classes').split(','); + var classOnEnter = classes[0]; + var classOnLeave = classes[1]; + function toggle_classes(toAdd, toRemove) { + el.classList.add(toAdd); + el.classList.remove(toRemove); + } + el.onmouseenter = function () { toggle_classes(classOnEnter, classOnLeave); }; + el.onmouseleave = function () { toggle_classes(classOnLeave, classOnEnter); }; }); - n2a(document.querySelectorAll('[data-onsubmit="return_false"]')).forEach(function (e) { - e.onsubmit = function () { return false; }; + document.querySelectorAll('[data-onsubmit="return_false"]').forEach(function (el) { + el.onsubmit = function () { return false; }; }); - n2a(document.querySelectorAll('[data-onclick="mark_watched"]')).forEach(function (e) { - e.onclick = function () { mark_watched(e); }; + document.querySelectorAll('[data-onclick="mark_watched"]').forEach(function (el) { + el.onclick = function () { mark_watched(el); }; }); - n2a(document.querySelectorAll('[data-onclick="mark_unwatched"]')).forEach(function (e) { - e.onclick = function () { mark_unwatched(e); }; + document.querySelectorAll('[data-onclick="mark_unwatched"]').forEach(function (el) { + el.onclick = function () { mark_unwatched(el); }; }); - n2a(document.querySelectorAll('[data-onclick="add_playlist_video"]')).forEach(function (e) { - e.onclick = function () { add_playlist_video(e); }; + document.querySelectorAll('[data-onclick="add_playlist_video"]').forEach(function (el) { + el.onclick = function () { add_playlist_video(el); }; }); - n2a(document.querySelectorAll('[data-onclick="add_playlist_item"]')).forEach(function (e) { - e.onclick = function () { add_playlist_item(e); }; + document.querySelectorAll('[data-onclick="add_playlist_item"]').forEach(function (el) { + el.onclick = function () { add_playlist_item(el); }; }); - n2a(document.querySelectorAll('[data-onclick="remove_playlist_item"]')).forEach(function (e) { - e.onclick = function () { remove_playlist_item(e); }; + document.querySelectorAll('[data-onclick="remove_playlist_item"]').forEach(function (el) { + el.onclick = function () { remove_playlist_item(el); }; }); - n2a(document.querySelectorAll('[data-onclick="revoke_token"]')).forEach(function (e) { - e.onclick = function () { revoke_token(e); }; + document.querySelectorAll('[data-onclick="revoke_token"]').forEach(function (el) { + el.onclick = function () { revoke_token(el); }; }); - n2a(document.querySelectorAll('[data-onclick="remove_subscription"]')).forEach(function (e) { - e.onclick = function () { remove_subscription(e); }; + document.querySelectorAll('[data-onclick="remove_subscription"]').forEach(function (el) { + el.onclick = function () { remove_subscription(el); }; }); - n2a(document.querySelectorAll('[data-onclick="notification_requestPermission"]')).forEach(function (e) { - e.onclick = function () { Notification.requestPermission(); }; + document.querySelectorAll('[data-onclick="notification_requestPermission"]').forEach(function (el) { + el.onclick = function () { Notification.requestPermission(); }; }); - n2a(document.querySelectorAll('[data-onrange="update_volume_value"]')).forEach(function (e) { - var cb = function () { update_volume_value(e); }; - e.oninput = cb; - e.onchange = cb; + document.querySelectorAll('[data-onrange="update_volume_value"]').forEach(function (el) { + function update_volume_value() { + document.getElementById('volume-value').innerText = el.value; + } + el.oninput = update_volume_value; + el.onchange = update_volume_value; }); - function update_volume_value(element) { - document.getElementById('volume-value').innerText = element.value; - } function revoke_token(target) { var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; row.style.display = 'none'; var count = document.getElementById('count'); - count.innerText = count.innerText - 1; + count.innerText = parseInt(count.innerText) - 1; - var referer = window.encodeURIComponent(document.location.href); var url = '/token_ajax?action_revoke_token=1&redirect=false' + - '&referer=' + referer + + '&referer=' + encodeURIComponent(location.href) + '&session=' + target.getAttribute('data-session'); - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status !== 200) { - count.innerText = parseInt(count.innerText) + 1; - row.style.display = ''; - } + var payload = 'csrf_token=' + target.parentNode.querySelector('input[name="csrf_token"]').value; + + helpers.xhr('POST', url, {payload: payload}, { + onNon200: function (xhr) { + count.innerText = parseInt(count.innerText) + 1; + row.style.display = ''; } - }; - - var csrf_token = target.parentNode.querySelector('input[name="csrf_token"]').value; - xhr.send('csrf_token=' + csrf_token); + }); } function remove_subscription(target) { var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; row.style.display = 'none'; var count = document.getElementById('count'); - count.innerText = count.innerText - 1; + count.innerText = parseInt(count.innerText) - 1; - var referer = window.encodeURIComponent(document.location.href); var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' + - '&referer=' + referer + + '&referer=' + encodeURIComponent(location.href) + '&c=' + target.getAttribute('data-ucid'); - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status !== 200) { - count.innerText = parseInt(count.innerText) + 1; - row.style.display = ''; - } + var payload = 'csrf_token=' + target.parentNode.querySelector('input[name="csrf_token"]').value; + + helpers.xhr('POST', url, {payload: payload}, { + onNon200: function (xhr) { + count.innerText = parseInt(count.innerText) + 1; + row.style.display = ''; } - }; - - var csrf_token = target.parentNode.querySelector('input[name="csrf_token"]').value; - xhr.send('csrf_token=' + csrf_token); + }); } // Handle keypresses - window.addEventListener('keydown', function (event) { + addEventListener('keydown', function (event) { // Ignore modifier keys if (event.ctrlKey || event.metaKey) return; diff --git a/assets/js/notifications.js b/assets/js/notifications.js index ec5f6dd3..f8cc750b 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -2,42 +2,20 @@ var notification_data = JSON.parse(document.getElementById('notification_data').textContent); var notifications, delivered; +var notifications_substitution = { close: function () { } }; -function get_subscriptions(callback, retries) { - if (retries === undefined) retries = 5; - - if (retries <= 0) { - return; - } - - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('GET', '/api/v1/auth/subscriptions?fields=authorId', true); - - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - var subscriptions = xhr.response; - callback(subscriptions); - } - } - }; - - xhr.onerror = function () { - console.warn('Pulling subscriptions failed... ' + retries + '/5'); - setTimeout(function () { get_subscriptions(callback, retries - 1); }, 1000); - }; - - xhr.ontimeout = function () { - console.warn('Pulling subscriptions failed... ' + retries + '/5'); - get_subscriptions(callback, retries - 1); - }; - - xhr.send(); +function get_subscriptions() { + helpers.xhr('GET', '/api/v1/auth/subscriptions?fields=authorId', { + retries: 5, + entity_name: 'subscriptions' + }, { + on200: create_notification_stream + }); } function create_notification_stream(subscriptions) { + // sse.js can't be replaced to EventSource in place as it lack support of payload and headers + // see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource notifications = new SSE( '/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', { withCredentials: true, @@ -49,9 +27,7 @@ function create_notification_stream(subscriptions) { var start_time = Math.round(new Date() / 1000); notifications.onmessage = function (event) { - if (!event.id) { - return; - } + if (!event.id) return; var notification = JSON.parse(event.data); console.info('Got notification:', notification); @@ -67,17 +43,17 @@ function create_notification_stream(subscriptions) { }); system_notification.onclick = function (event) { - window.open('/watch?v=' + event.currentTarget.tag, '_blank'); + open('/watch?v=' + event.currentTarget.tag, '_blank'); }; } delivered.push(notification.videoId); - localStorage.setItem('notification_count', parseInt(localStorage.getItem('notification_count') || '0') + 1); + helpers.storage.set('notification_count', parseInt(helpers.storage.get('notification_count') || '0') + 1); var notification_ticker = document.getElementById('notification_ticker'); - if (parseInt(localStorage.getItem('notification_count')) > 0) { + if (parseInt(helpers.storage.get('notification_count')) > 0) { notification_ticker.innerHTML = - '' + localStorage.getItem('notification_count') + ' '; + '' + helpers.storage.get('notification_count') + ' '; } else { notification_ticker.innerHTML = ''; @@ -91,35 +67,35 @@ function create_notification_stream(subscriptions) { function handle_notification_error(event) { console.warn('Something went wrong with notifications, trying to reconnect...'); - notifications = { close: function () { } }; - setTimeout(function () { get_subscriptions(create_notification_stream); }, 1000); + notifications = notifications_substitution; + setTimeout(get_subscriptions, 1000); } -window.addEventListener('load', function (e) { - localStorage.setItem('notification_count', document.getElementById('notification_count') ? document.getElementById('notification_count').innerText : '0'); +addEventListener('load', function (e) { + helpers.storage.set('notification_count', document.getElementById('notification_count') ? document.getElementById('notification_count').innerText : '0'); - if (localStorage.getItem('stream')) { - localStorage.removeItem('stream'); + if (helpers.storage.get('stream')) { + helpers.storage.remove('stream'); } else { setTimeout(function () { - if (!localStorage.getItem('stream')) { - notifications = { close: function () { } }; - localStorage.setItem('stream', true); - get_subscriptions(create_notification_stream); + if (!helpers.storage.get('stream')) { + notifications = notifications_substitution; + helpers.storage.set('stream', true); + get_subscriptions(); } }, Math.random() * 1000 + 50); } - window.addEventListener('storage', function (e) { + addEventListener('storage', function (e) { if (e.key === 'stream' && !e.newValue) { if (notifications) { - localStorage.setItem('stream', true); + helpers.storage.set('stream', true); } else { setTimeout(function () { - if (!localStorage.getItem('stream')) { - notifications = { close: function () { } }; - localStorage.setItem('stream', true); - get_subscriptions(create_notification_stream); + if (!helpers.storage.get('stream')) { + notifications = notifications_substitution; + helpers.storage.set('stream', true); + get_subscriptions(); } }, Math.random() * 1000 + 50); } @@ -137,8 +113,6 @@ window.addEventListener('load', function (e) { }); }); -window.addEventListener('unload', function (e) { - if (notifications) { - localStorage.removeItem('stream'); - } +addEventListener('unload', function (e) { + if (notifications) helpers.storage.remove('stream'); }); diff --git a/assets/js/player.js b/assets/js/player.js index 6ddb1158..07a5c128 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -42,7 +42,7 @@ embed_url = location.origin + '/embed/' + video_data.id + embed_url.search; var save_player_pos_key = 'save_player_pos'; videojs.Vhs.xhr.beforeRequest = function(options) { - if (options.uri.indexOf('videoplayback') === -1 && options.uri.indexOf('local=true') === -1) { + if (options.uri.includes('videoplayback') && options.uri.includes('local=true')) { options.uri = options.uri + '?local=true'; } return options; @@ -50,37 +50,38 @@ videojs.Vhs.xhr.beforeRequest = function(options) { var player = videojs('player', options); -player.on('error', () => { - if (video_data.params.quality !== 'dash') { - if (!player.currentSrc().includes("local=true") && !video_data.local_disabled) { - var currentSources = player.currentSources(); - for (var i = 0; i < currentSources.length; i++) { - currentSources[i]["src"] += "&local=true" - } - player.src(currentSources) - } - else if (player.error().code === 2 || player.error().code === 4) { - setTimeout(function (event) { - console.log('An error occurred in the player, reloading...'); +player.on('error', function () { + if (video_data.params.quality === 'dash') return; - var currentTime = player.currentTime(); - var playbackRate = player.playbackRate(); - var paused = player.paused(); - - player.load(); - - if (currentTime > 0.5) currentTime -= 0.5; - - player.currentTime(currentTime); - player.playbackRate(playbackRate); - - if (!paused) player.play(); - }, 10000); - } + var localNotDisabled = !player.currentSrc().includes('local=true') && !video_data.local_disabled; + var reloadMakesSense = player.error().code === MediaError.MEDIA_ERR_NETWORK || player.error().code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED; + + if (localNotDisabled) { + // add local=true to all current sources + player.src(player.currentSources().map(function (source) { + source.src += '&local=true'; + })); + } else if (reloadMakesSense) { + setTimeout(function (event) { + console.log('An error occurred in the player, reloading...'); + + var currentTime = player.currentTime(); + var playbackRate = player.playbackRate(); + var paused = player.paused(); + + player.load(); + + if (currentTime > 0.5) currentTime -= 0.5; + + player.currentTime(currentTime); + player.playbackRate(playbackRate); + + if (!paused) player.play(); + }, 10000); } }); -if (video_data.params.quality == 'dash') { +if (video_data.params.quality === 'dash') { player.reloadSourceOnError({ errorInterval: 10 }); @@ -89,7 +90,7 @@ if (video_data.params.quality == 'dash') { /** * Function for add time argument to url * @param {String} url - * @returns urlWithTimeArg + * @returns {URL} urlWithTimeArg */ function addCurrentTimeToURL(url) { var urlUsed = new URL(url); @@ -117,13 +118,6 @@ var shareOptions = { } }; -const storage = (function () { - try { if (localStorage.length !== -1) return localStorage; } - catch (e) { console.info('No storage available: ' + e); } - - return undefined; -})(); - if (location.pathname.startsWith('/embed/')) { var overlay_content = '

' + player_data.title + '

'; player.overlay({ @@ -162,7 +156,7 @@ if (isMobile()) { buttons.forEach(function (child) {primary_control_bar.removeChild(child);}); var operations_bar_element = operations_bar.el(); - operations_bar_element.className += ' mobile-operations-bar'; + operations_bar_element.classList.add('mobile-operations-bar'); player.addChild(operations_bar); // Playback menu doesn't work when it's initialized outside of the primary control bar @@ -175,8 +169,8 @@ if (isMobile()) { operations_bar_element.append(share_element); if (video_data.params.quality === 'dash') { - var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0]; - operations_bar_element.append(http_source_selector); + var http_source_selector = document.getElementsByClassName('vjs-http-source-selector vjs-menu-button')[0]; + operations_bar_element.append(http_source_selector); } }); } @@ -220,14 +214,14 @@ player.playbackRate(video_data.params.speed); * Method for getting the contents of a cookie * * @param {String} name Name of cookie - * @returns cookieValue + * @returns {String|null} cookieValue */ function getCookieValue(name) { - var value = document.cookie.split(';').filter(function (item) {return item.includes(name + '=');}); - - return (value.length >= 1) - ? value[0].substring((name + '=').length, value[0].length) - : null; + var cookiePrefix = name + '='; + var matchedCookie = document.cookie.split(';').find(function (item) {return item.includes(cookiePrefix);}); + if (matchedCookie) + return matchedCookie.replace(cookiePrefix, ''); + return null; } /** @@ -257,11 +251,11 @@ function updateCookie(newVolume, newSpeed) { date.setTime(date.getTime() + 63115200); var ipRegex = /^((\d+\.){3}\d+|[A-Fa-f0-9]*:[A-Fa-f0-9:]*:[A-Fa-f0-9:]+)$/; - var domainUsed = window.location.hostname; + var domainUsed = location.hostname; // Fix for a bug in FF where the leading dot in the FQDN is not ignored if (domainUsed.charAt(0) !== '.' && !ipRegex.test(domainUsed) && domainUsed !== 'localhost') - domainUsed = '.' + window.location.hostname; + domainUsed = '.' + location.hostname; document.cookie = 'PREFS=' + cookieData + '; SameSite=Strict; path=/; domain=' + domainUsed + '; expires=' + date.toGMTString() + ';'; @@ -280,7 +274,7 @@ player.on('volumechange', function () { player.on('waiting', function () { if (player.playbackRate() > 1 && player.liveTracker.isLive() && player.liveTracker.atLiveEdge()) { - console.info('Player has caught up to source, resetting playbackRate.'); + console.info('Player has caught up to source, resetting playbackRate'); player.playbackRate(1); } }); @@ -292,12 +286,12 @@ if (video_data.premiere_timestamp && Math.round(new Date() / 1000) < video_data. if (video_data.params.save_player_pos) { const url = new URL(location); const hasTimeParam = url.searchParams.has('t'); - const remeberedTime = get_video_time(); + const rememberedTime = get_video_time(); let lastUpdated = 0; - if(!hasTimeParam) set_seconds_after_start(remeberedTime); + if(!hasTimeParam) set_seconds_after_start(rememberedTime); - const updateTime = function () { + player.on('timeupdate', function () { const raw = player.currentTime(); const time = Math.floor(raw); @@ -305,9 +299,7 @@ if (video_data.params.save_player_pos) { save_video_time(time); lastUpdated = time; } - }; - - player.on('timeupdate', updateTime); + }); } else remove_all_video_times(); @@ -347,53 +339,31 @@ if (!video_data.params.listen && video_data.params.quality === 'dash') { targetQualityLevel = 0; break; default: - const targetHeight = Number.parseInt(video_data.params.quality_dash, 10); + const targetHeight = parseInt(video_data.params.quality_dash, 10); for (let i = 0; i < qualityLevels.length; i++) { - if (qualityLevels[i].height <= targetHeight) { + if (qualityLevels[i].height <= targetHeight) targetQualityLevel = i; - } else { + else break; - } } } - for (let i = 0; i < qualityLevels.length; i++) { - qualityLevels[i].enabled = (i === targetQualityLevel); - } + qualityLevels.forEach(function (level, index) { + level.enabled = (index === targetQualityLevel); + }); }); }); } } player.vttThumbnails({ - src: location.origin + '/api/v1/storyboards/' + video_data.id + '?height=90', + src: '/api/v1/storyboards/' + video_data.id + '?height=90', showTimestamp: true }); // Enable annotations if (!video_data.params.listen && video_data.params.annotations) { - window.addEventListener('load', function (e) { - var video_container = document.getElementById('player'); - let xhr = new XMLHttpRequest(); - xhr.responseType = 'text'; - xhr.timeout = 60000; - xhr.open('GET', '/api/v1/annotations/' + video_data.id, true); - - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - videojs.registerPlugin('youtubeAnnotationsPlugin', youtubeAnnotationsPlugin); - if (!player.paused()) { - player.youtubeAnnotationsPlugin({ annotationXml: xhr.response, videoContainer: video_container }); - } else { - player.one('play', function (event) { - player.youtubeAnnotationsPlugin({ annotationXml: xhr.response, videoContainer: video_container }); - }); - } - } - } - }; - - window.addEventListener('__ar_annotation_click', function (e) { + addEventListener('load', function (e) { + addEventListener('__ar_annotation_click', function (e) { const url = e.detail.url, target = e.detail.target, seconds = e.detail.seconds; @@ -406,41 +376,48 @@ if (!video_data.params.listen && video_data.params.annotations) { path = path.pathname + path.search; if (target === 'current') { - window.location.href = path; + location.href = path; } else if (target === 'new') { - window.open(path, '_blank'); + open(path, '_blank'); + } + }); + + helpers.xhr('GET', '/api/v1/annotations/' + video_data.id, { + responseType: 'text', + timeout: 60000 + }, { + on200: function (response) { + var video_container = document.getElementById('player'); + videojs.registerPlugin('youtubeAnnotationsPlugin', youtubeAnnotationsPlugin); + if (player.paused()) { + player.one('play', function (event) { + player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container }); + }); + } else { + player.youtubeAnnotationsPlugin({ annotationXml: response, videoContainer: video_container }); + } } }); - 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; - } + const newVolume = curVolume + delta; + helpers.clamp(newVolume, 0, 1); player.volume(newVolume); } function toggle_muted() { - const isMuted = player.muted(); - player.muted(!isMuted); + player.muted(!player.muted()); } 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; - } + const newTime = curTime + delta; + helpers.clamp(newTime, 0, duration); player.currentTime(newTime); } @@ -455,52 +432,24 @@ function save_video_time(seconds) { all_video_times[videoId] = seconds; - set_all_video_times(all_video_times); + helpers.storage.set(save_player_pos_key, JSON.stringify(all_video_times)); } function get_video_time() { - try { - const videoId = video_data.id; - const all_video_times = get_all_video_times(); - const timestamp = all_video_times[videoId]; + const videoId = video_data.id; + const all_video_times = get_all_video_times(); + const timestamp = all_video_times[videoId]; - return timestamp || 0; - } - catch (e) { - return 0; - } -} - -function set_all_video_times(times) { - if (storage) { - if (times) { - try { - storage.setItem(save_player_pos_key, JSON.stringify(times)); - } catch (e) { - console.warn('set_all_video_times: ' + e); - } - } else { - storage.removeItem(save_player_pos_key); - } - } + return timestamp || 0; } function get_all_video_times() { - if (storage) { - const raw = storage.getItem(save_player_pos_key); - if (raw !== null) { - try { - return JSON.parse(raw); - } catch (e) { - console.warn('get_all_video_times: ' + e); - } - } - } - return {}; + const raw = helpers.storage.get(save_player_pos_key); + return raw ? JSON.parse(raw) : {}; } function remove_all_video_times() { - set_all_video_times(null); + helpers.storage.remove(save_player_pos_key); } function set_time_percent(percent) { @@ -516,21 +465,23 @@ function toggle_play() { player.paused() ? play() : pause(); } const toggle_captions = (function () { let toggledTrack = null; - const onChange = function (e) { - toggledTrack = null; - }; - const bindChange = function (onOrOff) { - player.textTracks()[onOrOff]('change', onChange); - }; + + function bindChange(onOrOff) { + player.textTracks()[onOrOff]('change', function (e) { + toggledTrack = null; + }); + } + // 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) { + function setMode(track, mode) { bindChange('off'); track.mode = mode; - window.setTimeout(function () { + setTimeout(function () { bindChange('on'); }, 0); - }; + } + bindChange('on'); return function () { if (toggledTrack !== null) { @@ -577,16 +528,12 @@ function toggle_fullscreen() { 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; - } + const newIndex = curIndex + steps; + helpers.clamp(newIndex, 0, maxIndex); player.playbackRate(options.playbackRates[newIndex]); } -window.addEventListener('keydown', function (e) { +addEventListener('keydown', function (e) { if (e.target.tagName.toLowerCase() === 'input') { // Ignore input when focus is on certain elements, e.g. form fields. return; @@ -673,12 +620,11 @@ window.addEventListener('keydown', function (e) { // 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; + // TODO: More precise step. Now FPS is taken equal to 29.97 + // Common FPS: https://forum.videohelp.com/threads/81868#post323588 + // Possible solution is new HTMLVideoElement.requestVideoFrameCallback() https://wicg.github.io/video-rvfc/ + case '.': action = function () { pause(); skip_seconds(-1/29.97); }; break; + case ',': action = function () { pause(); skip_seconds( 1/29.97); }; break; case '>': action = increase_playback_rate.bind(this, 1); break; case '<': action = increase_playback_rate.bind(this, -1); break; @@ -697,10 +643,6 @@ window.addEventListener('keydown', function (e) { // 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; @@ -710,39 +652,23 @@ window.addEventListener('keydown', function (e) { 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; - } - + function mouseScroll(event) { // 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 (!player.controls() || !volumeHover) return; - if (delta === 1) { - increase_volume(volumeStep); - } else if (delta === -1) { - increase_volume(-volumeStep); - } - } - } - } - }; + event.preventDefault(); + var wheelMove = event.wheelDelta || -event.detail; + var volumeSign = Math.sign(wheelMove); + + increase_volume(volumeSign * 0.05); // decrease/increase by 5% + } player.on('mousewheel', mouseScroll); player.on('DOMMouseScroll', mouseScroll); }()); // Since videojs-share can sometimes be blocked, we defer it until last -if (player.share) { - player.share(shareOptions); -} +if (player.share) player.share(shareOptions); // show the preferred caption by default if (player_data.preferred_caption_found) { @@ -763,7 +689,7 @@ if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) { } // Watch on Invidious link -if (window.location.pathname.startsWith('/embed/')) { +if (location.pathname.startsWith('/embed/')) { const Button = videojs.getComponent('Button'); let watch_on_invidious_button = new Button(player); diff --git a/assets/js/playlist_widget.js b/assets/js/playlist_widget.js index c2565874..8f8da6d5 100644 --- a/assets/js/playlist_widget.js +++ b/assets/js/playlist_widget.js @@ -1,5 +1,6 @@ 'use strict'; var playlist_data = JSON.parse(document.getElementById('playlist_data').textContent); +var payload = 'csrf_token=' + playlist_data.csrf_token; function add_playlist_video(target) { var select = target.parentNode.children[0].children[1]; @@ -8,21 +9,12 @@ function add_playlist_video(target) { var url = '/playlist_ajax?action_add_video=1&redirect=false' + '&video_id=' + target.getAttribute('data-id') + '&playlist_id=' + option.getAttribute('data-plid'); - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - option.innerText = '✓' + option.innerText; - } + helpers.xhr('POST', url, {payload: payload}, { + on200: function (response) { + option.innerText = '✓' + option.innerText; } - }; - - xhr.send('csrf_token=' + playlist_data.csrf_token); + }); } function add_playlist_item(target) { @@ -32,21 +24,12 @@ function add_playlist_item(target) { var url = '/playlist_ajax?action_add_video=1&redirect=false' + '&video_id=' + target.getAttribute('data-id') + '&playlist_id=' + target.getAttribute('data-plid'); - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status !== 200) { - tile.style.display = ''; - } + helpers.xhr('POST', url, {payload: payload}, { + onNon200: function (xhr) { + tile.style.display = ''; } - }; - - xhr.send('csrf_token=' + playlist_data.csrf_token); + }); } function remove_playlist_item(target) { @@ -56,19 +39,10 @@ function remove_playlist_item(target) { var url = '/playlist_ajax?action_remove_video=1&redirect=false' + '&set_video_id=' + target.getAttribute('data-index') + '&playlist_id=' + target.getAttribute('data-plid'); - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status !== 200) { - tile.style.display = ''; - } + helpers.xhr('POST', url, {payload: payload}, { + onNon200: function (xhr) { + tile.style.display = ''; } - }; - - xhr.send('csrf_token=' + playlist_data.csrf_token); + }); } diff --git a/assets/js/subscribe_widget.js b/assets/js/subscribe_widget.js index 45ff5706..7665a00b 100644 --- a/assets/js/subscribe_widget.js +++ b/assets/js/subscribe_widget.js @@ -1,8 +1,9 @@ 'use strict'; var subscribe_data = JSON.parse(document.getElementById('subscribe_data').textContent); +var payload = 'csrf_token=' + subscribe_data.csrf_token; var subscribe_button = document.getElementById('subscribe'); -subscribe_button.parentNode['action'] = 'javascript:void(0)'; +subscribe_button.parentNode.action = 'javascript:void(0)'; if (subscribe_button.getAttribute('data-type') === 'subscribe') { subscribe_button.onclick = subscribe; @@ -10,87 +11,34 @@ if (subscribe_button.getAttribute('data-type') === 'subscribe') { subscribe_button.onclick = unsubscribe; } -function subscribe(retries) { - if (retries === undefined) retries = 5; - - if (retries <= 0) { - console.warn('Failed to subscribe.'); - return; - } - - var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' + - '&c=' + subscribe_data.ucid; - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - +function subscribe() { var fallback = subscribe_button.innerHTML; subscribe_button.onclick = unsubscribe; subscribe_button.innerHTML = '' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + ''; - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status !== 200) { - subscribe_button.onclick = subscribe; - subscribe_button.innerHTML = fallback; - } + var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' + + '&c=' + subscribe_data.ucid; + + helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, { + onNon200: function (xhr) { + subscribe_button.onclick = subscribe; + subscribe_button.innerHTML = fallback; } - }; - - xhr.onerror = function () { - console.warn('Subscribing failed... ' + retries + '/5'); - setTimeout(function () { subscribe(retries - 1); }, 1000); - }; - - xhr.ontimeout = function () { - console.warn('Subscribing failed... ' + retries + '/5'); - subscribe(retries - 1); - }; - - xhr.send('csrf_token=' + subscribe_data.csrf_token); + }); } -function unsubscribe(retries) { - if (retries === undefined) - retries = 5; - - if (retries <= 0) { - console.warn('Failed to subscribe'); - return; - } - - var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' + - '&c=' + subscribe_data.ucid; - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - +function unsubscribe() { var fallback = subscribe_button.innerHTML; subscribe_button.onclick = subscribe; subscribe_button.innerHTML = '' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + ''; - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status !== 200) { - subscribe_button.onclick = unsubscribe; - subscribe_button.innerHTML = fallback; - } + var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' + + '&c=' + subscribe_data.ucid; + + helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, { + onNon200: function (xhr) { + subscribe_button.onclick = unsubscribe; + subscribe_button.innerHTML = fallback; } - }; - - xhr.onerror = function () { - console.warn('Unsubscribing failed... ' + retries + '/5'); - setTimeout(function () { unsubscribe(retries - 1); }, 1000); - }; - - xhr.ontimeout = function () { - console.warn('Unsubscribing failed... ' + retries + '/5'); - unsubscribe(retries - 1); - }; - - xhr.send('csrf_token=' + subscribe_data.csrf_token); + }); } diff --git a/assets/js/themes.js b/assets/js/themes.js index 3f503b38..7e86e9ac 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -1,60 +1,48 @@ 'use strict'; var toggle_theme = document.getElementById('toggle_theme'); -toggle_theme.href = 'javascript:void(0);'; +toggle_theme.href = 'javascript:void(0)'; toggle_theme.addEventListener('click', function () { var dark_mode = document.body.classList.contains('light-theme'); - var url = '/toggle_theme?redirect=false'; - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('GET', url, true); - set_mode(dark_mode); - try { - window.localStorage.setItem('dark_mode', dark_mode ? 'dark' : 'light'); - } catch (e) {} + helpers.storage.set('dark_mode', dark_mode ? 'dark' : 'light'); - xhr.send(); + helpers.xhr('GET', '/toggle_theme?redirect=false', {}, {}); }); -window.addEventListener('storage', function (e) { +// Handles theme change event caused by other tab +addEventListener('storage', function (e) { if (e.key === 'dark_mode') { update_mode(e.newValue); } }); -window.addEventListener('DOMContentLoaded', function () { +addEventListener('DOMContentLoaded', function () { const dark_mode = document.getElementById('dark_mode_pref').textContent; - try { - // Update localStorage if dark mode preference changed on preferences page - window.localStorage.setItem('dark_mode', dark_mode); - } catch (e) {} + // Update storage if dark mode preference changed on preferences page + helpers.storage.set('dark_mode', dark_mode); update_mode(dark_mode); }); -var darkScheme = window.matchMedia('(prefers-color-scheme: dark)'); -var lightScheme = window.matchMedia('(prefers-color-scheme: light)'); +var darkScheme = matchMedia('(prefers-color-scheme: dark)'); +var lightScheme = matchMedia('(prefers-color-scheme: light)'); darkScheme.addListener(scheme_switch); lightScheme.addListener(scheme_switch); function scheme_switch (e) { - // ignore this method if we have a preference set - try { - if (localStorage.getItem('dark_mode')) { - return; - } - } catch (exception) {} - if (e.matches) { + // ignore this method if we have a preference set + if (helpers.storage.get('dark_mode')) return; + + if (!e.matches) return; + if (e.media.includes('dark')) { - set_mode(true); + set_mode(true); } else if (e.media.includes('light')) { - set_mode(false); + set_mode(false); } - } } function set_mode (bool) { @@ -82,7 +70,7 @@ function update_mode (mode) { // If preference for light mode indicated set_mode(false); } - else if (document.getElementById('dark_mode_pref').textContent === '' && window.matchMedia('(prefers-color-scheme: dark)').matches) { + else if (document.getElementById('dark_mode_pref').textContent === '' && 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); } diff --git a/assets/js/watch.js b/assets/js/watch.js index 29d58be5..ff0f7822 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -1,5 +1,7 @@ 'use strict'; var video_data = JSON.parse(document.getElementById('video_data').textContent); +var spinnerHTML = '

'; +var spinnerHTMLwithHR = spinnerHTML + '
'; String.prototype.supplant = function (o) { return this.replace(/{([^{}]*)}/g, function (a, b) { @@ -10,24 +12,24 @@ String.prototype.supplant = function (o) { function toggle_parent(target) { var body = target.parentNode.parentNode.children[1]; - if (body.style.display === null || body.style.display === '') { - target.textContent = '[ + ]'; - body.style.display = 'none'; - } else { + if (body.style.display === 'none') { target.textContent = '[ − ]'; body.style.display = ''; + } else { + target.textContent = '[ + ]'; + body.style.display = 'none'; } } function toggle_comments(event) { var target = event.target; var body = target.parentNode.parentNode.parentNode.children[1]; - if (body.style.display === null || body.style.display === '') { - target.textContent = '[ + ]'; - body.style.display = 'none'; - } else { + if (body.style.display === 'none') { target.textContent = '[ − ]'; body.style.display = ''; + } else { + target.textContent = '[ + ]'; + body.style.display = 'none'; } } @@ -79,31 +81,22 @@ if (continue_button) { 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) { + if (video_data.params.autoplay || video_data.params.continue_autoplay) url.searchParams.set('autoplay', '1'); - } - - if (video_data.params.listen !== video_data.preferences.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) { + 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) { + 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); } function continue_autoplay(event) { if (event.target.checked) { - player.on('ended', function () { - next_video(); - }); + player.on('ended', next_video); } else { player.off('ended'); } @@ -116,19 +109,10 @@ function number_with_separator(val) { return val; } -function get_playlist(plid, retries) { - if (retries === undefined) retries = 5; +function get_playlist(plid) { var playlist = document.getElementById('playlist'); - if (retries <= 0) { - console.warn('Failed to pull playlist'); - playlist.innerHTML = ''; - return; - } - - playlist.innerHTML = ' \ -

\ -
'; + playlist.innerHTML = spinnerHTMLwithHR; var plid_url; if (plid.startsWith('RD')) { @@ -142,225 +126,144 @@ function get_playlist(plid, retries) { '&format=html&hl=' + video_data.preferences.locale; } - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('GET', plid_url, true); + helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, { + on200: function (response) { + playlist.innerHTML = response.playlistHtml; - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - playlist.innerHTML = xhr.response.playlistHtml; - var nextVideo = document.getElementById(xhr.response.nextVideo); - nextVideo.parentNode.parentNode.scrollTop = nextVideo.offsetTop; + if (!response.nextVideo) return; - if (xhr.response.nextVideo) { - player.on('ended', function () { - var url = new URL('https://example.com/watch?v=' + xhr.response.nextVideo); + var nextVideo = document.getElementById(response.nextVideo); + nextVideo.parentNode.parentNode.scrollTop = nextVideo.offsetTop; - url.searchParams.set('list', plid); - if (!plid.startsWith('RD')) { - url.searchParams.set('index', xhr.response.index); - } + player.on('ended', function () { + var url = new URL('https://example.com/watch?v=' + response.nextVideo); - if (video_data.params.autoplay || video_data.params.continue_autoplay) { - url.searchParams.set('autoplay', '1'); - } + url.searchParams.set('list', plid); + if (!plid.startsWith('RD')) + url.searchParams.set('index', response.index); + 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.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.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.local !== video_data.preferences.local) { - url.searchParams.set('local', video_data.params.local); - } - - location.assign(url.pathname + url.search); - }); - } - } else { - playlist.innerHTML = ''; - document.getElementById('continue').style.display = ''; - } + location.assign(url.pathname + url.search); + }); + }, + onNon200: function (xhr) { + playlist.innerHTML = ''; + document.getElementById('continue').style.display = ''; + }, + onError: function (xhr) { + playlist.innerHTML = spinnerHTMLwithHR; + }, + onTimeout: function (xhr) { + playlist.innerHTML = spinnerHTMLwithHR; } - }; - - xhr.onerror = function () { - playlist = document.getElementById('playlist'); - playlist.innerHTML = - '


'; - - console.warn('Pulling playlist timed out... ' + retries + '/5'); - setTimeout(function () { get_playlist(plid, retries - 1); }, 1000); - }; - - xhr.ontimeout = function () { - playlist = document.getElementById('playlist'); - playlist.innerHTML = - '


'; - - console.warn('Pulling playlist timed out... ' + retries + '/5'); - get_playlist(plid, retries - 1); - }; - - xhr.send(); + }); } -function get_reddit_comments(retries) { - if (retries === undefined) retries = 5; +function get_reddit_comments() { var comments = document.getElementById('comments'); - if (retries <= 0) { - console.warn('Failed to pull comments'); - comments.innerHTML = ''; - return; - } - var fallback = comments.innerHTML; - comments.innerHTML = - '

'; + comments.innerHTML = spinnerHTML; var url = '/api/v1/comments/' + video_data.id + '?source=reddit&format=html' + '&hl=' + video_data.preferences.locale; - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('GET', url, true); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - comments.innerHTML = ' \ -
\ -

\ - [ − ] \ - {title} \ -

\ -

\ - \ - \ - {youtubeCommentsText} \ - \ - \ -

\ + var onNon200 = function (xhr) { comments.innerHTML = fallback; }; + if (video_data.params.comments[1] === 'youtube') + onNon200 = function (xhr) {}; + + helpers.xhr('GET', url, {retries: 5, entity_name: ''}, { + on200: function (response) { + comments.innerHTML = ' \ +
\ +

\ + [ − ] \ + {title} \ +

\ +

\ \ - {redditPermalinkText} \ + \ + {youtubeCommentsText} \ + \ \ -

\ -
{contentHtml}
\ -
'.supplant({ - title: xhr.response.title, - youtubeCommentsText: video_data.youtube_comments_text, - redditPermalinkText: video_data.reddit_permalink_text, - permalink: xhr.response.permalink, - contentHtml: xhr.response.contentHtml - }); +

\ + \ + {redditPermalinkText} \ + \ +
\ +
{contentHtml}
\ +
'.supplant({ + title: response.title, + youtubeCommentsText: video_data.youtube_comments_text, + redditPermalinkText: video_data.reddit_permalink_text, + permalink: response.permalink, + contentHtml: response.contentHtml + }); - comments.children[0].children[0].children[0].onclick = toggle_comments; - comments.children[0].children[1].children[0].onclick = swap_comments; - } else { - if (video_data.params.comments[1] === 'youtube') { - console.warn('Pulling comments failed... ' + retries + '/5'); - setTimeout(function () { get_youtube_comments(retries - 1); }, 1000); - } else { - comments.innerHTML = fallback; - } - } - } - }; - - xhr.onerror = function () { - console.warn('Pulling comments failed... ' + retries + '/5'); - setTimeout(function () { get_reddit_comments(retries - 1); }, 1000); - }; - - xhr.ontimeout = function () { - console.warn('Pulling comments failed... ' + retries + '/5'); - get_reddit_comments(retries - 1); - }; - - xhr.send(); + comments.children[0].children[0].children[0].onclick = toggle_comments; + comments.children[0].children[1].children[0].onclick = swap_comments; + }, + onNon200: onNon200, // declared above + }); } -function get_youtube_comments(retries) { - if (retries === undefined) retries = 5; +function get_youtube_comments() { var comments = document.getElementById('comments'); - if (retries <= 0) { - console.warn('Failed to pull comments'); - comments.innerHTML = ''; - return; - } - var fallback = comments.innerHTML; - comments.innerHTML = - '

'; + comments.innerHTML = spinnerHTML; var url = '/api/v1/comments/' + video_data.id + '?format=html' + '&hl=' + video_data.preferences.locale + '&thin_mode=' + video_data.preferences.thin_mode; - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('GET', url, true); + + var onNon200 = function (xhr) { comments.innerHTML = fallback; }; + if (video_data.params.comments[1] === 'youtube') + onNon200 = function (xhr) {}; + + helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, { + on200: function (response) { + comments.innerHTML = ' \ +
\ +

\ + [ − ] \ + {commentsText} \ +

\ + \ + \ + {redditComments} \ + \ + \ +
\ +
{contentHtml}
\ +
'.supplant({ + contentHtml: response.contentHtml, + redditComments: video_data.reddit_comments_text, + commentsText: video_data.comments_text.supplant( + { commentCount: number_with_separator(response.commentCount) } + ) + }); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - comments.innerHTML = ' \ -
\ -

\ - [ − ] \ - {commentsText} \ -

\ - \ - \ - {redditComments} \ - \ - \ -
\ -
{contentHtml}
\ -
'.supplant({ - contentHtml: xhr.response.contentHtml, - redditComments: video_data.reddit_comments_text, - commentsText: video_data.comments_text.supplant( - { commentCount: number_with_separator(xhr.response.commentCount) } - ) - }); - - comments.children[0].children[0].children[0].onclick = toggle_comments; - comments.children[0].children[1].children[0].onclick = swap_comments; - } else { - if (video_data.params.comments[1] === 'youtube') { - setTimeout(function () { get_youtube_comments(retries - 1); }, 1000); - } else { - comments.innerHTML = ''; - } - } + comments.children[0].children[0].children[0].onclick = toggle_comments; + comments.children[0].children[1].children[0].onclick = swap_comments; + }, + onNon200: onNon200, // declared above + onError: function (xhr) { + comments.innerHTML = spinnerHTML; + }, + onTimeout: function (xhr) { + comments.innerHTML = spinnerHTML; } - }; - - xhr.onerror = function () { - comments.innerHTML = - '

'; - console.warn('Pulling comments failed... ' + retries + '/5'); - setTimeout(function () { get_youtube_comments(retries - 1); }, 1000); - }; - - xhr.ontimeout = function () { - comments.innerHTML = - '

'; - console.warn('Pulling comments failed... ' + retries + '/5'); - get_youtube_comments(retries - 1); - }; - - xhr.send(); + }); } function get_youtube_replies(target, load_more, load_replies) { @@ -368,91 +271,72 @@ function get_youtube_replies(target, load_more, load_replies) { var body = target.parentNode.parentNode; var fallback = body.innerHTML; - body.innerHTML = - '

'; + body.innerHTML = spinnerHTML; var url = '/api/v1/comments/' + video_data.id + '?format=html' + '&hl=' + video_data.preferences.locale + '&thin_mode=' + video_data.preferences.thin_mode + '&continuation=' + continuation; - if (load_replies) { - url += '&action=action_get_comment_replies'; - } - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('GET', url, true); + if (load_replies) url += '&action=action_get_comment_replies'; - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - if (load_more) { - body = body.parentNode.parentNode; - body.removeChild(body.lastElementChild); - body.innerHTML += xhr.response.contentHtml; - } else { - body.removeChild(body.lastElementChild); - - var p = document.createElement('p'); - var a = document.createElement('a'); - p.appendChild(a); - - a.href = 'javascript:void(0)'; - a.onclick = hide_youtube_replies; - a.setAttribute('data-sub-text', video_data.hide_replies_text); - a.setAttribute('data-inner-text', video_data.show_replies_text); - a.innerText = video_data.hide_replies_text; - - var div = document.createElement('div'); - div.innerHTML = xhr.response.contentHtml; - - body.appendChild(p); - body.appendChild(div); - } + helpers.xhr('GET', url, {}, { + on200: function (response) { + if (load_more) { + body = body.parentNode.parentNode; + body.removeChild(body.lastElementChild); + body.innerHTML += response.contentHtml; } else { - body.innerHTML = fallback; + body.removeChild(body.lastElementChild); + + var p = document.createElement('p'); + var a = document.createElement('a'); + p.appendChild(a); + + a.href = 'javascript:void(0)'; + a.onclick = hide_youtube_replies; + a.setAttribute('data-sub-text', video_data.hide_replies_text); + a.setAttribute('data-inner-text', video_data.show_replies_text); + a.innerText = video_data.hide_replies_text; + + var div = document.createElement('div'); + div.innerHTML = response.contentHtml; + + body.appendChild(p); + body.appendChild(div); } + }, + onNon200: function (xhr) { + body.innerHTML = fallback; + }, + onTimeout: function (xhr) { + console.warn('Pulling comments failed'); + body.innerHTML = fallback; } - }; - - xhr.ontimeout = function () { - console.warn('Pulling comments failed.'); - body.innerHTML = fallback; - }; - - xhr.send(); + }); } if (video_data.play_next) { player.on('ended', function () { var url = new URL('https://example.com/watch?v=' + video_data.next_video); - if (video_data.params.autoplay || video_data.params.continue_autoplay) { + if (video_data.params.autoplay || video_data.params.continue_autoplay) url.searchParams.set('autoplay', '1'); - } - - if (video_data.params.listen !== video_data.preferences.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) { + 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) { + 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); }); } -window.addEventListener('load', function (e) { - if (video_data.plid) { +addEventListener('load', function (e) { + if (video_data.plid) get_playlist(video_data.plid); - } if (video_data.params.comments[0] === 'youtube') { get_youtube_comments(); diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js index 87989a79..497b1878 100644 --- a/assets/js/watched_widget.js +++ b/assets/js/watched_widget.js @@ -1,5 +1,6 @@ 'use strict'; var watched_data = JSON.parse(document.getElementById('watched_data').textContent); +var payload = 'csrf_token=' + watched_data.csrf_token; function mark_watched(target) { var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; @@ -7,45 +8,27 @@ function mark_watched(target) { var url = '/watch_ajax?action_mark_watched=1&redirect=false' + '&id=' + target.getAttribute('data-id'); - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status !== 200) { - tile.style.display = ''; - } + helpers.xhr('POST', url, {payload: payload}, { + onNon200: function (xhr) { + tile.style.display = ''; } - }; - - xhr.send('csrf_token=' + watched_data.csrf_token); + }); } function mark_unwatched(target) { var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; tile.style.display = 'none'; var count = document.getElementById('count'); - count.innerText = count.innerText - 1; + count.innerText = parseInt(count.innerText) - 1; var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' + '&id=' + target.getAttribute('data-id'); - var xhr = new XMLHttpRequest(); - xhr.responseType = 'json'; - xhr.timeout = 10000; - xhr.open('POST', url, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status !== 200) { - count.innerText = count.innerText - 1 + 2; - tile.style.display = ''; - } + helpers.xhr('POST', url, {payload: payload}, { + onNon200: function (xhr) { + count.innerText = parseInt(count.innerText) + 1; + tile.style.display = ''; } - }; - - xhr.send('csrf_token=' + watched_data.csrf_token); + }); } diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 1f8de657..f50b5907 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -481,7 +481,7 @@ def template_reddit_comments(root, locale) html << <<-END_HTML

- [ - ] + [ − ] #{child.author} #{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)} #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index 22870317..758f3995 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -29,6 +29,7 @@ }.to_pretty_json %> +

diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr index 3bc29e55..154c40b5 100644 --- a/src/invidious/views/community.ecr +++ b/src/invidious/views/community.ecr @@ -93,4 +93,5 @@ }.to_pretty_json %> + diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr index fffefc9a..483807d7 100644 --- a/src/invidious/views/components/player.ecr +++ b/src/invidious/views/components/player.ecr @@ -66,4 +66,5 @@ }.to_pretty_json %> + diff --git a/src/invidious/views/components/subscribe_widget.ecr b/src/invidious/views/components/subscribe_widget.ecr index b9d5f783..7a8c7fda 100644 --- a/src/invidious/views/components/subscribe_widget.ecr +++ b/src/invidious/views/components/subscribe_widget.ecr @@ -31,6 +31,7 @@ }.to_pretty_json %> + <% else %>

diff --git a/src/invidious/views/embed.ecr b/src/invidious/views/embed.ecr index ce5ff7f0..82f80f9d 100644 --- a/src/invidious/views/embed.ecr +++ b/src/invidious/views/embed.ecr @@ -31,6 +31,7 @@ <%= rendered "components/player" %> + diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 6c1243c5..51dd78bd 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -25,6 +25,7 @@ }.to_pretty_json %> +

diff --git a/src/invidious/views/feeds/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr index 8d56ad14..957277fa 100644 --- a/src/invidious/views/feeds/subscriptions.ecr +++ b/src/invidious/views/feeds/subscriptions.ecr @@ -50,6 +50,7 @@ }.to_pretty_json %> +
diff --git a/src/invidious/views/licenses.ecr b/src/invidious/views/licenses.ecr index 861913d0..25b24ed4 100644 --- a/src/invidious/views/licenses.ecr +++ b/src/invidious/views/licenses.ecr @@ -9,6 +9,20 @@

<%= translate(locale, "JavaScript license information") %>

+ + + + + + + +
+ _helpers.js + + AGPL-3.0 + + <%= translate(locale, "source") %> +
community.js diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index df3112db..641cbe2c 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -97,6 +97,7 @@ }.to_pretty_json %> + <% end %> diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index bd908dd6..79decbe6 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -157,6 +157,7 @@
+ <% if env.get? "user" %> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 8b6eb903..e6a14d0f 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -165,6 +165,7 @@ we're going to need to do it here in order to allow for translations. }.to_pretty_json %> + <% end %> <% end %> @@ -303,4 +304,5 @@ we're going to need to do it here in order to allow for translations. <% end %> + From 835237382fd2e316a5e118dafc929f6ffb8e33fd Mon Sep 17 00:00:00 2001 From: meow Date: Fri, 6 May 2022 06:16:41 +0300 Subject: [PATCH 02/42] fix helpers --- assets/js/_helpers.js | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js index 04576348..4583dbe3 100644 --- a/assets/js/_helpers.js +++ b/assets/js/_helpers.js @@ -25,7 +25,7 @@ Math.sign = Math.sign || function(x) { }; // Monstrous global variable for handy code -helpers = helpers || { +window.helpers = window.helpers || { /** * https://en.wikipedia.org/wiki/Clamping_(graphics) * @param {Number} num Source number @@ -38,9 +38,9 @@ helpers = helpers || { var t = max; max = min; min = t; // swap max and min } - if (max > num) + if (max < num) return max; - if (min < num) + if (min > num) return min; return num; }, @@ -62,14 +62,17 @@ helpers = helpers || { if (method === 'POST') xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = function () { - if (xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 200) - if (callbacks.on200) - callbacks.on200(xhr.response); - else - if (callbacks.onNon200) - callbacks.onNon200(xhr); + // better than onreadystatechange because of 404 codes https://stackoverflow.com/a/36182963 + xhr.onloadend = function () { + if (xhr.status === 200) { + if (callbacks.on200) + callbacks.on200(xhr.response); + } else { + // handled by onerror + if (xhr.status === 0) return; + + if (callbacks.onNon200) + callbacks.onNon200(xhr); } }; @@ -89,14 +92,14 @@ helpers = helpers || { xhr.send(); }, /** @private */ - _xhrRetry(method, url, options, callbacks) { + _xhrRetry: function(method, url, options, callbacks) { if (options.retries <= 0) { console.warn('Failed to pull', options.entity_name); if (callbacks.onTotalFail) callbacks.onTotalFail(); return; } - helpers.xhr(method, url, options, callbacks); + helpers._xhr(method, url, options, callbacks); }, /** * @callback callbackXhrOn200 @@ -123,18 +126,19 @@ helpers = helpers || { * @param {callbackXhrError} [callbacks.onError] * @param {callbackXhrError} [callbacks.onTotalFail] - if failed after all retries */ - xhr(method, url, options, callbacks) { - if (options.retries > 1) { + xhr: function(method, url, options, callbacks) { + if (!options.retries || options.retries <= 1) { helpers._xhr(method, url, options, callbacks); return; } if (!options.entity_name) options.entity_name = 'unknown'; - if (!options.retry_timeout) options.retry_timeout = 1; + if (!options.retry_timeout) options.retry_timeout = 1000; const retries_total = options.retries; + let currentTry = 1; const retry = function () { - console.warn('Pulling ' + options.entity_name + ' failed... ' + options.retries + '/' + retries_total); + console.warn('Pulling ' + options.entity_name + ' failed... ' + (currentTry++) + '/' + retries_total); setTimeout(function () { options.retries--; helpers._xhrRetry(method, url, options, callbacks); From fd890f9c0a78635a3ea1ab56ec5a735fef27c1c4 Mon Sep 17 00:00:00 2001 From: meow Date: Fri, 6 May 2022 07:21:19 +0300 Subject: [PATCH 03/42] fix helpers storage --- assets/js/_helpers.js | 28 +++++++++++----------------- assets/js/notifications.js | 9 +++++++-- assets/js/player.js | 5 ++--- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js index 4583dbe3..838a4612 100644 --- a/assets/js/_helpers.js +++ b/assets/js/_helpers.js @@ -171,7 +171,7 @@ window.helpers = window.helpers || { */ /** - * Universal storage proxy. Uses inside localStorage or cookies + * Universal storage, stores and returns JS objects. Uses inside localStorage or cookies * @type {invidiousStorage} */ storage: (function () { @@ -181,8 +181,8 @@ window.helpers = window.helpers || { if (localStorageIsUsable) { return { - get: function (key) { return localStorage[key]; }, - set: function (key, value) { localStorage[key] = value; }, + get: function (key) { if (localStorage[key]) return JSON.parse(decodeURIComponent(localStorage[key])); }, + set: function (key, value) { localStorage[key] = encodeURIComponent(JSON.stringify(value)); }, remove: function (key) { localStorage.removeItem(key); } }; } @@ -192,27 +192,21 @@ window.helpers = window.helpers || { get: function (key) { const cookiePrefix = key + '='; function findCallback(cookie) {return cookie.startsWith(cookiePrefix);} - const matchedCookie = document.cookie.split(';').find(findCallback); - if (matchedCookie) - return matchedCookie.replace(cookiePrefix, ''); - return null; + const matchedCookie = document.cookie.split('; ').find(findCallback); + if (matchedCookie) { + const cookieBody = matchedCookie.replace(cookiePrefix, ''); + if (cookieBody.length === 0) return; + return JSON.parse(decodeURIComponent(cookieBody)); + } }, set: function (key, value) { const cookie_data = encodeURIComponent(JSON.stringify(value)); // Set expiration in 2 year const date = new Date(); - date.setTime(date.getTime() + 2*365.25*24*60*60); + date.setFullYear(date.getFullYear()+2); - const ip_regex = /^((\d+\.){3}\d+|[A-Fa-f0-9]*:[A-Fa-f0-9:]*:[A-Fa-f0-9:]+)$/; - let domain_used = location.hostname; - - // Fix for a bug in FF where the leading dot in the FQDN is not ignored - if (domain_used.charAt(0) !== '.' && !ip_regex.test(domain_used) && domain_used !== 'localhost') - domain_used = '.' + location.hostname; - - document.cookie = key + '=' + cookie_data + '; SameSite=Strict; path=/; domain=' + - domain_used + '; expires=' + date.toGMTString() + ';'; + document.cookie = key + '=' + cookie_data + '; expires=' + date.toGMTString(); }, remove: function (key) { document.cookie = key + '=; Max-Age=0'; diff --git a/assets/js/notifications.js b/assets/js/notifications.js index f8cc750b..568f5ff6 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -48,7 +48,7 @@ function create_notification_stream(subscriptions) { } delivered.push(notification.videoId); - helpers.storage.set('notification_count', parseInt(helpers.storage.get('notification_count') || '0') + 1); + helpers.storage.set('notification_count', (helpers.storage.get('notification_count') || 0) + 1); var notification_ticker = document.getElementById('notification_ticker'); if (parseInt(helpers.storage.get('notification_count')) > 0) { @@ -72,7 +72,12 @@ function handle_notification_error(event) { } addEventListener('load', function (e) { - helpers.storage.set('notification_count', document.getElementById('notification_count') ? document.getElementById('notification_count').innerText : '0'); + var notification_count = document.getElementById('notification_count'); + if (notification_count) { + helpers.storage.set('notification_count', parseInt(notification_count.innerText)); + } else { + helpers.storage.set('notification_count', 0); + } if (helpers.storage.get('stream')) { helpers.storage.remove('stream'); diff --git a/assets/js/player.js b/assets/js/player.js index 07a5c128..5bff7ee5 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -432,7 +432,7 @@ function save_video_time(seconds) { all_video_times[videoId] = seconds; - helpers.storage.set(save_player_pos_key, JSON.stringify(all_video_times)); + helpers.storage.set(save_player_pos_key, all_video_times); } function get_video_time() { @@ -444,8 +444,7 @@ function get_video_time() { } function get_all_video_times() { - const raw = helpers.storage.get(save_player_pos_key); - return raw ? JSON.parse(raw) : {}; + return helpers.storage.get(save_player_pos_key) || {}; } function remove_all_video_times() { From f06d5b973b1bc5aa1c00ff330ae5efeaf83bc5d1 Mon Sep 17 00:00:00 2001 From: meow Date: Fri, 6 May 2022 07:42:15 +0300 Subject: [PATCH 04/42] jsdoc type fix --- assets/js/_helpers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js index 838a4612..ad7dcb91 100644 --- a/assets/js/_helpers.js +++ b/assets/js/_helpers.js @@ -165,9 +165,9 @@ window.helpers = window.helpers || { /** * @typedef {Object} invidiousStorage - * @property {(key:String) => Object|null} get - * @property {(key:String, value:Object) => null} set - * @property {(key:String) => null} remove + * @property {(key:String) => Object} get + * @property {(key:String, value:Object)} set + * @property {(key:String)} remove */ /** From fd66084388399319993e9aaa0a15ba3ed5498404 Mon Sep 17 00:00:00 2001 From: meow Date: Sun, 15 May 2022 08:38:46 +0300 Subject: [PATCH 05/42] js code rewrite. Themes rewritten, bugs fixed --- assets/js/_helpers.js | 6 ++-- assets/js/player.js | 15 ++++---- assets/js/themes.js | 82 +++++++++++++++---------------------------- assets/js/watch.js | 17 ++++----- 4 files changed, 46 insertions(+), 74 deletions(-) diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js index ad7dcb91..3f79bf54 100644 --- a/assets/js/_helpers.js +++ b/assets/js/_helpers.js @@ -145,16 +145,14 @@ window.helpers = window.helpers || { }, options.retry_timeout); }; - if (callbacks.onError) - callbacks._onError = callbacks.onError; + callbacks._onError = callbacks.onError; callbacks.onError = function (xhr) { if (callbacks._onError) callbacks._onError(); retry(); }; - if (callbacks.onTimeout) - callbacks._onTimeout = callbacks.onTimeout; + callbacks._onTimeout = callbacks.onTimeout; callbacks.onTimeout = function (xhr) { if (callbacks._onTimeout) callbacks._onTimeout(); diff --git a/assets/js/player.js b/assets/js/player.js index 5bff7ee5..832c7d0e 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -42,9 +42,10 @@ embed_url = location.origin + '/embed/' + video_data.id + embed_url.search; var save_player_pos_key = 'save_player_pos'; videojs.Vhs.xhr.beforeRequest = function(options) { - if (options.uri.includes('videoplayback') && options.uri.includes('local=true')) { - options.uri = options.uri + '?local=true'; - } + // set local if requested not videoplayback + if (!options.uri.includes('videoplayback')) + if (!options.uri.includes('local=true')) + options.uri += '?local=true'; return options; }; @@ -402,7 +403,7 @@ if (!video_data.params.listen && video_data.params.annotations) { }); } -function increase_volume(delta) { +function change_volume(delta) { const curVolume = player.volume(); const newVolume = curVolume + delta; helpers.clamp(newVolume, 0, 1); @@ -565,10 +566,10 @@ addEventListener('keydown', function (e) { case 'MediaStop': action = stop; break; case 'ArrowUp': - if (isPlayerFocused) action = increase_volume.bind(this, 0.1); + if (isPlayerFocused) action = change_volume.bind(this, 0.1); break; case 'ArrowDown': - if (isPlayerFocused) action = increase_volume.bind(this, -0.1); + if (isPlayerFocused) action = change_volume.bind(this, -0.1); break; case 'm': @@ -659,7 +660,7 @@ addEventListener('keydown', function (e) { var wheelMove = event.wheelDelta || -event.detail; var volumeSign = Math.sign(wheelMove); - increase_volume(volumeSign * 0.05); // decrease/increase by 5% + change_volume(volumeSign * 0.05); // decrease/increase by 5% } player.on('mousewheel', mouseScroll); diff --git a/assets/js/themes.js b/assets/js/themes.js index 7e86e9ac..eedf63a4 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -2,58 +2,39 @@ var toggle_theme = document.getElementById('toggle_theme'); toggle_theme.href = 'javascript:void(0)'; +const STORAGE_KEY_THEME = 'dark_mode'; +const THEME_DARK = 'dark'; +const THEME_LIGHT = 'light'; +const THEME_SYSTEM = ''; + +// TODO: theme state controlled by system toggle_theme.addEventListener('click', function () { - var dark_mode = document.body.classList.contains('light-theme'); - - set_mode(dark_mode); - helpers.storage.set('dark_mode', dark_mode ? 'dark' : 'light'); - + const isDarkTheme = helpers.storage.get(STORAGE_KEY_THEME) === THEME_DARK; + setTheme(isDarkTheme ? THEME_LIGHT : THEME_DARK); helpers.xhr('GET', '/toggle_theme?redirect=false', {}, {}); }); -// Handles theme change event caused by other tab -addEventListener('storage', function (e) { - if (e.key === 'dark_mode') { - update_mode(e.newValue); - } -}); -addEventListener('DOMContentLoaded', function () { - const dark_mode = document.getElementById('dark_mode_pref').textContent; - // Update storage if dark mode preference changed on preferences page - helpers.storage.set('dark_mode', dark_mode); - update_mode(dark_mode); +// Ask system about dark theme +var systemDarkTheme = matchMedia('(prefers-color-scheme: dark)'); +systemDarkTheme.addListener(function () { + // Ignore system events if theme set manually + if (!helpers.storage.get(STORAGE_KEY_THEME)) + setTheme(THEME_SYSTEM); }); -var darkScheme = matchMedia('(prefers-color-scheme: dark)'); -var lightScheme = matchMedia('(prefers-color-scheme: light)'); +/** @param {THEME_DARK|THEME_LIGHT|THEME_SYSTEM} theme */ +function setTheme(theme) { + if (theme !== THEME_SYSTEM) + helpers.storage.set(STORAGE_KEY_THEME, theme); -darkScheme.addListener(scheme_switch); -lightScheme.addListener(scheme_switch); - -function scheme_switch (e) { - // ignore this method if we have a preference set - if (helpers.storage.get('dark_mode')) return; - - if (!e.matches) return; - - if (e.media.includes('dark')) { - set_mode(true); - } else if (e.media.includes('light')) { - set_mode(false); - } -} - -function set_mode (bool) { - if (bool) { - // dark + if (theme === THEME_DARK || (theme === THEME_SYSTEM && systemDarkTheme.matches)) { toggle_theme.children[0].setAttribute('class', 'icon ion-ios-sunny'); document.body.classList.remove('no-theme'); document.body.classList.remove('light-theme'); document.body.classList.add('dark-theme'); } else { - // light toggle_theme.children[0].setAttribute('class', 'icon ion-ios-moon'); document.body.classList.remove('no-theme'); document.body.classList.remove('dark-theme'); @@ -61,18 +42,13 @@ function set_mode (bool) { } } -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 compatibility */ || mode === 'light') { - // If preference for light mode indicated - set_mode(false); - } - else if (document.getElementById('dark_mode_pref').textContent === '' && 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) -} +// Handles theme change event caused by other tab +addEventListener('storage', function (e) { + if (e.key === STORAGE_KEY_THEME) setTheme(e.newValue); +}); + +// Set theme from preferences on page load +addEventListener('DOMContentLoaded', function () { + const prefTheme = document.getElementById('dark_mode_pref').textContent; + setTheme(prefTheme); +}); diff --git a/assets/js/watch.js b/assets/js/watch.js index ff0f7822..45492241 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -102,13 +102,6 @@ function continue_autoplay(event) { } } -function number_with_separator(val) { - while (/(\d+)(\d{3})/.test(val.toString())) { - val = val.toString().replace(/(\d+)(\d{3})/, '$1' + ',' + '$2'); - } - return val; -} - function get_playlist(plid) { var playlist = document.getElementById('playlist'); @@ -248,9 +241,13 @@ function get_youtube_comments() {
'.supplant({ contentHtml: response.contentHtml, redditComments: video_data.reddit_comments_text, - commentsText: video_data.comments_text.supplant( - { commentCount: number_with_separator(response.commentCount) } - ) + commentsText: video_data.comments_text.supplant({ + // toLocaleString correctly splits number with local thousands separator. e.g.: + // '1,234,567.89' for user with English locale + // '1 234 567,89' for user with Russian locale + // '1.234.567,89' for user with Portuguese locale + commentCount: response.commentCount.toLocaleString() + }) }); comments.children[0].children[0].children[0].onclick = toggle_comments; From e18b10297b259460a3219edfeb9ccd0fabc34270 Mon Sep 17 00:00:00 2001 From: meow Date: Mon, 16 May 2022 13:13:00 +0300 Subject: [PATCH 06/42] JS fixes: recursion in themes, keys for frame walking, JSON XHR and details-summary in IE11 --- assets/js/_helpers.js | 23 +++++++++++++++++++++-- assets/js/player.js | 4 ++-- assets/js/themes.js | 11 ++++++----- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js index 3f79bf54..448e95d1 100644 --- a/assets/js/_helpers.js +++ b/assets/js/_helpers.js @@ -23,6 +23,20 @@ Math.sign = Math.sign || function(x) { if (!x) return x; // 0 and NaN return x > 0 ? 1 : -1; }; +if (!window.hasOwnProperty('HTMLDetailsElement') && !window.hasOwnProperty('mockHTMLDetailsElement')) { + window.mockHTMLDetailsElement = true; + const style = 'details:not([open]) > :not(summary) {display: none}'; + document.head.appendChild(document.createElement('style')).textContent = style; + + addEventListener('click', function (e) { + if (e.target.nodeName !== 'SUMMARY') return; + const details = e.target.parentElement; + if (details.hasAttribute('open')) + details.removeAttribute('open'); + else + details.setAttribute('open', ''); + }); +} // Monstrous global variable for handy code window.helpers = window.helpers || { @@ -65,8 +79,13 @@ window.helpers = window.helpers || { // better than onreadystatechange because of 404 codes https://stackoverflow.com/a/36182963 xhr.onloadend = function () { if (xhr.status === 200) { - if (callbacks.on200) - callbacks.on200(xhr.response); + if (callbacks.on200) { + // fix for IE11. It doesn't convert response to JSON + if (xhr.responseType === '' && typeof(xhr.response) === 'string') + callbacks.on200(JSON.parse(xhr.response)); + else + callbacks.on200(xhr.response); + } } else { // handled by onerror if (xhr.status === 0) return; diff --git a/assets/js/player.js b/assets/js/player.js index 832c7d0e..e2bd2df1 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -623,8 +623,8 @@ addEventListener('keydown', function (e) { // TODO: More precise step. Now FPS is taken equal to 29.97 // Common FPS: https://forum.videohelp.com/threads/81868#post323588 // Possible solution is new HTMLVideoElement.requestVideoFrameCallback() https://wicg.github.io/video-rvfc/ - case '.': action = function () { pause(); skip_seconds(-1/29.97); }; break; - case ',': action = function () { pause(); skip_seconds( 1/29.97); }; break; + case ',': action = function () { pause(); skip_seconds(-1/29.97); }; break; + case '.': action = function () { pause(); skip_seconds( 1/29.97); }; break; case '>': action = increase_playback_rate.bind(this, 1); break; case '<': action = increase_playback_rate.bind(this, -1); break; diff --git a/assets/js/themes.js b/assets/js/themes.js index eedf63a4..029d7c5d 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -10,7 +10,9 @@ const THEME_SYSTEM = ''; // TODO: theme state controlled by system toggle_theme.addEventListener('click', function () { const isDarkTheme = helpers.storage.get(STORAGE_KEY_THEME) === THEME_DARK; - setTheme(isDarkTheme ? THEME_LIGHT : THEME_DARK); + const newTheme = isDarkTheme ? THEME_LIGHT : THEME_DARK; + setTheme(newTheme); + helpers.storage.set(STORAGE_KEY_THEME, newTheme); helpers.xhr('GET', '/toggle_theme?redirect=false', {}, {}); }); @@ -26,9 +28,6 @@ systemDarkTheme.addListener(function () { /** @param {THEME_DARK|THEME_LIGHT|THEME_SYSTEM} theme */ function setTheme(theme) { - if (theme !== THEME_SYSTEM) - helpers.storage.set(STORAGE_KEY_THEME, theme); - if (theme === THEME_DARK || (theme === THEME_SYSTEM && systemDarkTheme.matches)) { toggle_theme.children[0].setAttribute('class', 'icon ion-ios-sunny'); document.body.classList.remove('no-theme'); @@ -44,11 +43,13 @@ function setTheme(theme) { // Handles theme change event caused by other tab addEventListener('storage', function (e) { - if (e.key === STORAGE_KEY_THEME) setTheme(e.newValue); + if (e.key === STORAGE_KEY_THEME) + setTheme(helpers.storage.get(STORAGE_KEY_THEME)); }); // Set theme from preferences on page load addEventListener('DOMContentLoaded', function () { const prefTheme = document.getElementById('dark_mode_pref').textContent; setTheme(prefTheme); + helpers.storage.set(STORAGE_KEY_THEME, prefTheme); }); From 2dead1a19b650155371ab3fa5296d60650868d90 Mon Sep 17 00:00:00 2001 From: meow Date: Mon, 16 May 2022 13:51:28 +0300 Subject: [PATCH 07/42] JS theme switching simplified --- assets/js/themes.js | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/assets/js/themes.js b/assets/js/themes.js index 029d7c5d..76767d5f 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -5,7 +5,6 @@ toggle_theme.href = 'javascript:void(0)'; const STORAGE_KEY_THEME = 'dark_mode'; const THEME_DARK = 'dark'; const THEME_LIGHT = 'light'; -const THEME_SYSTEM = ''; // TODO: theme state controlled by system toggle_theme.addEventListener('click', function () { @@ -16,28 +15,16 @@ toggle_theme.addEventListener('click', function () { helpers.xhr('GET', '/toggle_theme?redirect=false', {}, {}); }); - -// Ask system about dark theme -var systemDarkTheme = matchMedia('(prefers-color-scheme: dark)'); -systemDarkTheme.addListener(function () { - // Ignore system events if theme set manually - if (!helpers.storage.get(STORAGE_KEY_THEME)) - setTheme(THEME_SYSTEM); -}); - - -/** @param {THEME_DARK|THEME_LIGHT|THEME_SYSTEM} theme */ +/** @param {THEME_DARK|THEME_LIGHT} theme */ function setTheme(theme) { - if (theme === THEME_DARK || (theme === THEME_SYSTEM && systemDarkTheme.matches)) { - toggle_theme.children[0].setAttribute('class', 'icon ion-ios-sunny'); - document.body.classList.remove('no-theme'); - document.body.classList.remove('light-theme'); - document.body.classList.add('dark-theme'); + // By default body element has .no-theme class that uses OS theme via CSS @media rules + // It rewrites using hard className below + if (theme === THEME_DARK) { + toggle_theme.children[0].className = 'icon ion-ios-sunny'; + document.body.className = 'dark-theme'; } else { - toggle_theme.children[0].setAttribute('class', 'icon ion-ios-moon'); - document.body.classList.remove('no-theme'); - document.body.classList.remove('dark-theme'); - document.body.classList.add('light-theme'); + toggle_theme.children[0].className = 'icon ion-ios-moon'; + document.body.className = 'light-theme'; } } @@ -50,6 +37,8 @@ addEventListener('storage', function (e) { // Set theme from preferences on page load addEventListener('DOMContentLoaded', function () { const prefTheme = document.getElementById('dark_mode_pref').textContent; - setTheme(prefTheme); - helpers.storage.set(STORAGE_KEY_THEME, prefTheme); + if (prefTheme) { + setTheme(prefTheme); + helpers.storage.set(STORAGE_KEY_THEME, prefTheme); + } }); From 2ea423032e977145f9dbdd2f566616312b8af6b5 Mon Sep 17 00:00:00 2001 From: meow Date: Tue, 17 May 2022 09:43:05 +0300 Subject: [PATCH 08/42] Share video regression. Single quotes are required --- assets/js/player.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index e2bd2df1..38746787 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -114,8 +114,9 @@ var shareOptions = { description: player_data.description, image: player_data.thumbnail, get embedCode() { - return ''; + // Single quotes inside here required. HTML inserted as is into value attribute of input + return ""; } }; From 17e62134487aaa0fa5ede651f43616dd0fe20a52 Mon Sep 17 00:00:00 2001 From: meow Date: Tue, 17 May 2022 10:03:07 +0300 Subject: [PATCH 09/42] Less player reload timeout --- assets/js/player.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index 38746787..e8c18e21 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -63,9 +63,10 @@ player.on('error', function () { source.src += '&local=true'; })); } else if (reloadMakesSense) { - setTimeout(function (event) { - console.log('An error occurred in the player, reloading...'); + setTimeout(function () { + console.warn('An error occurred in the player, reloading...'); + // After load() all parameters are reset. Save them var currentTime = player.currentTime(); var playbackRate = player.playbackRate(); var paused = player.paused(); @@ -76,9 +77,8 @@ player.on('error', function () { player.currentTime(currentTime); player.playbackRate(playbackRate); - if (!paused) player.play(); - }, 10000); + }, 5000); } }); From 1097648f0a675215594f52c0e1b1f97975a07f39 Mon Sep 17 00:00:00 2001 From: meow Date: Tue, 17 May 2022 10:09:01 +0300 Subject: [PATCH 10/42] Fix HTML validation. This is how browser really split tags --- src/invidious/views/watch.ecr | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index e6a14d0f..f2d8ba03 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -279,24 +279,24 @@ we're going to need to do it here in order to allow for translations. <% end %>

<%= rv["title"] %>

-
-
- <% if rv["ucid"]? %> - "><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <% end %> - <% else %> - <%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <% end %> - <% end %> -
- -
- <%= - views = rv["view_count"]?.try &.to_i? - views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) } - translate_count(locale, "generic_views_count", views || 0, NumberFormatting::Short) - %> -
-
+
+
+ <% if rv["ucid"]? %> + "><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <% end %> + <% else %> + <%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <% end %> + <% end %> +
+ +
+ <%= + views = rv["view_count"]?.try &.to_i? + views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) } + translate_count(locale, "generic_views_count", views || 0, NumberFormatting::Short) + %> +
+
<% end %> <% end %> From 319bbd2f8113a775990895fff952a0228fb8f9e1 Mon Sep 17 00:00:00 2001 From: AHOHNMYC <24810600+AHOHNMYC@users.noreply.github.com> Date: Thu, 19 May 2022 07:15:17 +0300 Subject: [PATCH 11/42] JS code minor formatting Co-authored-by: Samantaz Fox --- assets/js/player.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index e8c18e21..d09892cb 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -54,8 +54,13 @@ var player = videojs('player', options); player.on('error', function () { if (video_data.params.quality === 'dash') return; - var localNotDisabled = !player.currentSrc().includes('local=true') && !video_data.local_disabled; - var reloadMakesSense = player.error().code === MediaError.MEDIA_ERR_NETWORK || player.error().code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED; + var localNotDisabled = ( + !player.currentSrc().includes('local=true') && !video_data.local_disabled + ); + var reloadMakesSense = ( + player.error().code === MediaError.MEDIA_ERR_NETWORK || + player.error().code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED + ); if (localNotDisabled) { // add local=true to all current sources From b72b917af239c46dbe3d4592fc8b215e63703459 Mon Sep 17 00:00:00 2001 From: meow Date: Sat, 21 May 2022 13:35:41 +0300 Subject: [PATCH 12/42] handled invalid values in storage partial rewrite notifications.js innerText to textContent fixed bug with clamping --- assets/js/_helpers.js | 30 +++++-- assets/js/community.js | 2 +- assets/js/handlers.js | 10 +-- assets/js/notifications.js | 159 ++++++++++++++++++----------------- assets/js/player.js | 28 +++--- assets/js/playlist_widget.js | 2 +- assets/js/watch.js | 2 +- assets/js/watched_widget.js | 4 +- 8 files changed, 128 insertions(+), 109 deletions(-) diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js index 448e95d1..0bd99a8c 100644 --- a/assets/js/_helpers.js +++ b/assets/js/_helpers.js @@ -39,6 +39,7 @@ if (!window.hasOwnProperty('HTMLDetailsElement') && !window.hasOwnProperty('mock } // Monstrous global variable for handy code +// Includes: clamp, xhr, storage.{get,set,remove} window.helpers = window.helpers || { /** * https://en.wikipedia.org/wiki/Clamping_(graphics) @@ -164,19 +165,20 @@ window.helpers = window.helpers || { }, options.retry_timeout); }; + // Pack retry() call into error handlers callbacks._onError = callbacks.onError; callbacks.onError = function (xhr) { if (callbacks._onError) - callbacks._onError(); + callbacks._onError(xhr); retry(); }; - callbacks._onTimeout = callbacks.onTimeout; callbacks.onTimeout = function (xhr) { if (callbacks._onTimeout) - callbacks._onTimeout(); + callbacks._onTimeout(xhr); retry(); - }; + }; + helpers._xhrRetry(method, url, options, callbacks); }, @@ -198,13 +200,22 @@ window.helpers = window.helpers || { if (localStorageIsUsable) { return { - get: function (key) { if (localStorage[key]) return JSON.parse(decodeURIComponent(localStorage[key])); }, + get: function (key) { + if (!localStorage[key]) return; + try { + return JSON.parse(decodeURIComponent(localStorage[key])); + } catch(e) { + // Erase non parsable value + helpers.storage.remove(key); + } + }, set: function (key, value) { localStorage[key] = encodeURIComponent(JSON.stringify(value)); }, remove: function (key) { localStorage.removeItem(key); } }; } - console.info('Storage: localStorage is disabled or unaccessible trying cookies'); + // TODO: fire 'storage' event for cookies + console.info('Storage: localStorage is disabled or unaccessible. Cookies used as fallback'); return { get: function (key) { const cookiePrefix = key + '='; @@ -213,7 +224,12 @@ window.helpers = window.helpers || { if (matchedCookie) { const cookieBody = matchedCookie.replace(cookiePrefix, ''); if (cookieBody.length === 0) return; - return JSON.parse(decodeURIComponent(cookieBody)); + try { + return JSON.parse(decodeURIComponent(cookieBody)); + } catch(e) { + // Erase non parsable value + helpers.storage.remove(key); + } } }, set: function (key, value) { diff --git a/assets/js/community.js b/assets/js/community.js index 33e2e3ed..608dc971 100644 --- a/assets/js/community.js +++ b/assets/js/community.js @@ -62,7 +62,7 @@ function get_youtube_replies(target, load_more) { a.onclick = hide_youtube_replies; a.setAttribute('data-sub-text', community_data.hide_replies_text); a.setAttribute('data-inner-text', community_data.show_replies_text); - a.innerText = community_data.hide_replies_text; + a.textContent = community_data.hide_replies_text; var div = document.createElement('div'); div.innerHTML = response.contentHtml; diff --git a/assets/js/handlers.js b/assets/js/handlers.js index 438832b1..29810e72 100644 --- a/assets/js/handlers.js +++ b/assets/js/handlers.js @@ -78,7 +78,7 @@ document.querySelectorAll('[data-onrange="update_volume_value"]').forEach(function (el) { function update_volume_value() { - document.getElementById('volume-value').innerText = el.value; + document.getElementById('volume-value').textContent = el.value; } el.oninput = update_volume_value; el.onchange = update_volume_value; @@ -89,7 +89,7 @@ var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; row.style.display = 'none'; var count = document.getElementById('count'); - count.innerText = parseInt(count.innerText) - 1; + count.textContent--; var url = '/token_ajax?action_revoke_token=1&redirect=false' + '&referer=' + encodeURIComponent(location.href) + @@ -99,7 +99,7 @@ helpers.xhr('POST', url, {payload: payload}, { onNon200: function (xhr) { - count.innerText = parseInt(count.innerText) + 1; + count.textContent++; row.style.display = ''; } }); @@ -109,7 +109,7 @@ var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; row.style.display = 'none'; var count = document.getElementById('count'); - count.innerText = parseInt(count.innerText) - 1; + count.textContent--; var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' + '&referer=' + encodeURIComponent(location.href) + @@ -119,7 +119,7 @@ helpers.xhr('POST', url, {payload: payload}, { onNon200: function (xhr) { - count.innerText = parseInt(count.innerText) + 1; + count.textContent++; row.style.display = ''; } }); diff --git a/assets/js/notifications.js b/assets/js/notifications.js index 568f5ff6..7a30375d 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -1,8 +1,13 @@ 'use strict'; var notification_data = JSON.parse(document.getElementById('notification_data').textContent); +/** Boolean meaning 'some tab have stream' */ +const STORAGE_KEY_STREAM = 'stream'; +/** Number of notifications. May be increased or reset */ +const STORAGE_KEY_NOTIF_COUNT = 'notification_count'; + var notifications, delivered; -var notifications_substitution = { close: function () { } }; +var notifications_mock = { close: function () { } }; function get_subscriptions() { helpers.xhr('GET', '/api/v1/auth/subscriptions?fields=authorId', { @@ -32,92 +37,96 @@ function create_notification_stream(subscriptions) { var notification = JSON.parse(event.data); console.info('Got notification:', notification); - if (start_time < notification.published && !delivered.includes(notification.videoId)) { - if (Notification.permission === 'granted') { - var system_notification = - new Notification((notification.liveNow ? notification_data.live_now_text : notification_data.upload_text).replace('`x`', notification.author), { - body: notification.title, - icon: '/ggpht' + new URL(notification.authorThumbnails[2].url).pathname, - img: '/ggpht' + new URL(notification.authorThumbnails[4].url).pathname, - tag: notification.videoId - }); + // Ignore not actual and delivered notifications + if (start_time > notification.published || delivered.includes(notification.videoId)) return; - system_notification.onclick = function (event) { - open('/watch?v=' + event.currentTarget.tag, '_blank'); - }; - } + delivered.push(notification.videoId); - delivered.push(notification.videoId); - helpers.storage.set('notification_count', (helpers.storage.get('notification_count') || 0) + 1); - var notification_ticker = document.getElementById('notification_ticker'); + let notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0; + notification_count++; + helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count); - if (parseInt(helpers.storage.get('notification_count')) > 0) { - notification_ticker.innerHTML = - '' + helpers.storage.get('notification_count') + ' '; - } else { - notification_ticker.innerHTML = - ''; - } + update_ticker_count(); + + // TODO: ask permission to show notifications via Notification.requestPermission + // https://developer.mozilla.org/en-US/docs/Web/API/notification + if (window.Notification && Notification.permission === 'granted') { + var notification_text = notification.liveNow ? notification_data.live_now_text : notification_data.upload_text; + notification_text = notification_text.replace('`x`', notification.author); + + var system_notification = new Notification(notification_text, { + body: notification.title, + icon: '/ggpht' + new URL(notification.authorThumbnails[2].url).pathname, + img: '/ggpht' + new URL(notification.authorThumbnails[4].url).pathname + }); + + system_notification.onclick = function (e) { + open('/watch?v=' + notification.videoId, '_blank'); + }; } }; - notifications.addEventListener('error', handle_notification_error); + notifications.addEventListener('error', function (e) { + console.warn('Something went wrong with notifications, trying to reconnect...'); + notifications = notifications_mock; + setTimeout(get_subscriptions, 1000); + }); + notifications.stream(); } -function handle_notification_error(event) { - console.warn('Something went wrong with notifications, trying to reconnect...'); - notifications = notifications_substitution; - setTimeout(get_subscriptions, 1000); +function update_ticker_count() { + var notification_ticker = document.getElementById('notification_ticker'); + + const notification_count = helpers.storage.get(STORAGE_KEY_STREAM); + if (notification_count > 0) { + notification_ticker.innerHTML = + '' + notification_count + ' '; + } else { + notification_ticker.innerHTML = + ''; + } } -addEventListener('load', function (e) { - var notification_count = document.getElementById('notification_count'); - if (notification_count) { - helpers.storage.set('notification_count', parseInt(notification_count.innerText)); - } else { - helpers.storage.set('notification_count', 0); - } - - if (helpers.storage.get('stream')) { - helpers.storage.remove('stream'); - } else { - setTimeout(function () { - if (!helpers.storage.get('stream')) { - notifications = notifications_substitution; - helpers.storage.set('stream', true); - get_subscriptions(); - } - }, Math.random() * 1000 + 50); - } - - addEventListener('storage', function (e) { - if (e.key === 'stream' && !e.newValue) { - if (notifications) { - helpers.storage.set('stream', true); - } else { - setTimeout(function () { - if (!helpers.storage.get('stream')) { - notifications = notifications_substitution; - helpers.storage.set('stream', true); - get_subscriptions(); - } - }, Math.random() * 1000 + 50); - } - } else if (e.key === 'notification_count') { - var notification_ticker = document.getElementById('notification_ticker'); - - if (parseInt(e.newValue) > 0) { - notification_ticker.innerHTML = - '' + e.newValue + ' '; - } else { - notification_ticker.innerHTML = - ''; - } +function start_stream_if_needed() { + // random wait for other tabs set 'stream' flag + setTimeout(function () { + if (!helpers.storage.get(STORAGE_KEY_STREAM)) { + // if no one set 'stream', set it by yourself and start stream + helpers.storage.set(STORAGE_KEY_STREAM, true); + notifications = notifications_mock; + get_subscriptions(); } - }); + }, Math.random() * 1000 + 50); // [0.050 .. 1.050) second +} + + +addEventListener('storage', function (e) { + if (e.key === STORAGE_KEY_NOTIF_COUNT) + update_ticker_count(); + + // if 'stream' key was removed + if (e.key === STORAGE_KEY_STREAM && !helpers.storage.get(STORAGE_KEY_STREAM)) { + if (notifications) { + // restore it if we have active stream + helpers.storage.set(STORAGE_KEY_STREAM, true); + } else { + start_stream_if_needed(); + } + } }); -addEventListener('unload', function (e) { - if (notifications) helpers.storage.remove('stream'); +addEventListener('load', function () { + var notification_count_el = document.getElementById('notification_count'); + var notification_count = notification_count_el ? parseInt(notification_count_el.textContent) : 0; + helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count); + + if (helpers.storage.get(STORAGE_KEY_STREAM)) + helpers.storage.remove(STORAGE_KEY_STREAM); + start_stream_if_needed(); +}); + +addEventListener('unload', function () { + // let chance to other tabs to be a streamer via firing 'storage' event + if (notifications) helpers.storage.remove(STORAGE_KEY_STREAM); }); diff --git a/assets/js/player.js b/assets/js/player.js index d09892cb..ff9302b7 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -43,9 +43,10 @@ var save_player_pos_key = 'save_player_pos'; videojs.Vhs.xhr.beforeRequest = function(options) { // set local if requested not videoplayback - if (!options.uri.includes('videoplayback')) + if (!options.uri.includes('videoplayback')) { if (!options.uri.includes('local=true')) options.uri += '?local=true'; + } return options; }; @@ -346,7 +347,7 @@ if (!video_data.params.listen && video_data.params.quality === 'dash') { targetQualityLevel = 0; break; default: - const targetHeight = parseInt(video_data.params.quality_dash, 10); + const targetHeight = parseInt(video_data.params.quality_dash); for (let i = 0; i < qualityLevels.length; i++) { if (qualityLevels[i].height <= targetHeight) targetQualityLevel = i; @@ -411,8 +412,8 @@ if (!video_data.params.listen && video_data.params.annotations) { function change_volume(delta) { const curVolume = player.volume(); - const newVolume = curVolume + delta; - helpers.clamp(newVolume, 0, 1); + let newVolume = curVolume + delta; + newVolume = helpers.clamp(newVolume, 0, 1); player.volume(newVolume); } @@ -423,8 +424,8 @@ function toggle_muted() { function skip_seconds(delta) { const duration = player.duration(); const curTime = player.currentTime(); - const newTime = curTime + delta; - helpers.clamp(newTime, 0, duration); + let newTime = curTime + delta; + newTime = helpers.clamp(newTime, 0, duration); player.currentTime(newTime); } @@ -434,20 +435,13 @@ function set_seconds_after_start(delta) { } function save_video_time(seconds) { - const videoId = video_data.id; const all_video_times = get_all_video_times(); - - all_video_times[videoId] = seconds; - + all_video_times[video_data.id] = seconds; helpers.storage.set(save_player_pos_key, all_video_times); } function get_video_time() { - const videoId = video_data.id; - const all_video_times = get_all_video_times(); - const timestamp = all_video_times[videoId]; - - return timestamp || 0; + return get_all_video_times()[video_data.id] || 0; } function get_all_video_times() { @@ -534,8 +528,8 @@ function toggle_fullscreen() { function increase_playback_rate(steps) { const maxIndex = options.playbackRates.length - 1; const curIndex = options.playbackRates.indexOf(player.playbackRate()); - const newIndex = curIndex + steps; - helpers.clamp(newIndex, 0, maxIndex); + let newIndex = curIndex + steps; + newIndex = helpers.clamp(newIndex, 0, maxIndex); player.playbackRate(options.playbackRates[newIndex]); } diff --git a/assets/js/playlist_widget.js b/assets/js/playlist_widget.js index 8f8da6d5..c92592ac 100644 --- a/assets/js/playlist_widget.js +++ b/assets/js/playlist_widget.js @@ -12,7 +12,7 @@ function add_playlist_video(target) { helpers.xhr('POST', url, {payload: payload}, { on200: function (response) { - option.innerText = '✓' + option.innerText; + option.textContent = '✓' + option.textContent; } }); } diff --git a/assets/js/watch.js b/assets/js/watch.js index 45492241..f78b9242 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -294,7 +294,7 @@ function get_youtube_replies(target, load_more, load_replies) { a.onclick = hide_youtube_replies; a.setAttribute('data-sub-text', video_data.hide_replies_text); a.setAttribute('data-inner-text', video_data.show_replies_text); - a.innerText = video_data.hide_replies_text; + a.textContent = video_data.hide_replies_text; var div = document.createElement('div'); div.innerHTML = response.contentHtml; diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js index 497b1878..f1ac9cb4 100644 --- a/assets/js/watched_widget.js +++ b/assets/js/watched_widget.js @@ -20,14 +20,14 @@ function mark_unwatched(target) { var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; tile.style.display = 'none'; var count = document.getElementById('count'); - count.innerText = parseInt(count.innerText) - 1; + count.textContent--; var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' + '&id=' + target.getAttribute('data-id'); helpers.xhr('POST', url, {payload: payload}, { onNon200: function (xhr) { - count.innerText = parseInt(count.innerText) + 1; + count.textContent++; tile.style.display = ''; } }); From b7295977284f060b4c294332d9029ed46aa8f35d Mon Sep 17 00:00:00 2001 From: meow Date: Sat, 21 May 2022 19:30:51 +0300 Subject: [PATCH 13/42] comment changed extra spaces removed --- assets/js/notifications.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/js/notifications.js b/assets/js/notifications.js index 7a30375d..51ff1f98 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -48,8 +48,7 @@ function create_notification_stream(subscriptions) { update_ticker_count(); - // TODO: ask permission to show notifications via Notification.requestPermission - // https://developer.mozilla.org/en-US/docs/Web/API/notification + // permission for notifications handled on settings page. JS handler is in handlers.js if (window.Notification && Notification.permission === 'granted') { var notification_text = notification.liveNow ? notification_data.live_now_text : notification_data.upload_text; notification_text = notification_text.replace('`x`', notification.author); @@ -62,7 +61,7 @@ function create_notification_stream(subscriptions) { system_notification.onclick = function (e) { open('/watch?v=' + notification.videoId, '_blank'); - }; + }; } }; From d66ef8fe22ccf095205c0586700728d33d81fbd3 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 30 Apr 2022 18:10:24 +0200 Subject: [PATCH 14/42] Add a script to install dependencies --- scripts/install-dependencies.sh | 174 ++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 scripts/install-dependencies.sh diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh new file mode 100644 index 00000000..27e0bf15 --- /dev/null +++ b/scripts/install-dependencies.sh @@ -0,0 +1,174 @@ +#!/bin/sh +# +# Script that installs the various dependencies of invidious +# +# Dependencies: +# - crystal => Language in which Invidious is developed +# - postgres => Database server +# - git => required to clone Invidious +# - librsvg2-bin => For login captcha (provides 'rsvg-convert') +# +# - libssl-dev => Used by Crystal's SSL module (standard library) +# - libxml2-dev => Used by Crystal's XML module (standard library) +# - libyaml-dev => Used by Crystal's YAML module (standard library) +# - libgmp-dev => Used by Crystal's BigNumbers module (standard library) +# - libevent-dev => Used by crystal's internal scheduler (?) +# - libpcre3-dev => Used by Crystal's regex engine (?) +# +# - libsqlite3-dev => Used to open .db files from NewPipe exports +# - zlib1g-dev => TBD +# - libreadline-dev => TBD +# +# +# Tested on: +# - OpenSUSE Leap 15.3 + +# +# Load system details +# + +if [ -e /etc/os-release ]; then + . /etc/os-release +elif [ -e /usr/lib/os-release ]; then + . /usr/lib/os-release +else + echo "Unsupported Linux system" + exit 2 +fi + +# +# Some variables +# + +repo_base_url="https://download.opensuse.org/repositories/devel:/languages:/crystal/" +repo_end_url="devel:languages:crystal.repo" + +apt_gpg_key="/usr/share/keyrings/crystal.gpg" +apt_list_file="/etc/apt/sources.list.d/crystal.list" + +yum_repo_file="/etc/yum.repos.d/crystal.repo" + +# +# Major install functions +# + +make_repo_url() { + echo "${repo_base_url}/${1}/${repo_end_url}" +} + + +install_apt() { + repo="$1" + + echo "Adding Crystal repository" + + curl -fsSL "${repo_base_url}/${repo}/Release.key" \ + | gpg --dearmor \ + | sudo tee "${apt_gpg_key}" > /dev/null + + echo "deb [signed-by=${apt_gpg_key}] ${repo_base_url}/${repo}/ /" \ + | sudo tee "$apt_list_file" + + sudo apt-get update + + sudo apt-get install --yes --no-install-recommends \ + libssl-dev libxml2-dev libyaml-dev libgmp-dev libevent-dev \ + libpcre3-dev libreadline-dev libsqlite3-dev zlib1g-dev \ + crystal postgres git librsvg2-bin make +} + +install_yum() { + repo=$(make_repo_url "$1") + + echo "Adding Crystal repository" + + cat << END | sudo tee "${yum_repo_file}" > /dev/null +[crystal] +name=Crystal +type=rpm-md +baseurl=${repo}/ +gpgcheck=1 +gpgkey=${repo}/repodata/repomd.xml.key +enabled=1 +END + + sudo yum -y install \ + openssl-devel libxml2-devel libyaml-devel gmp-devel \ + readline-devel sqlite-devel \ + crystal postgresql postgresql-server git librsvg2-tools make +} + +install_pacman() { + # TODO: find an alternative to --no-confirm? + sudo pacman -S --no-confirm \ + base-devel librsvg postgresql crystal +} + +install_zypper() +{ + repo=$(make_repo_url "$1") + + echo "Adding Crystal repository" + sudo zypper --non-interactive addrepo -f "$repo" + + sudo zypper --non-interactive --gpg-auto-import-keys install --no-recommends \ + libopenssl-devel libxml2-devel libyaml-devel gmp-devel libevent-devel \ + pcre-devel readline-devel sqlite3-devel zlib-devel \ + crystal postgresql postgresql-server git rsvg-convert make +} + + +# +# System-specific logic +# + +case "$ID" in + archlinux) install_pacman;; + + centos) install_dnf "CentOS_${VERSION_ID}";; + + debian) + case "$VERSION_CODENAME" in + sid) install_apt "Debian_Unstable";; + bookworm) install_apt "Debian_Testing";; + *) install_apt "Debian_${VERSION_ID}";; + esac + ;; + + fedora) + if [ "$VERSION" == *"Prerelease"* ]; then + install_dnf "Fedora_Rawhide" + else + install_dnf "Fedora_${VERSION}" + fi + ;; + + opensuse-leap) install_zypper "openSUSE_Leap_${VERSION}";; + + opensuse-tumbleweed) install_zypper "openSUSE_Tumbleweed";; + + rhel) install_dnf "RHEL_${VERSION_ID}";; + + ubuntu) + # Small workaround for recently released 22.04 + case "$VERSION_ID" in + 22.04) install_apt "xUbuntu_21.04";; + *) install_apt "xUbuntu_${VERSION_ID}";; + esac + ;; + + *) + # Try to match on ID_LIKE instead + # Not guaranteed to 100% work + case "$ID_LIKE" in + archlinux) install_pacman;; + centos) install_dnf "CentOS_${VERSION_ID}";; + debian) install_apt "Debian_${VERSION_ID}";; + *) + echo "Error: distribution ${CODENAME} is not supported" + echo "Please install dependencies manually" + exit 2 + ;; + esac + ;; +esac From fe53b5503cc175ad7cef74d9ad1d8096031c44ac Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 30 Apr 2022 18:11:12 +0200 Subject: [PATCH 15/42] Add a script to start postgres and create user/DB --- scripts/deploy-database.sh | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 scripts/deploy-database.sh diff --git a/scripts/deploy-database.sh b/scripts/deploy-database.sh new file mode 100644 index 00000000..9f0bffcb --- /dev/null +++ b/scripts/deploy-database.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# +# Parameters +# + +interactive=true + +if [ "$1" == "--no-interactive" ]; then + interactive=false +fi + +# +# Enable and start Postgres +# + +sudo systemctl start postgresql.service +sudo systemctl enable postgresql.service + +# +# Create databse and user +# + +if [ "$interactive" == "true" ]; then + sudo -u postgres -- createuser -P kemal + sudo -u postgres -- createdb -O kemal invidious +else + # Generate a DB password + if [ -z "$POSTGRES_PASS" ]; then + echo "Generating database password" + POSTGRES_PASS=$(tr -dc 'A-Za-z0-9.;!?{[()]}\\/' < /dev/urandom | head -c16) + fi + + # hostname:port:database:username:password + echo "Writing .pgpass" + echo "127.0.0.1:*:invidious:kemal:${POSTGRES_PASS}" > "$HOME/.pgpass" + + sudo -u postgres -- psql -c "CREATE USER kemal WITH PASSWORD '$POSTGRES_PASS';" + sudo -u postgres -- psql -c "CREATE DATABASE invidious WITH OWNER kemal;" + sudo -u postgres -- psql -c "GRANT ALL ON DATABASE invidious TO kemal;" +fi From 1f359f5a13f3bfd9dddbac47d8e6dbc2ab1c6f49 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 21 May 2022 19:18:01 +0200 Subject: [PATCH 16/42] Print some helpful notice for PostgreSQL configuration --- scripts/deploy-database.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/deploy-database.sh b/scripts/deploy-database.sh index 9f0bffcb..ed9464e6 100644 --- a/scripts/deploy-database.sh +++ b/scripts/deploy-database.sh @@ -39,3 +39,22 @@ else sudo -u postgres -- psql -c "CREATE DATABASE invidious WITH OWNER kemal;" sudo -u postgres -- psql -c "GRANT ALL ON DATABASE invidious TO kemal;" fi + + +# +# Instructions for modification of pg_hba.conf +# + +if [ "$interactive" = "true" ]; then + echo + echo "-------------" + echo " NOTICE " + echo "-------------" + echo + echo "Make sure that your postgreSQL's pg_hba.conf file contains the follwong" + echo "lines before previous 'host' configurations:" + echo + echo "host invidious kemal 127.0.0.1/32 md5" + echo "host invidious kemal ::1/128 md5" + echo +fi From ad37db4c820064d08e72014af339c7d789067937 Mon Sep 17 00:00:00 2001 From: DoodlesEpic Date: Tue, 24 May 2022 20:34:36 -0300 Subject: [PATCH 17/42] Fix document is empty error on yt kids video when reddit comments are enabled --- src/invidious/comments.cr | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index d8496978..1aa14935 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -500,6 +500,12 @@ def template_reddit_comments(root, locale) end def replace_links(html) + # Check if the document is empty + # Prevents edge-case bug with Reddit comments, see issue #3115 + if html.nil? || html.empty? + return html + end + html = XML.parse_html(html) html.xpath_nodes(%q(//a)).each do |anchor| @@ -541,6 +547,12 @@ def replace_links(html) end def fill_links(html, scheme, host) + # Check if the document is empty + # Prevents edge-case bug with Reddit comments, see issue #3115 + if html.nil? || html.empty? + return html + end + html = XML.parse_html(html) html.xpath_nodes("//a").each do |match| From b50de2f2ed7b41aa5733ed95311a8972b1af93cc Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Wed, 25 May 2022 20:58:58 +0000 Subject: [PATCH 18/42] Add "Popular Enabled: " string to localisation --- locales/en-US.json | 1 + locales/fr.json | 1 + 2 files changed, 2 insertions(+) diff --git a/locales/en-US.json b/locales/en-US.json index 7518c3a1..9701a621 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -136,6 +136,7 @@ "preferences_default_home_label": "Default homepage: ", "preferences_feed_menu_label": "Feed menu: ", "preferences_show_nick_label": "Show nickname on top: ", + "Popular enabled: ": "Popular enabled: ", "Top enabled: ": "Top enabled: ", "CAPTCHA enabled: ": "CAPTCHA enabled: ", "Login enabled: ": "Login enabled: ", diff --git a/locales/fr.json b/locales/fr.json index 6fee70f9..b6c86504 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -116,6 +116,7 @@ "preferences_default_home_label": "Page d'accueil par défaut : ", "preferences_feed_menu_label": "Préferences des abonnements : ", "preferences_show_nick_label": "Afficher le nom d'utilisateur en haut à droite : ", + "Popular enabled: ": "Populaire enabled: ", "Top enabled: ": "Top activé : ", "CAPTCHA enabled: ": "CAPTCHA activé : ", "Login enabled: ": "Autoriser l'ouverture de sessions utilisateur : ", From 958867e92b4677620b4c58b46c64d0a34061251e Mon Sep 17 00:00:00 2001 From: Gauthier POGAM--LE MONTAGNER Date: Wed, 25 May 2022 23:41:12 +0200 Subject: [PATCH 19/42] Fix wrong french translation Co-authored-by: Samantaz Fox --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index b6c86504..928a4400 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -116,7 +116,7 @@ "preferences_default_home_label": "Page d'accueil par défaut : ", "preferences_feed_menu_label": "Préferences des abonnements : ", "preferences_show_nick_label": "Afficher le nom d'utilisateur en haut à droite : ", - "Popular enabled: ": "Populaire enabled: ", + "Popular enabled: ": "Page \"populaire\" activée: ", "Top enabled: ": "Top activé : ", "CAPTCHA enabled: ": "CAPTCHA activé : ", "Login enabled: ": "Autoriser l'ouverture de sessions utilisateur : ", From b12149bafd7ed20daa757163e84381f6650e9d2e Mon Sep 17 00:00:00 2001 From: meow Date: Tue, 31 May 2022 11:58:12 +0300 Subject: [PATCH 20/42] Save time during redirection on another instance --- assets/js/player.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/assets/js/player.js b/assets/js/player.js index ff9302b7..4244f2e2 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -704,3 +704,9 @@ if (location.pathname.startsWith('/embed/')) { var cb = player.getChild('ControlBar'); cb.addChild(watch_on_invidious_button); } + +// Save time during redirection on another instance +const changeInstanceLink = document.querySelector('#watch-on-another-invidious-instance > a'); +if (changeInstanceLink) changeInstanceLink.addEventListener('click', function () { + changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href); +}); From f2f3f045e5482618c6dcc18c410556a3b4045f99 Mon Sep 17 00:00:00 2001 From: meow Date: Tue, 31 May 2022 12:18:42 +0300 Subject: [PATCH 21/42] fix time adding dirung redirection --- assets/js/player.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index 4244f2e2..48533b3e 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -705,8 +705,10 @@ if (location.pathname.startsWith('/embed/')) { cb.addChild(watch_on_invidious_button); } -// Save time during redirection on another instance -const changeInstanceLink = document.querySelector('#watch-on-another-invidious-instance > a'); -if (changeInstanceLink) changeInstanceLink.addEventListener('click', function () { - changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href); +addEventListener('DOMContentLoaded', function () { + // Save time during redirection on another instance + const changeInstanceLink = document.querySelector('#watch-on-another-invidious-instance > a'); + if (changeInstanceLink) changeInstanceLink.addEventListener('click', function () { + changeInstanceLink.href = addCurrentTimeToURL(changeInstanceLink.href); + }); }); From 1533a28817c15052453c66b03bbf0cfe68f06c3c Mon Sep 17 00:00:00 2001 From: 777 <71448324+lhc-sudo@users.noreply.github.com> Date: Wed, 1 Jun 2022 18:48:52 +0100 Subject: [PATCH 22/42] Add TubiTui to Projects Using Invidious section --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dd131d79..9ed68a4b 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab, - [HoloPlay](https://github.com/stephane-r/HoloPlay): Funny Android application connecting on Invidious API's with search, playlists and favorites. - [WatchTube](https://github.com/WatchTubeTeam/WatchTube): Powerful YouTube client for Apple Watch. - [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV. +- [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client. ## Liability From e84416e56d3916477b2f2873eb1f4535d2777783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20Devos?= Date: Sat, 4 Jun 2022 12:58:34 +0200 Subject: [PATCH 23/42] Remove dislikes icon (#3092) --- src/invidious/views/watch.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 8b6eb903..367fde33 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -173,7 +173,7 @@ we're going to need to do it here in order to allow for translations.

<%= number_with_separator(video.views) %>

<%= number_with_separator(video.likes) %>

-

<%= number_with_separator(video.dislikes) %>

+

<%= translate(locale, "Genre: ") %> <% if !video.genre_url %> <%= video.genre %> From 4ae77bcef95ccaa0b07bf750d660297c97be89b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20Devos?= Date: Sat, 4 Jun 2022 15:39:04 +0200 Subject: [PATCH 24/42] Remove rating display from the frontend --- src/invidious/views/watch.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 367fde33..783eff1d 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -186,7 +186,7 @@ we're going to need to do it here in order to allow for translations. <% end %>

<%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %>

<%= translate(locale, "Wilson score: ") %><%= video.wilson_score %>

-

<%= translate(locale, "Rating: ") %><%= video.average_rating %> / 5

+

<%= translate(locale, "Engagement: ") %><%= video.engagement %>%

<% if video.allowed_regions.size != REGIONS.size %>

From d3ab4a51457ee2f0596db8c2a735ef220105dea8 Mon Sep 17 00:00:00 2001 From: meow Date: Sun, 5 Jun 2022 20:54:48 +0300 Subject: [PATCH 25/42] JS. Trailing spaces removed --- assets/js/_helpers.js | 2 +- assets/js/community.js | 2 +- assets/js/notifications.js | 4 ++-- assets/js/player.js | 8 ++++---- assets/js/watch.js | 10 +++++----- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js index 0bd99a8c..7c50670e 100644 --- a/assets/js/_helpers.js +++ b/assets/js/_helpers.js @@ -164,7 +164,7 @@ window.helpers = window.helpers || { helpers._xhrRetry(method, url, options, callbacks); }, options.retry_timeout); }; - + // Pack retry() call into error handlers callbacks._onError = callbacks.onError; callbacks.onError = function (xhr) { diff --git a/assets/js/community.js b/assets/js/community.js index 608dc971..32fe4ebc 100644 --- a/assets/js/community.js +++ b/assets/js/community.js @@ -38,7 +38,7 @@ function get_youtube_replies(target, load_more) { var fallback = body.innerHTML; body.innerHTML = '

'; - + var url = '/api/v1/channels/comments/' + community_data.ucid + '?format=html' + '&hl=' + community_data.preferences.locale + diff --git a/assets/js/notifications.js b/assets/js/notifications.js index 51ff1f98..058553d9 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -52,13 +52,13 @@ function create_notification_stream(subscriptions) { if (window.Notification && Notification.permission === 'granted') { var notification_text = notification.liveNow ? notification_data.live_now_text : notification_data.upload_text; notification_text = notification_text.replace('`x`', notification.author); - + var system_notification = new Notification(notification_text, { body: notification.title, icon: '/ggpht' + new URL(notification.authorThumbnails[2].url).pathname, img: '/ggpht' + new URL(notification.authorThumbnails[4].url).pathname }); - + system_notification.onclick = function (e) { open('/watch?v=' + notification.videoId, '_blank'); }; diff --git a/assets/js/player.js b/assets/js/player.js index 48533b3e..7d099e66 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -54,12 +54,12 @@ var player = videojs('player', options); player.on('error', function () { if (video_data.params.quality === 'dash') return; - + var localNotDisabled = ( !player.currentSrc().includes('local=true') && !video_data.local_disabled ); var reloadMakesSense = ( - player.error().code === MediaError.MEDIA_ERR_NETWORK || + player.error().code === MediaError.MEDIA_ERR_NETWORK || player.error().code === MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED ); @@ -465,7 +465,7 @@ function toggle_play() { player.paused() ? play() : pause(); } const toggle_captions = (function () { let toggledTrack = null; - + function bindChange(onOrOff) { player.textTracks()[onOrOff]('change', function (e) { toggledTrack = null; @@ -481,7 +481,7 @@ const toggle_captions = (function () { bindChange('on'); }, 0); } - + bindChange('on'); return function () { if (toggledTrack !== null) { diff --git a/assets/js/watch.js b/assets/js/watch.js index f78b9242..cff84e4d 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -172,7 +172,7 @@ function get_reddit_comments() { var onNon200 = function (xhr) { comments.innerHTML = fallback; }; if (video_data.params.comments[1] === 'youtube') onNon200 = function (xhr) {}; - + helpers.xhr('GET', url, {retries: 5, entity_name: ''}, { on200: function (response) { comments.innerHTML = ' \ @@ -218,11 +218,11 @@ function get_youtube_comments() { '?format=html' + '&hl=' + video_data.preferences.locale + '&thin_mode=' + video_data.preferences.thin_mode; - + var onNon200 = function (xhr) { comments.innerHTML = fallback; }; if (video_data.params.comments[1] === 'youtube') onNon200 = function (xhr) {}; - + helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, { on200: function (response) { comments.innerHTML = ' \ @@ -304,11 +304,11 @@ function get_youtube_replies(target, load_more, load_replies) { } }, onNon200: function (xhr) { - body.innerHTML = fallback; + body.innerHTML = fallback; }, onTimeout: function (xhr) { console.warn('Pulling comments failed'); - body.innerHTML = fallback; + body.innerHTML = fallback; } }); } From a402128a7d4a2d3dccbeeb553e5363447f501a37 Mon Sep 17 00:00:00 2001 From: meow Date: Sun, 5 Jun 2022 21:19:59 +0300 Subject: [PATCH 26/42] Move `_helpers.js` include from various .ecr's into `template.ecr` `head` tag --- src/invidious/views/add_playlist_items.ecr | 1 - src/invidious/views/community.ecr | 1 - src/invidious/views/components/player.ecr | 1 - src/invidious/views/components/subscribe_widget.ecr | 1 - src/invidious/views/embed.ecr | 1 - src/invidious/views/feeds/history.ecr | 1 - src/invidious/views/feeds/subscriptions.ecr | 1 - src/invidious/views/playlist.ecr | 1 - src/invidious/views/template.ecr | 2 +- src/invidious/views/watch.ecr | 2 -- 10 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index 758f3995..22870317 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -29,7 +29,6 @@ }.to_pretty_json %> -
diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr index 154c40b5..3bc29e55 100644 --- a/src/invidious/views/community.ecr +++ b/src/invidious/views/community.ecr @@ -93,5 +93,4 @@ }.to_pretty_json %> - diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr index 483807d7..fffefc9a 100644 --- a/src/invidious/views/components/player.ecr +++ b/src/invidious/views/components/player.ecr @@ -66,5 +66,4 @@ }.to_pretty_json %> - diff --git a/src/invidious/views/components/subscribe_widget.ecr b/src/invidious/views/components/subscribe_widget.ecr index 7a8c7fda..b9d5f783 100644 --- a/src/invidious/views/components/subscribe_widget.ecr +++ b/src/invidious/views/components/subscribe_widget.ecr @@ -31,7 +31,6 @@ }.to_pretty_json %> - <% else %>

diff --git a/src/invidious/views/embed.ecr b/src/invidious/views/embed.ecr index 82f80f9d..ce5ff7f0 100644 --- a/src/invidious/views/embed.ecr +++ b/src/invidious/views/embed.ecr @@ -31,7 +31,6 @@ <%= rendered "components/player" %> - diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 51dd78bd..6c1243c5 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -25,7 +25,6 @@ }.to_pretty_json %> -

diff --git a/src/invidious/views/feeds/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr index 957277fa..8d56ad14 100644 --- a/src/invidious/views/feeds/subscriptions.ecr +++ b/src/invidious/views/feeds/subscriptions.ecr @@ -50,7 +50,6 @@ }.to_pretty_json %> -
diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 641cbe2c..df3112db 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -97,7 +97,6 @@ }.to_pretty_json %> - <% end %> diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 79decbe6..4e2b29f0 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -17,6 +17,7 @@ + <% @@ -157,7 +158,6 @@
- <% if env.get? "user" %> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index f2d8ba03..861b2048 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -165,7 +165,6 @@ we're going to need to do it here in order to allow for translations. }.to_pretty_json %> - <% end %> <% end %> @@ -304,5 +303,4 @@ we're going to need to do it here in order to allow for translations.
<% end %> - From 7ad111e2f65c2688c7accb31ff75171c29f2cc26 Mon Sep 17 00:00:00 2001 From: Mohammed Anas Date: Sun, 5 Jun 2022 23:05:19 +0300 Subject: [PATCH 27/42] Update actions used in GH workflows (#3138) --- .github/workflows/ci.yml | 16 ++++++++-------- .github/workflows/container-release.yml | 20 ++++++++++---------- .github/workflows/stale.yml | 4 ++-- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e68b7f2..6107e260 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,15 +46,15 @@ jobs: stable: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Crystal - uses: crystal-lang/install-crystal@v1.5.3 + uses: crystal-lang/install-crystal@v1.6.0 with: crystal: ${{ matrix.crystal }} - name: Cache Shards - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ./lib key: shards-${{ hashFiles('shard.lock') }} @@ -84,7 +84,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build Docker run: docker-compose build --build-arg release=0 @@ -100,18 +100,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 with: platforms: arm64 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Build Docker ARM64 image - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . file: docker/Dockerfile.arm64 diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index 36fb566e..212487c8 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -15,20 +15,20 @@ on: - screenshots/* - .github/ISSUE_TEMPLATE/* - kubernetes/** - + jobs: release: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 - + uses: actions/checkout@v3 + - name: Install Crystal - uses: oprypin/install-crystal@v1.2.4 + uses: crystal-lang/install-crystal@v1.6.0 with: crystal: 1.2.2 - + - name: Run lint run: | if ! crystal tool format --check; then @@ -38,15 +38,15 @@ jobs: fi - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 with: platforms: arm64 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Login to registry - uses: docker/login-action@v1 + uses: docker/login-action@v2 with: registry: quay.io username: ${{ secrets.QUAY_USERNAME }} @@ -54,7 +54,7 @@ jobs: - name: Build and push Docker AMD64 image for Push Event if: github.ref == 'refs/heads/master' - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . file: docker/Dockerfile @@ -66,7 +66,7 @@ jobs: - name: Build and push Docker ARM64 image for Push Event if: github.ref == 'refs/heads/master' - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v3 with: context: . file: docker/Dockerfile.arm64 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 86275da7..ff28d49b 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,11 +10,11 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v3 + - uses: actions/stale@v5 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 365 - days-before-pr-stale: 45 # PRs should be active. Anything that hasn't had activity in more than 45 days should be considered abandoned. + days-before-pr-stale: 45 # PRs should be active. Anything that hasn't had activity in more than 45 days should be considered abandoned. days-before-close: 30 exempt-pr-labels: blocked stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.' From 33da64a6696e757aa98b2c771e3e8c03f5e58b2b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 26 May 2022 18:31:02 +0200 Subject: [PATCH 28/42] Add support for hashtags --- src/invidious.cr | 1 + src/invidious/hashtag.cr | 44 ++++++++++++++++++++++++++ src/invidious/routes/search.cr | 31 ++++++++++++++++++ src/invidious/views/hashtag.ecr | 39 +++++++++++++++++++++++ src/invidious/yt_backend/extractors.cr | 26 +++++++++++++++ 5 files changed, 141 insertions(+) create mode 100644 src/invidious/hashtag.cr create mode 100644 src/invidious/views/hashtag.ecr diff --git a/src/invidious.cr b/src/invidious.cr index dd240852..4952b365 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -385,6 +385,7 @@ end Invidious::Routing.get "/opensearch.xml", Invidious::Routes::Search, :opensearch Invidious::Routing.get "/results", Invidious::Routes::Search, :results Invidious::Routing.get "/search", Invidious::Routes::Search, :search + Invidious::Routing.get "/hashtag/:hashtag", Invidious::Routes::Search, :hashtag # User routes define_user_routes() diff --git a/src/invidious/hashtag.cr b/src/invidious/hashtag.cr new file mode 100644 index 00000000..afe31a36 --- /dev/null +++ b/src/invidious/hashtag.cr @@ -0,0 +1,44 @@ +module Invidious::Hashtag + extend self + + def fetch(hashtag : String, page : Int, region : String? = nil) : Array(SearchItem) + cursor = (page - 1) * 60 + ctoken = generate_continuation(hashtag, cursor) + + client_config = YoutubeAPI::ClientConfig.new(region: region) + response = YoutubeAPI.browse(continuation: ctoken, client_config: client_config) + + return extract_items(response) + end + + def generate_continuation(hashtag : String, cursor : Int) + object = { + "80226972:embedded" => { + "2:string" => "FEhashtag", + "3:base64" => { + "1:varint" => cursor.to_i64, + }, + "7:base64" => { + "325477796:embedded" => { + "1:embedded" => { + "2:0:embedded" => { + "2:string" => '#' + hashtag, + "4:varint" => 0_i64, + "11:string" => "", + }, + "4:string" => "browse-feedFEhashtag", + }, + "2:string" => hashtag, + }, + }, + }, + } + + continuation = object.try { |i| Protodec::Any.cast_json(i) } + .try { |i| Protodec::Any.from_json(i) } + .try { |i| Base64.urlsafe_encode(i) } + .try { |i| URI.encode_www_form(i) } + + return continuation + end +end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index e60d0081..6f8bffea 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -63,4 +63,35 @@ module Invidious::Routes::Search templated "search" end end + + def self.hashtag(env : HTTP::Server::Context) + locale = env.get("preferences").as(Preferences).locale + + hashtag = env.params.url["hashtag"]? + if hashtag.nil? || hashtag.empty? + return error_template(400, "Invalid request") + end + + page = env.params.query["page"]? + if page.nil? + page = 1 + else + page = Math.max(1, page.to_i) + env.params.query.delete_all("page") + end + + begin + videos = Invidious::Hashtag.fetch(hashtag, page) + rescue ex + return error_template(500, ex) + end + + params = env.params.query.empty? ? "" : "&#{env.params.query}" + + hashtag_encoded = URI.encode_www_form(hashtag, space_to_plus: false) + url_prev_page = "/hashtag/#{hashtag_encoded}?page=#{page - 1}#{params}" + url_next_page = "/hashtag/#{hashtag_encoded}?page=#{page + 1}#{params}" + + templated "hashtag" + end end diff --git a/src/invidious/views/hashtag.ecr b/src/invidious/views/hashtag.ecr new file mode 100644 index 00000000..0ecfe832 --- /dev/null +++ b/src/invidious/views/hashtag.ecr @@ -0,0 +1,39 @@ +<% content_for "header" do %> +<%= HTML.escape(hashtag) %> - Invidious +<% end %> + +
+ +
+
+ <%- if page > 1 -%> + <%= translate(locale, "Previous page") %> + <%- end -%> +
+
+
+ <%- if videos.size >= 60 -%> + <%= translate(locale, "Next page") %> + <%- end -%> +
+
+ +
+ <%- videos.each do |item| -%> + <%= rendered "components/item" %> + <%- end -%> +
+ +
+
+ <%- if page > 1 -%> + <%= translate(locale, "Previous page") %> + <%- end -%> +
+
+
+ <%- if videos.size >= 60 -%> + <%= translate(locale, "Next page") %> + <%- end -%> +
+
diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index a2ec7d59..7e7cf85b 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -1,3 +1,5 @@ +require "../helpers/serialized_yt_data" + # This file contains helper methods to parse the Youtube API json data into # neat little packages we can use @@ -14,6 +16,7 @@ private ITEM_PARSERS = { Parsers::GridPlaylistRendererParser, Parsers::PlaylistRendererParser, Parsers::CategoryRendererParser, + Parsers::RichItemRendererParser, } record AuthorFallback, name : String, id : String @@ -374,6 +377,29 @@ private module Parsers return {{@type.name}} end end + + # Parses an InnerTube richItemRenderer into a SearchVideo. + # Returns nil when the given object isn't a shelfRenderer + # + # A richItemRenderer seems to be a simple wrapper for a videoRenderer, used + # by the result page for hashtags. It is located inside a continuationItems + # container. + # + module RichItemRendererParser + def self.process(item : JSON::Any, author_fallback : AuthorFallback) + if item_contents = item.dig?("richItemRenderer", "content") + return self.parse(item_contents, author_fallback) + end + end + + private def self.parse(item_contents, author_fallback) + return VideoRendererParser.process(item_contents, author_fallback) + end + + def self.parser_name + return {{@type.name}} + end + end end # The following are the extractors for extracting an array of items from From 96ac7f9f35e28bd83f706e116c651da8b87a625b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 28 May 2022 11:41:27 +0200 Subject: [PATCH 29/42] Add hashtag extractor spec --- spec/invidious/hashtag_spec.cr | 109 +++++++++++++++++++++++++++++++++ spec/parsers_helper.cr | 33 ++++++++++ 2 files changed, 142 insertions(+) create mode 100644 spec/invidious/hashtag_spec.cr create mode 100644 spec/parsers_helper.cr diff --git a/spec/invidious/hashtag_spec.cr b/spec/invidious/hashtag_spec.cr new file mode 100644 index 00000000..c09c59d4 --- /dev/null +++ b/spec/invidious/hashtag_spec.cr @@ -0,0 +1,109 @@ +require "../parsers_helper.cr" + +Spectator.describe Invidious::Hashtag do + it "parses richItemRenderer containers (test 1)" do + # Enable mock + test_content = load_mock("hashtag/martingarrix_page1") + videos = extract_items(test_content) + + expect(typeof(videos)).to eq(Array(SearchItem)) + expect(videos.size).to eq(60) + + # + # Random video check 1 + # + expect(typeof(videos[11])).to eq(SearchItem) + + video_11 = videos[11].as(SearchVideo) + + expect(video_11.id).to eq("06eSsOWcKYA") + expect(video_11.title).to eq("Martin Garrix - Live @ Tomorrowland 2018") + + expect(video_11.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA") + expect(video_11.author).to eq("Martin Garrix") + expect(video_11.author_verified).to be_true + + expect(video_11.published).to eq(Time.utc - 3.years) + expect(video_11.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32) + expect(video_11.views).to eq(40_504_893) + + expect(video_11.live_now).to be_false + expect(video_11.premium).to be_false + expect(video_11.premiere_timestamp).to be_nil + + # + # Random video check 2 + # + expect(typeof(videos[35])).to eq(SearchItem) + + video_35 = videos[35].as(SearchVideo) + + expect(video_35.id).to eq("b9HpOAYjY9I") + expect(video_35.title).to eq("Martin Garrix feat. Mike Yung - Dreamer (Official Video)") + + expect(video_35.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA") + expect(video_35.author).to eq("Martin Garrix") + expect(video_35.author_verified).to be_true + + expect(video_35.published).to eq(Time.utc - 3.years) + expect(video_35.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32) + expect(video_35.views).to eq(30_790_049) + + expect(video_35.live_now).to be_false + expect(video_35.premium).to be_false + expect(video_35.premiere_timestamp).to be_nil + end + + it "parses richItemRenderer containers (test 2)" do + # Enable mock + test_content = load_mock("hashtag/martingarrix_page2") + videos = extract_items(test_content) + + expect(typeof(videos)).to eq(Array(SearchItem)) + expect(videos.size).to eq(60) + + # + # Random video check 1 + # + expect(typeof(videos[41])).to eq(SearchItem) + + video_41 = videos[41].as(SearchVideo) + + expect(video_41.id).to eq("qhstH17zAjs") + expect(video_41.title).to eq("Martin Garrix Radio - Episode 391") + + expect(video_41.ucid).to eq("UC5H_KXkPbEsGs0tFt8R35mA") + expect(video_41.author).to eq("Martin Garrix") + expect(video_41.author_verified).to be_true + + expect(video_41.published).to eq(Time.utc - 2.months) + expect(video_41.length_seconds).to eq((1.hour).total_seconds.to_i32) + expect(video_41.views).to eq(63_240) + + expect(video_41.live_now).to be_false + expect(video_41.premium).to be_false + expect(video_41.premiere_timestamp).to be_nil + + # + # Random video check 2 + # + expect(typeof(videos[48])).to eq(SearchItem) + + video_48 = videos[48].as(SearchVideo) + + expect(video_48.id).to eq("lqGvW0NIfdc") + expect(video_48.title).to eq("Martin Garrix SENTIO Full Album Mix by Sakul") + + expect(video_48.ucid).to eq("UC3833PXeLTS6yRpwGMQpp4Q") + expect(video_48.author).to eq("SAKUL") + expect(video_48.author_verified).to be_false + + expect(video_48.published).to eq(Time.utc - 3.weeks) + expect(video_48.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32) + expect(video_48.views).to eq(68_704) + + expect(video_48.live_now).to be_false + expect(video_48.premium).to be_false + expect(video_48.premiere_timestamp).to be_nil + end +end diff --git a/spec/parsers_helper.cr b/spec/parsers_helper.cr new file mode 100644 index 00000000..6155fe33 --- /dev/null +++ b/spec/parsers_helper.cr @@ -0,0 +1,33 @@ +require "db" +require "json" +require "kemal" + +require "protodec/utils" + +require "spectator" + +require "../src/invidious/helpers/macros" +require "../src/invidious/helpers/logger" +require "../src/invidious/helpers/utils" + +require "../src/invidious/videos" +require "../src/invidious/comments" + +require "../src/invidious/helpers/serialized_yt_data" +require "../src/invidious/yt_backend/extractors" +require "../src/invidious/yt_backend/extractors_utils" + +OUTPUT = File.open(File::NULL, "w") +LOGGER = Invidious::LogHandler.new(OUTPUT, LogLevel::Off) + +def load_mock(file) : Hash(String, JSON::Any) + file = File.join(__DIR__, "..", "mocks", file + ".json") + content = File.read(file) + + return JSON.parse(content).as_h +end + +Spectator.configure do |config| + config.fail_blank + config.randomize +end From 2b1e1b11a331aea87b6b8e73d8d5bab97ae0f89b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 1 Jun 2022 23:07:18 +0200 Subject: [PATCH 30/42] Fix CI: support BADGE_STYLE_TYPE_VERIFIED_ARTIST --- src/invidious/channels/about.cr | 4 +- src/invidious/routes/feeds.cr | 2 +- src/invidious/videos.cr | 24 ++++++------ src/invidious/yt_backend/extractors.cr | 32 ++++++---------- src/invidious/yt_backend/extractors_utils.cr | 39 ++++++++++++++++++++ 5 files changed, 63 insertions(+), 38 deletions(-) diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr index da71e9a8..565f2bca 100644 --- a/src/invidious/channels/about.cr +++ b/src/invidious/channels/about.cr @@ -61,6 +61,7 @@ def get_about_info(ucid, locale) : AboutChannel author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s + author_verified = has_verified_badge?(initdata.dig?("header", "c4TabbedHeaderRenderer", "badges")) ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s @@ -71,9 +72,6 @@ def get_about_info(ucid, locale) : AboutChannel # if banner.includes? "channels/c4/default_banner" # banner = nil # end - # author_verified_badges = initdata["header"]?.try &.["c4TabbedHeaderRenderer"]?.try &.["badges"]? - author_verified_badge = initdata["header"].dig?("c4TabbedHeaderRenderer", "badges", 0, "metadataBadgeRenderer", "tooltip") - author_verified = (author_verified_badge && author_verified_badge == "Verified") description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]? diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index b5b58399..2e6043f7 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -182,7 +182,7 @@ module Invidious::Routes::Feeds paid: false, premium: false, premiere_timestamp: nil, - author_verified: false, # ¯\_(ツ)_/¯ + author_verified: false, }) end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index f65b05bb..8ba667db 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -868,11 +868,7 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)? .try &.dig?("runs", 0) author = channel_info.try &.dig?("text") - author_verified_badge = related["ownerBadges"]?.try do |badges_array| - badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified") - end - - author_verified = (author_verified_badge && author_verified_badge.size > 0).to_s + author_verified = has_verified_badge?(related["ownerBadges"]?) ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) } @@ -1089,17 +1085,19 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_ # Author infos - author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer") - author_thumbnail = author_info.try &.dig?("thumbnail", "thumbnails", 0, "url") + if author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer") + author_thumbnail = author_info.dig?("thumbnail", "thumbnails", 0, "url") + params["authorThumbnail"] = JSON::Any.new(author_thumbnail.try &.as_s || "") - author_verified_badge = author_info.try &.dig?("badges", 0, "metadataBadgeRenderer", "tooltip") - author_verified = (!author_verified_badge.nil? && author_verified_badge == "Verified") - params["authorVerified"] = JSON::Any.new(author_verified) + author_verified = has_verified_badge?(author_info["badges"]?) + params["authorVerified"] = JSON::Any.new(author_verified) - params["authorThumbnail"] = JSON::Any.new(author_thumbnail.try &.as_s || "") + subs_text = author_info["subscriberCountText"]? + .try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") } + .try &.as_s.split(" ", 2)[0] - params["subCountText"] = JSON::Any.new(author_info.try &.["subscriberCountText"]? - .try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }.try &.as_s.split(" ", 2)[0] || "-") + params["subCountText"] = JSON::Any.new(subs_text || "-") + end # Return data diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 7e7cf85b..f394da84 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -60,6 +60,8 @@ private module Parsers author_id = author_fallback.id end + author_verified = has_verified_badge?(item_contents["ownerBadges"]?) + # For live videos (and possibly recently premiered videos) there is no published information. # Instead, in its place is the amount of people currently watching. This behavior should be replicated # on Invidious once all features of livestreams are supported. On an unrelated note, defaulting to the current @@ -105,11 +107,7 @@ private module Parsers premium = false premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) } - author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array| - badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified") - end - author_verified = (author_verified_badge && author_verified_badge.size > 0) item_contents["badges"]?.try &.as_a.each do |badge| b = badge["metadataBadgeRenderer"] case b["label"].as_s @@ -136,7 +134,7 @@ private module Parsers live_now: live_now, premium: premium, premiere_timestamp: premiere_timestamp, - author_verified: author_verified || false, + author_verified: author_verified, }) end @@ -164,12 +162,9 @@ private module Parsers private def self.parse(item_contents, author_fallback) author = extract_text(item_contents["title"]) || author_fallback.name author_id = item_contents["channelId"]?.try &.as_s || author_fallback.id - author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array| - badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified") - end - - author_verified = (author_verified_badge && author_verified_badge.size > 0) + author_verified = has_verified_badge?(item_contents["ownerBadges"]?) author_thumbnail = HelperExtractors.get_thumbnails(item_contents) + # When public subscriber count is disabled, the subscriberCountText isn't sent by InnerTube. # Always simpleText # TODO change default value to nil @@ -191,7 +186,7 @@ private module Parsers video_count: video_count, description_html: description_html, auto_generated: auto_generated, - author_verified: author_verified || false, + author_verified: author_verified, }) end @@ -219,11 +214,9 @@ private module Parsers private def self.parse(item_contents, author_fallback) title = extract_text(item_contents["title"]) || "" plid = item_contents["playlistId"]?.try &.as_s || "" - author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array| - badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified") - end - author_verified = (author_verified_badge && author_verified_badge.size > 0) + author_verified = has_verified_badge?(item_contents["ownerBadges"]?) + video_count = HelperExtractors.get_video_count(item_contents) playlist_thumbnail = HelperExtractors.get_thumbnails(item_contents) @@ -235,7 +228,7 @@ private module Parsers video_count: video_count, videos: [] of SearchPlaylistVideo, thumbnail: playlist_thumbnail, - author_verified: author_verified || false, + author_verified: author_verified, }) end @@ -269,11 +262,8 @@ private module Parsers author_info = item_contents.dig?("shortBylineText", "runs", 0) author = author_info.try &.["text"].as_s || author_fallback.name author_id = author_info.try { |x| HelperExtractors.get_browse_id(x) } || author_fallback.id - author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array| - badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified") - end + author_verified = has_verified_badge?(item_contents["ownerBadges"]?) - author_verified = (author_verified_badge && author_verified_badge.size > 0) videos = item_contents["videos"]?.try &.as_a.map do |v| v = v["childVideoRenderer"] v_title = v.dig?("title", "simpleText").try &.as_s || "" @@ -296,7 +286,7 @@ private module Parsers video_count: video_count, videos: videos, thumbnail: playlist_thumbnail, - author_verified: author_verified || false, + author_verified: author_verified, }) end diff --git a/src/invidious/yt_backend/extractors_utils.cr b/src/invidious/yt_backend/extractors_utils.cr index add5f488..3d5e5787 100644 --- a/src/invidious/yt_backend/extractors_utils.cr +++ b/src/invidious/yt_backend/extractors_utils.cr @@ -29,6 +29,45 @@ def extract_text(item : JSON::Any?) : String? end end +# Check if an "ownerBadges" or a "badges" element contains a verified badge. +# There is currently two known types of verified badges: +# +# "ownerBadges": [{ +# "metadataBadgeRenderer": { +# "icon": { "iconType": "CHECK_CIRCLE_THICK" }, +# "style": "BADGE_STYLE_TYPE_VERIFIED", +# "tooltip": "Verified", +# "accessibilityData": { "label": "Verified" } +# } +# }], +# +# "ownerBadges": [{ +# "metadataBadgeRenderer": { +# "icon": { "iconType": "OFFICIAL_ARTIST_BADGE" }, +# "style": "BADGE_STYLE_TYPE_VERIFIED_ARTIST", +# "tooltip": "Official Artist Channel", +# "accessibilityData": { "label": "Official Artist Channel" } +# } +# }], +# +def has_verified_badge?(badges : JSON::Any?) + return false if badges.nil? + + badges.as_a.each do |badge| + style = badge.dig("metadataBadgeRenderer", "style").as_s + + return true if style == "BADGE_STYLE_TYPE_VERIFIED" + return true if style == "BADGE_STYLE_TYPE_VERIFIED_ARTIST" + end + + return false +rescue ex + LOGGER.debug("Unable to parse owner badges. Got exception: #{ex.message}") + LOGGER.trace("Owner badges data: #{badges.to_json}") + + return false +end + def extract_videos(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil) extracted = extract_items(initial_data, author_fallback, author_id_fallback) From fd99f20404196c21e2cf91986ff21d79ed31adf1 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 1 Jun 2022 23:09:58 +0200 Subject: [PATCH 31/42] Fix CI: use be_close() with 1s delta for Time comparisons --- spec/invidious/hashtag_spec.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/invidious/hashtag_spec.cr b/spec/invidious/hashtag_spec.cr index c09c59d4..77676878 100644 --- a/spec/invidious/hashtag_spec.cr +++ b/spec/invidious/hashtag_spec.cr @@ -23,7 +23,7 @@ Spectator.describe Invidious::Hashtag do expect(video_11.author).to eq("Martin Garrix") expect(video_11.author_verified).to be_true - expect(video_11.published).to eq(Time.utc - 3.years) + expect(video_11.published).to be_close(Time.utc - 3.years, 1.second) expect(video_11.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32) expect(video_11.views).to eq(40_504_893) @@ -45,7 +45,7 @@ Spectator.describe Invidious::Hashtag do expect(video_35.author).to eq("Martin Garrix") expect(video_35.author_verified).to be_true - expect(video_35.published).to eq(Time.utc - 3.years) + expect(video_35.published).to be_close(Time.utc - 3.years, 1.second) expect(video_35.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32) expect(video_35.views).to eq(30_790_049) @@ -76,7 +76,7 @@ Spectator.describe Invidious::Hashtag do expect(video_41.author).to eq("Martin Garrix") expect(video_41.author_verified).to be_true - expect(video_41.published).to eq(Time.utc - 2.months) + expect(video_41.published).to be_close(Time.utc - 2.months, 1.second) expect(video_41.length_seconds).to eq((1.hour).total_seconds.to_i32) expect(video_41.views).to eq(63_240) @@ -98,7 +98,7 @@ Spectator.describe Invidious::Hashtag do expect(video_48.author).to eq("SAKUL") expect(video_48.author_verified).to be_false - expect(video_48.published).to eq(Time.utc - 3.weeks) + expect(video_48.published).to be_close(Time.utc - 3.weeks, 1.second) expect(video_48.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32) expect(video_48.views).to eq(68_704) From d7f6b6b01869e044fa9a578894beede6562b3c8e Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 1 Jun 2022 23:17:28 +0200 Subject: [PATCH 32/42] Fix CI: support reloadContinuationItemsCommand containers --- src/invidious/yt_backend/extractors.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index f394da84..c4326cab 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -517,6 +517,8 @@ private module Extractors self.extract(target) elsif target = initial_data["appendContinuationItemsAction"]? self.extract(target) + elsif target = initial_data["reloadContinuationItemsCommand"]? + self.extract(target) end end From 93c1a1d42e3f145f1e030a793a2c709be209a21b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 2 Jun 2022 23:26:54 +0200 Subject: [PATCH 33/42] Add mocks as a submodule --- .gitmodules | 3 +++ mocks | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 mocks diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..3d19d888 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "mocks"] + path = mocks + url = ../mocks diff --git a/mocks b/mocks new file mode 160000 index 00000000..02033719 --- /dev/null +++ b/mocks @@ -0,0 +1 @@ +Subproject commit 020337194dd482c47ee2d53cd111d0ebf2831e52 From 1b251264a61e7b29a339cba9defee3e9c22758c3 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 2 Jun 2022 23:28:43 +0200 Subject: [PATCH 34/42] Pull submodules during CI --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6107e260..bc80c75c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + submodules: true - name: Install Crystal uses: crystal-lang/install-crystal@v1.6.0 From 3593f67eb60d46b4d4503364fe9f52109060cac7 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 8 Jun 2022 23:23:34 +0200 Subject: [PATCH 35/42] Fix: related videos is a Hash(String, String) --- src/invidious/videos.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 8ba667db..1504e390 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -853,6 +853,7 @@ end # the same 11 first entries as the compact rendered. # # TODO: "compactRadioRenderer" (Mix) and +# TODO: Use a proper struct/class instead of a hacky JSON object def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)? return nil if !related["videoId"]? @@ -868,7 +869,7 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)? .try &.dig?("runs", 0) author = channel_info.try &.dig?("text") - author_verified = has_verified_badge?(related["ownerBadges"]?) + author_verified = has_verified_badge?(related["ownerBadges"]?).to_s ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) } From c0e85f5687c31b7061c2ce6a4bea62b83ba8f4a2 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 9 Jun 2022 00:12:32 +0200 Subject: [PATCH 36/42] Update Russian translation Co-authored-by: AHOHNMYC Co-authored-by: Hosted Weblate --- locales/ru.json | 126 ++++++++++++++++++++++++------------------------ 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/locales/ru.json b/locales/ru.json index 0199f61f..00d24502 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -41,8 +41,8 @@ "User ID": "ID пользователя", "Password": "Пароль", "Time (h:mm:ss):": "Время (ч:мм:сс):", - "Text CAPTCHA": "Текст капчи", - "Image CAPTCHA": "Изображение капчи", + "Text CAPTCHA": "Текстовая капча (англ.)", + "Image CAPTCHA": "Капча-картинка", "Sign In": "Войти", "Register": "Зарегистрироваться", "E-mail": "Электронная почта", @@ -51,7 +51,7 @@ "preferences_category_player": "Настройки проигрывателя", "preferences_video_loop_label": "Всегда повторять: ", "preferences_autoplay_label": "Автовоспроизведение: ", - "preferences_continue_label": "Всегда включать следующее видео? ", + "preferences_continue_label": "Переходить к следующему видео? ", "preferences_continue_autoplay_label": "Автопроигрывание следующего видео: ", "preferences_listen_label": "Режим «только аудио» по умолчанию: ", "preferences_local_label": "Проигрывать видео через прокси? ", @@ -71,13 +71,13 @@ "preferences_player_style_label": "Стиль проигрывателя: ", "Dark mode: ": "Тёмное оформление: ", "preferences_dark_mode_label": "Тема: ", - "dark": "темная", + "dark": "тёмная", "light": "светлая", "preferences_thin_mode_label": "Облегчённое оформление: ", "preferences_category_misc": "Прочие настройки", "preferences_automatic_instance_redirect_label": "Автоматическое перенаправление на зеркало сайта (переход на redirect.invidious.io): ", "preferences_category_subscription": "Настройки подписок", - "preferences_annotations_subscribed_label": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ", + "preferences_annotations_subscribed_label": "Всегда показывать аннотации на каналах из ваших подписок? ", "Redirect homepage to feed: ": "Отображать видео с каналов, на которые вы подписаны, как главную страницу: ", "preferences_max_results_label": "Число видео, на которые вы подписаны, в ленте: ", "preferences_sort_label": "Сортировать видео: ", @@ -96,10 +96,10 @@ "`x` is live": "`x` в прямом эфире", "preferences_category_data": "Настройки данных", "Clear watch history": "Очистить историю просмотров", - "Import/export data": "Импорт/Экспорт данных", + "Import/export data": "Импорт и экспорт данных", "Change password": "Изменить пароль", - "Manage subscriptions": "Управлять подписками", - "Manage tokens": "Управлять токенами", + "Manage subscriptions": "Управление подписками", + "Manage tokens": "Управление токенами", "Watch history": "История просмотров", "Delete account": "Удалить аккаунт", "preferences_category_admin": "Администраторские настройки", @@ -112,8 +112,8 @@ "Registration enabled: ": "Включить регистрацию? ", "Report statistics: ": "Сообщать статистику? ", "Save preferences": "Сохранить настройки", - "Subscription manager": "Менеджер подписок", - "Token manager": "Менеджер токенов", + "Subscription manager": "Управление подписками", + "Token manager": "Управление токенами", "Token": "Токен", "Import/export": "Импорт и экспорт", "unsubscribe": "отписаться", @@ -122,9 +122,9 @@ "search": "поиск", "Log out": "Выйти", "Released under the AGPLv3 on Github.": "Выпущено под лицензией AGPLv3 на GitHub.", - "Source available here.": "Исходный код доступен здесь.", - "View JavaScript license information.": "Посмотреть информацию по лицензии JavaScript.", - "View privacy policy.": "Посмотреть политику конфиденциальности.", + "Source available here.": "Исходный код.", + "View JavaScript license information.": "Информация о лицензиях JavaScript.", + "View privacy policy.": "Политика конфиденциальности.", "Trending": "В тренде", "Public": "Публичный", "Unlisted": "Нет в списке", @@ -135,42 +135,42 @@ "Delete playlist": "Удалить плейлист", "Create playlist": "Создать плейлист", "Title": "Заголовок", - "Playlist privacy": "Конфиденциальность плейлиста", + "Playlist privacy": "Видимость плейлиста", "Editing playlist `x`": "Редактирование плейлиста `x`", - "Show more": "Показать больше", - "Show less": "Показать меньше", + "Show more": "Развернуть", + "Show less": "Свернуть", "Watch on YouTube": "Смотреть на YouTube", - "Switch Invidious Instance": "Сменить экземпляр Invidious", + "Switch Invidious Instance": "Сменить зеркало Invidious", "Hide annotations": "Скрыть аннотации", "Show annotations": "Показать аннотации", "Genre: ": "Жанр: ", "License: ": "Лицензия: ", "Family friendly? ": "Семейный просмотр: ", - "Wilson score: ": "Рейтинг Уилсона: ", + "Wilson score: ": "Оценка Уилсона: ", "Engagement: ": "Вовлечённость: ", "Whitelisted regions: ": "Доступно в регионах: ", "Blacklisted regions: ": "Недоступно в регионах: ", "Shared `x`": "Опубликовано `x`", "Premieres in `x`": "Премьера через `x`", "Premieres `x`": "Премьера `x`", - "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Похоже, у вас отключён JavaScript. Чтобы увидить комментарии, нажмите сюда, но учтите: они могут загружаться немного медленнее.", - "View YouTube comments": "Смотреть комментарии с YouTube", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Похоже, у вас отключён JavaScript. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.", + "View YouTube comments": "Показать комментарии с YouTube", "View more comments on Reddit": "Посмотреть больше комментариев на Reddit", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Показать `x` комментариев", - "": "Показать `x` комментариев" + "([^.,0-9]|^)1([^.,0-9]|$)": "Показано `x` комментариев", + "": "Показано`x` комментариев" }, "View Reddit comments": "Смотреть комментарии с Reddit", "Hide replies": "Скрыть ответы", "Show replies": "Показать ответы", "Incorrect password": "Неправильный пароль", "Quota exceeded, try again in a few hours": "Лимит превышен, попробуйте снова через несколько часов", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Войти не удаётся. Проверьте, не включена ли двухфакторная аутентификация (по коду или смс).", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Не удалось войти. Проверьте, не включена ли двухфакторная аутентификация (по коду или смс).", "Invalid TFA code": "Неправильный код двухфакторной аутентификации", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Не удаётся войти. Это может быть из-за того, что в вашем аккаунте не включена двухфакторная аутентификация.", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Не удалось войти. Это может быть из-за того, что в вашем аккаунте не включена двухфакторная аутентификация.", "Wrong answer": "Неправильный ответ", "Erroneous CAPTCHA": "Неправильная капча", - "CAPTCHA is a required field": "Необходимо пройти капчу", + "CAPTCHA is a required field": "Необходимо решить капчу", "User ID is a required field": "Необходимо ввести ID пользователя", "Password is a required field": "Необходимо ввести пароль", "Wrong username or password": "Неправильный логин или пароль", @@ -185,7 +185,7 @@ "Could not get channel info.": "Не удаётся получить информацию об этом канале.", "Could not fetch comments": "Не удаётся загрузить комментарии", "`x` ago": "`x` назад", - "Load more": "Загрузить больше", + "Load more": "Загрузить ещё", "Could not create mix.": "Не удаётся создать микс.", "Empty playlist": "Плейлист пуст", "Not a playlist.": "Некорректный плейлист.", @@ -219,7 +219,7 @@ "Croatian": "Хорватский", "Czech": "Чешский", "Danish": "Датский", - "Dutch": "Нидерландский", + "Dutch": "Голландский", "Esperanto": "Эсперанто", "Estonian": "Эстонский", "Filipino": "Филиппинский", @@ -229,8 +229,8 @@ "Georgian": "Грузинский", "German": "Немецкий", "Greek": "Греческий", - "Gujarati": "Гуджаратский", - "Haitian Creole": "Гаит. креольский", + "Gujarati": "Гуджарати", + "Haitian Creole": "Гаитянский креольский", "Hausa": "Хауса", "Hawaiian": "Гавайский", "Hebrew": "Иврит", @@ -251,7 +251,7 @@ "Kurdish": "Курдский", "Kyrgyz": "Киргизский", "Lao": "Лаосский", - "Latin": "Латинский", + "Latin": "Латынь", "Latvian": "Латышский", "Lithuanian": "Литовский", "Luxembourgish": "Люксембургский", @@ -262,9 +262,9 @@ "Maltese": "Мальтийский", "Maori": "Маори", "Marathi": "Маратхи", - "Mongolian": "Монгольская", + "Mongolian": "Монгольский", "Nepali": "Непальский", - "Norwegian Bokmål": "Норвежский", + "Norwegian Bokmål": "Норвежский букмол", "Nyanja": "Ньянджа", "Pashto": "Пушту", "Persian": "Персидский", @@ -299,7 +299,7 @@ "Vietnamese": "Вьетнамский", "Welsh": "Валлийский", "Western Frisian": "Западнофризский", - "Xhosa": "Коса", + "Xhosa": "Коса (кхоса)", "Yiddish": "Идиш", "Yoruba": "Йоруба", "Zulu": "Зулусский", @@ -311,7 +311,7 @@ "Rating: ": "Рейтинг: ", "preferences_locale_label": "Язык: ", "View as playlist": "Смотреть как плейлист", - "Default": "По-умолчанию", + "Default": "По умолчанию", "Music": "Музыка", "Gaming": "Игры", "News": "Новости", @@ -328,14 +328,14 @@ "Videos": "Видео", "Playlists": "Плейлисты", "Community": "Сообщество", - "search_filters_sort_option_relevance": "Актуальность", - "search_filters_sort_option_rating": "Рейтинг", - "search_filters_sort_option_date": "Дата загрузки", - "search_filters_sort_option_views": "Просмотры", + "search_filters_sort_option_relevance": "по актуальности", + "search_filters_sort_option_rating": "по рейтингу", + "search_filters_sort_option_date": "по дате загрузки", + "search_filters_sort_option_views": "по просмотрам", "search_filters_type_label": "Тип", "search_filters_duration_label": "Длительность", - "search_filters_features_label": "Функции", - "search_filters_sort_label": "Сортировать по", + "search_filters_features_label": "Дополнительно", + "search_filters_sort_label": "Сортировать", "search_filters_date_option_hour": "Последний час", "search_filters_date_option_today": "Сегодня", "search_filters_date_option_week": "Эта неделя", @@ -345,7 +345,7 @@ "search_filters_type_option_channel": "Канал", "search_filters_type_option_playlist": "Плейлист", "search_filters_type_option_movie": "Фильм", - "search_filters_type_option_show": "Показать", + "search_filters_type_option_show": "Сериал", "search_filters_features_option_hd": "HD", "search_filters_features_option_subtitles": "Субтитры", "search_filters_features_option_c_commons": "Creative Commons", @@ -368,28 +368,28 @@ "English (United States)": "Английский (США)", "Cantonese (Hong Kong)": "Кантонский (Гонконг)", "Chinese (Taiwan)": "Китайский (Тайвань)", - "Dutch (auto-generated)": "Голландский (автоматический)", - "German (auto-generated)": "Немецкий (автоматический)", - "Indonesian (auto-generated)": "Индонезийский (автоматический)", - "Italian (auto-generated)": "Итальянский (автоматический)", + "Dutch (auto-generated)": "Голландский (созданы автоматически)", + "German (auto-generated)": "Немецкий (созданы автоматически)", + "Indonesian (auto-generated)": "Индонезийский (созданы автоматически)", + "Italian (auto-generated)": "Итальянский (созданы автоматически)", "Interlingue": "Окциденталь", - "Russian (auto-generated)": "Русский (автоматический)", - "Spanish (auto-generated)": "Испанский (автоматический)", + "Russian (auto-generated)": "Русский (созданы автоматически)", + "Spanish (auto-generated)": "Испанский (созданы автоматически)", "Spanish (Spain)": "Испанский (Испания)", - "Turkish (auto-generated)": "Турецкий (автоматический)", - "Vietnamese (auto-generated)": "Вьетнамский (автоматический)", + "Turkish (auto-generated)": "Турецкий (созданы автоматически)", + "Vietnamese (auto-generated)": "Вьетнамский (созданы автоматически)", "footer_documentation": "Документация", "adminprefs_modified_source_code_url_label": "Ссылка на нашу ветку репозитория", "none": "ничего", "videoinfo_watch_on_youTube": "Смотреть на YouTube", - "videoinfo_youTube_embed_link": "Встраиваемый элемент", - "videoinfo_invidious_embed_link": "Встраиваемая ссылка", + "videoinfo_youTube_embed_link": "Версия для встраивания", + "videoinfo_invidious_embed_link": "Ссылка для встраивания", "download_subtitles": "Субтитры - `x` (.vtt)", "user_created_playlists": "`x` созданных плейлистов", - "crash_page_you_found_a_bug": "Похоже вы нашли баг в Invidious!", + "crash_page_you_found_a_bug": "Похоже, вы нашли ошибку в Invidious!", "crash_page_before_reporting": "Прежде чем сообщать об ошибке, убедитесь, что вы:", "crash_page_refresh": "пробовали перезагрузить страницу", - "crash_page_report_issue": "Если ни один вариант не помог, пожалуйста откройте новую проблему на GitHub (желательно на английском) и приложите следующий текст к вашему сообщению (НЕ переводите его):", + "crash_page_report_issue": "Если ни один вариант не помог, пожалуйста откройте новую проблему на GitHub (на английском, пжлста) и приложите следующий текст к вашему сообщению (НЕ переводите его):", "generic_videos_count_0": "{{count}} видео", "generic_videos_count_1": "{{count}} видео", "generic_videos_count_2": "{{count}} видео", @@ -417,8 +417,8 @@ "generic_views_count_0": "{{count}} просмотр", "generic_views_count_1": "{{count}} просмотра", "generic_views_count_2": "{{count}} просмотров", - "French (auto-generated)": "Французский (автоматический)", - "Portuguese (auto-generated)": "Португальский (автоматический)", + "French (auto-generated)": "Французский (созданы автоматически)", + "Portuguese (auto-generated)": "Португальский (созданы автоматически)", "generic_count_days_0": "{{count}} день", "generic_count_days_1": "{{count}} дня", "generic_count_days_2": "{{count}} дней", @@ -438,12 +438,12 @@ "search_filters_features_option_purchased": "Приобретено", "videoinfo_started_streaming_x_ago": "Трансляция началась `x` назад", "crash_page_switch_instance": "пробовали использовать другое зеркало", - "crash_page_read_the_faq": "прочли Частые Вопросы (ЧаВо)", + "crash_page_read_the_faq": "прочли ответы на Частые Вопросы (ЧаВо)", "Chinese": "Китайский", "Chinese (Hong Kong)": "Китайский (Гонконг)", - "Japanese (auto-generated)": "Японский (автоматический)", + "Japanese (auto-generated)": "Японский (созданы автоматически)", "Chinese (China)": "Китайский (Китай)", - "Korean (auto-generated)": "Корейский (автоматический)", + "Korean (auto-generated)": "Корейский (созданы автоматически)", "generic_count_months_0": "{{count}} месяц", "generic_count_months_1": "{{count}} месяца", "generic_count_months_2": "{{count}} месяцев", @@ -455,7 +455,7 @@ "footer_original_source_code": "Оригинальный исходный код", "footer_modfied_source_code": "Изменённый исходный код", "user_saved_playlists": "`x` сохранённых плейлистов", - "crash_page_search_issue": "искали похожую проблему на GitHub", + "crash_page_search_issue": "поискали похожую проблему на GitHub", "comments_points_count_0": "{{count}} плюс", "comments_points_count_1": "{{count}} плюса", "comments_points_count_2": "{{count}} плюсов", @@ -464,7 +464,7 @@ "preferences_quality_option_dash": "DASH (автоматическое качество)", "preferences_quality_option_hd720": "HD720", "preferences_quality_option_medium": "Среднее", - "preferences_quality_dash_label": "Предпочтительное автоматическое качество видео: ", + "preferences_quality_dash_label": "Предпочтительное качество для DASH: ", "preferences_quality_dash_option_worst": "Очень низкое", "preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_2160p": "2160p", @@ -475,16 +475,16 @@ "Video unavailable": "Видео недоступно", "preferences_save_player_pos_label": "Запоминать позицию: ", "preferences_region_label": "Страна: ", - "preferences_watch_history_label": "Включить историю просмотров ", + "preferences_watch_history_label": "Включить историю просмотров: ", "search_filters_title": "Фильтр", "search_filters_duration_option_none": "Любой длины", "search_filters_type_option_all": "Любого типа", - "search_filters_date_option_none": "Любой даты", + "search_filters_date_option_none": "Любая дата", "search_filters_date_label": "Дата загрузки", "search_message_no_results": "Ничего не найдено.", "search_message_use_another_instance": " Дополнительно вы можете поискать на других зеркалах.", "search_filters_features_option_vr180": "VR180", - "search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос и изменить фильтры.", + "search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос или изменить фильтры.", "search_filters_duration_option_medium": "Средние (4 - 20 минут)", "search_filters_apply_button": "Применить фильтры" } From e22f7583eb86d4ec962a57cdd6035f6f3ae16fcc Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 9 Jun 2022 00:12:32 +0200 Subject: [PATCH 37/42] Update Ukrainian translation Co-authored-by: Hosted Weblate Co-authored-by: Ihor Hordiichuk --- locales/uk.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/locales/uk.json b/locales/uk.json index dd03d559..23f56c9a 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -1,6 +1,6 @@ { - "LIVE": "ПРЯМИЙ ЕФІР", - "Shared `x` ago": "Розміщено `x` назад", + "LIVE": "НАЖИВО", + "Shared `x` ago": "Розміщено `x` тому", "Unsubscribe": "Відписатися", "Subscribe": "Підписатися", "View channel on YouTube": "Подивитися канал на YouTube", @@ -30,7 +30,7 @@ "Export subscriptions as OPML": "Експортувати підписки у форматі OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Експортувати підписки у форматі OPML (для NewPipe та FreeTube)", "Export data as JSON": "Експортувати дані Invidious у форматі JSON", - "Delete account?": "Видалити обліківку?", + "Delete account?": "Видалити обліковий запис?", "History": "Історія", "An alternative front-end to YouTube": "Альтернативний фронтенд до YouTube", "JavaScript license information": "Інформація щодо ліцензій JavaScript", @@ -40,9 +40,9 @@ "Log in with Google": "Увійти через Google", "User ID": "ID користувача", "Password": "Пароль", - "Time (h:mm:ss):": "Час (г:мм:сс):", - "Text CAPTCHA": "Текст капчі", - "Image CAPTCHA": "Зображення капчі", + "Time (h:mm:ss):": "Час (г:хх:сс):", + "Text CAPTCHA": "Текст CAPTCHA", + "Image CAPTCHA": "Зображення CAPTCHA", "Sign In": "Увійти", "Register": "Зареєструватися", "E-mail": "Електронна пошта", @@ -142,7 +142,7 @@ "Whitelisted regions: ": "Доступно у регіонах: ", "Blacklisted regions: ": "Недоступно у регіонах: ", "Shared `x`": "Розміщено `x`", - "Premieres in `x`": "Прем’єра через `x`", + "Premieres in `x`": "Прем’єра за `x`", "Premieres `x`": "Прем’єра `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Схоже, у вас відключений JavaScript. Щоб побачити коментарі, натисніть сюда, але майте на увазі, що вони можуть завантажуватися трохи довше.", "View YouTube comments": "Переглянути коментарі з YouTube", @@ -157,11 +157,11 @@ "Incorrect password": "Неправильний пароль", "Quota exceeded, try again in a few hours": "Ліміт перевищено, спробуйте знову за декілька годин", "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Не вдається увійти. Перевірте, чи не ввімкнена двофакторна аутентифікація (за кодом чи смс).", - "Invalid TFA code": "Неправильний код двофакторної аутентифікації", + "Invalid TFA code": "Неправильний код двофакторної автентифікації", "Login failed. This may be because two-factor authentication is not turned on for your account.": "Не вдається увійти. Це може бути через те, що у вашій обліківці не ввімкнена двофакторна аутентифікація.", "Wrong answer": "Неправильна відповідь", "Erroneous CAPTCHA": "Неправильна капча", - "CAPTCHA is a required field": "Необхідно пройти капчу", + "CAPTCHA is a required field": "Необхідно пройти CAPTCHA", "User ID is a required field": "Необхідно ввести ID користувача", "Password is a required field": "Необхідно ввести пароль", "Wrong username or password": "Неправильний логін чи пароль", @@ -169,7 +169,7 @@ "Password cannot be empty": "Пароль не може бути порожнім", "Password cannot be longer than 55 characters": "Пароль не може бути довшим за 55 знаків", "Please log in": "Будь ласка, увійдіть", - "Invidious Private Feed for `x`": "Приватний поток відео Invidious для `x`", + "Invidious Private Feed for `x`": "Приватний потік відео Invidious для `x`", "channel:`x`": "канал: `x`", "Deleted or invalid channel": "Канал видалено або не знайдено", "This channel does not exist.": "Такого каналу не існує.", From f7290dfcb685b55a924090e3420186688e12679f Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 9 Jun 2022 00:12:32 +0200 Subject: [PATCH 38/42] Update Croatian translation Update Croatian translation Co-authored-by: Hosted Weblate Co-authored-by: Milo Ivir --- locales/hr.json | 76 ++++++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/locales/hr.json b/locales/hr.json index 94633aac..7eb065dc 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -137,8 +137,8 @@ "Title": "Naslov", "Playlist privacy": "Privatnost zbirke", "Editing playlist `x`": "Uređivanje zbirke `x`", - "Show more": "Pokaži više", - "Show less": "Pokaži manje", + "Show more": "Prikaži više", + "Show less": "Prikaži manje", "Watch on YouTube": "Gledaj na YouTubeu", "Switch Invidious Instance": "Promijeni Invidious instancu", "Hide annotations": "Sakrij napomene", @@ -318,7 +318,7 @@ "Movies": "Filmovi", "Download": "Preuzmi", "Download as: ": "Preuzmi kao: ", - "%A %B %-d, %Y": "%A, %-d. %B %Y", + "%A %B %-d, %Y": "%A, %-d. %B %Y.", "(edited)": "(uređeno)", "YouTube comment permalink": "Stalna poveznica YouTube komentara", "permalink": "stalna poveznica", @@ -328,40 +328,40 @@ "Videos": "Videa", "Playlists": "Zbirke", "Community": "Zajednica", - "search_filters_sort_option_relevance": "značaj", - "search_filters_sort_option_rating": "ocjena", - "search_filters_sort_option_date": "datum", - "search_filters_sort_option_views": "prikazi", - "search_filters_type_label": "vrsta_sadržaja", - "search_filters_duration_label": "trajanje", - "search_filters_features_label": "funkcije", - "search_filters_sort_label": "redoslijed", - "search_filters_date_option_hour": "sat", - "search_filters_date_option_today": "danas", - "search_filters_date_option_week": "tjedan", - "search_filters_date_option_month": "mjesec", - "search_filters_date_option_year": "godina", - "search_filters_type_option_video": "video", - "search_filters_type_option_channel": "kanal", + "search_filters_sort_option_relevance": "Značaj", + "search_filters_sort_option_rating": "Ocjena", + "search_filters_sort_option_date": "Datum prijenosa", + "search_filters_sort_option_views": "Broj gledanja", + "search_filters_type_label": "Vrsta", + "search_filters_duration_label": "Trajanje", + "search_filters_features_label": "Funkcije", + "search_filters_sort_label": "Redoslijed", + "search_filters_date_option_hour": "Zadnjih sat vremena", + "search_filters_date_option_today": "Danas", + "search_filters_date_option_week": "Ovaj tjedan", + "search_filters_date_option_month": "Ovaj mjesec", + "search_filters_date_option_year": "Ova godina", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Kanal", "search_filters_type_option_playlist": "Zbirka", - "search_filters_type_option_movie": "film", - "search_filters_type_option_show": "emisija", - "search_filters_features_option_hd": "hd", - "search_filters_features_option_subtitles": "titlovi", - "search_filters_features_option_c_commons": "creative_commons", - "search_filters_features_option_three_d": "3d", - "search_filters_features_option_live": "uživo", + "search_filters_type_option_movie": "Film", + "search_filters_type_option_show": "Emisija", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Titlovi/CC", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Uživo", "search_filters_features_option_four_k": "4k", - "search_filters_features_option_location": "lokacija", - "search_filters_features_option_hdr": "hdr", + "search_filters_features_option_location": "Lokacija", + "search_filters_features_option_hdr": "HDR", "Current version: ": "Trenutačna verzija: ", "next_steps_error_message": "Nakon toga bi trebali pokušati sljedeće: ", "next_steps_error_message_refresh": "Aktualiziraj stranicu", "next_steps_error_message_go_to_youtube": "Idi na YouTube", "footer_donate_page": "Doniraj", "adminprefs_modified_source_code_url_label": "URL do repozitorija izmijenjenog izvornog koda", - "search_filters_duration_option_short": "Kratki (< 4 minute)", - "search_filters_duration_option_long": "Dugi (> 20 minute)", + "search_filters_duration_option_short": "Kratko (< 4 minute)", + "search_filters_duration_option_long": "Dugo (> 20 minute)", "footer_source_code": "Izvorni kod", "footer_modfied_source_code": "Izmijenjeni izvorni kod", "footer_documentation": "Dokumentacija", @@ -384,8 +384,8 @@ "search_filters_features_option_three_sixty": "360 °", "none": "bez", "videoinfo_youTube_embed_link": "Ugradi", - "user_created_playlists": "`x` stvorene zbirke", - "user_saved_playlists": "`x` spremljene zbirke", + "user_created_playlists": "`x` je stvorio/la zbirke", + "user_saved_playlists": "`x` je spremio/la zbirke", "Video unavailable": "Video nedostupan", "preferences_save_player_pos_label": "Spremi mjesto reprodukcije: ", "videoinfo_watch_on_youTube": "Gledaj na YouTubeu", @@ -432,7 +432,7 @@ "generic_subscriptions_count_2": "{{count}} pretplata", "generic_playlists_count_0": "{{count}} zbirka", "generic_playlists_count_1": "{{count}} zbirke", - "generic_playlists_count_2": "{{count}} zbirka", + "generic_playlists_count_2": "{{count}} zbiraka", "generic_videos_count_0": "{{count}} video", "generic_videos_count_1": "{{count}} videa", "generic_videos_count_2": "{{count}} videa", @@ -476,5 +476,15 @@ "Portuguese (auto-generated)": "Portugalski (automatski generiran)", "Spanish (auto-generated)": "Španjolski (automatski generiran)", "preferences_watch_history_label": "Aktiviraj povijest gledanja: ", - "search_filters_title": "Filtar" + "search_filters_title": "Filtri", + "search_filters_date_option_none": "Bilo koji datum", + "search_filters_date_label": "Datum prijenosa", + "search_message_no_results": "Nema rezultata.", + "search_message_use_another_instance": " Također možeš tražiti na jednoj drugoj instanci.", + "search_message_change_filters_or_query": "Pokušaj proširiti upit za pretragu i/ili promijeni filtre.", + "search_filters_features_option_vr180": "VR180", + "search_filters_duration_option_none": "Bilo koje duljine", + "search_filters_duration_option_medium": "Srednje (4 – 20 minuta)", + "search_filters_apply_button": "Primijeni odabrane filtre", + "search_filters_type_option_all": "Bilo koja vrsta" } From 600bd38630256d39b4c85c0e4285a42fce6e3721 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 9 Jun 2022 00:12:32 +0200 Subject: [PATCH 39/42] Update Portuguese translation Co-authored-by: Hosted Weblate Co-authored-by: SC --- locales/pt.json | 46 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index df237649..1abe46fa 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -11,7 +11,7 @@ "preferences_show_nick_label": "Mostrar nome de utilizador em cima: ", "preferences_automatic_instance_redirect_label": "Redirecionamento de instância automática (solução de último recurso para redirect.invidious.io): ", "preferences_category_misc": "Preferências diversas", - "preferences_vr_mode_label": "Vídeos interativos de 360 graus: ", + "preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ", "preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ", "next_steps_error_message_go_to_youtube": "Ir ao YouTube", "next_steps_error_message": "Pode tentar as seguintes opções: ", @@ -246,15 +246,15 @@ "JavaScript license information": "Informação de licença do JavaScript", "An alternative front-end to YouTube": "Uma interface alternativa ao YouTube", "History": "Histórico", - "Export data as JSON": "Exportar dados como JSON", + "Export data as JSON": "Exportar dados Invidious como JSON", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar subscrições como OPML (para NewPipe e FreeTube)", "Export subscriptions as OPML": "Exportar subscrições como OPML", "Export": "Exportar", "Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)", "Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)", "Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)", - "Import YouTube subscriptions": "Importar subscrições do YouTube", - "Import Invidious data": "Importar dados do Invidious", + "Import YouTube subscriptions": "Importar subscrições do YouTube/OPML", + "Import Invidious data": "Importar dados JSON do Invidious", "Import": "Importar", "No": "Não", "Yes": "Sim", @@ -432,9 +432,43 @@ "crash_page_before_reporting": "Antes de reportar um erro, verifique se:", "crash_page_refresh": "tentou recarregar a página", "crash_page_switch_instance": "tentou usar outra instância", - "crash_page_read_the_faq": "leu as Perguntas frequentes (FAQ)", + "crash_page_read_the_faq": "leia as Perguntas frequentes (FAQ)", "crash_page_search_issue": "procurou se o erro já foi reportado no GitHub", "crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor abra um novo problema no Github (preferencialmente em inglês) e inclua o seguinte texto tal qual (NÃO o traduza):", "user_created_playlists": "`x` listas de reprodução criadas", - "search_filters_title": "Filtro" + "search_filters_title": "Filtro", + "Chinese (Taiwan)": "Chinês (Taiwan)", + "search_message_no_results": "Nenhum resultado encontrado.", + "search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.", + "search_message_use_another_instance": " Também pode pesquisar noutra instância.", + "English (United Kingdom)": "Inglês (Reino Unido)", + "English (United States)": "Inglês (Estados Unidos)", + "Cantonese (Hong Kong)": "Cantonês (Hong Kong)", + "Chinese": "Chinês", + "Chinese (Hong Kong)": "Chinês (Hong Kong)", + "Dutch (auto-generated)": "Holandês (gerado automaticamente)", + "French (auto-generated)": "Francês (gerado automaticamente)", + "German (auto-generated)": "Alemão (gerado automaticamente)", + "Indonesian (auto-generated)": "Indonésio (gerado automaticamente)", + "Interlingue": "Interlíngua", + "Italian (auto-generated)": "Italiano (gerado automaticamente)", + "Japanese (auto-generated)": "Japonês (gerado automaticamente)", + "Korean (auto-generated)": "Coreano (gerado automaticamente)", + "Portuguese (auto-generated)": "Português (gerado automaticamente)", + "Portuguese (Brazil)": "Português (Brasil)", + "Turkish (auto-generated)": "Turco (gerado automaticamente)", + "Vietnamese (auto-generated)": "Vietnamita (gerado automaticamente)", + "search_filters_duration_option_medium": "Médio (4 - 20 minutos)", + "search_filters_features_option_vr180": "VR180", + "search_filters_apply_button": "Aplicar filtros selecionados", + "Spanish (auto-generated)": "Espanhol (gerado automaticamente)", + "Spanish (Mexico)": "Espanhol (México)", + "preferences_watch_history_label": "Ativar histórico de reprodução: ", + "Chinese (China)": "Chinês (China)", + "Russian (auto-generated)": "Russo (gerado automaticamente)", + "Spanish (Spain)": "Espanhol (Espanha)", + "search_filters_date_label": "Data de publicação", + "search_filters_date_option_none": "Qualquer data", + "search_filters_type_option_all": "Qualquer tipo", + "search_filters_duration_option_none": "Qualquer duração" } From 7708e7ab08b54d18762273ed8c060d5b2f266a9b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 9 Jun 2022 00:12:32 +0200 Subject: [PATCH 40/42] Update Slovenian translation Update Slovenian translation Co-authored-by: Damjan Gerl Co-authored-by: Hosted Weblate --- locales/sl.json | 58 ++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/locales/sl.json b/locales/sl.json index 791a01c5..9165e714 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -80,7 +80,7 @@ "preferences_category_admin": "Skrbniške nastavitve", "preferences_default_home_label": "Privzeta domača stran: ", "preferences_feed_menu_label": "Meni vira: ", - "Top enabled: ": "Vrh je omogočen: ", + "Top enabled: ": "Vrh omogočen: ", "CAPTCHA enabled: ": "CAPTCHA omogočeni: ", "Login enabled: ": "Prijava je omogočena: ", "Registration enabled: ": "Registracija je omogočena: ", @@ -112,7 +112,7 @@ "Wilson score: ": "Wilsonov rezultat: ", "Engagement: ": "Sodelovanje: ", "Blacklisted regions: ": "Regije na seznamu nedovoljenih: ", - "Shared `x`": "V skupni rabi `x`", + "Shared `x`": "V skupni rabi od: `x`", "Premieres `x`": "Premiere `x`", "View YouTube comments": "Oglej si YouTube komentarje", "View more comments on Reddit": "Prikaži več komentarjev na Reddit", @@ -201,22 +201,22 @@ "Yiddish": "jidiš", "Yoruba": "joruba", "Xhosa": "xhosa", - "generic_count_years_0": "{{count}} leto", + "generic_count_years_0": "{{count}} letom", "generic_count_years_1": "{{count}} leti", - "generic_count_years_2": "{{count}} leta", - "generic_count_years_3": "{{count}} let", - "generic_count_days_0": "{{count}} dan", - "generic_count_days_1": "{{count}} dneva", - "generic_count_days_2": "{{count}} dni", - "generic_count_days_3": "{{count}} dni", - "generic_count_hours_0": "{{count}} ura", - "generic_count_hours_1": "{{count}} uri", - "generic_count_hours_2": "{{count}} ure", - "generic_count_hours_3": "{{count}} ur", - "generic_count_minutes_0": "{{count}} minuta", - "generic_count_minutes_1": "{{count}} minuti", - "generic_count_minutes_2": "{{count}} minute", - "generic_count_minutes_3": "{{count}} minut", + "generic_count_years_2": "{{count}} leti", + "generic_count_years_3": "{{count}} leti", + "generic_count_days_0": "{{count}} dnevom", + "generic_count_days_1": "{{count}} dnevi", + "generic_count_days_2": "{{count}} dnevi", + "generic_count_days_3": "{{count}} dnevi", + "generic_count_hours_0": "{{count}} uro", + "generic_count_hours_1": "{{count}} urami", + "generic_count_hours_2": "{{count}} urami", + "generic_count_hours_3": "{{count}} urami", + "generic_count_minutes_0": "{{count}} minuto", + "generic_count_minutes_1": "{{count}} minutami", + "generic_count_minutes_2": "{{count}} minutami", + "generic_count_minutes_3": "{{count}} minutami", "Search": "Iskanje", "Top": "Vrh", "About": "O aplikaciji", @@ -423,23 +423,23 @@ "Spanish (Spain)": "španščina (Španija)", "Tajik": "tadžiščina", "Tamil": "tamilščina", - "generic_count_weeks_0": "{{count}} teden", - "generic_count_weeks_1": "{{count}} tedna", - "generic_count_weeks_2": "{{count}} tedne", - "generic_count_weeks_3": "{{count}} tednov", + "generic_count_weeks_0": "{{count}} tednom", + "generic_count_weeks_1": "{{count}} tedni", + "generic_count_weeks_2": "{{count}} tedni", + "generic_count_weeks_3": "{{count}} tedni", "Swahili": "svahilščina", "Swedish": "švedščina", "Vietnamese (auto-generated)": "vietnamščina (samodejno ustvarjeno)", - "generic_count_months_0": "{{count}} mesec", - "generic_count_months_1": "{{count}} meseca", - "generic_count_months_2": "{{count}} mesece", - "generic_count_months_3": "{{count}} mesecev", + "generic_count_months_0": "{{count}} mesecem", + "generic_count_months_1": "{{count}} meseci", + "generic_count_months_2": "{{count}} meseci", + "generic_count_months_3": "{{count}} meseci", "Uzbek": "uzbeščina", "Zulu": "zulujščina", - "generic_count_seconds_0": "{{count}} sekunda", - "generic_count_seconds_1": "{{count}} sekundi", - "generic_count_seconds_2": "{{count}} sekunde", - "generic_count_seconds_3": "{{count}} sekund", + "generic_count_seconds_0": "{{count}} sekundo", + "generic_count_seconds_1": "{{count}} sekundami", + "generic_count_seconds_2": "{{count}} sekundami", + "generic_count_seconds_3": "{{count}} sekundami", "Popular": "Priljubljeni", "Music": "Glasba", "Movies": "Filmi", From 233491940c5a614b81a531e8f0e4f44821cb5021 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 9 Jun 2022 00:12:33 +0200 Subject: [PATCH 41/42] Update Indonesian translation Co-authored-by: Hosted Weblate Co-authored-by: I. Musthafa --- locales/id.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/id.json b/locales/id.json index 71b7bdb1..c96495c3 100644 --- a/locales/id.json +++ b/locales/id.json @@ -418,5 +418,8 @@ "English (United States)": "Inggris (US)", "preferences_watch_history_label": "Aktifkan riwayat tontonan: ", "English (United Kingdom)": "Inggris (UK)", - "search_filters_title": "Saring" + "search_filters_title": "Saring", + "search_message_no_results": "Tidak ada hasil yang ditemukan.", + "search_message_change_filters_or_query": "Coba perbanyak kueri pencarian dan/atau ubah filter Anda.", + "search_message_use_another_instance": " Anda juga bisa mencari di peladen lain." } From 9418ba1687eb72f88d4793f4df21dd4aae1645a7 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 9 Jun 2022 00:12:33 +0200 Subject: [PATCH 42/42] Update Bengali translation Update Bengali translation Add Bengali translation Co-authored-by: Hosted Weblate Co-authored-by: Oymate --- locales/bn.json | 97 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 locales/bn.json diff --git a/locales/bn.json b/locales/bn.json new file mode 100644 index 00000000..3d1cb5da --- /dev/null +++ b/locales/bn.json @@ -0,0 +1,97 @@ +{ + "Subscribe": "সাবস্ক্রাইব", + "View channel on YouTube": "ইউটিউবে চ্যানেল দেখুন", + "View playlist on YouTube": "ইউটিউবে প্লেলিস্ট দেখুন", + "newest": "সর্ব-নতুন", + "oldest": "পুরানতম", + "popular": "জনপ্রিয়", + "last": "শেষটা", + "Next page": "পরের পৃষ্ঠা", + "Previous page": "আগের পৃষ্ঠা", + "Clear watch history?": "দেখার ইতিহাস সাফ করবেন?", + "New password": "নতুন পাসওয়ার্ড", + "New passwords must match": "নতুন পাসওয়ার্ড অবশ্যই মিলতে হবে", + "Cannot change password for Google accounts": "গুগল অ্যাকাউন্টগুলোর জন্য পাসওয়ার্ড পরিবর্তন করা যায় না", + "Authorize token?": "টোকেন অনুমোদন করবেন?", + "Authorize token for `x`?": "`x` -এর জন্য টোকেন অনুমোদন?", + "Yes": "হ্যাঁ", + "No": "না", + "Import and Export Data": "তথ্য আমদানি ও রপ্তানি", + "Import": "আমদানি", + "Import Invidious data": "ইনভিডিয়াস তথ্য আমদানি", + "Import YouTube subscriptions": "ইউটিউব সাবস্ক্রিপশন আনুন", + "Import FreeTube subscriptions (.db)": "ফ্রিটিউব সাবস্ক্রিপশন (.db) আনুন", + "Import NewPipe subscriptions (.json)": "নতুন পাইপ সাবস্ক্রিপশন আনুন (.json)", + "Import NewPipe data (.zip)": "নিউপাইপ তথ্য আনুন (.zip)", + "Export": "তথ্য বের করুন", + "Export subscriptions as OPML": "সাবস্ক্রিপশন OPML হিসাবে আনুন", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML-এ সাবস্ক্রিপশন বের করুন(নিউ পাইপ এবং ফ্রিউটিউব এর জন্য)", + "Export data as JSON": "JSON হিসাবে তথ্য বের করুন", + "Delete account?": "অ্যাকাউন্ট মুছে ফেলবেন?", + "History": "ইতিহাস", + "An alternative front-end to YouTube": "ইউটিউবের একটি বিকল্পস্বরূপ সম্মুখ-প্রান্ত", + "JavaScript license information": "জাভাস্ক্রিপ্ট লাইসেন্সের তথ্য", + "source": "সূত্র", + "Log in": "লগ ইন", + "Log in/register": "লগ ইন/রেজিস্টার", + "Log in with Google": "গুগল দিয়ে লগ ইন করুন", + "User ID": "ইউজার আইডি", + "Password": "পাসওয়ার্ড", + "Time (h:mm:ss):": "সময় (ঘণ্টা:মিনিট:সেকেন্ড):", + "Text CAPTCHA": "টেক্সট ক্যাপচা", + "Image CAPTCHA": "চিত্র ক্যাপচা", + "Sign In": "সাইন ইন", + "Register": "নিবন্ধন", + "E-mail": "ই-মেইল", + "Google verification code": "গুগল যাচাইকরণ কোড", + "Preferences": "পছন্দসমূহ", + "preferences_category_player": "প্লেয়ারের পছন্দসমূহ", + "preferences_video_loop_label": "সর্বদা লুপ: ", + "preferences_autoplay_label": "স্বয়ংক্রিয় চালু: ", + "preferences_continue_label": "ডিফল্টভাবে পরবর্তী চালাও: ", + "preferences_continue_autoplay_label": "পরবর্তী ভিডিও স্বয়ংক্রিয়ভাবে চালাও: ", + "preferences_listen_label": "সহজাতভাবে শোনো: ", + "preferences_local_label": "ভিডিও প্রক্সি করো: ", + "preferences_speed_label": "সহজাত গতি: ", + "preferences_quality_label": "পছন্দের ভিডিও মান: ", + "preferences_volume_label": "প্লেয়ার শব্দের মাত্রা: ", + "LIVE": "লাইভ", + "Shared `x` ago": "`x` আগে শেয়ার করা হয়েছে", + "Unsubscribe": "আনসাবস্ক্রাইব", + "generic_views_count": "{{count}}জন দেখেছে", + "generic_views_count_plural": "{{count}}জন দেখেছে", + "generic_videos_count": "{{count}}টি ভিডিও", + "generic_videos_count_plural": "{{count}}টি ভিডিও", + "generic_subscribers_count": "{{count}}জন অনুসরণকারী", + "generic_subscribers_count_plural": "{{count}}জন অনুসরণকারী", + "preferences_watch_history_label": "দেখার ইতিহাস চালু করো: ", + "preferences_quality_option_dash": "ড্যাশ (সময়োপযোগী মান)", + "preferences_quality_dash_option_auto": "স্বয়ংক্রিয়", + "preferences_quality_dash_option_best": "সেরা", + "preferences_quality_dash_option_worst": "মন্দতম", + "preferences_quality_dash_option_4320p": "৪৩২০পি", + "preferences_quality_dash_option_2160p": "২১৬০পি", + "preferences_quality_dash_option_1440p": "১৪৪০পি", + "preferences_quality_dash_option_480p": "৪৮০পি", + "preferences_quality_dash_option_360p": "৩৬০পি", + "preferences_quality_dash_option_240p": "২৪০পি", + "preferences_quality_dash_option_144p": "১৪৪পি", + "preferences_comments_label": "সহজাত মন্তব্য: ", + "youtube": "ইউটিউব", + "Fallback captions: ": "বিকল্প উপাখ্যান: ", + "preferences_related_videos_label": "সম্পর্কিত ভিডিও দেখাও: ", + "preferences_annotations_label": "সহজাতভাবে টীকা দেখাও ", + "preferences_quality_option_hd720": "উচ্চ৭২০", + "preferences_quality_dash_label": "পছন্দের ড্যাশ ভিডিও মান: ", + "preferences_captions_label": "সহজাত উপাখ্যান: ", + "generic_playlists_count": "{{count}}টি চালুতালিকা", + "generic_playlists_count_plural": "{{count}}টি চালুতালিকা", + "reddit": "রেডিট", + "invidious": "ইনভিডিয়াস", + "generic_subscriptions_count": "{{count}}টি অনুসরণ", + "generic_subscriptions_count_plural": "{{count}}টি অনুসরণ", + "preferences_quality_option_medium": "মধ্যম", + "preferences_quality_option_small": "ছোট", + "preferences_quality_dash_option_1080p": "১০৮০পি", + "preferences_quality_dash_option_720p": "৭২০পি" +}