From 8aff1d04094b5636714b580980fe30fb8139d7f9 Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Mon, 5 Jun 2023 00:16:23 -0700
Subject: [PATCH 01/45] Create compilations file
---
src/invidious/channels/compilations.cr | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 src/invidious/channels/compilations.cr
diff --git a/src/invidious/channels/compilations.cr b/src/invidious/channels/compilations.cr
new file mode 100644
index 000000000..e69de29bb
From 5fb47c5e9e76294312dd326a7861ae49eeb0e38d Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Tue, 6 Jun 2023 23:54:34 -0700
Subject: [PATCH 02/45] add Compilation symbols
---
config/config.example.yml | 6 +++---
locales/en-US.json | 1 +
spec/spec_helper.cr | 1 +
src/invidious.cr | 2 +-
src/invidious/routes/feeds.cr | 4 ++++
src/invidious/routes/misc.cr | 6 ++++++
src/invidious/routes/preferences.cr | 2 +-
src/invidious/routing.cr | 1 +
.../{channels/compilations.cr => views/compilation.ecr} | 0
src/invidious/views/components/feed_menu.ecr | 2 +-
src/invidious/views/feeds/compilations.ecr | 4 ++++
src/invidious/views/user/preferences.ecr | 2 +-
12 files changed, 24 insertions(+), 7 deletions(-)
rename src/invidious/{channels/compilations.cr => views/compilation.ecr} (100%)
create mode 100644 src/invidious/views/feeds/compilations.ecr
diff --git a/config/config.example.yml b/config/config.example.yml
index e925a5e3f..4c9f0cca5 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -627,11 +627,11 @@ default_user_preferences:
##
## Accepted values: A list of strings
## Each entry can be one of: "Popular", "Trending",
- ## "Subscriptions", "Playlists"
+ ## "Subscriptions", "Playlists", "Compilations"
##
- ## Default: ["Popular", "Trending", "Subscriptions", "Playlists"] (show all feeds)
+ ## Default: ["Popular", "Trending", "Subscriptions", "Playlists","Compilations"] (show all feeds)
##
- #feed_menu: ["Popular", "Trending", "Subscriptions", "Playlists"]
+ #feed_menu: ["Popular", "Trending", "Subscriptions", "Playlists","Compilations"]
##
## Default feed to display on the home page.
diff --git a/locales/en-US.json b/locales/en-US.json
index 06d095dcf..a65e2119f 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -413,6 +413,7 @@
"Audio mode": "Audio mode",
"Video mode": "Video mode",
"Playlists": "Playlists",
+ "Compilations": "Compilations",
"search_filters_title": "Filters",
"search_filters_date_label": "Upload date",
"search_filters_date_option_none": "Any date",
diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr
index b3060acfa..2ce03e333 100644
--- a/spec/spec_helper.cr
+++ b/spec/spec_helper.cr
@@ -8,6 +8,7 @@ require "../src/invidious/channels/*"
require "../src/invidious/videos/caption"
require "../src/invidious/videos"
require "../src/invidious/playlists"
+require "../src/invidious/compilations"
require "../src/invidious/search/ctoken"
require "../src/invidious/trending"
require "spectator"
diff --git a/src/invidious.cr b/src/invidious.cr
index e0bd01015..270e999a5 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -121,7 +121,7 @@ Kemal.config.extra_options do |parser|
puts SOFTWARE.to_pretty_json
exit
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
exit
end
diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr
index 40bca0088..cc275dfe7 100644
--- a/src/invidious/routes/feeds.cr
+++ b/src/invidious/routes/feeds.cr
@@ -5,6 +5,10 @@ module Invidious::Routes::Feeds
env.redirect "/feed/playlists"
end
+ def self.compilations(env)
+ templated "feeds/compilations"
+ end
+
def self.playlists(env)
locale = env.get("preferences").as(Preferences).locale
diff --git a/src/invidious/routes/misc.cr b/src/invidious/routes/misc.cr
index d6bd95711..bd3245cc7 100644
--- a/src/invidious/routes/misc.cr
+++ b/src/invidious/routes/misc.cr
@@ -23,6 +23,12 @@ module Invidious::Routes::Misc
else
env.redirect "/feed/popular"
end
+ when "Compilations"
+ if user
+ env.redirect "/feed/compilations"
+ else
+ env.redirect "/feed/popular"
+ end
else
templated "search_homepage", navbar_search: false
end
diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr
index abe0f34e3..6d410e876 100644
--- a/src/invidious/routes/preferences.cr
+++ b/src/invidious/routes/preferences.cr
@@ -99,7 +99,7 @@ module Invidious::Routes::PreferencesRoute
default_home = env.params.body["default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home
feed_menu = [] of String
- 4.times do |index|
+ 5.times do |index|
option = env.params.body["feed_menu[#{index}]"]?.try &.as(String) || ""
if !option.empty?
feed_menu << option
diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr
index 9c43171ca..a653c1a50 100644
--- a/src/invidious/routing.cr
+++ b/src/invidious/routing.cr
@@ -99,6 +99,7 @@ module Invidious::Routing
get "/feed/popular", Routes::Feeds, :popular
get "/feed/trending", Routes::Feeds, :trending
get "/feed/subscriptions", Routes::Feeds, :subscriptions
+ get "/feed/compilations", Routes::Feeds, :compilations
get "/feed/history", Routes::Feeds, :history
# RSS Feeds
diff --git a/src/invidious/channels/compilations.cr b/src/invidious/views/compilation.ecr
similarity index 100%
rename from src/invidious/channels/compilations.cr
rename to src/invidious/views/compilation.ecr
diff --git a/src/invidious/views/components/feed_menu.ecr b/src/invidious/views/components/feed_menu.ecr
index 3dbeaf371..180a263f0 100644
--- a/src/invidious/views/components/feed_menu.ecr
+++ b/src/invidious/views/components/feed_menu.ecr
@@ -1,7 +1,7 @@
<% if env.get?("user") %>
- <% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists"} %>
+ <% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists","Compilations"} %>
<% else %>
<% feed_options = {"", "Popular", "Trending"} %>
<% end %>
From d0a3c994f4885a38a6273151b81dfa2aeb084b12 Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Wed, 7 Jun 2023 01:11:15 -0700
Subject: [PATCH 03/45] Add Compilation symbols until "Compilations" appears in
feeds menu
---
.gitignore | 1 +
config/config.example.yml | 4 ++--
src/invidious/routes/compilations.cr | 2 ++
src/invidious/routes/feeds.cr | 2 ++
src/invidious/routes/preferences.cr | 2 +-
src/invidious/views/compilation.ecr | 6 ++++++
src/invidious/views/feeds/compilations.ecr | 2 ++
7 files changed, 16 insertions(+), 3 deletions(-)
create mode 100644 src/invidious/routes/compilations.cr
diff --git a/.gitignore b/.gitignore
index 7a26e1a6d..2a49e68d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
/invidious
/sentry
/config/config.yml
+.DS_Store
\ No newline at end of file
diff --git a/config/config.example.yml b/config/config.example.yml
index 4c9f0cca5..5f41a4f7e 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -629,9 +629,9 @@ default_user_preferences:
## Each entry can be one of: "Popular", "Trending",
## "Subscriptions", "Playlists", "Compilations"
##
- ## Default: ["Popular", "Trending", "Subscriptions", "Playlists","Compilations"] (show all feeds)
+ ## Default: ["Popular", "Trending", "Subscriptions", "Playlists", "Compilations"] (show all feeds)
##
- #feed_menu: ["Popular", "Trending", "Subscriptions", "Playlists","Compilations"]
+ feed_menu: ["Popular", "Trending", "Subscriptions", "Playlists", "Compilations"]
##
## Default feed to display on the home page.
diff --git a/src/invidious/routes/compilations.cr b/src/invidious/routes/compilations.cr
new file mode 100644
index 000000000..600d34de6
--- /dev/null
+++ b/src/invidious/routes/compilations.cr
@@ -0,0 +1,2 @@
+module Invidious::Routes::Compilations
+end
\ No newline at end of file
diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr
index cc275dfe7..affd165d9 100644
--- a/src/invidious/routes/feeds.cr
+++ b/src/invidious/routes/feeds.cr
@@ -6,6 +6,8 @@ module Invidious::Routes::Feeds
end
def self.compilations(env)
+ locale = env.get("preferences").as(Preferences).locale
+
templated "feeds/compilations"
end
diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr
index 6d410e876..eea938581 100644
--- a/src/invidious/routes/preferences.cr
+++ b/src/invidious/routes/preferences.cr
@@ -186,7 +186,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
admin_feed_menu = [] of String
- 4.times do |index|
+ 5.times do |index|
option = env.params.body["admin_feed_menu[#{index}]"]?.try &.as(String) || ""
if !option.empty?
admin_feed_menu << option
diff --git a/src/invidious/views/compilation.ecr b/src/invidious/views/compilation.ecr
index e69de29bb..0dff74177 100644
--- a/src/invidious/views/compilation.ecr
+++ b/src/invidious/views/compilation.ecr
@@ -0,0 +1,6 @@
+<% title = HTML.escape(compilation.title) %>
+<% author = HTML.escape(compilation.author) %>
+
+<% content_for "header" do %>
+<%= title %> - Invidious
+<% end %>
\ No newline at end of file
diff --git a/src/invidious/views/feeds/compilations.ecr b/src/invidious/views/feeds/compilations.ecr
index a55e1e5a7..de605f278 100644
--- a/src/invidious/views/feeds/compilations.ecr
+++ b/src/invidious/views/feeds/compilations.ecr
@@ -2,3 +2,5 @@
<%= translate(locale, "Compilations") %> - Invidious
<% end %>
+<%= rendered "components/feed_menu" %>
+
From 24f55a787626259e27d49b114606235ba208ea0e Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Wed, 7 Jun 2023 02:01:59 -0700
Subject: [PATCH 04/45] Add backend Compilation symbols
---
config/config.example.yml | 2 +-
config/sql/compilations.sql | 29 +++
src/invidious/compilations.cr | 14 ++
src/invidious/database/compilations.cr | 261 +++++++++++++++++++++++++
4 files changed, 305 insertions(+), 1 deletion(-)
create mode 100644 config/sql/compilations.sql
create mode 100644 src/invidious/compilations.cr
create mode 100644 src/invidious/database/compilations.cr
diff --git a/config/config.example.yml b/config/config.example.yml
index 5f41a4f7e..de051bb0a 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -631,7 +631,7 @@ default_user_preferences:
##
## Default: ["Popular", "Trending", "Subscriptions", "Playlists", "Compilations"] (show all feeds)
##
- feed_menu: ["Popular", "Trending", "Subscriptions", "Playlists", "Compilations"]
+ #feed_menu: ["Popular", "Trending", "Subscriptions", "Playlists", "Compilations"]
##
## Default feed to display on the home page.
diff --git a/config/sql/compilations.sql b/config/sql/compilations.sql
new file mode 100644
index 000000000..ccf04c30b
--- /dev/null
+++ b/config/sql/compilations.sql
@@ -0,0 +1,29 @@
+-- Type: public.privacy
+
+-- DROP TYPE public.privacy;
+
+CREATE TYPE public.privacy AS ENUM
+(
+ 'Public',
+ 'Unlisted',
+ 'Private'
+);
+
+-- Table: public.playlists
+
+-- DROP TABLE public.playlists;
+
+CREATE TABLE IF NOT EXISTS public.playlists
+(
+ title text,
+ id text primary key,
+ author text,
+ description text,
+ video_count integer,
+ created timestamptz,
+ updated timestamptz,
+ privacy privacy,
+ index int8[]
+);
+
+GRANT ALL ON public.playlists TO current_user;
\ No newline at end of file
diff --git a/src/invidious/compilations.cr b/src/invidious/compilations.cr
new file mode 100644
index 000000000..e2b037b8a
--- /dev/null
+++ b/src/invidious/compilations.cr
@@ -0,0 +1,14 @@
+struct Compilation
+ include DB::Serializable
+
+ property title : String
+ property id : String
+ property author : String
+ property ucid : String
+ property length_seconds : Int32
+ property published : Time
+ property plid : String
+ property index : Int64
+ property live_now : Bool
+
+end
\ No newline at end of file
diff --git a/src/invidious/database/compilations.cr b/src/invidious/database/compilations.cr
new file mode 100644
index 000000000..c6754a1ef
--- /dev/null
+++ b/src/invidious/database/compilations.cr
@@ -0,0 +1,261 @@
+require "./base.cr"
+
+#
+# This module contains functions related to the "playlists" table.
+#
+module Invidious::Database::Playlists
+ extend self
+
+ # -------------------
+ # Insert / delete
+ # -------------------
+
+ def insert(playlist : InvidiousPlaylist)
+ playlist_array = playlist.to_a
+
+ request = <<-SQL
+ INSERT INTO playlists
+ VALUES (#{arg_array(playlist_array)})
+ SQL
+
+ PG_DB.exec(request, args: playlist_array)
+ end
+
+ # deletes the given playlist and connected playlist videos
+ def delete(id : String)
+ PlaylistVideos.delete_by_playlist(id)
+ request = <<-SQL
+ DELETE FROM playlists *
+ WHERE id = $1
+ SQL
+
+ PG_DB.exec(request, id)
+ end
+
+ # -------------------
+ # Update
+ # -------------------
+
+ def update(id : String, title : String, privacy, description, updated)
+ request = <<-SQL
+ UPDATE playlists
+ 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 playlists
+ SET description = $1
+ WHERE id = $2
+ SQL
+
+ PG_DB.exec(request, description, id)
+ end
+
+ def update_subscription_time(id : String)
+ request = <<-SQL
+ UPDATE playlists
+ SET subscribed = now()
+ WHERE id = $1
+ SQL
+
+ PG_DB.exec(request, id)
+ end
+
+ def update_video_added(id : String, index : String | Int64)
+ request = <<-SQL
+ UPDATE playlists
+ 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 playlists
+ SET index = array_remove(index, $1),
+ video_count = cardinality(index) - 1,
+ updated = now()
+ WHERE id = $2
+ SQL
+
+ PG_DB.exec(request, index, id)
+ end
+
+ # -------------------
+ # Salect
+ # -------------------
+
+ def select(*, id : String) : InvidiousPlaylist?
+ request = <<-SQL
+ SELECT * FROM playlists
+ WHERE id = $1
+ SQL
+
+ return PG_DB.query_one?(request, id, as: InvidiousPlaylist)
+ end
+
+ def select_all(*, author : String) : Array(InvidiousPlaylist)
+ request = <<-SQL
+ SELECT * FROM playlists
+ WHERE author = $1
+ SQL
+
+ return PG_DB.query_all(request, author, as: InvidiousPlaylist)
+ end
+
+ # -------------------
+ # Salect (filtered)
+ # -------------------
+
+ def select_like_iv(email : String) : Array(InvidiousPlaylist)
+ request = <<-SQL
+ SELECT * FROM playlists
+ WHERE author = $1 AND id LIKE 'IV%'
+ ORDER BY created
+ SQL
+
+ PG_DB.query_all(request, email, as: InvidiousPlaylist)
+ end
+
+ def select_not_like_iv(email : String) : Array(InvidiousPlaylist)
+ request = <<-SQL
+ SELECT * FROM playlists
+ WHERE author = $1 AND id NOT LIKE 'IV%'
+ ORDER BY created
+ SQL
+
+ PG_DB.query_all(request, email, as: InvidiousPlaylist)
+ end
+
+ def select_user_created_playlists(email : String) : Array({String, String})
+ request = <<-SQL
+ SELECT id,title FROM playlists
+ WHERE author = $1 AND id LIKE 'IV%'
+ SQL
+
+ PG_DB.query_all(request, email, as: {String, String})
+ end
+
+ # -------------------
+ # Misc checks
+ # -------------------
+
+ # Check if given playlist ID exists
+ def exists?(id : String) : Bool
+ request = <<-SQL
+ SELECT id FROM playlists
+ WHERE id = $1
+ SQL
+
+ return PG_DB.query_one?(request, id, as: String).nil?
+ end
+
+ # Count how many playlist a user has created.
+ def count_owned_by(author : String) : Int64
+ request = <<-SQL
+ SELECT count(*) FROM playlists
+ WHERE author = $1
+ SQL
+
+ return PG_DB.query_one?(request, author, as: Int64) || 0_i64
+ end
+end
+
+#
+# This module contains functions related to the "playlist_videos" table.
+#
+module Invidious::Database::PlaylistVideos
+ extend self
+
+ private alias VideoIndex = Int64 | Array(Int64)
+
+ # -------------------
+ # Insert / Delete
+ # -------------------
+
+ def insert(video : PlaylistVideo)
+ video_array = video.to_a
+
+ request = <<-SQL
+ INSERT INTO playlist_videos
+ VALUES (#{arg_array(video_array)})
+ SQL
+
+ PG_DB.exec(request, args: video_array)
+ end
+
+ def delete(index)
+ request = <<-SQL
+ DELETE FROM playlist_videos *
+ WHERE index = $1
+ SQL
+
+ PG_DB.exec(request, index)
+ end
+
+ def delete_by_playlist(plid : String)
+ request = <<-SQL
+ DELETE FROM playlist_videos *
+ WHERE plid = $1
+ SQL
+
+ PG_DB.exec(request, plid)
+ end
+
+ # -------------------
+ # Salect
+ # -------------------
+
+ def select(plid : String, index : VideoIndex, offset, limit = 100) : Array(PlaylistVideo)
+ request = <<-SQL
+ SELECT * FROM playlist_videos
+ WHERE plid = $1
+ ORDER BY array_position($2, index)
+ LIMIT $3
+ OFFSET $4
+ SQL
+
+ return PG_DB.query_all(request, plid, index, limit, offset, as: PlaylistVideo)
+ end
+
+ def select_index(plid : String, vid : String) : Int64?
+ request = <<-SQL
+ SELECT index FROM playlist_videos
+ WHERE plid = $1 AND id = $2
+ LIMIT 1
+ SQL
+
+ return PG_DB.query_one?(request, plid, vid, as: Int64)
+ end
+
+ def select_one_id(plid : String, index : VideoIndex) : String?
+ request = <<-SQL
+ SELECT id FROM playlist_videos
+ WHERE plid = $1
+ ORDER BY array_position($2, index)
+ LIMIT 1
+ SQL
+
+ return PG_DB.query_one?(request, plid, index, as: String)
+ end
+
+ def select_ids(plid : String, index : VideoIndex, limit = 500) : Array(String)
+ request = <<-SQL
+ SELECT id FROM playlist_videos
+ WHERE plid = $1
+ ORDER BY array_position($2, index)
+ LIMIT $3
+ SQL
+
+ return PG_DB.query_all(request, plid, index, limit, as: String)
+ end
+end
From bef234fd747899fa13cc33fc25e0d7458f36b97f Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Wed, 14 Jun 2023 22:53:02 -0700
Subject: [PATCH 05/45] Add compilations to DB
---
config/sql/compilations.sql | 17 +++--------------
docker/init-invidious-db.sh | 2 ++
src/invidious/database/compilations.cr | 8 +++-----
3 files changed, 8 insertions(+), 19 deletions(-)
diff --git a/config/sql/compilations.sql b/config/sql/compilations.sql
index ccf04c30b..4076e1b8d 100644
--- a/config/sql/compilations.sql
+++ b/config/sql/compilations.sql
@@ -1,19 +1,8 @@
--- Type: public.privacy
+-- Table: public.compilations
--- DROP TYPE public.privacy;
+-- DROP TABLE public.compilations;
-CREATE TYPE public.privacy AS ENUM
-(
- 'Public',
- 'Unlisted',
- 'Private'
-);
-
--- Table: public.playlists
-
--- DROP TABLE public.playlists;
-
-CREATE TABLE IF NOT EXISTS public.playlists
+CREATE TABLE IF NOT EXISTS public.compilations
(
title text,
id text primary key,
diff --git a/docker/init-invidious-db.sh b/docker/init-invidious-db.sh
index 22b4cc5fd..f7fe3e7ff 100755
--- a/docker/init-invidious-db.sh
+++ b/docker/init-invidious-db.sh
@@ -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/playlists.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
diff --git a/src/invidious/database/compilations.cr b/src/invidious/database/compilations.cr
index c6754a1ef..80672abfb 100644
--- a/src/invidious/database/compilations.cr
+++ b/src/invidious/database/compilations.cr
@@ -3,7 +3,7 @@ require "./base.cr"
#
# This module contains functions related to the "playlists" table.
#
-module Invidious::Database::Playlists
+module Invidious::Database::Compilations
extend self
# -------------------
@@ -173,16 +173,14 @@ end
#
# This module contains functions related to the "playlist_videos" table.
#
-module Invidious::Database::PlaylistVideos
+module Invidious::Database::CompilationVideos
extend self
- private alias VideoIndex = Int64 | Array(Int64)
-
# -------------------
# Insert / Delete
# -------------------
- def insert(video : PlaylistVideo)
+ def insert(video : CompilationVideo)
video_array = video.to_a
request = <<-SQL
From 6b984c6677da0782dd0e4e52bd004a563c6c0326 Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Thu, 15 Jun 2023 03:25:36 -0700
Subject: [PATCH 06/45] Remove files that were prematurely added and add
backend Compilation files
---
config/config.example.yml | 6 +-
config/sql/compilation_videos.sql | 20 ++++
config/sql/compilations.sql | 12 +-
docker/init-invidious-db.sh | 2 +-
locales/en-US.json | 1 -
spec/spec_helper.cr | 1 -
src/invidious/compilations.cr | 14 ---
src/invidious/database/compilations.cr | 118 +++++++++----------
src/invidious/database/playlists.cr | 2 +-
src/invidious/routes/compilations.cr | 2 -
src/invidious/routes/feeds.cr | 20 ++++
src/invidious/routes/misc.cr | 6 -
src/invidious/routes/preferences.cr | 4 +-
src/invidious/views/compilation.ecr | 6 -
src/invidious/views/components/feed_menu.ecr | 4 +-
src/invidious/views/feeds/compilations.ecr | 6 -
src/invidious/views/user/preferences.ecr | 2 +-
17 files changed, 115 insertions(+), 111 deletions(-)
create mode 100644 config/sql/compilation_videos.sql
delete mode 100644 src/invidious/compilations.cr
delete mode 100644 src/invidious/routes/compilations.cr
delete mode 100644 src/invidious/views/compilation.ecr
delete mode 100644 src/invidious/views/feeds/compilations.ecr
diff --git a/config/config.example.yml b/config/config.example.yml
index de051bb0a..e925a5e3f 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -627,11 +627,11 @@ default_user_preferences:
##
## Accepted values: A list of strings
## Each entry can be one of: "Popular", "Trending",
- ## "Subscriptions", "Playlists", "Compilations"
+ ## "Subscriptions", "Playlists"
##
- ## Default: ["Popular", "Trending", "Subscriptions", "Playlists", "Compilations"] (show all feeds)
+ ## Default: ["Popular", "Trending", "Subscriptions", "Playlists"] (show all feeds)
##
- #feed_menu: ["Popular", "Trending", "Subscriptions", "Playlists", "Compilations"]
+ #feed_menu: ["Popular", "Trending", "Subscriptions", "Playlists"]
##
## Default feed to display on the home page.
diff --git a/config/sql/compilation_videos.sql b/config/sql/compilation_videos.sql
new file mode 100644
index 000000000..91eb886c6
--- /dev/null
+++ b/config/sql/compilation_videos.sql
@@ -0,0 +1,20 @@
+-- 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,
+ PRIMARY KEY (index,compid)
+);
+
+GRANT ALL ON TABLE public.compilation_videos TO current_user;
diff --git a/config/sql/compilations.sql b/config/sql/compilations.sql
index 4076e1b8d..d1ee949be 100644
--- a/config/sql/compilations.sql
+++ b/config/sql/compilations.sql
@@ -1,3 +1,13 @@
+-- Type: public.privacy
+
+-- DROP TYPE public.privacy;
+
+CREATE TYPE public.privacy AS ENUM
+(
+ 'Unlisted',
+ 'Private'
+);
+
-- Table: public.compilations
-- DROP TABLE public.compilations;
@@ -15,4 +25,4 @@ CREATE TABLE IF NOT EXISTS public.compilations
index int8[]
);
-GRANT ALL ON public.playlists TO current_user;
\ No newline at end of file
+GRANT ALL ON public.compilations TO current_user;
diff --git a/docker/init-invidious-db.sh b/docker/init-invidious-db.sh
index f7fe3e7ff..62df42298 100755
--- a/docker/init-invidious-db.sh
+++ b/docker/init-invidious-db.sh
@@ -11,4 +11,4 @@ psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotation
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/compilations.sql
-psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/compilation_videos.sql
+psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/compilation_videos.sql
\ No newline at end of file
diff --git a/locales/en-US.json b/locales/en-US.json
index a65e2119f..06d095dcf 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -413,7 +413,6 @@
"Audio mode": "Audio mode",
"Video mode": "Video mode",
"Playlists": "Playlists",
- "Compilations": "Compilations",
"search_filters_title": "Filters",
"search_filters_date_label": "Upload date",
"search_filters_date_option_none": "Any date",
diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr
index 2ce03e333..b3060acfa 100644
--- a/spec/spec_helper.cr
+++ b/spec/spec_helper.cr
@@ -8,7 +8,6 @@ require "../src/invidious/channels/*"
require "../src/invidious/videos/caption"
require "../src/invidious/videos"
require "../src/invidious/playlists"
-require "../src/invidious/compilations"
require "../src/invidious/search/ctoken"
require "../src/invidious/trending"
require "spectator"
diff --git a/src/invidious/compilations.cr b/src/invidious/compilations.cr
deleted file mode 100644
index e2b037b8a..000000000
--- a/src/invidious/compilations.cr
+++ /dev/null
@@ -1,14 +0,0 @@
-struct Compilation
- include DB::Serializable
-
- property title : String
- property id : String
- property author : String
- property ucid : String
- property length_seconds : Int32
- property published : Time
- property plid : String
- property index : Int64
- property live_now : Bool
-
-end
\ No newline at end of file
diff --git a/src/invidious/database/compilations.cr b/src/invidious/database/compilations.cr
index 80672abfb..69281a55d 100644
--- a/src/invidious/database/compilations.cr
+++ b/src/invidious/database/compilations.cr
@@ -1,7 +1,7 @@
require "./base.cr"
#
-# This module contains functions related to the "playlists" table.
+# This module contains functions related to the "compilations" table.
#
module Invidious::Database::Compilations
extend self
@@ -10,22 +10,22 @@ module Invidious::Database::Compilations
# Insert / delete
# -------------------
- def insert(playlist : InvidiousPlaylist)
- playlist_array = playlist.to_a
+ def insert(compilation : InvidiousCompilation)
+ compilation_array = compilation.to_a
request = <<-SQL
- INSERT INTO playlists
- VALUES (#{arg_array(playlist_array)})
+ INSERT INTO compilations
+ VALUES (#{arg_array(compilation_array)})
SQL
- PG_DB.exec(request, args: playlist_array)
+ PG_DB.exec(request, args: compilation_array)
end
- # deletes the given playlist and connected playlist videos
+ # deletes the given compilation and connected compilation videos
def delete(id : String)
- PlaylistVideos.delete_by_playlist(id)
+ CompilationVideos.delete_by_compilation(id)
request = <<-SQL
- DELETE FROM playlists *
+ DELETE FROM compilations *
WHERE id = $1
SQL
@@ -38,7 +38,7 @@ module Invidious::Database::Compilations
def update(id : String, title : String, privacy, description, updated)
request = <<-SQL
- UPDATE playlists
+ UPDATE compilations
SET title = $1, privacy = $2, description = $3, updated = $4
WHERE id = $5
SQL
@@ -48,7 +48,7 @@ module Invidious::Database::Compilations
def update_description(id : String, description)
request = <<-SQL
- UPDATE playlists
+ UPDATE compilations
SET description = $1
WHERE id = $2
SQL
@@ -56,19 +56,9 @@ module Invidious::Database::Compilations
PG_DB.exec(request, description, id)
end
- def update_subscription_time(id : String)
- request = <<-SQL
- UPDATE playlists
- SET subscribed = now()
- WHERE id = $1
- SQL
-
- PG_DB.exec(request, id)
- end
-
def update_video_added(id : String, index : String | Int64)
request = <<-SQL
- UPDATE playlists
+ UPDATE compilations
SET index = array_append(index, $1),
video_count = cardinality(index) + 1,
updated = now()
@@ -80,7 +70,7 @@ module Invidious::Database::Compilations
def update_video_removed(id : String, index : String | Int64)
request = <<-SQL
- UPDATE playlists
+ UPDATE compilations
SET index = array_remove(index, $1),
video_count = cardinality(index) - 1,
updated = now()
@@ -94,51 +84,51 @@ module Invidious::Database::Compilations
# Salect
# -------------------
- def select(*, id : String) : InvidiousPlaylist?
+ def select(*, id : String) : InvidiousCompilation?
request = <<-SQL
- SELECT * FROM playlists
+ SELECT * FROM compilations
WHERE id = $1
SQL
- return PG_DB.query_one?(request, id, as: InvidiousPlaylist)
+ return PG_DB.query_one?(request, id, as: InvidiousCompilation)
end
- def select_all(*, author : String) : Array(InvidiousPlaylist)
+ def select_all(*, author : String) : Array(InvidiousCompilation)
request = <<-SQL
- SELECT * FROM playlists
+ SELECT * FROM compilations
WHERE author = $1
SQL
- return PG_DB.query_all(request, author, as: InvidiousPlaylist)
+ return PG_DB.query_all(request, author, as: InvidiousCompilation)
end
# -------------------
# Salect (filtered)
# -------------------
- def select_like_iv(email : String) : Array(InvidiousPlaylist)
+ def select_like_iv(email : String) : Array(InvidiousCompilation)
request = <<-SQL
- SELECT * FROM playlists
+ SELECT * FROM compilation
WHERE author = $1 AND id LIKE 'IV%'
ORDER BY created
SQL
- PG_DB.query_all(request, email, as: InvidiousPlaylist)
+ PG_DB.query_all(request, email, as: InvidiousCompilation)
end
- def select_not_like_iv(email : String) : Array(InvidiousPlaylist)
+ def select_not_like_iv(email : String) : Array(InvidiousCompilation)
request = <<-SQL
- SELECT * FROM playlists
+ SELECT * FROM compilations
WHERE author = $1 AND id NOT LIKE 'IV%'
ORDER BY created
SQL
- PG_DB.query_all(request, email, as: InvidiousPlaylist)
+ PG_DB.query_all(request, email, as: InvidiousCompilation)
end
- def select_user_created_playlists(email : String) : Array({String, String})
+ def select_user_created_compilations(email : String) : Array({String, String})
request = <<-SQL
- SELECT id,title FROM playlists
+ SELECT id,title FROM compilations
WHERE author = $1 AND id LIKE 'IV%'
SQL
@@ -149,20 +139,20 @@ module Invidious::Database::Compilations
# Misc checks
# -------------------
- # Check if given playlist ID exists
+ # Check if given compilation ID exists
def exists?(id : String) : Bool
request = <<-SQL
- SELECT id FROM playlists
+ SELECT id FROM compilations
WHERE id = $1
SQL
return PG_DB.query_one?(request, id, as: String).nil?
end
- # Count how many playlist a user has created.
+ # Count how many compilations a user has created.
def count_owned_by(author : String) : Int64
request = <<-SQL
- SELECT count(*) FROM playlists
+ SELECT count(*) FROM compilations
WHERE author = $1
SQL
@@ -171,7 +161,7 @@ module Invidious::Database::Compilations
end
#
-# This module contains functions related to the "playlist_videos" table.
+# This module contains functions related to the "compilation_videos" table.
#
module Invidious::Database::CompilationVideos
extend self
@@ -184,7 +174,7 @@ module Invidious::Database::CompilationVideos
video_array = video.to_a
request = <<-SQL
- INSERT INTO playlist_videos
+ INSERT INTO compilation_videos
VALUES (#{arg_array(video_array)})
SQL
@@ -193,67 +183,67 @@ module Invidious::Database::CompilationVideos
def delete(index)
request = <<-SQL
- DELETE FROM playlist_videos *
+ DELETE FROM compilation_videos *
WHERE index = $1
SQL
PG_DB.exec(request, index)
end
- def delete_by_playlist(plid : String)
+ def delete_by_compilation(compid : String)
request = <<-SQL
- DELETE FROM playlist_videos *
- WHERE plid = $1
+ DELETE FROM compilation_videos *
+ WHERE compid = $1
SQL
- PG_DB.exec(request, plid)
+ PG_DB.exec(request, compid)
end
# -------------------
# Salect
# -------------------
- def select(plid : String, index : VideoIndex, offset, limit = 100) : Array(PlaylistVideo)
+ def select(compid : String, index : VideoIndex, offset, limit = 100) : Array(CompilationVideo)
request = <<-SQL
- SELECT * FROM playlist_videos
- WHERE plid = $1
+ SELECT * FROM compilation_videos
+ WHERE compid = $1
ORDER BY array_position($2, index)
LIMIT $3
OFFSET $4
SQL
- return PG_DB.query_all(request, plid, index, limit, offset, as: PlaylistVideo)
+ return PG_DB.query_all(request, compid, index, limit, offset, as: CompilationVideo)
end
- def select_index(plid : String, vid : String) : Int64?
+ def select_index(compid : String, vid : String) : Int64?
request = <<-SQL
- SELECT index FROM playlist_videos
- WHERE plid = $1 AND id = $2
+ SELECT index FROM compilation_videos
+ WHERE compid = $1 AND id = $2
LIMIT 1
SQL
- return PG_DB.query_one?(request, plid, vid, as: Int64)
+ return PG_DB.query_one?(request, compid, vid, as: Int64)
end
- def select_one_id(plid : String, index : VideoIndex) : String?
+ def select_one_id(compid : String, index : VideoIndex) : String?
request = <<-SQL
- SELECT id FROM playlist_videos
- WHERE plid = $1
+ SELECT id FROM compilation_videos
+ WHERE compid = $1
ORDER BY array_position($2, index)
LIMIT 1
SQL
- return PG_DB.query_one?(request, plid, index, as: String)
+ return PG_DB.query_one?(request, compid, index, as: String)
end
- def select_ids(plid : String, index : VideoIndex, limit = 500) : Array(String)
+ def select_ids(compid : String, index : VideoIndex, limit = 500) : Array(String)
request = <<-SQL
- SELECT id FROM playlist_videos
- WHERE plid = $1
+ SELECT id FROM compilation_videos
+ WHERE compid = $1
ORDER BY array_position($2, index)
LIMIT $3
SQL
- return PG_DB.query_all(request, plid, index, limit, as: String)
+ return PG_DB.query_all(request, compid, index, limit, as: String)
end
end
diff --git a/src/invidious/database/playlists.cr b/src/invidious/database/playlists.cr
index c6754a1ef..c961c16ef 100644
--- a/src/invidious/database/playlists.cr
+++ b/src/invidious/database/playlists.cr
@@ -159,7 +159,7 @@ module Invidious::Database::Playlists
return PG_DB.query_one?(request, id, as: String).nil?
end
- # Count how many playlist a user has created.
+ # Count how many playlists a user has created.
def count_owned_by(author : String) : Int64
request = <<-SQL
SELECT count(*) FROM playlists
diff --git a/src/invidious/routes/compilations.cr b/src/invidious/routes/compilations.cr
deleted file mode 100644
index 600d34de6..000000000
--- a/src/invidious/routes/compilations.cr
+++ /dev/null
@@ -1,2 +0,0 @@
-module Invidious::Routes::Compilations
-end
\ No newline at end of file
diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr
index affd165d9..fec946826 100644
--- a/src/invidious/routes/feeds.cr
+++ b/src/invidious/routes/feeds.cr
@@ -8,6 +8,26 @@ module Invidious::Routes::Feeds
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
diff --git a/src/invidious/routes/misc.cr b/src/invidious/routes/misc.cr
index bd3245cc7..d6bd95711 100644
--- a/src/invidious/routes/misc.cr
+++ b/src/invidious/routes/misc.cr
@@ -23,12 +23,6 @@ module Invidious::Routes::Misc
else
env.redirect "/feed/popular"
end
- when "Compilations"
- if user
- env.redirect "/feed/compilations"
- else
- env.redirect "/feed/popular"
- end
else
templated "search_homepage", navbar_search: false
end
diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr
index eea938581..abe0f34e3 100644
--- a/src/invidious/routes/preferences.cr
+++ b/src/invidious/routes/preferences.cr
@@ -99,7 +99,7 @@ module Invidious::Routes::PreferencesRoute
default_home = env.params.body["default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home
feed_menu = [] of String
- 5.times do |index|
+ 4.times do |index|
option = env.params.body["feed_menu[#{index}]"]?.try &.as(String) || ""
if !option.empty?
feed_menu << option
@@ -186,7 +186,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
admin_feed_menu = [] of String
- 5.times do |index|
+ 4.times do |index|
option = env.params.body["admin_feed_menu[#{index}]"]?.try &.as(String) || ""
if !option.empty?
admin_feed_menu << option
diff --git a/src/invidious/views/compilation.ecr b/src/invidious/views/compilation.ecr
deleted file mode 100644
index 0dff74177..000000000
--- a/src/invidious/views/compilation.ecr
+++ /dev/null
@@ -1,6 +0,0 @@
-<% title = HTML.escape(compilation.title) %>
-<% author = HTML.escape(compilation.author) %>
-
-<% content_for "header" do %>
-<%= title %> - Invidious
-<% end %>
\ No newline at end of file
diff --git a/src/invidious/views/components/feed_menu.ecr b/src/invidious/views/components/feed_menu.ecr
index 180a263f0..3fb200866 100644
--- a/src/invidious/views/components/feed_menu.ecr
+++ b/src/invidious/views/components/feed_menu.ecr
@@ -1,11 +1,11 @@
+
\ No newline at end of file
diff --git a/src/invidious/views/feeds/compilations.ecr b/src/invidious/views/feeds/compilations.ecr
deleted file mode 100644
index de605f278..000000000
--- a/src/invidious/views/feeds/compilations.ecr
+++ /dev/null
@@ -1,6 +0,0 @@
-<% content_for "header" do %>
-<%= translate(locale, "Compilations") %> - Invidious
-<% end %>
-
-<%= rendered "components/feed_menu" %>
-
diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr
index 1f24bd4e0..dfda14341 100644
--- a/src/invidious/views/user/preferences.ecr
+++ b/src/invidious/views/user/preferences.ecr
@@ -165,7 +165,7 @@
<% if env.get?("user") %>
- <% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists","Compilations"} %>
+ <% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists"} %>
<% else %>
<% feed_options = {"", "Popular", "Trending"} %>
<% end %>
From 696ca4227414b3d3336d91b79f5ceae2f5cf9ceb Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Thu, 15 Jun 2023 04:19:06 -0700
Subject: [PATCH 07/45] Add structs and DB methods for Compilations
---
src/invidious/compilations.cr | 454 +++++++++++++++++++++
src/invidious/database/compilations.cr | 2 +-
src/invidious/views/feeds/compilations.ecr | 5 +
3 files changed, 460 insertions(+), 1 deletion(-)
create mode 100644 src/invidious/compilations.cr
create mode 100644 src/invidious/views/feeds/compilations.ecr
diff --git a/src/invidious/compilations.cr b/src/invidious/compilations.cr
new file mode 100644
index 000000000..b048fb849
--- /dev/null
+++ b/src/invidious/compilations.cr
@@ -0,0 +1,454 @@
+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
+
+ 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("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 "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?
+
+ 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)
+
+ @[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)
+ plid = "IVPL#{Random::Secure.urlsafe_base64(24)[0, 31]}"
+
+ playlist = 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,
+ })
+
+ 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,
+ })
+
+ 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 compid.starts_with? "IV"
+ if compilation = Invidious::Database::Compilations.select(id: compid)
+ return compilation
+ else
+ raise NotFoundException.new("Compilation does not exist.")
+ end
+ else
+ return fetch_compilation(compid)
+ 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: plid,
+ index: index,
+ })
+ end
+ end
+
+ return videos
+end
+
+def template_compilation(compilation)
+ html = <<-END_HTML
+
+
+
+ END_HTML
+
+ html
+end
diff --git a/src/invidious/database/compilations.cr b/src/invidious/database/compilations.cr
index 69281a55d..ff727e998 100644
--- a/src/invidious/database/compilations.cr
+++ b/src/invidious/database/compilations.cr
@@ -108,7 +108,7 @@ module Invidious::Database::Compilations
def select_like_iv(email : String) : Array(InvidiousCompilation)
request = <<-SQL
- SELECT * FROM compilation
+ SELECT * FROM compilations
WHERE author = $1 AND id LIKE 'IV%'
ORDER BY created
SQL
diff --git a/src/invidious/views/feeds/compilations.ecr b/src/invidious/views/feeds/compilations.ecr
new file mode 100644
index 000000000..096a50904
--- /dev/null
+++ b/src/invidious/views/feeds/compilations.ecr
@@ -0,0 +1,5 @@
+<% content_for "header" do %>
+<%= translate(locale, "Compilations") %> - Invidious
+<% end %>
+
+<%= rendered "components/feed_menu" %>
\ No newline at end of file
From 7c92b051b3a966b5d8d37af14d3b911262223229 Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Thu, 15 Jun 2023 20:38:46 -0700
Subject: [PATCH 08/45] Add custom item for CompilationVideo
---
locales/en-US.json | 3 +++
src/invidious/compilations.cr | 4 ++--
src/invidious/database/compilations.cr | 2 ++
src/invidious/views/components/item.ecr | 9 ++++++--
src/invidious/views/feeds/compilations.ecr | 26 +++++++++++++++++++++-
5 files changed, 39 insertions(+), 5 deletions(-)
diff --git a/locales/en-US.json b/locales/en-US.json
index 06d095dcf..e206bc0e5 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -174,6 +174,7 @@
"Delete playlist `x`?": "Delete playlist `x`?",
"Delete playlist": "Delete playlist",
"Create playlist": "Create playlist",
+ "Create compilation": "Create compilation",
"Title": "Title",
"Playlist privacy": "Playlist privacy",
"Editing playlist `x`": "Editing playlist `x`",
@@ -413,6 +414,7 @@
"Audio mode": "Audio mode",
"Video mode": "Video mode",
"Playlists": "Playlists",
+ "Compilations": "Compilations",
"search_filters_title": "Filters",
"search_filters_date_label": "Upload date",
"search_filters_date_option_none": "Any date",
@@ -469,6 +471,7 @@
"download_subtitles": "Subtitles - `x` (.vtt)",
"user_created_playlists": "`x` created playlists",
"user_saved_playlists": "`x` saved playlists",
+ "user_created_compilations": "`x` created compilations",
"Video unavailable": "Video unavailable",
"preferences_save_player_pos_label": "Save playback position: ",
"crash_page_you_found_a_bug": "It looks like you found a bug in Invidious!",
diff --git a/src/invidious/compilations.cr b/src/invidious/compilations.cr
index b048fb849..42e9aaf50 100644
--- a/src/invidious/compilations.cr
+++ b/src/invidious/compilations.cr
@@ -240,9 +240,9 @@ struct InvidiousCompilation
end
def create_compilation(title, privacy, user)
- plid = "IVPL#{Random::Secure.urlsafe_base64(24)[0, 31]}"
+ compid = "IVPL#{Random::Secure.urlsafe_base64(24)[0, 31]}"
- playlist = InvidiousCompilation.new({
+ compilation = InvidiousCompilation.new({
title: title.byte_slice(0, 150),
id: compid,
author: user.email,
diff --git a/src/invidious/database/compilations.cr b/src/invidious/database/compilations.cr
index ff727e998..76e456410 100644
--- a/src/invidious/database/compilations.cr
+++ b/src/invidious/database/compilations.cr
@@ -166,6 +166,8 @@ end
module Invidious::Database::CompilationVideos
extend self
+ private alias VideoIndex = Int64 | Array(Int64)
+
# -------------------
# Insert / Delete
# -------------------
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index c29ec47b6..867e2e8f4 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -1,6 +1,6 @@
<%-
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
-%>
@@ -53,7 +53,7 @@
<%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %>
<%- end -%>
- <% when SearchPlaylist, InvidiousPlaylist %>
+ <% when SearchPlaylist, InvidiousPlaylist, InvidiousCompilation %>
<%-
if item.id.starts_with? "RD"
link_url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}"
@@ -87,6 +87,11 @@
+ <% when CompilationVideo %>
+
+ <% if !env.get("preferences").as(Preferences).thin_mode %>
+ <% end %>
+
<% when Category %>
<% else %>
<%-
diff --git a/src/invidious/views/feeds/compilations.ecr b/src/invidious/views/feeds/compilations.ecr
index 096a50904..a7280e300 100644
--- a/src/invidious/views/feeds/compilations.ecr
+++ b/src/invidious/views/feeds/compilations.ecr
@@ -2,4 +2,28 @@
<%= translate(locale, "Compilations") %> - Invidious
<% end %>
-<%= rendered "components/feed_menu" %>
\ No newline at end of file
+<%= rendered "components/feed_menu" %>
+
+
+
+
<%= translate(locale, "user_created_compilations", %(#{items_created.size} )) %>
+
+
+
+
+
+
+<% items_created.each do |item| %>
+ <%= rendered "components/item" %>
+<% end %>
+
\ No newline at end of file
From b6181b9d97d270356d5dae3dabe1f03d8dd34803 Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Thu, 15 Jun 2023 21:29:37 -0700
Subject: [PATCH 09/45] Add authenticated API for compilation creation
---
assets/css/default.css | 7 ++++++
src/invidious/routes/api/v1/authenticated.cr | 26 ++++++++++++++++++++
src/invidious/routing.cr | 2 ++
src/invidious/views/components/item.ecr | 3 +++
src/invidious/views/feeds/compilations.ecr | 2 +-
5 files changed, 39 insertions(+), 1 deletion(-)
diff --git a/assets/css/default.css b/assets/css/default.css
index c31b24e5d..9828513a9 100644
--- a/assets/css/default.css
+++ b/assets/css/default.css
@@ -759,3 +759,10 @@ h1, h2, h3, h4, h5, p,
.channel-emoji {
margin: 0 2px;
}
+
+div.compilation-video-panel {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ color: #d9d9d9;
+}
diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr
index a35d2f2b2..0f96965c9 100644
--- a/src/invidious/routes/api/v1/authenticated.cr
+++ b/src/invidious/routes/api/v1/authenticated.cr
@@ -207,6 +207,32 @@ module Invidious::Routes::API::V1::Authenticated
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/#{playlist.id}"
+ env.response.status_code = 201
+ {
+ "title" => title,
+ "compilationId" => compilation.id,
+ }.to_json
+
def self.create_playlist(env)
env.response.content_type = "application/json"
user = env.get("user").as(User)
diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr
index a653c1a50..a40bef73e 100644
--- a/src/invidious/routing.cr
+++ b/src/invidious/routing.cr
@@ -274,6 +274,8 @@ module Invidious::Routing
post "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :subscribe_channel
delete "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :unsubscribe_channel
+ get "/api/v1/auth/compilations", {{namespace}}::Authenticated, :create_compilation
+
get "/api/v1/auth/playlists", {{namespace}}::Authenticated, :list_playlists
post "/api/v1/auth/playlists", {{namespace}}::Authenticated, :create_playlist
patch "/api/v1/auth/playlists/:plid",{{namespace}}:: Authenticated, :update_playlist_attribute
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index 867e2e8f4..8f8ba40fd 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -90,6 +90,9 @@
<% when CompilationVideo %>
<% if !env.get("preferences").as(Preferences).thin_mode %>
+
+
+
<% end %>
<% when Category %>
diff --git a/src/invidious/views/feeds/compilations.ecr b/src/invidious/views/feeds/compilations.ecr
index a7280e300..31d073c43 100644
--- a/src/invidious/views/feeds/compilations.ecr
+++ b/src/invidious/views/feeds/compilations.ecr
@@ -10,7 +10,7 @@
From bac4fd90979432873663c6e67faac4f1f1de3c7f Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Thu, 15 Jun 2023 23:17:44 -0700
Subject: [PATCH 10/45] Add create_compilation route and ecr
---
src/invidious/routes/api/v1/authenticated.cr | 3 ++-
src/invidious/routing.cr | 5 +++++
src/invidious/views/create_compilation.ecr | 3 +++
3 files changed, 10 insertions(+), 1 deletion(-)
create mode 100644 src/invidious/views/create_compilation.ecr
diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr
index 0f96965c9..11ffe7ffd 100644
--- a/src/invidious/routes/api/v1/authenticated.cr
+++ b/src/invidious/routes/api/v1/authenticated.cr
@@ -226,12 +226,13 @@ module Invidious::Routes::API::V1::Authenticated
end
compilation = create_compilation(title, privacy, user)
- env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/compilations/#{playlist.id}"
+ 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)
env.response.content_type = "application/json"
diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr
index a40bef73e..434392e9e 100644
--- a/src/invidious/routing.cr
+++ b/src/invidious/routing.cr
@@ -80,6 +80,11 @@ module Invidious::Routing
get "/subscription_manager", Routes::Subscriptions, :subscription_manager
end
+ def register_iv_compilation_routes
+ get "/create_compilation", Routes::Compilations, :new
+ post "/create_compilation", Routes::Compilations, :create
+ end
+
def register_iv_playlist_routes
get "/create_playlist", Routes::Playlists, :new
post "/create_playlist", Routes::Playlists, :create
diff --git a/src/invidious/views/create_compilation.ecr b/src/invidious/views/create_compilation.ecr
new file mode 100644
index 000000000..296d873e7
--- /dev/null
+++ b/src/invidious/views/create_compilation.ecr
@@ -0,0 +1,3 @@
+<% content_for "header" do %>
+<%= translate(locale, "Create compilation") %> - Invidious
+<% end %>
\ No newline at end of file
From 74d42c18dda03a999f8f59142ba673b59cf8572c Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Fri, 16 Jun 2023 01:49:38 -0700
Subject: [PATCH 11/45] Add ecr for individual compilation items (videos)
---
assets/js/compilation_widget.js | 48 ++
config/config.example.yml | 8 +
locales/en-US.json | 1 +
src/invidious/routes/compilations.cr | 410 ++++++++++++++++++
src/invidious/views/add_compilation_items.ecr | 40 ++
src/invidious/views/components/feed_menu.ecr | 2 +-
src/invidious/views/create_compilation.ecr | 38 +-
7 files changed, 545 insertions(+), 2 deletions(-)
create mode 100644 assets/js/compilation_widget.js
create mode 100644 src/invidious/routes/compilations.cr
create mode 100644 src/invidious/views/add_compilation_items.ecr
diff --git a/assets/js/compilation_widget.js b/assets/js/compilation_widget.js
new file mode 100644
index 000000000..7e8e6356e
--- /dev/null
+++ b/assets/js/compilation_widget.js
@@ -0,0 +1,48 @@
+'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 = '';
+ }
+ });
+}
diff --git a/config/config.example.yml b/config/config.example.yml
index e925a5e3f..ff568a5b9 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -483,6 +483,14 @@ hmac_key: "CHANGE_ME!!"
##
#playlist_length_limit: 500
+##
+## Maximum custom compilation length limit.
+##
+## Accepted values: Integer
+## Default: 500
+##
+#compilation_length_limit: 500
+
#########################################
#
# Default user preferences
diff --git a/locales/en-US.json b/locales/en-US.json
index e206bc0e5..3905de6ec 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -177,6 +177,7 @@
"Create compilation": "Create compilation",
"Title": "Title",
"Playlist privacy": "Playlist privacy",
+ "Compilation privacy": "Compilation privacy",
"Editing playlist `x`": "Editing playlist `x`",
"playlist_button_add_items": "Add videos",
"Show more": "Show more",
diff --git a/src/invidious/routes/compilations.cr b/src/invidious/routes/compilations.cr
new file mode 100644
index 000000000..0189b60bc
--- /dev/null
+++ b/src/invidious/routes/compilations.cr
@@ -0,0 +1,410 @@
+{% 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?list=#{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["list"]?
+ 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["list"]?
+ 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["list"]?
+ if !compid || !compid.starts_with?("IV")
+ 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["list"]?
+ 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?list=#{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["list"]?
+ if !compid || !compid.starts_with?("IV")
+ 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)
+ videos = query.process.select(SearchVideo).map(&.as(SearchVideo))
+ rescue ex
+ videos = [] of SearchVideo
+ end
+
+ 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"
+ 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: video.length_seconds,
+ 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),
+ })
+
+ Invidious::Database::CompilationVideos.insert(compilation_video)
+ Invidious::Database::Compilations.update_video_added(compilation_id, compilation_video.index)
+ 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)
+ when "action_move_video_before"
+ # TODO: Compilation stub
+ 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["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
+ if !compid
+ return env.redirect "/"
+ end
+
+ page = env.params.query["page"]?.try &.to_i?
+ page ||= 1
+
+ if compid.starts_with? "RD"
+ return env.redirect "/mix?list=#{compid}"
+ end
+
+ 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?list=#{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. #{ex.message}")
+ end
+
+ if compilation.author == user.try &.email
+ env.set "remove_compilation_items", compid
+ end
+
+ templated "compilation"
+ end
+end
\ No newline at end of file
diff --git a/src/invidious/views/add_compilation_items.ecr b/src/invidious/views/add_compilation_items.ecr
new file mode 100644
index 000000000..28494d983
--- /dev/null
+++ b/src/invidious/views/add_compilation_items.ecr
@@ -0,0 +1,40 @@
+<% content_for "header" do %>
+<%= compilation.title %> - Invidious
+
+<% end %>
+
+
+
+
+
+
+
+ <% videos.each_slice(4) do |slice| %>
+ <% slice.each do |item| %>
+ <%= rendered "components/item" %>
+ <% end %>
+ <% end %>
+
\ No newline at end of file
diff --git a/src/invidious/views/components/feed_menu.ecr b/src/invidious/views/components/feed_menu.ecr
index 3fb200866..ff3b2474d 100644
--- a/src/invidious/views/components/feed_menu.ecr
+++ b/src/invidious/views/components/feed_menu.ecr
@@ -1,7 +1,7 @@
<% if env.get?("user") %>
- <% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists"} %>
+ <% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists", "Compilations"} %>
<% else %>
<% feed_options = {"", "Popular", "Trending"} %>
<% end %>
From 7cfa09984ea7b1b149a23d852d82377611f9753b Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Sun, 18 Jun 2023 00:12:33 -0700
Subject: [PATCH 13/45] Add compilations to feeds menu
---
assets/js/handlers.js | 9 +++++++++
config/sql/compilations.sql | 8 ++++----
src/invidious/compilations.cr | 2 --
src/invidious/config.cr | 5 ++++-
src/invidious/routes/feeds.cr | 4 ++++
src/invidious/routes/misc.cr | 2 +-
src/invidious/routes/preferences.cr | 2 +-
src/invidious/routing.cr | 4 +++-
src/invidious/views/user/preferences.ecr | 4 ++--
9 files changed, 28 insertions(+), 12 deletions(-)
diff --git a/assets/js/handlers.js b/assets/js/handlers.js
index 539974fbf..f36da2441 100644
--- a/assets/js/handlers.js
+++ b/assets/js/handlers.js
@@ -60,12 +60,21 @@
document.querySelectorAll('[data-onclick="add_playlist_video"]').forEach(function (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) {
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) {
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) {
el.onclick = function () { revoke_token(el); };
});
diff --git a/config/sql/compilations.sql b/config/sql/compilations.sql
index d1ee949be..95d2d0af2 100644
--- a/config/sql/compilations.sql
+++ b/config/sql/compilations.sql
@@ -1,8 +1,8 @@
--- Type: public.privacy
+-- Type: public.compilation_privacy
--- DROP TYPE public.privacy;
+-- DROP TYPE public.compilation_privacy;
-CREATE TYPE public.privacy AS ENUM
+CREATE TYPE public.compilation_privacy AS ENUM
(
'Unlisted',
'Private'
@@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS public.compilations
video_count integer,
created timestamptz,
updated timestamptz,
- privacy privacy,
+ privacy compilation_privacy,
index int8[]
);
diff --git a/src/invidious/compilations.cr b/src/invidious/compilations.cr
index 42e9aaf50..695a37eae 100644
--- a/src/invidious/compilations.cr
+++ b/src/invidious/compilations.cr
@@ -319,8 +319,6 @@ def get_compilation(compid : String)
else
raise NotFoundException.new("Compilation does not exist.")
end
- else
- return fetch_compilation(compid)
end
end
diff --git a/src/invidious/config.cr b/src/invidious/config.cr
index cee33ce18..df64c9b8e 100644
--- a/src/invidious/config.cr
+++ b/src/invidious/config.cr
@@ -30,7 +30,7 @@ struct ConfigPreferences
property quality : String = "hd720"
property quality_dash : String = "auto"
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 region : String = "US"
property related_videos : Bool = true
@@ -138,6 +138,9 @@ class Config
# Playlist length limit
property playlist_length_limit : Int32 = 500
+ # Compilation length limit
+ property compilation_length_limit : Int32 = 500
+
def disabled?(option)
case disabled = CONFIG.disable_proxy
when Bool
diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr
index fec946826..3e68bcfb1 100644
--- a/src/invidious/routes/feeds.cr
+++ b/src/invidious/routes/feeds.cr
@@ -5,6 +5,10 @@ module Invidious::Routes::Feeds
env.redirect "/feed/playlists"
end
+ def self.view_all_compilations_redirect(env)
+ env.redirect "/feed/compilations"
+ end
+
def self.compilations(env)
locale = env.get("preferences").as(Preferences).locale
diff --git a/src/invidious/routes/misc.cr b/src/invidious/routes/misc.cr
index 4f7f65a8d..bd3245cc7 100644
--- a/src/invidious/routes/misc.cr
+++ b/src/invidious/routes/misc.cr
@@ -27,7 +27,7 @@ module Invidious::Routes::Misc
if user
env.redirect "/feed/compilations"
else
- env.redirect "/feed/popyular"
+ env.redirect "/feed/popular"
end
else
templated "search_homepage", navbar_search: false
diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr
index 6d410e876..eea938581 100644
--- a/src/invidious/routes/preferences.cr
+++ b/src/invidious/routes/preferences.cr
@@ -186,7 +186,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
admin_feed_menu = [] of String
- 4.times do |index|
+ 5.times do |index|
option = env.params.body["admin_feed_menu[#{index}]"]?.try &.as(String) || ""
if !option.empty?
admin_feed_menu << option
diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr
index 434392e9e..5f743ab5b 100644
--- a/src/invidious/routing.cr
+++ b/src/invidious/routing.cr
@@ -26,6 +26,7 @@ module Invidious::Routing
self.register_watch_routes
self.register_iv_playlist_routes
+ self.register_iv_compilation_routes
self.register_yt_playlist_routes
self.register_search_routes
@@ -83,6 +84,7 @@ module Invidious::Routing
def register_iv_compilation_routes
get "/create_compilation", Routes::Compilations, :new
post "/create_compilation", Routes::Compilations, :create
+ post "/compilation_ajax", Routes::Compilations, :compilation_ajax
end
def register_iv_playlist_routes
@@ -279,7 +281,7 @@ module Invidious::Routing
post "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :subscribe_channel
delete "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :unsubscribe_channel
- get "/api/v1/auth/compilations", {{namespace}}::Authenticated, :create_compilation
+ post "/api/v1/auth/compilations", {{namespace}}::Authenticated, :create_compilation
get "/api/v1/auth/playlists", {{namespace}}::Authenticated, :list_playlists
post "/api/v1/auth/playlists", {{namespace}}::Authenticated, :create_playlist
diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr
index 1ef99cd2f..93f03a054 100644
--- a/src/invidious/views/user/preferences.ecr
+++ b/src/invidious/views/user/preferences.ecr
@@ -174,7 +174,7 @@
<%= translate(locale, "preferences_default_home_label") %>
<% feed_options.each do |option| %>
- selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %>
+ selected <% end %>><%= translate(locale, option.blank? ? "Compilations" : option) %>
<% end %>
@@ -184,7 +184,7 @@
<% (feed_options.size - 1).times do |index| %>
<% end %>
From 1d209c422e1422c831a826ca87b40b5777917821 Mon Sep 17 00:00:00 2001
From: broquemonsieur
Date: Tue, 27 Jun 2023 00:25:01 -0700
Subject: [PATCH 14/45] getcompilation route to routes that are registered
---
src/invidious/compilations.cr | 9 ++-
src/invidious/database/base.cr | 3 +
src/invidious/database/compilations.cr | 2 +-
src/invidious/database/playlists.cr | 2 +-
src/invidious/playlists.cr | 5 ++
src/invidious/routes/api/v1/authenticated.cr | 24 ++++++-
src/invidious/routes/api/v1/misc.cr | 69 ++++++++++++++++++++
src/invidious/routes/before_all.cr | 1 +
src/invidious/routes/compilations.cr | 23 ++++++-
src/invidious/routes/embed.cr | 1 +
src/invidious/routes/feeds.cr | 3 +
src/invidious/routes/playlists.cr | 4 ++
src/invidious/routes/watch.cr | 3 +
src/invidious/routing.cr | 8 +++
src/invidious/views/compilation.ecr | 45 +------------
src/invidious/views/components/item.ecr | 16 ++++-
src/invidious/views/watch.ecr | 28 ++++++++
17 files changed, 196 insertions(+), 50 deletions(-)
diff --git a/src/invidious/compilations.cr b/src/invidious/compilations.cr
index 695a37eae..8e9ee21bd 100644
--- a/src/invidious/compilations.cr
+++ b/src/invidious/compilations.cr
@@ -240,7 +240,9 @@ struct InvidiousCompilation
end
def create_compilation(title, privacy, user)
+ LOGGER.info("2. create_compilation")
compid = "IVPL#{Random::Secure.urlsafe_base64(24)[0, 31]}"
+ LOGGER.info("generated compilation id")
compilation = InvidiousCompilation.new({
title: title.byte_slice(0, 150),
@@ -253,8 +255,10 @@ def create_compilation(title, privacy, user)
privacy: privacy,
index: [] of Int64,
})
+ LOGGER.info("Creating compilation db")
Invidious::Database::Compilations.insert(compilation)
+ LOGGER.info("inserted compilation db entry")
return compilation
end
@@ -313,6 +317,7 @@ def produce_compilation_continuation(id, index)
end
def get_compilation(compid : String)
+ LOGGER.info("8. get_compilation")
if compid.starts_with? "IV"
if compilation = Invidious::Database::Compilations.select(id: compid)
return compilation
@@ -323,6 +328,8 @@ def get_compilation(compid : String)
end
def get_compilation_videos(compilation : InvidiousCompilation | Compilation, offset : Int32, video_id = nil)
+ LOGGER.info("1. get_compilation")
+ LOGGER.info("Getting compilation")
# 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
@@ -405,7 +412,7 @@ def extract_compilation_videos(initial_data : Hash(String, JSON::Any))
starting_timestamp_seconds: starting_timestamp_seconds,
ending_timestamp_seconds: ending_timestamp_seconds,
published: Time.utc,
- compid: plid,
+ compid: compid,
index: index,
})
end
diff --git a/src/invidious/database/base.cr b/src/invidious/database/base.cr
index 0fb1b6af0..9cd8d705b 100644
--- a/src/invidious/database/base.cr
+++ b/src/invidious/database/base.cr
@@ -10,11 +10,14 @@ module Invidious::Database
def check_integrity(cfg)
return if !cfg.check_tables
Invidious::Database.check_enum("privacy", PlaylistPrivacy)
+ Invidious::Database.check_enum("compilation_privacy", CompilationPrivacy)
Invidious::Database.check_table("channels", InvidiousChannel)
Invidious::Database.check_table("channel_videos", ChannelVideo)
Invidious::Database.check_table("playlists", InvidiousPlaylist)
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("session_ids", SessionId)
Invidious::Database.check_table("users", User)
diff --git a/src/invidious/database/compilations.cr b/src/invidious/database/compilations.cr
index 76e456410..f03063646 100644
--- a/src/invidious/database/compilations.cr
+++ b/src/invidious/database/compilations.cr
@@ -202,7 +202,7 @@ module Invidious::Database::CompilationVideos
end
# -------------------
- # Salect
+ # Select
# -------------------
def select(compid : String, index : VideoIndex, offset, limit = 100) : Array(CompilationVideo)
diff --git a/src/invidious/database/playlists.cr b/src/invidious/database/playlists.cr
index c961c16ef..a51deab6f 100644
--- a/src/invidious/database/playlists.cr
+++ b/src/invidious/database/playlists.cr
@@ -212,7 +212,7 @@ module Invidious::Database::PlaylistVideos
end
# -------------------
- # Salect
+ # Select
# -------------------
def select(plid : String, index : VideoIndex, offset, limit = 100) : Array(PlaylistVideo)
diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr
index 013be2680..dc05d209c 100644
--- a/src/invidious/playlists.cr
+++ b/src/invidious/playlists.cr
@@ -240,6 +240,8 @@ struct InvidiousPlaylist
end
def create_playlist(title, privacy, user)
+ LOGGER.info("2. create_playlist")
+ LOGGER.info("create playlist inv/pl.cr")
plid = "IVPL#{Random::Secure.urlsafe_base64(24)[0, 31]}"
playlist = InvidiousPlaylist.new({
@@ -313,6 +315,7 @@ def produce_playlist_continuation(id, index)
end
def get_playlist(plid : String)
+ LOGGER.info("8. get_playlist")
if plid.starts_with? "IV"
if playlist = Invidious::Database::Playlists.select(id: plid)
return playlist
@@ -401,6 +404,8 @@ def fetch_playlist(plid : String)
end
def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32, video_id = nil)
+ LOGGER.info("1. get_playlist_videos")
+ LOGGER.info("get_playlist_videos")
# Show empty playlist if requested page is out of range
# (e.g, when a new playlist has been created, offset will be negative)
if offset >= playlist.video_count || offset < 0
diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr
index 11ffe7ffd..2cbcd7cb2 100644
--- a/src/invidious/routes/api/v1/authenticated.cr
+++ b/src/invidious/routes/api/v1/authenticated.cr
@@ -192,6 +192,21 @@ module Invidious::Routes::API::V1::Authenticated
env.response.status_code = 204
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)
env.response.content_type = "application/json"
user = env.get("user").as(User)
@@ -208,33 +223,40 @@ module Invidious::Routes::API::V1::Authenticated
end
def self.create_compilation(env)
+ LOGGER.info("creating comp in auth fashion")
env.response.content_type = "application/json"
user = env.get("user").as(User)
+ LOGGER.info("app json compilation")
title = env.params.json["title"]?.try &.as(String).delete("<>").byte_slice(0, 150)
if !title
return error_json(400, "Invalid title.")
end
-
+ LOGGER.info("set title")
privacy = env.params.json["privacy"]?.try { |p| CompilationPrivacy.parse(p.as(String).downcase) }
if !privacy
return error_json(400, "Invalid privacy setting.")
end
+ LOGGER.info("set privacy")
if Invidious::Database::Compilations.count_owned_by(user.email) >= 100
return error_json(400, "User cannot have more than 100 compilations.")
end
+ LOGGER.info("400 forgone")
compilation = create_compilation(title, privacy, user)
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/compilations/#{compilation.id}"
env.response.status_code = 201
+ LOGGER.info("location set")
{
"title" => title,
"compilationId" => compilation.id,
}.to_json
+ LOGGER.info("Creating json")
end
def self.create_playlist(env)
+ LOGGER.info("7. create_playlist")
env.response.content_type = "application/json"
user = env.get("user").as(User)
diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr
index e499f4d6d..b99a15f10 100644
--- a/src/invidious/routes/api/v1/misc.cr
+++ b/src/invidious/routes/api/v1/misc.cr
@@ -10,6 +10,75 @@ module Invidious::Routes::API::V1::Misc
end
end
+ def self.get_compilation(env : HTTP::Server::Context)
+ LOGGER.info("15. get_compilation")
+ env.response.content_type = "application/json"
+ compid = env.params.url["compid"]
+ LOGGER.info("the compid is #{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
# user playlists and Invidious playlists. This means that we can't
# reasonably split them yet. This should be addressed in APIv2
diff --git a/src/invidious/routes/before_all.cr b/src/invidious/routes/before_all.cr
index 396840a4b..3c027027b 100644
--- a/src/invidious/routes/before_all.cr
+++ b/src/invidious/routes/before_all.cr
@@ -85,6 +85,7 @@ module Invidious::Routes::BeforeAll
csrf_token = generate_response(sid, {
":authorize_token",
":playlist_ajax",
+ ":compilation_ajax",
":signout",
":subscription_ajax",
":token_ajax",
diff --git a/src/invidious/routes/compilations.cr b/src/invidious/routes/compilations.cr
index 0189b60bc..5a0dd7263 100644
--- a/src/invidious/routes/compilations.cr
+++ b/src/invidious/routes/compilations.cr
@@ -1,7 +1,11 @@
{% skip_file if flag?(:api_only) %}
module Invidious::Routes::Compilations
+ def self.handle(env)
+ return "Hello