mirror of
https://github.com/iv-org/invidious.git
synced 2024-12-23 00:29:53 +05:30
Merge pull request #673 from omarroth/add-playlists
Add initial support for custom playlists
This commit is contained in:
commit
5c69110658
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
|
}
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
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'
|
||||||
|
);
|
@ -126,7 +126,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": "عرض الملاحظات فى الفيديو",
|
||||||
|
@ -126,7 +126,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",
|
||||||
|
@ -141,7 +141,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",
|
||||||
@ -74,11 +78,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? ",
|
||||||
@ -141,7 +145,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",
|
||||||
@ -162,7 +176,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",
|
||||||
@ -359,7 +376,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",
|
||||||
|
@ -126,7 +126,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",
|
||||||
|
@ -126,7 +126,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",
|
||||||
|
@ -126,7 +126,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": "",
|
||||||
|
@ -126,7 +126,17 @@
|
|||||||
"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": "",
|
||||||
"Unlisted": "Non répertoriée",
|
"Unlisted": "Non répertoriée",
|
||||||
|
"Private": "",
|
||||||
|
"View all playlists": "",
|
||||||
|
"Updated `x` ago": "",
|
||||||
|
"Delete playlist `x`?": "",
|
||||||
|
"Delete playlist": "",
|
||||||
|
"Create playlist": "",
|
||||||
|
"Title": "",
|
||||||
|
"Playlist privacy": "",
|
||||||
|
"Editing playlist `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",
|
||||||
|
@ -126,7 +126,17 @@
|
|||||||
"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",
|
||||||
|
@ -141,7 +141,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",
|
||||||
|
@ -126,7 +126,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",
|
||||||
|
@ -126,7 +126,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",
|
||||||
|
@ -126,7 +126,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": "",
|
||||||
|
@ -126,7 +126,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": "Показать аннотации",
|
||||||
|
@ -126,7 +126,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": "Показати анотації",
|
||||||
|
@ -126,7 +126,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": "显示注释",
|
||||||
|
882
src/invidious.cr
882
src/invidious.cr
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
@ -598,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")
|
||||||
|
@ -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>
|
||||||
|
@ -431,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
|
||||||
|
@ -282,6 +282,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
|
||||||
|
@ -1274,6 +1274,20 @@ 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 { |q| (q == "true" || q == "1").to_unsafe }
|
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
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 %>
|
@ -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 %>,
|
||||||
|
@ -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| %>
|
||||||
|
@ -261,6 +261,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>
|
||||||
|
22
src/invidious/views/view_all_playlists.ecr
Normal file
22
src/invidious/views/view_all_playlists.ecr
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "Playlists") %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<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