summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.ameba.yml4
-rw-r--r--.github/workflows/ci.yml4
-rw-r--r--README.md9
-rw-r--r--assets/css/default.css29
-rw-r--r--assets/css/player.css3
-rw-r--r--assets/css/search.css118
-rw-r--r--assets/js/community.js27
-rw-r--r--assets/js/embed.js26
-rw-r--r--assets/js/handlers.js22
-rw-r--r--assets/js/notifications.js31
-rw-r--r--assets/js/player.js264
-rw-r--r--assets/js/playlist_widget.js23
-rw-r--r--assets/js/silvermine-videojs-quality-selector.min.js4
-rw-r--r--assets/js/subscribe_widget.js48
-rw-r--r--assets/js/themes.js23
-rw-r--r--assets/js/watch.js130
-rw-r--r--assets/js/watched_widget.js19
-rw-r--r--docker-compose.yml2
-rw-r--r--docker/Dockerfile2
-rw-r--r--docker/Dockerfile.arm646
-rw-r--r--locales/ar.json67
-rw-r--r--locales/ca.json28
-rw-r--r--locales/cs.json117
-rw-r--r--locales/da.json140
-rw-r--r--locales/de.json117
-rw-r--r--locales/el.json66
-rw-r--r--locales/en-US.json77
-rw-r--r--locales/eo.json63
-rw-r--r--locales/es.json81
-rw-r--r--locales/eu.json221
-rw-r--r--locales/fa.json63
-rw-r--r--locales/fi.json81
-rw-r--r--locales/fr.json79
-rw-r--r--locales/he.json56
-rw-r--r--locales/hr.json69
-rw-r--r--locales/hu-HU.json65
-rw-r--r--locales/id.json78
-rw-r--r--locales/it.json69
-rw-r--r--locales/ja.json63
-rw-r--r--locales/ko.json63
-rw-r--r--locales/lt.json63
-rw-r--r--locales/nb-NO.json99
-rw-r--r--locales/nl.json65
-rw-r--r--locales/pl.json67
-rw-r--r--locales/pt-BR.json69
-rw-r--r--locales/pt-PT.json59
-rw-r--r--locales/pt.json69
-rw-r--r--locales/ro.json182
-rw-r--r--locales/ru.json69
-rw-r--r--locales/sk.json26
-rw-r--r--locales/sq.json69
-rw-r--r--locales/sr.json63
-rw-r--r--locales/sr_Cyrl.json63
-rw-r--r--locales/sv-SE.json61
-rw-r--r--locales/tr.json79
-rw-r--r--locales/uk.json180
-rw-r--r--locales/vi.json57
-rw-r--r--locales/zh-CN.json79
-rw-r--r--locales/zh-TW.json75
-rw-r--r--shard.lock4
-rw-r--r--shard.yml5
-rw-r--r--spec/invidious/helpers_spec.cr14
-rw-r--r--spec/invidious/search/iv_filters_spec.cr371
-rw-r--r--spec/invidious/search/query_spec.cr200
-rw-r--r--spec/invidious/search/yt_filters_spec.cr143
-rw-r--r--spec/spec_helper.cr2
-rw-r--r--src/ext/kemal_content_for.cr16
-rw-r--r--src/ext/kemal_static_file_handler.cr (renamed from src/invidious/helpers/static_file_handler.cr)0
-rw-r--r--src/invidious.cr7
-rw-r--r--src/invidious/comments.cr18
-rw-r--r--src/invidious/exceptions.cr8
-rw-r--r--src/invidious/frontend/misc.cr14
-rw-r--r--src/invidious/frontend/search_filters.cr135
-rw-r--r--src/invidious/helpers/errors.cr2
-rw-r--r--src/invidious/helpers/i18n.cr6
-rw-r--r--src/invidious/helpers/macros.cr12
-rw-r--r--src/invidious/helpers/utils.cr8
-rw-r--r--src/invidious/routes/api/manifest.cr10
-rw-r--r--src/invidious/routes/api/v1/channels.cr16
-rw-r--r--src/invidious/routes/api/v1/search.cr24
-rw-r--r--src/invidious/routes/playlists.cr18
-rw-r--r--src/invidious/routes/search.cr24
-rw-r--r--src/invidious/search.cr254
-rw-r--r--src/invidious/search/ctoken.cr32
-rw-r--r--src/invidious/search/filters.cr376
-rw-r--r--src/invidious/search/processors.cr64
-rw-r--r--src/invidious/search/query.cr151
-rw-r--r--src/invidious/user/cookies.cr6
-rw-r--r--src/invidious/videos.cr41
-rw-r--r--src/invidious/views/add_playlist_items.ecr11
-rw-r--r--src/invidious/views/components/item.ecr12
-rw-r--r--src/invidious/views/components/player.ecr18
-rw-r--r--src/invidious/views/embed.ecr3
-rw-r--r--src/invidious/views/search.ecr159
-rw-r--r--src/invidious/views/watch.ecr3
-rw-r--r--src/invidious/yt_backend/youtube_api.cr9
-rw-r--r--videojs-dependencies.yml8
97 files changed, 4147 insertions, 2008 deletions
diff --git a/.ameba.yml b/.ameba.yml
index 247705e8..96cbc8f0 100644
--- a/.ameba.yml
+++ b/.ameba.yml
@@ -77,10 +77,6 @@ Metrics/CyclomaticComplexity:
# process_video_params(query, preferences) => [20/10]
- src/invidious/videos.cr
- # produce_search_params(page, sort, ...) => [29/10]
- # process_search_query(query, page, ...) => [14/10]
- - src/invidious/search.cr
-
#src/invidious/playlists.cr:327:5
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index db0987cf..4e68b7f2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -38,9 +38,9 @@ jobs:
matrix:
stable: [true]
crystal:
- - 1.0.0
- - 1.1.1
- 1.2.2
+ - 1.3.2
+ - 1.4.0
include:
- crystal: nightly
stable: false
diff --git a/README.md b/README.md
index e79faa7e..2d7bba93 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@
 • 
<a href="https://instances.invidious.io/">Instances list</a>
&nbsp;•&nbsp;
- <a href="https://docs.invidious.io/FAQ/">FAQ</a>
+ <a href="https://docs.invidious.io/faq/">FAQ</a>
&nbsp;•&nbsp;
<a href="https://docs.invidious.io/">Documentation</a>
&nbsp;•&nbsp;
@@ -88,7 +88,7 @@
**Technical features**
- Embedded video support
-- [Developer API](https://docs.invidious.io/API/)
+- [Developer API](https://docs.invidious.io/api/)
- Does not use official YouTube APIs
- No Contributor License Agreement (CLA)
@@ -101,7 +101,7 @@
**Hosting invidious:**
-- [Follow the installation instructions](https://docs.invidious.io/Installation/)
+- [Follow the installation instructions](https://docs.invidious.io/installation/)
## Documentation
@@ -119,7 +119,7 @@ embedded youtube videos on other websites with invidious.
The documentation contains a list of browser extensions that we recommended to use along with Invidious.
-You can read more here: https://docs.invidious.io/Extensions/
+You can read more here: https://docs.invidious.io/applications/
## Contribute
@@ -150,6 +150,7 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab,
- [PeerTubeify](https://gitlab.com/Cha_deL/peertubeify): On YouTube, displays a link to the same video on PeerTube, if it exists.
- [MusicPiped](https://github.com/deep-gaurav/MusicPiped): A material design music player that streams music from YouTube.
- [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.
## Liability
diff --git a/assets/css/default.css b/assets/css/default.css
index 8b2b3578..49069c92 100644
--- a/assets/css/default.css
+++ b/assets/css/default.css
@@ -15,6 +15,11 @@ body {
background-color: rgb(255, 0, 0, 0.5);
}
+.underlined {
+ border-bottom: 1px solid;
+ margin-bottom: 20px;
+}
+
.channel-profile > * {
font-size: 1.17em;
font-weight: bold;
@@ -475,30 +480,6 @@ body.dark-theme {
}
}
-#filters {
- display: inline;
- margin-top: 15px;
-}
-
-#filters > div {
- display: inline-block;
-}
-
-#filters > summary {
- display: block;
- margin-bottom: 15px;
-}
-
-#filters > summary::before {
- content: "[ + ]";
- font-size: 1.5em;
-}
-
-#filters[open] > summary::before {
- content: "[ - ]";
- font-size: 1.5em;
-}
-
/*With commit d9528f5 all contents of the page is now within a flexbox. However,
the hr element is rendered improperly within one.
See https://stackoverflow.com/a/34372979 for more info */
diff --git a/assets/css/player.css b/assets/css/player.css
index 120fd2f8..304375b5 100644
--- a/assets/css/player.css
+++ b/assets/css/player.css
@@ -70,6 +70,9 @@
margin-bottom: 2em;
}
+.video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder, .video-js.player-style-youtube .vjs-progress-control {height: 5px;
+margin-bottom: 10px;}
+
ul.vjs-menu-content::-webkit-scrollbar {
display: none;
}
diff --git a/assets/css/search.css b/assets/css/search.css
new file mode 100644
index 00000000..5ca141d0
--- /dev/null
+++ b/assets/css/search.css
@@ -0,0 +1,118 @@
+summary {
+ /* This should hide the marker */
+ display: block;
+
+ font-size: 1.17em;
+ font-weight: bold;
+ margin: 0 auto 10px auto;
+ cursor: pointer;
+}
+
+summary::-webkit-details-marker,
+summary::marker { display: none; }
+
+summary:before {
+ border-radius: 5px;
+ content: "[ + ]";
+ margin: -2px 10px 0 10px;
+ padding: 1px 0 3px 0;
+ text-align: center;
+ width: 40px;
+}
+
+details[open] > summary:before { content: "[ − ]"; }
+
+
+#filters-box {
+ padding: 10px 20px 20px 10px;
+ margin: 10px 15px;
+}
+#filters-flex {
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: row;
+ align-items: flex-start;
+ align-content: flex-start;
+ justify-content: flex-start;
+}
+
+
+fieldset, legend {
+ display: contents !important;
+ border: none !important;
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+
+.filter-column {
+ display: inline-block;
+ display: inline-flex;
+ width: max-content;
+ min-width: max-content;
+ max-width: 16em;
+ margin: 15px;
+ flex-grow: 2;
+ flex-basis: auto;
+ flex-direction: column;
+}
+.filter-name, .filter-options {
+ display: block;
+ padding: 5px 10px;
+ margin: 0;
+ text-align: start;
+}
+
+.filter-options div { margin: 6px 0; }
+.filter-options div * { vertical-align: middle; }
+.filter-options label { margin: 0 10px; }
+
+
+#filters-apply { text-align: end; }
+
+/* Error message */
+
+.no-results-error {
+ text-align: center;
+ line-height: 180%;
+ font-size: 110%;
+ padding: 15px 15px 125px 15px;
+}
+
+/* Responsive rules */
+
+@media only screen and (max-width: 800px) {
+ summary { font-size: 1.30em; }
+ #filters-box {
+ margin: 10px 0 0 0;
+ padding: 0;
+ }
+ #filters-apply {
+ text-align: center;
+ padding: 15px;
+ }
+}
+
+/* Light theme */
+
+.light-theme #filters-box {
+ background: #dfdfdf;
+}
+
+@media (prefers-color-scheme: light) {
+ .no-theme #filters-box {
+ background: #dfdfdf;
+ }
+}
+
+/* Dark theme */
+
+.dark-theme #filters-box {
+ background: #373737;
+}
+
+@media (prefers-color-scheme: dark) {
+ .no-theme #filters-box {
+ background: #373737;
+ }
+}
diff --git a/assets/js/community.js b/assets/js/community.js
index 4077f1cd..44066a58 100644
--- a/assets/js/community.js
+++ b/assets/js/community.js
@@ -1,19 +1,20 @@
-var community_data = JSON.parse(document.getElementById('community_data').innerHTML);
+'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;
- sub_text = target.getAttribute('data-inner-text');
- inner_text = target.getAttribute('data-sub-text');
+ var sub_text = target.getAttribute('data-inner-text');
+ var inner_text = target.getAttribute('data-sub-text');
- body = target.parentNode.parentNode.children[1];
+ var body = target.parentNode.parentNode.children[1];
body.style.display = 'none';
target.innerHTML = sub_text;
@@ -25,10 +26,10 @@ function hide_youtube_replies(event) {
function show_youtube_replies(event) {
var target = event.target;
- sub_text = target.getAttribute('data-inner-text');
- inner_text = target.getAttribute('data-sub-text');
+ var sub_text = target.getAttribute('data-inner-text');
+ var inner_text = target.getAttribute('data-sub-text');
- body = target.parentNode.parentNode.children[1];
+ var body = target.parentNode.parentNode.children[1];
body.style.display = '';
target.innerHTML = sub_text;
@@ -63,8 +64,8 @@ function get_youtube_replies(target, load_more) {
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status === 200) {
if (load_more) {
body = body.parentNode.parentNode;
body.removeChild(body.lastElementChild);
@@ -92,12 +93,12 @@ function get_youtube_replies(target, load_more) {
body.innerHTML = fallback;
}
}
- }
+ };
xhr.ontimeout = function () {
- console.log('Pulling comments failed.');
+ console.warn('Pulling comments failed.');
body.innerHTML = fallback;
- }
+ };
xhr.send();
}
diff --git a/assets/js/embed.js b/assets/js/embed.js
index 9d0be0ea..7e9ac605 100644
--- a/assets/js/embed.js
+++ b/assets/js/embed.js
@@ -1,19 +1,21 @@
-var video_data = JSON.parse(document.getElementById('video_data').innerHTML);
+'use strict';
+var video_data = JSON.parse(document.getElementById('video_data').textContent);
function get_playlist(plid, retries) {
- if (retries == undefined) retries = 5;
+ if (retries === undefined) retries = 5;
if (retries <= 0) {
- console.log('Failed to pull playlist');
+ console.warn('Failed to pull playlist');
return;
}
+ var plid_url;
if (plid.startsWith('RD')) {
- var plid_url = '/api/v1/mixes/' + plid +
+ plid_url = '/api/v1/mixes/' + plid +
'?continuation=' + video_data.id +
'&format=html&hl=' + video_data.preferences.locale;
} else {
- var plid_url = '/api/v1/playlists/' + plid +
+ plid_url = '/api/v1/playlists/' + plid +
'?index=' + video_data.index +
'&continuation' + video_data.id +
'&format=html&hl=' + video_data.preferences.locale;
@@ -57,17 +59,17 @@ function get_playlist(plid, retries) {
}
}
}
- }
+ };
xhr.onerror = function () {
- console.log('Pulling playlist failed... ' + retries + '/5');
- setTimeout(function () { get_playlist(plid, retries - 1) }, 1000);
- }
+ console.warn('Pulling playlist failed... ' + retries + '/5');
+ setTimeout(function () { get_playlist(plid, retries - 1); }, 1000);
+ };
xhr.ontimeout = function () {
- console.log('Pulling playlist failed... ' + retries + '/5');
+ console.warn('Pulling playlist failed... ' + retries + '/5');
get_playlist(plid, retries - 1);
- }
+ };
xhr.send();
}
@@ -96,7 +98,7 @@ window.addEventListener('load', function (e) {
}
if (video_data.video_series.length !== 0) {
- url.searchParams.set('playlist', video_data.video_series.join(','))
+ 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 02175957..3224e668 100644
--- a/assets/js/handlers.js
+++ b/assets/js/handlers.js
@@ -76,7 +76,7 @@
});
n2a(document.querySelectorAll('[data-onrange="update_volume_value"]')).forEach(function (e) {
- var cb = function () { update_volume_value(e); }
+ var cb = function () { update_volume_value(e); };
e.oninput = cb;
e.onchange = cb;
});
@@ -102,13 +102,13 @@
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status != 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status !== 200) {
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);
@@ -131,20 +131,20 @@
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status != 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status !== 200) {
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', (event) => {
+ window.addEventListener('keydown', function (event) {
// Ignore modifier keys
if (event.ctrlKey || event.metaKey) return;
@@ -152,14 +152,14 @@
let focused_tag = document.activeElement.tagName.toLowerCase();
const allowed = /^(button|checkbox|file|radio|submit)$/;
- if (focused_tag === "textarea") return;
- if (focused_tag === "input") {
+ if (focused_tag === 'textarea') return;
+ if (focused_tag === 'input') {
let focused_type = document.activeElement.type.toLowerCase();
if (!focused_type.match(allowed)) return;
}
// Focus search bar on '/'
- if (event.key == "/") {
+ if (event.key === '/') {
document.getElementById('searchbox').focus();
event.preventDefault();
}
diff --git a/assets/js/notifications.js b/assets/js/notifications.js
index 3d1ec1ed..ec5f6dd3 100644
--- a/assets/js/notifications.js
+++ b/assets/js/notifications.js
@@ -1,9 +1,10 @@
-var notification_data = JSON.parse(document.getElementById('notification_data').innerHTML);
+'use strict';
+var notification_data = JSON.parse(document.getElementById('notification_data').textContent);
var notifications, delivered;
function get_subscriptions(callback, retries) {
- if (retries == undefined) retries = 5;
+ if (retries === undefined) retries = 5;
if (retries <= 0) {
return;
@@ -17,21 +18,21 @@ function get_subscriptions(callback, retries) {
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
- subscriptions = xhr.response;
+ var subscriptions = xhr.response;
callback(subscriptions);
}
}
- }
+ };
xhr.onerror = function () {
- console.log('Pulling subscriptions failed... ' + retries + '/5');
- setTimeout(function () { get_subscriptions(callback, retries - 1) }, 1000);
- }
+ console.warn('Pulling subscriptions failed... ' + retries + '/5');
+ setTimeout(function () { get_subscriptions(callback, retries - 1); }, 1000);
+ };
xhr.ontimeout = function () {
- console.log('Pulling subscriptions failed... ' + retries + '/5');
+ console.warn('Pulling subscriptions failed... ' + retries + '/5');
get_subscriptions(callback, retries - 1);
- }
+ };
xhr.send();
}
@@ -40,7 +41,7 @@ function create_notification_stream(subscriptions) {
notifications = new SSE(
'/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', {
withCredentials: true,
- payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId }).join(','),
+ payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId; }).join(','),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
delivered = [];
@@ -53,7 +54,7 @@ function create_notification_stream(subscriptions) {
}
var notification = JSON.parse(event.data);
- console.log('Got notification:', notification);
+ console.info('Got notification:', notification);
if (start_time < notification.published && !delivered.includes(notification.videoId)) {
if (Notification.permission === 'granted') {
@@ -67,7 +68,7 @@ function create_notification_stream(subscriptions) {
system_notification.onclick = function (event) {
window.open('/watch?v=' + event.currentTarget.tag, '_blank');
- }
+ };
}
delivered.push(notification.videoId);
@@ -82,16 +83,16 @@ function create_notification_stream(subscriptions) {
'<i class="icon ion-ios-notifications-outline"></i>';
}
}
- }
+ };
notifications.addEventListener('error', handle_notification_error);
notifications.stream();
}
function handle_notification_error(event) {
- console.log('Something went wrong with notifications, trying to reconnect...');
+ console.warn('Something went wrong with notifications, trying to reconnect...');
notifications = { close: function () { } };
- setTimeout(function () { get_subscriptions(create_notification_stream) }, 1000);
+ setTimeout(function () { get_subscriptions(create_notification_stream); }, 1000);
}
window.addEventListener('load', function (e) {
diff --git a/assets/js/player.js b/assets/js/player.js
index a1a2cd16..6ddb1158 100644
--- a/assets/js/player.js
+++ b/assets/js/player.js
@@ -1,5 +1,6 @@
-var player_data = JSON.parse(document.getElementById('player_data').innerHTML);
-var video_data = JSON.parse(document.getElementById('video_data').innerHTML);
+'use strict';
+var player_data = JSON.parse(document.getElementById('player_data').textContent);
+var video_data = JSON.parse(document.getElementById('video_data').textContent);
var options = {
preload: 'auto',
@@ -27,7 +28,7 @@ var options = {
overrideNative: true
}
}
-}
+};
if (player_data.aspect_ratio) {
options.aspectRatio = player_data.aspect_ratio;
@@ -38,7 +39,7 @@ embed_url.searchParams.delete('v');
var short_url = location.origin + '/' + video_data.id + embed_url.search;
embed_url = location.origin + '/embed/' + video_data.id + embed_url.search;
-var save_player_pos_key = "save_player_pos";
+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) {
@@ -49,6 +50,42 @@ 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...');
+
+ 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') {
+ player.reloadSourceOnError({
+ errorInterval: 10
+ });
+}
+
/**
* Function for add time argument to url
* @param {String} url
@@ -75,12 +112,12 @@ var shareOptions = {
description: player_data.description,
image: player_data.thumbnail,
get embedCode() {
- return "<iframe id='ivplayer' width='640' height='360' src='" +
- addCurrentTimeToURL(embed_url) + "' style='border:none;'></iframe>";
+ return '<iframe id="ivplayer" width="640" height="360" src="' +
+ addCurrentTimeToURL(embed_url) + '" style="border:none;"></iframe>';
}
};
-const storage = (() => {
+const storage = (function () {
try { if (localStorage.length !== -1) return localStorage; }
catch (e) { console.info('No storage available: ' + e); }
@@ -101,78 +138,57 @@ if (location.pathname.startsWith('/embed/')) {
// Detection code taken from https://stackoverflow.com/a/20293441
function isMobile() {
- try{ document.createEvent("TouchEvent"); return true; }
+ try{ document.createEvent('TouchEvent'); return true; }
catch(e){ return false; }
}
if (isMobile()) {
player.mobileUi();
- buttons = ["playToggle", "volumePanel", "captionsButton"];
+ var buttons = ['playToggle', 'volumePanel', 'captionsButton'];
- if (video_data.params.quality !== 'dash') buttons.push("qualitySelector")
+ if (video_data.params.quality !== 'dash') buttons.push('qualitySelector');
// Create new control bar object for operation buttons
- const ControlBar = videojs.getComponent("controlBar");
+ const ControlBar = videojs.getComponent('controlBar');
let operations_bar = new ControlBar(player, {
children: [],
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0]
});
- buttons.slice(1).forEach(child => operations_bar.addChild(child))
+ buttons.slice(1).forEach(function (child) {operations_bar.addChild(child);});
// Remove operation buttons from primary control bar
- primary_control_bar = player.getChild("controlBar");
- buttons.forEach(child => primary_control_bar.removeChild(child));
+ var primary_control_bar = player.getChild('controlBar');
+ buttons.forEach(function (child) {primary_control_bar.removeChild(child);});
- operations_bar_element = operations_bar.el();
- operations_bar_element.className += " mobile-operations-bar"
- player.addChild(operations_bar)
+ var operations_bar_element = operations_bar.el();
+ operations_bar_element.className += ' mobile-operations-bar';
+ player.addChild(operations_bar);
// Playback menu doesn't work when it's initialized outside of the primary control bar
- playback_element = document.getElementsByClassName("vjs-playback-rate")[0]
- operations_bar_element.append(playback_element)
+ var playback_element = document.getElementsByClassName('vjs-playback-rate')[0];
+ operations_bar_element.append(playback_element);
// The share and http source selector element can't be fetched till the players ready.
- player.one("playing", () => {
- share_element = document.getElementsByClassName("vjs-share-control")[0]
- operations_bar_element.append(share_element)
-
- if (video_data.params.quality === 'dash') {
- http_source_selector = document.getElementsByClassName("vjs-http-source-selector vjs-menu-button")[0]
- operations_bar_element.append(http_source_selector)
- }
- })
-}
+ player.one('playing', function () {
+ var share_element = document.getElementsByClassName('vjs-share-control')[0];
+ operations_bar_element.append(share_element);
-player.on('error', function (event) {
- if (player.error().code === 2 || player.error().code === 4) {
- 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();
- }, 5000);
- }
-});
+ 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);
+ }
+ });
+}
// Enable VR video support
if (!video_data.params.listen && video_data.vr && video_data.params.vr_mode) {
- player.crossOrigin("anonymous")
+ player.crossOrigin('anonymous');
switch (video_data.projection_type) {
- case "EQUIRECTANGULAR":
- player.vr({projection: "equirectangular"});
- default: // Should only be "MESH" but we'll use this as a fallback.
- player.vr({projection: "EAC"});
+ case 'EQUIRECTANGULAR':
+ player.vr({projection: 'equirectangular'});
+ default: // Should only be 'MESH' but we'll use this as a fallback.
+ player.vr({projection: 'EAC'});
}
}
@@ -200,9 +216,71 @@ if (video_data.params.video_start > 0 || video_data.params.video_end > 0) {
player.volume(video_data.params.volume / 100);
player.playbackRate(video_data.params.speed);
+/**
+ * Method for getting the contents of a cookie
+ *
+ * @param {String} name Name of cookie
+ * @returns 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;
+}
+
+/**
+ * Method for updating the 'PREFS' cookie (or creating it if missing)
+ *
+ * @param {number} newVolume New volume defined (null if unchanged)
+ * @param {number} newSpeed New speed defined (null if unchanged)
+ */
+function updateCookie(newVolume, newSpeed) {
+ var volumeValue = newVolume !== null ? newVolume : video_data.params.volume;
+ var speedValue = newSpeed !== null ? newSpeed : video_data.params.speed;
+
+ var cookieValue = getCookieValue('PREFS');
+ var cookieData;
+
+ if (cookieValue !== null) {
+ var cookieJson = JSON.parse(decodeURIComponent(cookieValue));
+ cookieJson.volume = volumeValue;
+ cookieJson.speed = speedValue;
+ cookieData = encodeURIComponent(JSON.stringify(cookieJson));
+ } else {
+ cookieData = encodeURIComponent(JSON.stringify({ 'volume': volumeValue, 'speed': speedValue }));
+ }
+
+ // Set expiration in 2 year
+ var date = new Date();
+ 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;
+
+ // 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;
+
+ document.cookie = 'PREFS=' + cookieData + '; SameSite=Strict; path=/; domain=' +
+ domainUsed + '; expires=' + date.toGMTString() + ';';
+
+ video_data.params.volume = volumeValue;
+ video_data.params.speed = speedValue;
+}
+
+player.on('ratechange', function () {
+ updateCookie(null, player.playbackRate());
+});
+
+player.on('volumechange', function () {
+ updateCookie(Math.ceil(player.volume() * 100), null);
+});
+
player.on('waiting', function () {
if (player.playbackRate() > 1 && player.liveTracker.isLive() && player.liveTracker.atLiveEdge()) {
- console.log('Player has caught up to source, resetting playbackRate.')
+ console.info('Player has caught up to source, resetting playbackRate.');
player.playbackRate(1);
}
});
@@ -213,13 +291,13 @@ 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 hasTimeParam = url.searchParams.has('t');
const remeberedTime = get_video_time();
let lastUpdated = 0;
if(!hasTimeParam) set_seconds_after_start(remeberedTime);
- const updateTime = () => {
+ const updateTime = function () {
const raw = player.currentTime();
const time = Math.floor(raw);
@@ -229,7 +307,7 @@ if (video_data.params.save_player_pos) {
}
};
- player.on("timeupdate", updateTime);
+ player.on('timeupdate', updateTime);
}
else remove_all_video_times();
@@ -239,13 +317,13 @@ if (video_data.params.autoplay) {
player.ready(function () {
new Promise(function (resolve, reject) {
- setTimeout(() => resolve(1), 1);
+ setTimeout(function () {resolve(1);}, 1);
}).then(function (result) {
var promise = player.play();
if (promise !== undefined) {
- promise.then(_ => {
- }).catch(error => {
+ promise.then(function () {
+ }).catch(function (error) {
bpb.show();
});
}
@@ -256,16 +334,16 @@ if (video_data.params.autoplay) {
if (!video_data.params.listen && video_data.params.quality === 'dash') {
player.httpSourceSelector();
- if (video_data.params.quality_dash != "auto") {
- player.ready(() => {
- player.on("loadedmetadata", () => {
- const qualityLevels = Array.from(player.qualityLevels()).sort((a, b) => a.height - b.height);
+ if (video_data.params.quality_dash !== 'auto') {
+ player.ready(function () {
+ player.on('loadedmetadata', function () {
+ const qualityLevels = Array.from(player.qualityLevels()).sort(function (a, b) {return a.height - b.height;});
let targetQualityLevel;
switch (video_data.params.quality_dash) {
- case "best":
+ case 'best':
targetQualityLevel = qualityLevels.length - 1;
break;
- case "worst":
+ case 'worst':
targetQualityLevel = 0;
break;
default:
@@ -279,7 +357,7 @@ if (!video_data.params.listen && video_data.params.quality === 'dash') {
}
}
for (let i = 0; i < qualityLevels.length; i++) {
- qualityLevels[i].enabled = (i == targetQualityLevel);
+ qualityLevels[i].enabled = (i === targetQualityLevel);
}
});
});
@@ -313,10 +391,12 @@ if (!video_data.params.listen && video_data.params.annotations) {
}
}
}
- }
+ };
- window.addEventListener('__ar_annotation_click', e => {
- const { url, target, seconds } = e.detail;
+ window.addEventListener('__ar_annotation_click', function (e) {
+ const url = e.detail.url,
+ target = e.detail.target,
+ seconds = e.detail.seconds;
var path = new URL(url);
if (path.href.startsWith('https://www.youtube.com/watch?') && seconds) {
@@ -386,7 +466,7 @@ function get_video_time() {
return timestamp || 0;
}
- catch {
+ catch (e) {
return 0;
}
}
@@ -397,7 +477,7 @@ function set_all_video_times(times) {
try {
storage.setItem(save_player_pos_key, JSON.stringify(times));
} catch (e) {
- console.debug('set_all_video_times: ' + e);
+ console.warn('set_all_video_times: ' + e);
}
} else {
storage.removeItem(save_player_pos_key);
@@ -412,7 +492,7 @@ function get_all_video_times() {
try {
return JSON.parse(raw);
} catch (e) {
- console.debug('get_all_video_times: ' + e);
+ console.warn('get_all_video_times: ' + e);
}
}
}
@@ -506,7 +586,7 @@ function increase_playback_rate(steps) {
player.playbackRate(options.playbackRates[newIndex]);
}
-window.addEventListener('keydown', e => {
+window.addEventListener('keydown', function (e) {
if (e.target.tagName.toLowerCase() === 'input') {
// Ignore input when focus is on certain elements, e.g. form fields.
return;
@@ -625,7 +705,7 @@ window.addEventListener('keydown', e => {
var volumeHover = false;
var volumeSelector = pEl.querySelector('.vjs-volume-menu-button') || pEl.querySelector('.vjs-volume-panel');
- if (volumeSelector != null) {
+ if (volumeSelector !== null) {
volumeSelector.onmouseover = function () { volumeHover = true; };
volumeSelector.onmouseout = function () { volumeHover = false; };
}
@@ -645,9 +725,9 @@ window.addEventListener('keydown', e => {
var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail)));
event.preventDefault();
- if (delta == 1) {
+ if (delta === 1) {
increase_volume(volumeStep);
- } else if (delta == -1) {
+ } else if (delta === -1) {
increase_volume(-volumeStep);
}
}
@@ -656,7 +736,7 @@ window.addEventListener('keydown', e => {
};
player.on('mousewheel', mouseScroll);
- player.on("DOMMouseScroll", mouseScroll);
+ player.on('DOMMouseScroll', mouseScroll);
}());
// Since videojs-share can sometimes be blocked, we defer it until last
@@ -666,35 +746,35 @@ if (player.share) {
// show the preferred caption by default
if (player_data.preferred_caption_found) {
- player.ready(() => {
+ player.ready(function () {
player.textTracks()[1].mode = 'showing';
});
}
// Safari audio double duration fix
-if (navigator.vendor == "Apple Computer, Inc." && video_data.params.listen) {
+if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) {
player.on('loadedmetadata', function () {
player.on('timeupdate', function () {
- if (player.remainingTime() < player.duration() / 2) {
- player.currentTime(player.duration() + 1);
+ if (player.remainingTime() < player.duration() / 2 && player.remainingTime() >= 2) {
+ player.currentTime(player.duration() - 1);
}
});
});
}
// Watch on Invidious link
-if (window.location.pathname.startsWith("/embed/")) {
+if (window.location.pathname.startsWith('/embed/')) {
const Button = videojs.getComponent('Button');
let watch_on_invidious_button = new Button(player);
// Create hyperlink for current instance
- redirect_element = document.createElement("a");
- redirect_element.setAttribute("href", `http://${window.location.host}/watch?v=${window.location.pathname.replace("/embed/","")}`)
- redirect_element.appendChild(document.createTextNode("Invidious"))
+ var redirect_element = document.createElement('a');
+ redirect_element.setAttribute('href', location.pathname.replace('/embed/', '/watch?v='));
+ redirect_element.appendChild(document.createTextNode('Invidious'));
- watch_on_invidious_button.el().appendChild(redirect_element)
- watch_on_invidious_button.addClass("watch-on-invidious")
+ watch_on_invidious_button.el().appendChild(redirect_element);
+ watch_on_invidious_button.addClass('watch-on-invidious');
- cb = player.getChild('ControlBar')
- cb.addChild(watch_on_invidious_button)
-};
+ var cb = player.getChild('ControlBar');
+ cb.addChild(watch_on_invidious_button);
+}
diff --git a/assets/js/playlist_widget.js b/assets/js/playlist_widget.js
index 0ec27859..c2565874 100644
--- a/assets/js/playlist_widget.js
+++ b/assets/js/playlist_widget.js
@@ -1,4 +1,5 @@
-var playlist_data = JSON.parse(document.getElementById('playlist_data').innerHTML);
+'use strict';
+var playlist_data = JSON.parse(document.getElementById('playlist_data').textContent);
function add_playlist_video(target) {
var select = target.parentNode.children[0].children[1];
@@ -14,12 +15,12 @@ function add_playlist_video(target) {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status === 200) {
option.innerText = '✓' + option.innerText;
}
}
- }
+ };
xhr.send('csrf_token=' + playlist_data.csrf_token);
}
@@ -38,12 +39,12 @@ function add_playlist_item(target) {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status != 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status !== 200) {
tile.style.display = '';
}
}
- }
+ };
xhr.send('csrf_token=' + playlist_data.csrf_token);
}
@@ -62,12 +63,12 @@ function remove_playlist_item(target) {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status != 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status !== 200) {
tile.style.display = '';
}
}
- }
+ };
xhr.send('csrf_token=' + playlist_data.csrf_token);
-} \ No newline at end of file
+}
diff --git a/assets/js/silvermine-videojs-quality-selector.min.js b/assets/js/silvermine-videojs-quality-selector.min.js
index 88621e8d..1877047d 100644
--- a/assets/js/silvermine-videojs-quality-selector.min.js
+++ b/assets/js/silvermine-videojs-quality-selector.min.js
@@ -1,4 +1,4 @@
-/*! @silvermine/videojs-quality-selector 2020-03-02 v1.1.2-36-g64d620a-dirty */
+/*! @silvermine/videojs-quality-selector 2022-04-13 v1.1.2-43-gaa06e72-dirty */
-!function u(o,c,a){function l(e,n){if(!c[e]){if(!o[e]){var t="function"==typeof require&&require;if(!n&&t)return t(e,!0);if(s)return s(e,!0);var r=new Error("Cannot find module '"+e+"'");throw r.code="MODULE_NOT_FOUND",r}var i=c[e]={exports:{}};o[e][0].call(i.exports,function(n){return l(o[e][1][n]||n)},i,i.exports,u,o,c,a)}return c[e].exports}for(var s="function"==typeof require&&require,n=0;n<a.length;n++)l(a[n]);return l}({1:[function(n,e,t){!function(){var u=!1,o=/xyz/.test(function(){xyz})?/\b_super\b/:/.*/;this.Class=function(){},Class.extend=function(n){var i=this.prototype;u=!0;var e=new this;for(var t in u=!1,n)e[t]="function"==typeof n[t]&&"function"==typeof i[t]&&o.test(n[t])?function(t,r){return function(){var n=this._super;this._super=i[t];var e=r.apply(this,arguments);return this._super=n,e}}(t,n[t]):n[t];function r(){!u&&this.init&&this.init.apply(this,arguments)}return((r.prototype=e).constructor=r).extend=arguments.callee,r},e.exports=Class}()},{}],2:[function(n,J,$){(function(V){!function(){function t(){}var n="object"==typeof self&&self.self===self&&self||"object"==typeof V&&V.global===V&&V||this||{},e=n._,r=Array.prototype,o=Object.prototype,f="undefined"!=typeof Symbol?Symbol.prototype:null,i=r.push,a=r.slice,p=o.toString,u=o.hasOwnProperty,c=Array.isArray,l=Object.keys,s=Object.create,h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};void 0===$||$.nodeType?n._=h:(void 0!==J&&!J.nodeType&&J.exports&&($=J.exports=h),$._=h),h.VERSION="1.9.1";function d(i,u,n){if(void 0===u)return i;switch(null==n?3:n){case 1:return function(n){return i.call(u,n)};case 3:return function(n,e,t){return i.call(u,n,e,t)};case 4:return function(n,e,t,r){return i.call(u,n,e,t,r)}}return function(){return i.apply(u,arguments)}}function v(n,e,t){return h.iteratee!==y?h.iteratee(n,e):null==n?h.identity:h.isFunction(n)?d(n,e,t):h.isObject(n)&&!h.isArray(n)?h.matcher(n):h.property(n)}var y;h.iteratee=y=function(n,e){return v(n,e,1/0)};function g(i,u){return u=null==u?i.length-1:+u,function(){for(var n=Math.max(arguments.length-u,0),e=Array(n),t=0;t<n;t++)e[t]=arguments[t+u];switch(u){case 0:return i.call(this,e);case 1:return i.call(this,arguments[0],e);case 2:return i.call(this,arguments[0],arguments[1],e)}var r=Array(u+1);for(t=0;t<u;t++)r[t]=arguments[t];return r[u]=e,i.apply(this,r)}}function S(n){if(!h.isObject(n))return{};if(s)return s(n);t.prototype=n;var e=new t;return t.prototype=null,e}function m(e){return function(n){return null==n?void 0:n[e]}}function _(n,e){return null!=n&&u.call(n,e)}function b(n,e){for(var t=e.length,r=0;r<t;r++){if(null==n)return;n=n[e[r]]}return t?n:void 0}function x(n){var e=j(n);return"number"==typeof e&&0<=e&&e<=k}var k=Math.pow(2,53)-1,j=m("length");h.each=h.forEach=function(n,e,t){var r,i;if(e=d(e,t),x(n))for(r=0,i=n.length;r<i;r++)e(n[r],r,n);else{var u=h.keys(n);for(r=0,i=u.length;r<i;r++)e(n[u[r]],u[r],n)}return n},h.map=h.collect=function(n,e,t){e=v(e,t);for(var r=!x(n)&&h.keys(n),i=(r||n).length,u=Array(i),o=0;o<i;o++){var c=r?r[o]:o;u[o]=e(n[c],c,n)}return u};function E(a){return function(n,e,t,r){var i=3<=arguments.length;return function(n,e,t,r){var i=!x(n)&&h.keys(n),u=(i||n).length,o=0<a?0:u-1;for(r||(t=n[i?i[o]:o],o+=a);0<=o&&o<u;o+=a){var c=i?i[o]:o;t=e(t,n[c],c,n)}return t}(n,d(e,r,4),t,i)}}h.reduce=h.foldl=h.inject=E(1),h.reduceRight=h.foldr=E(-1),h.find=h.detect=function(n,e,t){var r=(x(n)?h.findIndex:h.findKey)(n,e,t);if(void 0!==r&&-1!==r)return n[r]},h.filter=h.select=function(n,r,e){var i=[];return r=v(r,e),h.each(n,function(n,e,t){r(n,e,t)&&i.push(n)}),i},h.reject=function(n,e,t){return h.filter(n,h.negate(v(e)),t)},h.every=h.all=function(n,e,t){e=v(e,t);for(var r=!x(n)&&h.keys(n),i=(r||n).length,u=0;u<i;u++){var o=r?r[u]:u;if(!e(n[o],o,n))return!1}return!0},h.some=h.any=function(n,e,t){e=v(e,t);for(var r=!x(n)&&h.keys(n),i=(r||n).length,u=0;u<i;u++){var o=r?r[u]:u;if(e(n[o],o,n))return!0}return!1},h.contains=h.includes=h.include=function(n,e,t,r){return x(n)||(n=h.values(n)),"number"==typeof t&&!r||(t=0),0<=h.indexOf(n,e,t)},h.invoke=g(function(n,t,r){var i,u;return h.isFunction(t)?u=t:h.isArray(t)&&(i=t.slice(0,-1),t=t[t.length-1]),h.map(n,function(n){var e=u;if(!e){if(i&&i.length&&(n=b(n,i)),null==n)return;e=n[t]}return null==e?e:e.apply(n,r)})}),h.pluck=function(n,e){return h.map(n,h.property(e))},h.where=function(n,e){return h.filter(n,h.matcher(e))},h.findWhere=function(n,e){return h.find(n,h.matcher(e))},h.max=function(n,r,e){var t,i,u=-1/0,o=-1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var c=0,a=(n=x(n)?n:h.values(n)).length;c<a;c++)null!=(t=n[c])&&u<t&&(u=t);else r=v(r,e),h.each(n,function(n,e,t){i=r(n,e,t),(o<i||i===-1/0&&u===-1/0)&&(u=n,o=i)});return u},h.min=function(n,r,e){var t,i,u=1/0,o=1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var c=0,a=(n=x(n)?n:h.values(n)).length;c<a;c++)null!=(t=n[c])&&t<u&&(u=t);else r=v(r,e),h.each(n,function(n,e,t){((i=r(n,e,t))<o||i===1/0&&u===1/0)&&(u=n,o=i)});return u},h.shuffle=function(n){return h.sample(n,1/0)},h.sample=function(n,e,t){if(null==e||t)return x(n)||(n=h.values(n)),n[h.random(n.length-1)];var r=x(n)?h.clone(n):h.values(n),i=j(r);e=Math.max(Math.min(e,i),0);for(var u=i-1,o=0;o<e;o++){var c=h.random(o,u),a=r[o];r[o]=r[c],r[c]=a}return r.slice(0,e)},h.sortBy=function(n,r,e){var i=0;return r=v(r,e),h.pluck(h.map(n,function(n,e,t){return{value:n,index:i++,criteria:r(n,e,t)}}).sort(function(n,e){var t=n.criteria,r=e.criteria;if(t!==r){if(r<t||void 0===t)return 1;if(t<r||void 0===r)return-1}return n.index-e.index}),"value")};function w(o,e){return function(r,i,n){var u=e?[[],[]]:{};return i=v(i,n),h.each(r,function(n,e){var t=i(n,e,r);o(u,n,t)}),u}}h.groupBy=w(function(n,e,t){_(n,t)?n[t].push(e):n[t]=[e]}),h.indexBy=w(function(n,e,t){n[t]=e}),h.countBy=w(function(n,e,t){_(n,t)?n[t]++:n[t]=1});var A=/[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;h.toArray=function(n){return n?h.isArray(n)?a.call(n):h.isString(n)?n.match(A):x(n)?h.map(n,h.identity):h.values(n):[]},h.size=function(n){return null==n?0:x(n)?n.length:h.keys(n).length},h.partition=w(function(n,e,t){n[t?0:1].push(e)},!0),h.first=h.head=h.take=function(n,e,t){return null==n||n.length<1?null==e?void 0:[]:null==e||t?n[0]:h.initial(n,n.length-e)},h.initial=function(n,e,t){return a.call(n,0,Math.max(0,n.length-(null==e||t?1:e)))},h.last=function(n,e,t){return null==n||n.length<1?null==e?void 0:[]:null==e||t?n[n.length-1]:h.rest(n,Math.max(0,n.length-e))},h.rest=h.tail=h.drop=function(n,e,t){return a.call(n,null==e||t?1:e)},h.compact=function(n){return h.filter(n,Boolean)};var T=function(n,e,t,r){for(var i=(r=r||[]).length,u=0,o=j(n);u<o;u++){var c=n[u];if(x(c)&&(h.isArray(c)||h.isArguments(c)))if(e)for(var a=0,l=c.length;a<l;)r[i++]=c[a++];else T(c,e,t,r),i=r.length;else t||(r[i++]=c)}return r};h.flatten=function(n,e){return T(n,e,!1)},h.without=g(function(n,e){return h.difference(n,e)}),h.uniq=h.unique=function(n,e,t,r){h.isBoolean(e)||(r=t,t=e,e=!1),null!=t&&(t=v(t,r));for(var i=[],u=[],o=0,c=j(n);o<c;o++){var a=n[o],l=t?t(a,o,n):a;e&&!t?(o&&u===l||i.push(a),u=l):t?h.contains(u,l)||(u.push(l),i.push(a)):h.contains(i,a)||i.push(a)}return i},h.union=g(function(n){return h.uniq(T(n,!0,!0))}),h.intersection=function(n){for(var e=[],t=arguments.length,r=0,i=j(n);r<i;r++){var u=n[r];if(!h.contains(e,u)){var o;for(o=1;o<t&&h.contains(arguments[o],u);o++);o===t&&e.push(u)}}return e},h.difference=g(function(n,e){return e=T(e,!0,!0),h.filter(n,function(n){return!h.contains(e,n)})}),h.unzip=function(n){for(var e=n&&h.max(n,j).length||0,t=Array(e),r=0;r<e;r++)t[r]=h.pluck(n,r);return t},h.zip=g(h.unzip),h.object=function(n,e){for(var t={},r=0,i=j(n);r<i;r++)e?t[n[r]]=e[r]:t[n[r][0]]=n[r][1];return t};function O(u){return function(n,e,t){e=v(e,t);for(var r=j(n),i=0<u?0:r-1;0<=i&&i<r;i+=u)if(e(n[i],i,n))return i;return-1}}h.findIndex=O(1),h.findLastIndex=O(-1),h.sortedIndex=function(n,e,t,r){for(var i=(t=v(t,r,1))(e),u=0,o=j(n);u<o;){var c=Math.floor((u+o)/2);t(n[c])<i?u=c+1:o=c}return u};function C(u,o,c){return function(n,e,t){var r=0,i=j(n);if("number"==typeof t)0<u?r=0<=t?t:Math.max(t+i,r):i=0<=t?Math.min(t+1,i):t+i+1;else if(c&&t&&i)return n[t=c(n,e)]===e?t:-1;if(e!=e)return 0<=(t=o(a.call(n,r,i),h.isNaN))?t+r:-1;for(t=0<u?r:i-1;0<=t&&t<i;t+=u)if(n[t]===e)return t;return-1}}h.indexOf=C(1,h.findIndex,h.sortedIndex),h.lastIndexOf=C(-1,h.findLastIndex),h.range=function(n,e,t){null==e&&(e=n||0,n=0),t=t||(e<n?-1:1);for(var r=Math.max(Math.ceil((e-n)/t),0),i=Array(r),u=0;u<r;u++,n+=t)i[u]=n;return i},h.chunk=function(n,e){if(null==e||e<1)return[];for(var t=[],r=0,i=n.length;r<i;)t.push(a.call(n,r,r+=e));return t};function I(n,e,t,r,i){if(!(r instanceof e))return n.apply(t,i);var u=S(n.prototype),o=n.apply(u,i);return h.isObject(o)?o:u}h.bind=g(function(e,t,r){if(!h.isFunction(e))throw new TypeError("Bind must be called on a function");var i=g(function(n){return I(e,i,t,this,r.concat(n))});return i}),h.partial=g(function(i,u){var o=h.partial.placeholder,c=function(){for(var n=0,e=u.length,t=Array(e),r=0;r<e;r++)t[r]=u[r]===o?arguments[n++]:u[r];for(;n<arguments.length;)t.push(arguments[n++]);return I(i,c,this,this,t)};return c}),(h.partial.placeholder=h).bindAll=g(function(n,e){var t=(e=T(e,!1,!1)).length;if(t<1)throw new Error("bindAll must be passed function names");for(;t--;){var r=e[t];n[r]=h.bind(n[r],n)}}),h.memoize=function(r,i){var u=function(n){var e=u.cache,t=""+(i?i.apply(this,arguments):n);return _(e,t)||(e[t]=r.apply(this,arguments)),e[t]};return u.cache={},u},h.delay=g(function(n,e,t){return setTimeout(function(){return n.apply(null,t)},e)}),h.defer=h.partial(h.delay,h,1),h.throttle=function(t,r,i){var u,o,c,a,l=0;i=i||{};function s(){l=!1===i.leading?0:h.now(),u=null,a=t.apply(o,c),u||(o=c=null)}function n(){var n=h.now();l||!1!==i.leading||(l=n);var e=r-(n-l);return o=this,c=arguments,e<=0||r<e?(u&&(clearTimeout(u),u=null),l=n,a=t.apply(o,c),u||(o=c=null)):u||!1===i.trailing||(u=setTimeout(s,e)),a}return n.cancel=function(){clearTimeout(u),l=0,u=o=c=null},n},h.debounce=function(t,r,i){function u(n,e){o=null,e&&(c=t.apply(n,e))}var o,c,n=g(function(n){if(o&&clearTimeout(o),i){var e=!o;o=setTimeout(u,r),e&&(c=t.apply(this,n))}else o=h.delay(u,r,this,n);return c});return n.cancel=function(){clearTimeout(o),o=null},n},h.wrap=function(n,e){return h.partial(e,n)},h.negate=function(n){return function(){return!n.apply(this,arguments)}},h.compose=function(){var t=arguments,r=t.length-1;return function(){for(var n=r,e=t[r].apply(this,arguments);n--;)e=t[n].call(this,e);return e}},h.after=function(n,e){return function(){if(--n<1)return e.apply(this,arguments)}},h.before=function(n,e){var t;return function(){return 0<--n&&(t=e.apply(this,arguments)),n<=1&&(e=null),t}},h.once=h.partial(h.before,2),h.restArguments=g;function F(n,e){var t=M.length,r=n.constructor,i=h.isFunction(r)&&r.prototype||o,u="constructor";for(_(n,u)&&!h.contains(e,u)&&e.push(u);t--;)(u=M[t])in n&&n[u]!==i[u]&&!h.contains(e,u)&&e.push(u)}var q=!{toString:null}.propertyIsEnumerable("toString"),M=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];h.keys=function(n){if(!h.isObject(n))return[];if(l)return l(n);var e=[];for(var t in n)_(n,t)&&e.push(t);return q&&F(n,e),e},h.allKeys=function(n){if(!h.isObject(n))return[];var e=[];for(var t in n)e.push(t);return q&&F(n,e),e},h.values=function(n){for(var e=h.keys(n),t=e.length,r=Array(t),i=0;i<t;i++)r[i]=n[e[i]];return r},h.mapObject=function(n,e,t){e=v(e,t);for(var r=h.keys(n),i=r.length,u={},o=0;o<i;o++){var c=r[o];u[c]=e(n[c],c,n)}return u},h.pairs=function(n){for(var e=h.keys(n),t=e.length,r=Array(t),i=0;i<t;i++)r[i]=[e[i],n[e[i]]];return r},h.invert=function(n){for(var e={},t=h.keys(n),r=0,i=t.length;r<i;r++)e[n[t[r]]]=t[r];return e},h.functions=h.methods=function(n){var e=[];for(var t in n)h.isFunction(n[t])&&e.push(t);return e.sort()};function N(a,l){return function(n){var e=arguments.length;if(l&&(n=Object(n)),e<2||null==n)return n;for(var t=1;t<e;t++)for(var r=arguments[t],i=a(r),u=i.length,o=0;o<u;o++){var c=i[o];l&&void 0!==n[c]||(n[c]=r[c])}return n}}h.extend=N(h.allKeys),h.extendOwn=h.assign=N(h.keys),h.findKey=function(n,e,t){e=v(e,t);for(var r,i=h.keys(n),u=0,o=i.length;u<o;u++)if(e(n[r=i[u]],r,n))return r};function R(n,e,t){return e in t}var Q,L;h.pick=g(function(n,e){var t={},r=e[0];if(null==n)return t;h.isFunction(r)?(1<e.length&&(r=d(r,e[1])),e=h.allKeys(n)):(r=R,e=T(e,!1,!1),n=Object(n));for(var i=0,u=e.length;i<u;i++){var o=e[i],c=n[o];r(c,o,n)&&(t[o]=c)}return t}),h.omit=g(function(n,t){var e,r=t[0];return h.isFunction(r)?(r=h.negate(r),1<t.length&&(e=t[1])):(t=h.map(T(t,!1,!1),String),r=function(n,e){return!h.contains(t,e)}),h.pick(n,r,e)}),h.defaults=N(h.allKeys,!0),h.create=function(n,e){var t=S(n);return e&&h.extendOwn(t,e),t},h.clone=function(n){return h.isObject(n)?h.isArray(n)?n.slice():h.extend({},n):n},h.tap=function(n,e){return e(n),n},h.isMatch=function(n,e){var t=h.keys(e),r=t.length;if(null==n)return!r;for(var i=Object(n),u=0;u<r;u++){var o=t[u];if(e[o]!==i[o]||!(o in i))return!1}return!0},Q=function(n,e,t,r){if(n===e)return 0!==n||1/n==1/e;if(null==n||null==e)return!1;if(n!=n)return e!=e;var i=typeof n;return("function"==i||"object"==i||"object"==typeof e)&&L(n,e,t,r)},L=function(n,e,t,r){n instanceof h&&(n=n._wrapped),e instanceof h&&(e=e._wrapped);var i=p.call(n);if(i!==p.call(e))return!1;switch(i){case"[object RegExp]":case"[object String]":return""+n==""+e;case"[object Number]":return+n!=+n?+e!=+e:0==+n?1/+n==1/e:+n==+e;case"[object Date]":case"[object Boolean]":return+n==+e;case"[object Symbol]":return f.valueOf.call(n)===f.valueOf.call(e)}var u="[object Array]"===i;if(!u){if("object"!=typeof n||"object"!=typeof e)return!1;var o=n.constructor,c=e.constructor;if(o!==c&&!(h.isFunction(o)&&o instanceof o&&h.isFunction(c)&&c instanceof c)&&"constructor"in n&&"constructor"in e)return!1}r=r||[];for(var a=(t=t||[]).length;a--;)if(t[a]===n)return r[a]===e;if(t.push(n),r.push(e),u){if((a=n.length)!==e.length)return!1;for(;a--;)if(!Q(n[a],e[a],t,r))return!1}else{var l,s=h.keys(n);if(a=s.length,h.keys(e).length!==a)return!1;for(;a--;)if(l=s[a],!_(e,l)||!Q(n[l],e[l],t,r))return!1}return t.pop(),r.pop(),!0},h.isEqual=function(n,e){return Q(n,e)},h.isEmpty=function(n){return null==n||(x(n)&&(h.isArray(n)||h.isString(n)||h.isArguments(n))?0===n.length:0===h.keys(n).length)},h.isElement=function(n){return!(!n||1!==n.nodeType)},h.isArray=c||function(n){return"[object Array]"===p.call(n)},h.isObject=function(n){var e=typeof n;return"function"==e||"object"==e&&!!n},h.each(["Arguments","Function","String","Number","Date","RegExp","Error","Symbol","Map","WeakMap","Set","WeakSet"],function(e){h["is"+e]=function(n){return p.call(n)==="[object "+e+"]"}}),h.isArguments(arguments)||(h.isArguments=function(n){return _(n,"callee")});var U=n.document&&n.document.childNodes;"function"!=typeof/./&&"object"!=typeof Int8Array&&"function"!=typeof U&&(h.isFunction=function(n){return"function"==typeof n||!1}),h.isFinite=function(n){return!h.isSymbol(n)&&isFinite(n)&&!isNaN(parseFloat(n))},h.isNaN=function(n){return h.isNumber(n)&&isNaN(n)},h.isBoolean=function(n){return!0===n||!1===n||"[object Boolean]"===p.call(n)},h.isNull=function(n){return null===n},h.isUndefined=function(n){return void 0===n},h.has=function(n,e){if(!h.isArray(e))return _(n,e);for(var t=e.length,r=0;r<t;r++){var i=e[r];if(null==n||!u.call(n,i))return!1;n=n[i]}return!!t},h.noConflict=function(){return n._=e,this},h.identity=function(n){return n},h.constant=function(n){return function(){return n}},h.noop=function(){},h.property=function(e){return h.isArray(e)?function(n){return b(n,e)}:m(e)},h.propertyOf=function(e){return null==e?function(){}:function(n){return h.isArray(n)?b(e,n):e[n]}},h.matcher=h.matches=function(e){return e=h.extendOwn({},e),function(n){return h.isMatch(n,e)}},h.times=function(n,e,t){var r=Array(Math.max(0,n));e=d(e,t,1);for(var i=0;i<n;i++)r[i]=e(i);return r},h.random=function(n,e){return null==e&&(e=n,n=0),n+Math.floor(Math.random()*(e-n+1))},h.now=Date.now||function(){return(new Date).getTime()};function D(e){function t(n){return e[n]}var n="(?:"+h.keys(e).join("|")+")",r=RegExp(n),i=RegExp(n,"g");return function(n){return n=null==n?"":""+n,r.test(n)?n.replace(i,t):n}}var P={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},W=h.invert(P);h.escape=D(P),h.unescape=D(W),h.result=function(n,e,t){h.isArray(e)||(e=[e]);var r=e.length;if(!r)return h.isFunction(t)?t.call(n):t;for(var i=0;i<r;i++){var u=null==n?void 0:n[e[i]];void 0===u&&(u=t,i=r),n=h.isFunction(u)?u.call(n):u}return n};var B=0;h.uniqueId=function(n){var e=++B+"";return n?n+e:e},h.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};function Y(n){return"\\"+K[n]}var z=/(.)^/,K={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},G=/\\|'|\r|\n|\u2028|\u2029/g;h.template=function(u,n,e){!n&&e&&(n=e),n=h.defaults({},n,h.templateSettings);var t,r=RegExp([(n.escape||z).source,(n.interpolate||z).source,(n.evaluate||z).source].join("|")+"|$","g"),o=0,c="__p+='";u.replace(r,function(n,e,t,r,i){return c+=u.slice(o,i).replace(G,Y),o=i+n.length,e?c+="'+\n((__t=("+e+"))==null?'':_.escape(__t))+\n'":t?c+="'+\n((__t=("+t+"))==null?'':__t)+\n'":r&&(c+="';\n"+r+"\n__p+='"),n}),c+="';\n",n.variable||(c="with(obj||{}){\n"+c+"}\n"),c="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+c+"return __p;\n";try{t=new Function(n.variable||"obj","_",c)}catch(n){throw n.source=c,n}function i(n){return t.call(this,n,h)}var a=n.variable||"obj";return i.source="function("+a+"){\n"+c+"}",i},h.chain=function(n){var e=h(n);return e._chain=!0,e};function H(n,e){return n._chain?h(e).chain():e}h.mixin=function(t){return h.each(h.functions(t),function(n){var e=h[n]=t[n];h.prototype[n]=function(){var n=[this._wrapped];return i.apply(n,arguments),H(this,e.apply(h,n))}}),h},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(e){var t=r[e];h.prototype[e]=function(){var n=this._wrapped;return t.apply(n,arguments),"shift"!==e&&"splice"!==e||0!==n.length||delete n[0],H(this,n)}}),h.each(["concat","join","slice"],function(n){var e=r[n];h.prototype[n]=function(){return H(this,e.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},h.prototype.valueOf=h.prototype.toJSON=h.prototype.value,h.prototype.toString=function(){return String(this._wrapped)},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}()}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],3:[function(n,e,t){"use strict";var i=n("underscore"),u=n("../events");e.exports=function(n){var r=n.getComponent("MenuItem");return n.extend(r,{constructor:function(n,e){var t=e.source;if(!i.isObject(t))throw new Error('was not provided a "source" object, but rather: '+typeof t);e=i.extend({selectable:!0,label:t.label},e),r.call(this,n,e),this.source=t},handleClick:function(n){r.prototype.handleClick.call(this,n),this.player().trigger(u.QUALITY_REQUESTED,this.source)}})}},{"../events":5,underscore:2}],4:[function(n,e,t){"use strict";var i=n("underscore"),u=n("../events"),o=n("./QualityOption"),c="vjs-quality-changing";e.exports=function(n){var e,r=n.getComponent("MenuButton"),t=o(n);return e=n.extend(r,{constructor:function(t,n){r.call(this,t,n),t.on(u.QUALITY_REQUESTED,function(n,e){this.setSelectedSource(e),t.addClass(c),t.one("loadeddata",function(){t.removeClass(c)})}.bind(this)),t.on(u.PLAYER_SOURCES_CHANGED,function(){this.update()}.bind(this)),t.on(u.QUALITY_SELECTED,function(n,e){this.setSelectedSource(e)}.bind(this)),t.one("ready",function(){this.selectedSrc=t.src(),this.update()}.bind(this)),this.controlText("Open quality selector menu")},setSelectedSource:function(n){var e=n?n.src:void 0;this.selectedSrc!==e&&(this.selectedSrc=e,i.each(this.items,function(n){n.selected(n.source.src===e)}))},createItems:function(){var e=this.player(),n=e.currentSources();return i.map(n,function(n){return new t(e,{source:n,selected:n.src===this.selectedSrc})}.bind(this))},buildWrapperCSSClass:function(){return"vjs-quality-selector "+r.prototype.buildWrapperCSSClass.call(this)}}),n.registerComponent("QualitySelector",e),e}},{"../events":5,"./QualityOption":3,underscore:2}],5:[function(n,e,t){"use strict";e.exports={QUALITY_REQUESTED:"qualityRequested",QUALITY_SELECTED:"qualitySelected",PLAYER_SOURCES_CHANGED:"playerSourcesChanged"}},{}],6:[function(n,e,t){"use strict";var c=n("underscore"),r=n("./events"),i=n("./components/QualitySelector"),u=n("./middleware/SourceInterceptor"),a=n("./util/SafeSeek");e.exports=function(n){n=n||window.videojs,i(n),u(n),n.hook("setup",function(o){o.on(r.QUALITY_REQUESTED,function(n,e){var t=o.currentSources(),r=o.currentTime(),i=o.playbackRate(),u=o.paused();c.each(t,function(n){n.selected=!1}),c.findWhere(t,{src:e.src}).selected=!0,o._qualitySelectorSafeSeek&&o._qualitySelectorSafeSeek.onQualitySelectionChange(),o.src(t),o.ready(function(){o._qualitySelectorSafeSeek&&!o._qualitySelectorSafeSeek.hasFinished()||(o._qualitySelectorSafeSeek=new a(o,r),o.playbackRate(i)),u||o.play()})})})},e.exports.EVENTS=r},{"./components/QualitySelector":4,"./events":5,"./middleware/SourceInterceptor":7,"./util/SafeSeek":9,underscore:2}],7:[function(n,e,t){"use strict";var u=n("underscore"),o=n("../events");e.exports=function(n){n.use("*",function(i){return{setSource:function(n,e){var t,r=i.currentSources();i._qualitySelectorSafeSeek&&i._qualitySelectorSafeSeek.onPlayerSourcesChange(),u.isEqual(r,i._qualitySelectorPreviousSources)||(i.trigger(o.PLAYER_SOURCES_CHANGED,r),i._qualitySelectorPreviousSources=r),t=u.find(r,function(n){return!0===n.selected||"true"===n.selected||"selected"===n.selected})||n,i.trigger(o.QUALITY_SELECTED,t),e(null,t)}}})}},{"../events":5,underscore:2}],8:[function(n,e,t){"use strict";n("./index")()},{"./index":6}],9:[function(n,e,t){"use strict";var r=n("class.extend");e.exports=r.extend({init:function(n,e){this._player=n,this._seekToTime=e,this._hasFinished=!1,this._keepThisInstanceWhenPlayerSourcesChange=!1,this._seekWhenSafe()},_seekWhenSafe:function(){this._player.readyState()<3?(this._seekFn=this._seek.bind(this),this._player.one("canplay",this._seekFn)):this._seek()},onPlayerSourcesChange:function(){this._keepThisInstanceWhenPlayerSourcesChange?this._keepThisInstanceWhenPlayerSourcesChange=!1:this.cancel()},onQualitySelectionChange:function(){this.hasFinished()||(this._keepThisInstanceWhenPlayerSourcesChange=!0)},_seek:function(){this._player.currentTime(this._seekToTime),this._keepThisInstanceWhenPlayerSourcesChange=!1,this._hasFinished=!0},hasFinished:function(){return this._hasFinished},cancel:function(){this._player.off("canplay",this._seekFn),this._keepThisInstanceWhenPlayerSourcesChange=!1,this._hasFinished=!0}})},{"class.extend":1}]},{},[8]);
+!function u(o,c,a){function l(e,n){if(!c[e]){if(!o[e]){var t="function"==typeof require&&require;if(!n&&t)return t(e,!0);if(s)return s(e,!0);var r=new Error("Cannot find module '"+e+"'");throw r.code="MODULE_NOT_FOUND",r}var i=c[e]={exports:{}};o[e][0].call(i.exports,function(n){return l(o[e][1][n]||n)},i,i.exports,u,o,c,a)}return c[e].exports}for(var s="function"==typeof require&&require,n=0;n<a.length;n++)l(a[n]);return l}({1:[function(n,e,t){!function(){var u=!1,o=/xyz/.test(function(){xyz})?/\b_super\b/:/.*/;this.Class=function(){},Class.extend=function(n){var i=this.prototype;u=!0;var e=new this;for(var t in u=!1,n)e[t]="function"==typeof n[t]&&"function"==typeof i[t]&&o.test(n[t])?function(t,r){return function(){var n=this._super;this._super=i[t];var e=r.apply(this,arguments);return this._super=n,e}}(t,n[t]):n[t];function r(){!u&&this.init&&this.init.apply(this,arguments)}return((r.prototype=e).constructor=r).extend=arguments.callee,r},e.exports=Class}()},{}],2:[function(n,J,$){(function(V){!function(){function t(){}var n="object"==typeof self&&self.self===self&&self||"object"==typeof V&&V.global===V&&V||this||{},e=n._,r=Array.prototype,o=Object.prototype,f="undefined"!=typeof Symbol?Symbol.prototype:null,i=r.push,a=r.slice,p=o.toString,u=o.hasOwnProperty,c=Array.isArray,l=Object.keys,s=Object.create,h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};void 0===$||$.nodeType?n._=h:(void 0!==J&&!J.nodeType&&J.exports&&($=J.exports=h),$._=h),h.VERSION="1.9.1";function d(i,u,n){if(void 0===u)return i;switch(null==n?3:n){case 1:return function(n){return i.call(u,n)};case 3:return function(n,e,t){return i.call(u,n,e,t)};case 4:return function(n,e,t,r){return i.call(u,n,e,t,r)}}return function(){return i.apply(u,arguments)}}function y(n,e,t){return h.iteratee!==v?h.iteratee(n,e):null==n?h.identity:h.isFunction(n)?d(n,e,t):h.isObject(n)&&!h.isArray(n)?h.matcher(n):h.property(n)}var v;h.iteratee=v=function(n,e){return y(n,e,1/0)};function g(i,u){return u=null==u?i.length-1:+u,function(){for(var n=Math.max(arguments.length-u,0),e=Array(n),t=0;t<n;t++)e[t]=arguments[t+u];switch(u){case 0:return i.call(this,e);case 1:return i.call(this,arguments[0],e);case 2:return i.call(this,arguments[0],arguments[1],e)}var r=Array(u+1);for(t=0;t<u;t++)r[t]=arguments[t];return r[u]=e,i.apply(this,r)}}function S(n){if(!h.isObject(n))return{};if(s)return s(n);t.prototype=n;var e=new t;return t.prototype=null,e}function m(e){return function(n){return null==n?void 0:n[e]}}function _(n,e){return null!=n&&u.call(n,e)}function b(n,e){for(var t=e.length,r=0;r<t;r++){if(null==n)return;n=n[e[r]]}return t?n:void 0}function x(n){var e=j(n);return"number"==typeof e&&0<=e&&e<=k}var k=Math.pow(2,53)-1,j=m("length");h.each=h.forEach=function(n,e,t){var r,i;if(e=d(e,t),x(n))for(r=0,i=n.length;r<i;r++)e(n[r],r,n);else{var u=h.keys(n);for(r=0,i=u.length;r<i;r++)e(n[u[r]],u[r],n)}return n},h.map=h.collect=function(n,e,t){e=y(e,t);for(var r=!x(n)&&h.keys(n),i=(r||n).length,u=Array(i),o=0;o<i;o++){var c=r?r[o]:o;u[o]=e(n[c],c,n)}return u};function E(a){return function(n,e,t,r){var i=3<=arguments.length;return function(n,e,t,r){var i=!x(n)&&h.keys(n),u=(i||n).length,o=0<a?0:u-1;for(r||(t=n[i?i[o]:o],o+=a);0<=o&&o<u;o+=a){var c=i?i[o]:o;t=e(t,n[c],c,n)}return t}(n,d(e,r,4),t,i)}}h.reduce=h.foldl=h.inject=E(1),h.reduceRight=h.foldr=E(-1),h.find=h.detect=function(n,e,t){var r=(x(n)?h.findIndex:h.findKey)(n,e,t);if(void 0!==r&&-1!==r)return n[r]},h.filter=h.select=function(n,r,e){var i=[];return r=y(r,e),h.each(n,function(n,e,t){r(n,e,t)&&i.push(n)}),i},h.reject=function(n,e,t){return h.filter(n,h.negate(y(e)),t)},h.every=h.all=function(n,e,t){e=y(e,t);for(var r=!x(n)&&h.keys(n),i=(r||n).length,u=0;u<i;u++){var o=r?r[u]:u;if(!e(n[o],o,n))return!1}return!0},h.some=h.any=function(n,e,t){e=y(e,t);for(var r=!x(n)&&h.keys(n),i=(r||n).length,u=0;u<i;u++){var o=r?r[u]:u;if(e(n[o],o,n))return!0}return!1},h.contains=h.includes=h.include=function(n,e,t,r){return x(n)||(n=h.values(n)),"number"==typeof t&&!r||(t=0),0<=h.indexOf(n,e,t)},h.invoke=g(function(n,t,r){var i,u;return h.isFunction(t)?u=t:h.isArray(t)&&(i=t.slice(0,-1),t=t[t.length-1]),h.map(n,function(n){var e=u;if(!e){if(i&&i.length&&(n=b(n,i)),null==n)return;e=n[t]}return null==e?e:e.apply(n,r)})}),h.pluck=function(n,e){return h.map(n,h.property(e))},h.where=function(n,e){return h.filter(n,h.matcher(e))},h.findWhere=function(n,e){return h.find(n,h.matcher(e))},h.max=function(n,r,e){var t,i,u=-1/0,o=-1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var c=0,a=(n=x(n)?n:h.values(n)).length;c<a;c++)null!=(t=n[c])&&u<t&&(u=t);else r=y(r,e),h.each(n,function(n,e,t){i=r(n,e,t),(o<i||i===-1/0&&u===-1/0)&&(u=n,o=i)});return u},h.min=function(n,r,e){var t,i,u=1/0,o=1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var c=0,a=(n=x(n)?n:h.values(n)).length;c<a;c++)null!=(t=n[c])&&t<u&&(u=t);else r=y(r,e),h.each(n,function(n,e,t){((i=r(n,e,t))<o||i===1/0&&u===1/0)&&(u=n,o=i)});return u},h.shuffle=function(n){return h.sample(n,1/0)},h.sample=function(n,e,t){if(null==e||t)return x(n)||(n=h.values(n)),n[h.random(n.length-1)];var r=x(n)?h.clone(n):h.values(n),i=j(r);e=Math.max(Math.min(e,i),0);for(var u=i-1,o=0;o<e;o++){var c=h.random(o,u),a=r[o];r[o]=r[c],r[c]=a}return r.slice(0,e)},h.sortBy=function(n,r,e){var i=0;return r=y(r,e),h.pluck(h.map(n,function(n,e,t){return{value:n,index:i++,criteria:r(n,e,t)}}).sort(function(n,e){var t=n.criteria,r=e.criteria;if(t!==r){if(r<t||void 0===t)return 1;if(t<r||void 0===r)return-1}return n.index-e.index}),"value")};function w(o,e){return function(r,i,n){var u=e?[[],[]]:{};return i=y(i,n),h.each(r,function(n,e){var t=i(n,e,r);o(u,n,t)}),u}}h.groupBy=w(function(n,e,t){_(n,t)?n[t].push(e):n[t]=[e]}),h.indexBy=w(function(n,e,t){n[t]=e}),h.countBy=w(function(n,e,t){_(n,t)?n[t]++:n[t]=1});var A=/[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;h.toArray=function(n){return n?h.isArray(n)?a.call(n):h.isString(n)?n.match(A):x(n)?h.map(n,h.identity):h.values(n):[]},h.size=function(n){return null==n?0:x(n)?n.length:h.keys(n).length},h.partition=w(function(n,e,t){n[t?0:1].push(e)},!0),h.first=h.head=h.take=function(n,e,t){return null==n||n.length<1?null==e?void 0:[]:null==e||t?n[0]:h.initial(n,n.length-e)},h.initial=function(n,e,t){return a.call(n,0,Math.max(0,n.length-(null==e||t?1:e)))},h.last=function(n,e,t){return null==n||n.length<1?null==e?void 0:[]:null==e||t?n[n.length-1]:h.rest(n,Math.max(0,n.length-e))},h.rest=h.tail=h.drop=function(n,e,t){return a.call(n,null==e||t?1:e)},h.compact=function(n){return h.filter(n,Boolean)};var T=function(n,e,t,r){for(var i=(r=r||[]).length,u=0,o=j(n);u<o;u++){var c=n[u];if(x(c)&&(h.isArray(c)||h.isArguments(c)))if(e)for(var a=0,l=c.length;a<l;)r[i++]=c[a++];else T(c,e,t,r),i=r.length;else t||(r[i++]=c)}return r};h.flatten=function(n,e){return T(n,e,!1)},h.without=g(function(n,e){return h.difference(n,e)}),h.uniq=h.unique=function(n,e,t,r){h.isBoolean(e)||(r=t,t=e,e=!1),null!=t&&(t=y(t,r));for(var i=[],u=[],o=0,c=j(n);o<c;o++){var a=n[o],l=t?t(a,o,n):a;e&&!t?(o&&u===l||i.push(a),u=l):t?h.contains(u,l)||(u.push(l),i.push(a)):h.contains(i,a)||i.push(a)}return i},h.union=g(function(n){return h.uniq(T(n,!0,!0))}),h.intersection=function(n){for(var e=[],t=arguments.length,r=0,i=j(n);r<i;r++){var u=n[r];if(!h.contains(e,u)){var o;for(o=1;o<t&&h.contains(arguments[o],u);o++);o===t&&e.push(u)}}return e},h.difference=g(function(n,e){return e=T(e,!0,!0),h.filter(n,function(n){return!h.contains(e,n)})}),h.unzip=function(n){for(var e=n&&h.max(n,j).length||0,t=Array(e),r=0;r<e;r++)t[r]=h.pluck(n,r);return t},h.zip=g(h.unzip),h.object=function(n,e){for(var t={},r=0,i=j(n);r<i;r++)e?t[n[r]]=e[r]:t[n[r][0]]=n[r][1];return t};function O(u){return function(n,e,t){e=y(e,t);for(var r=j(n),i=0<u?0:r-1;0<=i&&i<r;i+=u)if(e(n[i],i,n))return i;return-1}}h.findIndex=O(1),h.findLastIndex=O(-1),h.sortedIndex=function(n,e,t,r){for(var i=(t=y(t,r,1))(e),u=0,o=j(n);u<o;){var c=Math.floor((u+o)/2);t(n[c])<i?u=c+1:o=c}return u};function C(u,o,c){return function(n,e,t){var r=0,i=j(n);if("number"==typeof t)0<u?r=0<=t?t:Math.max(t+i,r):i=0<=t?Math.min(t+1,i):t+i+1;else if(c&&t&&i)return n[t=c(n,e)]===e?t:-1;if(e!=e)return 0<=(t=o(a.call(n,r,i),h.isNaN))?t+r:-1;for(t=0<u?r:i-1;0<=t&&t<i;t+=u)if(n[t]===e)return t;return-1}}h.indexOf=C(1,h.findIndex,h.sortedIndex),h.lastIndexOf=C(-1,h.findLastIndex),h.range=function(n,e,t){null==e&&(e=n||0,n=0),t=t||(e<n?-1:1);for(var r=Math.max(Math.ceil((e-n)/t),0),i=Array(r),u=0;u<r;u++,n+=t)i[u]=n;return i},h.chunk=function(n,e){if(null==e||e<1)return[];for(var t=[],r=0,i=n.length;r<i;)t.push(a.call(n,r,r+=e));return t};function I(n,e,t,r,i){if(!(r instanceof e))return n.apply(t,i);var u=S(n.prototype),o=n.apply(u,i);return h.isObject(o)?o:u}h.bind=g(function(e,t,r){if(!h.isFunction(e))throw new TypeError("Bind must be called on a function");var i=g(function(n){return I(e,i,t,this,r.concat(n))});return i}),h.partial=g(function(i,u){var o=h.partial.placeholder,c=function(){for(var n=0,e=u.length,t=Array(e),r=0;r<e;r++)t[r]=u[r]===o?arguments[n++]:u[r];for(;n<arguments.length;)t.push(arguments[n++]);return I(i,c,this,this,t)};return c}),(h.partial.placeholder=h).bindAll=g(function(n,e){var t=(e=T(e,!1,!1)).length;if(t<1)throw new Error("bindAll must be passed function names");for(;t--;){var r=e[t];n[r]=h.bind(n[r],n)}}),h.memoize=function(r,i){var u=function(n){var e=u.cache,t=""+(i?i.apply(this,arguments):n);return _(e,t)||(e[t]=r.apply(this,arguments)),e[t]};return u.cache={},u},h.delay=g(function(n,e,t){return setTimeout(function(){return n.apply(null,t)},e)}),h.defer=h.partial(h.delay,h,1),h.throttle=function(t,r,i){var u,o,c,a,l=0;i=i||{};function s(){l=!1===i.leading?0:h.now(),u=null,a=t.apply(o,c),u||(o=c=null)}function n(){var n=h.now();l||!1!==i.leading||(l=n);var e=r-(n-l);return o=this,c=arguments,e<=0||r<e?(u&&(clearTimeout(u),u=null),l=n,a=t.apply(o,c),u||(o=c=null)):u||!1===i.trailing||(u=setTimeout(s,e)),a}return n.cancel=function(){clearTimeout(u),l=0,u=o=c=null},n},h.debounce=function(t,r,i){function u(n,e){o=null,e&&(c=t.apply(n,e))}var o,c,n=g(function(n){if(o&&clearTimeout(o),i){var e=!o;o=setTimeout(u,r),e&&(c=t.apply(this,n))}else o=h.delay(u,r,this,n);return c});return n.cancel=function(){clearTimeout(o),o=null},n},h.wrap=function(n,e){return h.partial(e,n)},h.negate=function(n){return function(){return!n.apply(this,arguments)}},h.compose=function(){var t=arguments,r=t.length-1;return function(){for(var n=r,e=t[r].apply(this,arguments);n--;)e=t[n].call(this,e);return e}},h.after=function(n,e){return function(){if(--n<1)return e.apply(this,arguments)}},h.before=function(n,e){var t;return function(){return 0<--n&&(t=e.apply(this,arguments)),n<=1&&(e=null),t}},h.once=h.partial(h.before,2),h.restArguments=g;function F(n,e){var t=M.length,r=n.constructor,i=h.isFunction(r)&&r.prototype||o,u="constructor";for(_(n,u)&&!h.contains(e,u)&&e.push(u);t--;)(u=M[t])in n&&n[u]!==i[u]&&!h.contains(e,u)&&e.push(u)}var q=!{toString:null}.propertyIsEnumerable("toString"),M=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];h.keys=function(n){if(!h.isObject(n))return[];if(l)return l(n);var e=[];for(var t in n)_(n,t)&&e.push(t);return q&&F(n,e),e},h.allKeys=function(n){if(!h.isObject(n))return[];var e=[];for(var t in n)e.push(t);return q&&F(n,e),e},h.values=function(n){for(var e=h.keys(n),t=e.length,r=Array(t),i=0;i<t;i++)r[i]=n[e[i]];return r},h.mapObject=function(n,e,t){e=y(e,t);for(var r=h.keys(n),i=r.length,u={},o=0;o<i;o++){var c=r[o];u[c]=e(n[c],c,n)}return u},h.pairs=function(n){for(var e=h.keys(n),t=e.length,r=Array(t),i=0;i<t;i++)r[i]=[e[i],n[e[i]]];return r},h.invert=function(n){for(var e={},t=h.keys(n),r=0,i=t.length;r<i;r++)e[n[t[r]]]=t[r];return e},h.functions=h.methods=function(n){var e=[];for(var t in n)h.isFunction(n[t])&&e.push(t);return e.sort()};function N(a,l){return function(n){var e=arguments.length;if(l&&(n=Object(n)),e<2||null==n)return n;for(var t=1;t<e;t++)for(var r=arguments[t],i=a(r),u=i.length,o=0;o<u;o++){var c=i[o];l&&void 0!==n[c]||(n[c]=r[c])}return n}}h.extend=N(h.allKeys),h.extendOwn=h.assign=N(h.keys),h.findKey=function(n,e,t){e=y(e,t);for(var r,i=h.keys(n),u=0,o=i.length;u<o;u++)if(e(n[r=i[u]],r,n))return r};function R(n,e,t){return e in t}var Q,L;h.pick=g(function(n,e){var t={},r=e[0];if(null==n)return t;h.isFunction(r)?(1<e.length&&(r=d(r,e[1])),e=h.allKeys(n)):(r=R,e=T(e,!1,!1),n=Object(n));for(var i=0,u=e.length;i<u;i++){var o=e[i],c=n[o];r(c,o,n)&&(t[o]=c)}return t}),h.omit=g(function(n,t){var e,r=t[0];return h.isFunction(r)?(r=h.negate(r),1<t.length&&(e=t[1])):(t=h.map(T(t,!1,!1),String),r=function(n,e){return!h.contains(t,e)}),h.pick(n,r,e)}),h.defaults=N(h.allKeys,!0),h.create=function(n,e){var t=S(n);return e&&h.extendOwn(t,e),t},h.clone=function(n){return h.isObject(n)?h.isArray(n)?n.slice():h.extend({},n):n},h.tap=function(n,e){return e(n),n},h.isMatch=function(n,e){var t=h.keys(e),r=t.length;if(null==n)return!r;for(var i=Object(n),u=0;u<r;u++){var o=t[u];if(e[o]!==i[o]||!(o in i))return!1}return!0},Q=function(n,e,t,r){if(n===e)return 0!==n||1/n==1/e;if(null==n||null==e)return!1;if(n!=n)return e!=e;var i=typeof n;return("function"==i||"object"==i||"object"==typeof e)&&L(n,e,t,r)},L=function(n,e,t,r){n instanceof h&&(n=n._wrapped),e instanceof h&&(e=e._wrapped);var i=p.call(n);if(i!==p.call(e))return!1;switch(i){case"[object RegExp]":case"[object String]":return""+n==""+e;case"[object Number]":return+n!=+n?+e!=+e:0==+n?1/+n==1/e:+n==+e;case"[object Date]":case"[object Boolean]":return+n==+e;case"[object Symbol]":return f.valueOf.call(n)===f.valueOf.call(e)}var u="[object Array]"===i;if(!u){if("object"!=typeof n||"object"!=typeof e)return!1;var o=n.constructor,c=e.constructor;if(o!==c&&!(h.isFunction(o)&&o instanceof o&&h.isFunction(c)&&c instanceof c)&&"constructor"in n&&"constructor"in e)return!1}r=r||[];for(var a=(t=t||[]).length;a--;)if(t[a]===n)return r[a]===e;if(t.push(n),r.push(e),u){if((a=n.length)!==e.length)return!1;for(;a--;)if(!Q(n[a],e[a],t,r))return!1}else{var l,s=h.keys(n);if(a=s.length,h.keys(e).length!==a)return!1;for(;a--;)if(l=s[a],!_(e,l)||!Q(n[l],e[l],t,r))return!1}return t.pop(),r.pop(),!0},h.isEqual=function(n,e){return Q(n,e)},h.isEmpty=function(n){return null==n||(x(n)&&(h.isArray(n)||h.isString(n)||h.isArguments(n))?0===n.length:0===h.keys(n).length)},h.isElement=function(n){return!(!n||1!==n.nodeType)},h.isArray=c||function(n){return"[object Array]"===p.call(n)},h.isObject=function(n){var e=typeof n;return"function"==e||"object"==e&&!!n},h.each(["Arguments","Function","String","Number","Date","RegExp","Error","Symbol","Map","WeakMap","Set","WeakSet"],function(e){h["is"+e]=function(n){return p.call(n)==="[object "+e+"]"}}),h.isArguments(arguments)||(h.isArguments=function(n){return _(n,"callee")});var U=n.document&&n.document.childNodes;"function"!=typeof/./&&"object"!=typeof Int8Array&&"function"!=typeof U&&(h.isFunction=function(n){return"function"==typeof n||!1}),h.isFinite=function(n){return!h.isSymbol(n)&&isFinite(n)&&!isNaN(parseFloat(n))},h.isNaN=function(n){return h.isNumber(n)&&isNaN(n)},h.isBoolean=function(n){return!0===n||!1===n||"[object Boolean]"===p.call(n)},h.isNull=function(n){return null===n},h.isUndefined=function(n){return void 0===n},h.has=function(n,e){if(!h.isArray(e))return _(n,e);for(var t=e.length,r=0;r<t;r++){var i=e[r];if(null==n||!u.call(n,i))return!1;n=n[i]}return!!t},h.noConflict=function(){return n._=e,this},h.identity=function(n){return n},h.constant=function(n){return function(){return n}},h.noop=function(){},h.property=function(e){return h.isArray(e)?function(n){return b(n,e)}:m(e)},h.propertyOf=function(e){return null==e?function(){}:function(n){return h.isArray(n)?b(e,n):e[n]}},h.matcher=h.matches=function(e){return e=h.extendOwn({},e),function(n){return h.isMatch(n,e)}},h.times=function(n,e,t){var r=Array(Math.max(0,n));e=d(e,t,1);for(var i=0;i<n;i++)r[i]=e(i);return r},h.random=function(n,e){return null==e&&(e=n,n=0),n+Math.floor(Math.random()*(e-n+1))},h.now=Date.now||function(){return(new Date).getTime()};function D(e){function t(n){return e[n]}var n="(?:"+h.keys(e).join("|")+")",r=RegExp(n),i=RegExp(n,"g");return function(n){return n=null==n?"":""+n,r.test(n)?n.replace(i,t):n}}var P={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#x27;","`":"&#x60;"},W=h.invert(P);h.escape=D(P),h.unescape=D(W),h.result=function(n,e,t){h.isArray(e)||(e=[e]);var r=e.length;if(!r)return h.isFunction(t)?t.call(n):t;for(var i=0;i<r;i++){var u=null==n?void 0:n[e[i]];void 0===u&&(u=t,i=r),n=h.isFunction(u)?u.call(n):u}return n};var B=0;h.uniqueId=function(n){var e=++B+"";return n?n+e:e},h.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};function Y(n){return"\\"+K[n]}var z=/(.)^/,K={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},G=/\\|'|\r|\n|\u2028|\u2029/g;h.template=function(u,n,e){!n&&e&&(n=e),n=h.defaults({},n,h.templateSettings);var t,r=RegExp([(n.escape||z).source,(n.interpolate||z).source,(n.evaluate||z).source].join("|")+"|$","g"),o=0,c="__p+='";u.replace(r,function(n,e,t,r,i){return c+=u.slice(o,i).replace(G,Y),o=i+n.length,e?c+="'+\n((__t=("+e+"))==null?'':_.escape(__t))+\n'":t?c+="'+\n((__t=("+t+"))==null?'':__t)+\n'":r&&(c+="';\n"+r+"\n__p+='"),n}),c+="';\n",n.variable||(c="with(obj||{}){\n"+c+"}\n"),c="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+c+"return __p;\n";try{t=new Function(n.variable||"obj","_",c)}catch(n){throw n.source=c,n}function i(n){return t.call(this,n,h)}var a=n.variable||"obj";return i.source="function("+a+"){\n"+c+"}",i},h.chain=function(n){var e=h(n);return e._chain=!0,e};function H(n,e){return n._chain?h(e).chain():e}h.mixin=function(t){return h.each(h.functions(t),function(n){var e=h[n]=t[n];h.prototype[n]=function(){var n=[this._wrapped];return i.apply(n,arguments),H(this,e.apply(h,n))}}),h},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(e){var t=r[e];h.prototype[e]=function(){var n=this._wrapped;return t.apply(n,arguments),"shift"!==e&&"splice"!==e||0!==n.length||delete n[0],H(this,n)}}),h.each(["concat","join","slice"],function(n){var e=r[n];h.prototype[n]=function(){return H(this,e.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},h.prototype.valueOf=h.prototype.toJSON=h.prototype.value,h.prototype.toString=function(){return String(this._wrapped)},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}()}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],3:[function(n,e,t){"use strict";var i=n("underscore"),u=n("../events");e.exports=function(n){var r=n.getComponent("MenuItem");return n.extend(r,{constructor:function(n,e){var t=e.source;if(!i.isObject(t))throw new Error('was not provided a "source" object, but rather: '+typeof t);e=i.extend({selectable:!0,label:t.label},e),r.call(this,n,e),this.source=t},handleClick:function(n){r.prototype.handleClick.call(this,n),this.player().trigger(u.QUALITY_REQUESTED,this.source)}})}},{"../events":5,underscore:2}],4:[function(n,e,t){"use strict";var i=n("underscore"),u=n("../events"),o=n("./QualityOption"),c="vjs-quality-changing";e.exports=function(n){var e,r=n.getComponent("MenuButton"),t=o(n);return e=n.extend(r,{constructor:function(t,n){r.call(this,t,n),t.on(u.QUALITY_REQUESTED,function(n,e){this.setSelectedSource(e),t.addClass(c),t.one("loadeddata",function(){t.removeClass(c)})}.bind(this)),t.on(u.PLAYER_SOURCES_CHANGED,function(){this.update()}.bind(this)),t.on(u.QUALITY_SELECTED,function(n,e){this.setSelectedSource(e)}.bind(this)),t.one("ready",function(){this.selectedSrc=t.src(),this.update()}.bind(this)),this.controlText("Open quality selector menu")},setSelectedSource:function(n){var e=n?n.src:void 0;this.selectedSrc!==e&&(this.selectedSrc=e,i.each(this.items,function(n){n.selected(n.source.src===e)}))},createItems:function(){var e=this.player(),n=e.currentSources();return n=n.filter(function(n){return null==n.hidequalityoption}),i.map(n,function(n){return new t(e,{source:n,selected:n.src===this.selectedSrc})}.bind(this))},buildWrapperCSSClass:function(){return"vjs-quality-selector "+r.prototype.buildWrapperCSSClass.call(this)}}),n.registerComponent("QualitySelector",e),e}},{"../events":5,"./QualityOption":3,underscore:2}],5:[function(n,e,t){"use strict";e.exports={QUALITY_REQUESTED:"qualityRequested",QUALITY_SELECTED:"qualitySelected",PLAYER_SOURCES_CHANGED:"playerSourcesChanged"}},{}],6:[function(n,e,t){"use strict";var c=n("underscore"),r=n("./events"),i=n("./components/QualitySelector"),u=n("./middleware/SourceInterceptor"),a=n("./util/SafeSeek");e.exports=function(n){n=n||window.videojs,i(n),u(n),n.hook("setup",function(o){o.on(r.QUALITY_REQUESTED,function(n,e){var t=o.currentSources(),r=o.currentTime(),i=o.playbackRate(),u=o.paused();c.each(t,function(n){n.selected=!1}),c.findWhere(t,{src:e.src}).selected=!0,o._qualitySelectorSafeSeek&&o._qualitySelectorSafeSeek.onQualitySelectionChange(),o.src(t),o.ready(function(){o._qualitySelectorSafeSeek&&!o._qualitySelectorSafeSeek.hasFinished()||(o._qualitySelectorSafeSeek=new a(o,r),o.playbackRate(i)),u||o.play()})})})},e.exports.EVENTS=r},{"./components/QualitySelector":4,"./events":5,"./middleware/SourceInterceptor":7,"./util/SafeSeek":9,underscore:2}],7:[function(n,e,t){"use strict";var u=n("underscore"),o=n("../events");e.exports=function(n){n.use("*",function(i){return{setSource:function(n,e){var t,r=i.currentSources();i._qualitySelectorSafeSeek&&i._qualitySelectorSafeSeek.onPlayerSourcesChange(),u.isEqual(r,i._qualitySelectorPreviousSources)||(i.trigger(o.PLAYER_SOURCES_CHANGED,r),i._qualitySelectorPreviousSources=r),t=u.find(r,function(n){return!0===n.selected||"true"===n.selected||"selected"===n.selected})||n,i.trigger(o.QUALITY_SELECTED,t),e(null,t)}}})}},{"../events":5,underscore:2}],8:[function(n,e,t){"use strict";n("./index")()},{"./index":6}],9:[function(n,e,t){"use strict";var r=n("class.extend");e.exports=r.extend({init:function(n,e){this._player=n,this._seekToTime=e,this._hasFinished=!1,this._keepThisInstanceWhenPlayerSourcesChange=!1,this._seekWhenSafe()},_seekWhenSafe:function(){this._player.readyState()<3?(this._seekFn=this._seek.bind(this),this._player.one("canplay",this._seekFn)):this._seek()},onPlayerSourcesChange:function(){this._keepThisInstanceWhenPlayerSourcesChange?this._keepThisInstanceWhenPlayerSourcesChange=!1:this.cancel()},onQualitySelectionChange:function(){this.hasFinished()||(this._keepThisInstanceWhenPlayerSourcesChange=!0)},_seek:function(){this._player.currentTime(this._seekToTime),this._keepThisInstanceWhenPlayerSourcesChange=!1,this._hasFinished=!0},hasFinished:function(){return this._hasFinished},cancel:function(){this._player.off("canplay",this._seekFn),this._keepThisInstanceWhenPlayerSourcesChange=!1,this._hasFinished=!0}})},{"class.extend":1}]},{},[8]);
//# sourceMappingURL=silvermine-videojs-quality-selector.min.js.map \ No newline at end of file
diff --git a/assets/js/subscribe_widget.js b/assets/js/subscribe_widget.js
index 216c36fe..45ff5706 100644
--- a/assets/js/subscribe_widget.js
+++ b/assets/js/subscribe_widget.js
@@ -1,4 +1,5 @@
-var subscribe_data = JSON.parse(document.getElementById('subscribe_data').innerHTML);
+'use strict';
+var subscribe_data = JSON.parse(document.getElementById('subscribe_data').textContent);
var subscribe_button = document.getElementById('subscribe');
subscribe_button.parentNode['action'] = 'javascript:void(0)';
@@ -9,9 +10,11 @@ if (subscribe_button.getAttribute('data-type') === 'subscribe') {
subscribe_button.onclick = unsubscribe;
}
-function subscribe(retries = 5) {
+function subscribe(retries) {
+ if (retries === undefined) retries = 5;
+
if (retries <= 0) {
- console.log('Failed to subscribe.');
+ console.warn('Failed to subscribe.');
return;
}
@@ -28,30 +31,33 @@ function subscribe(retries = 5) {
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status != 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status !== 200) {
subscribe_button.onclick = subscribe;
subscribe_button.innerHTML = fallback;
}
}
- }
+ };
xhr.onerror = function () {
- console.log('Subscribing failed... ' + retries + '/5');
- setTimeout(function () { subscribe(retries - 1) }, 1000);
- }
+ console.warn('Subscribing failed... ' + retries + '/5');
+ setTimeout(function () { subscribe(retries - 1); }, 1000);
+ };
xhr.ontimeout = function () {
- console.log('Subscribing failed... ' + retries + '/5');
+ console.warn('Subscribing failed... ' + retries + '/5');
subscribe(retries - 1);
- }
+ };
xhr.send('csrf_token=' + subscribe_data.csrf_token);
}
-function unsubscribe(retries = 5) {
+function unsubscribe(retries) {
+ if (retries === undefined)
+ retries = 5;
+
if (retries <= 0) {
- console.log('Failed to subscribe');
+ console.warn('Failed to subscribe');
return;
}
@@ -68,23 +74,23 @@ function unsubscribe(retries = 5) {
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status != 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status !== 200) {
subscribe_button.onclick = unsubscribe;
subscribe_button.innerHTML = fallback;
}
}
- }
+ };
xhr.onerror = function () {
- console.log('Unsubscribing failed... ' + retries + '/5');
- setTimeout(function () { unsubscribe(retries - 1) }, 1000);
- }
+ console.warn('Unsubscribing failed... ' + retries + '/5');
+ setTimeout(function () { unsubscribe(retries - 1); }, 1000);
+ };
xhr.ontimeout = function () {
- console.log('Unsubscribing failed... ' + retries + '/5');
+ 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 0214a7f0..3f503b38 100644
--- a/assets/js/themes.js
+++ b/assets/js/themes.js
@@ -1,8 +1,9 @@
+'use strict';
var toggle_theme = document.getElementById('toggle_theme');
toggle_theme.href = 'javascript:void(0);';
toggle_theme.addEventListener('click', function () {
- var dark_mode = document.body.classList.contains("light-theme");
+ var dark_mode = document.body.classList.contains('light-theme');
var url = '/toggle_theme?redirect=false';
var xhr = new XMLHttpRequest();
@@ -13,7 +14,7 @@ toggle_theme.addEventListener('click', function () {
set_mode(dark_mode);
try {
window.localStorage.setItem('dark_mode', dark_mode ? 'dark' : 'light');
- } catch {}
+ } catch (e) {}
xhr.send();
});
@@ -29,7 +30,7 @@ window.addEventListener('DOMContentLoaded', function () {
try {
// Update localStorage if dark mode preference changed on preferences page
window.localStorage.setItem('dark_mode', dark_mode);
- } catch {}
+ } catch (e) {}
update_mode(dark_mode);
});
@@ -46,11 +47,11 @@ function scheme_switch (e) {
if (localStorage.getItem('dark_mode')) {
return;
}
- } catch {}
+ } catch (exception) {}
if (e.matches) {
- if (e.media.includes("dark")) {
+ if (e.media.includes('dark')) {
set_mode(true);
- } else if (e.media.includes("light")) {
+ } else if (e.media.includes('light')) {
set_mode(false);
}
}
@@ -77,15 +78,13 @@ function update_mode (mode) {
// 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 (mode === 'false' /* for backwards compatibility */ || mode === 'light') {
+ // If preference for light mode indicated
+ set_mode(false);
+ }
else if (document.getElementById('dark_mode_pref').textContent === '' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
// If no preference indicated here and no preference indicated on the preferences page (backend), but the browser tells us that the operating system has a dark theme
set_mode(true);
}
// else do nothing, falling back to the mode defined by the `dark_mode` preference on the preferences page (backend)
}
-
-
diff --git a/assets/js/watch.js b/assets/js/watch.js
index 1579abf4..29d58be5 100644
--- a/assets/js/watch.js
+++ b/assets/js/watch.js
@@ -1,31 +1,32 @@
-var video_data = JSON.parse(document.getElementById('video_data').innerHTML);
+'use strict';
+var video_data = JSON.parse(document.getElementById('video_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 toggle_parent(target) {
- body = target.parentNode.parentNode.children[1];
+ var body = target.parentNode.parentNode.children[1];
if (body.style.display === null || body.style.display === '') {
- target.innerHTML = '[ + ]';
+ target.textContent = '[ + ]';
body.style.display = 'none';
} else {
- target.innerHTML = '[ - ]';
+ target.textContent = '[ − ]';
body.style.display = '';
}
}
function toggle_comments(event) {
var target = event.target;
- body = target.parentNode.parentNode.parentNode.children[1];
+ var body = target.parentNode.parentNode.parentNode.children[1];
if (body.style.display === null || body.style.display === '') {
- target.innerHTML = '[ + ]';
+ target.textContent = '[ + ]';
body.style.display = 'none';
} else {
- target.innerHTML = '[ - ]';
+ target.textContent = '[ − ]';
body.style.display = '';
}
}
@@ -43,13 +44,13 @@ function swap_comments(event) {
function hide_youtube_replies(event) {
var target = event.target;
- sub_text = target.getAttribute('data-inner-text');
- inner_text = target.getAttribute('data-sub-text');
+ var sub_text = target.getAttribute('data-inner-text');
+ var inner_text = target.getAttribute('data-sub-text');
- body = target.parentNode.parentNode.children[1];
+ var body = target.parentNode.parentNode.children[1];
body.style.display = 'none';
- target.innerHTML = sub_text;
+ target.textContent = sub_text;
target.onclick = show_youtube_replies;
target.setAttribute('data-inner-text', inner_text);
target.setAttribute('data-sub-text', sub_text);
@@ -58,13 +59,13 @@ function hide_youtube_replies(event) {
function show_youtube_replies(event) {
var target = event.target;
- sub_text = target.getAttribute('data-inner-text');
- inner_text = target.getAttribute('data-sub-text');
+ var sub_text = target.getAttribute('data-inner-text');
+ var inner_text = target.getAttribute('data-sub-text');
- body = target.parentNode.parentNode.children[1];
+ var body = target.parentNode.parentNode.children[1];
body.style.display = '';
- target.innerHTML = sub_text;
+ target.textContent = sub_text;
target.onclick = hide_youtube_replies;
target.setAttribute('data-inner-text', inner_text);
target.setAttribute('data-sub-text', sub_text);
@@ -116,25 +117,26 @@ function number_with_separator(val) {
}
function get_playlist(plid, retries) {
- if (retries == undefined) retries = 5;
- playlist = document.getElementById('playlist');
+ if (retries === undefined) retries = 5;
+ var playlist = document.getElementById('playlist');
if (retries <= 0) {
- console.log('Failed to pull playlist');
+ console.warn('Failed to pull playlist');
playlist.innerHTML = '';
return;
}
playlist.innerHTML = ' \
<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3> \
- <hr>'
+ <hr>';
+ var plid_url;
if (plid.startsWith('RD')) {
- var plid_url = '/api/v1/mixes/' + plid +
+ plid_url = '/api/v1/mixes/' + plid +
'?continuation=' + video_data.id +
'&format=html&hl=' + video_data.preferences.locale;
} else {
- var plid_url = '/api/v1/playlists/' + plid +
+ plid_url = '/api/v1/playlists/' + plid +
'?index=' + video_data.index +
'&continuation=' + video_data.id +
'&format=html&hl=' + video_data.preferences.locale;
@@ -146,8 +148,8 @@ function get_playlist(plid, retries) {
xhr.open('GET', plid_url, true);
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
+ 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;
@@ -185,35 +187,35 @@ function get_playlist(plid, retries) {
document.getElementById('continue').style.display = '';
}
}
- }
+ };
xhr.onerror = function () {
playlist = document.getElementById('playlist');
playlist.innerHTML =
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3><hr>';
- console.log('Pulling playlist timed out... ' + retries + '/5');
- setTimeout(function () { get_playlist(plid, retries - 1) }, 1000);
- }
+ 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 =
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3><hr>';
- console.log('Pulling playlist timed out... ' + retries + '/5');
+ 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;
- comments = document.getElementById('comments');
+ if (retries === undefined) retries = 5;
+ var comments = document.getElementById('comments');
if (retries <= 0) {
- console.log('Failed to pull comments');
+ console.warn('Failed to pull comments');
comments.innerHTML = '';
return;
}
@@ -231,12 +233,12 @@ function get_reddit_comments(retries) {
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status === 200) {
comments.innerHTML = ' \
<div> \
<h3> \
- <a href="javascript:void(0)">[ - ]</a> \
+ <a href="javascript:void(0)">[ − ]</a> \
{title} \
</h3> \
<p> \
@@ -263,34 +265,34 @@ function get_reddit_comments(retries) {
comments.children[0].children[1].children[0].onclick = swap_comments;
} else {
if (video_data.params.comments[1] === 'youtube') {
- console.log('Pulling comments failed... ' + retries + '/5');
- setTimeout(function () { get_youtube_comments(retries - 1) }, 1000);
+ console.warn('Pulling comments failed... ' + retries + '/5');
+ setTimeout(function () { get_youtube_comments(retries - 1); }, 1000);
} else {
comments.innerHTML = fallback;
}
}
}
- }
+ };
xhr.onerror = function () {
- console.log('Pulling comments failed... ' + retries + '/5');
- setTimeout(function () { get_reddit_comments(retries - 1) }, 1000);
- }
+ console.warn('Pulling comments failed... ' + retries + '/5');
+ setTimeout(function () { get_reddit_comments(retries - 1); }, 1000);
+ };
xhr.ontimeout = function () {
- console.log('Pulling comments failed... ' + retries + '/5');
+ console.warn('Pulling comments failed... ' + retries + '/5');
get_reddit_comments(retries - 1);
- }
+ };
xhr.send();
}
function get_youtube_comments(retries) {
- if (retries == undefined) retries = 5;
- comments = document.getElementById('comments');
+ if (retries === undefined) retries = 5;
+ var comments = document.getElementById('comments');
if (retries <= 0) {
- console.log('Failed to pull comments');
+ console.warn('Failed to pull comments');
comments.innerHTML = '';
return;
}
@@ -309,12 +311,12 @@ function get_youtube_comments(retries) {
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status === 200) {
comments.innerHTML = ' \
<div> \
<h3> \
- <a href="javascript:void(0)">[ - ]</a> \
+ <a href="javascript:void(0)">[ − ]</a> \
{commentsText} \
</h3> \
<b> \
@@ -336,27 +338,27 @@ function get_youtube_comments(retries) {
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);
+ setTimeout(function () { get_youtube_comments(retries - 1); }, 1000);
} else {
comments.innerHTML = '';
}
}
}
- }
+ };
xhr.onerror = function () {
comments.innerHTML =
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
- console.log('Pulling comments failed... ' + retries + '/5');
- setTimeout(function () { get_youtube_comments(retries - 1) }, 1000);
- }
+ console.warn('Pulling comments failed... ' + retries + '/5');
+ setTimeout(function () { get_youtube_comments(retries - 1); }, 1000);
+ };
xhr.ontimeout = function () {
comments.innerHTML =
'<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
- console.log('Pulling comments failed... ' + retries + '/5');
+ console.warn('Pulling comments failed... ' + retries + '/5');
get_youtube_comments(retries - 1);
- }
+ };
xhr.send();
}
@@ -373,7 +375,7 @@ function get_youtube_replies(target, load_more, load_replies) {
'?format=html' +
'&hl=' + video_data.preferences.locale +
'&thin_mode=' + video_data.preferences.thin_mode +
- '&continuation=' + continuation
+ '&continuation=' + continuation;
if (load_replies) {
url += '&action=action_get_comment_replies';
}
@@ -383,8 +385,8 @@ function get_youtube_replies(target, load_more, load_replies) {
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status === 200) {
if (load_more) {
body = body.parentNode.parentNode;
body.removeChild(body.lastElementChild);
@@ -412,12 +414,12 @@ function get_youtube_replies(target, load_more, load_replies) {
body.innerHTML = fallback;
}
}
- }
+ };
xhr.ontimeout = function () {
- console.log('Pulling comments failed.');
+ console.warn('Pulling comments failed.');
body.innerHTML = fallback;
- }
+ };
xhr.send();
}
@@ -461,7 +463,7 @@ window.addEventListener('load', function (e) {
} else if (video_data.params.comments[1] === 'reddit') {
get_reddit_comments();
} else {
- comments = document.getElementById('comments');
+ var comments = document.getElementById('comments');
comments.innerHTML = '';
}
});
diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js
index ba741974..87989a79 100644
--- a/assets/js/watched_widget.js
+++ b/assets/js/watched_widget.js
@@ -1,4 +1,5 @@
-var watched_data = JSON.parse(document.getElementById('watched_data').innerHTML);
+'use strict';
+var watched_data = JSON.parse(document.getElementById('watched_data').textContent);
function mark_watched(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
@@ -13,12 +14,12 @@ function mark_watched(target) {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status != 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status !== 200) {
tile.style.display = '';
}
}
- }
+ };
xhr.send('csrf_token=' + watched_data.csrf_token);
}
@@ -26,7 +27,7 @@ function mark_watched(target) {
function mark_unwatched(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
- var count = document.getElementById('count')
+ var count = document.getElementById('count');
count.innerText = count.innerText - 1;
var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' +
@@ -38,13 +39,13 @@ function mark_unwatched(target) {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
- if (xhr.readyState == 4) {
- if (xhr.status != 200) {
+ if (xhr.readyState === 4) {
+ if (xhr.status !== 200) {
count.innerText = count.innerText - 1 + 2;
tile.style.display = '';
}
}
- }
+ };
xhr.send('csrf_token=' + watched_data.csrf_token);
-} \ No newline at end of file
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index cd1df4ff..fa14a8e8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -39,7 +39,7 @@ services:
- invidious-db
invidious-db:
- image: docker.io/library/postgres:14
+ image: docker.io/library/postgres:13
restart: unless-stopped
volumes:
- postgresdata:/var/lib/postgresql/data
diff --git a/docker/Dockerfile b/docker/Dockerfile
index df35a179..1346f6eb 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,4 +1,4 @@
-FROM crystallang/crystal:1.2.2-alpine AS builder
+FROM crystallang/crystal:1.4.1-alpine AS builder
RUN apk add --no-cache sqlite-static yaml-static
ARG release
diff --git a/docker/Dockerfile.arm64 b/docker/Dockerfile.arm64
index 5f4d3793..75cab819 100644
--- a/docker/Dockerfile.arm64
+++ b/docker/Dockerfile.arm64
@@ -1,5 +1,5 @@
-FROM alpine:3.15 AS builder
-RUN apk add --no-cache 'crystal=1.2.2-r0' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev
+FROM alpine:edge AS builder
+RUN apk add --no-cache 'crystal=1.4.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev
ARG release
@@ -34,7 +34,7 @@ RUN if [ ${release} == 1 ] ; then \
--link-flags "-lxml2 -llzma"; \
fi
-FROM alpine:3.15
+FROM alpine:edge
RUN apk add --no-cache librsvg ttf-opensans
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \
diff --git a/locales/ar.json b/locales/ar.json
index 10ef200b..01c9bbb9 100644
--- a/locales/ar.json
+++ b/locales/ar.json
@@ -121,7 +121,7 @@
"Subscriptions": "الاشتراكات",
"search": "بحث",
"Log out": "تسجيل الخروج",
- "Released under the AGPLv3 on Github.": "صدر تحت AGPLv3 على Github.",
+ "Released under the AGPLv3 on Github.": "صدر تحت AGPLv3 على GitHub.",
"Source available here.": "الأكواد متوفرة هنا.",
"View JavaScript license information.": "مشاهدة معلومات حول تراخيص الجافاسكريبت.",
"View privacy policy.": "عرض سياسة الخصوصية.",
@@ -141,7 +141,6 @@
"Show less": "عرض اقل",
"Watch on YouTube": "مشاهدة الفيديو على اليوتيوب",
"Switch Invidious Instance": "تبديل المثيل Invidious",
- "Broken? Try another Invidious Instance": "معطل؟ جرب مثيل Invidious آخر",
"Hide annotations": "إخفاء الملاحظات في الفيديو",
"Show annotations": "عرض الملاحظات في الفيديو",
"Genre: ": "النوع: ",
@@ -329,39 +328,38 @@
"Videos": "الفيديوهات",
"Playlists": "قوائم التشغيل",
"Community": "المجتمع",
- "relevance": "ملاؤم",
- "rating": "تقييم",
- "date": "التاريخ",
- "views": "مشاهدات",
- "content_type": "نوع المحتوى",
- "duration": "المدة الزمنية",
- "features": "الميزات",
- "sort": "فرز",
- "hour": "آخر ساعة",
- "today": "اليوم",
- "week": "هذا الأسبوع",
- "month": "هذا الشهر",
- "year": "هذه السنة",
- "video": "فيديو",
- "channel": "قناة",
- "playlist": "قائمة التشغيل",
- "movie": "فيلم",
- "show": "عرض",
- "hd": "عالية الدقة",
- "subtitles": "ترجمات",
- "creative_commons": "المشاع الإبداعي",
- "3d": "ثلاثي الأبعاد",
- "live": "مباشر",
- "4k": "4k",
- "location": "الأماكن",
- "hdr": "وضع التباين العالي",
- "filter": "معامل الفرز",
+ "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_date_option_hour": "آخر ساعة",
+ "search_filters_date_option_today": "اليوم",
+ "search_filters_date_option_week": "هذا الأسبوع",
+ "search_filters_date_option_month": "هذا الشهر",
+ "search_filters_date_option_year": "هذه السنة",
+ "search_filters_type_option_video": "فيديو",
+ "search_filters_type_option_channel": "قناة",
+ "search_filters_type_option_playlist": "قائمة التشغيل",
+ "search_filters_type_option_movie": "فيلم",
+ "search_filters_type_option_show": "عرض",
+ "search_filters_features_option_hd": "عالية الدقة",
+ "search_filters_features_option_subtitles": "ترجمات",
+ "search_filters_features_option_c_commons": "المشاع الإبداعي",
+ "search_filters_features_option_three_d": "ثلاثي الأبعاد",
+ "search_filters_features_option_live": "مباشر",
+ "search_filters_features_option_four_k": "4k",
+ "search_filters_features_option_location": "الأماكن",
+ "search_filters_features_option_hdr": "وضع التباين العالي",
"Current version: ": "الإصدار الحالي: ",
"next_steps_error_message": "بعد ذلك يجب أن تحاول: ",
"next_steps_error_message_refresh": "تحديث",
"next_steps_error_message_go_to_youtube": "انتقل إلى يوتيوب",
- "short": "قصير (< 4 دقائق)",
- "long": "طويل (> 20 دقيقة)",
+ "search_filters_duration_option_short": "قصير (< 4 دقائق)",
+ "search_filters_duration_option_long": "طويل (> 20 دقيقة)",
"footer_source_code": "شفرة المصدر",
"footer_original_source_code": "كود المصدر الأصلي",
"footer_modfied_source_code": "شفرة المصدر المعدلة",
@@ -386,7 +384,7 @@
"preferences_quality_dash_option_360p": "360p",
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_144p": "144p",
- "purchased": "تم شراؤها",
+ "search_filters_features_option_purchased": "تم شراؤها",
"none": "لاشيء",
"videoinfo_started_streaming_x_ago": "بدأ البث منذ `x`",
"videoinfo_watch_on_youTube": "مشاهدة على يوتيوب",
@@ -395,7 +393,7 @@
"user_created_playlists": "'x' إنشاء قوائم التشغيل",
"user_saved_playlists": "قوائم التشغيل المحفوظة 'x'",
"Video unavailable": "الفيديو غير متوفر",
- "360": "360°",
+ "search_filters_features_option_three_sixty": "360°",
"download_subtitles": "ترجمات - 'x' (.vtt)",
"invidious": "الخيالي",
"preferences_save_player_pos_label": "حفظ موضع التشغيل: ",
@@ -459,5 +457,6 @@
"Portuguese (Brazil)": "البرتغالية (البرازيل)",
"Russian (auto-generated)": "الروسية (منشأة تلقائيا)",
"Spanish (Spain)": "الإسبانية (إسبانيا)",
- "crash_page_search_issue": "بحثت عن <a href=\"`x`\"> المشكلات الموجودة على Github </a>"
+ "crash_page_search_issue": "بحثت عن <a href=\"`x`\"> المشكلات الموجودة على GitHub </a>",
+ "search_filters_title": "معامل الفرز"
}
diff --git a/locales/ca.json b/locales/ca.json
index 1fa7cc1f..741414d2 100644
--- a/locales/ca.json
+++ b/locales/ca.json
@@ -52,16 +52,16 @@
"Download": "Descarrega",
"Download as: ": "Descarrega com: ",
"Videos": "Vídeos",
- "content_type": "Tipus",
- "duration": "Duració",
- "sort": "Ordena per",
- "week": "Aquesta setmana",
- "month": "Aquest mes",
- "year": "Aquest any",
- "video": "Vídeo",
- "channel": "Canal",
- "short": "Curt (< 4 minuts)",
- "long": "Llarg (> 20 minuts)",
+ "search_filters_type_label": "Tipus",
+ "search_filters_duration_label": "Duració",
+ "search_filters_sort_label": "Ordena per",
+ "search_filters_date_option_week": "Aquesta setmana",
+ "search_filters_date_option_month": "Aquest mes",
+ "search_filters_date_option_year": "Aquest any",
+ "search_filters_type_option_video": "Vídeo",
+ "search_filters_type_option_channel": "Canal",
+ "search_filters_duration_option_short": "Curt (< 4 minuts)",
+ "search_filters_duration_option_long": "Llarg (> 20 minuts)",
"Current version: ": "Versió actual: ",
"Malay": "Malai",
"Persian": "Persa",
@@ -93,11 +93,11 @@
"Spanish": "Castellà",
"Vietnamese": "Vietnamita",
"News": "Notícies",
- "show": "Mostra",
+ "search_filters_type_option_show": "Mostra",
"footer_documentation": "Documentació",
"Thai": "Tailandès",
"Music": "Música",
- "relevance": "Rellevància",
- "hour": "Última hora",
- "today": "Avui"
+ "search_filters_sort_option_relevance": "Rellevància",
+ "search_filters_date_option_hour": "Última hora",
+ "search_filters_date_option_today": "Avui"
}
diff --git a/locales/cs.json b/locales/cs.json
index 10dd685e..318866b1 100644
--- a/locales/cs.json
+++ b/locales/cs.json
@@ -137,7 +137,7 @@
"Show annotations": "Zobrazit poznámky",
"Genre: ": "Žánr: ",
"License: ": "Licence: ",
- "Family friendly? ": "Vhodné pro děti? ",
+ "Family friendly? ": "Vhodné pro rodiny? ",
"Engagement: ": "Zapojení: ",
"English": "Angličtina",
"English (auto-generated)": "Angličtina (automaticky generováno)",
@@ -262,35 +262,34 @@
"Video mode": "Videový režim",
"Videos": "Videa",
"Community": "Komunita",
- "rating": "Hodnocení",
- "date": "Datum zveřejnění",
- "views": "Počet zhlédnutí",
- "duration": "Délka",
- "hour": "Před hodinou",
- "today": "Dnes",
- "week": "Tento týden",
- "month": "Tento měsíc",
- "year": "Tento rok",
- "video": "Video",
- "channel": "Kanál",
- "playlist": "Playlist",
- "movie": "Film",
- "show": "Show",
- "hd": "HD",
- "subtitles": "Titulky",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "Živě",
- "4k": "4K",
- "location": "Umístění",
- "hdr": "HDR",
- "filter": "Filtr",
- "generic_count_days_0": "{{count}} den",
+ "search_filters_sort_option_rating": "Hodnocení",
+ "search_filters_sort_option_date": "Datum nahrání",
+ "search_filters_sort_option_views": "Počet zhlédnutí",
+ "search_filters_duration_label": "Délka",
+ "search_filters_date_option_hour": "Poslední hodina",
+ "search_filters_date_option_today": "Dnes",
+ "search_filters_date_option_week": "Tento týden",
+ "search_filters_date_option_month": "Tento měsíc",
+ "search_filters_date_option_year": "Tento rok",
+ "search_filters_type_option_video": "Video",
+ "search_filters_type_option_channel": "Kanál",
+ "search_filters_type_option_playlist": "Playlist",
+ "search_filters_type_option_movie": "Film",
+ "search_filters_type_option_show": "Seriál",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "Titulky",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "Živě",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "Umístění",
+ "search_filters_features_option_hdr": "HDR",
+ "generic_count_days_0": "{{count}} dnem",
"generic_count_days_1": "{{count}} dny",
- "generic_count_days_2": "{{count}} dní",
- "generic_count_hours_0": "{{count}} hodina",
- "generic_count_hours_1": "{{count}} hodiny",
- "generic_count_hours_2": "{{count}} hodin",
+ "generic_count_days_2": "{{count}} dny",
+ "generic_count_hours_0": "{{count}} hodinou",
+ "generic_count_hours_1": "{{count}} hodinami",
+ "generic_count_hours_2": "{{count}} hodinami",
"crash_page_refresh": "zkusili <a href=\"`x`\">obnovit stránku</a>",
"crash_page_switch_instance": "zkusili <a href=\"`x`\">použít jinou instanci</a>",
"preferences_vr_mode_label": "Interaktivní 360-stupňová videa (vyžaduje WebGL): ",
@@ -302,9 +301,9 @@
"Spanish (auto-generated)": "Španělština (automaticky generováno)",
"Spanish (Mexico)": "Španělština (Mexiko)",
"Spanish (Spain)": "Španělština (Španělsko)",
- "generic_count_years_0": "{{count}} rok",
- "generic_count_years_1": "{{count}} roky",
- "generic_count_years_2": "{{count}} let",
+ "generic_count_years_0": "{{count}} rokem",
+ "generic_count_years_1": "{{count}} lety",
+ "generic_count_years_2": "{{count}} lety",
"Fallback comments: ": "Záložní komentáře: ",
"Search": "Hledat",
"Top": "Nejlepší",
@@ -365,29 +364,24 @@
"Japanese (auto-generated)": "Japonština (automaticky generováno)",
"Korean (auto-generated)": "Korejština (automaticky generováno)",
"Russian (auto-generated)": "Ruština (automaticky generováno)",
- "generic_count_months_0": "{{count}} měsíc",
- "generic_count_months_1": "{{count}} měsíce",
- "generic_count_months_2": "{{count}} měsíců",
- "generic_count_weeks_0": "{{count}} týden",
+ "generic_count_months_0": "{{count}} měsícem",
+ "generic_count_months_1": "{{count}} měsíci",
+ "generic_count_months_2": "{{count}} měsíci",
+ "generic_count_weeks_0": "{{count}} týdnem",
"generic_count_weeks_1": "{{count}} týdny",
- "generic_count_weeks_2": "{{count}} týdnů",
- "generic_count_minutes_0": "{{count}} minuta",
- "generic_count_minutes_1": "{{count}} minuty",
- "generic_count_minutes_2": "{{count}} minut",
- "short": "Krátké (< 4 minuty)",
- "long": "Dlouhé (> 20 minut)",
+ "generic_count_weeks_2": "{{count}} týdny",
+ "generic_count_minutes_0": "{{count}} minutou",
+ "generic_count_minutes_1": "{{count}} minutami",
+ "generic_count_minutes_2": "{{count}} minutami",
"footer_documentation": "Dokumentace",
"next_steps_error_message_refresh": "Obnovit stránku",
"Chinese": "Čínština",
- "360": "360°",
"Dutch (auto-generated)": "Nizozemština (automaticky generováno)",
"Erroneous token": "Chybný token",
"tokens_count_0": "{{count}} token",
"tokens_count_1": "{{count}} tokeny",
"tokens_count_2": "{{count}} tokenů",
"Portuguese (Brazil)": "Portugalština (Brazílie)",
- "content_type": "Typ",
- "sort": "Řazení",
"Token is expired, please try again": "Token vypršel, zkuste to prosím znovu",
"English (United States)": "Angličtina (Spojené státy)",
"Cantonese (Hong Kong)": "Kantonština (Hong Kong)",
@@ -401,7 +395,6 @@
"%A %B %-d, %Y": "%A %B %-d, %Y",
"YouTube comment permalink": "Permanentní odkaz YouTube komentáře",
"permalink": "permalink",
- "purchased": "Zakoupeno",
"footer_original_source_code": "Původní zdrojový kód",
"adminprefs_modified_source_code_url_label": "URL repozitáře s upraveným zdrojovým kódem",
"Video unavailable": "Video není dostupné",
@@ -420,9 +413,9 @@
"preferences_quality_dash_option_worst": "Nejhorší",
"preferences_quality_dash_option_480p": "480p",
"Top enabled: ": "Povoleny nejlepší: ",
- "generic_count_seconds_0": "{{count}} sekunda",
- "generic_count_seconds_1": "{{count}} sekundy",
- "generic_count_seconds_2": "{{count}} sekund",
+ "generic_count_seconds_0": "{{count}} sekundou",
+ "generic_count_seconds_1": "{{count}} sekundami",
+ "generic_count_seconds_2": "{{count}} sekundami",
"preferences_save_player_pos_label": "Uložit pozici přehrávání: ",
"Incorrect password": "Nesprávné heslo",
"View as playlist": "Zobrazit jako playlist",
@@ -434,11 +427,9 @@
"Erroneous CAPTCHA": "Chybná CAPTCHA",
"Password is a required field": "Heslo je vyžadované pole",
"preferences_automatic_instance_redirect_label": "Automatické přesměrování instance (fallback na redirect.invidious.io): ",
- "Broken? Try another Invidious Instance": "Je něco rozbité? Zkuste jinou instanci Invidious",
"Switch Invidious Instance": "Přepnout instanci Invidious",
"Empty playlist": "Prázdný playlist",
"footer_source_code": "Zdrojový kód",
- "relevance": "Relevantnost",
"View YouTube comments": "Zobrazit YouTube komentáře",
"Blacklisted regions: ": "Oblasti na černé listině: ",
"Wrong username or password": "Nesprávné uživatelské jméno nebo heslo",
@@ -454,9 +445,8 @@
"Deleted or invalid channel": "Smazaný nebo neplatný kanál",
"This channel does not exist.": "Tento kanál neexistuje.",
"Hidden field \"token\" is a required field": "Skryté pole \"token\" je vyžadované",
- "features": "Funkce",
"Wilson score: ": "Skóre Wilson: ",
- "Shared `x`": "Sdíleno `x`",
+ "Shared `x`": "Zveřejněno `x`",
"Premieres in `x`": "Premiéra za `x`",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Zobrazit `x` komentář",
@@ -477,5 +467,24 @@
"Erroneous challenge": "Chybná výzva",
"Premieres `x`": "Premiéra `x`",
"CAPTCHA is a required field": "CAPTCHA je vyžadované pole",
- "`x` ago": "Před `x`"
+ "`x` ago": "Před `x`",
+ "search_message_change_filters_or_query": "Zkuste rozšířit vyhledávaný dotaz a/nebo změnit filtry.",
+ "search_filters_date_option_none": "Jakékoli datum",
+ "search_filters_date_label": "Datum nahrání",
+ "search_filters_type_option_all": "Jakýkoli typ",
+ "search_filters_duration_option_none": "Jakákoli délka",
+ "search_filters_type_label": "Typ",
+ "search_filters_duration_option_short": "Krátká (< 4 minuty)",
+ "search_message_no_results": "Nenalezeny žádné výsledky.",
+ "search_filters_title": "Filtry",
+ "search_filters_duration_option_medium": "Střední (4 - 20 minut)",
+ "search_filters_duration_option_long": "Dlouhá (> 20 minut)",
+ "search_message_use_another_instance": " Můžete také <a href=\"`x`\">hledat na jiné instanci</a>.",
+ "search_filters_features_label": "Vlastnosti",
+ "search_filters_features_option_three_sixty": "360°",
+ "search_filters_features_option_vr180": "VR180",
+ "search_filters_features_option_purchased": "Zakoupeno",
+ "search_filters_sort_label": "Řadit dle",
+ "search_filters_sort_option_relevance": "Relevantnost",
+ "search_filters_apply_button": "Použít vybrané filtry"
}
diff --git a/locales/da.json b/locales/da.json
index 92e4e9f9..4816c2c9 100644
--- a/locales/da.json
+++ b/locales/da.json
@@ -21,15 +21,15 @@
"No": "Nej",
"Import and Export Data": "Importer og Eksporter Data",
"Import": "Importer",
- "Import Invidious data": "Importer Invidious data",
- "Import YouTube subscriptions": "Importer YouTube abonnementer",
+ "Import Invidious data": "Importer Invidious JSON-data",
+ "Import YouTube subscriptions": "Importer YouTube/OPML-abonnementer",
"Import FreeTube subscriptions (.db)": "Importer FreeTube abonnementer (.db)",
"Import NewPipe subscriptions (.json)": "Importer NewPipe abonnementer (.json)",
"Import NewPipe data (.zip)": "Importer NewPipe data (.zip)",
"Export": "Exporter",
"Export subscriptions as OPML": "Exporter abonnementer som OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporter abonnementer som OPML (til NewPipe & FreeTube)",
- "Export data as JSON": "Exporter data som JSON",
+ "Export data as JSON": "Eksporter Invidious-data som JSON",
"Delete account?": "Slet konto?",
"History": "Historik",
"An alternative front-end to YouTube": "Et alternativt front-end til YouTube",
@@ -66,7 +66,7 @@
"preferences_related_videos_label": "Vis relaterede videoer: ",
"preferences_annotations_label": "Vis annotationer som standard: ",
"preferences_extend_desc_label": "Automatisk udvid videoens beskrivelse: ",
- "preferences_vr_mode_label": "Interaktiv 360 graders videoer: ",
+ "preferences_vr_mode_label": "Interaktive 360 graders videoer (kræver WebGL): ",
"preferences_category_visual": "Visuelle præferencer",
"preferences_player_style_label": "Afspiller stil: ",
"Dark mode: ": "Mørk tilstand: ",
@@ -202,7 +202,7 @@
"Hidden field \"challenge\" is a required field": "Det skjulte felt \"challenge\" er et påkrævet felt",
"Albanian": "Albansk",
"preferences_quality_dash_label": "Fortrukket DASH video kvalitet: ",
- "live": "Direkte",
+ "search_filters_features_option_live": "Direkte",
"Lao": "Lao-tse",
"Filipino": "Filippinsk",
"Greek": "Græsk",
@@ -213,23 +213,22 @@
"preferences_locale_label": "Sprog: ",
"News": "Nyheder",
"permalink": "permalink",
- "date": "Upload dato",
- "features": "Funktioner",
- "filter": "Filter",
+ "search_filters_sort_option_date": "Upload dato",
+ "search_filters_features_label": "Funktioner",
"Khmer": "Khmer",
"Finnish": "Finsk",
- "week": "Denne uge",
+ "search_filters_date_option_week": "Denne uge",
"Korean": "Koreansk",
"Telugu": "Telugu",
"Malayalam": "Malayalam",
"View as playlist": "Se som spilleliste",
"Hungarian": "Ungarsk",
"Welsh": "Walisisk",
- "subtitles": "Undertekster/CC",
+ "search_filters_features_option_subtitles": "Undertekster/CC",
"Bosnian": "Bosnisk",
"Yiddish": "Jiddisch",
"Belarusian": "Belarussisk",
- "today": "I dag",
+ "search_filters_date_option_today": "I dag",
"Shona": "Shona",
"Slovenian": "Slovensk",
"Gaming": "Gaming",
@@ -246,35 +245,35 @@
"footer_documentation": "Dokumentation",
"Pashto": "Pashto",
"footer_modfied_source_code": "Modificeret Kildekode",
- "Released under the AGPLv3 on Github.": "Udgivet under AGPLv3 på Github.",
+ "Released under the AGPLv3 on Github.": "Udgivet under AGPLv3 på GitHub.",
"Tajik": "Tadsjikisk",
- "month": "Denne måned",
+ "search_filters_date_option_month": "Denne måned",
"Hebrew": "Hebraisk",
"Kannada": "Kannada",
"Current version: ": "Nuværende version: ",
"Amharic": "Amharisk",
"Swedish": "Svensk",
"Corsican": "Korsikansk",
- "movie": "Film",
+ "search_filters_type_option_movie": "Film",
"Could not pull trending pages.": "Kunne ikke hente trending sider.",
"English": "Engelsk",
- "hd": "HD",
+ "search_filters_features_option_hd": "HD",
"Hausa": "Islandsk",
- "year": "Dette år",
+ "search_filters_date_option_year": "Dette år",
"Japanese": "Japansk",
- "content_type": "Type",
+ "search_filters_type_label": "Type",
"Icelandic": "Islandsk",
"Basque": "Baskisk",
- "rating": "Bedømmelse",
+ "search_filters_sort_option_rating": "Bedømmelse",
"Yoruba": "Yoruba",
"Erroneous token": "Fejlagtig token",
"Videos": "Videoer",
- "show": "Vis",
+ "search_filters_type_option_show": "Vis",
"Luxembourgish": "Luxemboursk",
"Vietnamese": "Vietnamesisk",
"Latvian": "Lettisk",
"Indonesian": "Indonesisk",
- "duration": "Varighed",
+ "search_filters_duration_label": "Varighed",
"footer_original_source_code": "Original kildekode",
"Search": "Søg",
"Serbian": "Serbisk",
@@ -289,8 +288,8 @@
"Rating: ": "Bedømmelse: ",
"Movies": "Film",
"YouTube comment permalink": "Youtube kommentarer permalink",
- "location": "Lokation",
- "hdr": "HDR",
+ "search_filters_features_option_location": "Lokation",
+ "search_filters_features_option_hdr": "HDR",
"Cebuano": "Cebuano (Sugbuanon)",
"Nyanja": "Nyanja",
"Chinese (Simplified)": "Kinesisk (forenklet)",
@@ -306,11 +305,11 @@
"German": "Tysk",
"Maori": "Maori",
"Slovak": "Slovakisk",
- "relevance": "Relevans",
- "hour": "Sidste time",
- "playlist": "Spilleliste",
- "long": "Lang (> 20 minutter)",
- "creative_commons": "Creative Commons",
+ "search_filters_sort_option_relevance": "Relevans",
+ "search_filters_date_option_hour": "Sidste time",
+ "search_filters_type_option_playlist": "Spilleliste",
+ "search_filters_duration_option_long": "Lang (> 20 minutter)",
+ "search_filters_features_option_c_commons": "Creative Commons",
"Marathi": "Marathi",
"Sindhi": "Sindhi",
"preferences_category_misc": "Diverse indstillinger",
@@ -327,8 +326,8 @@
"Western Frisian": "Vestfrisisk",
"Top": "Top",
"Music": "Musik",
- "views": "Antal visninger",
- "sort": "Sorter efter",
+ "search_filters_sort_option_views": "Antal visninger",
+ "search_filters_sort_label": "Sorter efter",
"Zulu": "Zulu",
"Invidious Private Feed for `x`": "Invidious Privat Feed til `x`",
"English (auto-generated)": "Engelsk (autogenereret)",
@@ -349,7 +348,6 @@
"next_steps_error_message": "Efter det burde du prøve at: ",
"Sinhala": "Singalesisk (Sinhala)",
"Thai": "Thai",
- "Broken? Try another Invidious Instance": "I stykker? Prøv en anden Invidious instans",
"No such user": "Brugeren findes ikke",
"Token is expired, please try again": "Token er udløbet, prøv igen",
"Catalan": "Catalansk",
@@ -359,16 +357,16 @@
"Scottish Gaelic": "Skotsk Gælisk",
"Default": "Standard",
"Video mode": "Videotilstand",
- "short": "Kort (< 4 minutter)",
+ "search_filters_duration_option_short": "Kort (< 4 minutter)",
"Hidden field \"token\" is a required field": "Det skjulte felt \"token\" er et påkrævet felt",
"Azerbaijani": "Aserbajdsjansk",
"Georgian": "Georgisk",
"Italian": "Italiensk",
"Audio mode": "Lydtilstand",
- "video": "Video",
- "channel": "Kanal",
- "3d": "3D",
- "4k": "4K",
+ "search_filters_type_option_video": "Video",
+ "search_filters_type_option_channel": "Kanal",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_four_k": "4K",
"Hmong": "Hmong",
"preferences_quality_option_medium": "Medium",
"preferences_quality_option_small": "Lille",
@@ -381,8 +379,8 @@
"preferences_quality_dash_option_360p": "360p",
"preferences_quality_dash_option_144p": "144p",
"invidious": "Invidious",
- "purchased": "Købt",
- "360": "360°",
+ "search_filters_features_option_purchased": "Købt",
+ "search_filters_features_option_three_sixty": "360°",
"none": "ingen",
"videoinfo_started_streaming_x_ago": "Streamen blev startet for `x`siden",
"videoinfo_watch_on_youTube": "Se på YouTube",
@@ -392,11 +390,75 @@
"user_created_playlists": "`x`opretede spillelister",
"user_saved_playlists": "´x`gemte spillelister",
"Video unavailable": "Video ikke tilgængelig",
- "preferences_save_player_pos_label": "Gem den nuværende videotid: ",
+ "preferences_save_player_pos_label": "Gem afspilningsposition: ",
"preferences_quality_dash_option_auto": "Auto",
"preferences_quality_option_hd720": "HD720",
"preferences_quality_dash_option_2160p": "2160p",
"preferences_quality_option_dash": "DASH (adaptiv kvalitet)",
"preferences_quality_dash_option_1440p": "1440p",
- "preferences_quality_dash_option_240p": "240p"
+ "preferences_quality_dash_option_240p": "240p",
+ "subscriptions_unseen_notifs_count": "{{count}} uset notifikation",
+ "subscriptions_unseen_notifs_count_plural": "{{count}} usete notifikationer",
+ "comments_view_x_replies": "Vis {{count}} svar",
+ "comments_view_x_replies_plural": "Vis {{count}} svar",
+ "comments_points_count": "{{count}} point",
+ "comments_points_count_plural": "{{count}} point",
+ "generic_count_years": "{{count}} år",
+ "generic_count_years_plural": "{{count}} år",
+ "generic_count_months": "{{count}} måned",
+ "generic_count_months_plural": "{{count}} måneder",
+ "generic_count_days": "{{count}} dag",
+ "generic_count_days_plural": "{{count}} dage",
+ "generic_count_minutes": "{{count}} minut",
+ "generic_count_minutes_plural": "{{count}} minutter",
+ "generic_count_seconds": "{{count}} sekund",
+ "generic_count_seconds_plural": "{{count}} sekunder",
+ "generic_subscribers_count": "{{count}} abonnent",
+ "generic_subscribers_count_plural": "{{count}} abonnenter",
+ "generic_subscriptions_count": "{{count}} abonnement",
+ "generic_subscriptions_count_plural": "{{count}} abonnementer",
+ "generic_videos_count": "{{count}} video",
+ "generic_videos_count_plural": "{{count}} videoer",
+ "English (United States)": "Engelsk (USA)",
+ "French (auto-generated)": "Fransk (autogenereret)",
+ "Spanish (auto-generated)": "Spansk (autogenereret)",
+ "crash_page_before_reporting": "Før du rapporterer en fejl, skal du sikre dig, at du har:",
+ "crash_page_refresh": "forsøgte at <a href=\"`x`\">opdatere siden</a>",
+ "generic_playlists_count": "{{count}} spilleliste",
+ "generic_playlists_count_plural": "{{count}} spillelister",
+ "preferences_watch_history_label": "Aktiver afspilningshistorik: ",
+ "tokens_count": "{{count}} token",
+ "tokens_count_plural": "{{count}} tokens",
+ "Cantonese (Hong Kong)": "Kantonesisk (Hongkong)",
+ "Chinese": "Kinesisk",
+ "Chinese (China)": "Kinesisk (Kina)",
+ "Chinese (Hong Kong)": "Kinesisk (Hongkong)",
+ "Chinese (Taiwan)": "Kinesisk (Taiwan)",
+ "Dutch (auto-generated)": "Hollandsk (autogenereret)",
+ "Indonesian (auto-generated)": "Indonesisk (autogenereret)",
+ "Interlingue": "Interlingue",
+ "Japanese (auto-generated)": "Japansk (autogenereret)",
+ "Korean (auto-generated)": "Koreansk (autogenereret)",
+ "Russian (auto-generated)": "Russisk (autogenereret)",
+ "Turkish (auto-generated)": "Tyrkisk (autogenereret)",
+ "Vietnamese (auto-generated)": "Vietnamesisk (autogenereret)",
+ "crash_page_report_issue": "Hvis intet af ovenstående hjalp, bedes du <a href=\"`x`\">åbne et nyt problem på GitHub</a> (helst på engelsk) og inkludere følgende tekst i din besked (oversæt IKKE denne tekst):",
+ "English (United Kingdom)": "Engelsk (Storbritannien)",
+ "Italian (auto-generated)": "Italiensk (autogenereret)",
+ "Portuguese (auto-generated)": "Portugisisk (autogenereret)",
+ "Portuguese (Brazil)": "Portugisisk (Brasilien)",
+ "generic_views_count": "{{count}} visning",
+ "generic_views_count_plural": "{{count}} visninger",
+ "generic_count_hours": "{{count}} time",
+ "generic_count_hours_plural": "{{count}} timer",
+ "Spanish (Spain)": "Spansk (Spanien)",
+ "crash_page_switch_instance": "forsøgte at <a href=\"`x`\">bruge en anden instans</a>",
+ "German (auto-generated)": "Tysk (autogenereret)",
+ "Spanish (Mexico)": "Spansk (Mexico)",
+ "generic_count_weeks": "{{count}} uge",
+ "generic_count_weeks_plural": "{{count}} uger",
+ "crash_page_you_found_a_bug": "Det ser ud til, at du har fundet en fejl i Invidious!",
+ "crash_page_read_the_faq": "læs <a href=\"`x`\">Ofte stillede spørgsmål (FAQ)</a>",
+ "crash_page_search_issue": "søgte efter <a href=\"`x`\">eksisterende problemer på GitHub</a>",
+ "search_filters_title": "Filter"
}
diff --git a/locales/de.json b/locales/de.json
index 665810a4..24b83bb3 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -21,15 +21,15 @@
"No": "Nein",
"Import and Export Data": "Daten importieren und exportieren",
"Import": "Importieren",
- "Import Invidious data": "Invidious Daten importieren",
- "Import YouTube subscriptions": "YouTube Abonnements importieren",
+ "Import Invidious data": "Invidious-JSON-Daten importieren",
+ "Import YouTube subscriptions": "YouTube-/OPML-Abonnements importieren",
"Import FreeTube subscriptions (.db)": "FreeTube Abonnements importieren (.db)",
"Import NewPipe subscriptions (.json)": "NewPipe Abonnements importieren (.json)",
"Import NewPipe data (.zip)": "NewPipe Daten importieren (.zip)",
"Export": "Exportieren",
"Export subscriptions as OPML": "Abonnements als OPML exportieren",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Abonnements als OPML exportieren (für NewPipe & FreeTube)",
- "Export data as JSON": "Daten als JSON exportieren",
+ "Export data as JSON": "Invidious-Daten als JSON exportieren",
"Delete account?": "Konto löschen?",
"History": "Verlauf",
"An alternative front-end to YouTube": "Eine alternative Oberfläche für YouTube",
@@ -51,10 +51,10 @@
"preferences_category_player": "Wiedergabeeinstellungen",
"preferences_video_loop_label": "Immer wiederholen: ",
"preferences_autoplay_label": "Automatisch abspielen: ",
- "preferences_continue_label": "Immer automatisch nächstes Video spielen: ",
- "preferences_continue_autoplay_label": "nächstes Video automatisch abspielen: ",
+ "preferences_continue_label": "Immer automatisch nächstes Video abspielen: ",
+ "preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ",
"preferences_listen_label": "Nur Ton als Standard: ",
- "preferences_local_label": "Proxy-Videos: ",
+ "preferences_local_label": "Videos durch Proxy leiten: ",
"preferences_speed_label": "Standardgeschwindigkeit: ",
"preferences_quality_label": "Bevorzugte Videoqualität: ",
"preferences_volume_label": "Wiedergabelautstärke: ",
@@ -63,12 +63,12 @@
"reddit": "Reddit",
"preferences_captions_label": "Standarduntertitel: ",
"Fallback captions: ": "Ersatzuntertitel: ",
- "preferences_related_videos_label": "Ähnliche Videos anzeigen? ",
- "preferences_annotations_label": "Standardmäßig Anmerkungen anzeigen? ",
+ "preferences_related_videos_label": "Ähnliche Videos anzeigen: ",
+ "preferences_annotations_label": "Anmerkungen standardmäßig anzeigen: ",
"preferences_extend_desc_label": "Videobeschreibung automatisch erweitern: ",
- "preferences_vr_mode_label": "Interaktive 360 Grad Videos: ",
+ "preferences_vr_mode_label": "Interaktive 360-Grad-Videos (erfordert WebGL): ",
"preferences_category_visual": "Anzeigeeinstellungen",
- "preferences_player_style_label": "Abspielgeräterstil: ",
+ "preferences_player_style_label": "Player-Stil: ",
"Dark mode: ": "Nachtmodus: ",
"preferences_dark_mode_label": "Modus: ",
"dark": "Nachtmodus",
@@ -121,7 +121,7 @@
"Subscriptions": "Abonnements",
"search": "Suchen",
"Log out": "Abmelden",
- "Released under the AGPLv3 on Github.": "Auf Github unter der AGPLv3 Lizenz veröffentlicht.",
+ "Released under the AGPLv3 on Github.": "Auf GitHub unter der AGPLv3 Lizenz veröffentlicht.",
"Source available here.": "Quellcode verfügbar hier.",
"View JavaScript license information.": "Javascript Lizenzinformationen anzeigen.",
"View privacy policy.": "Datenschutzerklärung einsehen.",
@@ -141,7 +141,6 @@
"Show less": "Weniger anzeigen",
"Watch on YouTube": "Video auf YouTube ansehen",
"Switch Invidious Instance": "Invidious Instanz wechseln",
- "Broken? Try another Invidious Instance": "Kaputt? Versuche eine andere Invidious Instanz",
"Hide annotations": "Anmerkungen ausblenden",
"Show annotations": "Anmerkungen anzeigen",
"Genre: ": "Genre: ",
@@ -329,45 +328,44 @@
"Videos": "Videos",
"Playlists": "Wiedergabelisten",
"Community": "Gemeinschaft",
- "relevance": "Relevanz",
- "rating": "Bewertung",
- "date": "Datum",
- "views": "Aufrufe",
- "content_type": "Inhaltstyp",
- "duration": "Dauer",
- "features": "Eigenschaften",
- "sort": "sortieren",
- "hour": "Letzte Stunde",
- "today": "Heute",
- "week": "Diese Woche",
- "month": "Diesen Monat",
- "year": "Dieses Jahr",
- "video": "Video",
- "channel": "Kanal",
- "playlist": "Wiedergabeliste",
- "movie": "Film",
- "show": "anzeigen",
- "hd": "HD",
- "subtitles": "Untertitel / CC",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "Live",
- "4k": "4K",
- "location": "Standort",
- "hdr": "HDR",
- "filter": "Filtern",
+ "search_filters_sort_option_relevance": "Relevanz",
+ "search_filters_sort_option_rating": "Bewertung",
+ "search_filters_sort_option_date": "Datum",
+ "search_filters_sort_option_views": "Aufrufe",
+ "search_filters_type_label": "Inhaltstyp",
+ "search_filters_duration_label": "Dauer",
+ "search_filters_features_label": "Eigenschaften",
+ "search_filters_sort_label": "sortieren",
+ "search_filters_date_option_hour": "Letzte Stunde",
+ "search_filters_date_option_today": "Heute",
+ "search_filters_date_option_week": "Diese Woche",
+ "search_filters_date_option_month": "Diesen Monat",
+ "search_filters_date_option_year": "Dieses Jahr",
+ "search_filters_type_option_video": "Video",
+ "search_filters_type_option_channel": "Kanal",
+ "search_filters_type_option_playlist": "Wiedergabeliste",
+ "search_filters_type_option_movie": "Film",
+ "search_filters_type_option_show": "Anzeigen",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "Untertitel / CC",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "Live",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "Standort",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Aktuelle Version: ",
"next_steps_error_message": "Danach folgendes versuchen: ",
"next_steps_error_message_refresh": "Aktualisieren",
"next_steps_error_message_go_to_youtube": "Zu YouTube gehen",
"footer_donate_page": "Spende",
- "long": "Lang (> 20 Minuten)",
+ "search_filters_duration_option_long": "Lang (> 20 Minuten)",
"footer_original_source_code": "Original Quellcode",
"footer_modfied_source_code": "Modifizierter Quellcode",
"footer_documentation": "Dokumentation",
"footer_source_code": "Quellcode",
"adminprefs_modified_source_code_url_label": "URL zum Repositorie des modifizierten Quellcodes",
- "short": "Kurz (< 4 Minuten)",
+ "search_filters_duration_option_short": "Kurz (< 4 Minuten)",
"preferences_region_label": "Land der Inhalte: ",
"preferences_quality_option_dash": "DASH (automatische Qualität)",
"preferences_quality_option_hd720": "HD720",
@@ -388,13 +386,13 @@
"Video unavailable": "Video nicht verfügbar",
"user_created_playlists": "`x` Wiedergabelisten erstellt",
"user_saved_playlists": "`x` Wiedergabelisten gespeichert",
- "preferences_save_player_pos_label": "Aktuelle Position im Video speichern: ",
- "360": "360°",
+ "preferences_save_player_pos_label": "Wiedergabeposition speichern: ",
+ "search_filters_features_option_three_sixty": "360°",
"preferences_quality_dash_option_best": "Höchste",
"preferences_quality_dash_option_worst": "Niedrigste",
"preferences_quality_dash_option_1440p": "1440p",
"videoinfo_youTube_embed_link": "Eingebettet",
- "purchased": "Gekauft",
+ "search_filters_features_option_purchased": "Gekauft",
"none": "keine",
"videoinfo_started_streaming_x_ago": "Stream begann vor `x`",
"videoinfo_watch_on_youTube": "Auf YouTube ansehen",
@@ -421,7 +419,7 @@
"generic_count_minutes": "{{count}} Minute",
"generic_count_minutes_plural": "{{count}} Minuten",
"crash_page_read_the_faq": "Das <a href=\"`x`\">FAQ</a> gelesen haben",
- "crash_page_search_issue": "Nach <a href=\"`x`\">bereits gemeldeten Bugs auf Github</a> gesucht haben",
+ "crash_page_search_issue": "Nach <a href=\"`x`\">bereits gemeldeten Bugs auf GitHub</a> gesucht haben",
"crash_page_report_issue": "Wenn all dies nicht geholfen hat, <a href=\"`x`\">öffnen Sie bitte ein neues Problem (issue) auf Github</a> (vorzugsweise auf Englisch) und fügen Sie den folgenden Text in Ihre Nachricht ein (bitte übersetzen Sie diesen Text NICHT):",
"generic_views_count": "{{count}} Aufruf",
"generic_views_count_plural": "{{count}} Aufrufe",
@@ -435,5 +433,32 @@
"comments_points_count_plural": "{{count}} Punkte",
"crash_page_you_found_a_bug": "Anscheinend haben Sie einen Fehler in Invidious gefunden!",
"generic_count_months": "{{count}} Monat",
- "generic_count_months_plural": "{{count}} Monate"
+ "generic_count_months_plural": "{{count}} Monate",
+ "Cantonese (Hong Kong)": "Kantonesisch (Hong Kong)",
+ "Chinese (Hong Kong)": "Chinesisch (Hong Kong)",
+ "generic_playlists_count": "{{count}} Wiedergabeliste",
+ "generic_playlists_count_plural": "{{count}} Wiedergabelisten",
+ "preferences_watch_history_label": "Wiedergabeverlauf aktivieren: ",
+ "English (United Kingdom)": "Englisch (Vereinigtes Königreich)",
+ "English (United States)": "Englisch (Vereinigte Staaten)",
+ "Dutch (auto-generated)": "Niederländisch (automatisch generiert)",
+ "French (auto-generated)": "Französisch (automatisch generiert)",
+ "German (auto-generated)": "Deutsch (automatisch generiert)",
+ "Indonesian (auto-generated)": "Indonesisch (automatisch generiert)",
+ "Interlingue": "Interlingue",
+ "Italian (auto-generated)": "Italienisch (automatisch generiert)",
+ "Japanese (auto-generated)": "Japanisch (automatisch generiert)",
+ "Spanish (Mexico)": "Spanisch (Mexiko)",
+ "Spanish (Spain)": "Spanisch (Spanien)",
+ "Vietnamese (auto-generated)": "Vietnamesisch (automatisch generiert)",
+ "Russian (auto-generated)": "Russisch (automatisch generiert)",
+ "Chinese": "Chinesisch",
+ "Portuguese (Brazil)": "Portugiesisch (Brasilien)",
+ "Spanish (auto-generated)": "Spanisch (automatisch generiert)",
+ "Turkish (auto-generated)": "Türkisch (automatisch generiert)",
+ "Chinese (China)": "Chinesisch (China)",
+ "Chinese (Taiwan)": "Chinesisch (Taiwan)",
+ "Korean (auto-generated)": "Koreanisch (automatisch generiert)",
+ "Portuguese (auto-generated)": "Portugiesisch (automatisch generiert)",
+ "search_filters_title": "Filtern"
}
diff --git a/locales/el.json b/locales/el.json
index 24e42153..048a520b 100644
--- a/locales/el.json
+++ b/locales/el.json
@@ -358,7 +358,7 @@
"crash_page_before_reporting": "Πριν αναφέρετε ένα σφάλμα, βεβαιωθείτε ότι έχετε:",
"crash_page_refresh": "προσπαθήσει να <a href=\"`x`\">ανανεώσετε τη σελίδα</a>",
"crash_page_read_the_faq": "διαβάσει τις <a href=\"`x`\">Συχνές Ερωτήσεις (ΣΕ)</a>",
- "crash_page_search_issue": "αναζητήσει για <a href=\"`x`\">υπάρχοντα θέματα στο Github</a>",
+ "crash_page_search_issue": "αναζητήσει για <a href=\"`x`\">υπάρχοντα θέματα στο GitHub</a>",
"generic_views_count": "{{count}} προβολή",
"generic_views_count_plural": "{{count}} προβολές",
"generic_videos_count": "{{count}} βίντεο",
@@ -373,51 +373,50 @@
"preferences_region_label": "Χώρα περιεχομένου: ",
"preferences_category_misc": "Διάφορες προτιμήσεις",
"Show more": "Εμφάνιση περισσότερων",
- "today": "Σήμερα",
- "360": "360°",
+ "search_filters_date_option_today": "Σήμερα",
+ "search_filters_features_option_three_sixty": "360°",
"videoinfo_started_streaming_x_ago": "Ξεκίνησε η ροή `x` πριν από",
"videoinfo_watch_on_youTube": "Παρακολουθήστε στο YouTube",
"download_subtitles": "Υπότιτλοι - `x` (.vtt)",
"user_created_playlists": "`x` δημιουργημένες λίστες αναπαραγωγής",
"user_saved_playlists": "`x` αποθηκευμένες λίστες αναπαραγωγής",
- "rating": "Αξιολόγηση",
- "relevance": "Συνάφεια",
- "purchased": "Αγορασμένο",
- "date": "Ημερομηνία μεταφόρτωσης",
- "content_type": "Τύπος",
- "duration": "Διάρκεια",
- "week": "Αυτή την εβδομάδα",
- "year": "Φέτος",
- "channel": "Κανάλι",
- "playlist": "Λίστα αναπαραγωγής",
- "long": "Μεγάλο (> 20 λεπτά)",
- "hd": "HD",
- "location": "Τοποθεσία",
- "3d": "3D",
+ "search_filters_sort_option_rating": "Αξιολόγηση",
+ "search_filters_sort_option_relevance": "Συνάφεια",
+ "search_filters_features_option_purchased": "Αγορασμένο",
+ "search_filters_sort_option_date": "Ημερομηνία μεταφόρτωσης",
+ "search_filters_type_label": "Τύπος",
+ "search_filters_duration_label": "Διάρκεια",
+ "search_filters_date_option_week": "Αυτή την εβδομάδα",
+ "search_filters_date_option_year": "Φέτος",
+ "search_filters_type_option_channel": "Κανάλι",
+ "search_filters_type_option_playlist": "Λίστα αναπαραγωγής",
+ "search_filters_duration_option_long": "Μεγάλο (> 20 λεπτά)",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_location": "Τοποθεσία",
+ "search_filters_features_option_three_d": "3D",
"next_steps_error_message": "Μετά από αυτό θα πρέπει να προσπαθήσετε να: ",
"next_steps_error_message_go_to_youtube": "Μεταβείτε στο YouTube",
"footer_donate_page": "Δωρεά",
"footer_original_source_code": "Πρωτότυπος πηγαίος κώδικας",
"preferences_show_nick_label": "Εμφάνιση ψευδώνυμου στην κορυφή: ",
- "hour": "Τελευταία ώρα",
+ "search_filters_date_option_hour": "Τελευταία ώρα",
"adminprefs_modified_source_code_url_label": "URL σε αποθετήριο τροποποιημένου πηγαίου κώδικα",
- "subtitles": "Υπότιτλοι/CC",
- "month": "Αυτόν τον μήνα",
- "Released under the AGPLv3 on Github.": "Κυκλοφορεί υπό την AGPLv3 στο Github.",
- "sort": "Ταξινόμηση κατά",
- "filter": "Φίλτρο",
- "movie": "Ταινία",
+ "search_filters_features_option_subtitles": "Υπότιτλοι/CC",
+ "search_filters_date_option_month": "Αυτόν τον μήνα",
+ "Released under the AGPLv3 on Github.": "Κυκλοφορεί υπό την AGPLv3 στο GitHub.",
+ "search_filters_sort_label": "Ταξινόμηση κατά",
+ "search_filters_type_option_movie": "Ταινία",
"footer_modfied_source_code": "Τροποποιημένος πηγαίος κώδικας",
- "features": "Χαρακτηριστικά",
- "4k": "4K",
+ "search_filters_features_label": "Χαρακτηριστικά",
+ "search_filters_features_option_four_k": "4K",
"footer_documentation": "Τεκμηρίωση",
- "short": "Σύντομο (< 4 λεπτά)",
+ "search_filters_duration_option_short": "Σύντομο (< 4 λεπτά)",
"next_steps_error_message_refresh": "Ανανέωση",
- "video": "Βίντεο",
- "live": "Ζωντανά",
- "creative_commons": "Creative Commons",
+ "search_filters_type_option_video": "Βίντεο",
+ "search_filters_features_option_live": "Ζωντανά",
+ "search_filters_features_option_c_commons": "Creative Commons",
"Search": "Αναζήτηση",
- "hdr": "HDR",
+ "search_filters_features_option_hdr": "HDR",
"preferences_extend_desc_label": "Αυτόματη επέκταση της περιγραφής του βίντεο: ",
"preferences_vr_mode_label": "Διαδραστικά βίντεο 360 μοιρών (απαιτεί WebGL): ",
"Show less": "Εμφάνιση λιγότερων",
@@ -448,6 +447,7 @@
"none": "κανένα",
"videoinfo_youTube_embed_link": "Ενσωμάτωση",
"videoinfo_invidious_embed_link": "Σύνδεσμος Ενσωμάτωσης",
- "show": "Μπάρα προόδου διαβάσματος",
- "preferences_watch_history_label": "Ενεργοποίηση ιστορικού παρακολούθησης: "
+ "search_filters_type_option_show": "Μπάρα προόδου διαβάσματος",
+ "preferences_watch_history_label": "Ενεργοποίηση ιστορικού παρακολούθησης: ",
+ "search_filters_title": "Φίλτρο"
}
diff --git a/locales/en-US.json b/locales/en-US.json
index a78d8062..c57670fc 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -155,7 +155,7 @@
"subscriptions_unseen_notifs_count_plural": "{{count}} unseen notifications",
"search": "search",
"Log out": "Log out",
- "Released under the AGPLv3 on Github.": "Released under the AGPLv3 on Github.",
+ "Released under the AGPLv3 on Github.": "Released under the AGPLv3 on GitHub.",
"Source available here.": "Source available here.",
"View JavaScript license information.": "View JavaScript license information.",
"View privacy policy.": "View privacy policy.",
@@ -175,7 +175,9 @@
"Show less": "Show less",
"Watch on YouTube": "Watch on YouTube",
"Switch Invidious Instance": "Switch Invidious Instance",
- "Broken? Try another Invidious Instance": "Broken? Try another Invidious Instance",
+ "search_message_no_results": "No results found.",
+ "search_message_change_filters_or_query": "Try widening your search query and/or changing the filters.",
+ "search_message_use_another_instance": " You can also <a href=\"`x`\">search on another instance</a>.",
"Hide annotations": "Hide annotations",
"Show annotations": "Show annotations",
"Genre: ": "Genre: ",
@@ -404,37 +406,44 @@
"Videos": "Videos",
"Playlists": "Playlists",
"Community": "Community",
- "relevance": "Relevance",
- "rating": "Rating",
- "date": "Upload date",
- "views": "View count",
- "content_type": "Type",
- "duration": "Duration",
- "features": "Features",
- "sort": "Sort By",
- "hour": "Last Hour",
- "today": "Today",
- "week": "This week",
- "month": "This month",
- "year": "This year",
- "video": "Video",
- "channel": "Channel",
- "playlist": "Playlist",
- "movie": "Movie",
- "show": "Show",
- "short": "Short (< 4 minutes)",
- "long": "Long (> 20 minutes)",
- "hd": "HD",
- "subtitles": "Subtitles/CC",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "Live",
- "4k": "4K",
- "location": "Location",
- "hdr": "HDR",
- "purchased": "Purchased",
- "360": "360°",
- "filter": "Filter",
+ "search_filters_title": "Filters",
+ "search_filters_date_label": "Upload date",
+ "search_filters_date_option_none": "Any date",
+ "search_filters_date_option_hour": "Last Hour",
+ "search_filters_date_option_today": "Today",
+ "search_filters_date_option_week": "This week",
+ "search_filters_date_option_month": "This month",
+ "search_filters_date_option_year": "This year",
+ "search_filters_type_label": "Type",
+ "search_filters_type_option_all": "Any type",
+ "search_filters_type_option_video": "Video",
+ "search_filters_type_option_channel": "Channel",
+ "search_filters_type_option_playlist": "Playlist",
+ "search_filters_type_option_movie": "Movie",
+ "search_filters_type_option_show": "Show",
+ "search_filters_duration_label": "Duration",
+ "search_filters_duration_option_none": "Any duration",
+ "search_filters_duration_option_short": "Short (< 4 minutes)",
+ "search_filters_duration_option_medium": "Medium (4 - 20 minutes)",
+ "search_filters_duration_option_long": "Long (> 20 minutes)",
+ "search_filters_features_label": "Features",
+ "search_filters_features_option_live": "Live",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "Subtitles/CC",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_sixty": "360°",
+ "search_filters_features_option_vr180": "VR180",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_hdr": "HDR",
+ "search_filters_features_option_location": "Location",
+ "search_filters_features_option_purchased": "Purchased",
+ "search_filters_sort_label": "Sort By",
+ "search_filters_sort_option_relevance": "Relevance",
+ "search_filters_sort_option_rating": "Rating",
+ "search_filters_sort_option_date": "Upload Date",
+ "search_filters_sort_option_views": "View count",
+ "search_filters_apply_button": "Apply selected filters",
"Current version: ": "Current version: ",
"next_steps_error_message": "After which you should try to: ",
"next_steps_error_message_refresh": "Refresh",
@@ -460,6 +469,6 @@
"crash_page_refresh": "tried to <a href=\"`x`\">refresh the page</a>",
"crash_page_switch_instance": "tried to <a href=\"`x`\">use another instance</a>",
"crash_page_read_the_faq": "read the <a href=\"`x`\">Frequently Asked Questions (FAQ)</a>",
- "crash_page_search_issue": "searched for <a href=\"`x`\">existing issues on Github</a>",
+ "crash_page_search_issue": "searched for <a href=\"`x`\">existing issues on GitHub</a>",
"crash_page_report_issue": "If none of the above helped, please <a href=\"`x`\">open a new issue on GitHub</a> (preferably in English) and include the following text in your message (do NOT translate that text):"
}
diff --git a/locales/eo.json b/locales/eo.json
index e7a8453e..40ab5f39 100644
--- a/locales/eo.json
+++ b/locales/eo.json
@@ -121,7 +121,7 @@
"Subscriptions": "Abonoj",
"search": "serĉi",
"Log out": "Elsaluti",
- "Released under the AGPLv3 on Github.": "Eldonita sub la AGPLv3 en Github.",
+ "Released under the AGPLv3 on Github.": "Eldonita sub la AGPLv3 en GitHub.",
"Source available here.": "Fonto havebla ĉi tie.",
"View JavaScript license information.": "Vidi Ĝavoskriptan licencan informon.",
"View privacy policy.": "Vidi regularon pri privateco.",
@@ -141,7 +141,6 @@
"Show less": "Montri malpli",
"Watch on YouTube": "Vidi filmeton en JuTubo",
"Switch Invidious Instance": "Ŝanĝi instalaĵon de Indivious",
- "Broken? Try another Invidious Instance": "Ĉu misfunkcio? Provu alian instalaĵon de Indivious",
"Hide annotations": "Kaŝi prinotojn",
"Show annotations": "Montri prinotojn",
"Genre: ": "Ĝenro: ",
@@ -329,39 +328,38 @@
"Videos": "Filmetoj",
"Playlists": "Ludlistoj",
"Community": "Komunumo",
- "relevance": "rilateco",
- "rating": "takso",
- "date": "dato",
- "views": "vidoj",
- "content_type": "enhavtipo",
- "duration": "daŭro",
- "features": "trajtoj",
- "sort": "ordigi",
- "hour": "horo",
- "today": "hodiaŭ",
- "week": "semajno",
- "month": "monato",
- "year": "jaro",
- "video": "filmeto",
- "channel": "kanalo",
- "playlist": "ludlisto",
- "movie": "filmo",
- "show": "spektaĵo",
- "hd": "altdistingiva",
- "subtitles": "subtekstoj",
- "creative_commons": "Krea Komunaĵo",
- "3d": "3D",
- "live": "nuna",
- "4k": "4k",
- "location": "loko",
- "hdr": "granddinamikgama",
- "filter": "filtri",
+ "search_filters_sort_option_relevance": "rilateco",
+ "search_filters_sort_option_rating": "takso",
+ "search_filters_sort_option_date": "dato",
+ "search_filters_sort_option_views": "vidoj",
+ "search_filters_type_label": "enhavtipo",
+ "search_filters_duration_label": "daŭro",
+ "search_filters_features_label": "trajtoj",
+ "search_filters_sort_label": "ordigi",
+ "search_filters_date_option_hour": "horo",
+ "search_filters_date_option_today": "hodiaŭ",
+ "search_filters_date_option_week": "semajno",
+ "search_filters_date_option_month": "monato",
+ "search_filters_date_option_year": "jaro",
+ "search_filters_type_option_video": "filmeto",
+ "search_filters_type_option_channel": "kanalo",
+ "search_filters_type_option_playlist": "ludlisto",
+ "search_filters_type_option_movie": "filmo",
+ "search_filters_type_option_show": "spektaĵo",
+ "search_filters_features_option_hd": "altdistingiva",
+ "search_filters_features_option_subtitles": "subtekstoj",
+ "search_filters_features_option_c_commons": "Krea Komunaĵo",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "nuna",
+ "search_filters_features_option_four_k": "4k",
+ "search_filters_features_option_location": "loko",
+ "search_filters_features_option_hdr": "granddinamikgama",
"Current version: ": "Nuna versio: ",
"next_steps_error_message": "Poste, vi provu: ",
"next_steps_error_message_refresh": "Reŝargi",
"next_steps_error_message_go_to_youtube": "Iri al JuTubo",
- "long": "Longa (> 20 minutos)",
- "short": "Mallonga (< 4 minutos)",
+ "search_filters_duration_option_long": "Longa (> 20 minutos)",
+ "search_filters_duration_option_short": "Mallonga (< 4 minutos)",
"footer_documentation": "Dokumentaro",
"footer_source_code": "Fontkodo",
"adminprefs_modified_source_code_url_label": "URL al modifita deponejo de fontkodo",
@@ -369,5 +367,6 @@
"footer_original_source_code": "Originala fontkodo",
"footer_donate_page": "Donaci",
"preferences_region_label": "Lando de la enhavo: ",
- "preferences_quality_dash_label": "Preferata DASH-a videkvalito: "
+ "preferences_quality_dash_label": "Preferata DASH-a videkvalito: ",
+ "search_filters_title": "Filtri"
}
diff --git a/locales/es.json b/locales/es.json
index 689cb310..0958a736 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -121,7 +121,7 @@
"Subscriptions": "Suscripciones",
"search": "buscar",
"Log out": "Cerrar la sesión",
- "Released under the AGPLv3 on Github.": "Publicado bajo la AGPLv3 en Github.",
+ "Released under the AGPLv3 on Github.": "Publicado bajo la AGPLv3 en GitHub.",
"Source available here.": "Código fuente disponible aquí.",
"View JavaScript license information.": "Ver información de licencia de JavaScript.",
"View privacy policy.": "Ver la política de privacidad.",
@@ -141,7 +141,6 @@
"Show less": "Mostrar menos",
"Watch on YouTube": "Ver el vídeo en YouTube",
"Switch Invidious Instance": "Cambiar Instancia de Invidious",
- "Broken? Try another Invidious Instance": "¿Algún error? Prueba otra instancia de Invidious",
"Hide annotations": "Ocultar anotaciones",
"Show annotations": "Mostrar anotaciones",
"Genre: ": "Género: ",
@@ -329,39 +328,38 @@
"Videos": "Vídeos",
"Playlists": "Listas de reproducción",
"Community": "Comunidad",
- "relevance": "relevancia",
- "rating": "valoración",
- "date": "fecha",
- "views": "visualizaciones",
- "content_type": "content_type",
- "duration": "duración",
- "features": "funcionalidades",
- "sort": "ordenar",
- "hour": "hora",
- "today": "hoy",
- "week": "semana",
- "month": "mes",
- "year": "año",
- "video": "vídeo",
- "channel": "canal",
- "playlist": "lista de reproducción",
- "movie": "película",
- "show": "programa",
- "hd": "hd",
- "subtitles": "subtítulos",
- "creative_commons": "creative_commons",
- "3d": "3d",
- "live": "directo",
- "4k": "4k",
- "location": "ubicación",
- "hdr": "hdr",
- "filter": "filtro",
+ "search_filters_sort_option_relevance": "relevancia",
+ "search_filters_sort_option_rating": "valoración",
+ "search_filters_sort_option_date": "fecha",
+ "search_filters_sort_option_views": "visualizaciones",
+ "search_filters_type_label": "content_type",
+ "search_filters_duration_label": "duración",
+ "search_filters_features_label": "funcionalidades",
+ "search_filters_sort_label": "ordenar",
+ "search_filters_date_option_hour": "hora",
+ "search_filters_date_option_today": "hoy",
+ "search_filters_date_option_week": "semana",
+ "search_filters_date_option_month": "mes",
+ "search_filters_date_option_year": "año",
+ "search_filters_type_option_video": "vídeo",
+ "search_filters_type_option_channel": "canal",
+ "search_filters_type_option_playlist": "lista de reproducción",
+ "search_filters_type_option_movie": "película",
+ "search_filters_type_option_show": "programa",
+ "search_filters_features_option_hd": "hd",
+ "search_filters_features_option_subtitles": "subtítulos",
+ "search_filters_features_option_c_commons": "creative_commons",
+ "search_filters_features_option_three_d": "3d",
+ "search_filters_features_option_live": "directo",
+ "search_filters_features_option_four_k": "4k",
+ "search_filters_features_option_location": "ubicación",
+ "search_filters_features_option_hdr": "hdr",
"Current version: ": "Versión actual: ",
"next_steps_error_message": "Después de lo cual deberías intentar: ",
"next_steps_error_message_refresh": "Recargar la página",
"next_steps_error_message_go_to_youtube": "Ir a YouTube",
- "short": "Corto (< 4 minutos)",
- "long": "Largo (> 20 minutos)",
+ "search_filters_duration_option_short": "Corto (< 4 minutos)",
+ "search_filters_duration_option_long": "Largo (> 20 minutos)",
"footer_documentation": "Documentación",
"footer_original_source_code": "Código fuente original",
"adminprefs_modified_source_code_url_label": "URL al repositorio de código fuente modificado",
@@ -395,8 +393,8 @@
"preferences_quality_dash_option_worst": "La peor",
"videoinfo_invidious_embed_link": "Enlace para Insertar",
"preferences_quality_dash_option_1080p": "1080p",
- "purchased": "Comprado",
- "360": "360°",
+ "search_filters_features_option_purchased": "Comprado",
+ "search_filters_features_option_three_sixty": "360°",
"videoinfo_watch_on_youTube": "Ver en YouTube",
"preferences_save_player_pos_label": "Guardar posición de reproducción: ",
"generic_views_count": "{{count}} visualización",
@@ -432,7 +430,7 @@
"crash_page_before_reporting": "Antes de notificar un error asegúrate de que has:",
"crash_page_switch_instance": "probado a <a href=\"`x`\">usar otra instancia</a>",
"crash_page_read_the_faq": "leído las <a href=\"`x`\">Preguntas Frecuentes</a>",
- "crash_page_search_issue": "buscado <a href=\"`x`\">problemas existentes en Github</a>",
+ "crash_page_search_issue": "buscado <a href=\"`x`\">problemas existentes en GitHub</a>",
"crash_page_you_found_a_bug": "¡Parece que has encontrado un error en Invidious!",
"crash_page_refresh": "probado a <a href=\"`x`\">recargar la página</a>",
"crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, <a href=\"`x`\">abre una nueva incidencia en GitHub</a> (preferiblemente en inglés) e incluye el siguiente texto en tu mensaje (NO traduzcas este texto):",
@@ -459,5 +457,18 @@
"Korean (auto-generated)": "Coreano (generados automáticamente)",
"Spanish (Mexico)": "Español (Méjico)",
"Spanish (auto-generated)": "Español (generados automáticamente)",
- "preferences_watch_history_label": "Habilitar historial de reproducciones: "
+ "preferences_watch_history_label": "Habilitar historial de reproducciones: ",
+ "search_message_no_results": "No se han encontrado resultados.",
+ "search_message_change_filters_or_query": "Pruebe ampliar la consulta de búsqueda y/o a cambiar los filtros.",
+ "search_filters_title": "Filtros",
+ "search_filters_date_label": "Fecha de subida",
+ "search_filters_date_option_none": "Cualquier fecha",
+ "search_filters_type_option_all": "Cualquier tipo",
+ "search_filters_duration_option_none": "Cualquier duración",
+ "search_filters_features_option_vr180": "VR180",
+ "search_filters_apply_button": "Aplicar filtros seleccionados",
+ "tokens_count": "{{count}} token",
+ "tokens_count_plural": "{{count}} tokens",
+ "search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
+ "search_filters_duration_option_medium": "Medio (4 - 20 minutes)"
}
diff --git a/locales/eu.json b/locales/eu.json
index a5c7c562..9e093a52 100644
--- a/locales/eu.json
+++ b/locales/eu.json
@@ -20,15 +20,15 @@
"No": "Ez",
"Import and Export Data": "Datuak inportatu eta esportatu",
"Import": "Inportatu",
- "Import Invidious data": "Inportatu Invidiouseko datuak",
- "Import YouTube subscriptions": "Inportatu YouTubeko harpidetzak",
+ "Import Invidious data": "Inportatu Invidiouseko JSON datuak",
+ "Import YouTube subscriptions": "Inportatu YouTubeko/OPML harpidetzak",
"Import FreeTube subscriptions (.db)": "Inportatu FreeTubeko harpidetzak (.db)",
"Import NewPipe subscriptions (.json)": "Inportatu NewPipeko harpidetzak (.json)",
"Import NewPipe data (.zip)": "Inportatu NewPipeko datuak (.zip)",
"Export": "Esportatu",
"Export subscriptions as OPML": "Esportatu harpidetzak OPML bezala",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Esportatu harpidetzak OPML bezala (NewPipe eta FreeTuberako)",
- "Export data as JSON": "Esportatu datuak JSON bezala",
+ "Export data as JSON": "Esportatu Invidious datuak JSON gisa",
"Delete account?": "Kontua ezabatu?",
"History": "Historia",
"An alternative front-end to YouTube": "YouTuberako interfaze alternatibo bat",
@@ -53,7 +53,7 @@
"preferences_volume_label": "Erreproduzigailuaren bolumena: ",
"preferences_comments_label": "Lehenetsitako iruzkinak: ",
"youtube": "YouTube",
- "reddit": "reddit",
+ "reddit": "Reddit",
"preferences_captions_label": "Lehenetsitako azpitituluak: ",
"preferences_related_videos_label": "Erakutsi erlazionatutako bideoak: ",
"preferences_annotations_label": "Erakutsi oharrak modu lehenetsian: ",
@@ -62,5 +62,216 @@
"Dark mode: ": "Gai iluna: ",
"preferences_dark_mode_label": "Gaia: ",
"dark": "iluna",
- "light": "argia"
+ "light": "argia",
+ "generic_subscriptions_count": "{{count}} harpidetza",
+ "generic_subscriptions_count_plural": "{{count}} harpidetzak",
+ "tokens_count": "{{count}} tokena",
+ "tokens_count_plural": "{{count}} tokenak",
+ "comments_points_count": "{{count}} puntua",
+ "comments_points_count_plural": "{{count}} puntuak",
+ "View more comments on Reddit": "Iruzkin gehiago Redditen",
+ "Fallback captions: ": "Ordezko azpitituluak: ",
+ "generic_subscribers_count": "{{count}} harpidedun",
+ "generic_subscribers_count_plural": "{{count}} harpidedunak",
+ "preferences_quality_option_dash": "DASH (kalitate egokitua)",
+ "preferences_listen_label": "Lehenetsiz jo: ",
+ "preferences_speed_label": "Abiadura lehenetsia: ",
+ "preferences_quality_dash_option_2160p": "2160p",
+ "preferences_quality_dash_option_144p": "144p",
+ "preferences_quality_dash_option_auto": "Auto",
+ "preferences_quality_dash_option_worst": "Txarrena",
+ "preferences_quality_dash_option_best": "Hoberena",
+ "preferences_quality_dash_option_4320p": "4320p",
+ "preferences_quality_dash_option_480p": "480p",
+ "preferences_quality_dash_option_240p": "240p",
+ "preferences_extend_desc_label": "Bideoaren azalpena automatikoki zabaldu: ",
+ "preferences_annotations_subscribed_label": "Harpidetutako kanalen oharrak erakutsi lehenetsiz? ",
+ "Redirect homepage to feed: ": "Hasierako orrira bidali jarraitzeko: ",
+ "channel name - reverse": "kanalaren izena - alderantziz",
+ "preferences_notifications_only_label": "Jakinarazpenak soilik erakutsi (baldin badago): ",
+ "Top enabled: ": "Goikoa gaitu: ",
+ "Import/export data": "Inportatu/exportatu data",
+ "Create playlist": "Zerrenda sortu",
+ "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Aditu! JavaScript itzalita dakazula ematen du. Hemen sakatu iruzkinak ikusteko. Denbora luza leikeela kontuan hartu.",
+ "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Ezinezkoa izena eman. Ziurtatu berresteko bi faktoreak (Authenticator edo SMS) piztuta daudela.",
+ "generic_views_count": "{{count}}ikusia",
+ "generic_views_count_plural": "{{count}}ikusiak",
+ "generic_playlists_count": "{{count}}zerrenda",
+ "generic_playlists_count_plural": "{{count}}zerrendak",
+ "Could not fetch comments": "Iruzkinei ezin heldu",
+ "Erroneous token": "Token okerra",
+ "Albanian": "Albaniarra",
+ "Azerbaijani": "Azerbaitarra",
+ "No such user": "Ez dago erabiltzailerik",
+ "Bulgarian": "Bulgariarra",
+ "Filipino": "Filipinera",
+ "French": "Frantsesa",
+ "French (auto-generated)": "Frantsesa (auto-sortua)",
+ "Show more": "Erakutsi gehiago",
+ "Show less": "Erakutsi gutxiago",
+ "Delete playlist": "Zerrenda ezabatu",
+ "Delete account": "Kontua ezabatu",
+ "User ID is a required field": "Erabiltzailearen IDa beharrezkoa da",
+ "English (United Kingdom)": "Ingelesa (Britania Handia",
+ "preferences_vr_mode_label": "360 graduko bideo interaktiboak (WebGL beharko): ",
+ "English (United States)": "Estatu batuarra (AEB)",
+ "English (auto-generated)": "Ingelesa (autosortua)",
+ "Arabic": "Arabiarra",
+ "Armenian": "Armeniarra",
+ "Bangla": "Banglera",
+ "Belarusian": "Bielorrusiara",
+ "Burmese": "Burmesera",
+ "Chinese (Simplified)": "Txinera (sinplifikatua)",
+ "preferences_watch_history_label": "Baimendu historia ikusi ",
+ "generic_videos_count": "{{count}}bideo",
+ "generic_videos_count_plural": "{{count}}bideoak",
+ "View privacy policy.": "Pribatutasun politika ikusi.",
+ "Cantonese (Hong Kong)": "Kantoniera (Hong Kong)",
+ "subscriptions_unseen_notifs_count": "{{count}} ezikusitako oharra",
+ "subscriptions_unseen_notifs_count_plural": "{{count}} ezikusitako oharrak",
+ "Trending": "Joera",
+ "Playlist privacy": "Zerrendaren privatutasuna",
+ "Switch Invidious Instance": "Invidious adibidea aldatu",
+ "Genre: ": "Genero: ",
+ "License: ": "Lizentzia: ",
+ "Family friendly? ": "Adeikorra familiarekin? ",
+ "Wilson score: ": "Wilsonen puntuazioa: ",
+ "Quota exceeded, try again in a few hours": "Kuota gaindituta, ordu batzuren bueltan berriro saiatu",
+ "comments_view_x_replies": "{{count}} erantzuna ikusi",
+ "comments_view_x_replies_plural": "{{count}} erantzunak ikusi",
+ "Catalan": "Katalaniera",
+ "Chinese": "Txinera",
+ "Chinese (China)": "Txinatarra",
+ "Chinese (Hong Kong)": "Hongkondarra",
+ "Chinese (Taiwan)": "Taiwandarra",
+ "Corsican": "Korsikera",
+ "Dutch (auto-generated)": "Alemaniera (auto-sortua)",
+ "Estonian": "Estoniera",
+ "Finnish": "Finlandiera",
+ "Galician": "Galizera",
+ "German (auto-generated)": "Alemaiera (auto-sortua)",
+ "Greek": "Greziera",
+ "crash_page_report_issue": "Aurreko ezerk ez badizu lagundu, arren <a href=\"`x`\"> GitHuben gai berri bat zabaldu </a> (ingelesez ahal bada) eta zure mezuan hurrengo testua sartu (testuari EZ itzulpena egin):",
+ "crash_page_search_issue": "GitHuben dauden gaiak <a href=\"`x`\"> buruz</a>",
+ "preferences_quality_option_medium": "Erdixka",
+ "preferences_quality_option_small": "Txikia",
+ "preferences_quality_dash_label": "DASH bideo kalitate lehenetsia: ",
+ "preferences_quality_dash_option_1440p": "1440p",
+ "preferences_quality_dash_option_1080p": "1080p",
+ "preferences_quality_dash_option_720p": "720p",
+ "preferences_quality_option_hd720": "HD720",
+ "preferences_quality_dash_option_360p": "360p",
+ "invidious": "Invidious",
+ "Source available here.": "Iturburua hemen eskura.",
+ "View JavaScript license information.": "JavaScriptaren lizentzi adierazpena ikusi.",
+ "Blacklisted regions: ": "zerrenda beltzaren zonaldeak: ",
+ "Premieres `x`": "'x' estrenaldiak",
+ "Wrong answer": "Erantzun ez zuzena",
+ "Password is a required field": "Pasahitza beharrezkoa da",
+ "Wrong username or password": "Pasahitza edo ezizena gaizki",
+ "Password cannot be longer than 55 characters": "Pasahitza 55 karaktere baino luzeagoa ezin da izan",
+ "This channel does not exist.": "Kanal hau ez dago.",
+ "`x` ago": "duela 'x'",
+ "Czech": "Txekiera",
+ "preferences_region_label": "Herrialdeko edukiera: ",
+ "preferences_sort_label": "Bideoak ordenatu: ",
+ "published": "argitaratuta",
+ "Only show latest video from channel: ": "Kanalaren azken bideoa soilik erakutsi ",
+ "preferences_category_admin": "Administratzailearen lehentasunak",
+ "Registration enabled: ": "Harpidetza gaituta: ",
+ "Save preferences": "Baloreak gorde",
+ "Token manager": "Token kudeatzailea",
+ "unsubscribe": "Baja eman",
+ "search": "Bilatu",
+ "Log out": "Irten",
+ "English": "Ingelesa",
+ "Afrikaans": "Afrikarra",
+ "Amharic": "Amharerra",
+ "Basque": "Euskera",
+ "Bosnian": "Bosniarra",
+ "Cebuano": "Zebuera",
+ "Chinese (Traditional)": "Txinera (Tradizionala)",
+ "Croatian": "Croaziera",
+ "Danish": "Daniera",
+ "Dutch": "Alemaniera",
+ "Esperanto": "Esperanto",
+ "Erroneous challenge": "Erronka okerra",
+ "View all playlists": "Zerrenda guztiak ikusi",
+ "Show annotations": "Oharrak erakutsi",
+ "Empty playlist": "Zerrenda hutsik",
+ "Please log in": "Sartu, mesedez",
+ "CAPTCHA is a required field": "CAPTCHA beharrezko eremua da",
+ "preferences_category_data": "Dataren lehentasunak",
+ "preferences_default_home_label": "Homepage lehenetsia: ",
+ "preferences_automatic_instance_redirect_label": "berbideratze adibide automatikoa (atzera egin berbideratzeko: invidious.io) ",
+ "Please sign in using 'Log in with Google'": "'Log in Googlerekin' erabili",
+ "`x` uploaded a video": "' x'(e)k bideo bat igo du",
+ "published - reverse": "argitaratuta - alderantziz",
+ "Could not get channel info.": "Kanalaren adierazpena ezin lortu.",
+ "alphabetically - reverse": "alfabetikoki - alderantziz",
+ "Public": "Orokorra",
+ "Unlisted": "Ez zerrendatua",
+ "Subscription manager": "Harpidetzen kudeatzailea",
+ "Updated `x` ago": "Duela 'x' eguneratua",
+ "Hide replies": "Erantzunak izkutatu",
+ "preferences_thin_mode_label": "Urri eran: ",
+ "Show replies": "Erantzunak erakutsi",
+ "Watch on YouTube": "YouTuben ikusi",
+ "Premieres in `x`": "'x'eko estrenaldiak",
+ "Delete playlist `x`?": "'x' zerrenda ezabatu nahi?",
+ "Token is expired, please try again": "Token kadukatua, saiatu berriro",
+ "Invalid TFA code": "TFA kodea ez da zuzena",
+ "CAPTCHA enabled: ": "CAPTCHA gaitu: ",
+ "Released under the AGPLv3 on Github.": "GitHubeko AGPLv3pean argitaratuta.",
+ "channel:`x`": "Kanal: 'x'",
+ "Georgian": "Georgiera",
+ "Incorrect password": "Pasahitza gaizki",
+ "Playlist does not exist.": "Zerrenda ez da existitzen.",
+ "preferences_category_misc": "Askotariko lehentasunak",
+ "View `x` comments": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "'x' iruzkina ikusi",
+ "": "'x' iruzkinak ikusi"
+ },
+ "Report statistics: ": "Estatistikak adierazi: ",
+ "preferences_max_results_label": "Jotzeko bideo zerrendaren luzera: ",
+ "Subscriptions": "Harpidetzak",
+ "Load more": "Gehiago atera",
+ "Change password": "Pasahitza aldatu",
+ "preferences_show_nick_label": "Erakutsi ezizena goian: ",
+ "View Reddit comments": "Redditeko iruzkinak ikusi",
+ "preferences_category_subscription": "Harpidetzaren lehentasunak",
+ "Hidden field \"challenge\" is a required field": "\"challenge\" eremu ezkutua beharrezkoa da",
+ "German": "Alemaniarra",
+ "Login failed. This may be because two-factor authentication is not turned on for your account.": "Ezin izena eman. Izan leike zure konturako berresteko bi faktoreak piztuta ez daudela.",
+ "View YouTube comments": "YouTubeko iruzkinak ikusi",
+ "Google verification code": "Googleren berresteko kodea",
+ "`x` is live": "'x' bizirik darrai",
+ "Password cannot be empty": "Pasahitza ezin da hutsik utzi",
+ "preferences_video_loop_label": "Beti begiztatu: ",
+ "Only show latest unwatched video from channel: ": "kanalaren azken bideo ezikusia erakutsi soilik ",
+ "Enable web notifications": "Webaren jakinarazpenak baimendu",
+ "revoke": "ukatu",
+ "preferences_continue_label": "Hurrengo lehenetsia jo: ",
+ "Whitelisted regions: ": "Zuri zerrendaren zonaldeak: ",
+ "Erroneous CAPTCHA": "CAPTCHA gaizki",
+ "Deleted or invalid channel": "Ezgai edota ezabatutako kanala",
+ "Could not create mix.": "Nahastea ezin sortu.",
+ "Not a playlist.": "Ez da zerrenda.",
+ "Hidden field \"token\" is a required field": "\"token\" eremu ezkutua beharrezkoa da",
+ "Import/export": "Inportatu/esportatu",
+ "alphabetically": "alfabetikoki",
+ "preferences_unseen_only_label": "Ezikusiak besterik ez erakutsi: ",
+ "Clear watch history": "Historia ezabatu",
+ "Manage subscriptions": "Harpidetzak kudeatu",
+ "Manage tokens": "Fitxak kudeatu",
+ "Watch history": "Historia ikusi",
+ "Login enabled: ": "Login gaitu: ",
+ "Hide annotations": "Oharrak izkutatu",
+ "Title": "Titulua",
+ "channel name": "Kanalaren izena",
+ "Authorize token for `x`?": "Baimendu tokena 'x'tzako?",
+ "Private": "Pribatua",
+ "Editing playlist `x`": "'x' zerrenda editatu",
+ "Could not pull trending pages.": "Ezin ekarri orri arrakastatsuak.",
+ "crash_page_read_the_faq": "Bide <a href=\"`x`\"> (FAQ) ohiko galderak</a>"
}
diff --git a/locales/fa.json b/locales/fa.json
index 48b5a17d..5ea976f5 100644
--- a/locales/fa.json
+++ b/locales/fa.json
@@ -148,7 +148,6 @@
"Show less": "نمایش کم‌تر",
"Watch on YouTube": "تماشا در یوتیوب",
"Switch Invidious Instance": "تعویض نمونه اینویدیوس",
- "Broken? Try another Invidious Instance": "کار نمی‌کند؟ نمونه دیگری از اینویدیوس را امتحان کنید",
"Hide annotations": "مخفی کردن حاشیه نویسی ها",
"Show annotations": "نمایش حاشیه نویسی ها",
"Genre: ": "ژانر: ",
@@ -345,33 +344,32 @@
"Videos": "ویدیو ها",
"Playlists": "سیاهه‌های پخش",
"Community": "اجتماع",
- "relevance": "مرتبط بودن",
- "rating": "امتیاز",
- "date": "تاریخ بارگذاری",
- "views": "تعداد بازدید",
- "content_type": "نوع",
- "duration": "مدت",
- "features": "ویژگی‌ها",
- "sort": "به ترتیب",
- "hour": "یک ساعت گذشته",
- "today": "امروز",
- "week": "این هفته",
- "month": "این ماه",
- "year": "امسال",
- "video": "ویدئو",
- "channel": "کانال",
- "playlist": "سیاههٔ پخش",
- "movie": "فیلم",
- "show": "نمایش",
- "hd": "HD",
- "subtitles": "زیرنویس",
- "creative_commons": "کریتیو کامونز",
- "3d": "سه‌بعدی",
- "live": "زنده",
- "4k": "4K",
- "location": "مکان",
- "hdr": "HDR",
- "filter": "پالایه",
+ "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_date_option_hour": "یک ساعت گذشته",
+ "search_filters_date_option_today": "امروز",
+ "search_filters_date_option_week": "این هفته",
+ "search_filters_date_option_month": "این ماه",
+ "search_filters_date_option_year": "امسال",
+ "search_filters_type_option_video": "ویدئو",
+ "search_filters_type_option_channel": "کانال",
+ "search_filters_type_option_playlist": "سیاههٔ پخش",
+ "search_filters_type_option_movie": "فیلم",
+ "search_filters_type_option_show": "نمایش",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "زیرنویس",
+ "search_filters_features_option_c_commons": "کریتیو کامونز",
+ "search_filters_features_option_three_d": "سه‌بعدی",
+ "search_filters_features_option_live": "زنده",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "مکان",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "نسخه فعلی: ",
"next_steps_error_message": "اکنون بایستی یکی از این موارد را امتحان کنید: ",
"next_steps_error_message_refresh": "تازه‌سازی",
@@ -393,7 +391,7 @@
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_144p": "144p",
"invidious": "اینویدیوس",
- "360": "360°",
+ "search_filters_features_option_three_sixty": "360°",
"footer_donate_page": "کمک مالی",
"footer_source_code": "کد منبع",
"footer_modfied_source_code": "کد منبع ویرایش شده",
@@ -405,12 +403,13 @@
"download_subtitles": "زیرنویس‌ها - `x` (.vtt)",
"Video unavailable": "ویدئو دردسترس نیست",
"preferences_save_player_pos_label": "ذخیره زمان کنونی ویدئو: ",
- "purchased": "خریداری شده",
+ "search_filters_features_option_purchased": "خریداری شده",
"preferences_quality_dash_label": "کیفیت ترجیحی ویدئو DASH: ",
"preferences_region_label": "کشور محتوا: ",
"footer_documentation": "مستندات",
"footer_original_source_code": "کد منبع اصلی",
- "long": "بلند (> 20 دقیقه)",
+ "search_filters_duration_option_long": "بلند (> 20 دقیقه)",
"adminprefs_modified_source_code_url_label": "URL مخزن کد منبع ویریش شده",
- "short": "کوتاه (< 4 دقیقه)"
+ "search_filters_duration_option_short": "کوتاه (< 4 دقیقه)",
+ "search_filters_title": "پالایه"
}
diff --git a/locales/fi.json b/locales/fi.json
index 84090c24..2aa64ea7 100644
--- a/locales/fi.json
+++ b/locales/fi.json
@@ -140,7 +140,6 @@
"Show less": "Näytä vähemmän",
"Watch on YouTube": "Katso YouTubessa",
"Switch Invidious Instance": "Vaihda Invidious-instanssia",
- "Broken? Try another Invidious Instance": "Rikki? Kokeile toista Invidious-instanssia",
"Hide annotations": "Piilota merkkaukset",
"Show annotations": "Näytä merkkaukset",
"Genre: ": "Genre: ",
@@ -328,33 +327,32 @@
"Videos": "Videot",
"Playlists": "Soittolistat",
"Community": "Yhteisö",
- "relevance": "Osuvuus",
- "rating": "Arvostelu",
- "date": "Latauspäivämäärä",
- "views": "Katselukerrat",
- "content_type": "Tyyppi",
- "duration": "Kesto",
- "features": "Ominaisuudet",
- "sort": "Luokittele",
- "hour": "Viimeisin tunti",
- "today": "Tänään",
- "week": "Tämä viikko",
- "month": "Tämä kuukausi",
- "year": "Tämä vuosi",
- "video": "Video",
- "channel": "Kanava",
- "playlist": "Soittolista",
- "movie": "Elokuva",
- "show": "Ohjelma",
- "hd": "HD",
- "subtitles": "Tekstitys/CC",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "Suora lähetys",
- "4k": "4K",
- "location": "Sijainti",
- "hdr": "HDR",
- "filter": "Suodatin",
+ "search_filters_sort_option_relevance": "Osuvuus",
+ "search_filters_sort_option_rating": "Arvostelu",
+ "search_filters_sort_option_date": "Latauspäivämäärä",
+ "search_filters_sort_option_views": "Katselukerrat",
+ "search_filters_type_label": "Tyyppi",
+ "search_filters_duration_label": "Kesto",
+ "search_filters_features_label": "Ominaisuudet",
+ "search_filters_sort_label": "Luokittele",
+ "search_filters_date_option_hour": "Viimeisin tunti",
+ "search_filters_date_option_today": "Tänään",
+ "search_filters_date_option_week": "Tämä viikko",
+ "search_filters_date_option_month": "Tämä kuukausi",
+ "search_filters_date_option_year": "Tämä vuosi",
+ "search_filters_type_option_video": "Video",
+ "search_filters_type_option_channel": "Kanava",
+ "search_filters_type_option_playlist": "Soittolista",
+ "search_filters_type_option_movie": "Elokuva",
+ "search_filters_type_option_show": "Ohjelma",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "Tekstitys/CC",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "Suora lähetys",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "Sijainti",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Tämänhetkinen versio: ",
"next_steps_error_message": "Sinun tulisi kokeilla seuraavia: ",
"next_steps_error_message_refresh": "Päivitä",
@@ -390,7 +388,7 @@
"crash_page_before_reporting": "Varmista ennen bugin ilmoittamista, että sinä olet:",
"crash_page_refresh": "yrittänyt <a href=\"`x`\">päivittää sivun</a>",
"crash_page_read_the_faq": "lukenut <a href=\"`x`\">Usein kysytyt kysymykset (FAQ)</a>",
- "crash_page_search_issue": "etsinyt <a href=\"`x`\">olemassa olevia issueita Githubissa</a>",
+ "crash_page_search_issue": "etsinyt <a href=\"`x`\">olemassa olevia issueita GitHubissa</a>",
"generic_views_count": "{{count}} katselu",
"generic_views_count_plural": "{{count}} katselua",
"preferences_quality_dash_option_720p": "720p",
@@ -423,8 +421,8 @@
"preferences_quality_dash_label": "Haluttava DASH-videolaatu: ",
"generic_count_years": "{{count}} vuosi",
"generic_count_years_plural": "{{count}} vuotta",
- "purchased": "Ostettu",
- "360": "360°",
+ "search_filters_features_option_purchased": "Ostettu",
+ "search_filters_features_option_three_sixty": "360°",
"videoinfo_watch_on_youTube": "Katso YouTubessa",
"none": "ei mikään",
"videoinfo_started_streaming_x_ago": "Striimaaminen aloitettu `x` sitten",
@@ -433,14 +431,14 @@
"footer_source_code": "Lähdekoodi",
"adminprefs_modified_source_code_url_label": "URL muokattuun lähdekoodirepositoryyn",
"Released under the AGPLv3 on Github.": "Julkaistu AGPLv3-lisenssin alla GitHubissa.",
- "short": "Lyhyt (< 4 minuuttia)",
- "long": "Pitkä (> 20 minuuttia)",
+ "search_filters_duration_option_short": "Lyhyt (< 4 minuuttia)",
+ "search_filters_duration_option_long": "Pitkä (> 20 minuuttia)",
"footer_documentation": "Dokumentaatio",
"footer_original_source_code": "Alkuperäinen lähdekoodi",
"footer_modfied_source_code": "Muokattu lähdekoodi",
"Japanese (auto-generated)": "Japani (automaattisesti luotu)",
"German (auto-generated)": "Saksa (automaattisesti luotu)",
- "Portuguese (auto-generated)": "Portugali (automaattisesti luotu)",
+ "Portuguese (auto-generated)": "portugali (automaattisesti luotu)",
"Russian (auto-generated)": "Venäjä (automaattisesti luotu)",
"preferences_watch_history_label": "Ota katseluhistoria käyttöön: ",
"English (United Kingdom)": "Englanti (Iso-Britannia)",
@@ -456,10 +454,21 @@
"Interlingue": "Interlingue",
"Italian (auto-generated)": "Italia (automaattisesti luotu)",
"Korean (auto-generated)": "Korea (automaattisesti luotu)",
- "Portuguese (Brazil)": "Portugali (Brasilia)",
+ "Portuguese (Brazil)": "portugali (Brasilia)",
"Spanish (auto-generated)": "Espanja (automaattisesti luotu)",
"Spanish (Mexico)": "Espanja (Meksiko)",
"Spanish (Spain)": "Espanja (Espanja)",
"Turkish (auto-generated)": "Turkki (automaattisesti luotu)",
- "Vietnamese (auto-generated)": "Vietnam (automaattisesti luotu)"
+ "Vietnamese (auto-generated)": "Vietnam (automaattisesti luotu)",
+ "search_filters_title": "Suodatin",
+ "search_message_no_results": "Ei tuloksia löydetty.",
+ "search_message_change_filters_or_query": "Yritä hakukyselysi laajentamista ja/tai suodattimien muuttamista.",
+ "search_filters_duration_option_none": "Mikä tahansa kesto",
+ "search_filters_features_option_vr180": "VR180",
+ "search_filters_apply_button": "Ota valitut suodattimet käyttöön",
+ "search_filters_date_label": "Latausaika",
+ "search_filters_duration_option_medium": "Keskipituinen (4 - 20 minuuttia)",
+ "search_message_use_another_instance": " Voit myös <a href=\"`x`\">hakea toisella instanssilla</a>.",
+ "search_filters_date_option_none": "Milloin tahansa",
+ "search_filters_type_option_all": "Mikä tahansa tyyppi"
}
diff --git a/locales/fr.json b/locales/fr.json
index 96103580..6fee70f9 100644
--- a/locales/fr.json
+++ b/locales/fr.json
@@ -135,7 +135,7 @@
"subscriptions_unseen_notifs_count_plural": "{{count}} notifications non vues",
"search": "rechercher",
"Log out": "Se déconnecter",
- "Released under the AGPLv3 on Github.": "Publié sous licence AGPLv3 sur Github.",
+ "Released under the AGPLv3 on Github.": "Publié sous licence AGPLv3 sur GitHub.",
"Source available here.": "Code source disponible ici.",
"View JavaScript license information.": "Informations des licences JavaScript.",
"View privacy policy.": "Politique de confidentialité.",
@@ -155,7 +155,6 @@
"Show less": "Afficher moins",
"Watch on YouTube": "Voir la vidéo sur Youtube",
"Switch Invidious Instance": "Changer d'instance",
- "Broken? Try another Invidious Instance": "Instance Invidious défectueuse ? Essayez-en une autre",
"Hide annotations": "Masquer les annotations",
"Show annotations": "Afficher les annotations",
"Genre: ": "Genre : ",
@@ -361,33 +360,32 @@
"Videos": "Vidéos",
"Playlists": "Listes de lecture",
"Community": "Communauté",
- "relevance": "pertinence",
- "rating": "évaluation",
- "date": "date",
- "views": "nombre de vues",
- "content_type": "type",
- "duration": "durée",
- "features": "fonctionnalités",
- "sort": "Trier par",
- "hour": "dernière heure",
- "today": "aujourd'hui",
- "week": "semaine",
- "month": "mois",
- "year": "année",
- "video": "vidéo",
- "channel": "chaîne",
- "playlist": "liste de lecture",
- "movie": "film",
- "show": "émission",
- "hd": "HD",
- "subtitles": "sous-titres / CC",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "en direct",
- "4k": "4K",
- "location": "emplacement",
- "hdr": "HDR",
- "filter": "filtrer",
+ "search_filters_sort_option_relevance": "Pertinence",
+ "search_filters_sort_option_rating": "Notation",
+ "search_filters_sort_option_date": "Date d'ajout",
+ "search_filters_sort_option_views": "Nombre de vues",
+ "search_filters_type_label": "Type de contenu",
+ "search_filters_duration_label": "Durée",
+ "search_filters_features_label": "Fonctionnalités",
+ "search_filters_sort_label": "Trier par",
+ "search_filters_date_option_hour": "Dernière heure",
+ "search_filters_date_option_today": "Aujourd'hui",
+ "search_filters_date_option_week": "Cette semaine",
+ "search_filters_date_option_month": "Ce mois-ci",
+ "search_filters_date_option_year": "Cette année",
+ "search_filters_type_option_video": "Vidéo",
+ "search_filters_type_option_channel": "Chaîne",
+ "search_filters_type_option_playlist": "Liste de lecture",
+ "search_filters_type_option_movie": "Film",
+ "search_filters_type_option_show": "Émission",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "Sous-titres (CC)",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "En direct",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "emplacement",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Version actuelle : ",
"next_steps_error_message": "Vous pouvez essayer de : ",
"next_steps_error_message_refresh": "Rafraîchir la page",
@@ -397,8 +395,8 @@
"preferences_region_label": "Pays du contenu : ",
"footer_donate_page": "Faire un don",
"footer_modfied_source_code": "Code source modifié",
- "short": "Courte (< 4 minutes)",
- "long": "Longue (> 20 minutes)",
+ "search_filters_duration_option_short": "Courte (< 4 minutes)",
+ "search_filters_duration_option_long": "Longue (> 20 minutes)",
"adminprefs_modified_source_code_url_label": "URL du dépôt du code source modifié",
"footer_documentation": "Documentation",
"footer_original_source_code": "Code source original",
@@ -415,12 +413,12 @@
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_144p": "144p",
"invidious": "Invidious",
- "360": "360°",
+ "search_filters_features_option_three_sixty": "360°",
"none": "aucun",
"videoinfo_started_streaming_x_ago": "En stream depuis `x`",
"videoinfo_watch_on_youTube": "Regarder sur YouTube",
"videoinfo_youTube_embed_link": "Intégrer",
- "purchased": "Acheter",
+ "search_filters_features_option_purchased": "Acheté",
"videoinfo_invidious_embed_link": "Lien intégré",
"download_subtitles": "Sous-titres - `x` (.vtt)",
"user_saved_playlists": "`x` listes de lecture sauvegardées",
@@ -435,7 +433,7 @@
"crash_page_refresh": "tenté de <a href=\"`x`\">rafraîchir la page</a>",
"crash_page_switch_instance": "essayé d'<a href=\"`x`\">utiliser une autre instance</a>",
"crash_page_read_the_faq": "lu la <a href=\"`x`\">Foire Aux Questions (FAQ)</a>",
- "crash_page_search_issue": "<a href=\"`x`\">cherché ce bug sur Github</a>",
+ "crash_page_search_issue": "<a href=\"`x`\">cherché ce bug sur GitHub</a>",
"crash_page_before_reporting": "Avant de signaler un bug, veuillez vous assurez que vous avez :",
"crash_page_report_issue": "Si aucune des solutions proposées ci-dessus ne vous a aidé, veuillez <a href=\"`x`\">ouvrir une \"issue\" sur GitHub</a> (de préférence en anglais) et d'y inclure le message suivant (ne PAS traduire le texte) :",
"English (United States)": "Anglais (Etats-Unis)",
@@ -461,5 +459,16 @@
"Vietnamese (auto-generated)": "Vietnamien (auto-généré)",
"Russian (auto-generated)": "Russe (auto-généré)",
"Spanish (Spain)": "Espagnol (Espagne)",
- "preferences_watch_history_label": "Activer l'historique de visionnage : "
+ "preferences_watch_history_label": "Activer l'historique de visionnage : ",
+ "search_filters_title": "Filtres",
+ "search_message_change_filters_or_query": "Essayez d'élargir votre recherche et/ou de changer les filtres.",
+ "search_filters_date_option_none": "Toutes les dates",
+ "search_filters_duration_option_medium": "Moyenne (de 4 à 20 minutes)",
+ "search_filters_apply_button": "Appliquer les filtres",
+ "search_message_no_results": "Aucun résultat.",
+ "search_message_use_another_instance": " Vous pouvez également <a href=\"`x`\">effectuer votre recherche sur une autre instance</a>.",
+ "search_filters_type_option_all": "Tous les types",
+ "search_filters_date_label": "Date d'ajout",
+ "search_filters_features_option_vr180": "VR180",
+ "search_filters_duration_option_none": "Toutes les durées"
}
diff --git a/locales/he.json b/locales/he.json
index 2c9258b9..384b2657 100644
--- a/locales/he.json
+++ b/locales/he.json
@@ -274,32 +274,32 @@
"Videos": "סרטונים",
"Playlists": "פלייליסטים",
"Community": "קהילה",
- "relevance": "רלוונטיות",
- "rating": "דירוג",
- "date": "תאריך העלאה",
- "views": "מספר צפיות",
- "content_type": "סוג",
- "duration": "משך זמן",
- "features": "תכונות",
- "sort": "מיון לפי",
- "hour": "השעה האחרונה",
- "today": "היום",
- "week": "השבוע",
- "month": "החודש",
- "year": "השנה",
- "video": "סרטון",
- "channel": "ערוץ",
- "playlist": "פלייליסט",
- "movie": "סרט",
- "show": "תכנית טלוויזיה",
- "hd": "HD",
- "subtitles": "כתוביות",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "Live",
- "4k": "4K",
- "location": "מיקום",
- "hdr": "HDR",
- "filter": "סינון",
- "Current version: ": "הגרסה הנוכחית: "
+ "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_date_option_hour": "השעה האחרונה",
+ "search_filters_date_option_today": "היום",
+ "search_filters_date_option_week": "השבוע",
+ "search_filters_date_option_month": "החודש",
+ "search_filters_date_option_year": "השנה",
+ "search_filters_type_option_video": "סרטון",
+ "search_filters_type_option_channel": "ערוץ",
+ "search_filters_type_option_playlist": "פלייליסט",
+ "search_filters_type_option_movie": "סרט",
+ "search_filters_type_option_show": "תכנית טלוויזיה",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "כתוביות",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "Live",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "מיקום",
+ "search_filters_features_option_hdr": "HDR",
+ "Current version: ": "הגרסה הנוכחית: ",
+ "search_filters_title": "סינון"
}
diff --git a/locales/hr.json b/locales/hr.json
index 688368d2..94633aac 100644
--- a/locales/hr.json
+++ b/locales/hr.json
@@ -121,7 +121,7 @@
"Subscriptions": "Pretplate",
"search": "traži",
"Log out": "Odjavi se",
- "Released under the AGPLv3 on Github.": "Izdano pod licencom AGPLv3 na Github-u.",
+ "Released under the AGPLv3 on Github.": "Izdano pod licencom AGPLv3 na GitHub-u.",
"Source available here.": "Izvor je ovdje dostupan.",
"View JavaScript license information.": "Prikaži informacije o JavaScript licenci.",
"View privacy policy.": "Prikaži politiku privatnosti.",
@@ -141,7 +141,6 @@
"Show less": "Pokaži manje",
"Watch on YouTube": "Gledaj na YouTubeu",
"Switch Invidious Instance": "Promijeni Invidious instancu",
- "Broken? Try another Invidious Instance": "Pokvarena? Probaj jednu drugu Invidious instancu",
"Hide annotations": "Sakrij napomene",
"Show annotations": "Prikaži napomene",
"Genre: ": "Žanr: ",
@@ -329,41 +328,40 @@
"Videos": "Videa",
"Playlists": "Zbirke",
"Community": "Zajednica",
- "relevance": "značaj",
- "rating": "ocjena",
- "date": "datum",
- "views": "prikazi",
- "content_type": "vrsta_sadržaja",
- "duration": "trajanje",
- "features": "funkcije",
- "sort": "redoslijed",
- "hour": "sat",
- "today": "danas",
- "week": "tjedan",
- "month": "mjesec",
- "year": "godina",
- "video": "video",
- "channel": "kanal",
- "playlist": "Zbirka",
- "movie": "film",
- "show": "emisija",
- "hd": "hd",
- "subtitles": "titlovi",
- "creative_commons": "creative_commons",
- "3d": "3d",
- "live": "uživo",
- "4k": "4k",
- "location": "lokacija",
- "hdr": "hdr",
- "filter": "filtar",
+ "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_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_features_option_four_k": "4k",
+ "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",
- "short": "Kratki (< 4 minute)",
- "long": "Dugi (> 20 minute)",
+ "search_filters_duration_option_short": "Kratki (< 4 minute)",
+ "search_filters_duration_option_long": "Dugi (> 20 minute)",
"footer_source_code": "Izvorni kod",
"footer_modfied_source_code": "Izmijenjeni izvorni kod",
"footer_documentation": "Dokumentacija",
@@ -382,8 +380,8 @@
"preferences_quality_dash_option_240p": "240 p",
"preferences_quality_dash_option_144p": "144 p",
"invidious": "Invidious",
- "purchased": "Kupljeno",
- "360": "360 °",
+ "search_filters_features_option_purchased": "Kupljeno",
+ "search_filters_features_option_three_sixty": "360 °",
"none": "bez",
"videoinfo_youTube_embed_link": "Ugradi",
"user_created_playlists": "`x` stvorene zbirke",
@@ -452,7 +450,7 @@
"crash_page_refresh": "pokušaj <a href=\"`x`\">aktualizirati stranicu</a>",
"crash_page_switch_instance": "pokušaj <a href=\"`x`\">koristiti jednu drugu instancu</a>",
"crash_page_read_the_faq": "pročitaj <a href=\"`x`\">Često postavljena pitanja (ČPP)</a>",
- "crash_page_search_issue": "pretraži <a href=\"`x`\">postojeće probleme na Github-u</a>",
+ "crash_page_search_issue": "pretraži <a href=\"`x`\">postojeće probleme na GitHub-u</a>",
"crash_page_report_issue": "Ako ništa od gore navedenog ne pomaže, <a href=\"`x`\">prijavi novi problem na GitHub-u</a> (po mogućnosti na engleskom) i uključi sljedeći tekst u poruku (NEMOJ prevoditi taj tekst):",
"English (United Kingdom)": "Engleski (Ujedinjeno Kraljevstvo)",
"English (United States)": "Engleski (Sjedinjene Američke Države)",
@@ -477,5 +475,6 @@
"Korean (auto-generated)": "Korejski (automatski generiran)",
"Portuguese (auto-generated)": "Portugalski (automatski generiran)",
"Spanish (auto-generated)": "Španjolski (automatski generiran)",
- "preferences_watch_history_label": "Aktiviraj povijest gledanja: "
+ "preferences_watch_history_label": "Aktiviraj povijest gledanja: ",
+ "search_filters_title": "Filtar"
}
diff --git a/locales/hu-HU.json b/locales/hu-HU.json
index d1948a47..a3679813 100644
--- a/locales/hu-HU.json
+++ b/locales/hu-HU.json
@@ -365,9 +365,9 @@
"preferences_quality_dash_option_144p": "144p",
"invidious": "Invidious",
"videoinfo_started_streaming_x_ago": "`x` ezelőtt kezdte streamelni",
- "views": "Mennyien látták",
- "purchased": "Megvásárolva",
- "360": "360°-os",
+ "search_filters_sort_option_views": "Mennyien látták",
+ "search_filters_features_option_purchased": "Megvásárolva",
+ "search_filters_features_option_three_sixty": "360°-os",
"footer_original_source_code": "Eredeti forráskód",
"none": "egyik sem",
"videoinfo_watch_on_youTube": "YouTube-on megnézni",
@@ -382,14 +382,13 @@
"preferences_quality_dash_option_1440p": "1440p",
"preferences_quality_dash_label": "DASH-videó minősége: ",
"preferences_quality_option_small": "Rossz",
- "date": "Feltöltés dátuma",
+ "search_filters_sort_option_date": "Feltöltés dátuma",
"Video unavailable": "A videó nem érhető el",
"preferences_save_player_pos_label": "A videó folytatása onnan, ahol félbe lett hagyva: ",
"preferences_show_nick_label": "Becenév mutatása felül: ",
"Released under the AGPLv3 on Github.": "AGPLv3 licenc alapján a GitHubon",
- "3d": "3D-ben",
- "live": "Élőben",
- "filter": "Szűrők",
+ "search_filters_features_option_three_d": "3D-ben",
+ "search_filters_features_option_live": "Élőben",
"next_steps_error_message_refresh": "Újratöltés",
"footer_donate_page": "Adakozás",
"footer_source_code": "Forráskód",
@@ -397,40 +396,39 @@
"adminprefs_modified_source_code_url_label": "A módosított forráskód repositoryjának URL-je:",
"preferences_automatic_instance_redirect_label": "Váltáskor másik Invidious oldal automatikus betöltése (redirect.invidious.io töltődik, ha nem működne): ",
"preferences_region_label": "Ország tartalmainak mutatása: ",
- "relevance": "Relevancia",
- "rating": "Pontszám",
- "content_type": "Típus",
- "today": "Mai napon",
- "channel": "Csatorna",
- "video": "Videó",
- "playlist": "Lejátszási lista",
- "creative_commons": "Creative Commons",
- "features": "Jellemzők",
- "sort": "Rendezés módja",
+ "search_filters_sort_option_relevance": "Relevancia",
+ "search_filters_sort_option_rating": "Pontszám",
+ "search_filters_type_label": "Típus",
+ "search_filters_date_option_today": "Mai napon",
+ "search_filters_type_option_channel": "Csatorna",
+ "search_filters_type_option_video": "Videó",
+ "search_filters_type_option_playlist": "Lejátszási lista",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_label": "Jellemzők",
+ "search_filters_sort_label": "Rendezés módja",
"preferences_category_misc": "További beállítások",
"%A %B %-d, %Y": "%Y. %B %-d %A",
- "long": "Hosszú (20 percnél hosszabb)",
- "year": "Ebben az évben",
- "hour": "Az elmúlt órában",
- "movie": "Film",
- "hdr": "HDR",
- "Broken? Try another Invidious Instance": "Nem működik? Próbáld meg egy másik Invidious oldallal.",
- "duration": "Játékidő",
+ "search_filters_duration_option_long": "Hosszú (20 percnél hosszabb)",
+ "search_filters_date_option_year": "Ebben az évben",
+ "search_filters_date_option_hour": "Az elmúlt órában",
+ "search_filters_type_option_movie": "Film",
+ "search_filters_features_option_hdr": "HDR",
+ "search_filters_duration_label": "Játékidő",
"next_steps_error_message": "Az alábbi lehetőségek állnak rendelkezésre: ",
"Xhosa": "xhosza",
"Switch Invidious Instance": "Váltás másik Invidious-oldalra",
"Urdu": "urdu",
- "week": "Ezen a héten",
+ "search_filters_date_option_week": "Ezen a héten",
"Invalid TFA code": "A kétlépéses hitelesítés kódja nem megfelelő",
"footer_documentation": "Dokumentáció",
- "hd": "HD",
+ "search_filters_features_option_hd": "HD",
"next_steps_error_message_go_to_youtube": "Ugrás a YouTube-ra",
- "show": "Műsor",
- "4k": "4K",
- "short": "Rövid (4 percnél nem több)",
- "month": "Ebben a hónapban",
- "subtitles": "Felirattal",
- "location": "Közelben",
+ "search_filters_type_option_show": "Műsor",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_duration_option_short": "Rövid (4 percnél nem több)",
+ "search_filters_date_option_month": "Ebben a hónapban",
+ "search_filters_features_option_subtitles": "Felirattal",
+ "search_filters_features_option_location": "Közelben",
"crash_page_you_found_a_bug": "Úgy néz ki, találtál egy hibát az Invidiousban.",
"crash_page_before_reporting": "Mielőtt jelentenéd a hibát:",
"crash_page_read_the_faq": "olvasd el a <a href=\"`x`\">Gyakran Ismételt Kérdéseket (GYIK)</a>",
@@ -460,5 +458,6 @@
"Italian (auto-generated)": "olasz (automatikusan generált)",
"Dutch (auto-generated)": "holland (automatikusan generált)",
"French (auto-generated)": "francia (automatikusan generált)",
- "Vietnamese (auto-generated)": "vietnámi (automatikusan generált)"
+ "Vietnamese (auto-generated)": "vietnámi (automatikusan generált)",
+ "search_filters_title": "Szűrők"
}
diff --git a/locales/id.json b/locales/id.json
index be15e8e1..71b7bdb1 100644
--- a/locales/id.json
+++ b/locales/id.json
@@ -26,15 +26,15 @@
"No": "Tidak",
"Import and Export Data": "Impor dan Ekspor Data",
"Import": "Impor",
- "Import Invidious data": "Impor data Invidious",
- "Import YouTube subscriptions": "Impor langganan YouTube",
+ "Import Invidious data": "Impor JSON data Invidious",
+ "Import YouTube subscriptions": "Impor langganan YouTube/OPML",
"Import FreeTube subscriptions (.db)": "Impor langganan FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Impor langganan NewPipe (.json)",
"Import NewPipe data (.zip)": "Impor data NewPipe (.zip)",
"Export": "Ekspor",
"Export subscriptions as OPML": "Ekspor langganan sebagai OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Ekspor langganan sebagai OPML (untuk NewPipe & FreeTube)",
- "Export data as JSON": "Ekspor data sebagai JSON",
+ "Export data as JSON": "Ekspor data Invidious sebagai JSON",
"Delete account?": "Hapus akun?",
"History": "Riwayat",
"An alternative front-end to YouTube": "Sebuah alternatif layar depan untuk YouTube",
@@ -71,7 +71,7 @@
"preferences_related_videos_label": "Tampilkan video terkait: ",
"preferences_annotations_label": "Tampilkan anotasi secara baku: ",
"preferences_extend_desc_label": "Perluas deskripsi video secara otomatis: ",
- "preferences_vr_mode_label": "Video interaktif 360°: ",
+ "preferences_vr_mode_label": "Video interaktif 360° (memerlukan WebGL): ",
"preferences_category_visual": "Preferensi visual",
"preferences_player_style_label": "Gaya pemutar: ",
"Dark mode: ": "Mode gelap: ",
@@ -128,7 +128,7 @@
"subscriptions_unseen_notifs_count_0": "{{count}} pemberitahuan belum dilihat",
"search": "cari",
"Log out": "Keluar",
- "Released under the AGPLv3 on Github.": "Dirilis di bawah AGPLv3 di Github.",
+ "Released under the AGPLv3 on Github.": "Dirilis di bawah AGPLv3 di GitHub.",
"Source available here.": "Sumber tersedia di sini.",
"View JavaScript license information.": "Tampilkan informasi lisensi JavaScript.",
"View privacy policy.": "Lihat kebijakan privasi.",
@@ -148,7 +148,6 @@
"Show less": "Tampilkan lebih sedikit",
"Watch on YouTube": "Tonton di YouTube",
"Switch Invidious Instance": "Ganti peladen Invidious",
- "Broken? Try another Invidious Instance": "Rusak? Coba peladen Invidious yang lain",
"Hide annotations": "Sembunyikan anotasi",
"Show annotations": "Tampilkan anotasi",
"Genre: ": "Genre: ",
@@ -345,33 +344,32 @@
"Videos": "Video",
"Playlists": "Daftar putar",
"Community": "Komunitas",
- "relevance": "Relevansi",
- "rating": "Penilaian",
- "date": "Tanggal unggah",
- "views": "Jumlah ditonton",
- "content_type": "Tipe",
- "duration": "Durasi",
- "features": "Fitur",
- "sort": "Urut Berdasarkan",
- "hour": "Jam Terakhir",
- "today": "Hari Ini",
- "week": "Pekan Ini",
- "month": "Bulan Ini",
- "year": "Tahun Ini",
- "video": "Video",
- "channel": "Kanal",
- "playlist": "Daftar Putar",
- "movie": "Film",
- "show": "Pertunjukan/Acara",
- "hd": "HD",
- "subtitles": "Takarir",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "Siaran Langsung",
- "4k": "4K",
- "location": "Lokasi",
- "hdr": "HDR",
- "filter": "Saring",
+ "search_filters_sort_option_relevance": "Relevansi",
+ "search_filters_sort_option_rating": "Penilaian",
+ "search_filters_sort_option_date": "Tanggal unggah",
+ "search_filters_sort_option_views": "Jumlah ditonton",
+ "search_filters_type_label": "Tipe",
+ "search_filters_duration_label": "Durasi",
+ "search_filters_features_label": "Fitur",
+ "search_filters_sort_label": "Urut Berdasarkan",
+ "search_filters_date_option_hour": "Jam Terakhir",
+ "search_filters_date_option_today": "Hari Ini",
+ "search_filters_date_option_week": "Pekan Ini",
+ "search_filters_date_option_month": "Bulan Ini",
+ "search_filters_date_option_year": "Tahun Ini",
+ "search_filters_type_option_video": "Video",
+ "search_filters_type_option_channel": "Kanal",
+ "search_filters_type_option_playlist": "Daftar Putar",
+ "search_filters_type_option_movie": "Film",
+ "search_filters_type_option_show": "Pertunjukan/Acara",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "Takarir",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "Siaran Langsung",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "Lokasi",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Versi saat ini: ",
"next_steps_error_message": "Setelah itu Anda harus mencoba: ",
"next_steps_error_message_refresh": "Segarkan",
@@ -380,8 +378,8 @@
"adminprefs_modified_source_code_url_label": "URL ke repositori kode sumber yang dimodifikasi",
"footer_source_code": "Kode sumber",
"footer_original_source_code": "Kode sumber yang asli",
- "short": "Pendek (< 4 menit)",
- "long": "Panjang (> 20 menit)",
+ "search_filters_duration_option_short": "Pendek (< 4 menit)",
+ "search_filters_duration_option_long": "Panjang (> 20 menit)",
"footer_modfied_source_code": "Kode sumber yang dimodifikasi",
"footer_documentation": "Dokumentasi",
"preferences_region_label": "Konten dari negara: ",
@@ -398,8 +396,8 @@
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_144p": "144p",
"invidious": "Invidious",
- "purchased": "Dibeli",
- "360": "360°",
+ "search_filters_features_option_purchased": "Dibeli",
+ "search_filters_features_option_three_sixty": "360°",
"none": "tidak ada",
"videoinfo_watch_on_youTube": "Tonton di YouTube",
"videoinfo_youTube_embed_link": "Tersemat",
@@ -416,5 +414,9 @@
"Video unavailable": "Video tidak tersedia",
"preferences_save_player_pos_label": "Simpan posisi pemutaran: ",
"crash_page_you_found_a_bug": "Sepertinya kamu telah menemukan masalah di invidious!",
- "crash_page_before_reporting": "Sebelum melaporkan masalah, pastikan anda memiliki:"
+ "crash_page_before_reporting": "Sebelum melaporkan masalah, pastikan anda memiliki:",
+ "English (United States)": "Inggris (US)",
+ "preferences_watch_history_label": "Aktifkan riwayat tontonan: ",
+ "English (United Kingdom)": "Inggris (UK)",
+ "search_filters_title": "Saring"
}
diff --git a/locales/it.json b/locales/it.json
index 411148c9..7ba5ff2d 100644
--- a/locales/it.json
+++ b/locales/it.json
@@ -27,7 +27,7 @@
"No": "No",
"Import and Export Data": "Importazione ed esportazione dati",
"Import": "Importa",
- "Import Invidious data": "Importa dati Invidious",
+ "Import Invidious data": "Importa dati Invidious in formato JSON",
"Import YouTube subscriptions": "Importa le iscrizioni da YouTube",
"Import FreeTube subscriptions (.db)": "Importa le iscrizioni da FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Importa le iscrizioni da NewPipe (.json)",
@@ -35,7 +35,7 @@
"Export": "Esporta",
"Export subscriptions as OPML": "Esporta gli abbonamenti come OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Esporta gli abbonamenti come OPML (per NewPipe e FreeTube)",
- "Export data as JSON": "Esporta i dati in formato JSON",
+ "Export data as JSON": "Esporta i dati Invidious in formato JSON",
"Delete account?": "Eliminare l'account?",
"History": "Cronologia",
"An alternative front-end to YouTube": "Un'interfaccia alternativa per YouTube",
@@ -347,32 +347,31 @@
"Videos": "Video",
"Playlists": "Playlist",
"Community": "Comunità",
- "relevance": "Pertinenza",
- "rating": "Valutazione",
- "date": "Data di caricamento",
- "views": "Numero di visualizzazioni",
- "content_type": "Tipo",
- "duration": "Durata",
- "features": "Caratteristiche",
- "sort": "Ordina per",
- "hour": "Ultima ora",
- "today": "Oggi",
- "week": "Questa settimana",
- "month": "Questo mese",
- "year": "Quest'anno",
- "video": "Video",
- "channel": "Canale",
- "playlist": "Playlist",
- "movie": "Film",
- "hd": "AD",
- "subtitles": "Sottotitoli / CC",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "In diretta",
- "4k": "4K",
- "location": "Posizione",
- "hdr": "HDR",
- "filter": "Filtra",
+ "search_filters_sort_option_relevance": "Pertinenza",
+ "search_filters_sort_option_rating": "Valutazione",
+ "search_filters_sort_option_date": "Data di caricamento",
+ "search_filters_sort_option_views": "Numero di visualizzazioni",
+ "search_filters_type_label": "Tipo",
+ "search_filters_duration_label": "Durata",
+ "search_filters_features_label": "Caratteristiche",
+ "search_filters_sort_label": "Ordina per",
+ "search_filters_date_option_hour": "Ultima ora",
+ "search_filters_date_option_today": "Oggi",
+ "search_filters_date_option_week": "Questa settimana",
+ "search_filters_date_option_month": "Questo mese",
+ "search_filters_date_option_year": "Quest'anno",
+ "search_filters_type_option_video": "Video",
+ "search_filters_type_option_channel": "Canale",
+ "search_filters_type_option_playlist": "Playlist",
+ "search_filters_type_option_movie": "Film",
+ "search_filters_features_option_hd": "AD",
+ "search_filters_features_option_subtitles": "Sottotitoli / CC",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "In diretta",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "Posizione",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Versione attuale: ",
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_360p": "360p",
@@ -382,9 +381,9 @@
"preferences_quality_dash_option_1440p": "1440p",
"preferences_quality_dash_option_2160p": "2160p",
"preferences_quality_dash_option_4320p": "4320p",
- "360": "360°",
+ "search_filters_features_option_three_sixty": "360°",
"preferences_quality_dash_option_144p": "144p",
- "Released under the AGPLv3 on Github.": "Rilasciato su Github con licenza AGPLv3.",
+ "Released under the AGPLv3 on Github.": "Rilasciato su GitHub con licenza AGPLv3.",
"preferences_quality_option_medium": "Media",
"preferences_quality_option_small": "Piccola",
"preferences_quality_dash_option_best": "Migliore",
@@ -409,22 +408,22 @@
"preferences_automatic_instance_redirect_label": "Reindirizzamento automatico dell'istanza (ripiego su redirect.invidious.io): ",
"Video unavailable": "Video non disponibile",
"preferences_show_nick_label": "Mostra nickname in alto: ",
- "short": "Corto (< 4 minuti)",
"videoinfo_youTube_embed_link": "Incorpora",
"videoinfo_invidious_embed_link": "Incorpora collegamento",
"user_created_playlists": "playlist create da `x`",
"preferences_save_player_pos_label": "Memorizza il minutaggio raggiunto dal video: ",
- "purchased": "Acquistato",
"preferences_quality_option_dash": "DASH (qualità adattiva)",
"preferences_region_label": "Nazione del contenuto: ",
"preferences_category_misc": "Preferenze varie",
- "show": "Serie",
- "long": "Lungo (> 20 minuti)",
"next_steps_error_message": "Dopodiché dovresti provare a: ",
"next_steps_error_message_refresh": "Aggiornare",
"footer_donate_page": "Dona",
"footer_source_code": "Codice sorgente",
"adminprefs_modified_source_code_url_label": "Link per il repository del codice sorgente modificato",
"Show more": "Mostra di più",
- "Broken? Try another Invidious Instance": "Non funzionante? Prova un’altra istanza Invidious"
+ "search_filters_title": "Filtra",
+ "search_filters_type_option_show": "Serie",
+ "search_filters_duration_option_short": "Corto (< 4 minuti)",
+ "search_filters_duration_option_long": "Lungo (> 20 minuti)",
+ "search_filters_features_option_purchased": "Acquistato"
}
diff --git a/locales/ja.json b/locales/ja.json
index 9708c0ea..20d3c20e 100644
--- a/locales/ja.json
+++ b/locales/ja.json
@@ -148,7 +148,6 @@
"Show less": "表示を減らす",
"Watch on YouTube": "YouTube で視聴",
"Switch Invidious Instance": "Invidiousインスタンスの変更",
- "Broken? Try another Invidious Instance": "壊れる?違うInvidiousインスタンスを試してみる",
"Hide annotations": "アノテーションを隠す",
"Show annotations": "アノテーションを表示",
"Genre: ": "ジャンル: ",
@@ -345,44 +344,43 @@
"Videos": "動画",
"Playlists": "プレイリスト",
"Community": "コミュニティ",
- "relevance": "関連",
- "rating": "評価",
- "date": "時刻",
- "views": "再生回数",
- "content_type": "コンテンツの種類",
- "duration": "再生時間",
- "features": "機能",
- "sort": "順番",
- "hour": "1時間前",
- "today": "今日",
- "week": "今週",
- "month": "今月",
- "year": "今年",
- "video": "動画",
- "channel": "チャンネル",
- "playlist": "再生リスト",
- "movie": "映画",
- "show": "番組",
- "hd": "HD",
- "subtitles": "字幕",
- "creative_commons": "クリエイティブ・コモンズ",
- "3d": "3D",
- "live": "生配信",
- "4k": "4K",
- "location": "場所",
- "hdr": "HDR",
- "filter": "フィルタ",
+ "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_date_option_hour": "1時間前",
+ "search_filters_date_option_today": "今日",
+ "search_filters_date_option_week": "今週",
+ "search_filters_date_option_month": "今月",
+ "search_filters_date_option_year": "今年",
+ "search_filters_type_option_video": "動画",
+ "search_filters_type_option_channel": "チャンネル",
+ "search_filters_type_option_playlist": "再生リスト",
+ "search_filters_type_option_movie": "映画",
+ "search_filters_type_option_show": "番組",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "字幕",
+ "search_filters_features_option_c_commons": "クリエイティブ・コモンズ",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "生配信",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "場所",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "現在のバージョン: ",
"next_steps_error_message": "下記のものを試して下さい: ",
"next_steps_error_message_refresh": "再読込",
"next_steps_error_message_go_to_youtube": "YouTubeへ",
- "short": "4 分未満",
+ "search_filters_duration_option_short": "4 分未満",
"footer_documentation": "文書",
"footer_source_code": "ソースコード",
"footer_original_source_code": "ソースコード(元)",
"footer_modfied_source_code": "ソースコード(編集)",
"adminprefs_modified_source_code_url_label": "編集したソースコードのレポジトリーURL",
- "long": "20 分以上",
+ "search_filters_duration_option_long": "20 分以上",
"preferences_region_label": "地域: ",
"footer_donate_page": "寄付する",
"preferences_quality_dash_label": "優先するDash画質 : ",
@@ -404,7 +402,7 @@
"videoinfo_invidious_embed_link": "埋め込みリンク",
"none": "なし",
"download_subtitles": "字幕 - `x` (.vtt)",
- "purchased": "購入済み",
+ "search_filters_features_option_purchased": "購入済み",
"preferences_quality_option_dash": "DASH (適切な品質)",
"preferences_quality_dash_option_worst": "最悪",
"preferences_quality_dash_option_best": "最高",
@@ -434,5 +432,6 @@
"Spanish (Mexico)": "スペイン語 (メキシコ)",
"Spanish (Spain)": "スペイン語 (スペイン)",
"Vietnamese (auto-generated)": "ベトナム語 (自動生成)",
- "360": "360°"
+ "search_filters_title": "フィルタ",
+ "search_filters_features_option_three_sixty": "360°"
}
diff --git a/locales/ko.json b/locales/ko.json
index a579fe56..12c2b31f 100644
--- a/locales/ko.json
+++ b/locales/ko.json
@@ -86,7 +86,7 @@
"generic_playlists_count_0": "{{count}} 재생목록",
"generic_subscribers_count_0": "{{count}} 구독자",
"generic_subscriptions_count_0": "{{count}} 구독",
- "playlist": "재생목록",
+ "search_filters_type_option_playlist": "재생목록",
"Korean": "한국어",
"Japanese": "일본어",
"Greek": "그리스어",
@@ -129,7 +129,7 @@
"Delete playlist": "재생목록 삭제",
"Delete playlist `x`?": "재생목록 `x` 를 삭제 하시겠습니까?",
"Updated `x` ago": "`x` 전에 업데이트됨",
- "Released under the AGPLv3 on Github.": "Github에 AGPLv3 으로 배포됩니다.",
+ "Released under the AGPLv3 on Github.": "GitHub에 AGPLv3 으로 배포됩니다.",
"View all playlists": "모든 재생목록 보기",
"Private": "비공개",
"Unlisted": "목록에 없음",
@@ -195,16 +195,15 @@
"Maori": "마오리어",
"Maltese": "몰타어",
"Wrong answer": "잘못된 답변",
- "live": "실시간",
- "3d": "3D",
- "location": "지역",
- "4k": "4K",
- "filter": "필터",
- "hdr": "HDR",
+ "search_filters_features_option_live": "실시간",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_location": "지역",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "현재 버전: ",
"next_steps_error_message_refresh": "새로 고침",
"next_steps_error_message_go_to_youtube": "YouTube로 가기",
- "subtitles": "자막",
+ "search_filters_features_option_subtitles": "자막",
"`x` marked it with a ❤": "`x`님의 ❤",
"Download as: ": "다음으로 다운로드: ",
"Download": "다운로드",
@@ -219,7 +218,7 @@
"Latvian": "라트비아어",
"Latin": "라틴어",
"Lao": "라오어",
- "channel": "채널",
+ "search_filters_type_option_channel": "채널",
"Kyrgyz": "키르기스어",
"Kurdish": "쿠르드어",
"Khmer": "크메르어",
@@ -279,7 +278,7 @@
"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가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
"Shared `x`": "공유된 `x`",
"Whitelisted regions: ": "차단되지 않은 지역: ",
- "views": "조회수",
+ "search_filters_sort_option_views": "조회수",
"Please log in": "로그인하세요",
"Password cannot be longer than 55 characters": "비밀번호는 55자 이하여야 합니다",
"Password cannot be empty": "비밀번호는 비워둘 수 없습니다",
@@ -298,7 +297,6 @@
"Empty playlist": "재생목록 비어 있음",
"Show annotations": "주석 보이기",
"Hide annotations": "주석 숨기기",
- "Broken? Try another Invidious Instance": "안되나요? 다른 Invidious 인스턴스를 시도해보세요",
"Switch Invidious Instance": "Invidious 인스턴스 변경",
"Spanish": "스페인어",
"Southern Sotho": "소토어",
@@ -343,12 +341,12 @@
"Premieres `x`": "최초 공개 `x`",
"Premieres in `x`": "`x` 에 최초 공개",
"next_steps_error_message": "다음 방법을 시도해 보세요: ",
- "creative_commons": "크리에이티브 커먼즈",
- "duration": "길이",
- "content_type": "구분",
- "date": "업로드 날짜",
- "rating": "평점",
- "relevance": "관련성",
+ "search_filters_features_option_c_commons": "크리에이티브 커먼즈",
+ "search_filters_duration_label": "길이",
+ "search_filters_type_label": "구분",
+ "search_filters_sort_option_date": "업로드 날짜",
+ "search_filters_sort_option_rating": "평점",
+ "search_filters_sort_option_relevance": "관련성",
"Community": "커뮤니티",
"Videos": "동영상",
"Video mode": "비디오 모드",
@@ -365,22 +363,23 @@
"Rating: ": "평점: ",
"About": "정보",
"Top": "최고",
- "hd": "HD",
- "show": "쇼",
- "movie": "영화",
- "video": "동영상",
- "year": "올해",
- "month": "이번 달",
- "week": "이번 주",
- "today": "오늘",
- "hour": "지난 1시간",
- "sort": "정렬기준",
- "features": "기능별",
- "short": "4분 미만",
- "long": "20분 초과",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_type_option_show": "쇼",
+ "search_filters_type_option_movie": "영화",
+ "search_filters_type_option_video": "동영상",
+ "search_filters_date_option_year": "올해",
+ "search_filters_date_option_month": "이번 달",
+ "search_filters_date_option_week": "이번 주",
+ "search_filters_date_option_today": "오늘",
+ "search_filters_date_option_hour": "지난 1시간",
+ "search_filters_sort_label": "정렬기준",
+ "search_filters_features_label": "기능별",
+ "search_filters_duration_option_short": "4분 미만",
+ "search_filters_duration_option_long": "20분 초과",
"footer_documentation": "문서",
"footer_source_code": "소스 코드",
"footer_original_source_code": "원본 소스 코드",
"footer_modfied_source_code": "수정된 소스 코드",
- "adminprefs_modified_source_code_url_label": "수정된 소스 코드 저장소의 URL"
+ "adminprefs_modified_source_code_url_label": "수정된 소스 코드 저장소의 URL",
+ "search_filters_title": "필터"
}
diff --git a/locales/lt.json b/locales/lt.json
index a5cee472..607b3705 100644
--- a/locales/lt.json
+++ b/locales/lt.json
@@ -121,7 +121,7 @@
"Subscriptions": "Prenumeratos",
"search": "ieškoti",
"Log out": "Atsijungti",
- "Released under the AGPLv3 on Github.": "Išleista pagal AGPLv3 licenciją Github.",
+ "Released under the AGPLv3 on Github.": "Išleista pagal AGPLv3 licenciją GitHub.",
"Source available here.": "Kodas prieinamas čia.",
"View JavaScript license information.": "Žiūrėti JavaScript licencijos informaciją.",
"View privacy policy.": "Žiūrėti privatumo politiką.",
@@ -141,7 +141,6 @@
"Show less": "Rodyti mažiau",
"Watch on YouTube": "Žiaurėti Youtube",
"Switch Invidious Instance": "Keisti Invidious šaltinį",
- "Broken? Try another Invidious Instance": "Neveikia? Bandyk kitą Invidious šaltinį",
"Hide annotations": "Slėpti anotacijas",
"Show annotations": "Rodyti anotacijas",
"Genre: ": "Žanras: ",
@@ -329,39 +328,38 @@
"Videos": "Vaizdo įrašai",
"Playlists": "Grojaraiščiai",
"Community": "Bendruomenė",
- "relevance": "Aktualumas",
- "rating": "Reitingas",
- "date": "Įkėlimo data",
- "views": "Peržiūrų skaičius",
- "content_type": "Tipas",
- "duration": "Trukmė",
- "features": "Funkcijos",
- "sort": "Rūšiuoti pagal",
- "hour": "Per paskutinę valandą",
- "today": "Šiandien",
- "week": "Šią savaitę",
- "month": "Šį mėnesį",
- "year": "Šiais metais",
- "video": "Vaizdo įrašas",
- "channel": "Kanalas",
- "playlist": "Grojaraštis",
- "movie": "Filmas",
- "show": "Serialas",
- "hd": "HD",
- "subtitles": "Subtitrai/CC",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "Tiesiogiai",
- "4k": "4K",
- "location": "Vietovė",
- "hdr": "HDR",
- "filter": "Filtras",
+ "search_filters_sort_option_relevance": "Aktualumas",
+ "search_filters_sort_option_rating": "Reitingas",
+ "search_filters_sort_option_date": "Įkėlimo data",
+ "search_filters_sort_option_views": "Peržiūrų skaičius",
+ "search_filters_type_label": "Tipas",
+ "search_filters_duration_label": "Trukmė",
+ "search_filters_features_label": "Funkcijos",
+ "search_filters_sort_label": "Rūšiuoti pagal",
+ "search_filters_date_option_hour": "Per paskutinę valandą",
+ "search_filters_date_option_today": "Šiandien",
+ "search_filters_date_option_week": "Šią savaitę",
+ "search_filters_date_option_month": "Šį mėnesį",
+ "search_filters_date_option_year": "Šiais metais",
+ "search_filters_type_option_video": "Vaizdo įrašas",
+ "search_filters_type_option_channel": "Kanalas",
+ "search_filters_type_option_playlist": "Grojaraštis",
+ "search_filters_type_option_movie": "Filmas",
+ "search_filters_type_option_show": "Serialas",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "Subtitrai/CC",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "Tiesiogiai",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "Vietovė",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Dabartinė versija: ",
"next_steps_error_message": "Po to turėtumėte pabandyti: ",
"next_steps_error_message_refresh": "Atnaujinti",
"next_steps_error_message_go_to_youtube": "Eiti į YouTube",
- "short": "Trumpas (< 4 minučių)",
- "long": "Ilgas (> 20 minučių)",
+ "search_filters_duration_option_short": "Trumpas (< 4 minučių)",
+ "search_filters_duration_option_long": "Ilgas (> 20 minučių)",
"footer_documentation": "Dokumentacija",
"footer_source_code": "Pirminis kodas",
"footer_original_source_code": "Pradinis pirminis kodas",
@@ -372,5 +370,6 @@
"preferences_quality_dash_label": "Pageidaujama DASH vaizdo kokybė: ",
"preferences_quality_dash_option_best": "Geriausia",
"preferences_quality_dash_option_worst": "Blogiausia",
- "preferences_quality_dash_option_auto": "Automatinis"
+ "preferences_quality_dash_option_auto": "Automatinis",
+ "search_filters_title": "Filtras"
}
diff --git a/locales/nb-NO.json b/locales/nb-NO.json
index 1c5ffbc8..8d80c10c 100644
--- a/locales/nb-NO.json
+++ b/locales/nb-NO.json
@@ -29,7 +29,7 @@
"Export": "Eksporter",
"Export subscriptions as OPML": "Eksporter abonnementer som OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksporter abonnementer som OPML (for NewPipe og FreeTube)",
- "Export data as JSON": "Eksporter data som JSON",
+ "Export data as JSON": "Eksporter Invidiousdata som JSON",
"Delete account?": "Slett konto?",
"History": "Historikk",
"An alternative front-end to YouTube": "En alternativ grenseflate for YouTube",
@@ -66,7 +66,7 @@
"preferences_related_videos_label": "Vis relaterte videoer? ",
"preferences_annotations_label": "Vis merknader som forvalg? ",
"preferences_extend_desc_label": "Utvid videobeskrivelse automatisk: ",
- "preferences_vr_mode_label": "Interaktive 360-gradersfilmer: ",
+ "preferences_vr_mode_label": "Interaktive 360-gradersfilmer (krever WebGL): ",
"preferences_category_visual": "Visuelle innstillinger",
"preferences_player_style_label": "Avspillerstil: ",
"Dark mode: ": "Mørk drakt: ",
@@ -121,7 +121,7 @@
"Subscriptions": "Abonnement",
"search": "søk",
"Log out": "Logg ut",
- "Released under the AGPLv3 on Github.": "Tilgjengelig med AGPLv3-lisens på Github.",
+ "Released under the AGPLv3 on Github.": "Tilgjengelig med AGPLv3-lisens på GitHub.",
"Source available here.": "Kildekode tilgjengelig her.",
"View JavaScript license information.": "Vis JavaScript-lisensinfo.",
"View privacy policy.": "Vis personvernspraksis.",
@@ -141,7 +141,6 @@
"Show less": "Vis mindre",
"Watch on YouTube": "Vis video på YouTube",
"Switch Invidious Instance": "Bytt Invidious-instans",
- "Broken? Try another Invidious Instance": "Knekt? Forsøk en annen Invidious-instans",
"Hide annotations": "Skjul merknader",
"Show annotations": "Vis merknader",
"Genre: ": "Sjanger: ",
@@ -199,7 +198,7 @@
"No such user": "Ugyldig bruker",
"Token is expired, please try again": "Symbol utløpt, prøv igjen",
"English": "Engelsk",
- "English (auto-generated)": "Engelsk (auto-generert)",
+ "English (auto-generated)": "Engelsk (laget automatisk)",
"Afrikaans": "Afrikansk",
"Albanian": "Albansk",
"Amharic": "Amharisk",
@@ -329,40 +328,39 @@
"Videos": "Videoer",
"Playlists": "Spillelister",
"Community": "Gemenskap",
- "relevance": "relevans",
- "rating": "vurdering",
- "date": "dato",
- "views": "visninger",
- "content_type": "innholdstype",
- "duration": "varighet",
- "features": "funksjoner",
- "sort": "sorter",
- "hour": "time",
- "today": "i dag",
- "week": "uke",
- "month": "måned",
- "year": "år",
- "video": "video",
- "channel": "kanal",
- "playlist": "spilleliste",
- "movie": "film",
- "show": "vis",
- "hd": "HD",
- "subtitles": "undertekster",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "direkte",
- "4k": "4k",
- "location": "sted",
- "hdr": "HDR",
- "filter": "filtrer",
+ "search_filters_sort_option_relevance": "relevans",
+ "search_filters_sort_option_rating": "vurdering",
+ "search_filters_sort_option_date": "dato",
+ "search_filters_sort_option_views": "visninger",
+ "search_filters_type_label": "innholdstype",
+ "search_filters_duration_label": "varighet",
+ "search_filters_features_label": "funksjoner",
+ "search_filters_sort_label": "sorter",
+ "search_filters_date_option_hour": "time",
+ "search_filters_date_option_today": "i dag",
+ "search_filters_date_option_week": "uke",
+ "search_filters_date_option_month": "måned",
+ "search_filters_date_option_year": "år",
+ "search_filters_type_option_video": "video",
+ "search_filters_type_option_channel": "kanal",
+ "search_filters_type_option_playlist": "spilleliste",
+ "search_filters_type_option_movie": "film",
+ "search_filters_type_option_show": "vis",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "undertekster",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "direkte",
+ "search_filters_features_option_four_k": "4k",
+ "search_filters_features_option_location": "sted",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Gjeldende versjon: ",
"next_steps_error_message": "Etterpå bør du prøve dette: ",
"next_steps_error_message_refresh": "Gjenoppfrisk",
"next_steps_error_message_go_to_youtube": "Gå til YouTube",
- "long": "Lang (> 20 minutter)",
+ "search_filters_duration_option_long": "Lang (> 20 minutter)",
"footer_donate_page": "Doner",
- "short": "Kort (< 4 minutter)",
+ "search_filters_duration_option_short": "Kort (< 4 minutter)",
"footer_documentation": "Dokumentasjon",
"footer_source_code": "Kildekode",
"footer_original_source_code": "Opprinnelig kildekode",
@@ -384,8 +382,8 @@
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_144p": "144p",
"invidious": "Invidious",
- "purchased": "Kjøpt",
- "360": "360°",
+ "search_filters_features_option_purchased": "Kjøpt",
+ "search_filters_features_option_three_sixty": "360°",
"none": "intet",
"videoinfo_watch_on_youTube": "Se på YouTube",
"videoinfo_youTube_embed_link": "Bak inn",
@@ -432,10 +430,35 @@
"generic_count_years": "{{count}} år",
"generic_count_years_plural": "{{count}} år",
"crash_page_read_the_faq": "lest de <a href=\"`x`\">Ofte stilte spørsmålene (OSS/FAQ)</a>",
- "crash_page_search_issue": "søkt etter <a href=\"`x`\">eksisterende utfordringer på Github</a>",
+ "crash_page_search_issue": "søkt etter <a href=\"`x`\">eksisterende utfordringer på GitHub</a>",
"crash_page_you_found_a_bug": "Det ser ut til at du fant en feil i Invidious!",
"crash_page_refresh": "forsøkt å <a href=\"`x`\">laste siden på nytt</a>",
"crash_page_switch_instance": "forsøkt et <a href=\"`x`\">annet eksemplar</a>",
"crash_page_before_reporting": "Før du rapporterer en feil, sikre at du har:",
- "crash_page_report_issue": "Hvis intet av det overnevnte hjalp, <a href=\"`x`\">lag en ny utfordring på Github</a> (fortrinnsvis på engelsk) og ta med følgende tekstbit i meldingen dit (IKKE oversett denne teksten):"
+ "crash_page_report_issue": "Sett at det overnevnte ikke hjalp, <a href=\"`x`\">lag en ny utfordring på GitHub</a> (fortrinnsvis på engelsk) og få med følgende tekstbit i meldingen dithen (IKKE oversett denne teksten):",
+ "English (United Kingdom)": "Engelsk (Storbritannia)",
+ "English (United States)": "Engelsk (USA)",
+ "Cantonese (Hong Kong)": "Kantonesisk (Hong Kong)",
+ "Portuguese (Brazil)": "Portugisisk (Brasil)",
+ "Spanish (Mexico)": "Spansk (Mexico)",
+ "Spanish (Spain)": "Spansk (Spania)",
+ "Spanish (auto-generated)": "Spansk (laget automatisk)",
+ "Vietnamese (auto-generated)": "Vietnamesisk (laget automatisk)",
+ "preferences_watch_history_label": "Aktiver seerhistorikk: ",
+ "Chinese": "Kinesisk",
+ "Chinese (China)": "Kinesisk (Kina)",
+ "Chinese (Hong Kong)": "Kinesisk (Hong Kong)",
+ "Chinese (Taiwan)": "Kinesisk (Taiwan)",
+ "French (auto-generated)": "Fransk (laget automatisk)",
+ "German (auto-generated)": "Tysk (laget automatisk)",
+ "Indonesian (auto-generated)": "Indonesisk (laget automatisk)",
+ "Interlingue": "Interlingue",
+ "Italian (auto-generated)": "Italiensk (laget automatisk)",
+ "Japanese (auto-generated)": "Japansk (laget automatisk)",
+ "Korean (auto-generated)": "Koreansk (laget automatisk)",
+ "Portuguese (auto-generated)": "Portugisisk (laget automatisk)",
+ "Russian (auto-generated)": "Russisk (laget automatisk)",
+ "Dutch (auto-generated)": "Nederlandsk (laget automatisk)",
+ "Turkish (auto-generated)": "Tyrkisk (laget automatisk)",
+ "search_filters_title": "Filtrer"
}
diff --git a/locales/nl.json b/locales/nl.json
index d148d872..7e9ddba6 100644
--- a/locales/nl.json
+++ b/locales/nl.json
@@ -323,33 +323,32 @@
"Videos": "Video's",
"Playlists": "Afspeellijsten",
"Community": "Gemeenschap",
- "relevance": "relevantie",
- "rating": "beoordeling",
- "date": "datum",
- "views": "keren bekeken",
- "content_type": "Type inhoud",
- "duration": "duur",
- "features": "eigenschappen",
- "sort": "sorteren",
- "hour": "uur",
- "today": "vandaag",
- "week": "week",
- "month": "maand",
- "year": "jaar",
- "video": "video",
- "channel": "kanaal",
- "playlist": "afspeellijst",
- "movie": "film",
- "show": "show",
- "hd": "HD",
- "subtitles": "ondertitels",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "Live",
- "4k": "4K",
- "location": "locatie",
- "hdr": "HDR",
- "filter": "verfijnen",
+ "search_filters_sort_option_relevance": "relevantie",
+ "search_filters_sort_option_rating": "beoordeling",
+ "search_filters_sort_option_date": "datum",
+ "search_filters_sort_option_views": "keren bekeken",
+ "search_filters_type_label": "Type inhoud",
+ "search_filters_duration_label": "duur",
+ "search_filters_features_label": "eigenschappen",
+ "search_filters_sort_label": "sorteren",
+ "search_filters_date_option_hour": "uur",
+ "search_filters_date_option_today": "vandaag",
+ "search_filters_date_option_week": "week",
+ "search_filters_date_option_month": "maand",
+ "search_filters_date_option_year": "jaar",
+ "search_filters_type_option_video": "video",
+ "search_filters_type_option_channel": "kanaal",
+ "search_filters_type_option_playlist": "afspeellijst",
+ "search_filters_type_option_movie": "film",
+ "search_filters_type_option_show": "show",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "ondertitels",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "Live",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "locatie",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Huidige versie: ",
"Switch Invidious Instance": "Schakel tussen de Invidious Instanties",
"preferences_automatic_instance_redirect_label": "Automatische instantie-omleiding (terugval naar redirect.invidious.io): ",
@@ -357,8 +356,8 @@
"preferences_region_label": "Inhoud land: ",
"preferences_category_misc": "Diverse voorkeuren",
"preferences_show_nick_label": "Toon bijnaam bovenaan: ",
- "Released under the AGPLv3 on Github.": "Uitgebracht onder de AGPLv3 op Github.",
- "short": "Kort (<4 minuten)",
+ "Released under the AGPLv3 on Github.": "Uitgebracht onder de AGPLv3 op GitHub.",
+ "search_filters_duration_option_short": "Kort (<4 minuten)",
"next_steps_error_message_refresh": "Vernieuwen",
"next_steps_error_message_go_to_youtube": "Ga naar YouTube",
"footer_donate_page": "Doneren",
@@ -366,10 +365,9 @@
"footer_original_source_code": "Originele bron-code",
"footer_modfied_source_code": "Gewijzigde bron-code",
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
- "Broken? Try another Invidious Instance": "Kapot? Probeer een andere Invidious Instantie",
"next_steps_error_message": "Waarna u moet proberen om: ",
"footer_source_code": "Bron-code",
- "long": "Lang (> 20 minuten)",
+ "search_filters_duration_option_long": "Lang (> 20 minuten)",
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
"preferences_quality_option_hd720": "HD720",
"preferences_quality_option_medium": "Gemiddeld",
@@ -397,6 +395,7 @@
"Video unavailable": "Video onbeschikbaar",
"preferences_save_player_pos_label": "Huidig afspeeltijdstip opslaan: ",
"none": "geen",
- "purchased": "Gekocht",
- "360": "360º"
+ "search_filters_features_option_purchased": "Gekocht",
+ "search_filters_features_option_three_sixty": "360º",
+ "search_filters_title": "Verfijnen"
}
diff --git a/locales/pl.json b/locales/pl.json
index 0f4e0927..37f951a3 100644
--- a/locales/pl.json
+++ b/locales/pl.json
@@ -140,7 +140,6 @@
"Show less": "Pokaż mniej",
"Watch on YouTube": "Zobacz film na YouTube",
"Switch Invidious Instance": "Przełącz instancję Invidious",
- "Broken? Try another Invidious Instance": "Nie działa? Spróbuj innej instancji Invidious",
"Hide annotations": "Ukryj adnotacje",
"Show annotations": "Pokaż adnotacje",
"Genre: ": "Gatunek: ",
@@ -328,33 +327,32 @@
"Videos": "Filmy",
"Playlists": "Playlisty",
"Community": "Społeczność",
- "relevance": "Trafność",
- "rating": "Ocena",
- "date": "data",
- "views": "Liczba wyświetleń",
- "content_type": "Typ",
- "duration": "Długość",
- "features": "Funkcje",
- "sort": "sortuj",
- "hour": "godzina",
- "today": "dzisiaj",
- "week": "tydzień",
- "month": "miesiąc",
- "year": "rok",
- "video": "Film",
- "channel": "kanał",
- "playlist": "playlista",
- "movie": "film",
- "show": "pokaż",
- "hd": "hd",
- "subtitles": "napisy",
- "creative_commons": "creative_commons",
- "3d": "3d",
- "live": "Na żywo",
- "4k": "4k",
- "location": "Lokalizacja",
- "hdr": "hdr",
- "filter": "filtr",
+ "search_filters_sort_option_relevance": "Trafność",
+ "search_filters_sort_option_rating": "Ocena",
+ "search_filters_sort_option_date": "data",
+ "search_filters_sort_option_views": "Liczba wyświetleń",
+ "search_filters_type_label": "Typ",
+ "search_filters_duration_label": "Długość",
+ "search_filters_features_label": "Funkcje",
+ "search_filters_sort_label": "sortuj",
+ "search_filters_date_option_hour": "godzina",
+ "search_filters_date_option_today": "dzisiaj",
+ "search_filters_date_option_week": "tydzień",
+ "search_filters_date_option_month": "miesiąc",
+ "search_filters_date_option_year": "rok",
+ "search_filters_type_option_video": "Film",
+ "search_filters_type_option_channel": "kanał",
+ "search_filters_type_option_playlist": "playlista",
+ "search_filters_type_option_movie": "film",
+ "search_filters_type_option_show": "pokaż",
+ "search_filters_features_option_hd": "hd",
+ "search_filters_features_option_subtitles": "napisy",
+ "search_filters_features_option_c_commons": "creative_commons",
+ "search_filters_features_option_three_d": "3d",
+ "search_filters_features_option_live": "Na żywo",
+ "search_filters_features_option_four_k": "4k",
+ "search_filters_features_option_location": "Lokalizacja",
+ "search_filters_features_option_hdr": "hdr",
"Current version: ": "Aktualna wersja: ",
"next_steps_error_message": "Po czym powinien*ś spróbować: ",
"next_steps_error_message_refresh": "Odśwież",
@@ -432,8 +430,8 @@
"preferences_quality_dash_label": "Preferowana jakość filmu DASH: ",
"preferences_quality_dash_option_4320p": "4320p",
"preferences_quality_dash_option_2160p": "2160p",
- "purchased": "Zakupione",
- "360": "360°",
+ "search_filters_features_option_purchased": "Zakupione",
+ "search_filters_features_option_three_sixty": "360°",
"footer_donate_page": "Dotacja",
"none": "żadne",
"videoinfo_started_streaming_x_ago": "Transmisja rozpoczęta `x` temu",
@@ -446,9 +444,9 @@
"Video unavailable": "Film niedostępny",
"preferences_save_player_pos_label": "Zapisz pozycję odtwarzania: ",
"preferences_region_label": "Region zawartości: ",
- "Released under the AGPLv3 on Github.": "Wydany na licencji AGPLv3 na Github.",
- "short": "Krótkie (< 4 minutes)",
- "long": "Długie (> 20 minutes)",
+ "Released under the AGPLv3 on Github.": "Wydany na licencji AGPLv3 na GitHub.",
+ "search_filters_duration_option_short": "Krótkie (< 4 minutes)",
+ "search_filters_duration_option_long": "Długie (> 20 minutes)",
"footer_documentation": "Dokumentacja",
"footer_source_code": "Kod źródłowy",
"footer_modfied_source_code": "Zmodyfikowany Kod źródłowy",
@@ -476,5 +474,6 @@
"Japanese (auto-generated)": "japoński (wygenerowany automatycznie)",
"Russian (auto-generated)": "rosyjski (wygenerowany automatycznie)",
"Portuguese (auto-generated)": "portugalski (wygenerowany automatycznie)",
- "Portuguese (Brazil)": "portugalski (Brazylia)"
+ "Portuguese (Brazil)": "portugalski (Brazylia)",
+ "search_filters_title": "Filtr"
}
diff --git a/locales/pt-BR.json b/locales/pt-BR.json
index 71a232c7..f3a05a1a 100644
--- a/locales/pt-BR.json
+++ b/locales/pt-BR.json
@@ -123,7 +123,7 @@
"Subscriptions": "Inscrições",
"search": "Pesquisar",
"Log out": "Sair",
- "Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no Github.",
+ "Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no GitHub.",
"Source available here.": "Código-fonte disponível aqui.",
"View JavaScript license information.": "Ver informações da licença do JavaScript.",
"View privacy policy.": "Ver a política de privacidade.",
@@ -143,7 +143,6 @@
"Show less": "Mostrar menos",
"Watch on YouTube": "Assistir no YouTube",
"Switch Invidious Instance": "Mudar a instância do Invidious",
- "Broken? Try another Invidious Instance": "Quebrou? Tente outra Instância do Invidious",
"Hide annotations": "Ocultar anotações",
"Show annotations": "Mostrar anotações",
"Genre: ": "Gênero: ",
@@ -345,41 +344,40 @@
"Videos": "Vídeos",
"Playlists": "Listas de reprodução",
"Community": "Comunidade",
- "relevance": "relevância",
- "rating": "avaliação",
- "date": "data",
- "views": "visualizações",
- "content_type": "content_type",
- "duration": "duração",
- "features": "recursos",
- "sort": "ordenar",
- "hour": "hora",
- "today": "hoje",
- "week": "semana",
- "month": "mês",
- "year": "ano",
- "video": "vídeo",
- "channel": "Canal",
- "playlist": "playlist",
- "movie": "filme",
- "show": "show",
- "hd": "hd",
- "subtitles": "legendas",
- "creative_commons": "creative_commons",
- "3d": "3d",
- "live": "ao vivo",
- "4k": "4k",
- "location": "localização",
- "hdr": "hdr",
- "filter": "filtro",
+ "search_filters_sort_option_relevance": "relevância",
+ "search_filters_sort_option_rating": "avaliação",
+ "search_filters_sort_option_date": "data",
+ "search_filters_sort_option_views": "visualizações",
+ "search_filters_type_label": "content_type",
+ "search_filters_duration_label": "duração",
+ "search_filters_features_label": "recursos",
+ "search_filters_sort_label": "ordenar",
+ "search_filters_date_option_hour": "hora",
+ "search_filters_date_option_today": "hoje",
+ "search_filters_date_option_week": "semana",
+ "search_filters_date_option_month": "mês",
+ "search_filters_date_option_year": "ano",
+ "search_filters_type_option_video": "vídeo",
+ "search_filters_type_option_channel": "Canal",
+ "search_filters_type_option_playlist": "playlist",
+ "search_filters_type_option_movie": "filme",
+ "search_filters_type_option_show": "show",
+ "search_filters_features_option_hd": "hd",
+ "search_filters_features_option_subtitles": "legendas",
+ "search_filters_features_option_c_commons": "creative_commons",
+ "search_filters_features_option_three_d": "3d",
+ "search_filters_features_option_live": "ao vivo",
+ "search_filters_features_option_four_k": "4k",
+ "search_filters_features_option_location": "localização",
+ "search_filters_features_option_hdr": "hdr",
"Current version: ": "Versão atual: ",
"next_steps_error_message": "Depois disso, você deve tentar: ",
"next_steps_error_message_refresh": "Atualizar",
"next_steps_error_message_go_to_youtube": "Ir para o YouTube",
"footer_donate_page": "Doe",
"adminprefs_modified_source_code_url_label": "URL para repositório de código fonte modificado",
- "long": "Longo (> 20 minutos)",
- "short": "Curto (< 4 minutos)",
+ "search_filters_duration_option_long": "Longo (> 20 minutos)",
+ "search_filters_duration_option_short": "Curto (< 4 minutos)",
"footer_documentation": "Documentação",
"footer_source_code": "Código fonte",
"footer_original_source_code": "Código fonte original",
@@ -404,10 +402,10 @@
"crash_page_you_found_a_bug": "Parece que você encontrou um erro no Invidious!",
"crash_page_before_reporting": "Antes de reportar um erro, verifique se você:",
"preferences_save_player_pos_label": "Salvar a posição de reprodução: ",
- "purchased": "Comprado",
+ "search_filters_features_option_purchased": "Comprado",
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>",
"crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
- "crash_page_search_issue": "procurou por um <a href=\"`x`\">erro existente no Github</a>",
+ "crash_page_search_issue": "procurou por um <a href=\"`x`\">erro existente no GitHub</a>",
"crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto (NÃO traduza):",
"crash_page_read_the_faq": "leu as <a href=\"`x`\">Perguntas Frequentes (FAQ)</a>",
"generic_views_count": "{{count}} visualização",
@@ -428,7 +426,7 @@
"preferences_quality_dash_option_144p": "144p",
"invidious": "Invidious",
"preferences_quality_option_medium": "Médio",
- "360": "360°",
+ "search_filters_features_option_three_sixty": "360°",
"none": "none",
"videoinfo_watch_on_youTube": "Assistir no YouTube",
"videoinfo_youTube_embed_link": "Embutir",
@@ -437,5 +435,6 @@
"user_created_playlists": "`x` listas de reprodução criadas",
"user_saved_playlists": "`x` listas de reprodução salvas",
"Video unavailable": "Vídeo indisponível",
- "videoinfo_started_streaming_x_ago": "Iniciou a transmissão a `x`"
+ "videoinfo_started_streaming_x_ago": "Iniciou a transmissão a `x`",
+ "search_filters_title": "Filtro"
}
diff --git a/locales/pt-PT.json b/locales/pt-PT.json
index 4dba553e..a57a2939 100644
--- a/locales/pt-PT.json
+++ b/locales/pt-PT.json
@@ -123,7 +123,7 @@
"Subscriptions": "Subscrições",
"search": "pesquisar",
"Log out": "Terminar sessão",
- "Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no Github.",
+ "Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no GitHub.",
"Source available here.": "Código-fonte disponível aqui.",
"View JavaScript license information.": "Ver informações da licença do JavaScript.",
"View privacy policy.": "Ver a política de privacidade.",
@@ -143,7 +143,6 @@
"Show less": "Mostrar menos",
"Watch on YouTube": "Ver no YouTube",
"Switch Invidious Instance": "Mudar a instância do Invidious",
- "Broken? Try another Invidious Instance": "Falhou? Tente outra Instância do Invidious",
"Hide annotations": "Ocultar anotações",
"Show annotations": "Mostrar anotações",
"Genre: ": "Género: ",
@@ -345,35 +344,35 @@
"Videos": "Vídeos",
"Playlists": "Listas de reprodução",
"Community": "Comunidade",
- "relevance": "Relevância",
- "rating": "Avaliação",
- "date": "Data de envio",
- "views": "Visualizações",
- "content_type": "Tipo",
- "duration": "Duração",
- "features": "Funcionalidades",
- "sort": "Ordenar por",
- "hour": "Última hora",
- "today": "Hoje",
- "week": "Esta semana",
- "month": "Este mês",
- "year": "Este ano",
- "video": "Vídeo",
- "channel": "Canal",
- "playlist": "Lista de reprodução",
- "movie": "Filme",
- "show": "Espetáculo",
- "hd": "HD",
- "subtitles": "Legendas",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "Em direto",
- "4k": "4K",
- "location": "Localização",
- "hdr": "HDR",
- "filter": "Filtro",
+ "search_filters_sort_option_relevance": "Relevância",
+ "search_filters_sort_option_rating": "Avaliação",
+ "search_filters_sort_option_date": "Data de envio",
+ "search_filters_sort_option_views": "Visualizações",
+ "search_filters_type_label": "Tipo",
+ "search_filters_duration_label": "Duração",
+ "search_filters_features_label": "Funcionalidades",
+ "search_filters_sort_label": "Ordenar por",
+ "search_filters_date_option_hour": "Última hora",
+ "search_filters_date_option_today": "Hoje",
+ "search_filters_date_option_week": "Esta semana",
+ "search_filters_date_option_month": "Este mês",
+ "search_filters_date_option_year": "Este ano",
+ "search_filters_type_option_video": "Vídeo",
+ "search_filters_type_option_channel": "Canal",
+ "search_filters_type_option_playlist": "Lista de reprodução",
+ "search_filters_type_option_movie": "Filme",
+ "search_filters_type_option_show": "Espetáculo",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "Legendas",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "Em direto",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "Localização",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Versão atual: ",
"next_steps_error_message": "Pode tentar as seguintes opções: ",
"next_steps_error_message_refresh": "Atualizar",
- "next_steps_error_message_go_to_youtube": "Ir ao YouTube"
+ "next_steps_error_message_go_to_youtube": "Ir ao YouTube",
+ "search_filters_title": "Filtro"
}
diff --git a/locales/pt.json b/locales/pt.json
index 0a352f79..df237649 100644
--- a/locales/pt.json
+++ b/locales/pt.json
@@ -1,14 +1,13 @@
{
- "show": "Espetáculo",
- "views": "Visualizações",
- "date": "Data de envio",
- "rating": "Avaliação",
- "relevance": "Relevância",
- "Broken? Try another Invidious Instance": "Falhou? Tente outra Instância do Invidious",
+ "search_filters_type_option_show": "Espetáculo",
+ "search_filters_sort_option_views": "Visualizações",
+ "search_filters_sort_option_date": "Data de envio",
+ "search_filters_sort_option_rating": "Avaliação",
+ "search_filters_sort_option_relevance": "Relevância",
"Switch Invidious Instance": "Mudar a instância do Invidious",
"Show less": "Mostrar menos",
"Show more": "Mostrar mais",
- "Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no Github.",
+ "Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no GitHub.",
"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",
@@ -17,28 +16,27 @@
"next_steps_error_message_go_to_youtube": "Ir ao YouTube",
"next_steps_error_message": "Pode tentar as seguintes opções: ",
"next_steps_error_message_refresh": "Atualizar",
- "filter": "Filtro",
- "hdr": "HDR",
- "location": "Localização",
- "4k": "4K",
- "live": "Em direto",
- "3d": "3D",
- "creative_commons": "Creative Commons",
- "subtitles": "Legendas",
- "hd": "HD",
- "movie": "Filme",
- "playlist": "Lista de reprodução",
- "channel": "Canal",
- "video": "Vídeo",
- "year": "Este ano",
- "month": "Este mês",
- "week": "Esta semana",
- "today": "Hoje",
- "hour": "Última hora",
- "sort": "Ordenar por",
- "features": "Funcionalidades",
- "duration": "Duração",
- "content_type": "Tipo",
+ "search_filters_features_option_hdr": "HDR",
+ "search_filters_features_option_location": "Localização",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_live": "Em direto",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_subtitles": "Legendas",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_type_option_movie": "Filme",
+ "search_filters_type_option_playlist": "Lista de reprodução",
+ "search_filters_type_option_channel": "Canal",
+ "search_filters_type_option_video": "Vídeo",
+ "search_filters_date_option_year": "Este ano",
+ "search_filters_date_option_month": "Este mês",
+ "search_filters_date_option_week": "Esta semana",
+ "search_filters_date_option_today": "Hoje",
+ "search_filters_date_option_hour": "Última hora",
+ "search_filters_sort_label": "Ordenar por",
+ "search_filters_features_label": "Funcionalidades",
+ "search_filters_duration_label": "Duração",
+ "search_filters_type_label": "Tipo",
"permalink": "hiperligação permanente",
"YouTube comment permalink": "Hiperligação permanente do comentário no YouTube",
"Download as: ": "Descarregar como: ",
@@ -376,8 +374,8 @@
"Unsubscribe": "Anular subscrição",
"Shared `x` ago": "Partilhado `x` atrás",
"LIVE": "Em direto",
- "short": "Curto (< 4 minutos)",
- "long": "Longo (> 20 minutos)",
+ "search_filters_duration_option_short": "Curto (< 4 minutos)",
+ "search_filters_duration_option_long": "Longo (> 20 minutos)",
"footer_source_code": "Código-fonte",
"footer_original_source_code": "Código-fonte original",
"adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte alterado",
@@ -397,8 +395,8 @@
"preferences_quality_dash_option_360p": "360p",
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_144p": "144p",
- "purchased": "Comprado",
- "360": "360°",
+ "search_filters_features_option_purchased": "Comprado",
+ "search_filters_features_option_three_sixty": "360°",
"videoinfo_invidious_embed_link": "Incorporar hiperligação",
"Video unavailable": "Vídeo não disponível",
"invidious": "Invidious",
@@ -435,7 +433,8 @@
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>",
"crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
"crash_page_read_the_faq": "leu as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>",
- "crash_page_search_issue": "procurou se <a href=\"`x`\">o erro já foi reportado no Github</a>",
+ "crash_page_search_issue": "procurou se <a href=\"`x`\">o erro já foi reportado no GitHub</a>",
"crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto tal qual (NÃO o traduza):",
- "user_created_playlists": "`x` listas de reprodução criadas"
+ "user_created_playlists": "`x` listas de reprodução criadas",
+ "search_filters_title": "Filtro"
}
diff --git a/locales/ro.json b/locales/ro.json
index 2ea6496b..342f5f37 100644
--- a/locales/ro.json
+++ b/locales/ro.json
@@ -21,7 +21,7 @@
"No": "Nu",
"Import and Export Data": "Importați și Exportați Datele",
"Import": "Importați",
- "Import Invidious data": "Importați Datele de pe Invidious",
+ "Import Invidious data": "Importați datele JSON de pe Invidious",
"Import YouTube subscriptions": "Importați abonamentele de pe YouTube",
"Import FreeTube subscriptions (.db)": "Importați abonamentele de pe FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Importați abonamentele de pe NewPipe (.json)",
@@ -29,7 +29,7 @@
"Export": "Exportați",
"Export subscriptions as OPML": "Exportați abonamentele în format OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportați abonamentele în format OPML (pentru NewPipe și FreeTube)",
- "Export data as JSON": "Exportați datele în format JSON",
+ "Export data as JSON": "Exportați datele Invidious în format JSON",
"Delete account?": "Sunteți siguri că doriți să vă ștergeți contul?",
"History": "Istoric",
"An alternative front-end to YouTube": "O alternativă front-end pentru YouTube",
@@ -155,7 +155,7 @@
"Hide replies": "Ascundeți replicile",
"Show replies": "Afișați replicile",
"Incorrect password": "Parolă incorectă",
- "Quota exceeded, try again in a few hours": "Numărul de tentative de conectare a fost depășit. Va rugăm să încercați din nou în câteva ore.",
+ "Quota exceeded, try again in a few hours": "Numărul de tentative de conectare a fost depășit. Va rugăm să încercați din nou în câteva ore",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Conectare eșuată. Dacă nu reușiți să vă conectați, verificați dacă ați activat autentificarea cu doi factori (Autentificator sau SMS).",
"Invalid TFA code": "Codul de autentificare cu doi factori este invalid",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Conectare eșuată. Acest lucru ar putea fi cauzat de faptul că nu ați activat autentificarea cu doi factori.",
@@ -174,7 +174,7 @@
"Deleted or invalid channel": "Canal șters sau invalid",
"This channel does not exist.": "Acest canal nu există.",
"Could not get channel info.": "Nu am putut primi informații despre acest canal.",
- "Could not fetch comments": "Încărcarea comentariilor a eșuat.",
+ "Could not fetch comments": "Încărcarea comentariilor a eșuat",
"`x` ago": "acum `x`",
"Load more": "Vedeți mai mult",
"Could not create mix.": "Nu am putut crea această listă de redare.",
@@ -187,7 +187,7 @@
"Erroneous challenge": "Challenge invalid",
"Erroneous token": "Token invalid",
"No such user": "Acest utilizator nu există",
- "Token is expired, please try again": "Token-ul este expirat, vă rugăm să reîncercați.",
+ "Token is expired, please try again": "Jetonul a expirat, vă rugăm să încercați din nou",
"English": "Engleză",
"English (auto-generated)": "Engleză (generată automat)",
"Afrikaans": "Afrikaans",
@@ -295,7 +295,7 @@
"Yoruba": "Yoruba",
"Zulu": "Zoulou",
"Fallback comments: ": "Comentarii alternative: ",
- "Popular": "Popular",
+ "Popular": "Populare",
"Top": "Top",
"About": "Despre",
"Rating: ": "Evaluare: ",
@@ -318,5 +318,173 @@
"Videos": "Videoclipuri",
"Playlists": "Liste de redare",
"Community": "Comunitate",
- "Current version: ": "Versiunea actuală: "
+ "Current version: ": "Versiunea actuală: ",
+ "crash_page_read_the_faq": "citit lista <a href=\"`x`\">Întrebărilor Frecvente (FAQ)</a>",
+ "generic_count_days_0": "{{count}} zi",
+ "generic_count_days_1": "{{count}} zile",
+ "generic_count_days_2": "{{count}} de zile",
+ "generic_count_hours_0": "{{count}} oră",
+ "generic_count_hours_1": "{{count}} ore",
+ "generic_count_hours_2": "{{count}} de ore",
+ "generic_count_minutes_0": "{{count}} minut",
+ "generic_count_minutes_1": "{{count}} minute",
+ "generic_count_minutes_2": "{{count}} de minute",
+ "generic_views_count_0": "{{count}} vizionare",
+ "generic_views_count_1": "{{count}} vizionări",
+ "generic_views_count_2": "{{count}} de vizionări",
+ "subscriptions_unseen_notifs_count_0": "{{count}} notificare neverificată",
+ "subscriptions_unseen_notifs_count_1": "{{count}} notificări neverificate",
+ "subscriptions_unseen_notifs_count_2": "{{count}} de notificări neverificate",
+ "crash_page_refresh": "încercat să <a href=\"`x`\">reîmprospătați pagina</a>",
+ "crash_page_switch_instance": "am încercat să <a href=\"`x`\">folosim o altă instanță</a>",
+ "preferences_watch_history_label": "Activează istoricul: ",
+ "invidious": "Invidious",
+ "preferences_vr_mode_label": "Videoclipuri interactive de 360 de grade (necesită WebGL): ",
+ "English (United Kingdom)": "Engleză (Regatul Unit)",
+ "English (United States)": "Engleză (Statele Unite ale Americii)",
+ "Chinese": "Chineză",
+ "Chinese (China)": "Chineză (China)",
+ "Chinese (Hong Kong)": "Chineză (Hong Kong)",
+ "Chinese (Taiwan)": "Chineză (Taiwan)",
+ "Cantonese (Hong Kong)": "Cantoneză (Hong Kong)",
+ "Portuguese (auto-generated)": "Portugheză (generată automat)",
+ "Portuguese (Brazil)": "Portugheză (Brazilia)",
+ "Russian (auto-generated)": "Rusă (generată automat)",
+ "Turkish (auto-generated)": "Turcă (generată automat)",
+ "Vietnamese (auto-generated)": "Vietnameză (generată automat)",
+ "videoinfo_started_streaming_x_ago": "În direct de acum `x`",
+ "preferences_quality_dash_option_2160p": "2160p",
+ "footer_modfied_source_code": "Codul sursă modificat",
+ "preferences_quality_dash_label": "Calitatea video DASH preferată: ",
+ "generic_videos_count_0": "{{count}} videoclip",
+ "generic_videos_count_1": "{{count}} videoclipuri",
+ "generic_videos_count_2": "{{count}} de videoclipuri",
+ "generic_playlists_count_0": "{{count}} playlist",
+ "generic_playlists_count_1": "{{count}} playlisturi",
+ "generic_playlists_count_2": "{{count}} de playlisturi",
+ "tokens_count_0": "{{count}} jeton",
+ "tokens_count_1": "{{count}} jetoane",
+ "tokens_count_2": "{{count}} de jetoane",
+ "comments_points_count_0": "{{count}} punct",
+ "comments_points_count_1": "{{count}} puncte",
+ "comments_points_count_2": "{{count}} de puncte",
+ "Spanish (Spain)": "Spaniolă (Spania)",
+ "Video unavailable": "Videoclip indisponibil",
+ "crash_page_search_issue": "căutat <a href=\"`x`\">sugestiile existente pe GitHub</a>",
+ "Show more": "Afișați mai mult",
+ "Released under the AGPLv3 on Github.": "Lansat sub licența AGPLv3 pe GitHub.",
+ "preferences_quality_option_dash": "DASH (calitate adaptativă)",
+ "preferences_quality_option_hd720": "HD720",
+ "preferences_quality_option_small": "Mică",
+ "preferences_quality_dash_option_1440p": "1440p",
+ "preferences_quality_dash_option_1080p": "1080p",
+ "preferences_category_misc": "Setări diverse",
+ "preferences_automatic_instance_redirect_label": "Redirecționare automată de instanță (trecere prin redirect.invidious.io): ",
+ "preferences_quality_dash_option_480p": "480p",
+ "preferences_quality_option_medium": "Medie",
+ "Switch Invidious Instance": "Schimbă instanța Invidious",
+ "preferences_quality_dash_option_720p": "720p",
+ "preferences_quality_dash_option_auto": "Automatică",
+ "preferences_quality_dash_option_best": "Cea mai bună",
+ "preferences_quality_dash_option_worst": "Cea mai redusă",
+ "preferences_quality_dash_option_4320p": "4320p",
+ "preferences_quality_dash_option_360p": "360p",
+ "preferences_region_label": "Țară de conținut: ",
+ "preferences_extend_desc_label": "Extindeți automat descrierea: ",
+ "preferences_show_nick_label": "Afișați numele de utilizator pe partea de sus: ",
+ "generic_subscribers_count_0": "{{count}} abonat",
+ "generic_subscribers_count_1": "{{count}} abonați",
+ "generic_subscribers_count_2": "{{count}} de abonați",
+ "generic_subscriptions_count_0": "{{count}} abonament",
+ "generic_subscriptions_count_1": "{{count}} abonamente",
+ "generic_subscriptions_count_2": "{{count}} de abonamente",
+ "Search": "Căutați",
+ "search_filters_title": "Filtre",
+ "search_filters_date_label": "Data încărcării",
+ "none": "niciunul",
+ "search_message_use_another_instance": " Puteți <a href=\"`x`\">căuta într-o altă instanță</a>.",
+ "comments_view_x_replies_0": "Afișați {{count}} răspuns",
+ "comments_view_x_replies_1": "Afișați {{count}} răspunsuri",
+ "comments_view_x_replies_2": "Afișați {{count}} de răspunsuri",
+ "search_message_no_results": "Nu s-au găsit rezultate.",
+ "Dutch (auto-generated)": "Olandeză (generată automat)",
+ "Indonesian (auto-generated)": "Indoneziană (generată automat)",
+ "German (auto-generated)": "Germană (generată automat)",
+ "French (auto-generated)": "Franceză (generată automat)",
+ "Interlingue": "Interlingue",
+ "Italian (auto-generated)": "Italiană (generată automat)",
+ "Japanese (auto-generated)": "Japoneză (generată automat)",
+ "Korean (auto-generated)": "Coreeană (generată automat)",
+ "Spanish (auto-generated)": "Spaniolă (generată automat)",
+ "search_filters_date_option_none": "Oricând",
+ "search_filters_date_option_year": "an",
+ "search_filters_type_option_channel": "canal",
+ "Spanish (Mexico)": "Spaniolă (Mexic)",
+ "generic_count_weeks_0": "{{count}} săptămână",
+ "generic_count_weeks_1": "{{count}} săptămâni",
+ "generic_count_weeks_2": "{{count}} de săptămâni",
+ "generic_count_seconds_0": "{{count}} secundă",
+ "generic_count_seconds_1": "{{count}} secunde",
+ "generic_count_seconds_2": "{{count}} de secunde",
+ "search_filters_type_option_video": "videoclip",
+ "generic_count_years_0": "{{count}} an",
+ "generic_count_years_1": "{{count}} ani",
+ "generic_count_years_2": "{{count}} de ani",
+ "generic_count_months_0": "{{count}} lună",
+ "generic_count_months_1": "{{count}} luni",
+ "generic_count_months_2": "{{count}} de luni",
+ "search_filters_duration_label": "durată",
+ "search_filters_date_option_month": "lună",
+ "search_filters_type_label": "Tip",
+ "search_filters_date_option_today": "azi",
+ "search_filters_date_option_week": "săptămână",
+ "search_filters_features_option_vr180": "VR180",
+ "search_filters_type_option_playlist": "playlist",
+ "search_filters_type_option_movie": "film",
+ "search_filters_type_option_show": "emisiune",
+ "search_filters_duration_option_short": "Scurt (< 4 minute)",
+ "search_filters_duration_option_medium": "Medie (4 - 20 de minute)",
+ "search_filters_duration_option_none": "Fără limită",
+ "search_filters_duration_option_long": "Lungă (> 20 de minute)",
+ "search_filters_features_label": "atribute",
+ "search_filters_features_option_live": "în direct",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_sixty": "360°",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_subtitles": "subtitrări/CC",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_hdr": "HDR",
+ "search_filters_features_option_purchased": "Cumpărate",
+ "next_steps_error_message": "După ce ar trebui să încercați să: ",
+ "user_saved_playlists": "`x` playlisturi salvate",
+ "search_filters_features_option_location": "locație",
+ "search_filters_sort_label": "Sortați după",
+ "search_filters_sort_option_relevance": "relevanță",
+ "search_filters_sort_option_rating": "clasificare",
+ "search_filters_sort_option_date": "Data încărcării",
+ "search_filters_sort_option_views": "Numărul de vizionări",
+ "footer_source_code": "Codul sursă",
+ "search_filters_apply_button": "Aplicați filtrele selectate",
+ "footer_original_source_code": "Codul sursă original",
+ "next_steps_error_message_refresh": "Reîmprospătează",
+ "next_steps_error_message_go_to_youtube": "Mergeți pe YouTube",
+ "footer_donate_page": "Donați",
+ "adminprefs_modified_source_code_url_label": "URL către depozitul de cod sursă modificat",
+ "footer_documentation": "Documentație",
+ "videoinfo_youTube_embed_link": "Încorporați",
+ "videoinfo_watch_on_youTube": "Vizionați pe YouTube",
+ "videoinfo_invidious_embed_link": "Link de încorporare",
+ "download_subtitles": "Subtitrări - `x` (.vtt)",
+ "user_created_playlists": "`x` playlisturi create",
+ "preferences_save_player_pos_label": "Salvați poziția de redare: ",
+ "crash_page_you_found_a_bug": "Se pare că ați găsit un bug în aplicația Invidious!",
+ "crash_page_before_reporting": "Înainte de a reporta bugul, asigurați-vă că ați:",
+ "search_filters_date_option_hour": "oră",
+ "search_message_change_filters_or_query": "Încercați să lărgiți căutarea sau să modificați filtrele.",
+ "crash_page_report_issue": "Dacă niciuna dintre sugestiile de mai sus v-a ajutat, vă rugăm să <a href=\"`x`\">postați o nouă sugestie pe GitHub</a> (cel mai bine în engleză), și să includeți următorul text în post (să nu îl traduceți):",
+ "search_filters_type_option_all": "orice tip",
+ "preferences_quality_dash_option_240p": "240p",
+ "preferences_quality_dash_option_144p": "144p",
+ "Show less": "Afișați mai puțin"
}
diff --git a/locales/ru.json b/locales/ru.json
index c223bcf8..88bb64ad 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -121,7 +121,7 @@
"Subscriptions": "Подписки",
"search": "поиск",
"Log out": "Выйти",
- "Released under the AGPLv3 on Github.": "Выпущено под лицензией AGPLv3 на Github.",
+ "Released under the AGPLv3 on Github.": "Выпущено под лицензией AGPLv3 на GitHub.",
"Source available here.": "Исходный код доступен здесь.",
"View JavaScript license information.": "Посмотреть информацию по лицензии JavaScript.",
"View privacy policy.": "Посмотреть политику конфиденциальности.",
@@ -141,7 +141,6 @@
"Show less": "Показать меньше",
"Watch on YouTube": "Смотреть на YouTube",
"Switch Invidious Instance": "Сменить экземпляр Invidious",
- "Broken? Try another Invidious Instance": "Сломался? Попробуйте другой экземпляр Invidious",
"Hide annotations": "Скрыть аннотации",
"Show annotations": "Показать аннотации",
"Genre: ": "Жанр: ",
@@ -329,39 +328,38 @@
"Videos": "Видео",
"Playlists": "Плейлисты",
"Community": "Сообщество",
- "relevance": "Актуальность",
- "rating": "Рейтинг",
- "date": "Дата загрузки",
- "views": "Просмотры",
- "content_type": "Тип",
- "duration": "Длительность",
- "features": "Функции",
- "sort": "Сортировать по",
- "hour": "Последний час",
- "today": "Сегодня",
- "week": "Эта неделя",
- "month": "Этот месяц",
- "year": "Этот год",
- "video": "Видео",
- "channel": "Канал",
- "playlist": "Плейлист",
- "movie": "Фильм",
- "show": "Показать",
- "hd": "HD",
- "subtitles": "Субтитры",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "Прямой эфир",
- "4k": "4K",
- "location": "Местоположение",
- "hdr": "HDR",
- "filter": "Фильтр",
+ "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_date_option_hour": "Последний час",
+ "search_filters_date_option_today": "Сегодня",
+ "search_filters_date_option_week": "Эта неделя",
+ "search_filters_date_option_month": "Этот месяц",
+ "search_filters_date_option_year": "Этот год",
+ "search_filters_type_option_video": "Видео",
+ "search_filters_type_option_channel": "Канал",
+ "search_filters_type_option_playlist": "Плейлист",
+ "search_filters_type_option_movie": "Фильм",
+ "search_filters_type_option_show": "Показать",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "Субтитры",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "Прямой эфир",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "Местоположение",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Текущая версия: ",
"next_steps_error_message": "После чего следует попробовать: ",
"next_steps_error_message_refresh": "Обновить",
"next_steps_error_message_go_to_youtube": "Перейти на YouTube",
- "short": "Короткие (< 4 минут)",
- "long": "Длинные (> 20 минут)",
+ "search_filters_duration_option_short": "Короткие (< 4 минут)",
+ "search_filters_duration_option_long": "Длинные (> 20 минут)",
"preferences_quality_dash_option_best": "Наилучшее",
"generic_count_weeks_0": "{{count}} неделя",
"generic_count_weeks_1": "{{count}} недели",
@@ -437,7 +435,7 @@
"generic_count_seconds_0": "{{count}} секунда",
"generic_count_seconds_1": "{{count}} секунды",
"generic_count_seconds_2": "{{count}} секунд",
- "purchased": "Приобретено",
+ "search_filters_features_option_purchased": "Приобретено",
"videoinfo_started_streaming_x_ago": "Трансляция началась `x` назад",
"crash_page_switch_instance": "пробовали <a href=\"`x`\">использовать другое зеркало</a>",
"crash_page_read_the_faq": "прочли <a href=\"`x`\">Частые Вопросы (ЧаВо)</a>",
@@ -457,7 +455,7 @@
"footer_original_source_code": "Оригинальный исходный код",
"footer_modfied_source_code": "Изменённый исходный код",
"user_saved_playlists": "`x` сохранённых плейлистов",
- "crash_page_search_issue": "искали <a href=\"`x`\">похожую проблему на Github</a>",
+ "crash_page_search_issue": "искали <a href=\"`x`\">похожую проблему на GitHub</a>",
"comments_points_count_0": "{{count}} плюс",
"comments_points_count_1": "{{count}} плюса",
"comments_points_count_2": "{{count}} плюсов",
@@ -473,9 +471,10 @@
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_144p": "144p",
"invidious": "Invidious",
- "360": "360°",
+ "search_filters_features_option_three_sixty": "360°",
"Video unavailable": "Видео недоступно",
"preferences_save_player_pos_label": "Запоминать позицию: ",
"preferences_region_label": "Страна: ",
- "preferences_watch_history_label": "Включить историю просмотров "
+ "preferences_watch_history_label": "Включить историю просмотров ",
+ "search_filters_title": "Фильтр"
}
diff --git a/locales/sk.json b/locales/sk.json
index f20ad75a..cdb3a596 100644
--- a/locales/sk.json
+++ b/locales/sk.json
@@ -18,15 +18,15 @@
"No": "Nie",
"Import and Export Data": "Import a Export údajov",
"Import": "Import",
- "Import Invidious data": "Importovať údaje Invidious",
- "Import YouTube subscriptions": "Importovať odbery YouTube",
+ "Import Invidious data": "Importovať JSON údaje Invidious",
+ "Import YouTube subscriptions": "Importovať odbery YouTube/OPML",
"Import FreeTube subscriptions (.db)": "Importovať odbery FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Importovať odbery NewPipe (.json)",
"Import NewPipe data (.zip)": "Importovať údaje NewPipe (.zip)",
"Export": "Export",
"Export subscriptions as OPML": "Exportovať odbery ako OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportovať odbery ako OPML (pre NewPipe a FreeTube)",
- "Export data as JSON": "Export údajov ako JSON",
+ "Export data as JSON": "Exportovať údaje Invidious ako JSON",
"Delete account?": "Zrušiť účet?",
"History": "História",
"An alternative front-end to YouTube": "Alternatívny front-end pre YouTube",
@@ -84,5 +84,23 @@
"preferences_unseen_only_label": "Zobraziť iba neprehrané: ",
"preferences_notifications_only_label": "Zobraziť iba upozornenia (ak existujú): ",
"Enable web notifications": "Povoliť webové upozornenia",
- "`x` uploaded a video": "`x` nahral(a) video"
+ "`x` uploaded a video": "`x` nahral(a) video",
+ "generic_views_count_0": "{{count}} zhliadnutie",
+ "generic_views_count_1": "{{count}} zhliadnutia",
+ "generic_views_count_2": "{{count}} zhliadnutí",
+ "generic_subscribers_count_0": "{{count}} odberateľ",
+ "generic_subscribers_count_1": "{{count}} odberatelia",
+ "generic_subscribers_count_2": "{{count}} odberateľov",
+ "Shared `x` ago": "Zverejnené pred `x`",
+ "generic_playlists_count_0": "{{count}} playlist",
+ "generic_playlists_count_1": "{{count}} playlisty",
+ "generic_playlists_count_2": "{{count}} playlistov",
+ "generic_videos_count_0": "{{count}} video",
+ "generic_videos_count_1": "{{count}} videá",
+ "generic_videos_count_2": "{{count}} videí",
+ "generic_subscriptions_count_0": "{{count}} odber",
+ "generic_subscriptions_count_1": "{{count}} odbery",
+ "generic_subscriptions_count_2": "{{count}} odberov",
+ "Authorize token for `x`?": "Autorizovať token pre `x`?",
+ "View playlist on YouTube": "Zobraziť playlist na YouTube"
}
diff --git a/locales/sq.json b/locales/sq.json
index 3e2a3fb1..76f1eaa3 100644
--- a/locales/sq.json
+++ b/locales/sq.json
@@ -26,11 +26,11 @@
"Tamil": "Tamilisht",
"Telugu": "Telugu",
"Vietnamese": "Vietnamisht",
- "creative_commons": "Creative Commons",
- "3d": "3D",
- "live": "Drejtpërsëdrejti",
- "4k": "4K",
- "location": "Vendndodhja",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "Drejtpërsëdrejti",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "Vendndodhja",
"videoinfo_watch_on_youTube": "Shiheni në YouTube",
"videoinfo_youTube_embed_link": "Trupëzojeni",
"videoinfo_invidious_embed_link": "Lidhje Trupëzimi",
@@ -127,7 +127,7 @@
"Subscriptions": "Pajtime",
"search": "kërko",
"Log out": "Dilni",
- "Released under the AGPLv3 on Github.": "Hedhur në qarkullim në Github sipas licencës AGPLv3.",
+ "Released under the AGPLv3 on Github.": "Hedhur në qarkullim në GitHub sipas licencës AGPLv3.",
"Source available here.": "Burimi i passhëm që këtu.",
"View JavaScript license information.": "Shihni hollësi licence JavaScript.",
"View privacy policy.": "Shihni rregulla privatësie.",
@@ -147,7 +147,6 @@
"Show less": "Shfaq më pak",
"Watch on YouTube": "Shiheni në YouTube",
"Switch Invidious Instance": "Ndërroni Instancë Invidious",
- "Broken? Try another Invidious Instance": "E prishur? Provoni një tjetër Instancë Invidious",
"Hide annotations": "Fshihi shënimet",
"Show annotations": "Shfaq shënime",
"License: ": "Licencë: ",
@@ -261,32 +260,32 @@
"Audio mode": "Mënyrë për audion",
"Playlists": "Luajlista",
"Community": "Bashkësi",
- "relevance": "Rëndësi",
+ "search_filters_sort_option_relevance": "Rëndësi",
"Video mode": "Mënyrë video",
"Videos": "Video",
- "rating": "Vlerësim",
- "date": "Datë ngarkimi",
- "views": "Numër parjesh",
- "content_type": "Lloj",
- "duration": "Kohëzgjatje",
- "features": "Veçori",
- "sort": "Renditi Sipas",
- "hour": "Orën e Fundit",
- "today": "Sot",
- "long": "E gjatë (> 20 minuta)",
- "hd": "HD",
- "subtitles": "Titra/CC",
- "hdr": "HDR",
- "week": "Këtë javë",
- "month": "Këtë muaj",
- "year": "Këtë vit",
- "video": "Video",
- "channel": "Kanal",
- "playlist": "Luajlistë",
- "movie": "Film",
- "show": "Shfaqe",
- "short": "E shkurtër (< 4 minuta)",
- "purchased": "Të blera",
+ "search_filters_sort_option_rating": "Vlerësim",
+ "search_filters_sort_option_date": "Datë ngarkimi",
+ "search_filters_sort_option_views": "Numër parjesh",
+ "search_filters_type_label": "Lloj",
+ "search_filters_duration_label": "Kohëzgjatje",
+ "search_filters_features_label": "Veçori",
+ "search_filters_sort_label": "Renditi Sipas",
+ "search_filters_date_option_hour": "Orën e Fundit",
+ "search_filters_date_option_today": "Sot",
+ "search_filters_duration_option_long": "E gjatë (> 20 minuta)",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "Titra/CC",
+ "search_filters_features_option_hdr": "HDR",
+ "search_filters_date_option_week": "Këtë javë",
+ "search_filters_date_option_month": "Këtë muaj",
+ "search_filters_date_option_year": "Këtë vit",
+ "search_filters_type_option_video": "Video",
+ "search_filters_type_option_channel": "Kanal",
+ "search_filters_type_option_playlist": "Luajlistë",
+ "search_filters_type_option_movie": "Film",
+ "search_filters_type_option_show": "Shfaqe",
+ "search_filters_duration_option_short": "E shkurtër (< 4 minuta)",
+ "search_filters_features_option_purchased": "Të blera",
"footer_modfied_source_code": "Kod Burim i ndryshuar",
"adminprefs_modified_source_code_url_label": "URL e depos së ndryshuar të kodit burim",
"none": "asnjë",
@@ -370,8 +369,7 @@
"Mongolian": "Mongolisht",
"Nepali": "Nepaleze",
"Norwegian Bokmål": "Norvegjishte Bokmål",
- "360": "360°",
- "filter": "Filtroji",
+ "search_filters_features_option_three_sixty": "360°",
"Current version: ": "Versioni i tanishëm: ",
"next_steps_error_message": "Pas të cilës duhet të provoni të: ",
"next_steps_error_message_refresh": "Rifreskoje",
@@ -437,7 +435,7 @@
"Spanish (Spain)": "Spanjisht (Spanjë)",
"Turkish (auto-generated)": "Turqisht (të prodhuara automatikisht)",
"Vietnamese (auto-generated)": "Vietnamisht (të prodhuara automatikisht)",
- "crash_page_search_issue": "kërkuar për <a href=\"`x`\">çështje ekzistuese në Github</a>",
+ "crash_page_search_issue": "kërkuar për <a href=\"`x`\">çështje ekzistuese në GitHub</a>",
"crash_page_report_issue": "Nëse asnjë nga sa më sipër s’ndihmoi, ju lutemi, <a href=\"`x`\">hapni një çështje në GitHub</a> (mundësisht në anglisht) dhe përfshini në mesazhin tuaj tekstin vijues (MOS e përktheni këtë tekst):",
"generic_subscriptions_count": "{{count}} pajtim",
"generic_subscriptions_count_plural": "{{count}} pajtime",
@@ -448,5 +446,6 @@
"Import YouTube subscriptions": "Importoni pajtime YouTube/OPML",
"Export data as JSON": "Eksportoji të dhënat Invidious si JSON",
"preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ",
- "Shared `x`": "Ndau me të tjerë `x`"
+ "Shared `x`": "Ndau me të tjerë `x`",
+ "search_filters_title": "Filtra"
}
diff --git a/locales/sr.json b/locales/sr.json
index 40e53231..d2f990ae 100644
--- a/locales/sr.json
+++ b/locales/sr.json
@@ -131,26 +131,25 @@
"YouTube comment permalink": "YouTube komentar trajna veza",
"Audio mode": "Audio mod",
"Playlists": "Plej liste",
- "relevance": "Relevantnost",
- "rating": "Ocene",
- "date": "Datum otpremanja",
- "views": "Broj pregleda",
+ "search_filters_sort_option_relevance": "Relevantnost",
+ "search_filters_sort_option_rating": "Ocene",
+ "search_filters_sort_option_date": "Datum otpremanja",
+ "search_filters_sort_option_views": "Broj pregleda",
"`x` marked it with a ❤": "`x` je označio/la ovo sa ❤",
- "duration": "Trajanje",
- "features": "Karakteristike",
- "hour": "Poslednji sat",
- "week": "Ove sedmice",
- "month": "Ovaj mesec",
- "year": "Ove godine",
- "video": "Video",
- "playlist": "Plej lista",
- "movie": "Film",
- "long": "Dugo (> 20 minuta)",
- "hd": "HD",
- "creative_commons": "Creative Commons (Licenca)",
- "3d": "3D",
- "hdr": "Video Visoke Rezolucije",
- "filter": "Filter",
+ "search_filters_duration_label": "Trajanje",
+ "search_filters_features_label": "Karakteristike",
+ "search_filters_date_option_hour": "Poslednji sat",
+ "search_filters_date_option_week": "Ove sedmice",
+ "search_filters_date_option_month": "Ovaj mesec",
+ "search_filters_date_option_year": "Ove godine",
+ "search_filters_type_option_video": "Video",
+ "search_filters_type_option_playlist": "Plej lista",
+ "search_filters_type_option_movie": "Film",
+ "search_filters_duration_option_long": "Dugo (> 20 minuta)",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_c_commons": "Creative Commons (Licenca)",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_hdr": "Video Visoke Rezolucije",
"next_steps_error_message": "Nakon čega bi trebali probati: ",
"next_steps_error_message_go_to_youtube": "Idi na YouTube",
"footer_documentation": "Dokumentacija",
@@ -225,13 +224,12 @@
"preferences_category_visual": "Vizuelne preference",
"preferences_captions_label": "Podrazumevani titl: ",
"Music": "Muzika",
- "content_type": "Tip",
- "Broken? Try another Invidious Instance": "Ne funkcioniše ispravno? Probajte drugu Invidious instancu",
+ "search_filters_type_label": "Tip",
"Tamil": "Tamilski",
"Save preferences": "Sačuvaj podešavanja",
"Only show latest unwatched video from channel: ": "Prikaži samo poslednje video klipove koji nisu pogledani sa kanala: ",
"Xhosa": "Kosa (Jezik)",
- "channel": "Kanal",
+ "search_filters_type_option_channel": "Kanal",
"Hungarian": "Mađarski",
"Maori": "Maori (Jezik)",
"Manage subscriptions": "Upravljaj zapisima",
@@ -243,7 +241,7 @@
"preferences_default_home_label": "Podrazumevana početna stranica: ",
"Serbian": "Srpski",
"License: ": "Licenca: ",
- "live": "Uživo",
+ "search_filters_features_option_live": "Uživo",
"Report statistics: ": "Izveštavaj o statistici: ",
"Only show latest video from channel: ": "Prikazuj poslednje video klipove samo sa kanala: ",
"channel name - reverse": "ime kanala - obrnuto",
@@ -266,14 +264,14 @@
"alphabetically": "po alfabetu",
"No such user": "Nepostojeći korisnik",
"Subscriptions": "Praćenja",
- "today": "Danas",
+ "search_filters_date_option_today": "Danas",
"Finnish": "Finski",
"Lao": "Laoski",
"Login enabled: ": "Prijava omogućena: ",
"Shona": "Šona",
- "location": "Lokacija",
+ "search_filters_features_option_location": "Lokacija",
"Load more": "Učitaj više",
- "Released under the AGPLv3 on Github.": "Izbačeno pod licencom AGPLv3 na Github-u.",
+ "Released under the AGPLv3 on Github.": "Izbačeno pod licencom AGPLv3 na GitHub-u.",
"Slovenian": "Slovenački",
"View JavaScript license information.": "Pogledaj informacije licence vezane za JavaScript.",
"Chinese (Simplified)": "Kineski (Pojednostavljeni)",
@@ -292,7 +290,7 @@
"Czech": "Češki",
"Latin": "Latinski",
"Videos": "Video klipovi",
- "4k": "4К",
+ "search_filters_features_option_four_k": "4К",
"footer_donate_page": "Doniraj",
"English": "Engleski",
"Arabic": "Arapski",
@@ -310,7 +308,7 @@
"Swahili": "Svahili",
"Yiddish": "Jidiš",
"Zulu": "Zulu",
- "subtitles": "Titl/Prevod",
+ "search_filters_features_option_subtitles": "Titl/Prevod",
"Password cannot be longer than 55 characters": "Lozinka ne može biti duža od 55 karaktera",
"This channel does not exist.": "Ovaj kanal ne postoji.",
"Belarusian": "Beloruski",
@@ -329,9 +327,9 @@
"Clear watch history": "Obriši istoriju gledanja",
"preferences_category_admin": "Administratorska podešavanja",
"published": "objavljeno",
- "sort": "Poredaj prema",
- "show": "Emisija",
- "short": "Kratko (< 4 minute)",
+ "search_filters_sort_label": "Poredaj prema",
+ "search_filters_type_option_show": "Emisija",
+ "search_filters_duration_option_short": "Kratko (< 4 minute)",
"Current version: ": "Trenutna verzija: ",
"Top enabled: ": "Vrh omogućen: ",
"Public": "Javno",
@@ -369,5 +367,6 @@
"unsubscribe": "prekini sa praćenjem",
"Blacklisted regions: ": "Zabranjene oblasti: ",
"Polish": "Poljski",
- "Yoruba": "Joruba"
+ "Yoruba": "Joruba",
+ "search_filters_title": "Filter"
}
diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json
index 40c50674..c0f1224f 100644
--- a/locales/sr_Cyrl.json
+++ b/locales/sr_Cyrl.json
@@ -182,14 +182,13 @@
"Georgian": "Грузијски",
"Greek": "Грчки",
"Hausa": "Хауса",
- "video": "Видео",
- "playlist": "Плеј листа",
- "movie": "Филм",
- "long": "Дуго (> 20 минута)",
- "creative_commons": "Creative Commons (Лиценца)",
- "live": "Уживо",
- "location": "Локација",
- "filter": "Филтер",
+ "search_filters_type_option_video": "Видео",
+ "search_filters_type_option_playlist": "Плеј листа",
+ "search_filters_type_option_movie": "Филм",
+ "search_filters_duration_option_long": "Дуго (> 20 минута)",
+ "search_filters_features_option_c_commons": "Creative Commons (Лиценца)",
+ "search_filters_features_option_live": "Уживо",
+ "search_filters_features_option_location": "Локација",
"next_steps_error_message": "Након чега би требали пробати: ",
"footer_donate_page": "Донирај",
"footer_documentation": "Документација",
@@ -247,9 +246,9 @@
"`x` marked it with a ❤": "`x` је означио/ла ово са ❤",
"Audio mode": "Аудио мод",
"Videos": "Видео клипови",
- "views": "Број прегледа",
- "features": "Карактеристике",
- "today": "Данас",
+ "search_filters_sort_option_views": "Број прегледа",
+ "search_filters_features_label": "Карактеристике",
+ "search_filters_date_option_today": "Данас",
"%A %B %-d, %Y": "%A %B %-d, %Y",
"preferences_locale_label": "Језик: ",
"Persian": "Перзијски",
@@ -257,7 +256,7 @@
"": "Прикажи `x` коментара",
"([^.,0-9]|^)1([^.,0-9]|$)": "Прикажи `x` коментар"
},
- "channel": "Канал",
+ "search_filters_type_option_channel": "Канал",
"Haitian Creole": "Хаићански Креолски",
"Armenian": "Јерменски",
"next_steps_error_message_go_to_youtube": "Иди на YouTube",
@@ -265,10 +264,10 @@
"preferences_vr_mode_label": "Интерактивни видео клипови у 360 степени: ",
"Switch Invidious Instance": "Промени Invidious инстанцу",
"Portuguese": "Португалски",
- "week": "Ове седмице",
- "show": "Емисија",
+ "search_filters_date_option_week": "Ове седмице",
+ "search_filters_type_option_show": "Емисија",
"Fallback comments: ": "Коментари у случају отказивања: ",
- "hdr": "Видео Високе Резолуције",
+ "search_filters_features_option_hdr": "Видео Високе Резолуције",
"About": "О програму",
"Kazakh": "Казашки",
"Shared `x`": "Подељено `x`",
@@ -277,7 +276,7 @@
"Erroneous challenge": "Погрешан изазов",
"Danish": "Дански",
"Could not get channel info.": "Узимање података о каналу није успело.",
- "hd": "HD",
+ "search_filters_features_option_hd": "HD",
"Slovenian": "Словеначки",
"Load more": "Учитај више",
"German": "Немачки",
@@ -288,12 +287,12 @@
"Southern Sotho": "Јужни Сото",
"Popular": "Популарно",
"Gujarati": "Гуџарати",
- "year": "Ове године",
+ "search_filters_date_option_year": "Ове године",
"Irish": "Ирски",
"YouTube comment permalink": "YouTube коментар трајна веза",
"Malagasy": "Малгашки",
"Token is expired, please try again": "Жетон је истекао, молимо вас да покушате поново",
- "short": "Кратко (< 4 минуте)",
+ "search_filters_duration_option_short": "Кратко (< 4 минуте)",
"Samoan": "Самоански",
"Tamil": "Тамилски",
"Ukrainian": "Украјински",
@@ -307,22 +306,22 @@
"Lithuanian": "Литвански",
"Icelandic": "Исландски",
"Thai": "Тајски",
- "month": "Овај месец",
- "content_type": "Тип",
- "hour": "Последњи сат",
+ "search_filters_date_option_month": "Овај месец",
+ "search_filters_type_label": "Тип",
+ "search_filters_date_option_hour": "Последњи сат",
"Spanish": "Шпански",
- "date": "Датум отпремања",
+ "search_filters_sort_option_date": "Датум отпремања",
"View as playlist": "Погледај као плеј листу",
- "relevance": "Релевантност",
+ "search_filters_sort_option_relevance": "Релевантност",
"Estonian": "Естонски",
"Sinhala": "Синхалешки",
"Corsican": "Корзикански",
"Filipino": "Филипино",
"Gaming": "Игрице",
"Movies": "Филмови",
- "rating": "Оцене",
+ "search_filters_sort_option_rating": "Оцене",
"Top enabled: ": "Врх омогућен: ",
- "Released under the AGPLv3 on Github.": "Избачено под лиценцом AGPLv3 на Github-у.",
+ "Released under the AGPLv3 on Github.": "Избачено под лиценцом AGPLv3 на GitHub-у.",
"Afrikaans": "Африканс",
"preferences_automatic_instance_redirect_label": "Аутоматско пребацивање на другу инстанцу у случају отказивања (пречи ће назад на редирецт.инвидиоус.ио): ",
"Invalid TFA code": "Неважећа TFA кода",
@@ -340,12 +339,11 @@
"Swedish": "Шведски",
"Music": "Музика",
"Download as: ": "Преузми као: ",
- "duration": "Трајање",
- "sort": "Поредај према",
- "subtitles": "Титл/Превод",
+ "search_filters_duration_label": "Трајање",
+ "search_filters_sort_label": "Поредај према",
+ "search_filters_features_option_subtitles": "Титл/Превод",
"preferences_extend_desc_label": "Аутоматски прикажи цео опис видеа: ",
"Show less": "Прикажи мање",
- "Broken? Try another Invidious Instance": "Не функционише исправно? Пробајте другу Invidious инстанцу",
"Family friendly? ": "Погодно за породицу? ",
"Premieres `x`": "Премерe у `x`",
"Bosnian": "Босански",
@@ -359,8 +357,8 @@
"Top": "Врх",
"Video mode": "Видео мод",
"footer_source_code": "Изворна Кода",
- "3d": "3D",
- "4k": "4K",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_four_k": "4K",
"Erroneous CAPTCHA": "Погрешна CAPTCHA",
"`x` ago": "пре `x`",
"Arabic": "Арапски",
@@ -369,5 +367,6 @@
"Hebrew": "Хебрејски",
"Korean": "Корејски",
"Kurdish": "Курдски",
- "Malay": "Малајски"
+ "Malay": "Малајски",
+ "search_filters_title": "Филтер"
}
diff --git a/locales/sv-SE.json b/locales/sv-SE.json
index ab0d0773..777899d0 100644
--- a/locales/sv-SE.json
+++ b/locales/sv-SE.json
@@ -139,7 +139,6 @@
"Show less": "Visa mindre",
"Watch on YouTube": "Titta på YouTube",
"Switch Invidious Instance": "Byt Invidious Instans",
- "Broken? Try another Invidious Instance": "Trasig? Prova en annan Invidious Instance",
"Hide annotations": "Dölj länkar-i-video",
"Show annotations": "Visa länkar-i-video",
"Genre: ": "Genre: ",
@@ -327,39 +326,39 @@
"Videos": "Videor",
"Playlists": "Spellistor",
"Community": "Gemenskap",
- "relevance": "Relevans",
- "rating": "Rankning",
- "date": "datum",
- "views": "visningar",
- "content_type": "Typ",
- "duration": "Varaktighet",
- "features": "Funktioner",
- "sort": "Sortera efter",
- "hour": "timme",
- "today": "idag",
- "week": "vecka",
- "month": "månad",
- "year": "år",
- "video": "video",
- "channel": "kanal",
- "playlist": "spellista",
- "movie": "film",
- "show": "tv-serie",
- "hd": "hd",
- "subtitles": "undertexter",
- "creative_commons": "creative_commons",
- "3d": "3d",
- "live": "live",
- "4k": "4k",
- "location": "plats",
- "hdr": "hdr",
- "filter": "Filter",
+ "search_filters_sort_option_relevance": "Relevans",
+ "search_filters_sort_option_rating": "Rankning",
+ "search_filters_sort_option_date": "Datum",
+ "search_filters_sort_option_views": "Visningar",
+ "search_filters_type_label": "Typ",
+ "search_filters_duration_label": "Varaktighet",
+ "search_filters_features_label": "Funktioner",
+ "search_filters_sort_label": "Sortera efter",
+ "search_filters_date_option_hour": "timme",
+ "search_filters_date_option_today": "idag",
+ "search_filters_date_option_week": "vecka",
+ "search_filters_date_option_month": "månad",
+ "search_filters_date_option_year": "år",
+ "search_filters_type_option_video": "video",
+ "search_filters_type_option_channel": "kanal",
+ "search_filters_type_option_playlist": "spellista",
+ "search_filters_type_option_movie": "film",
+ "search_filters_type_option_show": "tv-serie",
+ "search_filters_features_option_hd": "hd",
+ "search_filters_features_option_subtitles": "undertexter",
+ "search_filters_features_option_c_commons": "creative_commons",
+ "search_filters_features_option_three_d": "3d",
+ "search_filters_features_option_live": "live",
+ "search_filters_features_option_four_k": "4k",
+ "search_filters_features_option_location": "plats",
+ "search_filters_features_option_hdr": "hdr",
"Current version: ": "Nuvarande version: ",
"next_steps_error_message_refresh": "Uppdatera",
"next_steps_error_message_go_to_youtube": "Gå till Youtube",
- "Released under the AGPLv3 on Github.": "Publicerad under AGPLv3 på Github.",
+ "Released under the AGPLv3 on Github.": "Publicerad under AGPLv3 på GitHub.",
"footer_source_code": "Källkod",
- "long": "Lång (> 20 minuter)",
+ "search_filters_duration_option_long": "Lång (> 20 minuter)",
"footer_documentation": "Dokumentation",
- "short": "Kort (< 4 minuter)"
+ "search_filters_duration_option_short": "Kort (< 4 minuter)",
+ "search_filters_title": "Filter"
}
diff --git a/locales/tr.json b/locales/tr.json
index 094728fa..b1991c35 100644
--- a/locales/tr.json
+++ b/locales/tr.json
@@ -121,7 +121,7 @@
"Subscriptions": "Abonelikler",
"search": "ara",
"Log out": "Çıkış yap",
- "Released under the AGPLv3 on Github.": "Github'da AGPLv3 altında yayınlandı.",
+ "Released under the AGPLv3 on Github.": "GitHub'da AGPLv3 altında yayınlandı.",
"Source available here.": "Kaynak kodları burada bulunabilir.",
"View JavaScript license information.": "JavaScript lisans bilgilerini görüntüle.",
"View privacy policy.": "Gizlilik politikasını görüntüle.",
@@ -141,7 +141,6 @@
"Show less": "Daha az göster",
"Watch on YouTube": "YouTube'da izle",
"Switch Invidious Instance": "Invidious Örneğini Değiştir",
- "Broken? Try another Invidious Instance": "Bozuk mu? Başka bir Invidious örneğini deneyin",
"Hide annotations": "Ek açıklamaları gizle",
"Show annotations": "Ek açıklamaları göster",
"Genre: ": "Tür: ",
@@ -329,39 +328,38 @@
"Videos": "Videolar",
"Playlists": "Oynatma listeleri",
"Community": "Topluluk",
- "relevance": "İlgi",
- "rating": "Değerlendirme",
- "date": "Yükleme tarihi",
- "views": "Görüntüleme sayısı",
- "content_type": "Tür",
- "duration": "Süre",
- "features": "Özellikler",
- "sort": "Sıralama Ölçütü",
- "hour": "Son Saat",
- "today": "Bugün",
- "week": "Bu hafta",
- "month": "Bu ay",
- "year": "Bu yıl",
- "video": "Video",
- "channel": "Kanal",
- "playlist": "Oynatma listesi",
- "movie": "Film",
- "show": "Gösteri",
- "hd": "HD",
- "subtitles": "Alt yazılar",
- "creative_commons": "Creative Commons",
- "3d": "3B",
- "live": "Canlı",
- "4k": "4K",
- "location": "Konum",
- "hdr": "HDR",
- "filter": "Filtrele",
+ "search_filters_sort_option_relevance": "İlgi",
+ "search_filters_sort_option_rating": "Değerlendirme",
+ "search_filters_sort_option_date": "Yükleme tarihi",
+ "search_filters_sort_option_views": "Görüntüleme sayısı",
+ "search_filters_type_label": "Tür",
+ "search_filters_duration_label": "Süre",
+ "search_filters_features_label": "Özellikler",
+ "search_filters_sort_label": "Sıralama Ölçütü",
+ "search_filters_date_option_hour": "Son Saat",
+ "search_filters_date_option_today": "Bugün",
+ "search_filters_date_option_week": "Bu hafta",
+ "search_filters_date_option_month": "Bu ay",
+ "search_filters_date_option_year": "Bu yıl",
+ "search_filters_type_option_video": "Video",
+ "search_filters_type_option_channel": "Kanal",
+ "search_filters_type_option_playlist": "Oynatma listesi",
+ "search_filters_type_option_movie": "Film",
+ "search_filters_type_option_show": "Gösteri",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "Alt yazılar",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_d": "3B",
+ "search_filters_features_option_live": "Canlı",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "Konum",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "Şu anki sürüm: ",
"next_steps_error_message": "Bundan sonra şunları denemelisiniz: ",
"next_steps_error_message_refresh": "Yenile",
"next_steps_error_message_go_to_youtube": "YouTube'a git",
- "short": "Kısa (4 dakikadan az)",
- "long": "Uzun (20 dakikadan fazla)",
+ "search_filters_duration_option_short": "Kısa (4 dakikadan az)",
+ "search_filters_duration_option_long": "Uzun (20 dakikadan fazla)",
"footer_documentation": "Belgelendirme",
"footer_source_code": "Kaynak kodları",
"footer_original_source_code": "Orijinal kaynak kodları",
@@ -394,8 +392,8 @@
"Video unavailable": "Video kullanılamıyor",
"preferences_quality_option_dash": "DASH (uyarlanabilir kalite)",
"preferences_quality_dash_option_auto": "Otomatik",
- "purchased": "Satın alınan",
- "360": "360°",
+ "search_filters_features_option_purchased": "Satın alınan",
+ "search_filters_features_option_three_sixty": "360°",
"videoinfo_watch_on_youTube": "YouTube'da izle",
"download_subtitles": "Alt yazılar - `x` (.vtt)",
"preferences_save_player_pos_label": "Oynatma konumunu kaydet: ",
@@ -436,7 +434,7 @@
"crash_page_refresh": "<a href=\"`x`\">sayfayı yenilemeye</a> çalıştınız",
"crash_page_switch_instance": "<a href=\"`x`\">başka bir örnek kullanmaya</a> çalıştınız",
"crash_page_read_the_faq": "<a href=\"`x`\">Sık Sorulan Soruları (SSS)</a> okudunuz",
- "crash_page_search_issue": "<a href=\"`x`\">Github'daki sorunlarda</a> aradınız",
+ "crash_page_search_issue": "<a href=\"`x`\">GitHub'daki sorunlarda</a> aradınız",
"crash_page_report_issue": "Yukarıdakilerin hiçbiri yardımcı olmadıysa, lütfen <a href=\"`x`\">GitHub'da yeni bir sorun açın</a> (tercihen İngilizce) ve mesajınıza aşağıdaki metni ekleyin (bu metni ÇEVİRMEYİN):",
"English (United Kingdom)": "İngilizce (Birleşik Krallık)",
"Chinese": "Çince",
@@ -461,5 +459,16 @@
"Portuguese (auto-generated)": "Portekizce (otomatik oluşturuldu)",
"Spanish (Spain)": "İspanyolca (İspanya)",
"Vietnamese (auto-generated)": "Vietnamca (otomatik oluşturuldu)",
- "preferences_watch_history_label": "İzleme geçmişini etkinleştir: "
+ "preferences_watch_history_label": "İzleme geçmişini etkinleştir: ",
+ "search_message_use_another_instance": " Ayrıca <a href=\"`x`\">başka bir örnekte arayabilirsiniz</a>.",
+ "search_filters_type_option_all": "Herhangi bir tür",
+ "search_filters_duration_option_none": "Herhangi bir süre",
+ "search_message_no_results": "Sonuç bulunamadı.",
+ "search_filters_date_label": "Yükleme tarihi",
+ "search_filters_apply_button": "Seçili filtreleri uygula",
+ "search_filters_date_option_none": "Herhangi bir tarih",
+ "search_filters_duration_option_medium": "Orta (4 - 20 dakika)",
+ "search_filters_features_option_vr180": "VR180",
+ "search_filters_title": "Filtreler",
+ "search_message_change_filters_or_query": "Arama sorgunuzu genişletmeyi ve/veya filtreleri değiştirmeyi deneyin."
}
diff --git a/locales/uk.json b/locales/uk.json
index 097752d9..dd03d559 100644
--- a/locales/uk.json
+++ b/locales/uk.json
@@ -21,15 +21,15 @@
"No": "Ні",
"Import and Export Data": "Імпорт і експорт даних",
"Import": "Імпорт",
- "Import Invidious data": "Імпортувати дані Invidious",
- "Import YouTube subscriptions": "Імпортувати підписки з YouTube",
+ "Import Invidious data": "Імпортувати JSON-дані Invidious",
+ "Import YouTube subscriptions": "Імпортувати підписки з YouTube чи OPML",
"Import FreeTube subscriptions (.db)": "Імпортувати підписки з FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Імпортувати підписки з NewPipe (.json)",
"Import NewPipe data (.zip)": "Імпортувати дані з NewPipe (.zip)",
"Export": "Експорт",
"Export subscriptions as OPML": "Експортувати підписки у форматі OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Експортувати підписки у форматі OPML (для NewPipe та FreeTube)",
- "Export data as JSON": "Експортувати дані у форматі JSON",
+ "Export data as JSON": "Експортувати дані Invidious у форматі JSON",
"Delete account?": "Видалити обліківку?",
"History": "Історія",
"An alternative front-end to YouTube": "Альтернативний фронтенд до YouTube",
@@ -189,7 +189,7 @@
"No such user": "Недопустиме ім’я користувача",
"Token is expired, please try again": "Термін дії токена закінчився, спробуйте пізніше",
"English": "Англійська",
- "English (auto-generated)": "Англійська (сгенеровано автоматично)",
+ "English (auto-generated)": "Англійська (автогенератор)",
"Afrikaans": "Африкаанс",
"Albanian": "Албанська",
"Amharic": "Амхарська",
@@ -275,7 +275,7 @@
"Somali": "Сомалійська",
"Southern Sotho": "Сесото (південна сото)",
"Spanish": "Іспанська",
- "Spanish (Latin America)": "Испанська (Латинська Америка)",
+ "Spanish (Latin America)": "Іспанська (Латинська Америка)",
"Sundanese": "Сунданська",
"Swahili": "Суахілі",
"Swedish": "Шведська",
@@ -318,5 +318,173 @@
"Videos": "Відео",
"Playlists": "Плейлисти",
"Community": "Спільнота",
- "Current version: ": "Поточна версія: "
+ "Current version: ": "Поточна версія: ",
+ "generic_views_count_0": "{{count}} перегляд",
+ "generic_views_count_1": "{{count}} перегляди",
+ "generic_views_count_2": "{{count}} переглядів",
+ "generic_videos_count_0": "{{count}} відео",
+ "generic_videos_count_1": "{{count}} відео",
+ "generic_videos_count_2": "{{count}} відео",
+ "generic_playlists_count_0": "{{count}} список відтворення",
+ "generic_playlists_count_1": "{{count}} списки відтворення",
+ "generic_playlists_count_2": "{{count}} списків відтворення",
+ "generic_subscribers_count_0": "{{count}} стежить",
+ "generic_subscribers_count_1": "{{count}} стежать",
+ "generic_subscribers_count_2": "{{count}} стежать",
+ "generic_subscriptions_count_0": "{{count}} підписка",
+ "generic_subscriptions_count_1": "{{count}} підписки",
+ "generic_subscriptions_count_2": "{{count}} підписок",
+ "tokens_count_0": "{{count}} токен",
+ "tokens_count_1": "{{count}} токени",
+ "tokens_count_2": "{{count}} токенів",
+ "subscriptions_unseen_notifs_count_0": "{{count}} нове сповіщення",
+ "subscriptions_unseen_notifs_count_1": "{{count}} нові сповіщення",
+ "subscriptions_unseen_notifs_count_2": "{{count}} нових сповіщень",
+ "comments_view_x_replies_0": "Переглянути {{count}} відповідь",
+ "comments_view_x_replies_1": "Переглянути {{count}} відповіді",
+ "comments_view_x_replies_2": "Переглянути {{count}} відповідей",
+ "generic_count_years_0": "{{count}} рік",
+ "generic_count_years_1": "{{count}} роки",
+ "generic_count_years_2": "{{count}} років",
+ "generic_count_weeks_0": "{{count}} тиждень",
+ "generic_count_weeks_1": "{{count}} тижні",
+ "generic_count_weeks_2": "{{count}} тижнів",
+ "generic_count_days_0": "{{count}} день",
+ "generic_count_days_1": "{{count}} дні",
+ "generic_count_days_2": "{{count}} днів",
+ "generic_count_hours_0": "{{count}} годину",
+ "generic_count_hours_1": "{{count}} години",
+ "generic_count_hours_2": "{{count}} годин",
+ "crash_page_switch_instance": "спробуйте <a href=\"`x`\">використати інший сервер</a>",
+ "crash_page_read_the_faq": "прочитайте <a href=\"`x`\">часті питання (ЧаП)</a>",
+ "crash_page_search_issue": "перегляньте <a href=\"`x`\">наявні обговорення на GitHub</a>",
+ "crash_page_report_issue": "Якщо нічого не допомогло, просимо <a href=\"`x`\">створити обговорення на GitHub</a> (бажано англійською), додавши наступний текст у повідомлення (НЕ перекладайте цього тексту):",
+ "Chinese (Hong Kong)": "Китайська (Гонконг)",
+ "Cantonese (Hong Kong)": "Кантонська (Гонконг)",
+ "Chinese": "Китайська",
+ "Chinese (China)": "Китайська (Китай)",
+ "Interlingue": "Інтерлінгва",
+ "Italian (auto-generated)": "Італійська (автогенератор)",
+ "Turkish (auto-generated)": "Турецька (автогенератор)",
+ "Vietnamese (auto-generated)": "В'єтнамська (автогенератор)",
+ "user_created_playlists": "Створено списків відтворення: `x`",
+ "user_saved_playlists": "Збережено списків відтворення: `x`",
+ "Video unavailable": "Відео недоступне",
+ "preferences_watch_history_label": "Історія переглядів: ",
+ "preferences_quality_dash_label": "Бажана DASH-якість відео: ",
+ "preferences_quality_dash_option_144p": "144p",
+ "preferences_vr_mode_label": "Взаємодія з 360-градусними відео (потребує WebGL): ",
+ "Released under the AGPLv3 on Github.": "Випущено під AGPLv3 на GitHub.",
+ "English (United Kingdom)": "Англійська (Сполучене Королівство)",
+ "English (United States)": "Англійська (США)",
+ "French (auto-generated)": "Французька (автогенератор)",
+ "German (auto-generated)": "Німецька (автогенератор)",
+ "Portuguese (auto-generated)": "Португальська (автогенератор)",
+ "Portuguese (Brazil)": "Португальська (Бразилія)",
+ "Russian (auto-generated)": ":^)",
+ "Spanish (auto-generated)": "Іспанська (автогенератор)",
+ "Spanish (Mexico)": "Іспанська (Мексика)",
+ "Spanish (Spain)": "Іспанська (Іспанія)",
+ "next_steps_error_message_go_to_youtube": "Перейти до YouTube",
+ "footer_donate_page": "Пожертвувати",
+ "footer_documentation": "Документація",
+ "footer_source_code": "Вихідний код",
+ "footer_original_source_code": "Оригінал вихідного коду",
+ "footer_modfied_source_code": "Змінений вихідний код",
+ "adminprefs_modified_source_code_url_label": "URL-адреса репозиторію зміненого вихідного коду",
+ "none": "нема",
+ "videoinfo_started_streaming_x_ago": "Трансляцію розпочато `x` тому",
+ "crash_page_you_found_a_bug": "Схоже, ви знайшли ваду в Invidious!",
+ "crash_page_before_reporting": "Перш ніж прозвітувати про ваду:",
+ "crash_page_refresh": "спробуйте <a href=\"`x`\">оновити сторінку</a>",
+ "preferences_quality_dash_option_auto": "Авто",
+ "preferences_quality_dash_option_best": "Найкраща",
+ "preferences_quality_dash_option_worst": "Найгірша",
+ "preferences_quality_dash_option_1440p": "1440p",
+ "preferences_quality_dash_option_1080p": "1080p",
+ "preferences_save_player_pos_label": "Зберегти позицію відтворення: ",
+ "preferences_show_nick_label": "Псевдонім угорі: ",
+ "Show more": "Докладніше",
+ "next_steps_error_message": "Після чого спробуйте: ",
+ "next_steps_error_message_refresh": "Оновити сторінку",
+ "Search": "Пошук",
+ "preferences_extend_desc_label": "Автоматично розширювати опис відео: ",
+ "preferences_category_misc": "Різноманітні параметри",
+ "Show less": "Коротше",
+ "preferences_quality_option_small": "Низька",
+ "preferences_quality_dash_option_240p": "240p",
+ "preferences_quality_option_medium": "Середня",
+ "preferences_quality_dash_option_4320p": "4320p",
+ "invidious": "Invidious",
+ "preferences_quality_dash_option_720p": "720p",
+ "preferences_quality_dash_option_360p": "360p",
+ "preferences_region_label": "Ваша країна: ",
+ "preferences_quality_option_dash": "DASH (змінна якість)",
+ "preferences_quality_option_hd720": "HD720",
+ "preferences_quality_dash_option_2160p": "2160p",
+ "preferences_automatic_instance_redirect_label": "Автоматична зміна сервера (redirect.invidious.io як резерв): ",
+ "Switch Invidious Instance": "Інший сервер Invidious",
+ "preferences_quality_dash_option_480p": "480p",
+ "Chinese (Taiwan)": "Китайська (Тайвань)",
+ "Dutch (auto-generated)": "Нідерландська (автогенератор)",
+ "Indonesian (auto-generated)": "Індонезійська (автогенератор)",
+ "Japanese (auto-generated)": "Японська (автогенератор)",
+ "Korean (auto-generated)": "Корейська (автогенератор)",
+ "generic_count_months_0": "{{count}} місяць",
+ "generic_count_months_1": "{{count}} місяці",
+ "generic_count_months_2": "{{count}} місяців",
+ "videoinfo_youTube_embed_link": "Вкласти",
+ "generic_count_minutes_0": "{{count}} хвилину",
+ "generic_count_minutes_1": "{{count}} хвилини",
+ "generic_count_minutes_2": "{{count}} хвилин",
+ "generic_count_seconds_0": "{{count}} секунду",
+ "generic_count_seconds_1": "{{count}} секунди",
+ "generic_count_seconds_2": "{{count}} секунд",
+ "videoinfo_watch_on_youTube": "Переглянути на YouTube",
+ "videoinfo_invidious_embed_link": "Вкласти посилання",
+ "download_subtitles": "Субтитри — `x` (.vtt)",
+ "comments_points_count_0": "{{count}} пункт",
+ "comments_points_count_1": "{{count}} пункти",
+ "comments_points_count_2": "{{count}} пунктів",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_location": "Геомітка",
+ "search_filters_duration_option_none": "Будь-які",
+ "search_filters_features_option_hd": "HD",
+ "search_message_change_filters_or_query": "Спробуйте ширший запит і/або інші фільтри.",
+ "search_filters_type_option_all": "Будь-що",
+ "search_filters_type_option_movie": "Фільм",
+ "search_filters_type_option_show": "Шоу",
+ "search_filters_duration_label": "Тривалість",
+ "search_filters_duration_option_short": "Короткі (до 4 хвилин)",
+ "search_message_no_results": "Результатів не знайдено.",
+ "search_filters_date_label": "Дата вивантаження",
+ "search_filters_date_option_none": "Будь-яка дата",
+ "search_filters_date_option_today": "Сьогодні",
+ "search_filters_date_option_week": "Цей тиждень",
+ "search_filters_type_label": "Тип",
+ "search_filters_type_option_channel": "Канал",
+ "search_message_use_another_instance": " Можете також <a href=\"`x`\">пошукати іншим сервером</a>.",
+ "search_filters_title": "Фільтри",
+ "search_filters_date_option_hour": "Остання година",
+ "search_filters_date_option_month": "Цей місяць",
+ "search_filters_date_option_year": "Цей рік",
+ "search_filters_type_option_video": "Відео",
+ "search_filters_type_option_playlist": "Добірка",
+ "search_filters_duration_option_medium": "Середні (4–20 хвилин)",
+ "search_filters_duration_option_long": "Довгі (понад 20 хвилин)",
+ "search_filters_features_label": "Особливості",
+ "search_filters_features_option_live": "Наживо",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_subtitles": "Субтитри",
+ "search_filters_features_option_c_commons": "Creative Commons",
+ "search_filters_features_option_three_sixty": "360°",
+ "search_filters_features_option_hdr": "HDR",
+ "search_filters_sort_label": "Спершу",
+ "search_filters_sort_option_date": "Нещодавні",
+ "search_filters_apply_button": "Застосувати фільтри",
+ "search_filters_features_option_vr180": "VR180",
+ "search_filters_features_option_purchased": "Придбано",
+ "search_filters_sort_option_relevance": "Відповідні",
+ "search_filters_sort_option_rating": "Рейтингові",
+ "search_filters_sort_option_views": "Популярні"
}
diff --git a/locales/vi.json b/locales/vi.json
index a8550686..709013a2 100644
--- a/locales/vi.json
+++ b/locales/vi.json
@@ -138,7 +138,6 @@
"Show less": "Hiện ít hơn",
"Watch on YouTube": "Xem trên YouTube",
"Switch Invidious Instance": "Chuyển phiên bản Invidious",
- "Broken? Try another Invidious Instance": "Bị hỏng? Hãy thử một Phiên bản Invidious khác",
"Hide annotations": "Ẩn chú thích",
"Show annotations": "Hiển thị chú thích",
"Genre: ": "Thể loại: ",
@@ -315,32 +314,32 @@
"Videos": "Video",
"Playlists": "Danh sách phát",
"Community": "Cộng đồng",
- "relevance": "liên quan",
- "rating": "Xếp hạng",
- "date": "ngày",
- "views": "lượt xem",
- "content_type": "content_type",
- "duration": "thời lượng",
- "features": "đặc trưng",
- "sort": "sắp xếp",
- "hour": "giờ",
- "today": "hôm nay",
- "week": "tuần",
- "month": "tháng",
- "year": "năm",
- "video": "video",
- "channel": "kênh",
- "playlist": "danh sách phát",
- "movie": "bộ phim",
- "show": "chỉ",
- "hd": "hd",
- "subtitles": "phụ đề",
- "creative_commons": "Commons sáng tạo",
- "3d": "3d",
- "live": "trực tiếp",
- "4k": "4k",
- "location": "vị trí",
- "hdr": "hdr",
- "filter": "bộ lọc",
- "Current version: ": "Phiên bản hiện tại: "
+ "search_filters_sort_option_relevance": "liên quan",
+ "search_filters_sort_option_rating": "Xếp hạng",
+ "search_filters_sort_option_date": "ngày",
+ "search_filters_sort_option_views": "lượt xem",
+ "search_filters_type_label": "content_type",
+ "search_filters_duration_label": "thời lượng",
+ "search_filters_features_label": "đặc trưng",
+ "search_filters_sort_label": "sắp xếp",
+ "search_filters_date_option_hour": "giờ",
+ "search_filters_date_option_today": "hôm nay",
+ "search_filters_date_option_week": "tuần",
+ "search_filters_date_option_month": "tháng",
+ "search_filters_date_option_year": "năm",
+ "search_filters_type_option_video": "video",
+ "search_filters_type_option_channel": "kênh",
+ "search_filters_type_option_playlist": "danh sách phát",
+ "search_filters_type_option_movie": "bộ phim",
+ "search_filters_type_option_show": "chỉ",
+ "search_filters_features_option_hd": "hd",
+ "search_filters_features_option_subtitles": "phụ đề",
+ "search_filters_features_option_c_commons": "Commons sáng tạo",
+ "search_filters_features_option_three_d": "3d",
+ "search_filters_features_option_live": "trực tiếp",
+ "search_filters_features_option_four_k": "4k",
+ "search_filters_features_option_location": "vị trí",
+ "search_filters_features_option_hdr": "hdr",
+ "Current version: ": "Phiên bản hiện tại: ",
+ "search_filters_title": "bộ lọc"
}
diff --git a/locales/zh-CN.json b/locales/zh-CN.json
index 4b760dd3..ed180628 100644
--- a/locales/zh-CN.json
+++ b/locales/zh-CN.json
@@ -128,7 +128,7 @@
"subscriptions_unseen_notifs_count_0": "{{count}} 条未读通知",
"search": "搜索",
"Log out": "登出",
- "Released under the AGPLv3 on Github.": "依据 AGPLv3 许可证发布于 Github。",
+ "Released under the AGPLv3 on Github.": "依据 AGPLv3 许可证发布于 GitHub。",
"Source available here.": "源码可在此查看。",
"View JavaScript license information.": "查看 JavaScript 协议信息。",
"View privacy policy.": "查看隐私政策。",
@@ -148,7 +148,6 @@
"Show less": "显示较少",
"Watch on YouTube": "在 YouTube 观看",
"Switch Invidious Instance": "切换 Invidious 实例",
- "Broken? Try another Invidious Instance": "无法正常工作? 尝试另一个 Invidious 实例",
"Hide annotations": "隐藏注释",
"Show annotations": "显示注释",
"Genre: ": "风格: ",
@@ -345,39 +344,38 @@
"Videos": "视频",
"Playlists": "播放列表",
"Community": "社区",
- "relevance": "相关度",
- "rating": "评分",
- "date": "上传日期",
- "views": "观看次数",
- "content_type": "类型",
- "duration": "持续时间",
- "features": "功能",
- "sort": "排序依据",
- "hour": "上个小时",
- "today": "今日",
- "week": "本周",
- "month": "本月",
- "year": "今年",
- "video": "视频",
- "channel": "频道",
- "playlist": "播放列表",
- "movie": "电影",
- "show": "真人秀",
- "hd": "高清",
- "subtitles": "字幕",
- "creative_commons": "creative_commons 许可",
- "3d": "3d",
- "live": "直播",
- "4k": "4k",
- "location": "位置",
- "hdr": "hdr",
- "filter": "过滤器",
+ "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_date_option_hour": "上个小时",
+ "search_filters_date_option_today": "今日",
+ "search_filters_date_option_week": "本周",
+ "search_filters_date_option_month": "本月",
+ "search_filters_date_option_year": "今年",
+ "search_filters_type_option_video": "视频",
+ "search_filters_type_option_channel": "频道",
+ "search_filters_type_option_playlist": "播放列表",
+ "search_filters_type_option_movie": "电影",
+ "search_filters_type_option_show": "真人秀",
+ "search_filters_features_option_hd": "高清",
+ "search_filters_features_option_subtitles": "字幕",
+ "search_filters_features_option_c_commons": "creative_commons 许可",
+ "search_filters_features_option_three_d": "3d",
+ "search_filters_features_option_live": "直播",
+ "search_filters_features_option_four_k": "4k",
+ "search_filters_features_option_location": "位置",
+ "search_filters_features_option_hdr": "hdr",
"Current version: ": "当前版本: ",
"next_steps_error_message": "在此之后你应尝试: ",
"next_steps_error_message_refresh": "刷新",
"next_steps_error_message_go_to_youtube": "转到 YouTube",
- "short": "短(少于4分钟)",
- "long": "长(多于 20 分钟)",
+ "search_filters_duration_option_short": "短(少于4分钟)",
+ "search_filters_duration_option_long": "长(多于 20 分钟)",
"footer_documentation": "文档",
"footer_source_code": "源代码",
"footer_modfied_source_code": "修改的源代码",
@@ -391,7 +389,7 @@
"crash_page_refresh": "试着 <a href=\"`x`\">刷新页面</a>",
"crash_page_switch_instance": "试着<a href=\"`x`\">使用另一个实例</a>",
"crash_page_read_the_faq": "阅读<a href=\"`x`\">常见问题</a>",
- "crash_page_search_issue": "搜索过 <a href=\"`x`\">Github 上的现有 issue</a>",
+ "crash_page_search_issue": "搜索过 <a href=\"`x`\">GitHub 上的现有 issue</a>",
"crash_page_report_issue": "如果以上这些都没用的话,请<a href=\"`x`\">在 Github 上新开一个 issue</a>(最好用英语撰写),并在你的消息中包含以下文本(不要翻译该文本):",
"videoinfo_invidious_embed_link": "嵌入链接",
"download_subtitles": "字幕 - `x` (.vtt)",
@@ -418,8 +416,8 @@
"user_created_playlists": "`x` 创建了播放列表",
"user_saved_playlists": "`x` 保存了播放列表",
"Video unavailable": "视频不可用",
- "purchased": "已购买",
- "360": "360°",
+ "search_filters_features_option_purchased": "已购买",
+ "search_filters_features_option_three_sixty": "360°",
"none": "无",
"preferences_save_player_pos_label": "保存播放位置: ",
"Spanish (Mexico)": "西班牙语 (墨西哥)",
@@ -445,5 +443,16 @@
"French (auto-generated)": "法语 (自动生成)",
"Turkish (auto-generated)": "土耳其语 (自动生成)",
"Spanish (Spain)": "西班牙语 (西班牙)",
- "preferences_watch_history_label": "启用观看历史: "
+ "preferences_watch_history_label": "启用观看历史: ",
+ "search_message_use_another_instance": " 你也可以 <a href=\"`x`\">在另一实例上搜索</a>。",
+ "search_filters_title": "过滤器",
+ "search_filters_date_label": "上传日期",
+ "search_filters_apply_button": "应用所选过滤器",
+ "search_message_no_results": "没找到结果。",
+ "search_filters_duration_option_medium": "中等(4-20 分钟)",
+ "search_filters_date_option_none": "任意日期",
+ "search_message_change_filters_or_query": "尝试扩大你的搜索查询和/或更改过滤器。",
+ "search_filters_duration_option_none": "任意时长",
+ "search_filters_type_option_all": "任意类型",
+ "search_filters_features_option_vr180": "VR180"
}
diff --git a/locales/zh-TW.json b/locales/zh-TW.json
index 84bd1dae..4b6fa71b 100644
--- a/locales/zh-TW.json
+++ b/locales/zh-TW.json
@@ -148,7 +148,6 @@
"Show less": "顯示較少",
"Watch on YouTube": "在 YouTube 上觀看",
"Switch Invidious Instance": "切換 Invidious 站台",
- "Broken? Try another Invidious Instance": "故障了嗎?試試看其他 Invidious 站台吧",
"Hide annotations": "隱藏註釋",
"Show annotations": "顯示註釋",
"Genre: ": "風格: ",
@@ -345,39 +344,38 @@
"Videos": "影片",
"Playlists": "播放清單",
"Community": "社群",
- "relevance": "關聯",
- "rating": "評分",
- "date": "日期",
- "views": "檢視",
- "content_type": "內容類型",
- "duration": "時長",
- "features": "特色",
- "sort": "排序",
- "hour": "小時",
- "today": "今天",
- "week": "週",
- "month": "月",
- "year": "年",
- "video": "影片",
- "channel": "頻道",
- "playlist": "播放清單",
- "movie": "電影",
- "show": "秀",
- "hd": "HD",
- "subtitles": "字幕",
- "creative_commons": "創用 CC",
- "3d": "3D",
- "live": "直播",
- "4k": "4K",
- "location": "位置",
- "hdr": "HDR",
- "filter": "篩選條件",
+ "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_date_option_hour": "小時",
+ "search_filters_date_option_today": "今天",
+ "search_filters_date_option_week": "週",
+ "search_filters_date_option_month": "月",
+ "search_filters_date_option_year": "年",
+ "search_filters_type_option_video": "影片",
+ "search_filters_type_option_channel": "頻道",
+ "search_filters_type_option_playlist": "播放清單",
+ "search_filters_type_option_movie": "電影",
+ "search_filters_type_option_show": "秀",
+ "search_filters_features_option_hd": "HD",
+ "search_filters_features_option_subtitles": "字幕",
+ "search_filters_features_option_c_commons": "創用 CC",
+ "search_filters_features_option_three_d": "3D",
+ "search_filters_features_option_live": "直播",
+ "search_filters_features_option_four_k": "4K",
+ "search_filters_features_option_location": "位置",
+ "search_filters_features_option_hdr": "HDR",
"Current version: ": "目前版本: ",
"next_steps_error_message": "之後您應該嘗試: ",
"next_steps_error_message_refresh": "重新整理",
"next_steps_error_message_go_to_youtube": "到 YouTube",
- "short": "短(小於4分鐘)",
- "long": "長(多於20分鐘)",
+ "search_filters_duration_option_short": "短(小於4分鐘)",
+ "search_filters_duration_option_long": "長(多於20分鐘)",
"footer_documentation": "文件",
"footer_source_code": "原始碼",
"footer_original_source_code": "原本的原始碼",
@@ -398,8 +396,8 @@
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_144p": "144p",
"invidious": "Invidious",
- "purchased": "已購買",
- "360": "360°",
+ "search_filters_features_option_purchased": "已購買",
+ "search_filters_features_option_three_sixty": "360°",
"none": "無",
"videoinfo_started_streaming_x_ago": "`x` 前開始串流",
"videoinfo_watch_on_youTube": "在 YouTube 上觀看",
@@ -445,5 +443,16 @@
"Portuguese (Brazil)": "葡萄牙語(巴西)",
"Japanese (auto-generated)": "日語(自動產生)",
"Portuguese (auto-generated)": "葡萄牙語(自動產生)",
- "preferences_watch_history_label": "啟用觀看紀錄: "
+ "preferences_watch_history_label": "啟用觀看紀錄: ",
+ "search_message_change_filters_or_query": "嘗試擴大您的查詢字詞與/或變更過濾條件。",
+ "search_filters_apply_button": "套用選定的過濾條件",
+ "search_message_no_results": "找不到結果。",
+ "search_filters_duration_option_none": "任何時長",
+ "search_filters_duration_option_medium": "中等(4到20分鐘)",
+ "search_filters_features_option_vr180": "VR180",
+ "search_message_use_another_instance": " 您也可以<a href=\"`x`\">在其他站台上搜尋</a>。",
+ "search_filters_title": "過濾條件",
+ "search_filters_date_label": "上傳日期",
+ "search_filters_type_option_all": "任何類型",
+ "search_filters_date_option_none": "任何日期"
}
diff --git a/shard.lock b/shard.lock
index be4333c1..cdce1160 100644
--- a/shard.lock
+++ b/shard.lock
@@ -14,11 +14,11 @@ shards:
exception_page:
git: https://github.com/crystal-loot/exception_page.git
- version: 0.2.0
+ version: 0.2.2
kemal:
git: https://github.com/kemalcr/kemal.git
- version: 1.1.0
+ version: 1.1.2
kilt:
git: https://github.com/jeromegn/kilt.git
diff --git a/shard.yml b/shard.yml
index bf382ec3..9c9b0d37 100644
--- a/shard.yml
+++ b/shard.yml
@@ -18,7 +18,10 @@ dependencies:
version: ~> 0.18.0
kemal:
github: kemalcr/kemal
- version: ~> 1.1.0
+ version: ~> 1.1.2
+ kilt:
+ github: jeromegn/kilt
+ version: ~> 0.6.1
protodec:
github: iv-org/protodec
version: ~> 0.1.4
diff --git a/spec/invidious/helpers_spec.cr b/spec/invidious/helpers_spec.cr
index b2436989..5ecebef3 100644
--- a/spec/invidious/helpers_spec.cr
+++ b/spec/invidious/helpers_spec.cr
@@ -29,20 +29,6 @@ Spectator.describe "Helper" do
end
end
- describe "#produce_search_params" do
- it "correctly produces token for searching with specified filters" do
- expect(produce_search_params).to eq("CAASAhABSAA%3D")
-
- expect(produce_search_params(sort: "upload_date", content_type: "video")).to eq("CAISAhABSAA%3D")
-
- expect(produce_search_params(content_type: "playlist")).to eq("CAASAhADSAA%3D")
-
- expect(produce_search_params(sort: "date", content_type: "video", features: ["hd", "cc", "purchased", "hdr"])).to eq("CAISCxABIAEwAUgByAEBSAA%3D")
-
- expect(produce_search_params(content_type: "channel")).to eq("CAASAhACSAA%3D")
- end
- end
-
describe "#produce_comment_continuation" do
it "correctly produces a continuation token for comments" do
expect(produce_comment_continuation("_cE8xSu6swE", "ADSJ_i2qvJeFtL0htmS5_K5Ctj3eGFVBMWL9Wd42o3kmUL6_mAzdLp85-liQZL0mYr_16BhaggUqX652Sv9JqV6VXinShSP-ZT6rL4NolPBaPXVtJsO5_rA_qE3GubAuLFw9uzIIXU2-HnpXbdgPLWTFavfX206hqWmmpHwUOrmxQV_OX6tYkM3ux3rPAKCDrT8eWL7MU3bLiNcnbgkW8o0h8KYLL_8BPa8LcHbTv8pAoNkjerlX1x7K4pqxaXPoyz89qNlnh6rRx6AXgAzzoHH1dmcyQ8CIBeOHg-m4i8ZxdX4dP88XWrIFg-jJGhpGP8JUMDgZgavxVx225hUEYZMyrLGler5em4FgbG62YWC51moLDLeYEA")).to eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyjAMK9gJBRFNKX2kycXZKZUZ0TDBodG1TNV9LNUN0ajNlR0ZWQk1XTDlXZDQybzNrbVVMNl9tQXpkTHA4NS1saVFaTDBtWXJfMTZCaGFnZ1VxWDY1MlN2OUpxVjZWWGluU2hTUC1aVDZyTDROb2xQQmFQWFZ0SnNPNV9yQV9xRTNHdWJBdUxGdzl1eklJWFUyLUhucFhiZGdQTFdURmF2ZlgyMDZocVdtbXBId1VPcm14UVZfT1g2dFlrTTN1eDNyUEFLQ0RyVDhlV0w3TVUzYkxpTmNuYmdrVzhvMGg4S1lMTF84QlBhOExjSGJUdjhwQW9Oa2plcmxYMXg3SzRwcXhhWFBveXo4OXFObG5oNnJSeDZBWGdBenpvSEgxZG1jeVE4Q0lCZU9IZy1tNGk4WnhkWDRkUDg4WFdySUZnLWpKR2hwR1A4SlVNRGdaZ2F2eFZ4MjI1aFVFWVpNeXJMR2xlcjVlbTRGZ2JHNjJZV0M1MW1vTERMZVlFQSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D")
diff --git a/spec/invidious/search/iv_filters_spec.cr b/spec/invidious/search/iv_filters_spec.cr
new file mode 100644
index 00000000..b0897a63
--- /dev/null
+++ b/spec/invidious/search/iv_filters_spec.cr
@@ -0,0 +1,371 @@
+require "../../../src/invidious/search/filters"
+
+require "http/params"
+require "spectator"
+
+Spectator.configure do |config|
+ config.fail_blank
+ config.randomize
+end
+
+FEATURES_TEXT = {
+ Invidious::Search::Filters::Features::Live => "live",
+ Invidious::Search::Filters::Features::FourK => "4k",
+ Invidious::Search::Filters::Features::HD => "hd",
+ Invidious::Search::Filters::Features::Subtitles => "subtitles",
+ Invidious::Search::Filters::Features::CCommons => "commons",
+ Invidious::Search::Filters::Features::ThreeSixty => "360",
+ Invidious::Search::Filters::Features::VR180 => "vr180",
+ Invidious::Search::Filters::Features::ThreeD => "3d",
+ Invidious::Search::Filters::Features::HDR => "hdr",
+ Invidious::Search::Filters::Features::Location => "location",
+ Invidious::Search::Filters::Features::Purchased => "purchased",
+}
+
+Spectator.describe Invidious::Search::Filters do
+ # -------------------
+ # Decode (legacy)
+ # -------------------
+
+ describe "#from_legacy_filters" do
+ it "Decodes channel: filter" do
+ query = "test channel:UC123456 request"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new)
+ expect(chan).to eq("UC123456")
+ expect(qury).to eq("test request")
+ expect(subs).to be_false
+ end
+
+ it "Decodes user: filter" do
+ query = "user:LinusTechTips broke something (again)"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new)
+ expect(chan).to eq("LinusTechTips")
+ expect(qury).to eq("broke something (again)")
+ expect(subs).to be_false
+ end
+
+ it "Decodes type: filter" do
+ Invidious::Search::Filters::Type.each do |value|
+ query = "Eiffel 65 - Blue [1 Hour] type:#{value}"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(type: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("Eiffel 65 - Blue [1 Hour]")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes content_type: filter" do
+ Invidious::Search::Filters::Type.each do |value|
+ query = "I like to watch content_type:#{value}"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(type: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("I like to watch")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes date: filter" do
+ Invidious::Search::Filters::Date.each do |value|
+ query = "This date:#{value} is old!"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(date: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("This is old!")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes duration: filter" do
+ Invidious::Search::Filters::Duration.each do |value|
+ query = "This duration:#{value} is old!"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(duration: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("This is old!")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes feature: filter" do
+ Invidious::Search::Filters::Features.each do |value|
+ string = FEATURES_TEXT[value]
+ query = "I like my precious feature:#{string} ^^"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(features: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("I like my precious ^^")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes features: filter" do
+ query = "This search has many features:vr180,cc,hdr :o"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ features = Invidious::Search::Filters::Features.flags(HDR, VR180, CCommons)
+
+ expect(fltr).to eq(described_class.new(features: features))
+ expect(chan).to eq("")
+ expect(qury).to eq("This search has many :o")
+ expect(subs).to be_false
+ end
+
+ it "Decodes sort: filter" do
+ Invidious::Search::Filters::Sort.each do |value|
+ query = "Computer? sort:#{value} my files!"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new(sort: value))
+ expect(chan).to eq("")
+ expect(qury).to eq("Computer? my files!")
+ expect(subs).to be_false
+ end
+ end
+
+ it "Decodes subscriptions: filter" do
+ query = "enable subscriptions:true"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new)
+ expect(chan).to eq("")
+ expect(qury).to eq("enable")
+ expect(subs).to be_true
+ end
+
+ it "Ignores junk data" do
+ query = "duration:I sort:like type:cleaning features:stuff date:up!"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new)
+ expect(chan).to eq("")
+ expect(qury).to eq("")
+ expect(subs).to be_false
+ end
+
+ it "Keeps unknown keys" do
+ query = "to:be or:not to:be"
+
+ fltr, chan, qury, subs = described_class.from_legacy_filters(query)
+
+ expect(fltr).to eq(described_class.new)
+ expect(chan).to eq("")
+ expect(qury).to eq("to:be or:not to:be")
+ expect(subs).to be_false
+ end
+ end
+
+ # -------------------
+ # Decode (URL)
+ # -------------------
+
+ describe "#from_iv_params" do
+ it "Decodes type= filter" do
+ Invidious::Search::Filters::Type.each do |value|
+ params = HTTP::Params.parse("type=#{value}")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(type: value))
+ end
+ end
+
+ it "Decodes date= filter" do
+ Invidious::Search::Filters::Date.each do |value|
+ params = HTTP::Params.parse("date=#{value}")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(date: value))
+ end
+ end
+
+ it "Decodes duration= filter" do
+ Invidious::Search::Filters::Duration.each do |value|
+ params = HTTP::Params.parse("duration=#{value}")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(duration: value))
+ end
+ end
+
+ it "Decodes features= filter (single)" do
+ Invidious::Search::Filters::Features.each do |value|
+ string = described_class.format_features(value)
+ params = HTTP::Params.parse("features=#{string}")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(features: value))
+ end
+ end
+
+ it "Decodes features= filter (multiple - comma separated)" do
+ features = Invidious::Search::Filters::Features.flags(HDR, VR180, CCommons)
+ params = HTTP::Params.parse("features=vr180%2Ccc%2Chdr") # %2C is a comma
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(features: features))
+ end
+
+ it "Decodes features= filter (multiple - URL parameters)" do
+ features = Invidious::Search::Filters::Features.flags(ThreeSixty, HD, FourK)
+ params = HTTP::Params.parse("features=4k&features=360&features=hd")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(features: features))
+ end
+
+ it "Decodes sort= filter" do
+ Invidious::Search::Filters::Sort.each do |value|
+ params = HTTP::Params.parse("sort=#{value}")
+
+ expect(described_class.from_iv_params(params))
+ .to eq(described_class.new(sort: value))
+ end
+ end
+
+ it "Ignores junk data" do
+ params = HTTP::Params.parse("foo=bar&sort=views&answer=42&type=channel")
+
+ expect(described_class.from_iv_params(params)).to eq(
+ described_class.new(
+ sort: Invidious::Search::Filters::Sort::Views,
+ type: Invidious::Search::Filters::Type::Channel
+ )
+ )
+ end
+ end
+
+ # -------------------
+ # Encode (URL)
+ # -------------------
+
+ describe "#to_iv_params" do
+ it "Encodes date filter" do
+ Invidious::Search::Filters::Date.each do |value|
+ filters = described_class.new(date: value)
+ params = filters.to_iv_params
+
+ if value.none?
+ expect("#{params}").to eq("")
+ else
+ expect("#{params}").to eq("date=#{value.to_s.underscore}")
+ end
+ end
+ end
+
+ it "Encodes type filter" do
+ Invidious::Search::Filters::Type.each do |value|
+ filters = described_class.new(type: value)
+ params = filters.to_iv_params
+
+ if value.all?
+ expect("#{params}").to eq("")
+ else
+ expect("#{params}").to eq("type=#{value.to_s.underscore}")
+ end
+ end
+ end
+
+ it "Encodes duration filter" do
+ Invidious::Search::Filters::Duration.each do |value|
+ filters = described_class.new(duration: value)
+ params = filters.to_iv_params
+
+ if value.none?
+ expect("#{params}").to eq("")
+ else
+ expect("#{params}").to eq("duration=#{value.to_s.underscore}")
+ end
+ end
+ end
+
+ it "Encodes features filter (single)" do
+ Invidious::Search::Filters::Features.each do |value|
+ string = described_class.format_features(value)
+ filters = described_class.new(features: value)
+
+ expect("#{filters.to_iv_params}")
+ .to eq("features=" + FEATURES_TEXT[value])
+ end
+ end
+
+ it "Encodes features filter (multiple)" do
+ features = Invidious::Search::Filters::Features.flags(Subtitles, Live, ThreeSixty)
+ filters = described_class.new(features: features)
+
+ expect("#{filters.to_iv_params}")
+ .to eq("features=live%2Csubtitles%2C360") # %2C is a comma
+ end
+
+ it "Encodes sort filter" do
+ Invidious::Search::Filters::Sort.each do |value|
+ filters = described_class.new(sort: value)
+ params = filters.to_iv_params
+
+ if value.relevance?
+ expect("#{params}").to eq("")
+ else
+ expect("#{params}").to eq("sort=#{value.to_s.underscore}")
+ end
+ end
+ end
+
+ it "Encodes multiple filters" do
+ filters = described_class.new(
+ date: Invidious::Search::Filters::Date::Today,
+ duration: Invidious::Search::Filters::Duration::Medium,
+ features: Invidious::Search::Filters::Features.flags(Location, Purchased),
+ sort: Invidious::Search::Filters::Sort::Relevance
+ )
+
+ params = filters.to_iv_params
+
+ # Check the `date` param
+ expect(params).to have_key("date")
+ expect(params.fetch_all("date")).to contain_exactly("today")
+
+ # Check the `type` param
+ expect(params).to_not have_key("type")
+ expect(params["type"]?).to be_nil
+
+ # Check the `duration` param
+ expect(params).to have_key("duration")
+ expect(params.fetch_all("duration")).to contain_exactly("medium")
+
+ # Check the `features` param
+ expect(params).to have_key("features")
+ expect(params.fetch_all("features")).to contain_exactly("location,purchased")
+
+ # Check the `sort` param
+ expect(params).to_not have_key("sort")
+ expect(params["sort"]?).to be_nil
+
+ # Check if there aren't other parameters
+ params.delete("date")
+ params.delete("duration")
+ params.delete("features")
+
+ expect(params).to be_empty
+ end
+ end
+end
diff --git a/spec/invidious/search/query_spec.cr b/spec/invidious/search/query_spec.cr
new file mode 100644
index 00000000..4853e9e9
--- /dev/null
+++ b/spec/invidious/search/query_spec.cr
@@ -0,0 +1,200 @@
+require "../../../src/invidious/search/filters"
+require "../../../src/invidious/search/query"
+
+require "http/params"
+require "spectator"
+
+Spectator.configure do |config|
+ config.fail_blank
+ config.randomize
+end
+
+Spectator.describe Invidious::Search::Query do
+ describe Type::Regular do
+ # -------------------
+ # Query parsing
+ # -------------------
+
+ it "parses query with URL prameters (q)" do
+ query = described_class.new(
+ HTTP::Params.parse("q=What+is+Love+10+hour&type=video&duration=long"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Regular)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("What is Love 10 hour")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ type: Invidious::Search::Filters::Type::Video,
+ duration: Invidious::Search::Filters::Duration::Long
+ )
+ )
+ end
+
+ it "parses query with URL prameters (search_query)" do
+ query = described_class.new(
+ HTTP::Params.parse("search_query=What+is+Love+10+hour&type=video&duration=long"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Regular)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("What is Love 10 hour")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ type: Invidious::Search::Filters::Type::Video,
+ duration: Invidious::Search::Filters::Duration::Long
+ )
+ )
+ end
+
+ it "parses query with legacy filters (q)" do
+ query = described_class.new(
+ HTTP::Params.parse("q=Nyan+cat+duration:long"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Regular)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Nyan cat")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ duration: Invidious::Search::Filters::Duration::Long
+ )
+ )
+ end
+
+ it "parses query with legacy filters (search_query)" do
+ query = described_class.new(
+ HTTP::Params.parse("search_query=Nyan+cat+duration:long"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Regular)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Nyan cat")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ duration: Invidious::Search::Filters::Duration::Long
+ )
+ )
+ end
+
+ it "parses query with both URL params and legacy filters" do
+ query = described_class.new(
+ HTTP::Params.parse("q=Vamos+a+la+playa+duration:long&type=Video&date=year"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Regular)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Vamos a la playa duration:long")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ type: Invidious::Search::Filters::Type::Video,
+ date: Invidious::Search::Filters::Date::Year
+ )
+ )
+ end
+
+ # -------------------
+ # Type switching
+ # -------------------
+
+ it "switches to channel search (URL param)" do
+ query = described_class.new(
+ HTTP::Params.parse("q=thunderbolt+4&channel=UC0vBXGSyV14uvJ4hECDOl0Q"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Channel)
+ expect(query.channel).to eq("UC0vBXGSyV14uvJ4hECDOl0Q")
+ expect(query.text).to eq("thunderbolt 4")
+ expect(query.filters.default?).to be_true
+ end
+
+ it "switches to channel search (legacy)" do
+ query = described_class.new(
+ HTTP::Params.parse("q=channel%3AUCRPdsCVuH53rcbTcEkuY4uQ+rdna3"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Channel)
+ expect(query.channel).to eq("UCRPdsCVuH53rcbTcEkuY4uQ")
+ expect(query.text).to eq("rdna3")
+ expect(query.filters.default?).to be_true
+ end
+
+ it "switches to subscriptions search" do
+ query = described_class.new(
+ HTTP::Params.parse("q=subscriptions:true+tunak+tunak+tun"),
+ Invidious::Search::Query::Type::Regular, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Subscriptions)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("tunak tunak tun")
+ expect(query.filters.default?).to be_true
+ end
+ end
+
+ describe Type::Channel do
+ it "ignores extra parameters" do
+ query = described_class.new(
+ HTTP::Params.parse("q=Take+on+me+channel%3AUC12345679&type=video&date=year"),
+ Invidious::Search::Query::Type::Channel, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Channel)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Take on me")
+ expect(query.filters.default?).to be_true
+ end
+ end
+
+ describe Type::Subscriptions do
+ it "works" do
+ query = described_class.new(
+ HTTP::Params.parse("q=Harlem+shake&type=video&date=year"),
+ Invidious::Search::Query::Type::Subscriptions, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Subscriptions)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Harlem shake")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ type: Invidious::Search::Filters::Type::Video,
+ date: Invidious::Search::Filters::Date::Year
+ )
+ )
+ end
+ end
+
+ describe Type::Playlist do
+ it "ignores extra parameters" do
+ query = described_class.new(
+ HTTP::Params.parse("q=Harlem+shake+type:video+date:year&channel=UC12345679"),
+ Invidious::Search::Query::Type::Playlist, nil
+ )
+
+ expect(query.type).to eq(Invidious::Search::Query::Type::Playlist)
+ expect(query.channel).to be_empty
+ expect(query.text).to eq("Harlem shake")
+
+ expect(query.filters).to eq(
+ Invidious::Search::Filters.new(
+ type: Invidious::Search::Filters::Type::Video,
+ date: Invidious::Search::Filters::Date::Year
+ )
+ )
+ end
+ end
+end
diff --git a/spec/invidious/search/yt_filters_spec.cr b/spec/invidious/search/yt_filters_spec.cr
new file mode 100644
index 00000000..bf7f21e7
--- /dev/null
+++ b/spec/invidious/search/yt_filters_spec.cr
@@ -0,0 +1,143 @@
+require "../../../src/invidious/search/filters"
+
+require "http/params"
+require "spectator"
+
+Spectator.configure do |config|
+ config.fail_blank
+ config.randomize
+end
+
+# Encoded filter values are extracted from the search
+# page of Youtube with any browser devtools HTML inspector.
+
+DATE_FILTERS = {
+ Invidious::Search::Filters::Date::Hour => "EgIIAQ%3D%3D",
+ Invidious::Search::Filters::Date::Today => "EgIIAg%3D%3D",
+ Invidious::Search::Filters::Date::Week => "EgIIAw%3D%3D",
+ Invidious::Search::Filters::Date::Month => "EgIIBA%3D%3D",
+ Invidious::Search::Filters::Date::Year => "EgIIBQ%3D%3D",
+}
+
+TYPE_FILTERS = {
+ Invidious::Search::Filters::Type::Video => "EgIQAQ%3D%3D",
+ Invidious::Search::Filters::Type::Channel => "EgIQAg%3D%3D",
+ Invidious::Search::Filters::Type::Playlist => "EgIQAw%3D%3D",
+ Invidious::Search::Filters::Type::Movie => "EgIQBA%3D%3D",
+}
+
+DURATION_FILTERS = {
+ Invidious::Search::Filters::Duration::Short => "EgIYAQ%3D%3D",
+ Invidious::Search::Filters::Duration::Medium => "EgIYAw%3D%3D",
+ Invidious::Search::Filters::Duration::Long => "EgIYAg%3D%3D",
+}
+
+FEATURE_FILTERS = {
+ Invidious::Search::Filters::Features::Live => "EgJAAQ%3D%3D",
+ Invidious::Search::Filters::Features::FourK => "EgJwAQ%3D%3D",
+ Invidious::Search::Filters::Features::HD => "EgIgAQ%3D%3D",
+ Invidious::Search::Filters::Features::Subtitles => "EgIoAQ%3D%3D",
+ Invidious::Search::Filters::Features::CCommons => "EgIwAQ%3D%3D",
+ Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AQ%3D%3D",
+ Invidious::Search::Filters::Features::VR180 => "EgPQAQE%3D",
+ Invidious::Search::Filters::Features::ThreeD => "EgI4AQ%3D%3D",
+ Invidious::Search::Filters::Features::HDR => "EgPIAQE%3D",
+ Invidious::Search::Filters::Features::Location => "EgO4AQE%3D",
+ Invidious::Search::Filters::Features::Purchased => "EgJIAQ%3D%3D",
+}
+
+SORT_FILTERS = {
+ Invidious::Search::Filters::Sort::Relevance => "",
+ Invidious::Search::Filters::Sort::Date => "CAI%3D",
+ Invidious::Search::Filters::Sort::Views => "CAM%3D",
+ Invidious::Search::Filters::Sort::Rating => "CAE%3D",
+}
+
+Spectator.describe Invidious::Search::Filters do
+ # -------------------
+ # Encode YT params
+ # -------------------
+
+ describe "#to_yt_params" do
+ sample DATE_FILTERS do |value, result|
+ it "Encodes upload date filter '#{value}'" do
+ expect(described_class.new(date: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample TYPE_FILTERS do |value, result|
+ it "Encodes content type filter '#{value}'" do
+ expect(described_class.new(type: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample DURATION_FILTERS do |value, result|
+ it "Encodes duration filter '#{value}'" do
+ expect(described_class.new(duration: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample FEATURE_FILTERS do |value, result|
+ it "Encodes feature filter '#{value}'" do
+ expect(described_class.new(features: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample SORT_FILTERS do |value, result|
+ it "Encodes sort filter '#{value}'" do
+ expect(described_class.new(sort: value).to_yt_params).to eq(result)
+ end
+ end
+ end
+
+ # -------------------
+ # Decode YT params
+ # -------------------
+
+ describe "#from_yt_params" do
+ sample DATE_FILTERS do |value, encoded|
+ it "Decodes upload date filter '#{value}'" do
+ params = HTTP::Params.parse("sp=#{encoded}")
+
+ expect(described_class.from_yt_params(params))
+ .to eq(described_class.new(date: value))
+ end
+ end
+
+ sample TYPE_FILTERS do |value, encoded|
+ it "Decodes content type filter '#{value}'" do
+ params = HTTP::Params.parse("sp=#{encoded}")
+
+ expect(described_class.from_yt_params(params))
+ .to eq(described_class.new(type: value))
+ end
+ end
+
+ sample DURATION_FILTERS do |value, encoded|
+ it "Decodes duration filter '#{value}'" do
+ params = HTTP::Params.parse("sp=#{encoded}")
+
+ expect(described_class.from_yt_params(params))
+ .to eq(described_class.new(duration: value))
+ end
+ end
+
+ sample FEATURE_FILTERS do |value, encoded|
+ it "Decodes feature filter '#{value}'" do
+ params = HTTP::Params.parse("sp=#{encoded}")
+
+ expect(described_class.from_yt_params(params))
+ .to eq(described_class.new(features: value))
+ end
+ end
+
+ sample SORT_FILTERS do |value, encoded|
+ it "Decodes sort filter '#{value}'" do
+ params = HTTP::Params.parse("sp=#{encoded}")
+
+ expect(described_class.from_yt_params(params))
+ .to eq(described_class.new(sort: value))
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr
index 09320750..6c492e2f 100644
--- a/spec/spec_helper.cr
+++ b/spec/spec_helper.cr
@@ -8,7 +8,7 @@ require "../src/invidious/channels/*"
require "../src/invidious/videos"
require "../src/invidious/comments"
require "../src/invidious/playlists"
-require "../src/invidious/search"
+require "../src/invidious/search/ctoken"
require "../src/invidious/trending"
require "spectator"
diff --git a/src/ext/kemal_content_for.cr b/src/ext/kemal_content_for.cr
new file mode 100644
index 00000000..a4f3fd96
--- /dev/null
+++ b/src/ext/kemal_content_for.cr
@@ -0,0 +1,16 @@
+# Overrides for Kemal's `content_for` macro in order to keep using
+# kilt as it was before Kemal v1.1.1 (Kemal PR #618).
+
+require "kemal"
+require "kilt"
+
+macro content_for(key, file = __FILE__)
+ %proc = ->() {
+ __kilt_io__ = IO::Memory.new
+ {{ yield }}
+ __kilt_io__.to_s
+ }
+
+ CONTENT_FOR_BLOCKS[{{key}}] = Tuple.new {{file}}, %proc
+ nil
+end
diff --git a/src/invidious/helpers/static_file_handler.cr b/src/ext/kemal_static_file_handler.cr
index 6ef2d74c..6ef2d74c 100644
--- a/src/invidious/helpers/static_file_handler.cr
+++ b/src/ext/kemal_static_file_handler.cr
diff --git a/src/invidious.cr b/src/invidious.cr
index a470c6b6..dd240852 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -16,7 +16,13 @@
require "digest/md5"
require "file_utils"
+
+# Require kemal, kilt, then our own overrides
require "kemal"
+require "kilt"
+require "./ext/kemal_content_for.cr"
+require "./ext/kemal_static_file_handler.cr"
+
require "athena-negotiation"
require "openssl/hmac"
require "option_parser"
@@ -35,6 +41,7 @@ require "./invidious/frontend/*"
require "./invidious/*"
require "./invidious/channels/*"
require "./invidious/user/*"
+require "./invidious/search/*"
require "./invidious/routes/**"
require "./invidious/jobs/**"
diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr
index 54cede37..3ae49aa6 100644
--- a/src/invidious/comments.cr
+++ b/src/invidious/comments.cr
@@ -580,31 +580,41 @@ def content_to_comment_html(content)
if run["navigationEndpoint"]?
if url = run["navigationEndpoint"]["urlEndpoint"]?.try &.["url"].as_s
url = URI.parse(url)
+ displayed_url = url
if url.host == "youtu.be"
url = "/watch?v=#{url.request_target.lstrip('/')}"
+ displayed_url = "youtube.com#{url}"
elsif url.host.nil? || url.host.not_nil!.ends_with?("youtube.com")
if url.path == "/redirect"
# Sometimes, links can be corrupted (why?) so make sure to fallback
# nicely. See https://github.com/iv-org/invidious/issues/2682
url = HTTP::Params.parse(url.query.not_nil!)["q"]? || ""
+ displayed_url = url
else
url = url.request_target
+ displayed_url = "youtube.com#{url}"
end
end
- text = %(<a href="#{url}">#{text}</a>)
+ text = %(<a href="#{url}">#{reduce_uri(displayed_url)}</a>)
elsif watch_endpoint = run["navigationEndpoint"]["watchEndpoint"]?
length_seconds = watch_endpoint["startTimeSeconds"]?
video_id = watch_endpoint["videoId"].as_s
- if length_seconds && length_seconds.as_i > 0
+ if length_seconds && length_seconds.as_i >= 0
text = %(<a href="javascript:void(0)" data-onclick="jump_to_time" data-jump-time="#{length_seconds}">#{text}</a>)
else
- text = %(<a href="/watch?v=#{video_id}">#{text}</a>)
+ text = %(<a href="/watch?v=#{video_id}">#{"youtube.com/watch?v=#{video_id}"}</a>)
end
elsif url = run.dig?("navigationEndpoint", "commandMetadata", "webCommandMetadata", "url").try &.as_s
- text = %(<a href="#{url}">#{text}</a>)
+ if text.starts_with?(/\s?@/)
+ # Handle "pings" in comments differently
+ # See: https://github.com/iv-org/invidious/issues/3038
+ text = %(<a href="#{url}">#{text}</a>)
+ else
+ text = %(<a href="#{url}">#{reduce_uri(url)}</a>)
+ end
end
end
diff --git a/src/invidious/exceptions.cr b/src/invidious/exceptions.cr
index 490d98cd..bfaa3fd5 100644
--- a/src/invidious/exceptions.cr
+++ b/src/invidious/exceptions.cr
@@ -1,3 +1,11 @@
+# Exception used to hold the bogus UCID during a channel search.
+class ChannelSearchException < InfoException
+ getter channel : String
+
+ def initialize(@channel)
+ end
+end
+
# Exception used to hold the name of the missing item
# Should be used in all parsing functions
class BrokenTubeException < Exception
diff --git a/src/invidious/frontend/misc.cr b/src/invidious/frontend/misc.cr
new file mode 100644
index 00000000..43ba9f5c
--- /dev/null
+++ b/src/invidious/frontend/misc.cr
@@ -0,0 +1,14 @@
+module Invidious::Frontend::Misc
+ extend self
+
+ def redirect_url(env : HTTP::Server::Context)
+ prefs = env.get("preferences").as(Preferences)
+
+ if prefs.automatic_instance_redirect
+ current_page = env.get?("current_page").as(String)
+ redirect_url = "/redirect?referer=#{current_page}"
+ else
+ redirect_url = "https://redirect.invidious.io#{env.request.resource}"
+ end
+ end
+end
diff --git a/src/invidious/frontend/search_filters.cr b/src/invidious/frontend/search_filters.cr
new file mode 100644
index 00000000..8ac0af2e
--- /dev/null
+++ b/src/invidious/frontend/search_filters.cr
@@ -0,0 +1,135 @@
+module Invidious::Frontend::SearchFilters
+ extend self
+
+ # Generate the search filters collapsable widget.
+ def generate(filters : Search::Filters, query : String, page : Int, locale : String) : String
+ return String.build(8000) do |str|
+ str << "<div id='filters'>\n"
+ str << "\t<details id='filters-collapse'>"
+ str << "\t\t<summary>" << translate(locale, "search_filters_title") << "</summary>\n"
+
+ str << "\t\t<div id='filters-box'><form action='/search' method='get'>\n"
+
+ str << "\t\t\t<input type='hidden' name='q' value='" << HTML.escape(query) << "'>\n"
+ str << "\t\t\t<input type='hidden' name='page' value='" << page << "'>\n"
+
+ str << "\t\t\t<div id='filters-flex'>"
+
+ filter_wrapper(date)
+ filter_wrapper(type)
+ filter_wrapper(duration)
+ filter_wrapper(features)
+ filter_wrapper(sort)
+
+ str << "\t\t\t</div>\n"
+
+ str << "\t\t\t<div id='filters-apply'>"
+ str << "<button type='submit' class=\"pure-button pure-button-primary\">"
+ str << translate(locale, "search_filters_apply_button")
+ str << "</button></div>\n"
+
+ str << "\t\t</form></div>\n"
+
+ str << "\t</details>\n"
+ str << "</div>\n"
+ end
+ end
+
+ # Generate wrapper HTML (`<div>`, filter name, etc...) around the
+ # `<input>` elements of a search filter
+ macro filter_wrapper(name)
+ str << "\t\t\t\t<div class=\"filter-column\"><fieldset>\n"
+
+ str << "\t\t\t\t\t<legend><div class=\"filter-name underlined\">"
+ str << translate(locale, "search_filters_{{name}}_label")
+ str << "</div></legend>\n"
+
+ str << "\t\t\t\t\t<div class=\"filter-options\">\n"
+ make_{{name}}_filter_options(str, filters.{{name}}, locale)
+ str << "\t\t\t\t\t</div>"
+
+ str << "\t\t\t\t</fieldset></div>\n"
+ end
+
+ # Generates the HTML for the list of radio buttons of the "date" search filter
+ def make_date_filter_options(str : String::Builder, value : Search::Filters::Date, locale : String)
+ {% for value in Invidious::Search::Filters::Date.constants %}
+ {% date = value.underscore %}
+
+ str << "\t\t\t\t\t\t<div>"
+ str << "<input type='radio' name='date' id='filter-date-{{date}}' value='{{date}}'"
+ str << " checked" if value.{{date}}?
+ str << '>'
+
+ str << "<label for='filter-date-{{date}}'>"
+ str << translate(locale, "search_filters_date_option_{{date}}")
+ str << "</label></div>\n"
+ {% end %}
+ end
+
+ # Generates the HTML for the list of radio buttons of the "type" search filter
+ def make_type_filter_options(str : String::Builder, value : Search::Filters::Type, locale : String)
+ {% for value in Invidious::Search::Filters::Type.constants %}
+ {% type = value.underscore %}
+
+ str << "\t\t\t\t\t\t<div>"
+ str << "<input type='radio' name='type' id='filter-type-{{type}}' value='{{type}}'"
+ str << " checked" if value.{{type}}?
+ str << '>'
+
+ str << "<label for='filter-type-{{type}}'>"
+ str << translate(locale, "search_filters_type_option_{{type}}")
+ str << "</label></div>\n"
+ {% end %}
+ end
+
+ # Generates the HTML for the list of radio buttons of the "duration" search filter
+ def make_duration_filter_options(str : String::Builder, value : Search::Filters::Duration, locale : String)
+ {% for value in Invidious::Search::Filters::Duration.constants %}
+ {% duration = value.underscore %}
+
+ str << "\t\t\t\t\t\t<div>"
+ str << "<input type='radio' name='duration' id='filter-duration-{{duration}}' value='{{duration}}'"
+ str << " checked" if value.{{duration}}?
+ str << '>'
+
+ str << "<label for='filter-duration-{{duration}}'>"
+ str << translate(locale, "search_filters_duration_option_{{duration}}")
+ str << "</label></div>\n"
+ {% end %}
+ end
+
+ # Generates the HTML for the list of checkboxes of the "features" search filter
+ def make_features_filter_options(str : String::Builder, value : Search::Filters::Features, locale : String)
+ {% for value in Invidious::Search::Filters::Features.constants %}
+ {% if value.stringify != "All" && value.stringify != "None" %}
+ {% feature = value.underscore %}
+
+ str << "\t\t\t\t\t\t<div>"
+ str << "<input type='checkbox' name='features' id='filter-feature-{{feature}}' value='{{feature}}'"
+ str << " checked" if value.{{feature}}?
+ str << '>'
+
+ str << "<label for='filter-feature-{{feature}}'>"
+ str << translate(locale, "search_filters_features_option_{{feature}}")
+ str << "</label></div>\n"
+ {% end %}
+ {% end %}
+ end
+
+ # Generates the HTML for the list of radio buttons of the "sort" search filter
+ def make_sort_filter_options(str : String::Builder, value : Search::Filters::Sort, locale : String)
+ {% for value in Invidious::Search::Filters::Sort.constants %}
+ {% sort = value.underscore %}
+
+ str << "\t\t\t\t\t\t<div>"
+ str << "<input type='radio' name='sort' id='filter-sort-{{sort}}' value='{{sort}}'"
+ str << " checked" if value.{{sort}}?
+ str << '>'
+
+ str << "<label for='filter-sort-{{sort}}'>"
+ str << translate(locale, "search_filters_sort_option_{{sort}}")
+ str << "</label></div>\n"
+ {% end %}
+ end
+end
diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr
index 6155e561..2eab6263 100644
--- a/src/invidious/helpers/errors.cr
+++ b/src/invidious/helpers/errors.cr
@@ -49,7 +49,7 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
issue_template += github_details("Backtrace", HTML.escape(exception.inspect_with_backtrace))
# URLs for the error message below
- url_faq = "https://github.com/iv-org/documentation/blob/master/FAQ.md"
+ url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
url_search_issues = "https://github.com/iv-org/invidious/issues"
url_switch = "https://redirect.invidious.io" + env.request.resource
diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr
index 39e183f2..982b97d8 100644
--- a/src/invidious/helpers/i18n.cr
+++ b/src/invidious/helpers/i18n.cr
@@ -29,10 +29,10 @@ LOCALES_LIST = {
"pt-BR" => "Português Brasileiro", # Portuguese (Brazil)
"pt-PT" => "Português de Portugal", # Portuguese (Portugal)
"ro" => "Română", # Romanian
- "ru" => "русский", # Russian
+ "ru" => "Русский", # Russian
"sq" => "Shqip", # Albanian
- "sr" => "srpski (latinica)", # Serbian (Latin)
- "sr_Cyrl" => "српски (ћирилица)", # Serbian (Cyrillic)
+ "sr" => "Srpski (latinica)", # Serbian (Latin)
+ "sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
"sv-SE" => "Svenska", # Swedish
"tr" => "Türkçe", # Turkish
"uk" => "Українська", # Ukrainian
diff --git a/src/invidious/helpers/macros.cr b/src/invidious/helpers/macros.cr
index 75df1612..43e7171b 100644
--- a/src/invidious/helpers/macros.cr
+++ b/src/invidious/helpers/macros.cr
@@ -48,13 +48,19 @@ module JSON::Serializable
end
end
-macro templated(filename, template = "template", navbar_search = true)
+macro templated(_filename, template = "template", navbar_search = true)
navbar_search = {{navbar_search}}
- render "src/invidious/views/#{{{filename}}}.ecr", "src/invidious/views/#{{{template}}}.ecr"
+
+ {{ filename = "src/invidious/views/" + _filename + ".ecr" }}
+ {{ layout = "src/invidious/views/" + template + ".ecr" }}
+
+ __content_filename__ = {{filename}}
+ content = Kilt.render({{filename}})
+ Kilt.render({{layout}})
end
macro rendered(filename)
- render "src/invidious/views/#{{{filename}}}.ecr"
+ Kilt.render("src/invidious/views/#{{{filename}}}.ecr")
end
# Similar to Kemals halt method but works in a
diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr
index c1dc17db..8ae5034a 100644
--- a/src/invidious/helpers/utils.cr
+++ b/src/invidious/helpers/utils.cr
@@ -383,3 +383,11 @@ def fetch_random_instance
return filtered_instance_list.sample(1)[0]
end
+
+def reduce_uri(uri : URI | String, max_length : Int32 = 50, suffix : String = "…") : String
+ str = uri.to_s.sub(/^https?:\/\//, "")
+ if str.size > max_length
+ str = "#{str[0, max_length]}#{suffix}"
+ end
+ return str
+end
diff --git a/src/invidious/routes/api/manifest.cr b/src/invidious/routes/api/manifest.cr
index ca429df5..8bc36946 100644
--- a/src/invidious/routes/api/manifest.cr
+++ b/src/invidious/routes/api/manifest.cr
@@ -56,12 +56,15 @@ module Invidious::Routes::API::Manifest
xml.element("Period") do
i = 0
- {"audio/mp4", "audio/webm"}.each do |mime_type|
+ {"audio/mp4"}.each do |mime_type|
mime_streams = audio_streams.select { |stream| stream["mimeType"].as_s.starts_with? mime_type }
next if mime_streams.empty?
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true) do
mime_streams.each do |fmt|
+ # OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415)
+ next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange"))
+
codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
bandwidth = fmt["bitrate"].as_i
itag = fmt["itag"].as_i
@@ -83,13 +86,16 @@ module Invidious::Routes::API::Manifest
potential_heights = {4320, 2160, 1440, 1080, 720, 480, 360, 240, 144}
- {"video/mp4", "video/webm"}.each do |mime_type|
+ {"video/mp4"}.each do |mime_type|
mime_streams = video_streams.select { |stream| stream["mimeType"].as_s.starts_with? mime_type }
next if mime_streams.empty?
heights = [] of Int32
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, scanType: "progressive") do
mime_streams.each do |fmt|
+ # OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415)
+ next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange"))
+
codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
bandwidth = fmt["bitrate"].as_i
itag = fmt["itag"].as_i
diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr
index c4d6643a..8650976d 100644
--- a/src/invidious/routes/api/v1/channels.cr
+++ b/src/invidious/routes/api/v1/channels.cr
@@ -251,18 +251,22 @@ module Invidious::Routes::API::V1::Channels
def self.search(env)
locale = env.get("preferences").as(Preferences).locale
+ region = env.params.query["region"]?
env.response.content_type = "application/json"
- ucid = env.params.url["ucid"]
+ query = Invidious::Search::Query.new(env.params.query, :channel, region)
- query = env.params.query["q"]?
- query ||= ""
+ # Required because we can't (yet) pass multiple parameter to the
+ # `Search::Query` initializer (in this case, an URL segment)
+ query.channel = env.params.url["ucid"]
- page = env.params.query["page"]?.try &.to_i?
- page ||= 1
+ begin
+ search_results = query.process
+ rescue ex
+ return error_json(400, ex)
+ end
- search_results = channel_search(query, page, ucid)
JSON.build do |json|
json.array do
search_results.each do |item|
diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr
index 5666460d..21451d33 100644
--- a/src/invidious/routes/api/v1/search.cr
+++ b/src/invidious/routes/api/v1/search.cr
@@ -5,34 +5,14 @@ module Invidious::Routes::API::V1::Search
env.response.content_type = "application/json"
- query = env.params.query["q"]?
- query ||= ""
-
- page = env.params.query["page"]?.try &.to_i?
- page ||= 1
-
- sort_by = env.params.query["sort_by"]?.try &.downcase
- sort_by ||= "relevance"
-
- date = env.params.query["date"]?.try &.downcase
- date ||= ""
-
- duration = env.params.query["duration"]?.try &.downcase
- duration ||= ""
-
- features = env.params.query["features"]?.try &.split(",").map(&.downcase)
- features ||= [] of String
-
- content_type = env.params.query["type"]?.try &.downcase
- content_type ||= "video"
+ query = Invidious::Search::Query.new(env.params.query, :regular, region)
begin
- search_params = produce_search_params(page, sort_by, date, content_type, duration, features)
+ search_results = query.process
rescue ex
return error_json(400, ex)
end
- search_results = search(query, search_params, region)
JSON.build do |json|
json.array do
search_results.each do |item|
diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr
index dbeb4f97..de981d81 100644
--- a/src/invidious/routes/playlists.cr
+++ b/src/invidious/routes/playlists.cr
@@ -212,7 +212,10 @@ module Invidious::Routes::Playlists
end
def self.add_playlist_items_page(env)
- locale = env.get("preferences").as(Preferences).locale
+ prefs = env.get("preferences").as(Preferences)
+ locale = prefs.locale
+
+ region = env.params.query["region"]? || prefs.region
user = env.get? "user"
sid = env.get? "sid"
@@ -236,15 +239,10 @@ module Invidious::Routes::Playlists
return env.redirect referer
end
- query = env.params.query["q"]?
- if query
- begin
- search_query, items, operators = process_search_query(query, page, user, region: nil)
- videos = items.select(SearchVideo).map(&.as(SearchVideo))
- rescue ex
- videos = [] of SearchVideo
- end
- else
+ begin
+ query = Invidious::Search::Query.new(env.params.query, :playlist, region)
+ videos = query.process.select(SearchVideo).map(&.as(SearchVideo))
+ rescue ex
videos = [] of SearchVideo
end
diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr
index 3f4c7e5e..e60d0081 100644
--- a/src/invidious/routes/search.cr
+++ b/src/invidious/routes/search.cr
@@ -37,37 +37,29 @@ module Invidious::Routes::Search
end
def self.search(env)
- locale = env.get("preferences").as(Preferences).locale
- region = env.params.query["region"]?
+ prefs = env.get("preferences").as(Preferences)
+ locale = prefs.locale
- query = env.params.query["search_query"]?
- query ||= env.params.query["q"]?
+ region = env.params.query["region"]? || prefs.region
+
+ query = Invidious::Search::Query.new(env.params.query, :regular, region)
- if !query || query.empty?
+ if query.empty?
# Display the full page search box implemented in #1977
env.set "search", ""
templated "search_homepage", navbar_search: false
else
- page = env.params.query["page"]?.try &.to_i?
- page ||= 1
-
user = env.get? "user"
begin
- search_query, videos, operators = process_search_query(query, page, user, region: region)
+ videos = query.process
rescue ex : ChannelSearchException
return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.")
rescue ex
return error_template(500, ex)
end
- operator_hash = {} of String => String
- operators.each do |operator|
- key, value = operator.downcase.split(":")
- operator_hash[key] = value
- end
-
- env.set "search", query
+ env.set "search", query.text
templated "search"
end
end
diff --git a/src/invidious/search.cr b/src/invidious/search.cr
deleted file mode 100644
index ae106bf6..00000000
--- a/src/invidious/search.cr
+++ /dev/null
@@ -1,254 +0,0 @@
-class ChannelSearchException < InfoException
- getter channel : String
-
- def initialize(@channel)
- end
-end
-
-def channel_search(query, page, channel) : Array(SearchItem)
- response = YT_POOL.client &.get("/channel/#{channel}")
-
- if response.status_code == 404
- response = YT_POOL.client &.get("/user/#{channel}")
- response = YT_POOL.client &.get("/c/#{channel}") if response.status_code == 404
- initial_data = extract_initial_data(response.body)
- ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?)
- raise ChannelSearchException.new(channel) if !ucid
- else
- ucid = channel
- end
-
- continuation = produce_channel_search_continuation(ucid, query, page)
- response_json = YoutubeAPI.browse(continuation)
-
- continuation_items = response_json["onResponseReceivedActions"]?
- .try &.[0]["appendContinuationItemsAction"]["continuationItems"]
-
- return [] of SearchItem if !continuation_items
-
- items = [] of SearchItem
- continuation_items.as_a.select(&.as_h.has_key?("itemSectionRenderer")).each do |item|
- extract_item(item["itemSectionRenderer"]["contents"].as_a[0]).try { |t| items << t }
- end
-
- return items
-end
-
-def search(query, search_params = produce_search_params(content_type: "all"), region = nil) : Array(SearchItem)
- return [] of SearchItem if query.empty?
-
- client_config = YoutubeAPI::ClientConfig.new(region: region)
- initial_data = YoutubeAPI.search(query, search_params, client_config: client_config)
-
- return extract_items(initial_data)
-end
-
-def produce_search_params(page = 1, sort : String = "relevance", date : String = "", content_type : String = "",
- duration : String = "", features : Array(String) = [] of String)
- object = {
- "1:varint" => 0_i64,
- "2:embedded" => {} of String => Int64,
- "9:varint" => ((page - 1) * 20).to_i64,
- }
-
- case sort
- when "relevance"
- object["1:varint"] = 0_i64
- when "rating"
- object["1:varint"] = 1_i64
- when "upload_date", "date"
- object["1:varint"] = 2_i64
- when "view_count", "views"
- object["1:varint"] = 3_i64
- else
- raise "No sort #{sort}"
- end
-
- case date
- when "hour"
- object["2:embedded"].as(Hash)["1:varint"] = 1_i64
- when "today"
- object["2:embedded"].as(Hash)["1:varint"] = 2_i64
- when "week"
- object["2:embedded"].as(Hash)["1:varint"] = 3_i64
- when "month"
- object["2:embedded"].as(Hash)["1:varint"] = 4_i64
- when "year"
- object["2:embedded"].as(Hash)["1:varint"] = 5_i64
- else nil # Ignore
- end
-
- case content_type
- when "video"
- object["2:embedded"].as(Hash)["2:varint"] = 1_i64
- when "channel"
- object["2:embedded"].as(Hash)["2:varint"] = 2_i64
- when "playlist"
- object["2:embedded"].as(Hash)["2:varint"] = 3_i64
- when "movie"
- object["2:embedded"].as(Hash)["2:varint"] = 4_i64
- when "show"
- object["2:embedded"].as(Hash)["2:varint"] = 5_i64
- when "all"
- #
- else
- object["2:embedded"].as(Hash)["2:varint"] = 1_i64
- end
-
- case duration
- when "short"
- object["2:embedded"].as(Hash)["3:varint"] = 1_i64
- when "long"
- object["2:embedded"].as(Hash)["3:varint"] = 2_i64
- else nil # Ignore
- end
-
- features.each do |feature|
- case feature
- when "hd"
- object["2:embedded"].as(Hash)["4:varint"] = 1_i64
- when "subtitles"
- object["2:embedded"].as(Hash)["5:varint"] = 1_i64
- when "creative_commons", "cc"
- object["2:embedded"].as(Hash)["6:varint"] = 1_i64
- when "3d"
- object["2:embedded"].as(Hash)["7:varint"] = 1_i64
- when "live", "livestream"
- object["2:embedded"].as(Hash)["8:varint"] = 1_i64
- when "purchased"
- object["2:embedded"].as(Hash)["9:varint"] = 1_i64
- when "4k"
- object["2:embedded"].as(Hash)["14:varint"] = 1_i64
- when "360"
- object["2:embedded"].as(Hash)["15:varint"] = 1_i64
- when "location"
- object["2:embedded"].as(Hash)["23:varint"] = 1_i64
- when "hdr"
- object["2:embedded"].as(Hash)["25:varint"] = 1_i64
- else nil # Ignore
- end
- end
-
- if object["2:embedded"].as(Hash).empty?
- object.delete("2:embedded")
- end
-
- params = 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 params
-end
-
-def produce_channel_search_continuation(ucid, query, page)
- if page <= 1
- idx = 0_i64
- else
- idx = 30_i64 * (page - 1)
- end
-
- object = {
- "80226972:embedded" => {
- "2:string" => ucid,
- "3:base64" => {
- "2:string" => "search",
- "6:varint" => 1_i64,
- "7:varint" => 1_i64,
- "12:varint" => 1_i64,
- "15:base64" => {
- "3:varint" => idx,
- },
- "23:varint" => 0_i64,
- },
- "11:string" => query,
- "35:string" => "browse-feed#{ucid}search",
- },
- }
-
- 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
-
-def process_search_query(query, page, user, region)
- if user
- user = user.as(Invidious::User)
- view_name = "subscriptions_#{sha256(user.email)}"
- end
-
- channel = nil
- content_type = "all"
- date = ""
- duration = ""
- features = [] of String
- sort = "relevance"
- subscriptions = nil
-
- operators = query.split(" ").select(&.match(/\w+:[\w,]+/))
- operators.each do |operator|
- key, value = operator.downcase.split(":")
-
- case key
- when "channel", "user"
- channel = operator.split(":")[-1]
- when "content_type", "type"
- content_type = value
- when "date"
- date = value
- when "duration"
- duration = value
- when "feature", "features"
- features = value.split(",")
- when "sort"
- sort = value
- when "subscriptions"
- subscriptions = value == "true"
- else
- operators.delete(operator)
- end
- end
-
- search_query = (query.split(" ") - operators).join(" ")
-
- if channel
- items = channel_search(search_query, page, channel)
- elsif subscriptions
- if view_name
- items = PG_DB.query_all("SELECT id,title,published,updated,ucid,author,length_seconds FROM (
- SELECT *,
- to_tsvector(#{view_name}.title) ||
- to_tsvector(#{view_name}.author)
- as document
- FROM #{view_name}
- ) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;", search_query, (page - 1) * 20, as: ChannelVideo)
- else
- items = [] of ChannelVideo
- end
- else
- search_params = produce_search_params(page: page, sort: sort, date: date, content_type: content_type,
- duration: duration, features: features)
-
- items = search(search_query, search_params, region)
- end
-
- # Light processing to flatten search results out of Categories.
- # They should ideally be supported in the future.
- items_without_category = [] of SearchItem | ChannelVideo
- items.each do |i|
- if i.is_a? Category
- i.contents.each do |nest_i|
- if !nest_i.is_a? Video
- items_without_category << nest_i
- end
- end
- else
- items_without_category << i
- end
- end
-
- {search_query, items_without_category, operators}
-end
diff --git a/src/invidious/search/ctoken.cr b/src/invidious/search/ctoken.cr
new file mode 100644
index 00000000..161065e0
--- /dev/null
+++ b/src/invidious/search/ctoken.cr
@@ -0,0 +1,32 @@
+def produce_channel_search_continuation(ucid, query, page)
+ if page <= 1
+ idx = 0_i64
+ else
+ idx = 30_i64 * (page - 1)
+ end
+
+ object = {
+ "80226972:embedded" => {
+ "2:string" => ucid,
+ "3:base64" => {
+ "2:string" => "search",
+ "6:varint" => 1_i64,
+ "7:varint" => 1_i64,
+ "12:varint" => 1_i64,
+ "15:base64" => {
+ "3:varint" => idx,
+ },
+ "23:varint" => 0_i64,
+ },
+ "11:string" => query,
+ "35:string" => "browse-feed#{ucid}search",
+ },
+ }
+
+ 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
diff --git a/src/invidious/search/filters.cr b/src/invidious/search/filters.cr
new file mode 100644
index 00000000..c2b5c758
--- /dev/null
+++ b/src/invidious/search/filters.cr
@@ -0,0 +1,376 @@
+require "protodec/utils"
+require "http/params"
+
+module Invidious::Search
+ struct Filters
+ # Values correspond to { "2:embedded": { "1:varint": <X> }}
+ # except for "None" which is only used by us (= nothing selected)
+ enum Date
+ None = 0
+ Hour = 1
+ Today = 2
+ Week = 3
+ Month = 4
+ Year = 5
+ end
+
+ # Values correspond to { "2:embedded": { "2:varint": <X> }}
+ # except for "All" which is only used by us (= nothing selected)
+ enum Type
+ All = 0
+ Video = 1
+ Channel = 2
+ Playlist = 3
+ Movie = 4
+
+ # Has it been removed?
+ # (Not available on youtube's UI)
+ Show = 5
+ end
+
+ # Values correspond to { "2:embedded": { "3:varint": <X> }}
+ # except for "None" which is only used by us (= nothing selected)
+ enum Duration
+ None = 0
+ Short = 1 # "Under 4 minutes"
+ Long = 2 # "Over 20 minutes"
+ Medium = 3 # "4 - 20 minutes"
+ end
+
+ # Note: flag enums automatically generate
+ # "none" and "all" members
+ @[Flags]
+ enum Features
+ Live
+ FourK # "4K"
+ HD
+ Subtitles # "Subtitles/CC"
+ CCommons # "Creative Commons"
+ ThreeSixty # "360°"
+ VR180
+ ThreeD # "3D"
+ HDR
+ Location
+ Purchased
+ end
+
+ # Values correspond to { "1:varint": <X> }
+ enum Sort
+ Relevance = 0
+ Rating = 1
+ Date = 2
+ Views = 3
+ end
+
+ # Parameters are sorted as on Youtube
+ property date : Date
+ property type : Type
+ property duration : Duration
+ property features : Features
+ property sort : Sort
+
+ def initialize(
+ *, # All parameters must be named
+ @date : Date = Date::None,
+ @type : Type = Type::All,
+ @duration : Duration = Duration::None,
+ @features : Features = Features::None,
+ @sort : Sort = Sort::Relevance
+ )
+ end
+
+ def default? : Bool
+ return @date.none? && @type.all? && @duration.none? && \
+ @features.none? && @sort.relevance?
+ end
+
+ # -------------------
+ # Invidious params
+ # -------------------
+
+ def self.parse_features(raw : Array(String)) : Features
+ # Initialize return variable
+ features = Features.new(0)
+
+ raw.each do |ft|
+ case ft.downcase
+ when "live", "livestream"
+ features = features | Features::Live
+ when "4k" then features = features | Features::FourK
+ when "hd" then features = features | Features::HD
+ when "subtitles" then features = features | Features::Subtitles
+ when "creative_commons", "commons", "cc"
+ features = features | Features::CCommons
+ when "360" then features = features | Features::ThreeSixty
+ when "vr180" then features = features | Features::VR180
+ when "3d" then features = features | Features::ThreeD
+ when "hdr" then features = features | Features::HDR
+ when "location" then features = features | Features::Location
+ when "purchased" then features = features | Features::Purchased
+ end
+ end
+
+ return features
+ end
+
+ def self.format_features(features : Features) : String
+ # Directly return an empty string if there are no features
+ return "" if features.none?
+
+ # Initialize return variable
+ str = [] of String
+
+ str << "live" if features.live?
+ str << "4k" if features.four_k?
+ str << "hd" if features.hd?
+ str << "subtitles" if features.subtitles?
+ str << "commons" if features.c_commons?
+ str << "360" if features.three_sixty?
+ str << "vr180" if features.vr180?
+ str << "3d" if features.three_d?
+ str << "hdr" if features.hdr?
+ str << "location" if features.location?
+ str << "purchased" if features.purchased?
+
+ return str.join(',')
+ end
+
+ def self.from_legacy_filters(str : String) : {Filters, String, String, Bool}
+ # Split search query on spaces
+ members = str.split(' ')
+
+ # Output variables
+ channel = ""
+ filters = Filters.new
+ subscriptions = false
+
+ # Array to hold the non-filter members
+ query = [] of String
+
+ # Parse!
+ members.each do |substr|
+ # Separator operators
+ operators = substr.split(':')
+
+ case operators[0]
+ when "user", "channel"
+ next if operators.size != 2
+ channel = operators[1]
+ #
+ when "type", "content_type"
+ next if operators.size != 2
+ type = Type.parse?(operators[1])
+ filters.type = type if !type.nil?
+ #
+ when "date"
+ next if operators.size != 2
+ date = Date.parse?(operators[1])
+ filters.date = date if !date.nil?
+ #
+ when "duration"
+ next if operators.size != 2
+ duration = Duration.parse?(operators[1])
+ filters.duration = duration if !duration.nil?
+ #
+ when "feature", "features"
+ next if operators.size != 2
+ features = parse_features(operators[1].split(','))
+ filters.features = features if !features.nil?
+ #
+ when "sort"
+ next if operators.size != 2
+ sort = Sort.parse?(operators[1])
+ filters.sort = sort if !sort.nil?
+ #
+ when "subscriptions"
+ next if operators.size != 2
+ subscriptions = {"true", "on", "yes", "1"}.any?(&.== operators[1])
+ #
+ else
+ query << substr
+ end
+ end
+
+ # Re-assemble query (without filters)
+ cleaned_query = query.join(' ')
+
+ return {filters, channel, cleaned_query, subscriptions}
+ end
+
+ def self.from_iv_params(params : HTTP::Params) : Filters
+ # Temporary variables
+ filters = Filters.new
+
+ if type = params["type"]?
+ filters.type = Type.parse?(type) || Type::All
+ params.delete("type")
+ end
+
+ if date = params["date"]?
+ filters.date = Date.parse?(date) || Date::None
+ params.delete("date")
+ end
+
+ if duration = params["duration"]?
+ filters.duration = Duration.parse?(duration) || Duration::None
+ params.delete("duration")
+ end
+
+ features = params.fetch_all("features")
+ if !features.empty?
+ # Un-array input so it can be treated as a comma-separated list
+ features = features[0].split(',') if features.size == 1
+
+ filters.features = parse_features(features) || Features::None
+ params.delete_all("features")
+ end
+
+ if sort = params["sort"]?
+ filters.sort = Sort.parse?(sort) || Sort::Relevance
+ params.delete("sort")
+ end
+
+ return filters
+ end
+
+ def to_iv_params : HTTP::Params
+ # Temporary variables
+ raw_params = {} of String => Array(String)
+
+ raw_params["date"] = [@date.to_s.underscore] if !@date.none?
+ raw_params["type"] = [@type.to_s.underscore] if !@type.all?
+ raw_params["sort"] = [@sort.to_s.underscore] if !@sort.relevance?
+
+ if !@duration.none?
+ raw_params["duration"] = [@duration.to_s.underscore]
+ end
+
+ if !@features.none?
+ raw_params["features"] = [Filters.format_features(@features)]
+ end
+
+ return HTTP::Params.new(raw_params)
+ end
+
+ # -------------------
+ # Youtube params
+ # -------------------
+
+ # Produce the youtube search parameters for the
+ # innertube API (base64-encoded protobuf object).
+ def to_yt_params(page : Int = 1) : String
+ # Initialize the embedded protobuf object
+ embedded = {} of String => Int64
+
+ # Add these field only if associated parameter is selected
+ embedded["1:varint"] = @date.to_i64 if !@date.none?
+ embedded["2:varint"] = @type.to_i64 if !@type.all?
+ embedded["3:varint"] = @duration.to_i64 if !@duration.none?
+
+ if !@features.none?
+ # All features have a value of "1" when enabled, and
+ # the field is omitted when the feature is no selected.
+ embedded["4:varint"] = 1_i64 if @features.includes?(Features::HD)
+ embedded["5:varint"] = 1_i64 if @features.includes?(Features::Subtitles)
+ embedded["6:varint"] = 1_i64 if @features.includes?(Features::CCommons)
+ embedded["7:varint"] = 1_i64 if @features.includes?(Features::ThreeD)
+ embedded["8:varint"] = 1_i64 if @features.includes?(Features::Live)
+ embedded["9:varint"] = 1_i64 if @features.includes?(Features::Purchased)
+ embedded["14:varint"] = 1_i64 if @features.includes?(Features::FourK)
+ embedded["15:varint"] = 1_i64 if @features.includes?(Features::ThreeSixty)
+ embedded["23:varint"] = 1_i64 if @features.includes?(Features::Location)
+ embedded["25:varint"] = 1_i64 if @features.includes?(Features::HDR)
+ embedded["26:varint"] = 1_i64 if @features.includes?(Features::VR180)
+ end
+
+ # Initialize an empty protobuf object
+ object = {} of String => (Int64 | String | Hash(String, Int64))
+
+ # As usual, everything can be omitted if it has no value
+ object["2:embedded"] = embedded if !embedded.empty?
+
+ # Default sort is "relevance", so when this option is selected,
+ # the associated field can be omitted.
+ if !@sort.relevance?
+ object["1:varint"] = @sort.to_i64
+ end
+
+ # Add page number (if provided)
+ if page > 1
+ object["9:varint"] = ((page - 1) * 20).to_i64
+ end
+
+ # If the object is empty, return an empty string,
+ # otherwise encode to protobuf then to base64
+ return "" if object.empty?
+
+ return 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) }
+ end
+
+ # Function to parse the `sp` URL parameter from Youtube
+ # search page. It's a base64-encoded protobuf object.
+ def self.from_yt_params(params : HTTP::Params) : Filters
+ # Initialize output variable
+ filters = Filters.new
+
+ # Get parameter, and check emptyness
+ search_params = params["sp"]?
+
+ if search_params.nil? || search_params.empty?
+ return filters
+ end
+
+ # Decode protobuf object
+ object = search_params
+ .try { |i| URI.decode_www_form(i) }
+ .try { |i| Base64.decode(i) }
+ .try { |i| IO::Memory.new(i) }
+ .try { |i| Protodec::Any.parse(i) }
+
+ # Parse items from embedded object
+ if embedded = object["2:0:embedded"]?
+ # All the following fields (date, type, duration) are optional.
+ if date = embedded["1:0:varint"]?
+ filters.date = Date.from_value?(date.as_i) || Date::None
+ end
+
+ if type = embedded["2:0:varint"]?
+ filters.type = Type.from_value?(type.as_i) || Type::All
+ end
+
+ if duration = embedded["3:0:varint"]?
+ filters.duration = Duration.from_value?(duration.as_i) || Duration::None
+ end
+
+ # All features should have a value of "1" when enabled, and
+ # the field should be omitted when the feature is no selected.
+ features = 0
+ features += (embedded["4:0:varint"]?.try &.as_i == 1_i64) ? Features::HD.value : 0
+ features += (embedded["5:0:varint"]?.try &.as_i == 1_i64) ? Features::Subtitles.value : 0
+ features += (embedded["6:0:varint"]?.try &.as_i == 1_i64) ? Features::CCommons.value : 0
+ features += (embedded["7:0:varint"]?.try &.as_i == 1_i64) ? Features::ThreeD.value : 0
+ features += (embedded["8:0:varint"]?.try &.as_i == 1_i64) ? Features::Live.value : 0
+ features += (embedded["9:0:varint"]?.try &.as_i == 1_i64) ? Features::Purchased.value : 0
+ features += (embedded["14:0:varint"]?.try &.as_i == 1_i64) ? Features::FourK.value : 0
+ features += (embedded["15:0:varint"]?.try &.as_i == 1_i64) ? Features::ThreeSixty.value : 0
+ features += (embedded["23:0:varint"]?.try &.as_i == 1_i64) ? Features::Location.value : 0
+ features += (embedded["25:0:varint"]?.try &.as_i == 1_i64) ? Features::HDR.value : 0
+ features += (embedded["26:0:varint"]?.try &.as_i == 1_i64) ? Features::VR180.value : 0
+
+ filters.features = Features.from_value?(features) || Features::None
+ end
+
+ if sort = object["1:0:varint"]?
+ filters.sort = Sort.from_value?(sort.as_i) || Sort::Relevance
+ end
+
+ # Remove URL parameter and return result
+ params.delete("sp")
+ return filters
+ end
+ end
+end
diff --git a/src/invidious/search/processors.cr b/src/invidious/search/processors.cr
new file mode 100644
index 00000000..d1409c06
--- /dev/null
+++ b/src/invidious/search/processors.cr
@@ -0,0 +1,64 @@
+module Invidious::Search
+ module Processors
+ extend self
+
+ # Regular search (`/search` endpoint)
+ def regular(query : Query) : Array(SearchItem)
+ search_params = query.filters.to_yt_params(page: query.page)
+
+ client_config = YoutubeAPI::ClientConfig.new(region: query.region)
+ initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config)
+
+ return extract_items(initial_data)
+ end
+
+ # Search a youtube channel
+ # TODO: clean code, and rely more on YoutubeAPI
+ def channel(query : Query) : Array(SearchItem)
+ response = YT_POOL.client &.get("/channel/#{query.channel}")
+
+ if response.status_code == 404
+ response = YT_POOL.client &.get("/user/#{query.channel}")
+ response = YT_POOL.client &.get("/c/#{query.channel}") if response.status_code == 404
+ initial_data = extract_initial_data(response.body)
+ ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?)
+ raise ChannelSearchException.new(query.channel) if !ucid
+ else
+ ucid = query.channel
+ end
+
+ continuation = produce_channel_search_continuation(ucid, query.text, query.page)
+ response_json = YoutubeAPI.browse(continuation)
+
+ continuation_items = response_json["onResponseReceivedActions"]?
+ .try &.[0]["appendContinuationItemsAction"]["continuationItems"]
+
+ return [] of SearchItem if !continuation_items
+
+ items = [] of SearchItem
+ continuation_items.as_a.select(&.as_h.has_key?("itemSectionRenderer")).each do |item|
+ extract_item(item["itemSectionRenderer"]["contents"].as_a[0]).try { |t| items << t }
+ end
+
+ return items
+ end
+
+ # Search inside of user subscriptions
+ def subscriptions(query : Query, user : Invidious::User) : Array(ChannelVideo)
+ view_name = "subscriptions_#{sha256(user.email)}"
+
+ return PG_DB.query_all("
+ SELECT id,title,published,updated,ucid,author,length_seconds
+ FROM (
+ SELECT *,
+ to_tsvector(#{view_name}.title) ||
+ to_tsvector(#{view_name}.author)
+ as document
+ FROM #{view_name}
+ ) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;",
+ query.text, (query.page - 1) * 20,
+ as: ChannelVideo
+ )
+ end
+ end
+end
diff --git a/src/invidious/search/query.cr b/src/invidious/search/query.cr
new file mode 100644
index 00000000..34b36b1d
--- /dev/null
+++ b/src/invidious/search/query.cr
@@ -0,0 +1,151 @@
+module Invidious::Search
+ class Query
+ enum Type
+ # Types related to YouTube
+ Regular # Youtube search page
+ Channel # Youtube channel search box
+
+ # Types specific to Invidious
+ Subscriptions # Search user subscriptions
+ Playlist # "Add playlist item" search
+ end
+
+ getter type : Type = Type::Regular
+
+ @raw_query : String
+ @query : String = ""
+
+ property filters : Filters = Filters.new
+ property page : Int32
+ property region : String?
+ property channel : String = ""
+
+ # Return true if @raw_query is either `nil` or empty
+ private def empty_raw_query?
+ return @raw_query.empty?
+ end
+
+ # Same as `empty_raw_query?`, but named for external use
+ def empty?
+ return self.empty_raw_query?
+ end
+
+ # Getter for the query string.
+ # It is named `text` to reduce confusion (`search_query.text` makes more
+ # sense than `search_query.query`)
+ def text
+ return @query
+ end
+
+ # Initialize a new search query.
+ # Parameters are used to get the query string, the page number
+ # and the search filters (if any). Type tells this function
+ # where it is being called from (See `Type` above).
+ def initialize(
+ params : HTTP::Params,
+ @type : Type = Type::Regular,
+ @region : String? = nil
+ )
+ # Get the raw search query string (common to all search types). In
+ # Regular search mode, also look for the `search_query` URL parameter
+ if @type.regular?
+ @raw_query = params["q"]? || params["search_query"]? || ""
+ else
+ @raw_query = params["q"]? || ""
+ end
+
+ # Get the page number (also common to all search types)
+ @page = params["page"]?.try &.to_i? || 1
+
+ # Stop here is raw query in empty
+ # NOTE: maybe raise in the future?
+ return if self.empty_raw_query?
+
+ # Specific handling
+ case @type
+ when .channel?
+ # In "channel search" mode, filters are ignored, but we still parse
+ # the query prevent transmission of legacy filters to youtube.
+ #
+ _, _, @query, _ = Filters.from_legacy_filters(@raw_query)
+ #
+ when .playlist?
+ # In "add playlist item" mode, filters are parsed from the query
+ # string itself (legacy), and the channel is ignored.
+ #
+ @filters, _, @query, _ = Filters.from_legacy_filters(@raw_query)
+ #
+ when .subscriptions?, .regular?
+ if params["sp"]?
+ # Parse the `sp` URL parameter (youtube compatibility)
+ @filters = Filters.from_yt_params(params)
+ @query = @raw_query || ""
+ else
+ # Parse invidious URL parameters (sort, date, etc...)
+ @filters = Filters.from_iv_params(params)
+ @channel = params["channel"]? || ""
+
+ if @filters.default? && @raw_query.includes?(':')
+ # Parse legacy filters from query
+ @filters, @channel, @query, subs = Filters.from_legacy_filters(@raw_query)
+ else
+ @query = @raw_query || ""
+ end
+
+ if !@channel.empty?
+ # Switch to channel search mode (filters will be ignored)
+ @type = Type::Channel
+ elsif subs
+ # Switch to subscriptions search mode
+ @type = Type::Subscriptions
+ end
+ end
+ end
+ end
+
+ # Run the search query using the corresponding search processor.
+ # Returns either the results or an empty array of `SearchItem`.
+ def process(user : Invidious::User? = nil) : Array(SearchItem) | Array(ChannelVideo)
+ items = [] of SearchItem
+
+ # Don't bother going further if search query is empty
+ return items if self.empty_raw_query?
+
+ case @type
+ when .regular?, .playlist?
+ items = unnest_items(Processors.regular(self))
+ #
+ when .channel?
+ items = Processors.channel(self)
+ #
+ when .subscriptions?
+ if user
+ items = Processors.subscriptions(self, user.as(Invidious::User))
+ end
+ end
+
+ return items
+ end
+
+ # TODO: clean code
+ private def unnest_items(all_items) : Array(SearchItem)
+ items = [] of SearchItem
+
+ # Light processing to flatten search results out of Categories.
+ # They should ideally be supported in the future.
+ all_items.each do |i|
+ if i.is_a? Category
+ i.contents.each do |nest_i|
+ if !nest_i.is_a? Video
+ items << nest_i
+ end
+ end
+ else
+ items << i
+ end
+ end
+
+ return items
+ end
+ end
+end
diff --git a/src/invidious/user/cookies.cr b/src/invidious/user/cookies.cr
index 99df1b07..65e079ec 100644
--- a/src/invidious/user/cookies.cr
+++ b/src/invidious/user/cookies.cr
@@ -17,7 +17,8 @@ struct Invidious::User
value: sid,
expires: Time.utc + 2.years,
secure: SECURE,
- http_only: true
+ http_only: true,
+ samesite: HTTP::Cookie::SameSite::Strict
)
end
@@ -30,7 +31,8 @@ struct Invidious::User
value: URI.encode_www_form(preferences.to_json),
expires: Time.utc + 2.years,
secure: SECURE,
- http_only: true
+ http_only: false,
+ samesite: HTTP::Cookie::SameSite::Strict
)
end
end
diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr
index 66952c93..4cb049ca 100644
--- a/src/invidious/videos.cr
+++ b/src/invidious/videos.cr
@@ -374,18 +374,25 @@ struct Video
json.array do
self.adaptive_fmts.each do |fmt|
json.object do
- json.field "index", "#{fmt["indexRange"]["start"]}-#{fmt["indexRange"]["end"]}"
- json.field "bitrate", fmt["bitrate"].as_i.to_s
- json.field "init", "#{fmt["initRange"]["start"]}-#{fmt["initRange"]["end"]}"
+ # Only available on regular videos, not livestreams/OTF streams
+ if init_range = fmt["initRange"]?
+ json.field "init", "#{init_range["start"]}-#{init_range["end"]}"
+ end
+ if index_range = fmt["indexRange"]?
+ json.field "index", "#{index_range["start"]}-#{index_range["end"]}"
+ end
+
+ # Not available on MPEG-4 Timed Text (`text/mp4`) streams (livestreams only)
+ json.field "bitrate", fmt["bitrate"].as_i.to_s if fmt["bitrate"]?
+
json.field "url", fmt["url"]
json.field "itag", fmt["itag"].as_i.to_s
json.field "type", fmt["mimeType"]
- json.field "clen", fmt["contentLength"]
+ json.field "clen", fmt["contentLength"]? || "-1"
json.field "lmt", fmt["lastModified"]
json.field "projectionType", fmt["projectionType"]
- fmt_info = itag_to_metadata?(fmt["itag"])
- if fmt_info
+ if fmt_info = itag_to_metadata?(fmt["itag"])
fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.as_i || 30
json.field "fps", fps
json.field "container", fmt_info["ext"]
@@ -405,6 +412,15 @@ struct Video
end
end
end
+
+ # Audio-related data
+ json.field "audioQuality", fmt["audioQuality"] if fmt.has_key?("audioQuality")
+ json.field "audioSampleRate", fmt["audioSampleRate"].as_s.to_i if fmt.has_key?("audioSampleRate")
+ json.field "audioChannels", fmt["audioChannels"] if fmt.has_key?("audioChannels")
+
+ # Extra misc stuff
+ json.field "colorInfo", fmt["colorInfo"] if fmt.has_key?("colorInfo")
+ json.field "captionTrack", fmt["captionTrack"] if fmt.has_key?("captionTrack")
end
end
end
@@ -585,7 +601,7 @@ struct Video
def allowed_regions
info
- .dig("microformat", "playerMicroformatRenderer", "availableCountries")
+ .dig?("microformat", "playerMicroformatRenderer", "availableCountries")
.try &.as_a.map &.as_s || [] of String
end
@@ -616,6 +632,7 @@ struct Video
fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}")
fmt["url"] = JSON::Any.new("#{fmt["url"]}&region=#{self.info["region"]}") if self.info["region"]?
end
+
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
@fmt_stream = fmt_stream
return @fmt_stream.as(Array(Hash(String, JSON::Any)))
@@ -635,9 +652,7 @@ struct Video
fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}")
fmt["url"] = JSON::Any.new("#{fmt["url"]}&region=#{self.info["region"]}") if self.info["region"]?
end
- # See https://github.com/TeamNewPipe/NewPipe/issues/2415
- # Some streams are segmented by URL `sq/` rather than index, for now we just filter them out
- fmt_stream.reject! { |f| !f["indexRange"]? }
+
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
@adaptive_fmts = fmt_stream
return @adaptive_fmts.as(Array(Hash(String, JSON::Any)))
@@ -887,7 +902,7 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
client_config = YoutubeAPI::ClientConfig.new(proxy_region: proxy_region)
if context_screen == "embed"
- client_config.client_type = YoutubeAPI::ClientType::WebScreenEmbed
+ client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
end
player_response = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config)
@@ -1108,6 +1123,10 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
end
return video
+rescue DB::Error
+ # Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
+ # Note: All DB errors inherit from `DB::Error`
+ return fetch_video(id, region)
end
def fetch_video(id, region)
diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr
index ad50909a..22870317 100644
--- a/src/invidious/views/add_playlist_items.ecr
+++ b/src/invidious/views/add_playlist_items.ecr
@@ -11,7 +11,9 @@
<legend><a href="/playlist?list=<%= playlist.id %>"><%= translate(locale, "Editing playlist `x`", %|"#{HTML.escape(playlist.title)}"|) %></a></legend>
<fieldset>
- <input class="pure-input-1" type="search" name="q" <% if query %>value="<%= HTML.escape(query) %>"<% else %>placeholder="<%= translate(locale, "Search for videos") %>"<% end %>>
+ <input class="pure-input-1" type="search" name="q"
+ <% if query %>value="<%= HTML.escape(query.text) %>"<% end %>
+ placeholder="<%= translate(locale, "Search for videos") %>">
<input type="hidden" name="list" value="<%= plid %>">
</fieldset>
</form>
@@ -38,10 +40,11 @@
</div>
<% if query %>
+ <%- query_encoded = URI.encode_www_form(query.text, space_to_plus: true) -%>
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-lg-1-5">
- <% if page > 1 %>
- <a href="/add_playlist_items?list=<%= plid %>&q=<%= URI.encode_www_form(query.not_nil!) %>&page=<%= page - 1 %>">
+ <% if query.page > 1 %>
+ <a href="/add_playlist_items?list=<%= plid %>&q=<%= query_encoded %>&page=<%= page - 1 %>">
<%= translate(locale, "Previous page") %>
</a>
<% end %>
@@ -49,7 +52,7 @@
<div class="pure-u-1 pure-u-lg-3-5"></div>
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
<% if videos.size >= 20 %>
- <a href="/add_playlist_items?list=<%= plid %>&q=<%= URI.encode_www_form(query.not_nil!) %>&page=<%= page + 1 %>">
+ <a href="/add_playlist_items?list=<%= plid %>&q=<%= query_encoded %>&page=<%= page + 1 %>">
<%= translate(locale, "Next page") %>
</a>
<% end %>
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index cc4ded74..fb7ad1dc 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -52,11 +52,11 @@
<% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail">
<img loading="lazy" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
- <% if plid = env.get?("remove_playlist_items") %>
- <form data-onsubmit="return_false" action="/playlist_ajax?action_remove_video=1&set_video_id=<%= item.index %>&playlist_id=<%= plid %>&referer=<%= env.get("current_page") %>" method="post">
+ <% if plid_form = env.get?("remove_playlist_items") %>
+ <form data-onsubmit="return_false" action="/playlist_ajax?action_remove_video=1&set_video_id=<%= item.index %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<p class="watched">
- <a data-onclick="remove_playlist_item" data-index="<%= item.index %>" data-plid="<%= plid %>" href="javascript:void(0)">
+ <a data-onclick="remove_playlist_item" data-index="<%= item.index %>" data-plid="<%= plid_form %>" href="javascript:void(0)">
<button type="submit" style="all:unset">
<i class="icon ion-md-trash"></i>
</button>
@@ -117,11 +117,11 @@
</a>
</p>
</form>
- <% elsif plid = env.get? "add_playlist_items" %>
- <form data-onsubmit="return_false" action="/playlist_ajax?action_add_video=1&video_id=<%= item.id %>&playlist_id=<%= plid %>&referer=<%= env.get("current_page") %>" method="post">
+ <% elsif plid_form = env.get? "add_playlist_items" %>
+ <form data-onsubmit="return_false" action="/playlist_ajax?action_add_video=1&video_id=<%= item.id %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<p class="watched">
- <a data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid %>" href="javascript:void(0)">
+ <a data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>" href="javascript:void(0)">
<button type="submit" style="all:unset">
<i class="icon ion-md-add"></i>
</button>
diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr
index 206ba380..fffefc9a 100644
--- a/src/invidious/views/components/player.ecr
+++ b/src/invidious/views/components/player.ecr
@@ -7,8 +7,19 @@
<source src="<%= URI.parse(hlsvp).request_target %><% if params.local %>?local=true<% end %>" type="application/x-mpegURL" label="livestream">
<% else %>
<% if params.listen %>
- <% audio_streams.each_with_index do |fmt, i| %>
- <source src="/latest_version?id=<%= video.id %>&itag=<%= fmt["itag"] %><% if params.local %>&local=true<% end %>" type='<%= fmt["mimeType"] %>' label="<%= fmt["bitrate"] %>k" selected="<%= i == 0 ? true : false %>">
+ <% audio_streams.each_with_index do |fmt, i|
+ src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
+ src_url += "&local=true" if params.local
+
+ bitrate = fmt["bitrate"]
+ mimetype = HTML.escape(fmt["mimeType"].as_s)
+
+ selected = i == 0 ? true : false
+ %>
+ <source src="<%= src_url %>" type='<%= mimetype %>' label="<%= bitrate %>k" selected="<%= selected %>">
+ <% if !params.local && !CONFIG.disabled?("local") %>
+ <source src="<%= src_url %>&local=true" type='<%= mimetype %>' hidequalityoption="true">
+ <% end %>
<% end %>
<% else %>
<% if params.quality == "dash" %>
@@ -28,6 +39,9 @@
selected = params.quality ? (params.quality == quality) : (i == 0)
%>
<source src="<%= src_url %>" type="<%= mimetype %>" label="<%= quality %>" selected="<%= selected %>">
+ <% if !params.local && !CONFIG.disabled?("local") %>
+ <source src="<%= src_url %>&local=true" type="<%= mimetype %>" hidequalityoption="true">
+ <% end %>
<% end %>
<% end %>
diff --git a/src/invidious/views/embed.ecr b/src/invidious/views/embed.ecr
index 27a8e266..ce5ff7f0 100644
--- a/src/invidious/views/embed.ecr
+++ b/src/invidious/views/embed.ecr
@@ -24,7 +24,8 @@
"video_series" => video_series,
"params" => params,
"preferences" => preferences,
- "premiere_timestamp" => video.premiere_timestamp.try &.to_unix
+ "premiere_timestamp" => video.premiere_timestamp.try &.to_unix,
+ "local_disabled" => CONFIG.disabled?("local")
}.to_pretty_json
%>
</script>
diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr
index 45bbdefc..7110703e 100644
--- a/src/invidious/views/search.ecr
+++ b/src/invidious/views/search.ecr
@@ -1,147 +1,62 @@
<% content_for "header" do %>
-<title><%= search_query.not_nil!.size > 30 ? HTML.escape(query.not_nil![0,30].rstrip(".") + "...") : HTML.escape(query.not_nil!) %> - Invidious</title>
+<title><%= query.text.size > 30 ? HTML.escape(query.text[0,30].rstrip(".")) + "&hellip;" : HTML.escape(query.text) %> - Invidious</title>
+<link rel="stylesheet" href="/css/search.css?v=<%= ASSET_COMMIT %>">
<% end %>
-<% search_query_encoded = env.get?("search").try { |x| URI.encode_www_form(x.as(String), space_to_plus: true) } %>
+<%-
+ search_query_encoded = URI.encode_www_form(query.text, space_to_plus: true)
+ filter_params = query.filters.to_iv_params
-<!-- Search redirection and filtering UI -->
-<% if videos.size == 0 %>
- <h3 style="text-align: center">
- <a href="/redirect?referer=<%= env.get?("current_page") %>"><%= translate(locale, "Broken? Try another Invidious Instance!") %></a>
- </h3>
-<% else %>
- <details id="filters">
- <summary>
- <h3 style="display:inline"> <%= translate(locale, "filter") %> </h3>
- </summary>
- <div id="filters" class="pure-g h-box">
- <div class="pure-u-1-3 pure-u-md-1-5">
- <b><%= translate(locale, "date") %></b>
- <hr/>
- <% ["hour", "today", "week", "month", "year"].each do |date| %>
- <div class="pure-u-1 pure-md-1-5">
- <% if operator_hash.fetch("date", "all") == date %>
- <b><%= translate(locale, date) %></b>
- <% else %>
- <a href="/search?q=<%= URI.encode_www_form(query.not_nil!.gsub(/ ?date:[a-z]+/, "") + " date:" + date) %>&page=<%= page %>">
- <%= translate(locale, date) %>
- </a>
- <% end %>
- </div>
- <% end %>
- </div>
- <div class="pure-u-1-3 pure-u-md-1-5">
- <b><%= translate(locale, "content_type") %></b>
- <hr/>
- <% ["video", "channel", "playlist", "movie", "show"].each do |content_type| %>
- <div class="pure-u-1 pure-md-1-5">
- <% if operator_hash.fetch("content_type", "all") == content_type %>
- <b><%= translate(locale, content_type) %></b>
- <% else %>
- <a href="/search?q=<%= URI.encode_www_form(query.not_nil!.gsub(/ ?content_type:[a-z]+/, "") + " content_type:" + content_type) %>&page=<%= page %>">
- <%= translate(locale, content_type) %>
- </a>
- <% end %>
- </div>
- <% end %>
- </div>
- <div class="pure-u-1-3 pure-u-md-1-5">
- <b><%= translate(locale, "duration") %></b>
- <hr/>
- <% ["short", "long"].each do |duration| %>
- <div class="pure-u-1 pure-md-1-5">
- <% if operator_hash.fetch("duration", "all") == duration %>
- <b><%= translate(locale, duration) %></b>
- <% else %>
- <a href="/search?q=<%= URI.encode_www_form(query.not_nil!.gsub(/ ?duration:[a-z]+/, "") + " duration:" + duration) %>&page=<%= page %>">
- <%= translate(locale, duration) %>
- </a>
- <% end %>
- </div>
- <% end %>
- </div>
- <div class="pure-u-1-3 pure-u-md-1-5">
- <b><%= translate(locale, "features") %></b>
- <hr/>
- <% ["hd", "subtitles", "creative_commons", "3d", "live", "purchased", "4k", "360", "location", "hdr"].each do |feature| %>
- <div class="pure-u-1 pure-md-1-5">
- <% if operator_hash.fetch("features", "all").includes?(feature) %>
- <b><%= translate(locale, feature) %></b>
- <% elsif operator_hash.has_key?("features") %>
- <a href="/search?q=<%= URI.encode_www_form(query.not_nil!.gsub(/features:/, "features:" + feature + ",")) %>&page=<%= page %>">
- <%= translate(locale, feature) %>
- </a>
- <% else %>
- <a href="/search?q=<%= URI.encode_www_form(query.not_nil! + " features:" + feature) %>&page=<%= page %>">
- <%= translate(locale, feature) %>
- </a>
- <% end %>
- </div>
- <% end %>
- </div>
- <div class="pure-u-1-3 pure-u-md-1-5">
- <b><%= translate(locale, "sort") %></b>
- <hr/>
- <% ["relevance", "rating", "date", "views"].each do |sort| %>
- <div class="pure-u-1 pure-md-1-5">
- <% if operator_hash.fetch("sort", "relevance") == sort %>
- <b><%= translate(locale, sort) %></b>
- <% else %>
- <a href="/search?q=<%= URI.encode_www_form(query.not_nil!.gsub(/ ?sort:[a-z]+/, "") + " sort:" + sort) %>&page=<%= page %>">
- <%= translate(locale, sort) %>
- </a>
- <% end %>
- </div>
- <% end %>
- </div>
- </div>
- </details>
-<% end %>
+ url_prev_page = "/search?q=#{search_query_encoded}&#{filter_params}&page=#{query.page - 1}"
+ url_next_page = "/search?q=#{search_query_encoded}&#{filter_params}&page=#{query.page + 1}"
-<% if videos.size == 0 %>
- <hr style="margin: 0;"/>
-<% else %>
- <hr/>
-<% end %>
+ redirect_url = Invidious::Frontend::Misc.redirect_url(env)
+-%>
+
+<!-- Search redirection and filtering UI -->
+<%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %>
+<hr/>
<div class="pure-g h-box v-box">
<div class="pure-u-1 pure-u-lg-1-5">
- <% if page > 1 %>
- <a href="/search?q=<%= search_query_encoded %>&page=<%= page - 1 %>">
- <%= translate(locale, "Previous page") %>
- </a>
- <% end %>
+ <%- if query.page > 1 -%>
+ <a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a>
+ <%- end -%>
</div>
<div class="pure-u-1 pure-u-lg-3-5"></div>
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
- <% if videos.size >= 20 %>
- <a href="/search?q=<%= search_query_encoded %>&page=<%= page + 1 %>">
- <%= translate(locale, "Next page") %>
- </a>
- <% end %>
+ <%- if videos.size >= 20 -%>
+ <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a>
+ <%- end -%>
</div>
</div>
+<%- if videos.empty? -%>
+<div class="h-box no-results-error">
+ <div>
+ <%= translate(locale, "search_message_no_results") %><br/><br/>
+ <%= translate(locale, "search_message_change_filters_or_query") %><br/><br/>
+ <%= translate(locale, "search_message_use_another_instance", redirect_url) %>
+ </div>
+</div>
+<%- else -%>
<div class="pure-g">
- <% videos.each do |item| %>
+ <%- videos.each do |item| -%>
<%= rendered "components/item" %>
- <% end %>
+ <%- end -%>
</div>
+<%- end -%>
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-lg-1-5">
- <% if page > 1 %>
- <a href="/search?q=<%= search_query_encoded %>&page=<%= page - 1 %>">
- <%= translate(locale, "Previous page") %>
- </a>
- <% end %>
+ <%- if query.page > 1 -%>
+ <a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a>
+ <%- end -%>
</div>
<div class="pure-u-1 pure-u-lg-3-5"></div>
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
- <% if videos.size >= 20 %>
- <a href="/search?q=<%= search_query_encoded %>&page=<%= page + 1 %>">
- <%= translate(locale, "Next page") %>
- </a>
- <% end %>
+ <%- if videos.size >= 20 -%>
+ <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a>
+ <%- end -%>
</div>
</div>
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr
index 74a5e69f..8b6eb903 100644
--- a/src/invidious/views/watch.ecr
+++ b/src/invidious/views/watch.ecr
@@ -64,7 +64,8 @@ we're going to need to do it here in order to allow for translations.
"preferences" => preferences,
"premiere_timestamp" => video.premiere_timestamp.try &.to_unix,
"vr" => video.is_vr,
- "projection_type" => video.projection_type
+ "projection_type" => video.projection_type,
+ "local_disabled" => CONFIG.disabled?("local")
}.to_pretty_json
%>
</script>
diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr
index 5bbd9213..2678ac6c 100644
--- a/src/invidious/yt_backend/youtube_api.cr
+++ b/src/invidious/yt_backend/youtube_api.cr
@@ -14,6 +14,7 @@ module YoutubeAPI
Android
AndroidEmbeddedPlayer
AndroidScreenEmbed
+ TvHtml5ScreenEmbed
end
# List of hard-coded values used by the different clients
@@ -60,6 +61,12 @@ module YoutubeAPI
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
screen: "EMBED",
},
+ ClientType::TvHtml5ScreenEmbed => {
+ name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
+ version: "2.0",
+ api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
+ screen: "EMBED",
+ },
}
####################################################################
@@ -401,7 +408,7 @@ module YoutubeAPI
client_config ||= DEFAULT_CLIENT_CONFIG
# Query parameters
- url = "#{endpoint}?key=#{client_config.api_key}"
+ url = "#{endpoint}?key=#{client_config.api_key}&prettyPrint=false"
headers = HTTP::Headers{
"Content-Type" => "application/json; charset=UTF-8",
diff --git a/videojs-dependencies.yml b/videojs-dependencies.yml
index 6de23d25..e9ccc9dd 100644
--- a/videojs-dependencies.yml
+++ b/videojs-dependencies.yml
@@ -1,9 +1,7 @@
-# Due to an firefox issue, we're stuck on 7.11.0. If you're hosting a private instance
-# and you're using a chromium based browser, feel free to bump this to the latest version
-# in order to get support for higher resolutions on more videos.
+# Due to a 'video append of' error (see #3011), we're stuck on 7.12.1.
video.js:
- version: 7.11.0
- shasum: e20747d890716085e7255a90d73c00f32324a224
+ version: 7.12.1
+ shasum: 1d12eeb1f52e3679e8e4c987d9b9eb37e2247fa2
videojs-contrib-quality-levels:
version: 2.1.0