mirror of
https://github.com/iv-org/invidious.git
synced 2025-08-05 04:08:33 +00:00
Merge branch 'master' into homepage
This commit is contained in:
commit
8c0222a27a
@ -3,18 +3,24 @@ dist: bionic
|
|||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- stage: build
|
- stage: build
|
||||||
|
# TODO: Shallowly clone again once the .git folder is no longer required for building
|
||||||
|
git:
|
||||||
|
depth: false
|
||||||
language: crystal
|
language: crystal
|
||||||
crystal: latest
|
crystal: latest
|
||||||
before_install:
|
before_install:
|
||||||
- shards update
|
- shards update
|
||||||
- shards install
|
- shards install
|
||||||
install:
|
install:
|
||||||
- crystal build --error-on-warnings src/invidious.cr
|
- crystal build --warnings all --error-on-warnings src/invidious.cr
|
||||||
script:
|
script:
|
||||||
- crystal tool format --check
|
- crystal tool format --check
|
||||||
- crystal spec
|
- crystal spec
|
||||||
|
|
||||||
- stage: build_docker
|
- stage: build_docker
|
||||||
|
# TODO: Shallowly clone again once the .git folder is no longer required for building
|
||||||
|
git:
|
||||||
|
depth: false
|
||||||
language: minimal
|
language: minimal
|
||||||
services:
|
services:
|
||||||
- docker
|
- docker
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
- Developer [API](https://github.com/omarroth/invidious/wiki/API)
|
- Developer [API](https://github.com/omarroth/invidious/wiki/API)
|
||||||
|
|
||||||
Liberapay: https://liberapay.com/omarroth
|
Liberapay: https://liberapay.com/omarroth
|
||||||
Patreon: https://patreon.com/omarroth
|
|
||||||
BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY
|
BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY
|
||||||
BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk
|
BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk
|
||||||
|
|
||||||
|
@ -21,10 +21,9 @@ body {
|
|||||||
color: #f0f0f0;
|
color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pure-form > fieldset > input,
|
input,
|
||||||
.pure-control-group > input,
|
select,
|
||||||
.pure-form > fieldset > select,
|
textarea {
|
||||||
.pure-control-group > select {
|
|
||||||
color: rgba(35, 35, 35, 1);
|
color: rgba(35, 35, 35, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,6 +168,7 @@ img.thumbnail {
|
|||||||
|
|
||||||
.navbar .index-link {
|
.navbar .index-link {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar > .searchbar .pure-form input[type="search"] {
|
.navbar > .searchbar .pure-form input[type="search"] {
|
||||||
@ -319,6 +320,10 @@ input[type="search"]::-webkit-search-cancel-button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.vjs-menu-content::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.vjs-user-inactive {
|
.vjs-user-inactive {
|
||||||
cursor: none;
|
cursor: none;
|
||||||
}
|
}
|
||||||
@ -367,6 +372,11 @@ input[type="search"]::-webkit-search-cancel-button {
|
|||||||
.vjs-control-bar {
|
.vjs-control-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-control-bar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-js .vjs-icon-cog {
|
.video-js .vjs-icon-cog {
|
||||||
@ -423,6 +433,7 @@ span > select {
|
|||||||
/* ProgressBar marker */
|
/* ProgressBar marker */
|
||||||
.vjs-marker {
|
.vjs-marker {
|
||||||
background-color: rgba(255, 255, 255, 1);
|
background-color: rgba(255, 255, 255, 1);
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Big "Play" Button */
|
/* Big "Play" Button */
|
||||||
|
@ -12,7 +12,8 @@ function get_playlist(plid, retries) {
|
|||||||
'&format=html&hl=' + video_data.preferences.locale;
|
'&format=html&hl=' + video_data.preferences.locale;
|
||||||
} else {
|
} else {
|
||||||
var plid_url = '/api/v1/playlists/' + plid +
|
var plid_url = '/api/v1/playlists/' + plid +
|
||||||
'?continuation=' + video_data.id +
|
'?index=' + video_data.index +
|
||||||
|
'&continuation' + video_data.id +
|
||||||
'&format=html&hl=' + video_data.preferences.locale;
|
'&format=html&hl=' + video_data.preferences.locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +46,9 @@ function get_playlist(plid, retries) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
url.searchParams.set('list', plid);
|
url.searchParams.set('list', plid);
|
||||||
|
if (!plid.startsWith('RD')) {
|
||||||
|
url.searchParams.set('index', xhr.response.index);
|
||||||
|
}
|
||||||
location.assign(url.pathname + url.search);
|
location.assign(url.pathname + url.search);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -65,9 +69,10 @@ function get_playlist(plid, retries) {
|
|||||||
xhr.send();
|
xhr.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video_data.plid) {
|
window.addEventListener('load', function (e) {
|
||||||
|
if (video_data.plid) {
|
||||||
get_playlist(video_data.plid);
|
get_playlist(video_data.plid);
|
||||||
} else if (video_data.video_series) {
|
} else if (video_data.video_series) {
|
||||||
player.on('ended', function () {
|
player.on('ended', function () {
|
||||||
var url = new URL('https://example.com/embed/' + video_data.video_series.shift());
|
var url = new URL('https://example.com/embed/' + video_data.video_series.shift());
|
||||||
|
|
||||||
@ -93,4 +98,5 @@ if (video_data.plid) {
|
|||||||
|
|
||||||
location.assign(url.pathname + url.search);
|
location.assign(url.pathname + url.search);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
@ -151,6 +151,7 @@ player.vttThumbnails({
|
|||||||
|
|
||||||
// Enable annotations
|
// Enable annotations
|
||||||
if (!video_data.params.listen && video_data.params.annotations) {
|
if (!video_data.params.listen && video_data.params.annotations) {
|
||||||
|
window.addEventListener('load', function (e) {
|
||||||
var video_container = document.getElementById('player');
|
var video_container = document.getElementById('player');
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
xhr.responseType = 'text';
|
xhr.responseType = 'text';
|
||||||
@ -190,6 +191,7 @@ if (!video_data.params.listen && video_data.params.annotations) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
xhr.send();
|
xhr.send();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function increase_volume(delta) {
|
function increase_volume(delta) {
|
||||||
@ -234,25 +236,25 @@ function toggle_play() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggle_captions = (function() {
|
const toggle_captions = (function () {
|
||||||
let toggledTrack = null;
|
let toggledTrack = null;
|
||||||
const onChange = function(e) {
|
const onChange = function (e) {
|
||||||
toggledTrack = null;
|
toggledTrack = null;
|
||||||
};
|
};
|
||||||
const bindChange = function(onOrOff) {
|
const bindChange = function (onOrOff) {
|
||||||
player.textTracks()[onOrOff]('change', onChange);
|
player.textTracks()[onOrOff]('change', onChange);
|
||||||
};
|
};
|
||||||
// Wrapper function to ignore our own emitted events and only listen
|
// Wrapper function to ignore our own emitted events and only listen
|
||||||
// to events emitted by Video.js on click on the captions menu items.
|
// to events emitted by Video.js on click on the captions menu items.
|
||||||
const setMode = function(track, mode) {
|
const setMode = function (track, mode) {
|
||||||
bindChange('off');
|
bindChange('off');
|
||||||
track.mode = mode;
|
track.mode = mode;
|
||||||
window.setTimeout(function() {
|
window.setTimeout(function () {
|
||||||
bindChange('on');
|
bindChange('on');
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
bindChange('on');
|
bindChange('on');
|
||||||
return function() {
|
return function () {
|
||||||
if (toggledTrack !== null) {
|
if (toggledTrack !== null) {
|
||||||
if (toggledTrack.mode !== 'showing') {
|
if (toggledTrack.mode !== 'showing') {
|
||||||
setMode(toggledTrack, 'showing');
|
setMode(toggledTrack, 'showing');
|
||||||
@ -422,7 +424,7 @@ window.addEventListener('keydown', e => {
|
|||||||
|
|
||||||
// Add support for controlling the player volume by scrolling over it. Adapted from
|
// Add support for controlling the player volume by scrolling over it. Adapted from
|
||||||
// https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L292-L328
|
// https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L292-L328
|
||||||
(function() {
|
(function () {
|
||||||
const volumeStep = 0.05;
|
const volumeStep = 0.05;
|
||||||
const enableVolumeScroll = true;
|
const enableVolumeScroll = true;
|
||||||
const enableHoverScroll = true;
|
const enableHoverScroll = true;
|
||||||
@ -432,8 +434,8 @@ window.addEventListener('keydown', e => {
|
|||||||
var volumeHover = false;
|
var volumeHover = false;
|
||||||
var volumeSelector = pEl.querySelector('.vjs-volume-menu-button') || pEl.querySelector('.vjs-volume-panel');
|
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.onmouseover = function () { volumeHover = true; };
|
||||||
volumeSelector.onmouseout = function() { volumeHover = false; };
|
volumeSelector.onmouseout = function () { volumeHover = false; };
|
||||||
}
|
}
|
||||||
|
|
||||||
var mouseScroll = function mouseScroll(event) {
|
var mouseScroll = function mouseScroll(event) {
|
||||||
|
47
assets/js/playlist_widget.js
Normal file
47
assets/js/playlist_widget.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
function add_playlist_item(target) {
|
||||||
|
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
|
tile.style.display = 'none';
|
||||||
|
|
||||||
|
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
|
||||||
|
'&video_id=' + target.getAttribute('data-id') +
|
||||||
|
'&playlist_id=' + target.getAttribute('data-plid');
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.responseType = 'json';
|
||||||
|
xhr.timeout = 10000;
|
||||||
|
xhr.open('POST', url, true);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (xhr.readyState == 4) {
|
||||||
|
if (xhr.status != 200) {
|
||||||
|
tile.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send('csrf_token=' + playlist_data.csrf_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_playlist_item(target) {
|
||||||
|
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
|
tile.style.display = 'none';
|
||||||
|
|
||||||
|
var url = '/playlist_ajax?action_remove_video=1&redirect=false' +
|
||||||
|
'&set_video_id=' + target.getAttribute('data-index') +
|
||||||
|
'&playlist_id=' + target.getAttribute('data-plid');
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.responseType = 'json';
|
||||||
|
xhr.timeout = 10000;
|
||||||
|
xhr.open('POST', url, true);
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||||
|
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (xhr.readyState == 4) {
|
||||||
|
if (xhr.status != 200) {
|
||||||
|
tile.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xhr.send('csrf_token=' + playlist_data.csrf_token);
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
@ -133,7 +133,8 @@ function get_playlist(plid, retries) {
|
|||||||
'&format=html&hl=' + video_data.preferences.locale;
|
'&format=html&hl=' + video_data.preferences.locale;
|
||||||
} else {
|
} else {
|
||||||
var plid_url = '/api/v1/playlists/' + plid +
|
var plid_url = '/api/v1/playlists/' + plid +
|
||||||
'?continuation=' + video_data.id +
|
'?index=' + video_data.index +
|
||||||
|
'&continuation=' + video_data.id +
|
||||||
'&format=html&hl=' + video_data.preferences.locale;
|
'&format=html&hl=' + video_data.preferences.locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +169,9 @@ function get_playlist(plid, retries) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
url.searchParams.set('list', plid);
|
url.searchParams.set('list', plid);
|
||||||
|
if (!plid.startsWith('RD')) {
|
||||||
|
url.searchParams.set('index', xhr.response.index);
|
||||||
|
}
|
||||||
location.assign(url.pathname + url.search);
|
location.assign(url.pathname + url.search);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -435,19 +439,21 @@ if (video_data.play_next) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video_data.plid) {
|
window.addEventListener('load', function (e) {
|
||||||
|
if (video_data.plid) {
|
||||||
get_playlist(video_data.plid);
|
get_playlist(video_data.plid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video_data.params.comments[0] === 'youtube') {
|
if (video_data.params.comments[0] === 'youtube') {
|
||||||
get_youtube_comments();
|
get_youtube_comments();
|
||||||
} else if (video_data.params.comments[0] === 'reddit') {
|
} else if (video_data.params.comments[0] === 'reddit') {
|
||||||
get_reddit_comments();
|
get_reddit_comments();
|
||||||
} else if (video_data.params.comments[1] === 'youtube') {
|
} else if (video_data.params.comments[1] === 'youtube') {
|
||||||
get_youtube_comments();
|
get_youtube_comments();
|
||||||
} else if (video_data.params.comments[1] === 'reddit') {
|
} else if (video_data.params.comments[1] === 'reddit') {
|
||||||
get_reddit_comments();
|
get_reddit_comments();
|
||||||
} else {
|
} else {
|
||||||
comments = document.getElementById('comments');
|
comments = document.getElementById('comments');
|
||||||
comments.innerHTML = '';
|
comments.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
19
config/sql/playlist_videos.sql
Normal file
19
config/sql/playlist_videos.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- Table: public.playlist_videos
|
||||||
|
|
||||||
|
-- DROP TABLE public.playlist_videos;
|
||||||
|
|
||||||
|
CREATE TABLE playlist_videos
|
||||||
|
(
|
||||||
|
title text,
|
||||||
|
id text,
|
||||||
|
author text,
|
||||||
|
ucid text,
|
||||||
|
length_seconds integer,
|
||||||
|
published timestamptz,
|
||||||
|
plid text references playlists(id),
|
||||||
|
index int8,
|
||||||
|
live_now boolean,
|
||||||
|
PRIMARY KEY (index,plid)
|
||||||
|
);
|
||||||
|
|
||||||
|
GRANT ALL ON TABLE public.playlist_videos TO kemal;
|
18
config/sql/playlists.sql
Normal file
18
config/sql/playlists.sql
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
-- Table: public.playlists
|
||||||
|
|
||||||
|
-- DROP TABLE public.playlists;
|
||||||
|
|
||||||
|
CREATE TABLE public.playlists
|
||||||
|
(
|
||||||
|
title text,
|
||||||
|
id text primary key,
|
||||||
|
author text,
|
||||||
|
description text,
|
||||||
|
video_count integer,
|
||||||
|
created timestamptz,
|
||||||
|
updated timestamptz,
|
||||||
|
privacy privacy,
|
||||||
|
index int8[]
|
||||||
|
);
|
||||||
|
|
||||||
|
GRANT ALL ON public.playlists TO kemal;
|
10
config/sql/privacy.sql
Normal file
10
config/sql/privacy.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
-- Type: public.privacy
|
||||||
|
|
||||||
|
-- DROP TYPE public.privacy;
|
||||||
|
|
||||||
|
CREATE TYPE public.privacy AS ENUM
|
||||||
|
(
|
||||||
|
'Public',
|
||||||
|
'Unlisted',
|
||||||
|
'Private'
|
||||||
|
);
|
@ -9,7 +9,7 @@ COPY ./src/ ./src/
|
|||||||
# TODO: .git folder is required for building – this is destructive.
|
# TODO: .git folder is required for building – this is destructive.
|
||||||
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
|
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
|
||||||
COPY ./.git/ ./.git/
|
COPY ./.git/ ./.git/
|
||||||
RUN crystal build --static --release \
|
RUN crystal build --static --release --warnings all --error-on-warnings \
|
||||||
# TODO: Remove next line, see https://github.com/crystal-lang/crystal/issues/7946
|
# TODO: Remove next line, see https://github.com/crystal-lang/crystal/issues/7946
|
||||||
-Dmusl \
|
-Dmusl \
|
||||||
./src/invidious.cr
|
./src/invidious.cr
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` المشتركين",
|
"`x` subscribers": "`x` المشتركين",
|
||||||
"`x` videos": "`x` الفيديوهات",
|
"`x` videos": "`x` الفيديوهات",
|
||||||
|
"`x` playlists": "`x` قوائم التشغيل",
|
||||||
"LIVE": "مباشر",
|
"LIVE": "مباشر",
|
||||||
"Shared `x` ago": "تم رفع الفيديو منذ `x`",
|
"Shared `x` ago": "تم رفع الفيديو منذ `x`",
|
||||||
"Unsubscribe": "إلغاء الإشتراك",
|
"Unsubscribe": "إلغاء الإشتراك",
|
||||||
@ -18,7 +19,7 @@
|
|||||||
"New passwords must match": "الأرقام السرية يجب ان تكون متطابقة",
|
"New passwords must match": "الأرقام السرية يجب ان تكون متطابقة",
|
||||||
"Cannot change password for Google accounts": "لا يستطيع تغيير الرقم السرى لحساب جوجل",
|
"Cannot change password for Google accounts": "لا يستطيع تغيير الرقم السرى لحساب جوجل",
|
||||||
"Authorize token?": "رمز الإذن ؟",
|
"Authorize token?": "رمز الإذن ؟",
|
||||||
"Authorize token for `x`?": "رمز الإذن لـ `x` ?",
|
"Authorize token for `x`?": "تصريح الرمز لـ `x` ؟",
|
||||||
"Yes": "نعم",
|
"Yes": "نعم",
|
||||||
"No": "لا",
|
"No": "لا",
|
||||||
"Import and Export Data": "استخراج و إضافة البيانات",
|
"Import and Export Data": "استخراج و إضافة البيانات",
|
||||||
@ -54,9 +55,9 @@
|
|||||||
"Always loop: ": "كرر الفيديو دائما: ",
|
"Always loop: ": "كرر الفيديو دائما: ",
|
||||||
"Autoplay: ": "تشغيل تلقائى: ",
|
"Autoplay: ": "تشغيل تلقائى: ",
|
||||||
"Play next by default: ": "شغل الفيديو التالي تلقائيا: ",
|
"Play next by default: ": "شغل الفيديو التالي تلقائيا: ",
|
||||||
"Autoplay next video: ": " شغل الفيديو التالى تلقائيا (فى قوائم التشغيل)",
|
"Autoplay next video: ": "شغل الفيديو التالي تلقائيا (في قوائم التشغيل) ",
|
||||||
"Listen by default: ": "تشغيل النسخة السمعية تلقائى: ",
|
"Listen by default: ": "تشغيل النسخة السمعية تلقائى: ",
|
||||||
"Proxy videos: ": "عرض الفيديوهات عن طريق الوكيل(proxy) ؟",
|
"Proxy videos: ": "عرض الفيديوهات عن طريق البروكسي؟ ",
|
||||||
"Default speed: ": "السرعة الإفتراضية: ",
|
"Default speed: ": "السرعة الإفتراضية: ",
|
||||||
"Preferred video quality: ": "الجودة المفضلة للفيديوهات: ",
|
"Preferred video quality: ": "الجودة المفضلة للفيديوهات: ",
|
||||||
"Player volume: ": "صوت المشغل: ",
|
"Player volume: ": "صوت المشغل: ",
|
||||||
@ -65,17 +66,17 @@
|
|||||||
"reddit": "Reddit",
|
"reddit": "Reddit",
|
||||||
"Default captions: ": "الترجمات الإفتراضية: ",
|
"Default captions: ": "الترجمات الإفتراضية: ",
|
||||||
"Fallback captions: ": "الترجمات المصاحبة: ",
|
"Fallback captions: ": "الترجمات المصاحبة: ",
|
||||||
"Show related videos: ": "عرض مقاطع الفيديو ذات الصلة؟",
|
"Show related videos: ": "اعرض الفيديوهات ذات الصلة: ",
|
||||||
"Show annotations by default: ": "عرض الملاحظات فى الفيديو تلقائيا ؟",
|
"Show annotations by default: ": "اعرض الملاحظات في الفيديو تلقائيا: ",
|
||||||
"Visual preferences": "التفضيلات المرئية",
|
"Visual preferences": "التفضيلات المرئية",
|
||||||
"Player style: ": "شكل مشغل الفيديوهات",
|
"Player style: ": "شكل مشغل الفيديوهات: ",
|
||||||
"Dark mode: ": "الوضع الليلى: ",
|
"Dark mode: ": "الوضع الليلى: ",
|
||||||
"Theme: ": "اللون",
|
"Theme: ": "المظهر: ",
|
||||||
"dark": "غامق (اسود)",
|
"dark": "غامق (اسود)",
|
||||||
"light": "فاتح (ابيض)",
|
"light": "فاتح (ابيض)",
|
||||||
"Thin mode: ": "الوضع الخفيف: ",
|
"Thin mode: ": "الوضع الخفيف: ",
|
||||||
"Subscription preferences": "تفضيلات الإشتراك",
|
"Subscription preferences": "تفضيلات الإشتراك",
|
||||||
"Show annotations by default for subscribed channels: ": "عرض الملاحظات فى الفيديوهات تلقائيا فى القنوات المشترك بها فقط ؟",
|
"Show annotations by default for subscribed channels: ": "عرض الملاحظات في الفيديوهات تلقائيا في القنوات المشترك بها فقط: ",
|
||||||
"Redirect homepage to feed: ": "إعادة التوجية من الصفحة الرئيسية لصفحة المشتركين (لرؤية اخر فيديوهات المشتركين): ",
|
"Redirect homepage to feed: ": "إعادة التوجية من الصفحة الرئيسية لصفحة المشتركين (لرؤية اخر فيديوهات المشتركين): ",
|
||||||
"Number of videos shown in feed: ": "عدد الفيديوهات التى ستظهر فى صفحة المشتركين: ",
|
"Number of videos shown in feed: ": "عدد الفيديوهات التى ستظهر فى صفحة المشتركين: ",
|
||||||
"Sort videos by: ": "ترتيب الفيديو بـ: ",
|
"Sort videos by: ": "ترتيب الفيديو بـ: ",
|
||||||
@ -102,12 +103,12 @@
|
|||||||
"Delete account": "حذف الحساب",
|
"Delete account": "حذف الحساب",
|
||||||
"Administrator preferences": "إعدادات المدير",
|
"Administrator preferences": "إعدادات المدير",
|
||||||
"Default homepage: ": "الصفحة الرئيسية الافتراضية ",
|
"Default homepage: ": "الصفحة الرئيسية الافتراضية ",
|
||||||
"Feed menu: ": "قائمة التغذية",
|
"Feed menu: ": "قائمة التدفقات: ",
|
||||||
"Top enabled: ": "تفعيل 'الأفضل' ؟ ",
|
"Top enabled: ": "تفعيل 'الأفضل' ؟ ",
|
||||||
"CAPTCHA enabled: ": "تفعيل الكابتشا ؟",
|
"CAPTCHA enabled: ": "تفعيل الكابتشا: ",
|
||||||
"Login enabled: ": "تفعيل تسجيل الدخول ؟",
|
"Login enabled: ": "تفعيل الولوج: ",
|
||||||
"Registration enabled: ": "تفعيل التسجيل ؟",
|
"Registration enabled: ": "تفعيل التسجيل: ",
|
||||||
"Report statistics: ": "إبلاغ الإحصائيات",
|
"Report statistics: ": "الإبلاغ عن الإحصائيات: ",
|
||||||
"Save preferences": "حفظ التفضيلات",
|
"Save preferences": "حفظ التفضيلات",
|
||||||
"Subscription manager": "مدير الإشتراكات",
|
"Subscription manager": "مدير الإشتراكات",
|
||||||
"Token manager": "إداره الرمز",
|
"Token manager": "إداره الرمز",
|
||||||
@ -118,7 +119,7 @@
|
|||||||
"unsubscribe": "إلغاء الإشتراك",
|
"unsubscribe": "إلغاء الإشتراك",
|
||||||
"revoke": "مسح",
|
"revoke": "مسح",
|
||||||
"Subscriptions": "الإشتراكات",
|
"Subscriptions": "الإشتراكات",
|
||||||
"`x` unseen notifications": "`x` إشعارات لم تشاهدها بعد ",
|
"`x` unseen notifications": "`x` إشعارات لم تشاهدها بعد",
|
||||||
"search": "بحث",
|
"search": "بحث",
|
||||||
"Log out": "تسجيل الخروج",
|
"Log out": "تسجيل الخروج",
|
||||||
"Released under the AGPLv3 by Omar Roth.": "تم الإنشاء تحت AGPLv3 بواسطة عمر روث.",
|
"Released under the AGPLv3 by Omar Roth.": "تم الإنشاء تحت AGPLv3 بواسطة عمر روث.",
|
||||||
@ -126,7 +127,17 @@
|
|||||||
"View JavaScript license information.": "مشاهدة معلومات حول تراخيص الجافاسكريبت.",
|
"View JavaScript license information.": "مشاهدة معلومات حول تراخيص الجافاسكريبت.",
|
||||||
"View privacy policy.": "عرض سياسة الخصوصية.",
|
"View privacy policy.": "عرض سياسة الخصوصية.",
|
||||||
"Trending": "الشائع",
|
"Trending": "الشائع",
|
||||||
|
"Public": "عام",
|
||||||
"Unlisted": "غير مصنف",
|
"Unlisted": "غير مصنف",
|
||||||
|
"Private": "خاص",
|
||||||
|
"View all playlists": "عرض جميع قوائم التشغيل",
|
||||||
|
"Updated `x` ago": "تم تحديثه منذ `x`",
|
||||||
|
"Delete playlist `x`?": "حذف قائمه التشغيل `x` ?",
|
||||||
|
"Delete playlist": "حذف قائمه التغشيل",
|
||||||
|
"Create playlist": "إنشاء قائمه تشغيل",
|
||||||
|
"Title": "العنوان",
|
||||||
|
"Playlist privacy": "إعدادات الخصوصيه",
|
||||||
|
"Editing playlist `x`": "تعديل قائمه التشفيل `x`",
|
||||||
"Watch on YouTube": "مشاهدة الفيديو على اليوتيوب",
|
"Watch on YouTube": "مشاهدة الفيديو على اليوتيوب",
|
||||||
"Hide annotations": "إخفاء الملاحظات فى الفيديو",
|
"Hide annotations": "إخفاء الملاحظات فى الفيديو",
|
||||||
"Show annotations": "عرض الملاحظات فى الفيديو",
|
"Show annotations": "عرض الملاحظات فى الفيديو",
|
||||||
@ -297,20 +308,20 @@
|
|||||||
"`x` hours": "`x` ساعات",
|
"`x` hours": "`x` ساعات",
|
||||||
"`x` minutes": "`x` دقائق",
|
"`x` minutes": "`x` دقائق",
|
||||||
"`x` seconds": "`x` ثوانى",
|
"`x` seconds": "`x` ثوانى",
|
||||||
"Fallback comments: ": "التعليقات المصاحبة",
|
"Fallback comments: ": "التعليقات البديلة: ",
|
||||||
"Popular": "الأكثر شعبية",
|
"Popular": "الأكثر شعبية",
|
||||||
"Top": "الأفضل",
|
"Top": "الأفضل",
|
||||||
"About": "حول",
|
"About": "حول",
|
||||||
"Rating: ": "التقييم",
|
"Rating: ": "التقييم: ",
|
||||||
"Language: ": "اللغة",
|
"Language: ": "اللغة: ",
|
||||||
"View as playlist": "عرض كا قائمة التشغيل",
|
"View as playlist": "عرض كا قائمة التشغيل",
|
||||||
"Default": "الكل",
|
"Default": "الكل",
|
||||||
"Music": "الاغانى",
|
"Music": "الاغانى",
|
||||||
"Gaming": "الألعاب",
|
"Gaming": "الألعاب",
|
||||||
"News": "الأخبار",
|
"News": "الأخبار",
|
||||||
"Movies": "الأفلام",
|
"Movies": "الأفلام",
|
||||||
"Download": "تحميل كـ",
|
"Download": "نزّل",
|
||||||
"Download as: ": "تحميل",
|
"Download as: ": "نزّله كـ: ",
|
||||||
"%A %B %-d, %Y": "%A %-d %B %Y",
|
"%A %B %-d, %Y": "%A %-d %B %Y",
|
||||||
"(edited)": "(تم تعديلة)",
|
"(edited)": "(تم تعديلة)",
|
||||||
"YouTube comment permalink": "رابط التعليق على اليوتيوب",
|
"YouTube comment permalink": "رابط التعليق على اليوتيوب",
|
||||||
@ -321,5 +332,5 @@
|
|||||||
"Videos": "الفيديوهات",
|
"Videos": "الفيديوهات",
|
||||||
"Playlists": "قوائم التشغيل",
|
"Playlists": "قوائم التشغيل",
|
||||||
"Community": "المجتمع",
|
"Community": "المجتمع",
|
||||||
"Current version: ": "الإصدار الحالى"
|
"Current version: ": "الإصدار الحالي: "
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` Abonnenten",
|
"`x` subscribers": "`x` Abonnenten",
|
||||||
"`x` videos": "`x` Videos",
|
"`x` videos": "`x` Videos",
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "LIVE",
|
"LIVE": "LIVE",
|
||||||
"Shared `x` ago": "Vor `x` geteilt",
|
"Shared `x` ago": "Vor `x` geteilt",
|
||||||
"Unsubscribe": "Abbestellen",
|
"Unsubscribe": "Abbestellen",
|
||||||
@ -129,7 +130,17 @@
|
|||||||
"View JavaScript license information.": "Javascript Lizenzinformationen anzeigen.",
|
"View JavaScript license information.": "Javascript Lizenzinformationen anzeigen.",
|
||||||
"View privacy policy.": "Datenschutzerklärung einsehen.",
|
"View privacy policy.": "Datenschutzerklärung einsehen.",
|
||||||
"Trending": "Trending",
|
"Trending": "Trending",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "Nicht aufgeführt",
|
"Unlisted": "Nicht aufgeführt",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "Video auf YouTube ansehen",
|
"Watch on YouTube": "Video auf YouTube ansehen",
|
||||||
"Hide annotations": "Anmerkungen ausblenden",
|
"Hide annotations": "Anmerkungen ausblenden",
|
||||||
"Show annotations": "Anmerkungen anzeigen",
|
"Show annotations": "Anmerkungen anzeigen",
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
"([^0-9]|^)1([^,0-9]|$)": "`x` βίντεο",
|
"([^0-9]|^)1([^,0-9]|$)": "`x` βίντεο",
|
||||||
"": "`x` βίντεο"
|
"": "`x` βίντεο"
|
||||||
},
|
},
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "ΖΩΝΤΑΝΑ",
|
"LIVE": "ΖΩΝΤΑΝΑ",
|
||||||
"Shared `x` ago": "Μοιράστηκε πριν `x`",
|
"Shared `x` ago": "Μοιράστηκε πριν `x`",
|
||||||
"Unsubscribe": "Απεγγραφή",
|
"Unsubscribe": "Απεγγραφή",
|
||||||
@ -141,7 +142,17 @@
|
|||||||
"View JavaScript license information.": "Προβολή πληροφοριών άδειας JavaScript.",
|
"View JavaScript license information.": "Προβολή πληροφοριών άδειας JavaScript.",
|
||||||
"View privacy policy.": "Προβολή πολιτικής απορρήτου.",
|
"View privacy policy.": "Προβολή πολιτικής απορρήτου.",
|
||||||
"Trending": "Τάσεις",
|
"Trending": "Τάσεις",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "Κρυφό",
|
"Unlisted": "Κρυφό",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "Προβολή στο YouTube",
|
"Watch on YouTube": "Προβολή στο YouTube",
|
||||||
"Hide annotations": "Απόκρυψη σημειώσεων",
|
"Hide annotations": "Απόκρυψη σημειώσεων",
|
||||||
"Show annotations": "Προβολή σημειώσεων",
|
"Show annotations": "Προβολή σημειώσεων",
|
||||||
|
@ -7,6 +7,10 @@
|
|||||||
"([^0-9]|^)1([^,0-9]|$)": "`x` video",
|
"([^0-9]|^)1([^,0-9]|$)": "`x` video",
|
||||||
"": "`x` videos"
|
"": "`x` videos"
|
||||||
},
|
},
|
||||||
|
"`x` playlists": {
|
||||||
|
"(\\D|^)1(\\D|$)": "`x` playlist",
|
||||||
|
"": "`x` playlists"
|
||||||
|
},
|
||||||
"LIVE": "LIVE",
|
"LIVE": "LIVE",
|
||||||
"Shared `x` ago": "Shared `x` ago",
|
"Shared `x` ago": "Shared `x` ago",
|
||||||
"Unsubscribe": "Unsubscribe",
|
"Unsubscribe": "Unsubscribe",
|
||||||
@ -77,11 +81,11 @@
|
|||||||
"Show related videos: ": "Show related videos: ",
|
"Show related videos: ": "Show related videos: ",
|
||||||
"Show annotations by default: ": "Show annotations by default: ",
|
"Show annotations by default: ": "Show annotations by default: ",
|
||||||
"Visual preferences": "Visual preferences",
|
"Visual preferences": "Visual preferences",
|
||||||
"Player style: ": "",
|
"Player style: ": "Player style: ",
|
||||||
"Dark mode: ": "Dark mode: ",
|
"Dark mode: ": "Dark mode: ",
|
||||||
"Theme: ": "",
|
"Theme: ": "Theme: ",
|
||||||
"dark": "",
|
"dark": "dark",
|
||||||
"light": "",
|
"light": "light",
|
||||||
"Thin mode: ": "Thin mode: ",
|
"Thin mode: ": "Thin mode: ",
|
||||||
"Subscription preferences": "Subscription preferences",
|
"Subscription preferences": "Subscription preferences",
|
||||||
"Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ",
|
"Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ",
|
||||||
@ -114,9 +118,9 @@
|
|||||||
"Feed menu: ": "Feed menu: ",
|
"Feed menu: ": "Feed menu: ",
|
||||||
"Top enabled: ": "Top enabled: ",
|
"Top enabled: ": "Top enabled: ",
|
||||||
"CAPTCHA enabled: ": "CAPTCHA enabled: ",
|
"CAPTCHA enabled: ": "CAPTCHA enabled: ",
|
||||||
"Login enabled: ": "Login enabled? ",
|
"Login enabled: ": "Login enabled: ",
|
||||||
"Registration enabled: ": "Registration enabled? ",
|
"Registration enabled: ": "Registration enabled: ",
|
||||||
"Report statistics: ": "Report statistics? ",
|
"Report statistics: ": "Report statistics: ",
|
||||||
"Save preferences": "Save preferences",
|
"Save preferences": "Save preferences",
|
||||||
"Subscription manager": "Subscription manager",
|
"Subscription manager": "Subscription manager",
|
||||||
"Token manager": "Token manager",
|
"Token manager": "Token manager",
|
||||||
@ -144,7 +148,17 @@
|
|||||||
"View JavaScript license information.": "View JavaScript license information.",
|
"View JavaScript license information.": "View JavaScript license information.",
|
||||||
"View privacy policy.": "View privacy policy.",
|
"View privacy policy.": "View privacy policy.",
|
||||||
"Trending": "Trending",
|
"Trending": "Trending",
|
||||||
|
"Public": "Public",
|
||||||
"Unlisted": "Unlisted",
|
"Unlisted": "Unlisted",
|
||||||
|
"Private": "Private",
|
||||||
|
"View all playlists": "View all playlists",
|
||||||
|
"Updated `x` ago": "Updated `x` ago",
|
||||||
|
"Delete playlist `x`?": "Delete playlist `x`?",
|
||||||
|
"Delete playlist": "Delete playlist",
|
||||||
|
"Create playlist": "Create playlist",
|
||||||
|
"Title": "Title",
|
||||||
|
"Playlist privacy": "Playlist privacy",
|
||||||
|
"Editing playlist `x`": "Editing playlist `x`",
|
||||||
"Watch on YouTube": "Watch on YouTube",
|
"Watch on YouTube": "Watch on YouTube",
|
||||||
"Hide annotations": "Hide annotations",
|
"Hide annotations": "Hide annotations",
|
||||||
"Show annotations": "Show annotations",
|
"Show annotations": "Show annotations",
|
||||||
@ -157,7 +171,7 @@
|
|||||||
"Blacklisted regions: ": "Blacklisted regions: ",
|
"Blacklisted regions: ": "Blacklisted regions: ",
|
||||||
"Shared `x`": "Shared `x`",
|
"Shared `x`": "Shared `x`",
|
||||||
"`x` views": {
|
"`x` views": {
|
||||||
"([^0-9]|^)1([^,0-9]|$)": "`x` views",
|
"([^0-9]|^)1([^,0-9]|$)": "`x` view",
|
||||||
"": "`x` views"
|
"": "`x` views"
|
||||||
},
|
},
|
||||||
"Premieres in `x`": "Premieres in `x`",
|
"Premieres in `x`": "Premieres in `x`",
|
||||||
@ -165,7 +179,10 @@
|
|||||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.",
|
||||||
"View YouTube comments": "View YouTube comments",
|
"View YouTube comments": "View YouTube comments",
|
||||||
"View more comments on Reddit": "View more comments on Reddit",
|
"View more comments on Reddit": "View more comments on Reddit",
|
||||||
"View `x` comments": "View `x` comments",
|
"View `x` comments": {
|
||||||
|
"(\\D|^)1(\\D|$)": "View `x` comment",
|
||||||
|
"": "View `x` comments"
|
||||||
|
},
|
||||||
"View Reddit comments": "View Reddit comments",
|
"View Reddit comments": "View Reddit comments",
|
||||||
"Hide replies": "Hide replies",
|
"Hide replies": "Hide replies",
|
||||||
"Show replies": "Show replies",
|
"Show replies": "Show replies",
|
||||||
@ -362,7 +379,7 @@
|
|||||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||||
"(edited)": "(edited)",
|
"(edited)": "(edited)",
|
||||||
"YouTube comment permalink": "YouTube comment permalink",
|
"YouTube comment permalink": "YouTube comment permalink",
|
||||||
"permalink": "",
|
"permalink": "permalink",
|
||||||
"`x` marked it with a ❤": "`x` marked it with a ❤",
|
"`x` marked it with a ❤": "`x` marked it with a ❤",
|
||||||
"Audio mode": "Audio mode",
|
"Audio mode": "Audio mode",
|
||||||
"Video mode": "Video mode",
|
"Video mode": "Video mode",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` abonantoj",
|
"`x` subscribers": "`x` abonantoj",
|
||||||
"`x` videos": "`x` videoj",
|
"`x` videos": "`x` videoj",
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "NUNA",
|
"LIVE": "NUNA",
|
||||||
"Shared `x` ago": "Konigita antaŭ `x`",
|
"Shared `x` ago": "Konigita antaŭ `x`",
|
||||||
"Unsubscribe": "Malaboni",
|
"Unsubscribe": "Malaboni",
|
||||||
@ -126,7 +127,17 @@
|
|||||||
"View JavaScript license information.": "Vidi Ĝavoskriptan licencan informon.",
|
"View JavaScript license information.": "Vidi Ĝavoskriptan licencan informon.",
|
||||||
"View privacy policy.": "Vidi regularon pri privateco.",
|
"View privacy policy.": "Vidi regularon pri privateco.",
|
||||||
"Trending": "Tendencoj",
|
"Trending": "Tendencoj",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "Ne listigita",
|
"Unlisted": "Ne listigita",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "Vidi videon en Youtube",
|
"Watch on YouTube": "Vidi videon en Youtube",
|
||||||
"Hide annotations": "Kaŝi prinotojn",
|
"Hide annotations": "Kaŝi prinotojn",
|
||||||
"Show annotations": "Montri prinotojn",
|
"Show annotations": "Montri prinotojn",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` suscriptores",
|
"`x` subscribers": "`x` suscriptores",
|
||||||
"`x` videos": "`x` vídeos",
|
"`x` videos": "`x` vídeos",
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "DIRECTO",
|
"LIVE": "DIRECTO",
|
||||||
"Shared `x` ago": "Compartido hace `x`",
|
"Shared `x` ago": "Compartido hace `x`",
|
||||||
"Unsubscribe": "Desuscribirse",
|
"Unsubscribe": "Desuscribirse",
|
||||||
@ -126,7 +127,17 @@
|
|||||||
"View JavaScript license information.": "Ver información de licencia de JavaScript.",
|
"View JavaScript license information.": "Ver información de licencia de JavaScript.",
|
||||||
"View privacy policy.": "Ver la política de privacidad.",
|
"View privacy policy.": "Ver la política de privacidad.",
|
||||||
"Trending": "Tendencias",
|
"Trending": "Tendencias",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "No listado",
|
"Unlisted": "No listado",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "Ver el vídeo en Youtube",
|
"Watch on YouTube": "Ver el vídeo en Youtube",
|
||||||
"Hide annotations": "Ocultar anotaciones",
|
"Hide annotations": "Ocultar anotaciones",
|
||||||
"Show annotations": "Mostrar anotaciones",
|
"Show annotations": "Mostrar anotaciones",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` harpidedun",
|
"`x` subscribers": "`x` harpidedun",
|
||||||
"`x` videos": "`x` bideo",
|
"`x` videos": "`x` bideo",
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "ZUZENEAN",
|
"LIVE": "ZUZENEAN",
|
||||||
"Shared `x` ago": "Duela `x` partekatua",
|
"Shared `x` ago": "Duela `x` partekatua",
|
||||||
"Unsubscribe": "Harpidetza kendu",
|
"Unsubscribe": "Harpidetza kendu",
|
||||||
@ -126,7 +127,17 @@
|
|||||||
"View JavaScript license information.": "",
|
"View JavaScript license information.": "",
|
||||||
"View privacy policy.": "",
|
"View privacy policy.": "",
|
||||||
"Trending": "",
|
"Trending": "",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "",
|
"Unlisted": "",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "",
|
"Watch on YouTube": "",
|
||||||
"Hide annotations": "",
|
"Hide annotations": "",
|
||||||
"Show annotations": "",
|
"Show annotations": "",
|
||||||
@ -140,6 +151,7 @@
|
|||||||
"Shared `x`": "",
|
"Shared `x`": "",
|
||||||
"`x` views": "",
|
"`x` views": "",
|
||||||
"Premieres in `x`": "",
|
"Premieres in `x`": "",
|
||||||
|
"Premieres `x`": "",
|
||||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "",
|
||||||
"View YouTube comments": "",
|
"View YouTube comments": "",
|
||||||
"View more comments on Reddit": "",
|
"View more comments on Reddit": "",
|
||||||
@ -317,5 +329,8 @@
|
|||||||
"`x` marked it with a ❤": "",
|
"`x` marked it with a ❤": "",
|
||||||
"Audio mode": "",
|
"Audio mode": "",
|
||||||
"Video mode": "",
|
"Video mode": "",
|
||||||
"Videos": ""
|
"Videos": "",
|
||||||
|
"Playlists": "",
|
||||||
|
"Community": "",
|
||||||
|
"Current version: ": ""
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` abonnés",
|
"`x` subscribers": "`x` abonnés",
|
||||||
"`x` videos": "`x` vidéos",
|
"`x` videos": "`x` vidéos",
|
||||||
|
"`x` playlists": "`x` listes de lecture",
|
||||||
"LIVE": "EN DIRECT",
|
"LIVE": "EN DIRECT",
|
||||||
"Shared `x` ago": "Ajoutée il y a `x`",
|
"Shared `x` ago": "Ajoutée il y a `x`",
|
||||||
"Unsubscribe": "Se désabonner",
|
"Unsubscribe": "Se désabonner",
|
||||||
@ -13,10 +14,10 @@
|
|||||||
"last": "Dernières",
|
"last": "Dernières",
|
||||||
"Next page": "Page suivante",
|
"Next page": "Page suivante",
|
||||||
"Previous page": "Page précédente",
|
"Previous page": "Page précédente",
|
||||||
"Clear watch history?": "Supprimer l'historique des vidéos regardées ?",
|
"Clear watch history?": "Êtes-vous sûr de vouloir supprimer l'historique des vidéos regardées ?",
|
||||||
"New password": "Nouveau mot de passe",
|
"New password": "Nouveau mot de passe",
|
||||||
"New passwords must match": "Les nouveaux mots de passe doivent être identiques",
|
"New passwords must match": "Les champs \"Nouveau mot de passe\" doivent être identiques",
|
||||||
"Cannot change password for Google accounts": "Le mot de passe d'un compte Google ne peut pas être changé",
|
"Cannot change password for Google accounts": "Le mot de passe d'un compte Google ne peut pas être changé depuis Invidious",
|
||||||
"Authorize token?": "Autoriser le token ?",
|
"Authorize token?": "Autoriser le token ?",
|
||||||
"Authorize token for `x`?": "Autoriser le token pour `x` ?",
|
"Authorize token for `x`?": "Autoriser le token pour `x` ?",
|
||||||
"Yes": "Oui",
|
"Yes": "Oui",
|
||||||
@ -29,8 +30,8 @@
|
|||||||
"Import NewPipe subscriptions (.json)": "Importer des abonnements NewPipe (.json)",
|
"Import NewPipe subscriptions (.json)": "Importer des abonnements NewPipe (.json)",
|
||||||
"Import NewPipe data (.zip)": "Importer des données NewPipe (.zip)",
|
"Import NewPipe data (.zip)": "Importer des données NewPipe (.zip)",
|
||||||
"Export": "Exporter",
|
"Export": "Exporter",
|
||||||
"Export subscriptions as OPML": "Exporter les abonnements en OPML",
|
"Export subscriptions as OPML": "Exporter les abonnements au format OPML",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporter les abonnements en OPML (pour NewPipe & FreeTube)",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporter les abonnements au format OPML (pour NewPipe & FreeTube)",
|
||||||
"Export data as JSON": "Exporter les données au format JSON",
|
"Export data as JSON": "Exporter les données au format JSON",
|
||||||
"Delete account?": "Êtes-vous sûr de vouloir supprimer votre compte ?",
|
"Delete account?": "Êtes-vous sûr de vouloir supprimer votre compte ?",
|
||||||
"History": "Historique",
|
"History": "Historique",
|
||||||
@ -52,9 +53,9 @@
|
|||||||
"Preferences": "Préférences",
|
"Preferences": "Préférences",
|
||||||
"Player preferences": "Préférences du lecteur",
|
"Player preferences": "Préférences du lecteur",
|
||||||
"Always loop: ": "Lire en boucle : ",
|
"Always loop: ": "Lire en boucle : ",
|
||||||
"Autoplay: ": "Lire automatiquement : ",
|
"Autoplay: ": "Lancer la lecture automatiquement : ",
|
||||||
"Play next by default: ": "Lire les vidéos suivantes par défaut (similaire a YouTube) : ",
|
"Play next by default: ": "Lire les vidéos suivantes par défaut : ",
|
||||||
"Autoplay next video: ": "Lire automatiquement la vidéo en file d'attente : ",
|
"Autoplay next video: ": "Lancer la lecture automatiquement pour la vidéo suivant la vidéo regardée : ",
|
||||||
"Listen by default: ": "Audio uniquement : ",
|
"Listen by default: ": "Audio uniquement : ",
|
||||||
"Proxy videos: ": "Charger les vidéos à travers un proxy : ",
|
"Proxy videos: ": "Charger les vidéos à travers un proxy : ",
|
||||||
"Default speed: ": "Vitesse par défaut : ",
|
"Default speed: ": "Vitesse par défaut : ",
|
||||||
@ -66,16 +67,16 @@
|
|||||||
"Default captions: ": "Sous-titres par défaut : ",
|
"Default captions: ": "Sous-titres par défaut : ",
|
||||||
"Fallback captions: ": "Sous-titres alternatifs : ",
|
"Fallback captions: ": "Sous-titres alternatifs : ",
|
||||||
"Show related videos: ": "Voir les vidéos liées : ",
|
"Show related videos: ": "Voir les vidéos liées : ",
|
||||||
"Show annotations by default: ": "Voir les annotations par défaut : ",
|
"Show annotations by default: ": "Afficher les annotations par défaut : ",
|
||||||
"Visual preferences": "Préférences du site",
|
"Visual preferences": "Préférences du site",
|
||||||
"Player style: ": "Style du lecteur : ",
|
"Player style: ": "Style du lecteur : ",
|
||||||
"Dark mode: ": "Mode Sombre : ",
|
"Dark mode: ": "Mode sombre : ",
|
||||||
"Theme: ": "Thème : ",
|
"Theme: ": "Thème : ",
|
||||||
"dark": "sombre",
|
"dark": "sombre",
|
||||||
"light": "clair",
|
"light": "clair",
|
||||||
"Thin mode: ": "Mode Simplifié : ",
|
"Thin mode: ": "Mode léger : ",
|
||||||
"Subscription preferences": "Préférences de la page d'abonnements",
|
"Subscription preferences": "Préférences de la page d'abonnements",
|
||||||
"Show annotations by default for subscribed channels: ": "Voir les annotations par défaut sur les chaînes suivies : ",
|
"Show annotations by default for subscribed channels: ": "Afficher les annotations par défaut sur les chaînes auxquelles vous êtes abonnés : ",
|
||||||
"Redirect homepage to feed: ": "Rediriger la page d'accueil vers la page d'abonnements : ",
|
"Redirect homepage to feed: ": "Rediriger la page d'accueil vers la page d'abonnements : ",
|
||||||
"Number of videos shown in feed: ": "Nombre de vidéos affichées dans la page d'abonnements : ",
|
"Number of videos shown in feed: ": "Nombre de vidéos affichées dans la page d'abonnements : ",
|
||||||
"Sort videos by: ": "Trier les vidéos par : ",
|
"Sort videos by: ": "Trier les vidéos par : ",
|
||||||
@ -85,12 +86,12 @@
|
|||||||
"alphabetically - reverse": "alphabétiquement - inversé",
|
"alphabetically - reverse": "alphabétiquement - inversé",
|
||||||
"channel name": "nom de la chaîne",
|
"channel name": "nom de la chaîne",
|
||||||
"channel name - reverse": "nom de la chaîne - inversé",
|
"channel name - reverse": "nom de la chaîne - inversé",
|
||||||
"Only show latest video from channel: ": "Afficher uniquement la dernière vidéo de la chaîne : ",
|
"Only show latest video from channel: ": "Afficher uniquement la dernière vidéo des chaînes auxquelles vous êtes abonnés : ",
|
||||||
"Only show latest unwatched video from channel: ": "Afficher uniquement la dernière vidéo de la chaîne non regardée : ",
|
"Only show latest unwatched video from channel: ": "Afficher uniquement la dernière vidéo des chaînes auxquelles vous êtes abonnés qui n'a pas était regardée : ",
|
||||||
"Only show unwatched: ": "Afficher uniquement les vidéos non regardées : ",
|
"Only show unwatched: ": "Afficher uniquement les vidéos qui n'ont pas étaient regardées : ",
|
||||||
"Only show notifications (if there are any): ": "Afficher uniquement les notifications (s'il y en a) : ",
|
"Only show notifications (if there are any): ": "Afficher uniquement les notifications (s'il y en a) : ",
|
||||||
"Enable web notifications": "Activer les notifications web",
|
"Enable web notifications": "Activer les notifications web",
|
||||||
"`x` uploaded a video": "`x` a partagé(e) une video",
|
"`x` uploaded a video": "`x` a partagé(e) une vidéo",
|
||||||
"`x` is live": "`x` est en direct",
|
"`x` is live": "`x` est en direct",
|
||||||
"Data preferences": "Préférences liées aux données",
|
"Data preferences": "Préférences liées aux données",
|
||||||
"Clear watch history": "Supprimer l'historique des vidéos regardées",
|
"Clear watch history": "Supprimer l'historique des vidéos regardées",
|
||||||
@ -100,9 +101,9 @@
|
|||||||
"Manage tokens": "Gérer les tokens",
|
"Manage tokens": "Gérer les tokens",
|
||||||
"Watch history": "Historique de visionnage",
|
"Watch history": "Historique de visionnage",
|
||||||
"Delete account": "Supprimer votre compte",
|
"Delete account": "Supprimer votre compte",
|
||||||
"Administrator preferences": "Préferences d'Administrateur",
|
"Administrator preferences": "Préferences d'Administration",
|
||||||
"Default homepage: ": "Page d'accueil par défaut : ",
|
"Default homepage: ": "Page d'accueil par défaut : ",
|
||||||
"Feed menu: ": "Menu des Flux : ",
|
"Feed menu: ": "Préferences des abonnements : ",
|
||||||
"Top enabled: ": "Top activé : ",
|
"Top enabled: ": "Top activé : ",
|
||||||
"CAPTCHA enabled: ": "CAPTCHA activé : ",
|
"CAPTCHA enabled: ": "CAPTCHA activé : ",
|
||||||
"Login enabled: ": "Connexion activé : ",
|
"Login enabled: ": "Connexion activé : ",
|
||||||
@ -122,19 +123,29 @@
|
|||||||
"search": "rechercher",
|
"search": "rechercher",
|
||||||
"Log out": "Déconnexion",
|
"Log out": "Déconnexion",
|
||||||
"Released under the AGPLv3 by Omar Roth.": "Publié sous licence AGPLv3 par Omar Roth.",
|
"Released under the AGPLv3 by Omar Roth.": "Publié sous licence AGPLv3 par Omar Roth.",
|
||||||
"Source available here.": "Code Source disponible ici.",
|
"Source available here.": "Code source disponible ici.",
|
||||||
"View JavaScript license information.": "Informations des licences JavaScript.",
|
"View JavaScript license information.": "Informations des licences JavaScript.",
|
||||||
"View privacy policy.": "Politique de confidentialité.",
|
"View privacy policy.": "Politique de confidentialité.",
|
||||||
"Trending": "Tendances",
|
"Trending": "Tendances",
|
||||||
|
"Public": "Publique",
|
||||||
"Unlisted": "Non répertoriée",
|
"Unlisted": "Non répertoriée",
|
||||||
|
"Private": "Privée",
|
||||||
|
"View all playlists": "Voir toutes vos playlists",
|
||||||
|
"Updated `x` ago": "Dernière mise à jour il y a `x`",
|
||||||
|
"Delete playlist `x`?": "Êtes-vous sûr de vouloir supprimer la liste de lecture ?",
|
||||||
|
"Delete playlist": "Supprimer la liste de lecture",
|
||||||
|
"Create playlist": "Créer une liste de lecture",
|
||||||
|
"Title": "Titre",
|
||||||
|
"Playlist privacy": "Paramètres de confidentialité de la liste de lecture",
|
||||||
|
"Editing playlist `x`": "Liste de lecture modifier le `x`",
|
||||||
"Watch on YouTube": "Voir la vidéo sur Youtube",
|
"Watch on YouTube": "Voir la vidéo sur Youtube",
|
||||||
"Hide annotations": "Masquer les annotations",
|
"Hide annotations": "Masquer les annotations",
|
||||||
"Show annotations": "Afficher les annotations",
|
"Show annotations": "Afficher les annotations",
|
||||||
"Genre: ": "Genre : ",
|
"Genre: ": "Genre : ",
|
||||||
"License: ": "Licence : ",
|
"License: ": "Licence : ",
|
||||||
"Family friendly? ": "Tout Public ? ",
|
"Family friendly? ": "Vidéo tout public ? ",
|
||||||
"Wilson score: ": "Score de Wilson : ",
|
"Wilson score: ": "Score de Wilson : ",
|
||||||
"Engagement: ": "Poucentage de spectateur aillant Like ou Dislike la vidéo : ",
|
"Engagement: ": "Pourcentage de spectateur aillant appuyé sur \"J'aime\" ou \"J'aime Pas\" : ",
|
||||||
"Whitelisted regions: ": "Régions sur liste blanche : ",
|
"Whitelisted regions: ": "Régions sur liste blanche : ",
|
||||||
"Blacklisted regions: ": "Régions sur liste noire : ",
|
"Blacklisted regions: ": "Régions sur liste noire : ",
|
||||||
"Shared `x`": "Ajoutée le `x`",
|
"Shared `x`": "Ajoutée le `x`",
|
||||||
@ -149,8 +160,8 @@
|
|||||||
"Hide replies": "Masquer les réponses",
|
"Hide replies": "Masquer les réponses",
|
||||||
"Show replies": "Afficher les réponses",
|
"Show replies": "Afficher les réponses",
|
||||||
"Incorrect password": "Mot de passe incorrect",
|
"Incorrect password": "Mot de passe incorrect",
|
||||||
"Quota exceeded, try again in a few hours": "Nombre de tentative de connexion dépassé, réessayez dans quelques heures",
|
"Quota exceeded, try again in a few hours": "Nombre de tentative de connexion dépassée, réessayez dans quelques heures",
|
||||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Si vous ne parvenez pas à vous connecter, assurez-vous que l'authentification à deux facteurs (Authenticator ou SMS) est activée.",
|
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Impossible de se connecter, si après plusieurs tentative vous ne parvenez toujours pas à vous connecter, assurez-vous que l'authentification à deux facteurs (Authenticator ou SMS) est activée.",
|
||||||
"Invalid TFA code": "Code d'authentification à deux facteurs invalide",
|
"Invalid TFA code": "Code d'authentification à deux facteurs invalide",
|
||||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "La connexion a échoué. Cela peut être dû au fait que l'authentification à deux facteurs n'est pas activée sur votre compte.",
|
"Login failed. This may be because two-factor authentication is not turned on for your account.": "La connexion a échoué. Cela peut être dû au fait que l'authentification à deux facteurs n'est pas activée sur votre compte.",
|
||||||
"Wrong answer": "Réponse invalide",
|
"Wrong answer": "Réponse invalide",
|
||||||
@ -171,17 +182,17 @@
|
|||||||
"Could not fetch comments": "Impossible de charger les commentaires",
|
"Could not fetch comments": "Impossible de charger les commentaires",
|
||||||
"View `x` replies": "Voir `x` réponses",
|
"View `x` replies": "Voir `x` réponses",
|
||||||
"`x` ago": "il y a `x`",
|
"`x` ago": "il y a `x`",
|
||||||
"Load more": "Charger plus",
|
"Load more": "Voir plus",
|
||||||
"`x` points": "`x` points",
|
"`x` points": "`x` points",
|
||||||
"Could not create mix.": "Impossible de charger cette liste de lecture.",
|
"Could not create mix.": "Impossible de charger cette liste de lecture.",
|
||||||
"Empty playlist": "Liste de lecture vide",
|
"Empty playlist": "La liste de lecture est vide",
|
||||||
"Not a playlist.": "Liste de lecture invalide.",
|
"Not a playlist.": "La liste de lecture est invalide.",
|
||||||
"Playlist does not exist.": "La liste de lecture n'existe pas.",
|
"Playlist does not exist.": "La liste de lecture n'existe pas.",
|
||||||
"Could not pull trending pages.": "Impossible de charger les pages de tendances.",
|
"Could not pull trending pages.": "Impossible de charger les pages de tendances.",
|
||||||
"Hidden field \"challenge\" is a required field": "Le champ masqué « challenge » est un champ obligatoire",
|
"Hidden field \"challenge\" is a required field": "Le champ masqué \"challenge\" est un champ obligatoire",
|
||||||
"Hidden field \"token\" is a required field": "Le champ caché \"token\" est requis",
|
"Hidden field \"token\" is a required field": "Le champ caché \"token\" est requis",
|
||||||
"Erroneous challenge": "Challenge Erroné",
|
"Erroneous challenge": "Challenge invalide",
|
||||||
"Erroneous token": "Token Erroné",
|
"Erroneous token": "Token invalide",
|
||||||
"No such user": "Cet utilisateur n'existe pas",
|
"No such user": "Cet utilisateur n'existe pas",
|
||||||
"Token is expired, please try again": "Le token est expiré, veuillez réessayer",
|
"Token is expired, please try again": "Le token est expiré, veuillez réessayer",
|
||||||
"English": "Anglais",
|
"English": "Anglais",
|
||||||
@ -314,12 +325,12 @@
|
|||||||
"%A %B %-d, %Y": "%A %-d %B %Y",
|
"%A %B %-d, %Y": "%A %-d %B %Y",
|
||||||
"(edited)": "(modifié)",
|
"(edited)": "(modifié)",
|
||||||
"YouTube comment permalink": "Lien permanent vers le commentaire sur YouTube",
|
"YouTube comment permalink": "Lien permanent vers le commentaire sur YouTube",
|
||||||
"permalink": "permalien",
|
"permalink": "Lien permanent",
|
||||||
"`x` marked it with a ❤": "`x` l'a marqué d'un ❤",
|
"`x` marked it with a ❤": "`x` l'a marqué d'un ❤",
|
||||||
"Audio mode": "Mode Audio",
|
"Audio mode": "Mode audio",
|
||||||
"Video mode": "Mode Vidéo",
|
"Video mode": "Mode vidéo",
|
||||||
"Videos": "Vidéos",
|
"Videos": "Vidéos",
|
||||||
"Playlists": "Liste de lecture",
|
"Playlists": "Listes de lecture",
|
||||||
"Community": "Communauté",
|
"Community": "Communauté",
|
||||||
"Current version: ": "Version actuelle : "
|
"Current version: ": "Version actuelle : "
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"`x` subscribers": "",
|
||||||
|
"`x` videos": "",
|
||||||
|
"`x` playlists": "",
|
||||||
"`x` subscribers.": "`x` áskrifandar.",
|
"`x` subscribers.": "`x` áskrifandar.",
|
||||||
"`x` videos.": "`x` myndbönd.",
|
"`x` videos.": "`x` myndbönd.",
|
||||||
"LIVE": "BEINT",
|
"LIVE": "BEINT",
|
||||||
@ -110,10 +113,13 @@
|
|||||||
"Report statistics: ": "Skrá talnagögn? ",
|
"Report statistics: ": "Skrá talnagögn? ",
|
||||||
"Save preferences": "Vista stillingar",
|
"Save preferences": "Vista stillingar",
|
||||||
"Subscription manager": "Áskriftarstjóri",
|
"Subscription manager": "Áskriftarstjóri",
|
||||||
|
"`x` subscriptions": "",
|
||||||
|
"`x` tokens": "",
|
||||||
"Token manager": "Táknstjóri",
|
"Token manager": "Táknstjóri",
|
||||||
"Token": "Tákn",
|
"Token": "Tákn",
|
||||||
"`x` subscriptions.": "`x` áskriftir.",
|
"`x` subscriptions.": "`x` áskriftir.",
|
||||||
"`x` tokens.": "`x` tákn.",
|
"`x` tokens.": "`x` tákn.",
|
||||||
|
"`x` unseen notifications": "",
|
||||||
"Import/export": "Flytja inn/út",
|
"Import/export": "Flytja inn/út",
|
||||||
"unsubscribe": "afskrá",
|
"unsubscribe": "afskrá",
|
||||||
"revoke": "afturkalla",
|
"revoke": "afturkalla",
|
||||||
@ -126,13 +132,24 @@
|
|||||||
"View JavaScript license information.": "Skoða JavaScript leyfisupplýsingar.",
|
"View JavaScript license information.": "Skoða JavaScript leyfisupplýsingar.",
|
||||||
"View privacy policy.": "Skoða meðferð persónuupplýsinga.",
|
"View privacy policy.": "Skoða meðferð persónuupplýsinga.",
|
||||||
"Trending": "Vinsælt",
|
"Trending": "Vinsælt",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "Óskráð",
|
"Unlisted": "Óskráð",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "Horfa á YouTube",
|
"Watch on YouTube": "Horfa á YouTube",
|
||||||
"Hide annotations": "Fela glósur",
|
"Hide annotations": "Fela glósur",
|
||||||
"Show annotations": "Sýna glósur",
|
"Show annotations": "Sýna glósur",
|
||||||
"Genre: ": "Tegund: ",
|
"Genre: ": "Tegund: ",
|
||||||
"License: ": "Notkunarleyfi: ",
|
"License: ": "Notkunarleyfi: ",
|
||||||
"Family friendly? ": "Fjölskylduvænt? ",
|
"Family friendly? ": "Fjölskylduvænt? ",
|
||||||
|
"`x` views": "",
|
||||||
"Wilson score: ": "Wilson stig: ",
|
"Wilson score: ": "Wilson stig: ",
|
||||||
"Engagement: ": "Þátttöku: ",
|
"Engagement: ": "Þátttöku: ",
|
||||||
"Whitelisted regions: ": "Svæði á hvítum lista: ",
|
"Whitelisted regions: ": "Svæði á hvítum lista: ",
|
||||||
@ -163,8 +180,10 @@
|
|||||||
"Password cannot be empty": "Lykilorð má ekki vera autt",
|
"Password cannot be empty": "Lykilorð má ekki vera autt",
|
||||||
"Password cannot be longer than 55 characters": "Lykilorð má ekki vera lengra en 55 stafir",
|
"Password cannot be longer than 55 characters": "Lykilorð má ekki vera lengra en 55 stafir",
|
||||||
"Please log in": "Vinsamlegast skráðu þig inn",
|
"Please log in": "Vinsamlegast skráðu þig inn",
|
||||||
|
"View `x` replies": "",
|
||||||
"Invidious Private Feed for `x`": "Invidious Persónulegur Straumur fyrir `x`",
|
"Invidious Private Feed for `x`": "Invidious Persónulegur Straumur fyrir `x`",
|
||||||
"channel:`x`": "rás:`x`",
|
"channel:`x`": "rás:`x`",
|
||||||
|
"`x` points": "",
|
||||||
"Deleted or invalid channel": "Eytt eða ógild rás",
|
"Deleted or invalid channel": "Eytt eða ógild rás",
|
||||||
"This channel does not exist.": "Þessi rás er ekki til.",
|
"This channel does not exist.": "Þessi rás er ekki til.",
|
||||||
"Could not get channel info.": "Ekki tókst að fá rásarupplýsingar.",
|
"Could not get channel info.": "Ekki tókst að fá rásarupplýsingar.",
|
||||||
@ -282,6 +301,13 @@
|
|||||||
"Turkish": "Tyrkneska",
|
"Turkish": "Tyrkneska",
|
||||||
"Ukrainian": "Úkraníska",
|
"Ukrainian": "Úkraníska",
|
||||||
"Urdu": "Úrdú",
|
"Urdu": "Úrdú",
|
||||||
|
"`x` years": "",
|
||||||
|
"`x` months": "",
|
||||||
|
"`x` weeks": "",
|
||||||
|
"`x` days": "",
|
||||||
|
"`x` hours": "",
|
||||||
|
"`x` minutes": "",
|
||||||
|
"`x` seconds": "",
|
||||||
"Uzbek": "Úsbekíska",
|
"Uzbek": "Úsbekíska",
|
||||||
"Vietnamese": "Víetnamska",
|
"Vietnamese": "Víetnamska",
|
||||||
"Welsh": "Velska",
|
"Welsh": "Velska",
|
||||||
@ -299,11 +325,13 @@
|
|||||||
"`x` seconds.": "`x` sekúndur.",
|
"`x` seconds.": "`x` sekúndur.",
|
||||||
"Fallback comments: ": "Vara ummæli: ",
|
"Fallback comments: ": "Vara ummæli: ",
|
||||||
"Popular": "Vinsælt",
|
"Popular": "Vinsælt",
|
||||||
|
"permalink": "",
|
||||||
"Top": "Topp",
|
"Top": "Topp",
|
||||||
"About": "Um",
|
"About": "Um",
|
||||||
"Rating: ": "Einkunn: ",
|
"Rating: ": "Einkunn: ",
|
||||||
"Language: ": "Tungumál: ",
|
"Language: ": "Tungumál: ",
|
||||||
"View as playlist": "Skoða sem spilunarlista",
|
"View as playlist": "Skoða sem spilunarlista",
|
||||||
|
"Community": "",
|
||||||
"Default": "Sjálfgefið",
|
"Default": "Sjálfgefið",
|
||||||
"Music": "Tónlist",
|
"Music": "Tónlist",
|
||||||
"Gaming": "Tólvuleikja",
|
"Gaming": "Tólvuleikja",
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
"([^0-9]|^)1([^,0-9]|$)": "`x` video",
|
"([^0-9]|^)1([^,0-9]|$)": "`x` video",
|
||||||
"": "`x` video"
|
"": "`x` video"
|
||||||
},
|
},
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "IN DIRETTA",
|
"LIVE": "IN DIRETTA",
|
||||||
"Shared `x` ago": "Condiviso `x` fa",
|
"Shared `x` ago": "Condiviso `x` fa",
|
||||||
"Unsubscribe": "Disiscriviti",
|
"Unsubscribe": "Disiscriviti",
|
||||||
@ -141,7 +142,17 @@
|
|||||||
"View JavaScript license information.": "Guarda le informazioni di licenza del codice JavaScript.",
|
"View JavaScript license information.": "Guarda le informazioni di licenza del codice JavaScript.",
|
||||||
"View privacy policy.": "Vedi la politica sulla privacy",
|
"View privacy policy.": "Vedi la politica sulla privacy",
|
||||||
"Trending": "Tendenze",
|
"Trending": "Tendenze",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "Non elencati",
|
"Unlisted": "Non elencati",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "Guarda su YouTube",
|
"Watch on YouTube": "Guarda su YouTube",
|
||||||
"Hide annotations": "Nascondi annotazioni",
|
"Hide annotations": "Nascondi annotazioni",
|
||||||
"Show annotations": "Mostra annotazioni",
|
"Show annotations": "Mostra annotazioni",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` abonnenter",
|
"`x` subscribers": "`x` abonnenter",
|
||||||
"`x` videos": "`x` videoer",
|
"`x` videos": "`x` videoer",
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "SANNTIDSVISNING",
|
"LIVE": "SANNTIDSVISNING",
|
||||||
"Shared `x` ago": "Delt for `x` siden",
|
"Shared `x` ago": "Delt for `x` siden",
|
||||||
"Unsubscribe": "Opphev abonnement",
|
"Unsubscribe": "Opphev abonnement",
|
||||||
@ -126,7 +127,17 @@
|
|||||||
"View JavaScript license information.": "Vis JavaScript-lisensinfo.",
|
"View JavaScript license information.": "Vis JavaScript-lisensinfo.",
|
||||||
"View privacy policy.": "Vis personvernspraksis.",
|
"View privacy policy.": "Vis personvernspraksis.",
|
||||||
"Trending": "Trendsettende",
|
"Trending": "Trendsettende",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "Ulistet",
|
"Unlisted": "Ulistet",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "Vis video på YouTube",
|
"Watch on YouTube": "Vis video på YouTube",
|
||||||
"Hide annotations": "Skjul merknader",
|
"Hide annotations": "Skjul merknader",
|
||||||
"Show annotations": "Vis merknader",
|
"Show annotations": "Vis merknader",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` abonnees",
|
"`x` subscribers": "`x` abonnees",
|
||||||
"`x` videos": "`x` video's",
|
"`x` videos": "`x` video's",
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "LIVE",
|
"LIVE": "LIVE",
|
||||||
"Shared `x` ago": "Gedeeld: `x` geleden",
|
"Shared `x` ago": "Gedeeld: `x` geleden",
|
||||||
"Unsubscribe": "Deabonneren",
|
"Unsubscribe": "Deabonneren",
|
||||||
@ -126,7 +127,17 @@
|
|||||||
"View JavaScript license information.": "JavaScript-licentieinformatie tonen.",
|
"View JavaScript license information.": "JavaScript-licentieinformatie tonen.",
|
||||||
"View privacy policy.": "Privacybeleid tonen",
|
"View privacy policy.": "Privacybeleid tonen",
|
||||||
"Trending": "Uitgelicht",
|
"Trending": "Uitgelicht",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "Verborgen",
|
"Unlisted": "Verborgen",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "Video bekijken op YouTube",
|
"Watch on YouTube": "Video bekijken op YouTube",
|
||||||
"Hide annotations": "Annotaties verbergen",
|
"Hide annotations": "Annotaties verbergen",
|
||||||
"Show annotations": "Annotaties tonen",
|
"Show annotations": "Annotaties tonen",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` subskrybcji",
|
"`x` subscribers": "`x` subskrybcji",
|
||||||
"`x` videos": "`x` filmów",
|
"`x` videos": "`x` filmów",
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "NA ŻYWO",
|
"LIVE": "NA ŻYWO",
|
||||||
"Shared `x` ago": "Udostępniono `x` temu",
|
"Shared `x` ago": "Udostępniono `x` temu",
|
||||||
"Unsubscribe": "Odsubskrybuj",
|
"Unsubscribe": "Odsubskrybuj",
|
||||||
@ -126,7 +127,17 @@
|
|||||||
"View JavaScript license information.": "Wyświetl informację o licencji JavaScript.",
|
"View JavaScript license information.": "Wyświetl informację o licencji JavaScript.",
|
||||||
"View privacy policy.": "Polityka prywatności.",
|
"View privacy policy.": "Polityka prywatności.",
|
||||||
"Trending": "Na czasie",
|
"Trending": "Na czasie",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "",
|
"Unlisted": "",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "Zobacz film na YouTube",
|
"Watch on YouTube": "Zobacz film na YouTube",
|
||||||
"Hide annotations": "",
|
"Hide annotations": "",
|
||||||
"Show annotations": "",
|
"Show annotations": "",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` подписчиков",
|
"`x` subscribers": "`x` подписчиков",
|
||||||
"`x` videos": "`x` видео",
|
"`x` videos": "`x` видео",
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "ПРЯМОЙ ЭФИР",
|
"LIVE": "ПРЯМОЙ ЭФИР",
|
||||||
"Shared `x` ago": "Опубликовано `x` назад",
|
"Shared `x` ago": "Опубликовано `x` назад",
|
||||||
"Unsubscribe": "Отписаться",
|
"Unsubscribe": "Отписаться",
|
||||||
@ -126,7 +127,17 @@
|
|||||||
"View JavaScript license information.": "Посмотреть информацию по лицензии JavaScript.",
|
"View JavaScript license information.": "Посмотреть информацию по лицензии JavaScript.",
|
||||||
"View privacy policy.": "Посмотреть политику конфиденциальности.",
|
"View privacy policy.": "Посмотреть политику конфиденциальности.",
|
||||||
"Trending": "В тренде",
|
"Trending": "В тренде",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "Нет в списке",
|
"Unlisted": "Нет в списке",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "Смотреть на YouTube",
|
"Watch on YouTube": "Смотреть на YouTube",
|
||||||
"Hide annotations": "Скрыть аннотации",
|
"Hide annotations": "Скрыть аннотации",
|
||||||
"Show annotations": "Показать аннотации",
|
"Show annotations": "Показать аннотации",
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"`x` subscribers": "",
|
||||||
|
"`x` videos": "",
|
||||||
|
"`x` playlists": "",
|
||||||
"`x` subscribers.": "`x` abone.",
|
"`x` subscribers.": "`x` abone.",
|
||||||
"`x` videos.": "`x` video.",
|
"`x` videos.": "`x` video.",
|
||||||
"LIVE": "CANLI",
|
"LIVE": "CANLI",
|
||||||
@ -110,10 +113,13 @@
|
|||||||
"Report statistics: ": "Rapor istatistikleri: ",
|
"Report statistics: ": "Rapor istatistikleri: ",
|
||||||
"Save preferences": "Tercihleri kaydet",
|
"Save preferences": "Tercihleri kaydet",
|
||||||
"Subscription manager": "Abonelik yöneticisi",
|
"Subscription manager": "Abonelik yöneticisi",
|
||||||
|
"`x` subscriptions": "",
|
||||||
|
"`x` tokens": "",
|
||||||
"Token manager": "Jeton yöneticisi",
|
"Token manager": "Jeton yöneticisi",
|
||||||
"Token": "Jeton",
|
"Token": "Jeton",
|
||||||
"`x` subscriptions.": "`x` abonelik.",
|
"`x` subscriptions.": "`x` abonelik.",
|
||||||
"`x` tokens.": "`x` jeton.",
|
"`x` tokens.": "`x` jeton.",
|
||||||
|
"`x` unseen notifications": "",
|
||||||
"Import/export": "İçe/dışa aktar",
|
"Import/export": "İçe/dışa aktar",
|
||||||
"unsubscribe": "abonelikten çık",
|
"unsubscribe": "abonelikten çık",
|
||||||
"revoke": "geri al",
|
"revoke": "geri al",
|
||||||
@ -121,7 +127,17 @@
|
|||||||
"`x` unseen notifications.": "`x` okunmamış bildirim.",
|
"`x` unseen notifications.": "`x` okunmamış bildirim.",
|
||||||
"search": "ara",
|
"search": "ara",
|
||||||
"Log out": "Çıkış yap",
|
"Log out": "Çıkış yap",
|
||||||
|
"Public": "",
|
||||||
"Released under the AGPLv3 by Omar Roth.": "Omar Roth tarafından AGPLv3 altında yayımlandı.",
|
"Released under the AGPLv3 by Omar Roth.": "Omar Roth tarafından AGPLv3 altında yayımlandı.",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Source available here.": "Kaynak kodu burada mevcut.",
|
"Source available here.": "Kaynak kodu burada mevcut.",
|
||||||
"View JavaScript license information.": "JavaScript lisans bilgilerini görüntüle.",
|
"View JavaScript license information.": "JavaScript lisans bilgilerini görüntüle.",
|
||||||
"View privacy policy.": "Gizlilik politikasını görüntüle.",
|
"View privacy policy.": "Gizlilik politikasını görüntüle.",
|
||||||
@ -133,6 +149,7 @@
|
|||||||
"Genre: ": "Tür: ",
|
"Genre: ": "Tür: ",
|
||||||
"License: ": "Lisans: ",
|
"License: ": "Lisans: ",
|
||||||
"Family friendly? ": "Aile için uygun? ",
|
"Family friendly? ": "Aile için uygun? ",
|
||||||
|
"`x` views": "",
|
||||||
"Wilson score: ": "Wilson puanı: ",
|
"Wilson score: ": "Wilson puanı: ",
|
||||||
"Engagement: ": "İzleyenlerin oy verme oranı: ",
|
"Engagement: ": "İzleyenlerin oy verme oranı: ",
|
||||||
"Whitelisted regions: ": "Beyaz listeye alınan bölgeler: ",
|
"Whitelisted regions: ": "Beyaz listeye alınan bölgeler: ",
|
||||||
@ -163,8 +180,10 @@
|
|||||||
"Password cannot be empty": "Parola boş olamaz",
|
"Password cannot be empty": "Parola boş olamaz",
|
||||||
"Password cannot be longer than 55 characters": "Parola 55 karakterden uzun olamaz",
|
"Password cannot be longer than 55 characters": "Parola 55 karakterden uzun olamaz",
|
||||||
"Please log in": "Lütfen oturum açın",
|
"Please log in": "Lütfen oturum açın",
|
||||||
|
"View `x` replies": "",
|
||||||
"Invidious Private Feed for `x`": "`x` için İnvidious Özel Akışı",
|
"Invidious Private Feed for `x`": "`x` için İnvidious Özel Akışı",
|
||||||
"channel:`x`": "kanal:`x`",
|
"channel:`x`": "kanal:`x`",
|
||||||
|
"`x` points": "",
|
||||||
"Deleted or invalid channel": "Silinmiş ya da geçersiz kanal",
|
"Deleted or invalid channel": "Silinmiş ya da geçersiz kanal",
|
||||||
"This channel does not exist.": "Bu kanal mevcut değil.",
|
"This channel does not exist.": "Bu kanal mevcut değil.",
|
||||||
"Could not get channel info.": "Kanal bilgisi alınamadı.",
|
"Could not get channel info.": "Kanal bilgisi alınamadı.",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` підписників",
|
"`x` subscribers": "`x` підписників",
|
||||||
"`x` videos": "`x` відео",
|
"`x` videos": "`x` відео",
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "ПРЯМИЙ ЕФІР",
|
"LIVE": "ПРЯМИЙ ЕФІР",
|
||||||
"Shared `x` ago": "Розміщено `x` назад",
|
"Shared `x` ago": "Розміщено `x` назад",
|
||||||
"Unsubscribe": "Відписатися",
|
"Unsubscribe": "Відписатися",
|
||||||
@ -126,7 +127,17 @@
|
|||||||
"View JavaScript license information.": "Переглянути інформацію щодо ліцензії JavaScript.",
|
"View JavaScript license information.": "Переглянути інформацію щодо ліцензії JavaScript.",
|
||||||
"View privacy policy.": "Переглянути політику приватності.",
|
"View privacy policy.": "Переглянути політику приватності.",
|
||||||
"Trending": "У тренді",
|
"Trending": "У тренді",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "Немає в списку",
|
"Unlisted": "Немає в списку",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "Дивитися на YouTube",
|
"Watch on YouTube": "Дивитися на YouTube",
|
||||||
"Hide annotations": "Приховати анотації",
|
"Hide annotations": "Приховати анотації",
|
||||||
"Show annotations": "Показати анотації",
|
"Show annotations": "Показати анотації",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "`x` 订阅者",
|
"`x` subscribers": "`x` 订阅者",
|
||||||
"`x` videos": "`x` 视频",
|
"`x` videos": "`x` 视频",
|
||||||
|
"`x` playlists": "",
|
||||||
"LIVE": "直播",
|
"LIVE": "直播",
|
||||||
"Shared `x` ago": "`x` 前分享",
|
"Shared `x` ago": "`x` 前分享",
|
||||||
"Unsubscribe": "取消订阅",
|
"Unsubscribe": "取消订阅",
|
||||||
@ -126,7 +127,17 @@
|
|||||||
"View JavaScript license information.": "查看 JavaScript 协议信息。",
|
"View JavaScript license information.": "查看 JavaScript 协议信息。",
|
||||||
"View privacy policy.": "查看隐私政策。",
|
"View privacy policy.": "查看隐私政策。",
|
||||||
"Trending": "时下流行",
|
"Trending": "时下流行",
|
||||||
|
"Public": "",
|
||||||
"Unlisted": "不公开",
|
"Unlisted": "不公开",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `x`": "",
|
||||||
"Watch on YouTube": "在 YouTube 观看",
|
"Watch on YouTube": "在 YouTube 观看",
|
||||||
"Hide annotations": "隐藏注释",
|
"Hide annotations": "隐藏注释",
|
||||||
"Show annotations": "显示注释",
|
"Show annotations": "显示注释",
|
||||||
|
381
locales/zh-TW.json
Normal file
381
locales/zh-TW.json
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
{
|
||||||
|
"`x` subscribers": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 個訂閱者",
|
||||||
|
"": "`x` 個訂閱者"
|
||||||
|
},
|
||||||
|
"`x` videos": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 部影片",
|
||||||
|
"": "`x` 部影片"
|
||||||
|
},
|
||||||
|
"`x` playlists": "",
|
||||||
|
"LIVE": "直播",
|
||||||
|
"Shared `x` ago": "`x` 前分享",
|
||||||
|
"Unsubscribe": "取消訂閱",
|
||||||
|
"Subscribe": "訂閱",
|
||||||
|
"View channel on YouTube": "在 YouTube 上檢視頻道",
|
||||||
|
"View playlist on YouTube": "在 YouTube 上檢視播放清單",
|
||||||
|
"newest": "最新",
|
||||||
|
"oldest": "最舊",
|
||||||
|
"popular": "流行",
|
||||||
|
"last": "上一個",
|
||||||
|
"Next page": "下一頁",
|
||||||
|
"Previous page": "上一頁",
|
||||||
|
"Clear watch history?": "清除觀看歷史?",
|
||||||
|
"New password": "新密碼",
|
||||||
|
"New passwords must match": "新密碼必須符合",
|
||||||
|
"Cannot change password for Google accounts": "無法變更 Google 帳號的密碼",
|
||||||
|
"Authorize token?": "授權 token?",
|
||||||
|
"Authorize token for `x`?": "`x` 的授權 token?",
|
||||||
|
"Yes": "是",
|
||||||
|
"No": "否",
|
||||||
|
"Import and Export Data": "匯入與匯出資料",
|
||||||
|
"Import": "匯入",
|
||||||
|
"Import Invidious data": "匯入 Invidious 資料",
|
||||||
|
"Import YouTube subscriptions": "匯入 YouTube 訂閱",
|
||||||
|
"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 匯出為 JSON",
|
||||||
|
"Delete account?": "刪除帳號?",
|
||||||
|
"History": "歷史",
|
||||||
|
"An alternative front-end to YouTube": "一個 YouTube 的替代前端",
|
||||||
|
"JavaScript license information": "JavaScript 授權條款資訊",
|
||||||
|
"source": "來源",
|
||||||
|
"Log in": "登入",
|
||||||
|
"Log in/register": "登入/註冊",
|
||||||
|
"Log in with Google": "使用 Google 登入",
|
||||||
|
"User ID": "使用者 ID",
|
||||||
|
"Password": "密碼",
|
||||||
|
"Time (h:mm:ss):": "時間 (h:mm:ss):",
|
||||||
|
"Text CAPTCHA": "文字 CAPTCHA",
|
||||||
|
"Image CAPTCHA": "圖片 CAPTCHA",
|
||||||
|
"Sign In": "登入",
|
||||||
|
"Register": "註冊",
|
||||||
|
"E-mail": "電子郵件",
|
||||||
|
"Google verification code": "Google 驗證碼",
|
||||||
|
"Preferences": "偏好設定",
|
||||||
|
"Player preferences": "播放器偏好設定",
|
||||||
|
"Always loop: ": "總是循環播放:",
|
||||||
|
"Autoplay: ": "自動播放:",
|
||||||
|
"Play next by default: ": "預設播放下一部:",
|
||||||
|
"Autoplay next video: ": "自動播放下一部影片:",
|
||||||
|
"Listen by default: ": "預設聆聽:",
|
||||||
|
"Proxy videos: ": "代理影片:",
|
||||||
|
"Default speed: ": "預設速度:",
|
||||||
|
"Preferred video quality: ": "偏好的影片畫質:",
|
||||||
|
"Player volume: ": "播放器音量:",
|
||||||
|
"Default comments: ": "預設留言:",
|
||||||
|
"youtube": "youtube",
|
||||||
|
"reddit": "reddit",
|
||||||
|
"Default captions: ": "預設字幕:",
|
||||||
|
"Fallback captions: ": "汰退字幕:",
|
||||||
|
"Show related videos: ": "顯示相關的影片:",
|
||||||
|
"Show annotations by default: ": "預設顯示註釋:",
|
||||||
|
"Visual preferences": "視覺偏好設定",
|
||||||
|
"Player style: ": "播放器樣式",
|
||||||
|
"Dark mode: ": "深色模式:",
|
||||||
|
"Theme: ": "佈景主題",
|
||||||
|
"dark": "深色",
|
||||||
|
"light": "淺色",
|
||||||
|
"Thin mode: ": "精簡模式:",
|
||||||
|
"Subscription preferences": "訂閱偏好設定",
|
||||||
|
"Show annotations by default for subscribed channels: ": "預設為已訂閱的頻道顯示註釋?",
|
||||||
|
"Redirect homepage to feed: ": "重新導向首頁至 feed:",
|
||||||
|
"Number of videos shown in feed: ": "顯示在 feed 中的影片數量:",
|
||||||
|
"Sort videos by: ": "以此種方式排序影片:",
|
||||||
|
"published": "已發佈",
|
||||||
|
"published - reverse": "已發佈 - 反向",
|
||||||
|
"alphabetically": "字母",
|
||||||
|
"alphabetically - reverse": "字母 - 反向",
|
||||||
|
"channel name": "頻道名稱",
|
||||||
|
"channel name - reverse": "頻道名稱 - 反向",
|
||||||
|
"Only show latest video from channel: ": "僅顯示從頻道而來的最新影片:",
|
||||||
|
"Only show latest unwatched video from channel: ": "僅顯示從頻道而來的未觀看影片:",
|
||||||
|
"Only show unwatched: ": "僅顯示未觀看的:",
|
||||||
|
"Only show notifications (if there are any): ": "僅顯示通知(如果有的話):",
|
||||||
|
"Enable web notifications": "啟用網路通知",
|
||||||
|
"`x` uploaded a video": "`x` 上傳了一部影片",
|
||||||
|
"`x` is live": "`x` 正在直播",
|
||||||
|
"Data preferences": "資料偏好設定",
|
||||||
|
"Clear watch history": "清除觀看歷史",
|
||||||
|
"Import/export data": "匯入/匯出資料",
|
||||||
|
"Change password": "變更密碼",
|
||||||
|
"Manage subscriptions": "管理訂閱",
|
||||||
|
"Manage tokens": "管理 tokens",
|
||||||
|
"Watch history": "觀看歷史",
|
||||||
|
"Delete account": "刪除帳號",
|
||||||
|
"Administrator preferences": "管理員偏好設定",
|
||||||
|
"Default homepage: ": "預設首頁:",
|
||||||
|
"Feed menu: ": "Feed 選單:",
|
||||||
|
"Top enabled: ": "頂部啟用:",
|
||||||
|
"CAPTCHA enabled: ": "CAPTCHA 啟用:",
|
||||||
|
"Login enabled: ": "啟用登入?",
|
||||||
|
"Registration enabled: ": "啟用註冊?",
|
||||||
|
"Report statistics: ": "回報統計?",
|
||||||
|
"Save preferences": "儲存偏好設定",
|
||||||
|
"Subscription manager": "訂閱管理員",
|
||||||
|
"Token manager": "Token 管理員",
|
||||||
|
"Token": "Token",
|
||||||
|
"`x` subscriptions": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 個訂閱",
|
||||||
|
"": "`x` 個訂閱"
|
||||||
|
},
|
||||||
|
"`x` tokens": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` token",
|
||||||
|
"": "`x` tokens"
|
||||||
|
},
|
||||||
|
"Import/export": "匯入/匯出",
|
||||||
|
"unsubscribe": "取消訂閱",
|
||||||
|
"revoke": "撤銷",
|
||||||
|
"Subscriptions": "訂閱",
|
||||||
|
"`x` unseen notifications": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 個未讀的通知",
|
||||||
|
"": "`x` 個未讀的通知"
|
||||||
|
},
|
||||||
|
"search": "搜尋",
|
||||||
|
"Log out": "登出",
|
||||||
|
"Released under the AGPLv3 by Omar Roth.": "Omar Roth 以 AGPLv3 釋出。",
|
||||||
|
"Source available here.": "原始碼在此提供。",
|
||||||
|
"View JavaScript license information.": "檢視 JavaScript 授權條款資訊。",
|
||||||
|
"View privacy policy.": "檢視隱私權政策。",
|
||||||
|
"Trending": "趨勢",
|
||||||
|
"Public": "公開",
|
||||||
|
"Unlisted": "未列出",
|
||||||
|
"Private": "私人",
|
||||||
|
"View all playlists": "檢視所有播放清單",
|
||||||
|
"Updated `x` ago": "更新於 `x` 之前",
|
||||||
|
"Delete playlist `x`?": "刪除播放清單",
|
||||||
|
"Delete playlist": "刪除播放清單",
|
||||||
|
"Create playlist": "建立播放清單",
|
||||||
|
"Title": "標題",
|
||||||
|
"Playlist privacy": "播放清單隱私",
|
||||||
|
"Editing playlist `x`": "已編輯播放清單 `x`",
|
||||||
|
"Watch on YouTube": "在 YouTube 上觀看",
|
||||||
|
"Hide annotations": "隱藏註釋",
|
||||||
|
"Show annotations": "顯示註釋",
|
||||||
|
"Genre: ": "風格:",
|
||||||
|
"License: ": "授權條款:",
|
||||||
|
"Family friendly? ": "家庭友好?",
|
||||||
|
"Wilson score: ": "威爾遜分數:",
|
||||||
|
"Engagement: ": "參與度:",
|
||||||
|
"Whitelisted regions: ": "白名單區域:",
|
||||||
|
"Blacklisted regions: ": "黑名單區域:",
|
||||||
|
"Shared `x`": "`x` 發佈",
|
||||||
|
"`x` views": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 次檢視",
|
||||||
|
"": "`x` 次檢視"
|
||||||
|
},
|
||||||
|
"Premieres in `x`": "首映於 `x`",
|
||||||
|
"Premieres `x`": "首映於 `x`",
|
||||||
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "嗨!看來您將 JavaScript 關閉了。點擊這裡以檢視留言,請注意,它們可能需要比較長的時間載入。",
|
||||||
|
"View YouTube comments": "檢視 YouTube 留言",
|
||||||
|
"View more comments on Reddit": "在 Reddit 上檢視更多留言",
|
||||||
|
"View `x` comments": "檢視 `x` 則留言",
|
||||||
|
"View Reddit comments": "檢視 Reddit 留言",
|
||||||
|
"Hide replies": "隱藏回覆",
|
||||||
|
"Show replies": "顯示回覆",
|
||||||
|
"Incorrect password": "不正確的密碼",
|
||||||
|
"Quota exceeded, try again in a few hours": "超過限額,請在幾個小時後再試一次",
|
||||||
|
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "無法登入,請確定雙因素驗證(驗證器或簡訊)已開啟。",
|
||||||
|
"Invalid TFA code": "無效的 TFA 代碼",
|
||||||
|
"Login failed. This may be because two-factor authentication is not turned on for your account.": "登入失敗。這可能是因為您的帳號未開啟雙因素驗證的關係。",
|
||||||
|
"Wrong answer": "錯誤的答案",
|
||||||
|
"Erroneous CAPTCHA": "錯誤的 CAPTCHA",
|
||||||
|
"CAPTCHA is a required field": "CAPTCHA 為必填欄位",
|
||||||
|
"User ID is a required field": "使用者 ID 為必填欄位",
|
||||||
|
"Password is a required field": "密碼為必填欄位",
|
||||||
|
"Wrong username or password": "錯誤的使用者名稱或密碼",
|
||||||
|
"Please sign in using 'Log in with Google'": "請使用「以 Google 登入」來登入",
|
||||||
|
"Password cannot be empty": "密碼不能為空",
|
||||||
|
"Password cannot be longer than 55 characters": "密碼不能長於55個字元",
|
||||||
|
"Please log in": "請登入",
|
||||||
|
"Invidious Private Feed for `x`": "`x` 的 Invidious 私密 feed",
|
||||||
|
"channel:`x`": "頻道:`x`",
|
||||||
|
"Deleted or invalid channel": "已刪除或無效的頻道",
|
||||||
|
"This channel does not exist.": "此頻道不存在。",
|
||||||
|
"Could not get channel info.": "無法取得頻道資訊。",
|
||||||
|
"Could not fetch comments": "無法擷取留言",
|
||||||
|
"View `x` replies": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "檢視 `x` 則回覆",
|
||||||
|
"": "檢視 `x` 則回覆"
|
||||||
|
},
|
||||||
|
"`x` ago": "`x` 以前",
|
||||||
|
"Load more": "載入更多",
|
||||||
|
"`x` points": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 點",
|
||||||
|
"": "`x` 點"
|
||||||
|
},
|
||||||
|
"Could not create mix.": "無法建立混合。",
|
||||||
|
"Empty playlist": "空的播放清單",
|
||||||
|
"Not a playlist.": "不是播放清單。",
|
||||||
|
"Playlist does not exist.": "播放清單不存在。",
|
||||||
|
"Could not pull trending pages.": "無法拉取趨勢頁面。",
|
||||||
|
"Hidden field \"challenge\" is a required field": "隱藏的欄位 \"challenge\" 是必填欄位",
|
||||||
|
"Hidden field \"token\" is a required field": "隱藏的欄位 \"token\" 是必填欄位",
|
||||||
|
"Erroneous challenge": "錯誤的 challenge",
|
||||||
|
"Erroneous token": "錯誤的 token",
|
||||||
|
"No such user": "無此使用者",
|
||||||
|
"Token is expired, please try again": "Token 已過期,請再試一次",
|
||||||
|
"English": "英文",
|
||||||
|
"English (auto-generated)": "英文(自動生成)",
|
||||||
|
"Afrikaans": "南非語",
|
||||||
|
"Albanian": "阿爾巴尼亞語",
|
||||||
|
"Amharic": "阿姆哈拉語",
|
||||||
|
"Arabic": "阿拉伯語",
|
||||||
|
"Armenian": "亞美尼亞語",
|
||||||
|
"Azerbaijani": "亞塞拜然語",
|
||||||
|
"Bangla": "孟加拉文",
|
||||||
|
"Basque": "巴斯克語",
|
||||||
|
"Belarusian": "白俄羅斯語",
|
||||||
|
"Bosnian": "波士尼亞語",
|
||||||
|
"Bulgarian": "保加利亞語",
|
||||||
|
"Burmese": "緬甸語",
|
||||||
|
"Catalan": "加泰隆尼亞語",
|
||||||
|
"Cebuano": "宿霧語",
|
||||||
|
"Chinese (Simplified)": "簡體中文",
|
||||||
|
"Chinese (Traditional)": "繁體中文",
|
||||||
|
"Corsican": "科西嘉語",
|
||||||
|
"Croatian": "克羅埃西亞語",
|
||||||
|
"Czech": "捷克語",
|
||||||
|
"Danish": "丹麥語",
|
||||||
|
"Dutch": "荷蘭語",
|
||||||
|
"Esperanto": "世界語",
|
||||||
|
"Estonian": "愛沙尼亞語",
|
||||||
|
"Filipino": "菲律賓語",
|
||||||
|
"Finnish": "芬蘭語",
|
||||||
|
"French": "法語",
|
||||||
|
"Galician": "加利西亞語",
|
||||||
|
"Georgian": "喬治亞語",
|
||||||
|
"German": "德語",
|
||||||
|
"Greek": "希臘語",
|
||||||
|
"Gujarati": "古吉拉特語",
|
||||||
|
"Haitian Creole": "海地克里奧爾語",
|
||||||
|
"Hausa": "豪薩語",
|
||||||
|
"Hawaiian": "夏威夷語",
|
||||||
|
"Hebrew": "希伯來語",
|
||||||
|
"Hindi": "印地語",
|
||||||
|
"Hmong": "苗文",
|
||||||
|
"Hungarian": "匈牙利語",
|
||||||
|
"Icelandic": "冰島語",
|
||||||
|
"Igbo": "伊博語",
|
||||||
|
"Indonesian": "印尼語",
|
||||||
|
"Irish": "愛爾蘭語",
|
||||||
|
"Italian": "義大利語",
|
||||||
|
"Japanese": "日語",
|
||||||
|
"Javanese": "爪哇語",
|
||||||
|
"Kannada": "康納達語",
|
||||||
|
"Kazakh": "哈薩克語",
|
||||||
|
"Khmer": "高棉文",
|
||||||
|
"Korean": "韓語",
|
||||||
|
"Kurdish": "庫德語",
|
||||||
|
"Kyrgyz": "吉爾吉斯語",
|
||||||
|
"Lao": "寮語",
|
||||||
|
"Latin": "拉丁語",
|
||||||
|
"Latvian": "拉脫維亞語",
|
||||||
|
"Lithuanian": "立陶宛語",
|
||||||
|
"Luxembourgish": "盧森堡語",
|
||||||
|
"Macedonian": "馬其頓語",
|
||||||
|
"Malagasy": "馬拉加斯語",
|
||||||
|
"Malay": "馬來語",
|
||||||
|
"Malayalam": "馬拉雅拉姆語",
|
||||||
|
"Maltese": "馬爾他語",
|
||||||
|
"Maori": "毛利語",
|
||||||
|
"Marathi": "馬拉提語",
|
||||||
|
"Mongolian": "蒙古語",
|
||||||
|
"Nepali": "尼泊爾語",
|
||||||
|
"Norwegian Bokmål": "書面挪威語",
|
||||||
|
"Nyanja": "尼揚賈語",
|
||||||
|
"Pashto": "普什圖語",
|
||||||
|
"Persian": "波斯語",
|
||||||
|
"Polish": "波蘭人",
|
||||||
|
"Portuguese": "葡萄牙語",
|
||||||
|
"Punjabi": "旁遮普語",
|
||||||
|
"Romanian": "羅馬尼亞語",
|
||||||
|
"Russian": "俄語",
|
||||||
|
"Samoan": "薩摩亞語",
|
||||||
|
"Scottish Gaelic": "蘇格蘭蓋爾語",
|
||||||
|
"Serbian": "塞爾維亞語",
|
||||||
|
"Shona": "修納語",
|
||||||
|
"Sindhi": "信德語",
|
||||||
|
"Sinhala": "僧伽羅語",
|
||||||
|
"Slovak": "斯洛伐克語",
|
||||||
|
"Slovenian": "斯洛維尼亞語",
|
||||||
|
"Somali": "索馬利亞語",
|
||||||
|
"Southern Sotho": "南塞索托語",
|
||||||
|
"Spanish": "西班牙語",
|
||||||
|
"Spanish (Latin America)": "西班牙語(拉丁美洲)",
|
||||||
|
"Sundanese": "巽他語",
|
||||||
|
"Swahili": "斯瓦希里語",
|
||||||
|
"Swedish": "瑞典語",
|
||||||
|
"Tajik": "塔吉克語",
|
||||||
|
"Tamil": "坦米爾語",
|
||||||
|
"Telugu": "泰盧固語",
|
||||||
|
"Thai": "泰語",
|
||||||
|
"Turkish": "土耳其語",
|
||||||
|
"Ukrainian": "烏克蘭語",
|
||||||
|
"Urdu": "烏爾都語",
|
||||||
|
"Uzbek": "烏茲別克語",
|
||||||
|
"Vietnamese": "越南語",
|
||||||
|
"Welsh": "威爾斯語",
|
||||||
|
"Western Frisian": "西菲士蘭語",
|
||||||
|
"Xhosa": "科薩語",
|
||||||
|
"Yiddish": "意第緒語",
|
||||||
|
"Yoruba": "約魯巴語",
|
||||||
|
"Zulu": "祖魯語",
|
||||||
|
"`x` years": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 年",
|
||||||
|
"": "`x` 年"
|
||||||
|
},
|
||||||
|
"`x` months": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 月",
|
||||||
|
"": "`x` 月"
|
||||||
|
},
|
||||||
|
"`x` weeks": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 週",
|
||||||
|
"": "`x` 週"
|
||||||
|
},
|
||||||
|
"`x` days": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 天",
|
||||||
|
"": "`x` 天"
|
||||||
|
},
|
||||||
|
"`x` hours": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 小時",
|
||||||
|
"": "`x` 小時"
|
||||||
|
},
|
||||||
|
"`x` minutes": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 天",
|
||||||
|
"": "`x` 天"
|
||||||
|
},
|
||||||
|
"`x` seconds": {
|
||||||
|
"([^0-9]|^)1([^,0-9]|$)": "`x` 秒",
|
||||||
|
"": "`x` 秒"
|
||||||
|
},
|
||||||
|
"Fallback comments: ": "汰退留言:",
|
||||||
|
"Popular": "熱門頻道",
|
||||||
|
"Top": "熱門影片",
|
||||||
|
"About": "關於",
|
||||||
|
"Rating: ": "評分:",
|
||||||
|
"Language: ": "語言:",
|
||||||
|
"View as playlist": "以播放清單檢視",
|
||||||
|
"Default": "預設值",
|
||||||
|
"Music": "音樂",
|
||||||
|
"Gaming": "遊戲",
|
||||||
|
"News": "新聞",
|
||||||
|
"Movies": "電影",
|
||||||
|
"Download": "下載",
|
||||||
|
"Download as: ": "下載為:",
|
||||||
|
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||||
|
"(edited)": "(已編輯)",
|
||||||
|
"YouTube comment permalink": "YouTube 留言永久連結",
|
||||||
|
"permalink": "",
|
||||||
|
"`x` marked it with a ❤": "`x` 為此標記 ❤",
|
||||||
|
"Audio mode": "音訊模式",
|
||||||
|
"Video mode": "視訊模式",
|
||||||
|
"Videos": "影片",
|
||||||
|
"Playlists": "播放清單",
|
||||||
|
"Community": "社群",
|
||||||
|
"Current version: ": "目前版本:"
|
||||||
|
}
|
@ -19,6 +19,6 @@ dependencies:
|
|||||||
github: kemalcr/kemal
|
github: kemalcr/kemal
|
||||||
version: ~> 0.26.0
|
version: ~> 0.26.0
|
||||||
|
|
||||||
crystal: 0.31.0
|
crystal: 0.31.1
|
||||||
|
|
||||||
license: AGPLv3
|
license: AGPLv3
|
||||||
|
988
src/invidious.cr
988
src/invidious.cr
File diff suppressed because it is too large
Load Diff
@ -41,13 +41,15 @@ struct ChannelVideo
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_xml(locale, host_url, xml : XML::Builder)
|
def to_xml(locale, host_url, query_params, xml : XML::Builder)
|
||||||
|
query_params["v"] = self.id
|
||||||
|
|
||||||
xml.element("entry") do
|
xml.element("entry") do
|
||||||
xml.element("id") { xml.text "yt:video:#{self.id}" }
|
xml.element("id") { xml.text "yt:video:#{self.id}" }
|
||||||
xml.element("yt:videoId") { xml.text self.id }
|
xml.element("yt:videoId") { xml.text self.id }
|
||||||
xml.element("yt:channelId") { xml.text self.ucid }
|
xml.element("yt:channelId") { xml.text self.ucid }
|
||||||
xml.element("title") { xml.text self.title }
|
xml.element("title") { xml.text self.title }
|
||||||
xml.element("link", rel: "alternate", href: "#{host_url}/watch?v=#{self.id}")
|
xml.element("link", rel: "alternate", href: "#{host_url}/watch?#{query_params}")
|
||||||
|
|
||||||
xml.element("author") do
|
xml.element("author") do
|
||||||
xml.element("name") { xml.text self.author }
|
xml.element("name") { xml.text self.author }
|
||||||
@ -56,7 +58,7 @@ struct ChannelVideo
|
|||||||
|
|
||||||
xml.element("content", type: "xhtml") do
|
xml.element("content", type: "xhtml") do
|
||||||
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
|
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
|
||||||
xml.element("a", href: "#{host_url}/watch?v=#{self.id}") do
|
xml.element("a", href: "#{host_url}/watch?#{query_params}") do
|
||||||
xml.element("img", src: "#{host_url}/vi/#{self.id}/mqdefault.jpg")
|
xml.element("img", src: "#{host_url}/vi/#{self.id}/mqdefault.jpg")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -129,7 +129,7 @@ class AuthHandler < Kemal::Handler
|
|||||||
|
|
||||||
error_message = {"error" => ex.message}.to_json
|
error_message = {"error" => ex.message}.to_json
|
||||||
env.response.status_code = 403
|
env.response.status_code = 403
|
||||||
env.response.puts error_message
|
env.response.print error_message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -159,7 +159,8 @@ class APIHandler < Kemal::Handler
|
|||||||
|
|
||||||
env.response.output.rewind
|
env.response.output.rewind
|
||||||
|
|
||||||
if env.response.headers.includes_word?("Content-Type", "application/json")
|
if env.response.output.as(IO::Memory).size != 0 &&
|
||||||
|
env.response.headers.includes_word?("Content-Type", "application/json")
|
||||||
response = JSON.parse(env.response.output)
|
response = JSON.parse(env.response.output)
|
||||||
|
|
||||||
if fields_text = env.params.query["fields"]?
|
if fields_text = env.params.query["fields"]?
|
||||||
@ -194,7 +195,7 @@ class APIHandler < Kemal::Handler
|
|||||||
end
|
end
|
||||||
ensure
|
ensure
|
||||||
env.response.output = output
|
env.response.output = output
|
||||||
env.response.puts response
|
env.response.print response
|
||||||
|
|
||||||
env.response.flush
|
env.response.flush
|
||||||
end
|
end
|
||||||
@ -237,16 +238,3 @@ class HTTP::Client
|
|||||||
response
|
response
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
struct Crystal::ThreadLocalValue(T)
|
|
||||||
@values = Hash(Thread, T).new
|
|
||||||
|
|
||||||
def get(&block : -> T)
|
|
||||||
th = Thread.current
|
|
||||||
if !@values[th]?
|
|
||||||
@values[th] = yield
|
|
||||||
else
|
|
||||||
@values[th]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
@ -94,9 +94,7 @@ struct ConfigPreferences
|
|||||||
result
|
result
|
||||||
end
|
end
|
||||||
rescue ex
|
rescue ex
|
||||||
result = value.read_bool
|
if value.read_bool
|
||||||
|
|
||||||
if result
|
|
||||||
"dark"
|
"dark"
|
||||||
else
|
else
|
||||||
"light"
|
"light"
|
||||||
@ -110,7 +108,7 @@ struct ConfigPreferences
|
|||||||
|
|
||||||
def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String
|
def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String
|
||||||
unless node.is_a?(YAML::Nodes::Scalar)
|
unless node.is_a?(YAML::Nodes::Scalar)
|
||||||
node.raise "Expected sequence, not #{node.class}"
|
node.raise "Expected scalar, not #{node.class}"
|
||||||
end
|
end
|
||||||
|
|
||||||
case node.value
|
case node.value
|
||||||
@ -134,8 +132,8 @@ struct ConfigPreferences
|
|||||||
comments: {type: Array(String), default: ["youtube", ""], converter: StringToArray},
|
comments: {type: Array(String), default: ["youtube", ""], converter: StringToArray},
|
||||||
continue: {type: Bool, default: false},
|
continue: {type: Bool, default: false},
|
||||||
continue_autoplay: {type: Bool, default: true},
|
continue_autoplay: {type: Bool, default: true},
|
||||||
dark_mode: {type: String, default: "", converter: BoolToString},
|
|
||||||
dismissals: {type: String, default: ""},
|
dismissals: {type: String, default: ""},
|
||||||
|
dark_mode: {type: String, default: "light", converter: BoolToString},
|
||||||
latest_only: {type: Bool, default: false},
|
latest_only: {type: Bool, default: false},
|
||||||
listen: {type: Bool, default: false},
|
listen: {type: Bool, default: false},
|
||||||
local: {type: Bool, default: false},
|
local: {type: Bool, default: false},
|
||||||
@ -144,7 +142,8 @@ struct ConfigPreferences
|
|||||||
notifications_only: {type: Bool, default: false},
|
notifications_only: {type: Bool, default: false},
|
||||||
player_style: {type: String, default: "invidious"},
|
player_style: {type: String, default: "invidious"},
|
||||||
quality: {type: String, default: "hd720"},
|
quality: {type: String, default: "hd720"},
|
||||||
redirect_feed: {type: Bool, default: false},
|
default_home: {type: String, default: "Popular"},
|
||||||
|
feed_menu: {type: Array(String), default: ["Popular", "Trending", "Subscriptions", "Playlists"]},
|
||||||
related_videos: {type: Bool, default: true},
|
related_videos: {type: Bool, default: true},
|
||||||
sort: {type: String, default: "published"},
|
sort: {type: String, default: "published"},
|
||||||
speed: {type: Float32, default: 1.0_f32},
|
speed: {type: Float32, default: 1.0_f32},
|
||||||
@ -599,7 +598,17 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil)
|
|||||||
return items
|
return items
|
||||||
end
|
end
|
||||||
|
|
||||||
def analyze_table(db, logger, table_name, struct_type = nil)
|
def check_enum(db, logger, enum_name, struct_type = nil)
|
||||||
|
if !db.query_one?("SELECT true FROM pg_type WHERE typname = $1", enum_name, as: Bool)
|
||||||
|
logger.puts("CREATE TYPE #{enum_name}")
|
||||||
|
|
||||||
|
db.using_connection do |conn|
|
||||||
|
conn.as(PG::Connection).exec_all(File.read("config/sql/#{enum_name}.sql"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_table(db, logger, table_name, struct_type = nil)
|
||||||
# Create table if it doesn't exist
|
# Create table if it doesn't exist
|
||||||
begin
|
begin
|
||||||
db.exec("SELECT * FROM #{table_name} LIMIT 0")
|
db.exec("SELECT * FROM #{table_name} LIMIT 0")
|
||||||
|
@ -86,6 +86,16 @@ class HTTPClient < HTTP::Client
|
|||||||
|
|
||||||
return opts
|
return opts
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def exec(request)
|
||||||
|
if self.host == "www.youtube.com"
|
||||||
|
request.headers["x-youtube-client-name"] ||= "1"
|
||||||
|
request.headers["x-youtube-client-version"] ||= "1.20180719"
|
||||||
|
request.headers["user-agent"] ||= "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36"
|
||||||
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_proxies(country_code = "US")
|
def get_proxies(country_code = "US")
|
||||||
|
@ -20,7 +20,7 @@ end
|
|||||||
|
|
||||||
def make_client(url : URI, region = nil)
|
def make_client(url : URI, region = nil)
|
||||||
client = HTTPClient.new(url)
|
client = HTTPClient.new(url)
|
||||||
client.family = CONFIG.force_resolve
|
client.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC
|
||||||
client.read_timeout = 15.seconds
|
client.read_timeout = 15.seconds
|
||||||
client.connect_timeout = 15.seconds
|
client.connect_timeout = 15.seconds
|
||||||
|
|
||||||
|
@ -1,5 +1,51 @@
|
|||||||
struct PlaylistVideo
|
struct PlaylistVideo
|
||||||
def to_json(locale, config, kemal_config, json : JSON::Builder)
|
def to_xml(host_url, auto_generated, xml : XML::Builder)
|
||||||
|
xml.element("entry") do
|
||||||
|
xml.element("id") { xml.text "yt:video:#{self.id}" }
|
||||||
|
xml.element("yt:videoId") { xml.text self.id }
|
||||||
|
xml.element("yt:channelId") { xml.text self.ucid }
|
||||||
|
xml.element("title") { xml.text self.title }
|
||||||
|
xml.element("link", rel: "alternate", href: "#{host_url}/watch?v=#{self.id}")
|
||||||
|
|
||||||
|
xml.element("author") do
|
||||||
|
if auto_generated
|
||||||
|
xml.element("name") { xml.text self.author }
|
||||||
|
xml.element("uri") { xml.text "#{host_url}/channel/#{self.ucid}" }
|
||||||
|
else
|
||||||
|
xml.element("name") { xml.text author }
|
||||||
|
xml.element("uri") { xml.text "#{host_url}/channel/#{ucid}" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
xml.element("content", type: "xhtml") do
|
||||||
|
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
|
||||||
|
xml.element("a", href: "#{host_url}/watch?v=#{self.id}") do
|
||||||
|
xml.element("img", src: "#{host_url}/vi/#{self.id}/mqdefault.jpg")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
|
||||||
|
|
||||||
|
xml.element("media:group") do
|
||||||
|
xml.element("media:title") { xml.text self.title }
|
||||||
|
xml.element("media:thumbnail", url: "#{host_url}/vi/#{self.id}/mqdefault.jpg",
|
||||||
|
width: "320", height: "180")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_xml(host_url, auto_generated, xml : XML::Builder? = nil)
|
||||||
|
if xml
|
||||||
|
to_xml(host_url, auto_generated, xml)
|
||||||
|
else
|
||||||
|
XML.build do |json|
|
||||||
|
to_xml(host_url, auto_generated, xml)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(locale, config, kemal_config, json : JSON::Builder, index : Int32?)
|
||||||
json.object do
|
json.object do
|
||||||
json.field "title", self.title
|
json.field "title", self.title
|
||||||
json.field "videoId", self.id
|
json.field "videoId", self.id
|
||||||
@ -12,17 +58,23 @@ struct PlaylistVideo
|
|||||||
generate_thumbnails(json, self.id, config, kemal_config)
|
generate_thumbnails(json, self.id, config, kemal_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if index
|
||||||
|
json.field "index", index
|
||||||
|
json.field "indexId", self.index.to_u64.to_s(16).upcase
|
||||||
|
else
|
||||||
json.field "index", self.index
|
json.field "index", self.index
|
||||||
|
end
|
||||||
|
|
||||||
json.field "lengthSeconds", self.length_seconds
|
json.field "lengthSeconds", self.length_seconds
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_json(locale, config, kemal_config, json : JSON::Builder | Nil = nil)
|
def to_json(locale, config, kemal_config, json : JSON::Builder? = nil, index : Int32? = nil)
|
||||||
if json
|
if json
|
||||||
to_json(locale, config, kemal_config, json)
|
to_json(locale, config, kemal_config, json, index: index)
|
||||||
else
|
else
|
||||||
JSON.build do |json|
|
JSON.build do |json|
|
||||||
to_json(locale, config, kemal_config, json)
|
to_json(locale, config, kemal_config, json, index: index)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -35,12 +87,66 @@ struct PlaylistVideo
|
|||||||
length_seconds: Int32,
|
length_seconds: Int32,
|
||||||
published: Time,
|
published: Time,
|
||||||
plid: String,
|
plid: String,
|
||||||
index: Int32,
|
index: Int64,
|
||||||
live_now: Bool,
|
live_now: Bool,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
struct Playlist
|
struct Playlist
|
||||||
|
def to_json(offset, locale, config, kemal_config, json : JSON::Builder, continuation : String? = nil)
|
||||||
|
json.object do
|
||||||
|
json.field "type", "playlist"
|
||||||
|
json.field "title", self.title
|
||||||
|
json.field "playlistId", self.id
|
||||||
|
json.field "playlistThumbnail", self.thumbnail
|
||||||
|
|
||||||
|
json.field "author", self.author
|
||||||
|
json.field "authorId", self.ucid
|
||||||
|
json.field "authorUrl", "/channel/#{self.ucid}"
|
||||||
|
|
||||||
|
json.field "authorThumbnails" do
|
||||||
|
json.array do
|
||||||
|
qualities = {32, 48, 76, 100, 176, 512}
|
||||||
|
|
||||||
|
qualities.each do |quality|
|
||||||
|
json.object do
|
||||||
|
json.field "url", self.author_thumbnail.not_nil!.gsub(/=\d+/, "=s#{quality}")
|
||||||
|
json.field "width", quality
|
||||||
|
json.field "height", quality
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
json.field "description", html_to_content(self.description_html)
|
||||||
|
json.field "descriptionHtml", self.description_html
|
||||||
|
json.field "videoCount", self.video_count
|
||||||
|
|
||||||
|
json.field "viewCount", self.views
|
||||||
|
json.field "updated", self.updated.to_unix
|
||||||
|
json.field "isListed", self.privacy.public?
|
||||||
|
|
||||||
|
json.field "videos" do
|
||||||
|
json.array do
|
||||||
|
videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, continuation: continuation)
|
||||||
|
videos.each_with_index do |video, index|
|
||||||
|
video.to_json(locale, config, Kemal.config, json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(offset, locale, config, kemal_config, json : JSON::Builder? = nil, continuation : String? = nil)
|
||||||
|
if json
|
||||||
|
to_json(offset, locale, config, kemal_config, json, continuation: continuation)
|
||||||
|
else
|
||||||
|
JSON.build do |json|
|
||||||
|
to_json(offset, locale, config, kemal_config, json, continuation: continuation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
db_mapping({
|
db_mapping({
|
||||||
title: String,
|
title: String,
|
||||||
id: String,
|
id: String,
|
||||||
@ -53,57 +159,122 @@ struct Playlist
|
|||||||
updated: Time,
|
updated: Time,
|
||||||
thumbnail: String?,
|
thumbnail: String?,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def privacy
|
||||||
|
PlaylistPrivacy::Public
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_playlist_videos(plid, page, video_count, continuation = nil, locale = nil)
|
enum PlaylistPrivacy
|
||||||
client = make_client(YT_URL)
|
Public = 0
|
||||||
|
Unlisted = 1
|
||||||
|
Private = 2
|
||||||
|
end
|
||||||
|
|
||||||
if continuation
|
struct InvidiousPlaylist
|
||||||
html = client.get("/watch?v=#{continuation}&list=#{plid}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")
|
def to_json(offset, locale, config, kemal_config, json : JSON::Builder, continuation : String? = nil)
|
||||||
html = XML.parse_html(html.body)
|
json.object do
|
||||||
|
json.field "type", "invidiousPlaylist"
|
||||||
|
json.field "title", self.title
|
||||||
|
json.field "playlistId", self.id
|
||||||
|
|
||||||
index = html.xpath_node(%q(//span[@id="playlist-current-index"])).try &.content.to_i?
|
json.field "author", self.author
|
||||||
if index
|
json.field "authorId", self.ucid
|
||||||
index -= 1
|
json.field "authorUrl", nil
|
||||||
|
json.field "authorThumbnails", [] of String
|
||||||
|
|
||||||
|
json.field "description", html_to_content(self.description_html)
|
||||||
|
json.field "descriptionHtml", self.description_html
|
||||||
|
json.field "videoCount", self.video_count
|
||||||
|
|
||||||
|
json.field "viewCount", self.views
|
||||||
|
json.field "updated", self.updated.to_unix
|
||||||
|
json.field "isListed", self.privacy.public?
|
||||||
|
|
||||||
|
json.field "videos" do
|
||||||
|
json.array do
|
||||||
|
videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, continuation: continuation)
|
||||||
|
videos.each_with_index do |video, index|
|
||||||
|
video.to_json(locale, config, Kemal.config, json, offset + index)
|
||||||
end
|
end
|
||||||
index ||= 0
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(offset, locale, config, kemal_config, json : JSON::Builder? = nil, continuation : String? = nil)
|
||||||
|
if json
|
||||||
|
to_json(offset, locale, config, kemal_config, json, continuation: continuation)
|
||||||
else
|
else
|
||||||
index = (page - 1) * 100
|
JSON.build do |json|
|
||||||
end
|
to_json(offset, locale, config, kemal_config, json, continuation: continuation)
|
||||||
|
|
||||||
if video_count > 100
|
|
||||||
url = produce_playlist_url(plid, index)
|
|
||||||
|
|
||||||
response = client.get(url)
|
|
||||||
response = JSON.parse(response.body)
|
|
||||||
if !response["content_html"]? || response["content_html"].as_s.empty?
|
|
||||||
raise translate(locale, "Empty playlist")
|
|
||||||
end
|
|
||||||
|
|
||||||
document = XML.parse_html(response["content_html"].as_s)
|
|
||||||
nodeset = document.xpath_nodes(%q(.//tr[contains(@class, "pl-video")]))
|
|
||||||
videos = extract_playlist(plid, nodeset, index)
|
|
||||||
else
|
|
||||||
# Playlist has less than one page of videos, so subsequent pages will be empty
|
|
||||||
if page > 1
|
|
||||||
videos = [] of PlaylistVideo
|
|
||||||
else
|
|
||||||
# Extract first page of videos
|
|
||||||
response = client.get("/playlist?list=#{plid}&gl=US&hl=en&disable_polymer=1")
|
|
||||||
document = XML.parse_html(response.body)
|
|
||||||
nodeset = document.xpath_nodes(%q(.//tr[contains(@class, "pl-video")]))
|
|
||||||
|
|
||||||
videos = extract_playlist(plid, nodeset, 0)
|
|
||||||
|
|
||||||
if continuation
|
|
||||||
until videos[0].id == continuation
|
|
||||||
videos.shift
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return videos
|
property thumbnail_id
|
||||||
|
|
||||||
|
module PlaylistPrivacyConverter
|
||||||
|
def self.from_rs(rs)
|
||||||
|
return PlaylistPrivacy.parse(String.new(rs.read(Slice(UInt8))))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
db_mapping({
|
||||||
|
title: String,
|
||||||
|
id: String,
|
||||||
|
author: String,
|
||||||
|
description: {type: String, default: ""},
|
||||||
|
video_count: Int32,
|
||||||
|
created: Time,
|
||||||
|
updated: Time,
|
||||||
|
privacy: {type: PlaylistPrivacy, default: PlaylistPrivacy::Private, converter: PlaylistPrivacyConverter},
|
||||||
|
index: Array(Int64),
|
||||||
|
})
|
||||||
|
|
||||||
|
def thumbnail
|
||||||
|
@thumbnail_id ||= PG_DB.query_one?("SELECT id FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 1", self.id, self.index, as: String) || "-----------"
|
||||||
|
"/vi/#{@thumbnail_id}/mqdefault.jpg"
|
||||||
|
end
|
||||||
|
|
||||||
|
def author_thumbnail
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def ucid
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def views
|
||||||
|
0_i64
|
||||||
|
end
|
||||||
|
|
||||||
|
def description_html
|
||||||
|
HTML.escape(self.description).gsub("\n", "<br>")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_playlist(db, title, privacy, user)
|
||||||
|
plid = "IVPL#{Random::Secure.urlsafe_base64(24)[0, 31]}"
|
||||||
|
|
||||||
|
playlist = InvidiousPlaylist.new(
|
||||||
|
title: title.byte_slice(0, 150),
|
||||||
|
id: plid,
|
||||||
|
author: user.email,
|
||||||
|
description: "", # Max 5000 characters
|
||||||
|
video_count: 0,
|
||||||
|
created: Time.utc,
|
||||||
|
updated: Time.utc,
|
||||||
|
privacy: privacy,
|
||||||
|
index: [] of Int64,
|
||||||
|
)
|
||||||
|
|
||||||
|
playlist_array = playlist.to_a
|
||||||
|
args = arg_array(playlist_array)
|
||||||
|
|
||||||
|
db.exec("INSERT INTO playlists VALUES (#{args})", args: playlist_array)
|
||||||
|
|
||||||
|
return playlist
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_playlist(plid, nodeset, index)
|
def extract_playlist(plid, nodeset, index)
|
||||||
@ -144,7 +315,7 @@ def extract_playlist(plid, nodeset, index)
|
|||||||
length_seconds: length_seconds,
|
length_seconds: length_seconds,
|
||||||
published: Time.utc,
|
published: Time.utc,
|
||||||
plid: plid,
|
plid: plid,
|
||||||
index: index + offset,
|
index: (index + offset).to_i64,
|
||||||
live_now: live_now
|
live_now: live_now
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -200,6 +371,18 @@ def produce_playlist_url(id, index)
|
|||||||
return url
|
return url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_playlist(db, plid, locale, refresh = true, force_refresh = false)
|
||||||
|
if plid.starts_with? "IV"
|
||||||
|
if playlist = db.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
|
||||||
|
return playlist
|
||||||
|
else
|
||||||
|
raise "Playlist does not exist."
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return fetch_playlist(plid, locale)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_playlist(plid, locale)
|
def fetch_playlist(plid, locale)
|
||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
|
|
||||||
@ -261,6 +444,59 @@ def fetch_playlist(plid, locale)
|
|||||||
return playlist
|
return playlist
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_playlist_videos(db, playlist, offset, locale = nil, continuation = nil)
|
||||||
|
if playlist.is_a? InvidiousPlaylist
|
||||||
|
if !offset
|
||||||
|
index = PG_DB.query_one?("SELECT index FROM playlist_videos WHERE plid = $1 AND id = $2 LIMIT 1", playlist.id, continuation, as: Int64)
|
||||||
|
offset = playlist.index.index(index) || 0
|
||||||
|
end
|
||||||
|
|
||||||
|
db.query_all("SELECT * FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 100 OFFSET $3", playlist.id, playlist.index, offset, as: PlaylistVideo)
|
||||||
|
else
|
||||||
|
fetch_playlist_videos(playlist.id, playlist.video_count, offset, locale, continuation)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_playlist_videos(plid, video_count, offset = 0, locale = nil, continuation = nil)
|
||||||
|
client = make_client(YT_URL)
|
||||||
|
|
||||||
|
if continuation
|
||||||
|
html = client.get("/watch?v=#{continuation}&list=#{plid}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")
|
||||||
|
html = XML.parse_html(html.body)
|
||||||
|
|
||||||
|
index = html.xpath_node(%q(//span[@id="playlist-current-index"])).try &.content.to_i?.try &.- 1
|
||||||
|
offset = index || offset
|
||||||
|
end
|
||||||
|
|
||||||
|
if video_count > 100
|
||||||
|
url = produce_playlist_url(plid, offset)
|
||||||
|
|
||||||
|
response = client.get(url)
|
||||||
|
response = JSON.parse(response.body)
|
||||||
|
if !response["content_html"]? || response["content_html"].as_s.empty?
|
||||||
|
raise translate(locale, "Empty playlist")
|
||||||
|
end
|
||||||
|
|
||||||
|
document = XML.parse_html(response["content_html"].as_s)
|
||||||
|
nodeset = document.xpath_nodes(%q(.//tr[contains(@class, "pl-video")]))
|
||||||
|
videos = extract_playlist(plid, nodeset, offset)
|
||||||
|
elsif offset > 100
|
||||||
|
return [] of PlaylistVideo
|
||||||
|
else # Extract first page of videos
|
||||||
|
response = client.get("/playlist?list=#{plid}&gl=US&hl=en&disable_polymer=1")
|
||||||
|
document = XML.parse_html(response.body)
|
||||||
|
nodeset = document.xpath_nodes(%q(.//tr[contains(@class, "pl-video")]))
|
||||||
|
|
||||||
|
videos = extract_playlist(plid, nodeset, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
until videos.empty? || videos[0].index == offset
|
||||||
|
videos.shift
|
||||||
|
end
|
||||||
|
|
||||||
|
return videos
|
||||||
|
end
|
||||||
|
|
||||||
def template_playlist(playlist)
|
def template_playlist(playlist)
|
||||||
html = <<-END_HTML
|
html = <<-END_HTML
|
||||||
<h3>
|
<h3>
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
struct SearchVideo
|
struct SearchVideo
|
||||||
def to_xml(host_url, auto_generated, xml : XML::Builder)
|
def to_xml(host_url, auto_generated, query_params, xml : XML::Builder)
|
||||||
|
query_params["v"] = self.id
|
||||||
|
|
||||||
xml.element("entry") do
|
xml.element("entry") do
|
||||||
xml.element("id") { xml.text "yt:video:#{self.id}" }
|
xml.element("id") { xml.text "yt:video:#{self.id}" }
|
||||||
xml.element("yt:videoId") { xml.text self.id }
|
xml.element("yt:videoId") { xml.text self.id }
|
||||||
xml.element("yt:channelId") { xml.text self.ucid }
|
xml.element("yt:channelId") { xml.text self.ucid }
|
||||||
xml.element("title") { xml.text self.title }
|
xml.element("title") { xml.text self.title }
|
||||||
xml.element("link", rel: "alternate", href: "#{host_url}/watch?v=#{self.id}")
|
xml.element("link", rel: "alternate", href: "#{host_url}/watch?#{query_params}")
|
||||||
|
|
||||||
xml.element("author") do
|
xml.element("author") do
|
||||||
if auto_generated
|
if auto_generated
|
||||||
@ -19,9 +21,11 @@ struct SearchVideo
|
|||||||
|
|
||||||
xml.element("content", type: "xhtml") do
|
xml.element("content", type: "xhtml") do
|
||||||
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
|
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
|
||||||
xml.element("a", href: "#{host_url}/watch?v=#{self.id}") do
|
xml.element("a", href: "#{host_url}/watch?#{query_params}") do
|
||||||
xml.element("img", src: "#{host_url}/vi/#{self.id}/mqdefault.jpg")
|
xml.element("img", src: "#{host_url}/vi/#{self.id}/mqdefault.jpg")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
xml.element("p", style: "white-space:pre-wrap") { xml.text html_to_content(self.description_html) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -40,12 +44,12 @@ struct SearchVideo
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_xml(host_url, auto_generated, xml : XML::Builder | Nil = nil)
|
def to_xml(host_url, auto_generated, query_params, xml : XML::Builder | Nil = nil)
|
||||||
if xml
|
if xml
|
||||||
to_xml(host_url, auto_generated, xml)
|
to_xml(host_url, auto_generated, query_params, xml)
|
||||||
else
|
else
|
||||||
XML.build do |json|
|
XML.build do |json|
|
||||||
to_xml(host_url, auto_generated, xml)
|
to_xml(host_url, auto_generated, query_params, xml)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -427,3 +431,69 @@ def produce_channel_search_url(ucid, query, page)
|
|||||||
|
|
||||||
return url
|
return url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_search_query(query, page, user, region)
|
||||||
|
if user
|
||||||
|
user = user.as(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 { |a| a.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
|
||||||
|
count, 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)
|
||||||
|
count = items.size
|
||||||
|
else
|
||||||
|
items = [] of ChannelVideo
|
||||||
|
count = 0
|
||||||
|
end
|
||||||
|
else
|
||||||
|
search_params = produce_search_params(sort: sort, date: date, content_type: content_type,
|
||||||
|
duration: duration, features: features)
|
||||||
|
|
||||||
|
count, items = search(search_query, page, search_params, region).as(Tuple)
|
||||||
|
end
|
||||||
|
|
||||||
|
{search_query, count, items}
|
||||||
|
end
|
||||||
|
@ -85,7 +85,8 @@ struct Preferences
|
|||||||
notifications_only: {type: Bool, default: CONFIG.default_user_preferences.notifications_only},
|
notifications_only: {type: Bool, default: CONFIG.default_user_preferences.notifications_only},
|
||||||
player_style: {type: String, default: CONFIG.default_user_preferences.player_style, converter: ProcessString},
|
player_style: {type: String, default: CONFIG.default_user_preferences.player_style, converter: ProcessString},
|
||||||
quality: {type: String, default: CONFIG.default_user_preferences.quality, converter: ProcessString},
|
quality: {type: String, default: CONFIG.default_user_preferences.quality, converter: ProcessString},
|
||||||
redirect_feed: {type: Bool, default: CONFIG.default_user_preferences.redirect_feed},
|
default_home: {type: String, default: CONFIG.default_user_preferences.default_home},
|
||||||
|
feed_menu: {type: Array(String), default: CONFIG.default_user_preferences.feed_menu},
|
||||||
related_videos: {type: Bool, default: CONFIG.default_user_preferences.related_videos},
|
related_videos: {type: Bool, default: CONFIG.default_user_preferences.related_videos},
|
||||||
sort: {type: String, default: CONFIG.default_user_preferences.sort, converter: ProcessString},
|
sort: {type: String, default: CONFIG.default_user_preferences.sort, converter: ProcessString},
|
||||||
speed: {type: Float32, default: CONFIG.default_user_preferences.speed},
|
speed: {type: Float32, default: CONFIG.default_user_preferences.speed},
|
||||||
@ -283,6 +284,49 @@ def subscribe_ajax(channel_id, action, env_headers)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: Playlist stub, sync with YouTube for Google accounts
|
||||||
|
# def playlist_ajax(video_ids, source_playlist_id, name, privacy, action, env_headers)
|
||||||
|
# headers = HTTP::Headers.new
|
||||||
|
# headers["Cookie"] = env_headers["Cookie"]
|
||||||
|
#
|
||||||
|
# client = make_client(YT_URL)
|
||||||
|
# html = client.get("/view_all_playlists?disable_polymer=1", headers)
|
||||||
|
#
|
||||||
|
# cookies = HTTP::Cookies.from_headers(headers)
|
||||||
|
# html.cookies.each do |cookie|
|
||||||
|
# if {"VISITOR_INFO1_LIVE", "YSC", "SIDCC"}.includes? cookie.name
|
||||||
|
# if cookies[cookie.name]?
|
||||||
|
# cookies[cookie.name] = cookie
|
||||||
|
# else
|
||||||
|
# cookies << cookie
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# headers = cookies.add_request_headers(headers)
|
||||||
|
#
|
||||||
|
# if match = html.body.match(/'XSRF_TOKEN': "(?<session_token>[A-Za-z0-9\_\-\=]+)"/)
|
||||||
|
# session_token = match["session_token"]
|
||||||
|
#
|
||||||
|
# headers["content-type"] = "application/x-www-form-urlencoded"
|
||||||
|
#
|
||||||
|
# post_req = {
|
||||||
|
# video_ids: [] of String,
|
||||||
|
# source_playlist_id: "",
|
||||||
|
# n: name,
|
||||||
|
# p: privacy,
|
||||||
|
# session_token: session_token,
|
||||||
|
# }
|
||||||
|
# post_url = "/playlist_ajax?#{action}=1"
|
||||||
|
#
|
||||||
|
# response = client.post(post_url, headers, form: post_req)
|
||||||
|
# if response.status_code == 200
|
||||||
|
# return JSON.parse(response.body)["result"]["playlistId"].as_s
|
||||||
|
# else
|
||||||
|
# return nil
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
def get_subscription_feed(db, user, max_results = 40, page = 1)
|
def get_subscription_feed(db, user, max_results = 40, page = 1)
|
||||||
limit = max_results.clamp(0, MAX_ITEMS_PER_PAGE)
|
limit = max_results.clamp(0, MAX_ITEMS_PER_PAGE)
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
|
@ -715,13 +715,14 @@ struct Video
|
|||||||
storyboards = player_response["storyboards"]?
|
storyboards = player_response["storyboards"]?
|
||||||
.try &.as_h
|
.try &.as_h
|
||||||
.try &.["playerStoryboardSpecRenderer"]?
|
.try &.["playerStoryboardSpecRenderer"]?
|
||||||
|
.try &.["spec"]?
|
||||||
|
.try &.as_s.split("|")
|
||||||
|
|
||||||
if !storyboards
|
if !storyboards
|
||||||
storyboards = player_response["storyboards"]?
|
if storyboard = player_response["storyboards"]?
|
||||||
.try &.as_h
|
.try &.as_h
|
||||||
.try &.["playerLiveStoryboardSpecRenderer"]?
|
.try &.["playerLiveStoryboardSpecRenderer"]?
|
||||||
|
.try &.["spec"]?
|
||||||
if storyboard = storyboards.try &.["spec"]?
|
|
||||||
.try &.as_s
|
.try &.as_s
|
||||||
return [{
|
return [{
|
||||||
url: storyboard.split("#")[0],
|
url: storyboard.split("#")[0],
|
||||||
@ -736,9 +737,6 @@ struct Video
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
storyboards = storyboards.try &.["spec"]?
|
|
||||||
.try &.as_s.split("|")
|
|
||||||
|
|
||||||
items = [] of NamedTuple(
|
items = [] of NamedTuple(
|
||||||
url: String,
|
url: String,
|
||||||
width: Int32,
|
width: Int32,
|
||||||
@ -767,6 +765,7 @@ struct Video
|
|||||||
interval = interval.to_i
|
interval = interval.to_i
|
||||||
storyboard_width = storyboard_width.to_i
|
storyboard_width = storyboard_width.to_i
|
||||||
storyboard_height = storyboard_height.to_i
|
storyboard_height = storyboard_height.to_i
|
||||||
|
storyboard_count = (count / (storyboard_width * storyboard_height)).ceil.to_i
|
||||||
|
|
||||||
items << {
|
items << {
|
||||||
url: url.to_s.sub("$L", i).sub("$N", "M$M"),
|
url: url.to_s.sub("$L", i).sub("$N", "M$M"),
|
||||||
@ -776,7 +775,7 @@ struct Video
|
|||||||
interval: interval,
|
interval: interval,
|
||||||
storyboard_width: storyboard_width,
|
storyboard_width: storyboard_width,
|
||||||
storyboard_height: storyboard_height,
|
storyboard_height: storyboard_height,
|
||||||
storyboard_count: (count.to_f / (storyboard_width.to_f * storyboard_height.to_f)).ceil.to_i,
|
storyboard_count: storyboard_count,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1229,7 +1228,7 @@ def fetch_video(id, region)
|
|||||||
avg_rating = avg_rating.nan? ? 0.0 : avg_rating
|
avg_rating = avg_rating.nan? ? 0.0 : avg_rating
|
||||||
info["avg_rating"] = "#{avg_rating}"
|
info["avg_rating"] = "#{avg_rating}"
|
||||||
|
|
||||||
description_html = html.xpath_node(%q(//p[@id="eow-description"])).try &.to_xml(options: XML::SaveOptions::NO_DECL) || ""
|
description_html = html.xpath_node(%q(//p[@id="eow-description"])).try &.to_xml(options: XML::SaveOptions::NO_DECL) || "<p></p>"
|
||||||
wilson_score = ci_lower_bound(likes, likes + dislikes)
|
wilson_score = ci_lower_bound(likes, likes + dislikes)
|
||||||
|
|
||||||
published = html.xpath_node(%q(//meta[@itemprop="datePublished"])).try &.["content"]
|
published = html.xpath_node(%q(//meta[@itemprop="datePublished"])).try &.["content"]
|
||||||
@ -1275,21 +1274,35 @@ def itag_to_metadata?(itag : String)
|
|||||||
return VIDEO_FORMATS[itag]?
|
return VIDEO_FORMATS[itag]?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def process_continuation(db, query, plid, id)
|
||||||
|
continuation = nil
|
||||||
|
if plid
|
||||||
|
if index = query["index"]?.try &.to_i?
|
||||||
|
continuation = index
|
||||||
|
else
|
||||||
|
continuation = id
|
||||||
|
end
|
||||||
|
continuation ||= 0
|
||||||
|
end
|
||||||
|
|
||||||
|
continuation
|
||||||
|
end
|
||||||
|
|
||||||
def process_video_params(query, preferences)
|
def process_video_params(query, preferences)
|
||||||
annotations = query["iv_load_policy"]?.try &.to_i?
|
annotations = query["iv_load_policy"]?.try &.to_i?
|
||||||
autoplay = query["autoplay"]?.try &.to_i?
|
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
comments = query["comments"]?.try &.split(",").map { |a| a.downcase }
|
comments = query["comments"]?.try &.split(",").map { |a| a.downcase }
|
||||||
continue = query["continue"]?.try &.to_i?
|
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
continue_autoplay = query["continue_autoplay"]?.try &.to_i?
|
continue_autoplay = query["continue_autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
listen = query["listen"]? && (query["listen"] == "true" || query["listen"] == "1").to_unsafe
|
listen = query["listen"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
local = query["local"]? && (query["local"] == "true" || query["local"] == "1").to_unsafe
|
local = query["local"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
player_style = query["player_style"]?
|
player_style = query["player_style"]?
|
||||||
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
|
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
|
||||||
quality = query["quality"]?
|
quality = query["quality"]?
|
||||||
region = query["region"]?
|
region = query["region"]?
|
||||||
related_videos = query["related_videos"]? && (query["related_videos"] == "true" || query["related_videos"] == "1").to_unsafe
|
related_videos = query["related_videos"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
speed = query["speed"]?.try &.rchop("x").to_f?
|
speed = query["speed"]?.try &.rchop("x").to_f?
|
||||||
video_loop = query["loop"]?.try &.to_i?
|
video_loop = query["loop"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
volume = query["volume"]?.try &.to_i?
|
volume = query["volume"]?.try &.to_i?
|
||||||
|
|
||||||
if preferences
|
if preferences
|
||||||
@ -1342,17 +1355,10 @@ def process_video_params(query, preferences)
|
|||||||
local = false
|
local = false
|
||||||
end
|
end
|
||||||
|
|
||||||
if query["t"]?
|
if start = query["t"]? || query["time_continue"]? || query["start"]?
|
||||||
video_start = decode_time(query["t"])
|
video_start = decode_time(start)
|
||||||
end
|
end
|
||||||
video_start ||= 0
|
video_start ||= 0
|
||||||
if query["time_continue"]?
|
|
||||||
video_start = decode_time(query["time_continue"])
|
|
||||||
end
|
|
||||||
video_start ||= 0
|
|
||||||
if query["start"]?
|
|
||||||
video_start = decode_time(query["start"])
|
|
||||||
end
|
|
||||||
|
|
||||||
if query["end"]?
|
if query["end"]?
|
||||||
video_end = decode_time(query["end"])
|
video_end = decode_time(query["end"])
|
||||||
|
56
src/invidious/views/add_playlist_items.ecr
Normal file
56
src/invidious/views/add_playlist_items.ecr
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= playlist.title %> - Invidious</title>
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/playlist/<%= plid %>" />
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-3-5">
|
||||||
|
<div class="h-box">
|
||||||
|
<form class="pure-form pure-form-aligned" action="/add_playlist_items" method="get">
|
||||||
|
<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 type="hidden" name="list" value="<%= plid %>">
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var playlist_data = {
|
||||||
|
csrf_token: '<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script src="/js/playlist_widget.js"></script>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<% videos.each_slice(4) do |slice| %>
|
||||||
|
<% slice.each do |item| %>
|
||||||
|
<%= rendered "components/item" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if query %>
|
||||||
|
<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=<%= HTML.escape(query.not_nil!) %>&page=<%= page - 1 %>">
|
||||||
|
<%= 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 count >= 20 %>
|
||||||
|
<a href="/add_playlist_items?list=<%= plid %>&q=<%= HTML.escape(query.not_nil!) %>&page=<%= page + 1 %>">
|
||||||
|
<%= translate(locale, "Next page") %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
@ -6,9 +6,9 @@
|
|||||||
<div class="pure-u-1 pure-u-md-1-4"></div>
|
<div class="pure-u-1 pure-u-md-1-4"></div>
|
||||||
<div class="pure-u-1 pure-u-md-1-2">
|
<div class="pure-u-1 pure-u-md-1-2">
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<% feed_menu = config.feed_menu.dup %>
|
<% feed_menu = env.get("preferences").as(Preferences).feed_menu.dup %>
|
||||||
<% if !env.get?("user") %>
|
<% if !env.get?("user") %>
|
||||||
<% feed_menu.reject! {|feed| feed == "Subscriptions"} %>
|
<% feed_menu.reject! {|item| {"Subscriptions", "Playlists"}.includes? item} %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% feed_menu.each do |feed| %>
|
<% feed_menu.each do |feed| %>
|
||||||
<div class="pure-u-1-2 pure-u-md-1-<%= feed_menu.size %>">
|
<div class="pure-u-1-2 pure-u-md-1-<%= feed_menu.size %>">
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<p><%= translate(locale, "`x` subscribers", number_with_separator(item.subscriber_count)) %></p>
|
<p><%= translate(locale, "`x` subscribers", number_with_separator(item.subscriber_count)) %></p>
|
||||||
<% if !item.auto_generated %><p><%= translate(locale, "`x` videos", number_with_separator(item.video_count)) %></p><% end %>
|
<% if !item.auto_generated %><p><%= translate(locale, "`x` videos", number_with_separator(item.video_count)) %></p><% end %>
|
||||||
<h5><%= item.description_html %></h5>
|
<h5><%= item.description_html %></h5>
|
||||||
<% when SearchPlaylist %>
|
<% when SearchPlaylist, InvidiousPlaylist %>
|
||||||
<% if item.id.starts_with? "RD" %>
|
<% if item.id.starts_with? "RD" %>
|
||||||
<% url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").full_path.split("/")[2]}" %>
|
<% url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").full_path.split("/")[2]}" %>
|
||||||
<% else %>
|
<% else %>
|
||||||
@ -56,6 +56,19 @@
|
|||||||
<% if !env.get("preferences").as(Preferences).thin_mode %>
|
<% if !env.get("preferences").as(Preferences).thin_mode %>
|
||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
|
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
|
||||||
|
<% if plid = env.get?("remove_playlist_items") %>
|
||||||
|
<form 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">
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
|
<p class="watched">
|
||||||
|
<a onclick="remove_playlist_item(this)" data-index="<%= item.index %>" data-plid="<%= plid %>" href="javascript:void(0)">
|
||||||
|
<button type="submit" style="all:unset">
|
||||||
|
<i class="icon ion-md-trash"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% if item.responds_to?(:live_now) && item.live_now %>
|
<% if item.responds_to?(:live_now) && item.live_now %>
|
||||||
<p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p>
|
<p class="length"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p>
|
||||||
<% elsif item.length_seconds != 0 %>
|
<% elsif item.length_seconds != 0 %>
|
||||||
@ -63,7 +76,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<p><%= item.title %></p>
|
<p><a href="/watch?v=<%= item.id %>"><%= item.title %></a></p>
|
||||||
</a>
|
</a>
|
||||||
<p>
|
<p>
|
||||||
<b>
|
<b>
|
||||||
@ -103,6 +116,17 @@
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
<% elsif plid = env.get? "add_playlist_items" %>
|
||||||
|
<form onsubmit="return false" action="/playlist_ajax?action_add_video=1&video_id=<%= item.id %>&playlist_id=<%= plid %>&referer=<%= env.get("current_page") %>" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
|
<p class="watched">
|
||||||
|
<a onclick="add_playlist_item(this)" data-id="<%= item.id %>" data-plid="<%= plid %>" href="javascript:void(0)">
|
||||||
|
<button type="submit" style="all:unset">
|
||||||
|
<i class="icon ion-md-add"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if item.responds_to?(:live_now) && item.live_now %>
|
<% if item.responds_to?(:live_now) && item.live_now %>
|
||||||
|
39
src/invidious/views/create_playlist.ecr
Normal file
39
src/invidious/views/create_playlist.ecr
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "Create playlist") %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-3-5">
|
||||||
|
<div class="h-box">
|
||||||
|
<form class="pure-form pure-form-aligned" action="/create_playlist?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||||
|
<fieldset>
|
||||||
|
<legend><%= translate(locale, "Create playlist") %></legend>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="title"><%= translate(locale, "Title") %> :</label>
|
||||||
|
<input required name="title" type="text" placeholder="<%= translate(locale, "Title") %>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="privacy"><%= translate(locale, "Playlist privacy") %> :</label>
|
||||||
|
<select name="privacy" id="privacy">
|
||||||
|
<% PlaylistPrivacy.names.each do |option| %>
|
||||||
|
<option value="<%= option %>" <% if option == "Public" %> selected <% end %>><%= translate(locale, option) %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-controls">
|
||||||
|
<button type="submit" name="action" value="create_playlist" class="pure-button pure-button-primary">
|
||||||
|
<%= translate(locale, "Create playlist") %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(csrf_token) %>">
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
</div>
|
24
src/invidious/views/delete_playlist.ecr
Normal file
24
src/invidious/views/delete_playlist.ecr
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "Delete playlist") %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<form class="pure-form pure-form-aligned" action="/delete_playlist?list=<%= plid %>&referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||||
|
<legend><%= translate(locale, "Delete playlist `x`?", %|"#{HTML.escape(playlist.title)}"|) %></legend>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="pure-u-1-2">
|
||||||
|
<button type="submit" name="submit" value="delete_playlist" class="pure-button pure-button-primary">
|
||||||
|
<%= translate(locale, "Yes") %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-2">
|
||||||
|
<a class="pure-button" href="/playlist?list=<%= plid %>">
|
||||||
|
<%= translate(locale, "No") %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(csrf_token) %>">
|
||||||
|
</form>
|
||||||
|
</div>
|
81
src/invidious/views/edit_playlist.ecr
Normal file
81
src/invidious/views/edit_playlist.ecr
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= playlist.title %> - Invidious</title>
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/playlist/<%= plid %>" />
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<form class="pure-form" action="/edit_playlist?list=<%= plid %>" method="post">
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-2-3">
|
||||||
|
<h3><input class="pure-input-1" maxlength="150" name="title" type="text" value="<%= playlist.title %>"></h3>
|
||||||
|
<b>
|
||||||
|
<%= playlist.author %> |
|
||||||
|
<%= translate(locale, "`x` videos", "#{playlist.video_count}") %> |
|
||||||
|
<%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> |
|
||||||
|
<i class="icon <%= {"ion-md-globe", "ion-ios-unlock", "ion-ios-lock"}[playlist.privacy.value] %>"></i>
|
||||||
|
<select name="privacy">
|
||||||
|
<% {"Public", "Unlisted", "Private"}.each do |option| %>
|
||||||
|
<option value="<%= option %>" <% if option == playlist.privacy.to_s %>selected<% end %>><%= translate(locale, option) %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3" style="text-align:right">
|
||||||
|
<h3>
|
||||||
|
<div class="pure-g user-field">
|
||||||
|
<div class="pure-u-1-3">
|
||||||
|
<a href="javascript:void(0)">
|
||||||
|
<button type="submit" style="all:unset">
|
||||||
|
<i class="icon ion-md-save"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3"><a href="/delete_playlist?list=<%= plid %>"><i class="icon ion-md-trash"></i></a></div>
|
||||||
|
<div class="pure-u-1-3"><a href="/feed/playlist/<%= plid %>"><i class="icon ion-logo-rss"></i></a></div>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<textarea maxlength="5000" name="description" style="margin-top:10px;max-width:100%;height:20vh" class="pure-input-1"><%= playlist.description %></textarea>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(csrf_token) %>">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %>
|
||||||
|
<div class="h-box" style="text-align:right">
|
||||||
|
<h3>
|
||||||
|
<a href="/add_playlist_items?list=<%= plid %>"><i class="icon ion-md-add"></i></a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<% videos.each_slice(4) do |slice| %>
|
||||||
|
<% slice.each do |item| %>
|
||||||
|
<%= rendered "components/item" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5">
|
||||||
|
<% if page > 1 %>
|
||||||
|
<a href="/playlist?list=<%= playlist.id %>&page=<%= page - 1 %>">
|
||||||
|
<%= 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 == 100 %>
|
||||||
|
<a href="/playlist?list=<%= playlist.id %>&page=<%= page + 1 %>">
|
||||||
|
<%= translate(locale, "Next page") %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -29,6 +29,7 @@
|
|||||||
<script>
|
<script>
|
||||||
var video_data = {
|
var video_data = {
|
||||||
id: '<%= video.id %>',
|
id: '<%= video.id %>',
|
||||||
|
index: '<%= continuation %>',
|
||||||
plid: '<%= plid %>',
|
plid: '<%= plid %>',
|
||||||
length_seconds: '<%= video.length_seconds.to_f %>',
|
length_seconds: '<%= video.length_seconds.to_f %>',
|
||||||
video_series: <%= video_series.to_json %>,
|
video_series: <%= video_series.to_json %>,
|
||||||
|
8
src/invidious/views/empty.ecr
Normal file
8
src/invidious/views/empty.ecr
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
||||||
|
<title>
|
||||||
|
Invidious
|
||||||
|
</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= rendered "components/feed_menu" %>
|
@ -6,36 +6,77 @@
|
|||||||
<div class="pure-g h-box">
|
<div class="pure-g h-box">
|
||||||
<div class="pure-u-2-3">
|
<div class="pure-u-2-3">
|
||||||
<h3><%= playlist.title %></h3>
|
<h3><%= playlist.title %></h3>
|
||||||
</div>
|
<% if playlist.is_a? InvidiousPlaylist %>
|
||||||
<div class="pure-u-1-3" style="text-align:right">
|
<b>
|
||||||
<h3>
|
<% if playlist.author == user.try &.email %>
|
||||||
<a href="/feed/playlist/<%= plid %>"><i class="icon ion-logo-rss"></i></a>
|
<a href="/view_all_playlists"><%= playlist.author %></a> |
|
||||||
</h3>
|
<% else %>
|
||||||
</div>
|
<%= playlist.author %> |
|
||||||
</div>
|
<% end %>
|
||||||
|
<%= translate(locale, "`x` videos", "#{playlist.video_count}") %> |
|
||||||
<div class="pure-g h-box">
|
<%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> |
|
||||||
<div class="pure-u-1-3">
|
<% case playlist.as(InvidiousPlaylist).privacy when %>
|
||||||
|
<% when PlaylistPrivacy::Public %>
|
||||||
|
<i class="icon ion-md-globe"></i> <%= translate(locale, "Public") %>
|
||||||
|
<% when PlaylistPrivacy::Unlisted %>
|
||||||
|
<i class="icon ion-ios-unlock"></i> <%= translate(locale, "Unlisted") %>
|
||||||
|
<% when PlaylistPrivacy::Private %>
|
||||||
|
<i class="icon ion-ios-lock"></i> <%= translate(locale, "Private") %>
|
||||||
|
<% end %>
|
||||||
|
</b>
|
||||||
|
<% else %>
|
||||||
|
<b>
|
||||||
|
<a href="/channel/<%= playlist.ucid %>"><%= playlist.author %></a> |
|
||||||
|
<%= translate(locale, "`x` videos", "#{playlist.video_count}") %> |
|
||||||
|
<%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %>
|
||||||
|
</b>
|
||||||
|
<% end %>
|
||||||
|
<% if !playlist.is_a? InvidiousPlaylist %>
|
||||||
|
<div class="pure-u-2-3">
|
||||||
<a href="https://www.youtube.com/playlist?list=<%= playlist.id %>">
|
<a href="https://www.youtube.com/playlist?list=<%= playlist.id %>">
|
||||||
<%= translate(locale, "View playlist on YouTube") %>
|
<%= translate(locale, "View playlist on YouTube") %>
|
||||||
</a>
|
</a>
|
||||||
<div class="pure-u-1 pure-md-1-3">
|
|
||||||
<a href="/channel/<%= playlist.ucid %>">
|
|
||||||
<b><%= playlist.author %></b>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3" style="text-align:right">
|
||||||
|
<h3>
|
||||||
|
<div class="pure-g user-field">
|
||||||
|
<% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %>
|
||||||
|
<div class="pure-u-1-3"><a href="/edit_playlist?list=<%= plid %>"><i class="icon ion-md-create"></i></a></div>
|
||||||
|
<div class="pure-u-1-3"><a href="/delete_playlist?list=<%= plid %>"><i class="icon ion-md-trash"></i></a></div>
|
||||||
|
<% end %>
|
||||||
|
<div class="pure-u-1-3"><a href="/feed/playlist/<%= plid %>"><i class="icon ion-logo-rss"></i></a></div>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-2"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
<p><%= playlist.description_html %></p>
|
<p><%= playlist.description_html %></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %>
|
||||||
|
<div class="h-box" style="text-align:right">
|
||||||
|
<h3>
|
||||||
|
<a href="/add_playlist_items?list=<%= plid %>"><i class="icon ion-md-add"></i></a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
<hr>
|
<hr>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %>
|
||||||
|
<script>
|
||||||
|
var playlist_data = {
|
||||||
|
csrf_token: '<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script src="/js/playlist_widget.js"></script>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<% videos.each_slice(4) do |slice| %>
|
<% videos.each_slice(4) do |slice| %>
|
||||||
<% slice.each do |item| %>
|
<% slice.each do |item| %>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<% content_for "header" do %>
|
<% content_for "header" do %>
|
||||||
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
||||||
<title>
|
<title>
|
||||||
<% if config.default_home != "Popular" %>
|
<% if env.get("preferences").as(Preferences).default_home != "Popular" %>
|
||||||
<%= translate(locale, "Popular") %> - Invidious
|
<%= translate(locale, "Popular") %> - Invidious
|
||||||
<% else %>
|
<% else %>
|
||||||
Invidious
|
Invidious
|
||||||
|
@ -135,6 +135,32 @@ function update_value(element) {
|
|||||||
<input name="thin_mode" id="thin_mode" type="checkbox" <% if preferences.thin_mode %>checked<% end %>>
|
<input name="thin_mode" id="thin_mode" type="checkbox" <% if preferences.thin_mode %>checked<% end %>>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<% if env.get?("user") %>
|
||||||
|
<% feed_options = {"", "Popular", "Top", "Trending", "Subscriptions", "Playlists"} %>
|
||||||
|
<% else %>
|
||||||
|
<% feed_options = {"", "Popular", "Top", "Trending"} %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="default_home"><%= translate(locale, "Default homepage: ") %></label>
|
||||||
|
<select name="default_home" id="default_home">
|
||||||
|
<% feed_options.each do |option| %>
|
||||||
|
<option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="feed_menu"><%= translate(locale, "Feed menu: ") %></label>
|
||||||
|
<% (feed_options.size - 1).times do |index| %>
|
||||||
|
<select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]">
|
||||||
|
<% feed_options.each do |option| %>
|
||||||
|
<option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% if env.get? "user" %>
|
<% if env.get? "user" %>
|
||||||
<legend><%= translate(locale, "Subscription preferences") %></legend>
|
<legend><%= translate(locale, "Subscription preferences") %></legend>
|
||||||
|
|
||||||
@ -143,11 +169,6 @@ function update_value(element) {
|
|||||||
<input name="annotations_subscribed" id="annotations_subscribed" type="checkbox" <% if preferences.annotations_subscribed %>checked<% end %>>
|
<input name="annotations_subscribed" id="annotations_subscribed" type="checkbox" <% if preferences.annotations_subscribed %>checked<% end %>>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
|
||||||
<label for="redirect_feed"><%= translate(locale, "Redirect homepage to feed: ") %></label>
|
|
||||||
<input name="redirect_feed" id="redirect_feed" type="checkbox" <% if preferences.redirect_feed %>checked<% end %>>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="max_results"><%= translate(locale, "Number of videos shown in feed: ") %></label>
|
<label for="max_results"><%= translate(locale, "Number of videos shown in feed: ") %></label>
|
||||||
<input name="max_results" id="max_results" type="number" value="<%= preferences.max_results %>">
|
<input name="max_results" id="max_results" type="number" value="<%= preferences.max_results %>">
|
||||||
@ -193,20 +214,20 @@ function update_value(element) {
|
|||||||
<legend><%= translate(locale, "Administrator preferences") %></legend>
|
<legend><%= translate(locale, "Administrator preferences") %></legend>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="default_home"><%= translate(locale, "Default homepage: ") %></label>
|
<label for="admin_default_home"><%= translate(locale, "Default homepage: ") %></label>
|
||||||
<select name="default_home" id="default_home">
|
<select name="admin_default_home" id="admin_default_home">
|
||||||
<% {"Popular", "Top", "Trending", "Subscriptions"}.each do |option| %>
|
<% feed_options.each do |option| %>
|
||||||
<option value="<%= option %>" <% if config.default_home == option %> selected <% end %>><%= translate(locale, option) %></option>
|
<option value="<%= option %>" <% if CONFIG.default_user_preferences.default_home == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="feed_menu"><%= translate(locale, "Feed menu: ") %></label>
|
<label for="admin_feed_menu"><%= translate(locale, "Feed menu: ") %></label>
|
||||||
<% 4.times do |index| %>
|
<% (feed_options.size - 1).times do |index| %>
|
||||||
<select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]">
|
<select name="admin_feed_menu[<%= index %>]" id="admin_feed_menu[<%= index %>]">
|
||||||
<% {"", "Popular", "Top", "Trending", "Subscriptions"}.each do |option| %>
|
<% feed_options.each do |option| %>
|
||||||
<option value="<%= option %>" <% if config.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option) %></option>
|
<option value="<%= option %>" <% if CONFIG.default_user_preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
<% end %>
|
<% end %>
|
||||||
@ -261,6 +282,10 @@ function update_value(element) {
|
|||||||
<a href="/token_manager"><%= translate(locale, "Manage tokens") %></a>
|
<a href="/token_manager"><%= translate(locale, "Manage tokens") %></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<a href="/view_all_playlists"><%= translate(locale, "View all playlists") %></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
<li>a randomly generated token for providing an RSS feed of a user's subscriptions</li>
|
<li>a randomly generated token for providing an RSS feed of a user's subscriptions</li>
|
||||||
<li>a list of video IDs identifying watched videos</li>
|
<li>a list of video IDs identifying watched videos</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>The above list reflects <a href="https://github.com/omarroth/invidious/blob/master/src/invidious/users.cr#L14-L51">this code</a>.</p>
|
|
||||||
<p>Users can clear their watch history using the <a href="/clear_watch_history">clear watch history</a> page.</p>
|
<p>Users can clear their watch history using the <a href="/clear_watch_history">clear watch history</a> page.</p>
|
||||||
<p>If a user is logged in with a Google account, no password will ever be stored. This website uses the session token provided by Google to identify a user, but does not store the information required to make requests on a user's behalf without their knowledge or consent.</p>
|
<p>If a user is logged in with a Google account, no password will ever be stored. This website uses the session token provided by Google to identify a user, but does not store the information required to make requests on a user's behalf without their knowledge or consent.</p>
|
||||||
|
|
||||||
|
@ -117,17 +117,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-1-3">
|
<div class="pure-u-1 pure-u-md-1-3">
|
||||||
<i class="icon ion-logo-bitcoin"></i>
|
<i class="icon ion-logo-bitcoin"></i>
|
||||||
BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY
|
BTC: <a href="bitcoin:356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY">356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-1-3">
|
<div class="pure-u-1 pure-u-md-1-3">
|
||||||
<i class="icon ion-logo-bitcoin"></i>
|
<i class="icon ion-logo-bitcoin"></i>
|
||||||
BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk
|
BCH: <a href="bitcoincash:qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk">qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-1-3">
|
<div class="pure-u-1 pure-u-md-1-3">
|
||||||
<i class="icon ion-logo-usd"></i>
|
<i class="icon ion-logo-usd"></i>
|
||||||
<a href="https://liberapay.com/omarroth">Liberapay</a>
|
<a href="https://liberapay.com/omarroth">Liberapay</a>
|
||||||
/
|
|
||||||
<a href="https://patreon.com/omarroth">Patreon</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-1-3">
|
<div class="pure-u-1 pure-u-md-1-3">
|
||||||
<i class="icon ion-logo-javascript"></i>
|
<i class="icon ion-logo-javascript"></i>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<% content_for "header" do %>
|
<% content_for "header" do %>
|
||||||
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
||||||
<title>
|
<title>
|
||||||
<% if config.default_home != "Top" %>
|
<% if env.get("preferences").as(Preferences).default_home != "Top" %>
|
||||||
<%= translate(locale, "Top") %> - Invidious
|
<%= translate(locale, "Top") %> - Invidious
|
||||||
<% else %>
|
<% else %>
|
||||||
Invidious
|
Invidious
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<% content_for "header" do %>
|
<% content_for "header" do %>
|
||||||
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
||||||
<title>
|
<title>
|
||||||
<% if config.default_home != "Trending" %>
|
<% if env.get("preferences").as(Preferences).default_home != "Trending" %>
|
||||||
<%= translate(locale, "Trending") %> - Invidious
|
<%= translate(locale, "Trending") %> - Invidious
|
||||||
<% else %>
|
<% else %>
|
||||||
Invidious
|
Invidious
|
||||||
|
24
src/invidious/views/view_all_playlists.ecr
Normal file
24
src/invidious/views/view_all_playlists.ecr
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "Playlists") %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= rendered "components/feed_menu" %>
|
||||||
|
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-2-3">
|
||||||
|
<h3><%= translate(locale, "`x` playlists", %(<span id="count">#{items.size}</span>)) %></h3>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3" style="text-align:right">
|
||||||
|
<h3>
|
||||||
|
<a href="/create_playlist?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Create playlist") %></a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<% items.each_slice(4) do |slice| %>
|
||||||
|
<% slice.each do |item| %>
|
||||||
|
<%= rendered "components/item" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
@ -29,6 +29,7 @@
|
|||||||
<script>
|
<script>
|
||||||
var video_data = {
|
var video_data = {
|
||||||
id: '<%= video.id %>',
|
id: '<%= video.id %>',
|
||||||
|
index: '<%= continuation %>',
|
||||||
plid: '<%= plid %>',
|
plid: '<%= plid %>',
|
||||||
length_seconds: <%= video.length_seconds.to_f %>,
|
length_seconds: <%= video.length_seconds.to_f %>,
|
||||||
play_next: <%= !rvs.empty? && !plid && params.continue %>,
|
play_next: <%= !rvs.empty? && !plid && params.continue %>,
|
||||||
|
Loading…
Reference in New Issue
Block a user