mirror of
https://github.com/iv-org/invidious.git
synced 2025-03-13 09:26:35 -04:00
Merge 0cf71b20a1bfddfea95136ae5438c49b22069f47 into adcdb8cb92bbf61bac46102eff026593d0bc87b0
This commit is contained in:
commit
f1113decde
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@
|
|||||||
/invidious
|
/invidious
|
||||||
/sentry
|
/sentry
|
||||||
/config/config.yml
|
/config/config.yml
|
||||||
|
.DS_Store
|
||||||
|
@ -814,5 +814,67 @@ h1, h2, h3, h4, h5, p,
|
|||||||
}
|
}
|
||||||
|
|
||||||
#download_widget {
|
#download_widget {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Compilations
|
||||||
|
*/
|
||||||
|
|
||||||
|
input.compilation-video-timestamp {
|
||||||
|
width: 50px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.compilation-video-panel {
|
||||||
|
display:flex;
|
||||||
|
justify-content:flex-start;
|
||||||
|
width:calc(100% - 20px);
|
||||||
|
height:100px;
|
||||||
|
border:2px solid #ccc;
|
||||||
|
margin: 10px;
|
||||||
|
/*background: #d9d9d9;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
div.compilation-order-swap-arrows {
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
justify-content:space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.compilation-video-swap-arrow {
|
||||||
|
border: solid black;
|
||||||
|
width:20px;
|
||||||
|
height:50%;
|
||||||
|
background-color: beige;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.compilation-video-input-panel {
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.compilation-video-title {
|
||||||
|
display:flex;
|
||||||
|
justify-content:flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.compilation-video-title {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.compilation-video-timestamp-set {
|
||||||
|
display:flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.compilation-video-thumbnail {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
63
assets/js/compilation_widget.js
Normal file
63
assets/js/compilation_widget.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
'use strict';
|
||||||
|
var compilation_data = JSON.parse(document.getElementById('compilation_data').textContent);
|
||||||
|
var payload = 'csrf_token=' + compilation_data.csrf_token;
|
||||||
|
|
||||||
|
function add_compilation_video(target) {
|
||||||
|
var select = target.parentNode.children[0].children[1];
|
||||||
|
var option = select.children[select.selectedIndex];
|
||||||
|
|
||||||
|
var url = '/compilation_ajax?action_add_video=1&redirect=false' +
|
||||||
|
'&video_id=' + target.getAttribute('data-id') +
|
||||||
|
'&compilation_id=' + option.getAttribute('data-compid');
|
||||||
|
|
||||||
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
|
on200: function (response) {
|
||||||
|
option.textContent = '✓' + option.textContent;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_compilation_item(target) {
|
||||||
|
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
|
tile.style.display = 'none';
|
||||||
|
|
||||||
|
var url = '/compilation_ajax?action_add_video=1&redirect=false' +
|
||||||
|
'&video_id=' + target.getAttribute('data-id') +
|
||||||
|
'&compilation_id=' + target.getAttribute('data-compid');
|
||||||
|
|
||||||
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
|
onNon200: function (xhr) {
|
||||||
|
tile.style.display = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_compilation_item(target) {
|
||||||
|
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
|
tile.style.display = 'none';
|
||||||
|
|
||||||
|
var url = '/compilation_ajax?action_remove_video=1&redirect=false' +
|
||||||
|
'&set_video_id=' + target.getAttribute('data-index') +
|
||||||
|
'&compilation_id=' + target.getAttribute('data-compid');
|
||||||
|
|
||||||
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
|
onNon200: function (xhr) {
|
||||||
|
tile.style.display = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function move_compilation_video_before(target) {
|
||||||
|
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
|
tile.style.display = 'none';
|
||||||
|
|
||||||
|
var url = '/compilation_ajax?action_move_video_before=1&redirect=false' +
|
||||||
|
'&set_video_id=' + target.getAttribute('data-index') +
|
||||||
|
'&compilation_id=' + target.getAttribute('data-compid');
|
||||||
|
|
||||||
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
|
onNon200: function (xhr) {
|
||||||
|
tile.style.display = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -1,6 +1,39 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
var video_data = JSON.parse(document.getElementById('video_data').textContent);
|
var video_data = JSON.parse(document.getElementById('video_data').textContent);
|
||||||
|
|
||||||
|
function get_compilation(compid) {
|
||||||
|
var compid_url;
|
||||||
|
compid_url = '/api/v1/compilations/' + compid +
|
||||||
|
'?index=' + video_data.index +
|
||||||
|
'&continuation' + video_data.id +
|
||||||
|
'&format=html&hl=' + video_data.preferences.locale;
|
||||||
|
|
||||||
|
helpers.xhr('GET', compid_url, {retries: 5, entity_name: 'compilation'}, {
|
||||||
|
on200: function (response) {
|
||||||
|
if (!response.nextVideo)
|
||||||
|
return;
|
||||||
|
|
||||||
|
player.on('ended', function () {
|
||||||
|
var url = new URL('https://example.com/embed/' + response.nextVideo);
|
||||||
|
|
||||||
|
url.searchParams.set('comp', compid);
|
||||||
|
if (!compid.startsWith('RD'))
|
||||||
|
url.searchParams.set('index', response.index);
|
||||||
|
if (video_data.params.autoplay || video_data.params.continue_autoplay)
|
||||||
|
url.searchParams.set('autoplay', '1');
|
||||||
|
if (video_data.params.listen !== video_data.preferences.listen)
|
||||||
|
url.searchParams.set('listen', video_data.params.listen);
|
||||||
|
if (video_data.params.speed !== video_data.preferences.speed)
|
||||||
|
url.searchParams.set('speed', video_data.params.speed);
|
||||||
|
if (video_data.params.local !== video_data.preferences.local)
|
||||||
|
url.searchParams.set('local', video_data.params.local);
|
||||||
|
|
||||||
|
location.assign(url.pathname + url.search);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function get_playlist(plid) {
|
function get_playlist(plid) {
|
||||||
var plid_url;
|
var plid_url;
|
||||||
if (plid.startsWith('RD')) {
|
if (plid.startsWith('RD')) {
|
||||||
@ -43,6 +76,8 @@ function get_playlist(plid) {
|
|||||||
addEventListener('load', function (e) {
|
addEventListener('load', function (e) {
|
||||||
if (video_data.plid) {
|
if (video_data.plid) {
|
||||||
get_playlist(video_data.plid);
|
get_playlist(video_data.plid);
|
||||||
|
} else if (video_data.compid) {
|
||||||
|
get_compilation(video_data.compid)
|
||||||
} 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());
|
||||||
|
@ -60,12 +60,21 @@
|
|||||||
document.querySelectorAll('[data-onclick="add_playlist_video"]').forEach(function (el) {
|
document.querySelectorAll('[data-onclick="add_playlist_video"]').forEach(function (el) {
|
||||||
el.onclick = function () { add_playlist_video(el); };
|
el.onclick = function () { add_playlist_video(el); };
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll('[data-onclick="add_compilation_video"]').forEach(function (el) {
|
||||||
|
el.onclick = function () { add_compilation_video(el); };
|
||||||
|
});
|
||||||
document.querySelectorAll('[data-onclick="add_playlist_item"]').forEach(function (el) {
|
document.querySelectorAll('[data-onclick="add_playlist_item"]').forEach(function (el) {
|
||||||
el.onclick = function () { add_playlist_item(el); };
|
el.onclick = function () { add_playlist_item(el); };
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll('[data-onclick="add_compilation_item"]').forEach(function (el) {
|
||||||
|
el.onclick = function () { add_compilation_item(el); };
|
||||||
|
});
|
||||||
document.querySelectorAll('[data-onclick="remove_playlist_item"]').forEach(function (el) {
|
document.querySelectorAll('[data-onclick="remove_playlist_item"]').forEach(function (el) {
|
||||||
el.onclick = function () { remove_playlist_item(el); };
|
el.onclick = function () { remove_playlist_item(el); };
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll('[data-onclick="remove_compilation_item"]').forEach(function (el) {
|
||||||
|
el.onclick = function () { remove_compilation_item(el); };
|
||||||
|
});
|
||||||
document.querySelectorAll('[data-onclick="revoke_token"]').forEach(function (el) {
|
document.querySelectorAll('[data-onclick="revoke_token"]').forEach(function (el) {
|
||||||
el.onclick = function () { revoke_token(el); };
|
el.onclick = function () { revoke_token(el); };
|
||||||
});
|
});
|
||||||
|
@ -260,8 +260,13 @@ if (video_data.params.video_start > 0 || video_data.params.video_end > 0) {
|
|||||||
|
|
||||||
player.markers({
|
player.markers({
|
||||||
onMarkerReached: function (marker) {
|
onMarkerReached: function (marker) {
|
||||||
if (marker.text === 'End')
|
if (marker.text === 'End') {
|
||||||
player.loop() ? player.markers.prev('Start') : player.pause();
|
if (video_data.ending_timestamp_seconds) {
|
||||||
|
player.currentTime(player.duration());
|
||||||
|
} else {
|
||||||
|
player.loop() ? player.markers.prev('Start') : player.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
markers: markers
|
markers: markers
|
||||||
});
|
});
|
||||||
|
@ -50,6 +50,59 @@ function continue_autoplay(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_compilation(compid) {
|
||||||
|
var compilation = document.getElementById('compilation');
|
||||||
|
|
||||||
|
compilation.innerHTML = spinnerHTMLwithHR;
|
||||||
|
|
||||||
|
var compid_url;
|
||||||
|
compid_url = '/api/v1/compilations/' + compid +
|
||||||
|
'?index=' + video_data.index +
|
||||||
|
'&continuation=' + video_data.id +
|
||||||
|
'&format=html&hl=' + video_data.preferences.locale;
|
||||||
|
|
||||||
|
helpers.xhr('GET', compid_url, {retries: 5, entity_name: 'compilation'}, {
|
||||||
|
on200: function (response) {
|
||||||
|
compilation.innerHTML = response.compilationHtml;
|
||||||
|
|
||||||
|
if (!response.nextVideo) return;
|
||||||
|
|
||||||
|
var nextVideo = document.getElementById(response.nextVideo);
|
||||||
|
nextVideo.parentNode.parentNode.scrollTop = nextVideo.offsetTop;
|
||||||
|
|
||||||
|
player.on('ended', function () {
|
||||||
|
var url = new URL('https://example.com/watch?v=' + response.nextVideo);
|
||||||
|
|
||||||
|
url.searchParams.set('comp', compid);
|
||||||
|
if (!compid.startsWith('RD'))
|
||||||
|
url.searchParams.set('index', response.index);
|
||||||
|
if (video_data.params.autoplay || video_data.params.continue_autoplay)
|
||||||
|
url.searchParams.set('autoplay', '1');
|
||||||
|
if (video_data.params.listen !== video_data.preferences.listen)
|
||||||
|
url.searchParams.set('listen', video_data.params.listen);
|
||||||
|
if (video_data.params.speed !== video_data.preferences.speed)
|
||||||
|
url.searchParams.set('speed', video_data.params.speed);
|
||||||
|
if (video_data.params.local !== video_data.preferences.local)
|
||||||
|
url.searchParams.set('local', video_data.params.local);
|
||||||
|
url.searchParams.set('t',video_data.starting_timestamp_seconds);
|
||||||
|
url.searchParams.set('end',video_data.ending_timestamp_seconds);
|
||||||
|
|
||||||
|
location.assign(url.pathname + url.search);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onNon200: function (xhr) {
|
||||||
|
compilation.innerHTML = '';
|
||||||
|
document.getElementById('continue').style.display = '';
|
||||||
|
},
|
||||||
|
onError: function (xhr) {
|
||||||
|
compilation.innerHTML = spinnerHTMLwithHR;
|
||||||
|
},
|
||||||
|
onTimeout: function (xhr) {
|
||||||
|
compilation.innerHTML = spinnerHTMLwithHR;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function get_playlist(plid) {
|
function get_playlist(plid) {
|
||||||
var playlist = document.getElementById('playlist');
|
var playlist = document.getElementById('playlist');
|
||||||
|
|
||||||
@ -181,7 +234,8 @@ if (video_data.play_next) {
|
|||||||
addEventListener('load', function (e) {
|
addEventListener('load', function (e) {
|
||||||
if (video_data.plid)
|
if (video_data.plid)
|
||||||
get_playlist(video_data.plid);
|
get_playlist(video_data.plid);
|
||||||
|
if (video_data.compid)
|
||||||
|
get_compilation(video_data.compid);
|
||||||
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') {
|
||||||
|
@ -521,6 +521,14 @@ hmac_key: "CHANGE_ME!!"
|
|||||||
##
|
##
|
||||||
#playlist_length_limit: 500
|
#playlist_length_limit: 500
|
||||||
|
|
||||||
|
##
|
||||||
|
## Maximum custom compilation length limit.
|
||||||
|
##
|
||||||
|
## Accepted values: Integer
|
||||||
|
## Default: 500
|
||||||
|
##
|
||||||
|
#compilation_length_limit: 500
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
#
|
#
|
||||||
# Default user preferences
|
# Default user preferences
|
||||||
|
21
config/sql/compilation_videos.sql
Normal file
21
config/sql/compilation_videos.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-- Table: public.compilation_videos
|
||||||
|
|
||||||
|
-- DROP TABLE public.compilation_videos;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.compilation_videos
|
||||||
|
(
|
||||||
|
title text,
|
||||||
|
id text,
|
||||||
|
author text,
|
||||||
|
ucid text,
|
||||||
|
length_seconds integer,
|
||||||
|
starting_timestamp_seconds integer,
|
||||||
|
ending_timestamp_seconds integer,
|
||||||
|
published timestamptz,
|
||||||
|
compid text references compilations(id),
|
||||||
|
index int8,
|
||||||
|
order_index integer,
|
||||||
|
PRIMARY KEY (index,compid)
|
||||||
|
);
|
||||||
|
|
||||||
|
GRANT ALL ON TABLE public.compilation_videos TO current_user;
|
31
config/sql/compilations.sql
Normal file
31
config/sql/compilations.sql
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
-- Type: public.compilation_privacy
|
||||||
|
|
||||||
|
-- DROP TYPE public.compilation_privacy;
|
||||||
|
|
||||||
|
CREATE TYPE public.compilation_privacy AS ENUM
|
||||||
|
(
|
||||||
|
'Unlisted',
|
||||||
|
'Private'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table: public.compilations
|
||||||
|
|
||||||
|
-- DROP TABLE public.compilations;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.compilations
|
||||||
|
(
|
||||||
|
title text,
|
||||||
|
id text primary key,
|
||||||
|
author text,
|
||||||
|
description text,
|
||||||
|
video_count integer,
|
||||||
|
created timestamptz,
|
||||||
|
updated timestamptz,
|
||||||
|
privacy compilation_privacy,
|
||||||
|
index int8[],
|
||||||
|
first_video_id text,
|
||||||
|
first_video_starting_timestamp_seconds integer,
|
||||||
|
first_video_ending_timestamp_seconds integer
|
||||||
|
);
|
||||||
|
|
||||||
|
GRANT ALL ON public.compilations TO current_user;
|
@ -10,3 +10,5 @@ psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/nonces.sql
|
|||||||
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotations.sql
|
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotations.sql
|
||||||
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlists.sql
|
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlists.sql
|
||||||
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlist_videos.sql
|
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlist_videos.sql
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/compilations.sql
|
||||||
|
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/compilation_videos.sql
|
||||||
|
@ -182,10 +182,15 @@
|
|||||||
"Delete playlist `x`?": "Delete playlist `x`?",
|
"Delete playlist `x`?": "Delete playlist `x`?",
|
||||||
"Delete playlist": "Delete playlist",
|
"Delete playlist": "Delete playlist",
|
||||||
"Create playlist": "Create playlist",
|
"Create playlist": "Create playlist",
|
||||||
|
"Create compilation": "Create compilation",
|
||||||
"Title": "Title",
|
"Title": "Title",
|
||||||
"Playlist privacy": "Playlist privacy",
|
"Playlist privacy": "Playlist privacy",
|
||||||
|
"Compilation privacy": "Compilation privacy",
|
||||||
"Editing playlist `x`": "Editing playlist `x`",
|
"Editing playlist `x`": "Editing playlist `x`",
|
||||||
|
"Editing compilation `x`": "Editing compilation `x`",
|
||||||
"playlist_button_add_items": "Add videos",
|
"playlist_button_add_items": "Add videos",
|
||||||
|
"compilation_button_add_items": "Add videos",
|
||||||
|
"compilation_button_play": "Play",
|
||||||
"Show more": "Show more",
|
"Show more": "Show more",
|
||||||
"Show less": "Show less",
|
"Show less": "Show less",
|
||||||
"Watch on YouTube": "Watch on YouTube",
|
"Watch on YouTube": "Watch on YouTube",
|
||||||
@ -247,6 +252,7 @@
|
|||||||
"Not a playlist.": "Not a playlist.",
|
"Not a playlist.": "Not a playlist.",
|
||||||
"Playlist does not exist.": "Playlist does not exist.",
|
"Playlist does not exist.": "Playlist does not exist.",
|
||||||
"Could not pull trending pages.": "Could not pull trending pages.",
|
"Could not pull trending pages.": "Could not pull trending pages.",
|
||||||
|
"Compilation does not exist.": "Compilation does not exist.",
|
||||||
"Hidden field \"challenge\" is a required field": "Hidden field \"challenge\" is a required field",
|
"Hidden field \"challenge\" is a required field": "Hidden field \"challenge\" is a required field",
|
||||||
"Hidden field \"token\" is a required field": "Hidden field \"token\" is a required field",
|
"Hidden field \"token\" is a required field": "Hidden field \"token\" is a required field",
|
||||||
"Erroneous challenge": "Erroneous challenge",
|
"Erroneous challenge": "Erroneous challenge",
|
||||||
@ -422,6 +428,7 @@
|
|||||||
"Audio mode": "Audio mode",
|
"Audio mode": "Audio mode",
|
||||||
"Video mode": "Video mode",
|
"Video mode": "Video mode",
|
||||||
"Playlists": "Playlists",
|
"Playlists": "Playlists",
|
||||||
|
"Compilations": "Compilations",
|
||||||
"search_filters_title": "Filters",
|
"search_filters_title": "Filters",
|
||||||
"search_filters_date_label": "Upload date",
|
"search_filters_date_label": "Upload date",
|
||||||
"search_filters_date_option_none": "Any date",
|
"search_filters_date_option_none": "Any date",
|
||||||
@ -478,6 +485,7 @@
|
|||||||
"download_subtitles": "Subtitles - `x` (.vtt)",
|
"download_subtitles": "Subtitles - `x` (.vtt)",
|
||||||
"user_created_playlists": "`x` created playlists",
|
"user_created_playlists": "`x` created playlists",
|
||||||
"user_saved_playlists": "`x` saved playlists",
|
"user_saved_playlists": "`x` saved playlists",
|
||||||
|
"user_created_compilations": "`x` created compilations",
|
||||||
"Video unavailable": "Video unavailable",
|
"Video unavailable": "Video unavailable",
|
||||||
"preferences_save_player_pos_label": "Save playback position: ",
|
"preferences_save_player_pos_label": "Save playback position: ",
|
||||||
"crash_page_you_found_a_bug": "It looks like you found a bug in Invidious!",
|
"crash_page_you_found_a_bug": "It looks like you found a bug in Invidious!",
|
||||||
|
@ -129,7 +129,7 @@ Kemal.config.extra_options do |parser|
|
|||||||
puts SOFTWARE.to_pretty_json
|
puts SOFTWARE.to_pretty_json
|
||||||
exit
|
exit
|
||||||
end
|
end
|
||||||
parser.on("--migrate", "Run any migrations (beta, use at your own risk!!") do
|
parser.on("--migrate", "Run any migrations (beta, use at your own risk!!)") do
|
||||||
Invidious::Database::Migrator.new(PG_DB).migrate
|
Invidious::Database::Migrator.new(PG_DB).migrate
|
||||||
exit
|
exit
|
||||||
end
|
end
|
||||||
|
487
src/invidious/compilations.cr
Normal file
487
src/invidious/compilations.cr
Normal file
@ -0,0 +1,487 @@
|
|||||||
|
struct CompilationVideo
|
||||||
|
include DB::Serializable
|
||||||
|
|
||||||
|
property title : String
|
||||||
|
property id : String
|
||||||
|
property author : String
|
||||||
|
property ucid : String
|
||||||
|
property length_seconds : Int32
|
||||||
|
property starting_timestamp_seconds : Int32
|
||||||
|
property ending_timestamp_seconds : Int32
|
||||||
|
property published : Time
|
||||||
|
property compid : String
|
||||||
|
property index : Int64
|
||||||
|
property order_index : Int32
|
||||||
|
|
||||||
|
def to_xml(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("orderIndex") { xml.text self.order_index }
|
||||||
|
xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?v=#{self.id}")
|
||||||
|
|
||||||
|
xml.element("author") do
|
||||||
|
xml.element("name") { xml.text self.author }
|
||||||
|
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" }
|
||||||
|
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(_xml : Nil = nil)
|
||||||
|
XML.build { |xml| to_xml(xml) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(json : JSON::Builder, index : Int32? = nil)
|
||||||
|
json.object do
|
||||||
|
json.field "title", self.title
|
||||||
|
json.field "videoId", self.id
|
||||||
|
|
||||||
|
json.field "author", self.author
|
||||||
|
json.field "authorId", self.ucid
|
||||||
|
json.field "authorUrl", "/channel/#{self.ucid}"
|
||||||
|
|
||||||
|
json.field "videoThumbnails" do
|
||||||
|
Invidious::JSONify::APIv1.thumbnails(json, self.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
if index
|
||||||
|
json.field "index", index
|
||||||
|
json.field "indexId", self.index.to_u64.to_s(16).upcase
|
||||||
|
else
|
||||||
|
json.field "index", self.index
|
||||||
|
end
|
||||||
|
|
||||||
|
json.field "orderIndex", self.order_index
|
||||||
|
json.field "lengthSeconds", self.length_seconds
|
||||||
|
json.field "startingTimestampSeconds", self.starting_timestamp_seconds
|
||||||
|
json.field "endingTimestampSeconds", self.ending_timestamp_seconds
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(_json : Nil, index : Int32? = nil)
|
||||||
|
JSON.build { |json| to_json(json, index: index) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
struct Compilation
|
||||||
|
include DB::Serializable
|
||||||
|
|
||||||
|
property title : String
|
||||||
|
property id : String
|
||||||
|
property author : String
|
||||||
|
property author_thumbnail : String
|
||||||
|
property ucid : String
|
||||||
|
property description : String
|
||||||
|
property description_html : String
|
||||||
|
property video_count : Int32
|
||||||
|
property views : Int64
|
||||||
|
property updated : Time
|
||||||
|
property thumbnail : String?
|
||||||
|
property first_video_id : String
|
||||||
|
property first_video_starting_timestamp_seconds : Int32
|
||||||
|
property first_video_ending_timestamp_seconds : Int32
|
||||||
|
|
||||||
|
def to_json(offset, json : JSON::Builder, video_id : String? = nil)
|
||||||
|
json.object do
|
||||||
|
json.field "type", "compilation"
|
||||||
|
json.field "title", self.title
|
||||||
|
json.field "compilationId", self.id
|
||||||
|
json.field "compilationThumbnail", 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", self.description
|
||||||
|
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 "videos" do
|
||||||
|
json.array do
|
||||||
|
videos = get_compilation_videos(self, offset: offset, video_id: video_id)
|
||||||
|
videos.each do |video|
|
||||||
|
video.to_json(json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(offset, _json : Nil = nil, video_id : String? = nil)
|
||||||
|
JSON.build do |json|
|
||||||
|
to_json(offset, json, video_id: video_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def privacy
|
||||||
|
CompilationPrivacy::Unlisted
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
enum CompilationPrivacy
|
||||||
|
Unlisted = 0
|
||||||
|
Private = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
struct InvidiousCompilation
|
||||||
|
include DB::Serializable
|
||||||
|
|
||||||
|
property title : String
|
||||||
|
property id : String
|
||||||
|
property author : String
|
||||||
|
property description : String = ""
|
||||||
|
property video_count : Int32
|
||||||
|
property created : Time
|
||||||
|
property updated : Time
|
||||||
|
|
||||||
|
@[DB::Field(converter: InvidiousCompilation::CompilationPrivacyConverter)]
|
||||||
|
property privacy : CompilationPrivacy = CompilationPrivacy::Private
|
||||||
|
property index : Array(Int64)
|
||||||
|
property first_video_id : String
|
||||||
|
property first_video_starting_timestamp_seconds : Int32
|
||||||
|
property first_video_ending_timestamp_seconds : Int32
|
||||||
|
|
||||||
|
@[DB::Field(ignore: true)]
|
||||||
|
property thumbnail_id : String?
|
||||||
|
|
||||||
|
module CompilationPrivacyConverter
|
||||||
|
def self.from_rs(rs)
|
||||||
|
return CompilationPrivacy.parse(String.new(rs.read(Slice(UInt8))))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(offset, json : JSON::Builder, video_id : String? = nil)
|
||||||
|
json.object do
|
||||||
|
json.field "type", "invidiousCompilation"
|
||||||
|
json.field "title", self.title
|
||||||
|
json.field "compilationId", self.id
|
||||||
|
|
||||||
|
json.field "author", self.author
|
||||||
|
json.field "authorId", self.ucid
|
||||||
|
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 "videos" do
|
||||||
|
json.array do
|
||||||
|
if (!offset || offset == 0) && !video_id.nil?
|
||||||
|
index = Invidious::Database::CompilationVideos.select_index(self.id, video_id)
|
||||||
|
offset = self.index.index(index) || 0
|
||||||
|
end
|
||||||
|
|
||||||
|
videos = get_compilation_videos(self, offset: offset, video_id: video_id)
|
||||||
|
videos.each_with_index do |video, idx|
|
||||||
|
video.to_json(json, offset + idx)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(offset, _json : Nil = nil, video_id : String? = nil)
|
||||||
|
JSON.build do |json|
|
||||||
|
to_json(offset, json, video_id: video_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def thumbnail
|
||||||
|
# TODO: Get compilation thumbnail from compilation data rather than first video
|
||||||
|
@thumbnail_id ||= Invidious::Database::CompilationVideos.select_one_id(self.id, self.index) || "-----------"
|
||||||
|
"/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)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_compilation(title, privacy, user)
|
||||||
|
compid = "IVCMP#{Random::Secure.urlsafe_base64(24)[0, 31]}"
|
||||||
|
|
||||||
|
compilation = InvidiousCompilation.new({
|
||||||
|
title: title.byte_slice(0, 150),
|
||||||
|
id: compid,
|
||||||
|
author: user.email,
|
||||||
|
description: "", # Max 5000 characters
|
||||||
|
video_count: 0,
|
||||||
|
created: Time.utc,
|
||||||
|
updated: Time.utc,
|
||||||
|
privacy: privacy,
|
||||||
|
index: [] of Int64,
|
||||||
|
first_video_id: "",
|
||||||
|
first_video_starting_timestamp_seconds: 0,
|
||||||
|
first_video_ending_timestamp_seconds: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
Invidious::Database::Compilations.insert(compilation)
|
||||||
|
|
||||||
|
return compilation
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscribe_compilation(user, compilation)
|
||||||
|
compilation = InvidiousCompilation.new({
|
||||||
|
title: compilation.title.byte_slice(0, 150),
|
||||||
|
id: compilation.id,
|
||||||
|
author: user.email,
|
||||||
|
description: "", # Max 5000 characters
|
||||||
|
video_count: compilation.video_count,
|
||||||
|
created: Time.utc,
|
||||||
|
updated: compilation.updated,
|
||||||
|
privacy: CompilationPrivacy::Private,
|
||||||
|
index: [] of Int64,
|
||||||
|
first_video_id: "",
|
||||||
|
first_video_starting_timestamp_seconds: 0,
|
||||||
|
first_video_ending_timestamp_seconds: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
Invidious::Database::Compilations.insert(compilation)
|
||||||
|
|
||||||
|
return compilation
|
||||||
|
end
|
||||||
|
|
||||||
|
def produce_compilation_continuation(id, index)
|
||||||
|
if id.starts_with? "UC"
|
||||||
|
id = "UU" + id.lchop("UC")
|
||||||
|
end
|
||||||
|
compid = "VL" + id
|
||||||
|
|
||||||
|
# Emulate a "request counter" increment, to make perfectly valid
|
||||||
|
# ctokens, even if at the time of writing, it's ignored by youtube.
|
||||||
|
request_count = (index / 100).to_i64 || 1_i64
|
||||||
|
|
||||||
|
data = {"1:varint" => index.to_i64}
|
||||||
|
.try { |i| Protodec::Any.cast_json(i) }
|
||||||
|
.try { |i| Protodec::Any.from_json(i) }
|
||||||
|
.try { |i| Base64.urlsafe_encode(i, padding: false) }
|
||||||
|
|
||||||
|
object = {
|
||||||
|
"80226972:embedded" => {
|
||||||
|
"2:string" => plid,
|
||||||
|
"3:base64" => {
|
||||||
|
"1:varint" => request_count,
|
||||||
|
"15:string" => "PT:#{data}",
|
||||||
|
"104:embedded" => {"1:0:varint" => 0_i64},
|
||||||
|
},
|
||||||
|
"35:string" => id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||||
|
.try { |i| Protodec::Any.from_json(i) }
|
||||||
|
.try { |i| Base64.urlsafe_encode(i) }
|
||||||
|
.try { |i| URI.encode_www_form(i) }
|
||||||
|
|
||||||
|
return continuation
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_compilation(compid : String)
|
||||||
|
if compilation = Invidious::Database::Compilations.select(id: compid)
|
||||||
|
update_first_video_params(compid)
|
||||||
|
return compilation
|
||||||
|
else
|
||||||
|
raise NotFoundException.new("Compilation does not exist.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_first_video_params(compid : String)
|
||||||
|
if compilation = Invidious::Database::Compilations.select(id: compid)
|
||||||
|
compilation_index_array = compilation.index
|
||||||
|
if (compilation_index_array.size > 0)
|
||||||
|
first_index = compilation_index_array[0]
|
||||||
|
first_id = Invidious::Database::CompilationVideos.select_id_from_index(first_index)
|
||||||
|
if !first_id.nil?
|
||||||
|
timestamps = Invidious::Database::CompilationVideos.select_timestamps(compid, first_id)
|
||||||
|
if (!timestamps.nil?)
|
||||||
|
starting_timestamp_seconds = timestamps[0]
|
||||||
|
ending_timestamp_seconds = timestamps[1]
|
||||||
|
Invidious::Database::Compilations.update_first_video_params(compid, first_id, starting_timestamp_seconds, ending_timestamp_seconds)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise NotFoundException.new("Compilation does not exist.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_compilation_videos(compilation : InvidiousCompilation | Compilation, offset : Int32, video_id = nil)
|
||||||
|
# Show empty compilation if requested page is out of range
|
||||||
|
# (e.g, when a new compilation has been created, offset will be negative)
|
||||||
|
if offset >= compilation.video_count || offset < 0
|
||||||
|
return [] of CompilationVideo
|
||||||
|
end
|
||||||
|
|
||||||
|
if compilation.is_a? InvidiousCompilation
|
||||||
|
Invidious::Database::CompilationVideos.select(compilation.id, compilation.index, offset, limit: 100)
|
||||||
|
else
|
||||||
|
if video_id
|
||||||
|
initial_data = YoutubeAPI.next({
|
||||||
|
"videoId" => video_id,
|
||||||
|
"compilationId" => compilation.id,
|
||||||
|
})
|
||||||
|
offset = initial_data.dig?("contents", "twoColumnWatchNextResults", "compilation", "compilation", "currentIndex").try &.as_i || offset
|
||||||
|
end
|
||||||
|
|
||||||
|
videos = [] of CompilationVideo
|
||||||
|
|
||||||
|
until videos.size >= 200 || videos.size == compilation.video_count || offset >= compilation.video_count
|
||||||
|
# 100 videos per request
|
||||||
|
ctoken = produce_compilation_continuation(compilation.id, offset)
|
||||||
|
initial_data = YoutubeAPI.browse(ctoken)
|
||||||
|
videos += extract_compilation_videos(initial_data)
|
||||||
|
|
||||||
|
offset += 100
|
||||||
|
end
|
||||||
|
|
||||||
|
return videos
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_compilation_videos(initial_data : Hash(String, JSON::Any))
|
||||||
|
videos = [] of CompilationVideo
|
||||||
|
|
||||||
|
if initial_data["contents"]?
|
||||||
|
tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]
|
||||||
|
tabs_renderer = tabs.as_a.select(&.["tabRenderer"]["selected"]?.try &.as_bool)[0]["tabRenderer"]
|
||||||
|
|
||||||
|
# Watch out the two versions, with and without "s"
|
||||||
|
if tabs_renderer["contents"]? || tabs_renderer["content"]?
|
||||||
|
# Initial compilation data
|
||||||
|
tabs_contents = tabs_renderer.["contents"]? || tabs_renderer.["content"]
|
||||||
|
|
||||||
|
list_renderer = tabs_contents.["sectionListRenderer"]["contents"][0]
|
||||||
|
item_renderer = list_renderer.["itemSectionRenderer"]["contents"][0]
|
||||||
|
contents = item_renderer.["compilationVideoListRenderer"]["contents"].as_a
|
||||||
|
else
|
||||||
|
# Continuation data
|
||||||
|
contents = initial_data["onResponseReceivedActions"][0]?
|
||||||
|
.try &.["appendContinuationItemsAction"]["continuationItems"].as_a
|
||||||
|
end
|
||||||
|
else
|
||||||
|
contents = initial_data["response"]?.try &.["continuationContents"]["compilationVideoListContinuation"]["contents"].as_a
|
||||||
|
end
|
||||||
|
|
||||||
|
contents.try &.each do |item|
|
||||||
|
if i = item["compilationVideoRenderer"]?
|
||||||
|
video_id = i["navigationEndpoint"]["watchEndpoint"]["videoId"].as_s
|
||||||
|
compid = i["navigationEndpoint"]["watchEndpoint"]["compilationId"].as_s
|
||||||
|
index = i["navigationEndpoint"]["watchEndpoint"]["index"].as_i64
|
||||||
|
|
||||||
|
title = i["title"].try { |t| t["simpleText"]? || t["runs"]?.try &.[0]["text"]? }.try &.as_s || ""
|
||||||
|
author = i["shortBylineText"]?.try &.["runs"][0]["text"].as_s || ""
|
||||||
|
ucid = i["shortBylineText"]?.try &.["runs"][0]["navigationEndpoint"]["browseEndpoint"]["browseId"].as_s || ""
|
||||||
|
length_seconds = i["lengthSeconds"]?.try &.as_s.to_i
|
||||||
|
live = false
|
||||||
|
|
||||||
|
if !length_seconds
|
||||||
|
live = true
|
||||||
|
length_seconds = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
videos << CompilationVideo.new({
|
||||||
|
title: title,
|
||||||
|
id: video_id,
|
||||||
|
author: author,
|
||||||
|
ucid: ucid,
|
||||||
|
length_seconds: length_seconds,
|
||||||
|
starting_timestamp_seconds: starting_timestamp_seconds,
|
||||||
|
ending_timestamp_seconds: ending_timestamp_seconds,
|
||||||
|
published: Time.utc,
|
||||||
|
compid: compid,
|
||||||
|
index: index,
|
||||||
|
order_index: order_index,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return videos
|
||||||
|
end
|
||||||
|
|
||||||
|
def template_compilation(compilation)
|
||||||
|
html = <<-END_HTML
|
||||||
|
<h3>
|
||||||
|
<a href="/compilation?comp=#{compilation["compilationId"]}">
|
||||||
|
#{compilation["title"]}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
<div class="pure-menu pure-menu-scrollable compilation-restricted">
|
||||||
|
<ol class="pure-menu-list">
|
||||||
|
END_HTML
|
||||||
|
|
||||||
|
compilation["videos"].as_a.each do |video|
|
||||||
|
html += <<-END_HTML
|
||||||
|
<li class="pure-menu-item" id="#{video["videoId"]}">
|
||||||
|
<a href="/watch?v=#{video["videoId"]}&comp=#{compilation["compilationId"]}&index=#{video["index"]}">
|
||||||
|
<div class="thumbnail">
|
||||||
|
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
|
||||||
|
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
|
||||||
|
</div>
|
||||||
|
<p style="width:100%">#{video["title"]}</p>
|
||||||
|
<p>
|
||||||
|
<b style="width:100%">#{video["author"]}</b>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
END_HTML
|
||||||
|
end
|
||||||
|
|
||||||
|
html += <<-END_HTML
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
END_HTML
|
||||||
|
|
||||||
|
html
|
||||||
|
end
|
@ -38,7 +38,7 @@ struct ConfigPreferences
|
|||||||
property quality : String = "hd720"
|
property quality : String = "hd720"
|
||||||
property quality_dash : String = "auto"
|
property quality_dash : String = "auto"
|
||||||
property default_home : String? = "Popular"
|
property default_home : String? = "Popular"
|
||||||
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"]
|
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists", "Compilations"]
|
||||||
property automatic_instance_redirect : Bool = false
|
property automatic_instance_redirect : Bool = false
|
||||||
property region : String = "US"
|
property region : String = "US"
|
||||||
property related_videos : Bool = true
|
property related_videos : Bool = true
|
||||||
@ -167,6 +167,9 @@ class Config
|
|||||||
# Playlist length limit
|
# Playlist length limit
|
||||||
property playlist_length_limit : Int32 = 500
|
property playlist_length_limit : Int32 = 500
|
||||||
|
|
||||||
|
# Compilation length limit
|
||||||
|
property compilation_length_limit : Int32 = 500
|
||||||
|
|
||||||
def disabled?(option)
|
def disabled?(option)
|
||||||
case disabled = CONFIG.disable_proxy
|
case disabled = CONFIG.disable_proxy
|
||||||
when Bool
|
when Bool
|
||||||
|
@ -10,11 +10,14 @@ module Invidious::Database
|
|||||||
def check_integrity(cfg)
|
def check_integrity(cfg)
|
||||||
return if !cfg.check_tables
|
return if !cfg.check_tables
|
||||||
Invidious::Database.check_enum("privacy", PlaylistPrivacy)
|
Invidious::Database.check_enum("privacy", PlaylistPrivacy)
|
||||||
|
Invidious::Database.check_enum("compilation_privacy", CompilationPrivacy)
|
||||||
|
|
||||||
Invidious::Database.check_table("channels", InvidiousChannel)
|
Invidious::Database.check_table("channels", InvidiousChannel)
|
||||||
Invidious::Database.check_table("channel_videos", ChannelVideo)
|
Invidious::Database.check_table("channel_videos", ChannelVideo)
|
||||||
Invidious::Database.check_table("playlists", InvidiousPlaylist)
|
Invidious::Database.check_table("playlists", InvidiousPlaylist)
|
||||||
Invidious::Database.check_table("playlist_videos", PlaylistVideo)
|
Invidious::Database.check_table("playlist_videos", PlaylistVideo)
|
||||||
|
Invidious::Database.check_table("compilations", InvidiousCompilation)
|
||||||
|
Invidious::Database.check_table("compilation_videos", CompilationVideo)
|
||||||
Invidious::Database.check_table("nonces", Nonce)
|
Invidious::Database.check_table("nonces", Nonce)
|
||||||
Invidious::Database.check_table("session_ids", SessionId)
|
Invidious::Database.check_table("session_ids", SessionId)
|
||||||
Invidious::Database.check_table("users", User)
|
Invidious::Database.check_table("users", User)
|
||||||
|
359
src/invidious/database/compilations.cr
Normal file
359
src/invidious/database/compilations.cr
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
require "./base.cr"
|
||||||
|
|
||||||
|
#
|
||||||
|
# This module contains functions related to the "compilations" table.
|
||||||
|
#
|
||||||
|
module Invidious::Database::Compilations
|
||||||
|
extend self
|
||||||
|
|
||||||
|
# -------------------
|
||||||
|
# Insert / delete
|
||||||
|
# -------------------
|
||||||
|
|
||||||
|
def insert(compilation : InvidiousCompilation)
|
||||||
|
compilation_array = compilation.to_a
|
||||||
|
|
||||||
|
request = <<-SQL
|
||||||
|
INSERT INTO compilations
|
||||||
|
VALUES (#{arg_array(compilation_array)})
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, args: compilation_array)
|
||||||
|
end
|
||||||
|
|
||||||
|
# deletes the given compilation and connected compilation videos
|
||||||
|
def delete(id : String)
|
||||||
|
CompilationVideos.delete_by_compilation(id)
|
||||||
|
request = <<-SQL
|
||||||
|
DELETE FROM compilations *
|
||||||
|
WHERE id = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# -------------------
|
||||||
|
# Update
|
||||||
|
# -------------------
|
||||||
|
|
||||||
|
def update(id : String, title : String, privacy, description, updated)
|
||||||
|
request = <<-SQL
|
||||||
|
UPDATE compilations
|
||||||
|
SET title = $1, privacy = $2, description = $3, updated = $4
|
||||||
|
WHERE id = $5
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, title, privacy, description, updated, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_description(id : String, description)
|
||||||
|
request = <<-SQL
|
||||||
|
UPDATE compilations
|
||||||
|
SET description = $1
|
||||||
|
WHERE id = $2
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, description, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_video_added(id : String, index : String | Int64)
|
||||||
|
request = <<-SQL
|
||||||
|
UPDATE compilations
|
||||||
|
SET index = array_append(index, $1),
|
||||||
|
video_count = cardinality(index) + 1,
|
||||||
|
updated = now()
|
||||||
|
WHERE id = $2
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, index, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_video_removed(id : String, index : String | Int64)
|
||||||
|
request = <<-SQL
|
||||||
|
UPDATE compilations
|
||||||
|
SET index = array_remove(index, $1),
|
||||||
|
video_count = cardinality(index) - 1,
|
||||||
|
updated = now()
|
||||||
|
WHERE id = $2
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, index, id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def move_video_position(id : String, index : Array(Int64))
|
||||||
|
request = <<-SQL
|
||||||
|
UPDATE compilations
|
||||||
|
SET index = $2
|
||||||
|
WHERE id = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, id, index)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_first_video_params(id : String, first_video_id : String, starting_timestamp_seconds : Int32, ending_timestamp_seconds : Int32)
|
||||||
|
request = <<-SQL
|
||||||
|
UPDATE compilations
|
||||||
|
SET first_video_id = $2,
|
||||||
|
first_video_starting_timestamp_seconds = $3,
|
||||||
|
first_video_ending_timestamp_seconds = $4
|
||||||
|
WHERE id = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, id, first_video_id, starting_timestamp_seconds, ending_timestamp_seconds)
|
||||||
|
end
|
||||||
|
|
||||||
|
# -------------------
|
||||||
|
# Select
|
||||||
|
# -------------------
|
||||||
|
|
||||||
|
def select(*, id : String) : InvidiousCompilation?
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT * FROM compilations
|
||||||
|
WHERE id = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_one?(request, id, as: InvidiousCompilation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_all(*, author : String) : Array(InvidiousCompilation)
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT * FROM compilations
|
||||||
|
WHERE author = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_all(request, author, as: InvidiousCompilation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_index_array(id : String)
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT index FROM compilations
|
||||||
|
WHERE id = $1
|
||||||
|
LIMIT 1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.query_one?(request, id, as: Array(Int64))
|
||||||
|
end
|
||||||
|
|
||||||
|
# -------------------
|
||||||
|
# Select (filtered)
|
||||||
|
# -------------------
|
||||||
|
|
||||||
|
def select_like_iv(email : String) : Array(InvidiousCompilation)
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT * FROM compilations
|
||||||
|
WHERE author = $1 AND id LIKE 'IV%'
|
||||||
|
ORDER BY created
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.query_all(request, email, as: InvidiousCompilation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_not_like_iv(email : String) : Array(InvidiousCompilation)
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT * FROM compilations
|
||||||
|
WHERE author = $1 AND id NOT LIKE 'IV%'
|
||||||
|
ORDER BY created
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.query_all(request, email, as: InvidiousCompilation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_user_created_compilations(email : String) : Array({String, String})
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT id,title FROM compilations
|
||||||
|
WHERE author = $1 AND id LIKE 'IV%'
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.query_all(request, email, as: {String, String})
|
||||||
|
end
|
||||||
|
|
||||||
|
# -------------------
|
||||||
|
# Misc checks
|
||||||
|
# -------------------
|
||||||
|
|
||||||
|
# Check if given compilation ID exists
|
||||||
|
def exists?(id : String) : Bool
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT id FROM compilations
|
||||||
|
WHERE id = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_one?(request, id, as: String).nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Count how many compilations a user has created.
|
||||||
|
def count_owned_by(author : String) : Int64
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT count(*) FROM compilations
|
||||||
|
WHERE author = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_one?(request, author, as: Int64) || 0_i64
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# This module contains functions related to the "compilation_videos" table.
|
||||||
|
#
|
||||||
|
module Invidious::Database::CompilationVideos
|
||||||
|
extend self
|
||||||
|
|
||||||
|
private alias VideoIndex = Int64 | Array(Int64)
|
||||||
|
|
||||||
|
# -------------------
|
||||||
|
# Insert / Delete
|
||||||
|
# -------------------
|
||||||
|
|
||||||
|
def insert(video : CompilationVideo)
|
||||||
|
video_array = video.to_a
|
||||||
|
|
||||||
|
request = <<-SQL
|
||||||
|
INSERT INTO compilation_videos
|
||||||
|
VALUES (#{arg_array(video_array)})
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, args: video_array)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(index)
|
||||||
|
request = <<-SQL
|
||||||
|
DELETE FROM compilation_videos *
|
||||||
|
WHERE index = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, index)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_by_compilation(compid : String)
|
||||||
|
request = <<-SQL
|
||||||
|
DELETE FROM compilation_videos *
|
||||||
|
WHERE compid = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, compid)
|
||||||
|
end
|
||||||
|
|
||||||
|
# -------------------
|
||||||
|
# Select
|
||||||
|
# -------------------
|
||||||
|
|
||||||
|
def select(compid : String, index : VideoIndex, offset, limit = 100) : Array(CompilationVideo)
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT * FROM compilation_videos
|
||||||
|
WHERE compid = $1
|
||||||
|
ORDER BY array_position($2, index)
|
||||||
|
LIMIT $3
|
||||||
|
OFFSET $4
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_all(request, compid, index, limit, offset, as: CompilationVideo)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_video(compid : String, index : VideoIndex, video_index, offset, limit = 100) : Array(CompilationVideo)
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT * FROM compilation_videos
|
||||||
|
WHERE compid = $1 AND index = $3
|
||||||
|
ORDER BY array_position($2, index)
|
||||||
|
LIMIT $5
|
||||||
|
OFFSET $4
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_all(request, compid, index, video_index, offset, limit, as: CompilationVideo)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_timestamps(compid : String, vid : String)
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT starting_timestamp_seconds,ending_timestamp_seconds FROM compilation_videos
|
||||||
|
WHERE compid = $1 AND id = $2
|
||||||
|
LIMIT 1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_one?(request, compid, vid, as: {Int32, Int32})
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_id_from_order_index(order_index : Int32)
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT id FROM compilation_videos
|
||||||
|
WHERE order_index = $1
|
||||||
|
LIMIT 1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_one?(request, order_index, as: String)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_id_from_index(index : Int64)
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT id FROM compilation_videos
|
||||||
|
WHERE index = $1
|
||||||
|
LIMIT 1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_one?(request, index, as: String)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_index_from_order_index(order_index : Int32)
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT index FROM compilation_videos
|
||||||
|
WHERE order_index = $1
|
||||||
|
LIMIT 1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_one?(request, order_index, as: VideoIndex)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_index(compid : String, vid : String) : Int64?
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT index FROM compilation_videos
|
||||||
|
WHERE compid = $1 AND id = $2
|
||||||
|
LIMIT 1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_one?(request, compid, vid, as: Int64)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_one_id(compid : String, index : VideoIndex) : String?
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT id FROM compilation_videos
|
||||||
|
WHERE compid = $1
|
||||||
|
ORDER BY array_position($2, index)
|
||||||
|
LIMIT 1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_one?(request, compid, index, as: String)
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_ids(compid : String, index : VideoIndex, limit = 500) : Array(String)
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT id FROM compilation_videos
|
||||||
|
WHERE compid = $1
|
||||||
|
ORDER BY array_position($2, index)
|
||||||
|
LIMIT $3
|
||||||
|
SQL
|
||||||
|
|
||||||
|
return PG_DB.query_all(request, compid, index, limit, as: String)
|
||||||
|
end
|
||||||
|
|
||||||
|
# -------------------
|
||||||
|
# Update
|
||||||
|
# -------------------
|
||||||
|
|
||||||
|
def update_start_timestamp(id : String, starting_timestamp_seconds : Int32)
|
||||||
|
request = <<-SQL
|
||||||
|
UPDATE compilation_videos
|
||||||
|
SET starting_timestamp_seconds = $2
|
||||||
|
WHERE id = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, id, starting_timestamp_seconds)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_end_timestamp(id : String, ending_timestamp_seconds : Int32)
|
||||||
|
request = <<-SQL
|
||||||
|
UPDATE compilation_videos
|
||||||
|
SET ending_timestamp_seconds = $2
|
||||||
|
WHERE id = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
|
PG_DB.exec(request, id, ending_timestamp_seconds)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,52 @@
|
|||||||
|
module Invidious::Database::Migrations
|
||||||
|
class CreateCompilationsTable < Migration
|
||||||
|
version 11
|
||||||
|
|
||||||
|
def up(conn : DB::Connection)
|
||||||
|
if !compilation_privacy_type_exists?(conn)
|
||||||
|
conn.exec <<-SQL
|
||||||
|
CREATE TYPE public.compilation_privacy AS ENUM
|
||||||
|
(
|
||||||
|
'Unlisted',
|
||||||
|
'Private'
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
conn.exec <<-SQL
|
||||||
|
CREATE TABLE IF NOT EXISTS public.compilations
|
||||||
|
(
|
||||||
|
title text,
|
||||||
|
id text primary key,
|
||||||
|
author text,
|
||||||
|
description text,
|
||||||
|
video_count integer,
|
||||||
|
created timestamptz,
|
||||||
|
updated timestamptz,
|
||||||
|
privacy compilation_privacy,
|
||||||
|
index int8[],
|
||||||
|
first_video_id text,
|
||||||
|
first_video_starting_timestamp_seconds integer,
|
||||||
|
first_video_ending_timestamp_seconds integer
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
|
||||||
|
conn.exec <<-SQL
|
||||||
|
GRANT ALL ON public.compilations TO current_user;
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
private def compilation_privacy_type_exists?(conn : DB::Connection) : Bool
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT 1 AS one
|
||||||
|
FROM pg_type
|
||||||
|
INNER JOIN pg_namespace ON pg_namespace.oid = pg_type.typnamespace
|
||||||
|
WHERE pg_namespace.nspname = 'public'
|
||||||
|
AND pg_type.typname = 'compilation_privacy'
|
||||||
|
LIMIT 1;
|
||||||
|
SQL
|
||||||
|
|
||||||
|
!conn.query_one?(request, as: Int32).nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,30 @@
|
|||||||
|
module Invidious::Database::Migrations
|
||||||
|
class CreateCompilationVideosTable < Migration
|
||||||
|
version 12
|
||||||
|
|
||||||
|
def up(conn : DB::Connection)
|
||||||
|
conn.exec <<-SQL
|
||||||
|
CREATE TABLE IF NOT EXISTS public.compilation_videos
|
||||||
|
(
|
||||||
|
title text,
|
||||||
|
id text,
|
||||||
|
author text,
|
||||||
|
ucid text,
|
||||||
|
length_seconds integer,
|
||||||
|
starting_timestamp_seconds integer,
|
||||||
|
ending_timestamp_seconds integer,
|
||||||
|
published timestamptz,
|
||||||
|
compid text references compilations(id),
|
||||||
|
index int8,
|
||||||
|
order_index integer,
|
||||||
|
PRIMARY KEY (index,compid)
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
|
||||||
|
conn.exec <<-SQL
|
||||||
|
GRANT ALL ON TABLE public.playlist_videos TO current_user;
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -91,7 +91,7 @@ module Invidious::Database::Playlists
|
|||||||
end
|
end
|
||||||
|
|
||||||
# -------------------
|
# -------------------
|
||||||
# Salect
|
# Select
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
def select(*, id : String) : InvidiousPlaylist?
|
def select(*, id : String) : InvidiousPlaylist?
|
||||||
@ -113,7 +113,7 @@ module Invidious::Database::Playlists
|
|||||||
end
|
end
|
||||||
|
|
||||||
# -------------------
|
# -------------------
|
||||||
# Salect (filtered)
|
# Select (filtered)
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
def select_like_iv(email : String) : Array(InvidiousPlaylist)
|
def select_like_iv(email : String) : Array(InvidiousPlaylist)
|
||||||
@ -160,7 +160,7 @@ module Invidious::Database::Playlists
|
|||||||
return PG_DB.query_one?(request, id, as: String).nil?
|
return PG_DB.query_one?(request, id, as: String).nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Count how many playlist a user has created.
|
# Count how many playlists a user has created.
|
||||||
def count_owned_by(author : String) : Int64
|
def count_owned_by(author : String) : Int64
|
||||||
request = <<-SQL
|
request = <<-SQL
|
||||||
SELECT count(*) FROM playlists
|
SELECT count(*) FROM playlists
|
||||||
@ -213,7 +213,7 @@ module Invidious::Database::PlaylistVideos
|
|||||||
end
|
end
|
||||||
|
|
||||||
# -------------------
|
# -------------------
|
||||||
# Salect
|
# Select
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
def select(plid : String, index : VideoIndex, offset, limit = 100) : Array(PlaylistVideo)
|
def select(plid : String, index : VideoIndex, offset, limit = 100) : Array(PlaylistVideo)
|
||||||
|
@ -35,8 +35,10 @@ def decode_length_seconds(string)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def recode_length_seconds(time)
|
def recode_length_seconds(time)
|
||||||
if time <= 0
|
if time < 0
|
||||||
return ""
|
return ""
|
||||||
|
elsif time == 0
|
||||||
|
return "0:00"
|
||||||
else
|
else
|
||||||
time = time.seconds
|
time = time.seconds
|
||||||
text = "#{time.minutes.to_s.rjust(2, '0')}:#{time.seconds.to_s.rjust(2, '0')}"
|
text = "#{time.minutes.to_s.rjust(2, '0')}:#{time.seconds.to_s.rjust(2, '0')}"
|
||||||
|
@ -192,6 +192,21 @@ module Invidious::Routes::API::V1::Authenticated
|
|||||||
env.response.status_code = 204
|
env.response.status_code = 204
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.list_compilations(env)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
|
||||||
|
compilations = Invidious::Database::Compilations.select_all(author: user.email)
|
||||||
|
|
||||||
|
JSON.build do |json|
|
||||||
|
json.array do
|
||||||
|
compilations.each do |compilation|
|
||||||
|
compilation.to_json(0, json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.list_playlists(env)
|
def self.list_playlists(env)
|
||||||
env.response.content_type = "application/json"
|
env.response.content_type = "application/json"
|
||||||
user = env.get("user").as(User)
|
user = env.get("user").as(User)
|
||||||
@ -207,6 +222,32 @@ module Invidious::Routes::API::V1::Authenticated
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.create_compilation(env)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user").as(User)
|
||||||
|
|
||||||
|
title = env.params.json["title"]?.try &.as(String).delete("<>").byte_slice(0, 150)
|
||||||
|
if !title
|
||||||
|
return error_json(400, "Invalid title.")
|
||||||
|
end
|
||||||
|
privacy = env.params.json["privacy"]?.try { |p| CompilationPrivacy.parse(p.as(String).downcase) }
|
||||||
|
if !privacy
|
||||||
|
return error_json(400, "Invalid privacy setting.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if Invidious::Database::Compilations.count_owned_by(user.email) >= 100
|
||||||
|
return error_json(400, "User cannot have more than 100 compilations.")
|
||||||
|
end
|
||||||
|
|
||||||
|
compilation = create_compilation(title, privacy, user)
|
||||||
|
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/compilations/#{compilation.id}"
|
||||||
|
env.response.status_code = 201
|
||||||
|
{
|
||||||
|
"title" => title,
|
||||||
|
"compilationId" => compilation.id,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
def self.create_playlist(env)
|
def self.create_playlist(env)
|
||||||
env.response.content_type = "application/json"
|
env.response.content_type = "application/json"
|
||||||
user = env.get("user").as(User)
|
user = env.get("user").as(User)
|
||||||
|
@ -26,6 +26,72 @@ module Invidious::Routes::API::V1::Misc
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.get_compilation(env : HTTP::Server::Context)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
compid = env.params.url["compid"]
|
||||||
|
offset = env.params.query["index"]?.try &.to_i?
|
||||||
|
offset ||= env.params.query["page"]?.try &.to_i?.try { |page| (page - 1) * 100 }
|
||||||
|
offset ||= 0
|
||||||
|
|
||||||
|
video_id = env.params.query["continuation"]?
|
||||||
|
|
||||||
|
format = env.params.query["format"]?
|
||||||
|
format ||= "json"
|
||||||
|
|
||||||
|
if compid.starts_with? "RD"
|
||||||
|
return env.redirect "/api/v1/mixes/#{compid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
compilation = get_compilation(compid)
|
||||||
|
rescue ex : InfoException
|
||||||
|
return error_json(404, ex)
|
||||||
|
rescue ex
|
||||||
|
return error_json(404, "Compilation does not exist.")
|
||||||
|
end
|
||||||
|
|
||||||
|
user = env.get?("user").try &.as(User)
|
||||||
|
if !compilation || compilation.privacy.private? && compilation.author != user.try &.email
|
||||||
|
return error_json(404, "Compilation does not exist.")
|
||||||
|
end
|
||||||
|
|
||||||
|
# includes into the compilation a maximum of 50 videos, before the offset
|
||||||
|
if offset > 0
|
||||||
|
lookback = offset < 50 ? offset : 50
|
||||||
|
response = compilation.to_json(offset - lookback)
|
||||||
|
json_response = JSON.parse(response)
|
||||||
|
else
|
||||||
|
# Unless the continuation is really the offset 0, it becomes expensive.
|
||||||
|
# It happens when the offset is not set.
|
||||||
|
# First we find the actual offset, and then we lookback
|
||||||
|
# it shouldn't happen often though
|
||||||
|
|
||||||
|
lookback = 0
|
||||||
|
response = compilation.to_json(offset, video_id: video_id)
|
||||||
|
json_response = JSON.parse(response)
|
||||||
|
|
||||||
|
if json_response["videos"].as_a[0]["index"] != offset
|
||||||
|
offset = json_response["videos"].as_a[0]["index"].as_i
|
||||||
|
lookback = offset < 50 ? offset : 50
|
||||||
|
response = compilation.to_json(offset - lookback)
|
||||||
|
json_response = JSON.parse(response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if format == "html"
|
||||||
|
compilation_html = template_compilation(json_response)
|
||||||
|
index, next_video = json_response["videos"].as_a.skip(1 + lookback).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"compilationHtml" => compilation_html,
|
||||||
|
"index" => index,
|
||||||
|
"nextVideo" => next_video,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
# APIv1 currently uses the same logic for both
|
# APIv1 currently uses the same logic for both
|
||||||
# user playlists and Invidious playlists. This means that we can't
|
# user playlists and Invidious playlists. This means that we can't
|
||||||
# reasonably split them yet. This should be addressed in APIv2
|
# reasonably split them yet. This should be addressed in APIv2
|
||||||
|
@ -85,6 +85,7 @@ module Invidious::Routes::BeforeAll
|
|||||||
csrf_token = generate_response(sid, {
|
csrf_token = generate_response(sid, {
|
||||||
":authorize_token",
|
":authorize_token",
|
||||||
":playlist_ajax",
|
":playlist_ajax",
|
||||||
|
":compilation_ajax",
|
||||||
":signout",
|
":signout",
|
||||||
":subscription_ajax",
|
":subscription_ajax",
|
||||||
":token_ajax",
|
":token_ajax",
|
||||||
|
538
src/invidious/routes/compilations.cr
Normal file
538
src/invidious/routes/compilations.cr
Normal file
@ -0,0 +1,538 @@
|
|||||||
|
{% skip_file if flag?(:api_only) %}
|
||||||
|
|
||||||
|
module Invidious::Routes::Compilations
|
||||||
|
def self.new(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
return env.redirect "/" if user.nil?
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
csrf_token = generate_response(sid, {":create_compilation"}, HMAC_KEY)
|
||||||
|
|
||||||
|
templated "create_compilation"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.create(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
return env.redirect "/" if user.nil?
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
|
begin
|
||||||
|
validate_request(token, sid, env.request, HMAC_KEY, locale)
|
||||||
|
rescue ex
|
||||||
|
return error_template(400, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
title = env.params.body["title"]?.try &.as(String)
|
||||||
|
if !title || title.empty?
|
||||||
|
return error_template(400, "Title cannot be empty.")
|
||||||
|
end
|
||||||
|
|
||||||
|
privacy = CompilationPrivacy.parse?(env.params.body["privacy"]?.try &.as(String) || "")
|
||||||
|
if !privacy
|
||||||
|
return error_template(400, "Invalid privacy setting.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if Invidious::Database::Compilations.count_owned_by(user.email) >= 100
|
||||||
|
return error_template(400, "User cannot have more than 100 compilations.")
|
||||||
|
end
|
||||||
|
|
||||||
|
compilation = create_compilation(title, privacy, user)
|
||||||
|
|
||||||
|
env.redirect "/compilation?comp=#{compilation.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.delete_page(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
return env.redirect "/" if user.nil?
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
|
||||||
|
compid = env.params.query["comp"]?
|
||||||
|
if !compid || compid.empty?
|
||||||
|
return error_template(400, "A compilation ID is required")
|
||||||
|
end
|
||||||
|
|
||||||
|
compilation = Invidious::Database::Compilations.select(id: compid)
|
||||||
|
if !compilation || compilation.author != user.email
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
csrf_token = generate_response(sid, {":delete_compilation"}, HMAC_KEY)
|
||||||
|
|
||||||
|
templated "delete_compilation"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.delete(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
return env.redirect "/" if user.nil?
|
||||||
|
|
||||||
|
compid = env.params.query["comp"]?
|
||||||
|
return env.redirect referer if compid.nil?
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
|
begin
|
||||||
|
validate_request(token, sid, env.request, HMAC_KEY, locale)
|
||||||
|
rescue ex
|
||||||
|
return error_template(400, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
compilation = Invidious::Database::Compilations.select(id: compid)
|
||||||
|
if !compilation || compilation.author != user.email
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
Invidious::Database::Compilations.delete(compid)
|
||||||
|
|
||||||
|
env.redirect "/feed/compilations"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.edit(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
return env.redirect "/" if user.nil?
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
|
||||||
|
compid = env.params.query["comp"]?
|
||||||
|
if !compid || !compid.starts_with?("IVCMP")
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
page = env.params.query["page"]?.try &.to_i?
|
||||||
|
page ||= 1
|
||||||
|
|
||||||
|
compilation = Invidious::Database::Compilations.select(id: compid)
|
||||||
|
if !compilation || compilation.author != user.email
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
videos = get_compilation_videos(compilation, offset: (page - 1) * 100)
|
||||||
|
rescue ex
|
||||||
|
videos = [] of CompilationVideo
|
||||||
|
end
|
||||||
|
|
||||||
|
csrf_token = generate_response(sid, {":edit_compilation"}, HMAC_KEY)
|
||||||
|
|
||||||
|
templated "edit_compilation"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.update(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
return env.redirect "/" if user.nil?
|
||||||
|
|
||||||
|
compid = env.params.query["comp"]?
|
||||||
|
return env.redirect referer if compid.nil?
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
|
begin
|
||||||
|
validate_request(token, sid, env.request, HMAC_KEY, locale)
|
||||||
|
rescue ex
|
||||||
|
return error_template(400, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
compilation = Invidious::Database::Compilations.select(id: compid)
|
||||||
|
if !compilation || compilation.author != user.email
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
title = env.params.body["title"]?.try &.delete("<>") || ""
|
||||||
|
privacy = CompilationPrivacy.parse(env.params.body["privacy"]? || "Unlisted")
|
||||||
|
description = env.params.body["description"]?.try &.delete("\r") || ""
|
||||||
|
|
||||||
|
if title != compilation.title ||
|
||||||
|
compilation != compilation.privacy ||
|
||||||
|
description != compilation.description
|
||||||
|
updated = Time.utc
|
||||||
|
else
|
||||||
|
updated = compilation.updated
|
||||||
|
end
|
||||||
|
|
||||||
|
Invidious::Database::Compilations.update(compid, title, privacy, description, updated)
|
||||||
|
|
||||||
|
env.redirect "/compilation?comp=#{compid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.adjust_timestamps(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
user = env.get("user")
|
||||||
|
sid = env.get? "sid"
|
||||||
|
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
return env.redirect "/" if user.nil?
|
||||||
|
|
||||||
|
compid = env.params.query["comp"]?
|
||||||
|
return env.redirect referer if compid.nil?
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
|
||||||
|
sid = sid.as(String)
|
||||||
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
|
begin
|
||||||
|
validate_request(token, sid, env.request, HMAC_KEY, locale)
|
||||||
|
rescue ex
|
||||||
|
return error_template(400, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
if !compid || compid.empty?
|
||||||
|
return error_json(400, "A compilation ID is required")
|
||||||
|
end
|
||||||
|
|
||||||
|
compilation = Invidious::Database::Compilations.select(id: compid)
|
||||||
|
if !compilation || compilation.author != user.email && compilation.privacy.private?
|
||||||
|
return error_json(404, "Compilation does not exist.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if compilation.author != user.email
|
||||||
|
return error_json(403, "Invalid user")
|
||||||
|
end
|
||||||
|
|
||||||
|
title = env.params.body["title"]?.try &.delete("<>") || ""
|
||||||
|
privacy = CompilationPrivacy.parse(env.params.body["privacy"]? || "Private")
|
||||||
|
|
||||||
|
Invidious::Database::Compilations.update(compid, title, privacy, "", compilation.updated)
|
||||||
|
|
||||||
|
(0..compilation.index.size - 1).each do |index|
|
||||||
|
compilation_video_index = compilation.index[index]
|
||||||
|
compilation_video = Invidious::Database::CompilationVideos.select_video(compid, compilation.index, compilation_video_index, 0, 1)
|
||||||
|
json_timestamp_query_start = compilation_video_index.to_s + "_start_timestamp"
|
||||||
|
start_timestamp = env.params.body[json_timestamp_query_start]?.try &.as(String).byte_slice(0, 8)
|
||||||
|
if !start_timestamp.nil? && !compilation_video[0].id.nil?
|
||||||
|
start_timestamp_seconds = decode_length_seconds(start_timestamp)
|
||||||
|
if !start_timestamp_seconds.nil?
|
||||||
|
if start_timestamp_seconds >= 0 && start_timestamp_seconds <= compilation_video[0].length_seconds
|
||||||
|
Invidious::Database::CompilationVideos.update_start_timestamp(compilation_video[0].id, start_timestamp_seconds.to_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
compilation_video = Invidious::Database::CompilationVideos.select_video(compid, compilation.index, compilation_video_index, 0, 1)
|
||||||
|
json_timestamp_query_end = compilation_video_index.to_s + "_end_timestamp"
|
||||||
|
end_timestamp = env.params.body[json_timestamp_query_end]?.try &.as(String).byte_slice(0, 8)
|
||||||
|
if !end_timestamp.nil? && !compilation_video[0].id.nil?
|
||||||
|
end_timestamp_seconds = decode_length_seconds(end_timestamp)
|
||||||
|
if !end_timestamp_seconds.nil?
|
||||||
|
if end_timestamp_seconds >= 0 && end_timestamp_seconds <= compilation_video[0].length_seconds && end_timestamp_seconds > compilation_video[0].starting_timestamp_seconds
|
||||||
|
Invidious::Database::CompilationVideos.update_end_timestamp(compilation_video[0].id, end_timestamp_seconds.to_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
update_first_video_params(compid)
|
||||||
|
|
||||||
|
env.redirect "/compilation?comp=#{compid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.add_compilation_items_page(env)
|
||||||
|
prefs = env.get("preferences").as(Preferences)
|
||||||
|
locale = prefs.locale
|
||||||
|
|
||||||
|
region = env.params.query["region"]? || prefs.region
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
return env.redirect "/" if user.nil?
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
|
||||||
|
compid = env.params.query["comp"]?
|
||||||
|
if !compid || !compid.starts_with?("IVCMP")
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
page = env.params.query["page"]?.try &.to_i?
|
||||||
|
page ||= 1
|
||||||
|
|
||||||
|
compilation = Invidious::Database::Compilations.select(id: compid)
|
||||||
|
if !compilation || compilation.author != user.email
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
query = Invidious::Search::Query.new(env.params.query, :compilation, region)
|
||||||
|
items = query.process.select(SearchVideo).map(&.as(SearchVideo))
|
||||||
|
rescue ex
|
||||||
|
items = [] of SearchVideo
|
||||||
|
end
|
||||||
|
|
||||||
|
query_encoded = URI.encode_www_form(query.try &.text || "", space_to_plus: true)
|
||||||
|
page_nav_html = Frontend::Pagination.nav_numeric(locale,
|
||||||
|
base_url: "/add_compilation_items?comp=#{compilation.id}&q=#{query_encoded}",
|
||||||
|
current_page: page,
|
||||||
|
show_next: (items.size >= 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
env.set "add_compilation_items", compid
|
||||||
|
templated "add_compilation_items"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.compilation_ajax(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env, "/")
|
||||||
|
|
||||||
|
redirect = env.params.query["redirect"]?
|
||||||
|
redirect ||= "true"
|
||||||
|
redirect = redirect == "true"
|
||||||
|
if !user
|
||||||
|
if redirect
|
||||||
|
return env.redirect referer
|
||||||
|
else
|
||||||
|
return error_json(403, "No such user")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
sid = sid.as(String)
|
||||||
|
token = env.params.body["csrf_token"]?
|
||||||
|
|
||||||
|
begin
|
||||||
|
validate_request(token, sid, env.request, HMAC_KEY, locale)
|
||||||
|
rescue ex
|
||||||
|
if redirect
|
||||||
|
return error_template(400, ex)
|
||||||
|
else
|
||||||
|
return error_json(400, ex)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if env.params.query["action_create_compilation"]?
|
||||||
|
action = "action_create_compilation"
|
||||||
|
elsif env.params.query["action_delete_compilation"]?
|
||||||
|
action = "action_delete_compilation"
|
||||||
|
elsif env.params.query["action_edit_compilation"]?
|
||||||
|
action = "action_edit_compilation"
|
||||||
|
elsif env.params.query["action_add_video"]?
|
||||||
|
action = "action_add_video"
|
||||||
|
video_id = env.params.query["video_id"]
|
||||||
|
elsif env.params.query["action_remove_video"]?
|
||||||
|
action = "action_remove_video"
|
||||||
|
elsif env.params.query["action_move_video_before"]?
|
||||||
|
action = "action_move_video_before"
|
||||||
|
elsif env.params.query["action_move_video_after"]?
|
||||||
|
action = "action_move_video_after"
|
||||||
|
else
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
compilation_id = env.params.query["compilation_id"]
|
||||||
|
compilation = get_compilation(compilation_id).as(InvidiousCompilation)
|
||||||
|
raise "Invalid user" if compilation.author != user.email
|
||||||
|
rescue ex : NotFoundException
|
||||||
|
return error_json(404, ex)
|
||||||
|
rescue ex
|
||||||
|
if redirect
|
||||||
|
return error_template(400, ex)
|
||||||
|
else
|
||||||
|
return error_json(400, ex)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
email = user.email
|
||||||
|
|
||||||
|
case action
|
||||||
|
when "action_edit_compilation"
|
||||||
|
# TODO: Compilation stub
|
||||||
|
|
||||||
|
when "action_add_video"
|
||||||
|
if compilation.index.size >= CONFIG.compilation_length_limit
|
||||||
|
if redirect
|
||||||
|
return error_template(400, "Compilation cannot have more than #{CONFIG.compilation_length_limit} videos")
|
||||||
|
else
|
||||||
|
return error_json(400, "Compilation cannot have more than #{CONFIG.compilation_length_limit} videos")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
video_id = env.params.query["video_id"]
|
||||||
|
|
||||||
|
begin
|
||||||
|
video = get_video(video_id)
|
||||||
|
rescue ex : NotFoundException
|
||||||
|
return error_json(404, ex)
|
||||||
|
rescue ex
|
||||||
|
if redirect
|
||||||
|
return error_template(500, ex)
|
||||||
|
else
|
||||||
|
return error_json(500, ex)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
compilation_video = CompilationVideo.new({
|
||||||
|
title: video.title,
|
||||||
|
id: video.id,
|
||||||
|
author: video.author,
|
||||||
|
ucid: video.ucid,
|
||||||
|
length_seconds: video.length_seconds,
|
||||||
|
starting_timestamp_seconds: 0,
|
||||||
|
ending_timestamp_seconds: video.length_seconds,
|
||||||
|
published: video.published,
|
||||||
|
compid: compilation_id,
|
||||||
|
live_now: video.live_now,
|
||||||
|
index: Random::Secure.rand(0_i64..Int64::MAX),
|
||||||
|
order_index: compilation.index.size,
|
||||||
|
})
|
||||||
|
|
||||||
|
Invidious::Database::CompilationVideos.insert(compilation_video)
|
||||||
|
Invidious::Database::Compilations.update_video_added(compilation_id, compilation_video.index)
|
||||||
|
update_first_video_params(compilation_id)
|
||||||
|
when "action_remove_video"
|
||||||
|
index = env.params.query["set_video_id"]
|
||||||
|
Invidious::Database::CompilationVideos.delete(index)
|
||||||
|
Invidious::Database::Compilations.update_video_removed(compilation_id, index)
|
||||||
|
update_first_video_params(compilation_id)
|
||||||
|
when "action_move_video_before"
|
||||||
|
# TODO: Compilation stub
|
||||||
|
video_index = env.params.query["video_index"]
|
||||||
|
begin
|
||||||
|
compilation_video = Invidious::Database::CompilationVideos.select_video(compilation_id, compilation.index, video_index, 0, 1)
|
||||||
|
compilation_index_array = compilation.index
|
||||||
|
rescue ex : NotFoundException
|
||||||
|
return error_json(404, ex)
|
||||||
|
rescue ex
|
||||||
|
if redirect
|
||||||
|
return error_template(500, ex)
|
||||||
|
else
|
||||||
|
return error_json(500, ex)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
compilation_index_array_position = compilation_index_array.index(compilation_video[0].index)
|
||||||
|
if !compilation_index_array_position.nil?
|
||||||
|
compilation_index_array.delete_at(compilation_index_array_position)
|
||||||
|
compilation_index_array.insert(compilation_index_array_position - 1, compilation_video[0].index)
|
||||||
|
Invidious::Database::Compilations.move_video_position(compilation_id, compilation_index_array)
|
||||||
|
end
|
||||||
|
update_first_video_params(compilation_id)
|
||||||
|
when "action_move_video_after"
|
||||||
|
# TODO: Compilation stub
|
||||||
|
video_index = env.params.query["video_index"]
|
||||||
|
begin
|
||||||
|
compilation_video = Invidious::Database::CompilationVideos.select_video(compilation_id, compilation.index, video_index, 0, 1)
|
||||||
|
compilation_index_array = compilation.index
|
||||||
|
rescue ex : NotFoundException
|
||||||
|
return error_json(404, ex)
|
||||||
|
rescue ex
|
||||||
|
if redirect
|
||||||
|
return error_template(500, ex)
|
||||||
|
else
|
||||||
|
return error_json(500, ex)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
compilation_index_array_position = compilation_index_array.index(compilation_video[0].index)
|
||||||
|
if !compilation_index_array_position.nil?
|
||||||
|
compilation_index_array.delete_at(compilation_index_array_position)
|
||||||
|
if (compilation_index_array_position == compilation_index_array.size)
|
||||||
|
compilation_index_array.insert(compilation_index_array_position, compilation_video[0].index)
|
||||||
|
else
|
||||||
|
compilation_index_array.insert(compilation_index_array_position + 1, compilation_video[0].index)
|
||||||
|
end
|
||||||
|
Invidious::Database::Compilations.move_video_position(compilation_id, compilation_index_array)
|
||||||
|
end
|
||||||
|
update_first_video_params(compilation_id)
|
||||||
|
else
|
||||||
|
return error_json(400, "Unsupported action #{action}")
|
||||||
|
end
|
||||||
|
|
||||||
|
if redirect
|
||||||
|
env.redirect referer
|
||||||
|
else
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
"{}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.show(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get?("user").try &.as(User)
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
compid = env.params.query["comp"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
|
||||||
|
if !compid
|
||||||
|
return env.redirect "/"
|
||||||
|
end
|
||||||
|
|
||||||
|
page = env.params.query["page"]?.try &.to_i?
|
||||||
|
page ||= 1
|
||||||
|
|
||||||
|
begin
|
||||||
|
compilation = get_compilation(compid)
|
||||||
|
rescue ex : NotFoundException
|
||||||
|
return error_template(404, ex)
|
||||||
|
rescue ex
|
||||||
|
return error_template(500, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
page_count = (compilation.video_count / 200).to_i
|
||||||
|
page_count += 1 if (compilation.video_count % 200) > 0
|
||||||
|
|
||||||
|
if page > page_count
|
||||||
|
return env.redirect "/compilation?comp=#{compid}&page=#{page_count}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if compilation.privacy == CompilationPrivacy::Private && compilation.author != user.try &.email
|
||||||
|
return error_template(403, "This compilation is private.")
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
videos = get_compilation_videos(compilation, offset: (page - 1) * 200)
|
||||||
|
rescue ex
|
||||||
|
return error_template(500, "Error encountered while retrieving compilation videos.<br>#{ex.message}")
|
||||||
|
end
|
||||||
|
|
||||||
|
if compilation.author == user.try &.email
|
||||||
|
env.set "remove_compilation_items", compid
|
||||||
|
end
|
||||||
|
|
||||||
|
templated "compilation"
|
||||||
|
end
|
||||||
|
end
|
@ -5,6 +5,36 @@ module Invidious::Routes::Feeds
|
|||||||
env.redirect "/feed/playlists"
|
env.redirect "/feed/playlists"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.view_all_compilations_redirect(env)
|
||||||
|
env.redirect "/feed/compilations"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.compilations(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
return env.redirect "/" if user.nil?
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
|
||||||
|
# TODO: make a single DB call and separate the items here?
|
||||||
|
items_created = Invidious::Database::Compilations.select_like_iv(user.email)
|
||||||
|
items_created.map! do |item|
|
||||||
|
item.author = ""
|
||||||
|
item
|
||||||
|
end
|
||||||
|
|
||||||
|
items_saved = Invidious::Database::Compilations.select_not_like_iv(user.email)
|
||||||
|
items_saved.map! do |item|
|
||||||
|
item.author = ""
|
||||||
|
item
|
||||||
|
end
|
||||||
|
|
||||||
|
templated "feeds/compilations"
|
||||||
|
end
|
||||||
|
|
||||||
def self.playlists(env)
|
def self.playlists(env)
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
@ -23,6 +23,12 @@ module Invidious::Routes::Misc
|
|||||||
else
|
else
|
||||||
env.redirect "/feed/popular"
|
env.redirect "/feed/popular"
|
||||||
end
|
end
|
||||||
|
when "Compilations"
|
||||||
|
if user
|
||||||
|
env.redirect "/feed/compilations"
|
||||||
|
else
|
||||||
|
env.redirect "/feed/popular"
|
||||||
|
end
|
||||||
else
|
else
|
||||||
templated "search_homepage", navbar_search: false
|
templated "search_homepage", navbar_search: false
|
||||||
end
|
end
|
||||||
|
@ -103,7 +103,7 @@ module Invidious::Routes::PreferencesRoute
|
|||||||
default_home = env.params.body["default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home
|
default_home = env.params.body["default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home
|
||||||
|
|
||||||
feed_menu = [] of String
|
feed_menu = [] of String
|
||||||
4.times do |index|
|
5.times do |index|
|
||||||
option = env.params.body["feed_menu[#{index}]"]?.try &.as(String) || ""
|
option = env.params.body["feed_menu[#{index}]"]?.try &.as(String) || ""
|
||||||
if !option.empty?
|
if !option.empty?
|
||||||
feed_menu << option
|
feed_menu << option
|
||||||
@ -191,7 +191,7 @@ module Invidious::Routes::PreferencesRoute
|
|||||||
CONFIG.default_user_preferences.default_home = env.params.body["admin_default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home
|
CONFIG.default_user_preferences.default_home = env.params.body["admin_default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home
|
||||||
|
|
||||||
admin_feed_menu = [] of String
|
admin_feed_menu = [] of String
|
||||||
4.times do |index|
|
5.times do |index|
|
||||||
option = env.params.body["admin_feed_menu[#{index}]"]?.try &.as(String) || ""
|
option = env.params.body["admin_feed_menu[#{index}]"]?.try &.as(String) || ""
|
||||||
if !option.empty?
|
if !option.empty?
|
||||||
admin_feed_menu << option
|
admin_feed_menu << option
|
||||||
|
@ -30,8 +30,43 @@ module Invidious::Routes::Watch
|
|||||||
return env.redirect "/"
|
return env.redirect "/"
|
||||||
end
|
end
|
||||||
|
|
||||||
plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
|
embed_link = "/embed/#{id}"
|
||||||
continuation = process_continuation(env.params.query, plid, id)
|
if env.params.query.size > 1
|
||||||
|
embed_params = HTTP::Params.parse(env.params.query.to_s)
|
||||||
|
embed_params.delete_all("v")
|
||||||
|
embed_link += "?"
|
||||||
|
embed_link += embed_params.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
if env.params.query["list"]?.try &.starts_with? "IVPL"
|
||||||
|
plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
|
||||||
|
continuation = process_continuation(env.params.query, plid, id)
|
||||||
|
elsif env.params.query["comp"]?.try &.starts_with? "IVCMP"
|
||||||
|
compid = env.params.query["comp"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
|
||||||
|
if (!compid.nil?)
|
||||||
|
index = Invidious::Database::CompilationVideos.select_index(compid, id)
|
||||||
|
indices_array = Invidious::Database::Compilations.select_index_array(compid)
|
||||||
|
if (!indices_array.nil?)
|
||||||
|
position_of_index = indices_array.index(index)
|
||||||
|
if (!position_of_index.nil? && position_of_index != indices_array.size - 1)
|
||||||
|
next_index = indices_array[position_of_index + 1]
|
||||||
|
else
|
||||||
|
next_index = indices_array[0]
|
||||||
|
end
|
||||||
|
if (!next_index.nil?)
|
||||||
|
next_id = Invidious::Database::CompilationVideos.select_id_from_index(next_index)
|
||||||
|
if (!next_id.nil?)
|
||||||
|
timestamps = Invidious::Database::CompilationVideos.select_timestamps(compid, next_id)
|
||||||
|
if (!timestamps.nil?)
|
||||||
|
starting_timestamp_seconds = timestamps[0]
|
||||||
|
ending_timestamp_seconds = timestamps[1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
continuation = process_continuation(env.params.query, compid, id)
|
||||||
|
end
|
||||||
|
|
||||||
nojs = env.params.query["nojs"]?
|
nojs = env.params.query["nojs"]?
|
||||||
|
|
||||||
|
@ -26,7 +26,9 @@ module Invidious::Routing
|
|||||||
self.register_watch_routes
|
self.register_watch_routes
|
||||||
|
|
||||||
self.register_iv_playlist_routes
|
self.register_iv_playlist_routes
|
||||||
|
self.register_iv_compilation_routes
|
||||||
self.register_yt_playlist_routes
|
self.register_yt_playlist_routes
|
||||||
|
self.register_compilation_routes
|
||||||
|
|
||||||
self.register_search_routes
|
self.register_search_routes
|
||||||
|
|
||||||
@ -80,6 +82,15 @@ module Invidious::Routing
|
|||||||
get "/subscription_manager", Routes::Subscriptions, :subscription_manager
|
get "/subscription_manager", Routes::Subscriptions, :subscription_manager
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def register_iv_compilation_routes
|
||||||
|
get "/create_compilation", Routes::Compilations, :new
|
||||||
|
post "/create_compilation", Routes::Compilations, :create
|
||||||
|
post "/compilation_ajax", Routes::Compilations, :compilation_ajax
|
||||||
|
get "/add_compilation_items", Routes::Compilations, :add_compilation_items_page
|
||||||
|
get "/edit_compilation", Routes::Compilations, :edit
|
||||||
|
post "/edit_compilation", Routes::Compilations, :adjust_timestamps
|
||||||
|
end
|
||||||
|
|
||||||
def register_iv_playlist_routes
|
def register_iv_playlist_routes
|
||||||
get "/create_playlist", Routes::Playlists, :new
|
get "/create_playlist", Routes::Playlists, :new
|
||||||
post "/create_playlist", Routes::Playlists, :create
|
post "/create_playlist", Routes::Playlists, :create
|
||||||
@ -99,6 +110,7 @@ module Invidious::Routing
|
|||||||
get "/feed/popular", Routes::Feeds, :popular
|
get "/feed/popular", Routes::Feeds, :popular
|
||||||
get "/feed/trending", Routes::Feeds, :trending
|
get "/feed/trending", Routes::Feeds, :trending
|
||||||
get "/feed/subscriptions", Routes::Feeds, :subscriptions
|
get "/feed/subscriptions", Routes::Feeds, :subscriptions
|
||||||
|
get "/feed/compilations", Routes::Feeds, :compilations
|
||||||
get "/feed/history", Routes::Feeds, :history
|
get "/feed/history", Routes::Feeds, :history
|
||||||
|
|
||||||
# RSS Feeds
|
# RSS Feeds
|
||||||
@ -180,6 +192,10 @@ module Invidious::Routing
|
|||||||
get "/watch_videos", Routes::Playlists, :watch_videos
|
get "/watch_videos", Routes::Playlists, :watch_videos
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def register_compilation_routes
|
||||||
|
get "/compilation", Routes::Compilations, :show
|
||||||
|
end
|
||||||
|
|
||||||
def register_search_routes
|
def register_search_routes
|
||||||
get "/opensearch.xml", Routes::Search, :opensearch
|
get "/opensearch.xml", Routes::Search, :opensearch
|
||||||
get "/results", Routes::Search, :results
|
get "/results", Routes::Search, :results
|
||||||
@ -292,6 +308,9 @@ module Invidious::Routing
|
|||||||
post "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :subscribe_channel
|
post "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :subscribe_channel
|
||||||
delete "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :unsubscribe_channel
|
delete "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :unsubscribe_channel
|
||||||
|
|
||||||
|
post "/api/v1/auth/compilations", {{namespace}}::Authenticated, :create_compilation
|
||||||
|
get "/api/v1/auth/compilations", {{namespace}}::Authenticated, :list_compilations
|
||||||
|
|
||||||
get "/api/v1/auth/playlists", {{namespace}}::Authenticated, :list_playlists
|
get "/api/v1/auth/playlists", {{namespace}}::Authenticated, :list_playlists
|
||||||
post "/api/v1/auth/playlists", {{namespace}}::Authenticated, :create_playlist
|
post "/api/v1/auth/playlists", {{namespace}}::Authenticated, :create_playlist
|
||||||
patch "/api/v1/auth/playlists/:plid",{{namespace}}:: Authenticated, :update_playlist_attribute
|
patch "/api/v1/auth/playlists/:plid",{{namespace}}:: Authenticated, :update_playlist_attribute
|
||||||
@ -312,6 +331,8 @@ module Invidious::Routing
|
|||||||
get "/api/v1/stats", {{namespace}}::Misc, :stats
|
get "/api/v1/stats", {{namespace}}::Misc, :stats
|
||||||
get "/api/v1/playlists/:plid", {{namespace}}::Misc, :get_playlist
|
get "/api/v1/playlists/:plid", {{namespace}}::Misc, :get_playlist
|
||||||
get "/api/v1/auth/playlists/:plid", {{namespace}}::Misc, :get_playlist
|
get "/api/v1/auth/playlists/:plid", {{namespace}}::Misc, :get_playlist
|
||||||
|
get "/api/v1/compilations/:compid", {{namespace}}::Misc, :get_compilation
|
||||||
|
get "/api/v1/auth/compilations/:compid", {{namespace}}::Misc, :get_compilation
|
||||||
get "/api/v1/mixes/:rdid", {{namespace}}::Misc, :mixes
|
get "/api/v1/mixes/:rdid", {{namespace}}::Misc, :mixes
|
||||||
get "/api/v1/resolveurl", {{namespace}}::Misc, :resolve_url
|
get "/api/v1/resolveurl", {{namespace}}::Misc, :resolve_url
|
||||||
{% end %}
|
{% end %}
|
||||||
|
@ -8,6 +8,7 @@ module Invidious::Search
|
|||||||
# Types specific to Invidious
|
# Types specific to Invidious
|
||||||
Subscriptions # Search user subscriptions
|
Subscriptions # Search user subscriptions
|
||||||
Playlist # "Add playlist item" search
|
Playlist # "Add playlist item" search
|
||||||
|
Compilation # "Add compilation item" search
|
||||||
end
|
end
|
||||||
|
|
||||||
getter type : Type = Type::Regular
|
getter type : Type = Type::Regular
|
||||||
@ -86,6 +87,12 @@ module Invidious::Search
|
|||||||
#
|
#
|
||||||
@filters, _, @query, _ = Filters.from_legacy_filters(@raw_query)
|
@filters, _, @query, _ = Filters.from_legacy_filters(@raw_query)
|
||||||
#
|
#
|
||||||
|
when .compilation?
|
||||||
|
# In "add compilation item" mode, filters are parsed from the query
|
||||||
|
# string itself (legacy), and the channel is ignored.
|
||||||
|
#
|
||||||
|
@filters, _, @query, _ = Filters.from_legacy_filters(@raw_query)
|
||||||
|
#
|
||||||
when .subscriptions?, .regular?
|
when .subscriptions?, .regular?
|
||||||
if params["sp"]?
|
if params["sp"]?
|
||||||
# Parse the `sp` URL parameter (youtube compatibility)
|
# Parse the `sp` URL parameter (youtube compatibility)
|
||||||
@ -123,7 +130,7 @@ module Invidious::Search
|
|||||||
return items if self.empty_raw_query?
|
return items if self.empty_raw_query?
|
||||||
|
|
||||||
case @type
|
case @type
|
||||||
when .regular?, .playlist?
|
when .regular?, .playlist?, .compilation?
|
||||||
items = Processors.regular(self)
|
items = Processors.regular(self)
|
||||||
#
|
#
|
||||||
when .channel?
|
when .channel?
|
||||||
|
@ -345,9 +345,9 @@ def fetch_video(id, region)
|
|||||||
return video
|
return video
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_continuation(query, plid, id)
|
def process_continuation(query, list_id, id)
|
||||||
continuation = nil
|
continuation = nil
|
||||||
if plid
|
if list_id
|
||||||
if index = query["index"]?.try &.to_i?
|
if index = query["index"]?.try &.to_i?
|
||||||
continuation = index
|
continuation = index
|
||||||
else
|
else
|
||||||
|
34
src/invidious/views/add_compilation_items.ecr
Normal file
34
src/invidious/views/add_compilation_items.ecr
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= compilation.title %> - Invidious</title>
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/compilation/<%= compid %>" />
|
||||||
|
<% 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_compilation_items" method="get">
|
||||||
|
<legend><a href="/compilation?comp=<%= compilation.id %>"><%= translate(locale, "Editing compilation `x`", %|"#{HTML.escape(compilation.title)}"|) %></a></legend>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<input class="pure-input-1" type="search" name="q"
|
||||||
|
<% if query %>value="<%= HTML.escape(query.text) %>"<% end %>
|
||||||
|
placeholder="<%= translate(locale, "Search for videos") %>">
|
||||||
|
<input type="hidden" name="comp" value="<%= compid %>">
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script id="compilation_data" type="application/json">
|
||||||
|
<%=
|
||||||
|
{
|
||||||
|
"csrf_token" => URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "")
|
||||||
|
}.to_pretty_json
|
||||||
|
%>
|
||||||
|
</script>
|
||||||
|
<script src="/js/compilation_widget.js?v=<%= ASSET_COMMIT %>"></script>
|
||||||
|
|
||||||
|
<%= rendered "components/items_paginated" %>
|
97
src/invidious/views/compilation.ecr
Normal file
97
src/invidious/views/compilation.ecr
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<% title = HTML.escape(compilation.title) %>
|
||||||
|
<% author = HTML.escape(compilation.author) %>
|
||||||
|
|
||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= title %> - Invidious</title>
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/compilation/<%= compid %>" />
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="h-box flexible title">
|
||||||
|
<div class="flex-left"><h3><%= title %></h3></div>
|
||||||
|
|
||||||
|
<div class="flex-right button-container">
|
||||||
|
<%- if compilation.is_a?(InvidiousCompilation) && compilation.author == user.try &.email -%>
|
||||||
|
<%- if compilation.index.size > 0 -%>
|
||||||
|
<div class="pure-u">
|
||||||
|
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/watch?v=<%= compilation.first_video_id %>&comp=<%= compid %>&index=<%= compilation.index[0] %>&t=<%= compilation.first_video_starting_timestamp_seconds %>&end=<%= compilation.first_video_ending_timestamp_seconds %>">
|
||||||
|
<i class="icon ion-md-play"></i> <%= translate(locale, "compilation_button_play") %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<%- end -%>
|
||||||
|
<div class="pure-u">
|
||||||
|
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/add_compilation_items?comp=<%= compid %>">
|
||||||
|
<i class="icon ion-md-add"></i> <%= translate(locale, "compilation_button_add_items") %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u">
|
||||||
|
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/edit_compilation?comp=<%= compid %>">
|
||||||
|
<i class="icon ion-md-create"></i> <%= translate(locale, "generic_button_edit") %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u">
|
||||||
|
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_compilation?comp=<%= compid %>">
|
||||||
|
<i class="icon ion-md-trash"></i> <%= translate(locale, "generic_button_delete") %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<%- end -%>
|
||||||
|
|
||||||
|
<div class="pure-u">
|
||||||
|
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/feed/compilation/<%= compid %>">
|
||||||
|
<i class="icon ion-logo-rss"></i> <%= translate(locale, "generic_button_rss") %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-1-1">
|
||||||
|
<% if compilation.is_a? InvidiousCompilation %>
|
||||||
|
<b>
|
||||||
|
<% if compilation.author == user.try &.email %>
|
||||||
|
<a href="/feed/compilations"><%= author %></a> |
|
||||||
|
<% else %>
|
||||||
|
<%= author %> |
|
||||||
|
<% end %>
|
||||||
|
<%= translate_count(locale, "generic_videos_count", compilation.video_count) %> |
|
||||||
|
<%= translate(locale, "Updated `x` ago", recode_date(compilation.updated, locale)) %> |
|
||||||
|
<% case compilation.as(InvidiousCompilation).privacy when %>
|
||||||
|
<% when CompilationPrivacy::Unlisted %>
|
||||||
|
<i class="icon ion-ios-unlock"></i> <%= translate(locale, "Unlisted") %>
|
||||||
|
<% when CompilationPrivacy::Private %>
|
||||||
|
<i class="icon ion-ios-lock"></i> <%= translate(locale, "Private") %>
|
||||||
|
<% end %>
|
||||||
|
</b>
|
||||||
|
<% else %>
|
||||||
|
<b>
|
||||||
|
<a href="/channel/<%= compilation.ucid %>"><%= author %></a> |
|
||||||
|
<%= translate_count(locale, "generic_videos_count", compilation.video_count) %> |
|
||||||
|
<%= translate(locale, "Updated `x` ago", recode_date(compilation.updated, locale)) %>
|
||||||
|
</b>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<div id="descriptionWrapper"><%= compilation.description_html %></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if compilation.is_a?(InvidiousCompilation) && compilation.author == user.try &.email %>
|
||||||
|
<script id="compilation_data" type="application/json">
|
||||||
|
<%=
|
||||||
|
{
|
||||||
|
"csrf_token" => URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "")
|
||||||
|
}.to_pretty_json
|
||||||
|
%>
|
||||||
|
</script>
|
||||||
|
<script src="/js/compilation_widget.js?v=<%= ASSET_COMMIT %>"></script>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<% videos.each do |compilation_video| %>
|
||||||
|
<%= rendered "components/compilation_video" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
43
src/invidious/views/components/compilation_video.ecr
Normal file
43
src/invidious/views/components/compilation_video.ecr
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<div class="pure-u-1 pure-u-md-1">
|
||||||
|
<div class="h-box">
|
||||||
|
<div class="compilation-video-panel">
|
||||||
|
<div class="compilation-order-swap-arrows">
|
||||||
|
<%- form_parameters = "action_move_video_before=1&video_index=#{compilation_video.index}&compilation_id=#{compilation_video.compid}" -%>
|
||||||
|
<form action="/compilation_ajax?<%= form_parameters %>" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
|
<button type="submit" style="margin:10px" class="pure-button pure-button-secondary low-profile">
|
||||||
|
<i class="icon ion-md-arrow-up"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<%- form_parameters = "action_move_video_after=1&video_index=#{compilation_video.index}&compilation_id=#{compilation_video.compid}" -%>
|
||||||
|
<form action="/compilation_ajax?<%= form_parameters %>" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
|
<button type="submit" style="margin:10px" class="pure-button pure-button-secondary low-profile">
|
||||||
|
<i class="icon ion-md-arrow-down"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<img loading="lazy" style="margin:10px;" src="/vi/<%= compilation_video.id %>/mqdefault.jpg" alt="" />
|
||||||
|
<div class="compilation-video-input-panel">
|
||||||
|
<div class="compilation-video-title">
|
||||||
|
<span class="compilation-video-title" dir="auto"><%= HTML.escape(compilation_video.title) %></span>
|
||||||
|
</div>
|
||||||
|
<% if compid_form = env.get?("remove_compilation_items") %>
|
||||||
|
<div class="compilation-video-timestamp-set">
|
||||||
|
<p style="margin-right:10px;">from</p>
|
||||||
|
<input class="compilation-video-timestamp" value="<%= recode_length_seconds(compilation_video.starting_timestamp_seconds) %>" disabled type="text">
|
||||||
|
<p style="margin-right:10px; margin-left:10px">to</p>
|
||||||
|
<input class="compilation-video-timestamp" value="<%= recode_length_seconds(compilation_video.ending_timestamp_seconds) %>" disabled type="text">
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<div class="compilation-video-timestamp-set">
|
||||||
|
<p style="margin-right:10px;">from</p>
|
||||||
|
<input maxlength="8" name="<%= compilation_video.index %>_start_timestamp" class="compilation-video-timestamp" value="<%= recode_length_seconds(compilation_video.starting_timestamp_seconds) %>" type="text">
|
||||||
|
<p style="margin-right:10px; margin-left:10px">to</p>
|
||||||
|
<input maxlength="8" name="<%= compilation_video.index %>_end_timestamp" class="compilation-video-timestamp" value="<%= recode_length_seconds(compilation_video.ending_timestamp_seconds) %>" type="text">
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,7 +1,7 @@
|
|||||||
<div class="feed-menu">
|
<div class="feed-menu">
|
||||||
<% feed_menu = env.get("preferences").as(Preferences).feed_menu.dup %>
|
<% feed_menu = env.get("preferences").as(Preferences).feed_menu.dup %>
|
||||||
<% if !env.get?("user") %>
|
<% if !env.get?("user") %>
|
||||||
<% feed_menu.reject! {|item| {"Subscriptions", "Playlists"}.includes? item} %>
|
<% feed_menu.reject! {|item| {"Subscriptions", "Playlists", "Compilations"}.includes? item} %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% feed_menu.each do |feed| %>
|
<% feed_menu.each do |feed| %>
|
||||||
<a href="/feed/<%= feed.downcase %>" class="feed-menu-item pure-menu-heading">
|
<a href="/feed/<%= feed.downcase %>" class="feed-menu-item pure-menu-heading">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<%-
|
<%-
|
||||||
thin_mode = env.get("preferences").as(Preferences).thin_mode
|
thin_mode = env.get("preferences").as(Preferences).thin_mode
|
||||||
item_watched = !item.is_a?(SearchChannel | SearchHashtag | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil
|
item_watched = !item.is_a?(SearchChannel | SearchHashtag | SearchPlaylist | InvidiousPlaylist | InvidiousCompilation | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil
|
||||||
author_verified = item.responds_to?(:author_verified) && item.author_verified
|
author_verified = item.responds_to?(:author_verified) && item.author_verified
|
||||||
-%>
|
-%>
|
||||||
|
|
||||||
@ -54,10 +54,12 @@
|
|||||||
<p><%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %></p>
|
<p><%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %></p>
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
</div>
|
</div>
|
||||||
<% when SearchPlaylist, InvidiousPlaylist %>
|
<% when SearchPlaylist, InvidiousPlaylist, InvidiousCompilation %>
|
||||||
<%-
|
<%-
|
||||||
if item.id.starts_with? "RD"
|
if item.id.starts_with? "RD"
|
||||||
link_url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}"
|
link_url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}"
|
||||||
|
elsif item.id.starts_with? "IVCMP"
|
||||||
|
link_url = "/compilation?comp=#{item.id}"
|
||||||
else
|
else
|
||||||
link_url = "/playlist?list=#{item.id}"
|
link_url = "/playlist?list=#{item.id}"
|
||||||
end
|
end
|
||||||
@ -144,6 +146,14 @@
|
|||||||
<button type="submit" class="pure-button pure-button-secondary low-profile"
|
<button type="submit" class="pure-button pure-button-secondary low-profile"
|
||||||
data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>"><i class="icon ion-md-add"></i></button>
|
data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>"><i class="icon ion-md-add"></i></button>
|
||||||
</form>
|
</form>
|
||||||
|
<%- elsif compid_form = env.get?("add_compilation_items") -%>
|
||||||
|
<%- form_parameters = "action_add_video=1&video_id=#{item.id}&compilation_id=#{compid_form}&referer=#{env.get("current_page")}" -%>
|
||||||
|
<form data-onsubmit="return_false" action="/compilation_ajax?<%= form_parameters %>" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
|
<button type="submit" class="pure-button pure-button-secondary low-profile"
|
||||||
|
data-onclick="add_compilation_item" data-id="<%= item.id %>" data-compid="<%= compid_form %>"><i class="icon ion-md-add"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
<%- elsif item.is_a?(PlaylistVideo) && (plid_form = env.get?("remove_playlist_items")) -%>
|
<%- elsif item.is_a?(PlaylistVideo) && (plid_form = env.get?("remove_playlist_items")) -%>
|
||||||
<%- form_parameters = "action=remove_video&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
|
<%- form_parameters = "action=remove_video&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
|
||||||
<form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post">
|
<form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post">
|
||||||
|
39
src/invidious/views/create_compilation.ecr
Normal file
39
src/invidious/views/create_compilation.ecr
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "Create compilation") %> - 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_compilation?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||||
|
<fieldset>
|
||||||
|
<legend><%= translate(locale, "Create compilation") %></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, "Compilation privacy") %> :</label>
|
||||||
|
<select name="privacy" id="privacy">
|
||||||
|
<% CompilationPrivacy.names.each do |option| %>
|
||||||
|
<option value="<%= option %>" <% if option == "Unlisted" %> selected <% end %>><%= translate(locale, option) %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-controls">
|
||||||
|
<button type="submit" name="action" value="create_compilation" class="pure-button pure-button-primary">
|
||||||
|
<%= translate(locale, "Create compilation") %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= HTML.escape(csrf_token) %>">
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
</div>
|
60
src/invidious/views/edit_compilation.ecr
Normal file
60
src/invidious/views/edit_compilation.ecr
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<% title = HTML.escape(compilation.title) %>
|
||||||
|
|
||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= title %> - Invidious </title>
|
||||||
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/compilation/<%= compid %>" />
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<form action="/edit_compilation?comp=<%= compid %>" method="post">
|
||||||
|
<div class="h-box flexible">
|
||||||
|
<div class="flex-right button-container">
|
||||||
|
<div class="pure-u">
|
||||||
|
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/compilation?comp=<%= compid %>">
|
||||||
|
<i class="icon ion-md-close"></i> <%= translate(locale, "generic_button_cancel") %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u">
|
||||||
|
<button class="pure-button pure-button-secondary low-profile" dir="auto" type="submit">
|
||||||
|
<i class="icon ion-md-save"></i> <%= translate(locale, "generic_button_save") %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u">
|
||||||
|
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_compilation?comp=<%= compid %>">
|
||||||
|
<i class="icon ion-md-trash"></i> <%= translate(locale, "generic_button_delete") %>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-box flexible title">
|
||||||
|
<div>
|
||||||
|
<h3><input class="pure-input-1" maxlength="150" name="title" type="text" value="<%= title %>"></h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<div class="pure-u-1-1">
|
||||||
|
<b>
|
||||||
|
<%= HTML.escape(compilation.author) %> |
|
||||||
|
<%= translate_count(locale, "generic_videos_count", compilation.video_count) %> |
|
||||||
|
</b>
|
||||||
|
<select name="privacy">
|
||||||
|
<%- {"Unlisted", "Private"}.each do |option| -%>
|
||||||
|
<option value="<%= option %>" <% if option == compilation.privacy.to_s %>selected<% end %>><%= translate(locale, option) %></option>
|
||||||
|
<%- end -%>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= HTML.escape(csrf_token) %>">
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<% videos.each do |compilation_video| %>
|
||||||
|
<%= rendered "components/compilation_video" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</form>
|
29
src/invidious/views/feeds/compilations.ecr
Normal file
29
src/invidious/views/feeds/compilations.ecr
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "Compilations") %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= rendered "components/feed_menu" %>
|
||||||
|
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-1-3">
|
||||||
|
<h3><%= translate(locale, "user_created_compilations", %(<span id="count">#{items_created.size}</span>)) %></h3>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3">
|
||||||
|
<h3 style="text-align:center">
|
||||||
|
<a href="/create_compilation?referer=<%= URI.encode_www_form("/feed/compilations") %>"><%= translate(locale, "Create compilation") %></a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3">
|
||||||
|
<h3 style="text-align:right">
|
||||||
|
<a href="/data_control?referer=<%= URI.encode_www_form("/feed/compilations") %>">
|
||||||
|
<%= translate(locale, "Import") %>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<% items_created.each do |item| %>
|
||||||
|
<%= rendered "components/item" %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
@ -170,7 +170,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if env.get?("user") %>
|
<% if env.get?("user") %>
|
||||||
<% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists"} %>
|
<% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists", "Compilations"} %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<% feed_options = {"", "Popular", "Trending"} %>
|
<% feed_options = {"", "Popular", "Trending"} %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -50,6 +50,9 @@ we're going to need to do it here in order to allow for translations.
|
|||||||
"id" => video.id,
|
"id" => video.id,
|
||||||
"index" => continuation,
|
"index" => continuation,
|
||||||
"plid" => plid,
|
"plid" => plid,
|
||||||
|
"compid" => compid,
|
||||||
|
"starting_timestamp_seconds" => starting_timestamp_seconds,
|
||||||
|
"ending_timestamp_seconds" => ending_timestamp_seconds,
|
||||||
"length_seconds" => video.length_seconds.to_f,
|
"length_seconds" => video.length_seconds.to_f,
|
||||||
"play_next" => !video.related_videos.empty? && !plid && params.continue,
|
"play_next" => !video.related_videos.empty? && !plid && params.continue,
|
||||||
"next_video" => video.related_videos.select { |rv| rv["id"]? }[0]?.try &.["id"],
|
"next_video" => video.related_videos.select { |rv| rv["id"]? }[0]?.try &.["id"],
|
||||||
@ -157,6 +160,34 @@ we're going to need to do it here in order to allow for translations.
|
|||||||
|
|
||||||
<% if user %>
|
<% if user %>
|
||||||
<% playlists = Invidious::Database::Playlists.select_user_created_playlists(user.email) %>
|
<% playlists = Invidious::Database::Playlists.select_user_created_playlists(user.email) %>
|
||||||
|
<% compilations = Invidious::Database::Compilations.select_user_created_compilations(user.email) %>
|
||||||
|
<% if !compilations.empty? %>
|
||||||
|
<form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/compilation_ajax" method="post" target="_blank">
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="compilation_id"><%= translate(locale, "Add to compilation: ") %></label>
|
||||||
|
<select style="width:100%" name="compilation_id" id="compilation_id">
|
||||||
|
<% compilations.each do |compid, compilation_title| %>
|
||||||
|
<option data-compid="<%= compid %>" value="<%= compid %>"><%= HTML.escape(compilation_title) %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||||
|
<input type="hidden" name="action_add_video" value="1">
|
||||||
|
<input type="hidden" name="video_id" value="<%= video.id %>">
|
||||||
|
<button data-onclick="add_compilation_video" data-id="<%= video.id %>" type="submit" class="pure-button pure-button-primary">
|
||||||
|
<b><%= translate(locale, "Add to compilation") %></b>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<script id="compilation_data" type="application/json">
|
||||||
|
<%=
|
||||||
|
{
|
||||||
|
"csrf_token" => URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "")
|
||||||
|
}.to_pretty_json
|
||||||
|
%>
|
||||||
|
</script>
|
||||||
|
<script src="/js/compilation_widget.js?v=<%= Time.utc.to_unix_ms %>"></script>
|
||||||
|
<% end %>
|
||||||
<% if !playlists.empty? %>
|
<% if !playlists.empty? %>
|
||||||
<form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax?action=add_video" method="post" target="_blank">
|
<form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax?action=add_video" method="post" target="_blank">
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
@ -301,16 +332,18 @@ we're going to need to do it here in order to allow for translations.
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if params.related_videos || plid %>
|
<% if params.related_videos || plid || compid%>
|
||||||
<div class="pure-u-1 pure-u-lg-1-5">
|
<div class="pure-u-1 pure-u-lg-1-5">
|
||||||
<% if plid %>
|
<% if plid %>
|
||||||
<div id="playlist" class="h-box"></div>
|
<div id="playlist" class="h-box"></div>
|
||||||
|
<% elsif compid %>
|
||||||
|
<div id="compilation" class="h-box"></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if params.related_videos %>
|
<% if params.related_videos %>
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
<% if !video.related_videos.empty? %>
|
<% if !video.related_videos.empty? %>
|
||||||
<div <% if plid %>style="display:none"<% end %>>
|
<div <% if plid || compid %>style="display:none"<% end %>>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="continue"><%= translate(locale, "preferences_continue_label") %></label>
|
<label for="continue"><%= translate(locale, "preferences_continue_label") %></label>
|
||||||
<input name="continue" id="continue" type="checkbox" <% if params.continue %>checked<% end %>>
|
<input name="continue" id="continue" type="checkbox" <% if params.continue %>checked<% end %>>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user