summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--assets/css/default.css25
-rw-r--r--locales/da.json212
-rw-r--r--spec/helpers_spec.cr12
-rw-r--r--src/invidious.cr19
-rw-r--r--src/invidious/channels.cr175
-rw-r--r--src/invidious/helpers/helpers.cr2
-rw-r--r--src/invidious/helpers/utils.cr2
-rw-r--r--src/invidious/playlists.cr3
-rw-r--r--src/invidious/routes/playlists.cr2
-rw-r--r--src/invidious/routes/watch.cr4
-rw-r--r--src/invidious/search.cr51
-rw-r--r--src/invidious/trending.cr24
-rw-r--r--src/invidious/users.cr2
-rw-r--r--src/invidious/views/template.ecr6
-rw-r--r--src/invidious/views/trending.ecr2
15 files changed, 302 insertions, 239 deletions
diff --git a/assets/css/default.css b/assets/css/default.css
index 2552263d..107973e6 100644
--- a/assets/css/default.css
+++ b/assets/css/default.css
@@ -5,6 +5,12 @@ body {
Arial, sans-serif;
}
+#contents {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
.deleted {
background-color: rgb(255, 0, 0, 0.5);
}
@@ -280,14 +286,16 @@ input[type="search"]::-webkit-search-cancel-button {
* Footer
*/
-.footer {
- color: #666666;
- margin: 2em 0;
+footer {
+ color: #919191;
+ margin-top: auto;
+ padding: 1.5em 0;
text-align: center;
+ max-height: 30vh;
}
-body .footer a {
- color: inherit;
+footer a {
+ color: #919191 !important;
text-decoration: underline;
}
@@ -654,3 +662,10 @@ body.dark-theme {
content: "[ - ]";
font-size: 1.5em;
}
+
+/*With commit d9528f5 all contents of the page is now within a flexbox. However,
+the hr element is rendered improperly within one.
+See https://stackoverflow.com/a/34372979 for more info */
+hr {
+ margin: auto 0 auto 0;
+}
diff --git a/locales/da.json b/locales/da.json
index ac60862c..f3867f6a 100644
--- a/locales/da.json
+++ b/locales/da.json
@@ -13,7 +13,7 @@
},
"LIVE": "DIREKTE",
"Shared `x` ago": "Delt for `x` siden",
- "Unsubscribe": "",
+ "Unsubscribe": "Opsig abonnement",
"Subscribe": "Abonner",
"View channel on YouTube": "Vis kanal på YouTube",
"View playlist on YouTube": "Vis afspilningsliste på YouTube",
@@ -28,13 +28,13 @@
"New passwords must match": "Nye kodeord skal matche",
"Cannot change password for Google accounts": "Kan ikke skifte kodeord til Google-konti",
"Authorize token?": "Godkend token?",
- "Authorize token for `x`?": "Godkende token til `x`?",
+ "Authorize token for `x`?": "Godkend token til `x`?",
"Yes": "Ja",
"No": "Nej",
"Import and Export Data": "Importer og Eksporter Data",
"Import": "Importer",
"Import Invidious data": "Importer Invidious data",
- "Import YouTube subscriptions": "Importer Youtube abonnementer",
+ "Import YouTube subscriptions": "Importer YouTube abonnementer",
"Import FreeTube subscriptions (.db)": "Importer FreeTube abonnementer (.db)",
"Import NewPipe subscriptions (.json)": "Importer NewPipe abonnementer (.json)",
"Import NewPipe data (.zip)": "Importer NewPipe data (.zip)",
@@ -58,9 +58,9 @@
"Sign In": "Log ind",
"Register": "Registrer",
"E-mail": "E-mail",
- "Google verification code": "Google verifications kode",
+ "Google verification code": "Google-verifikationskode",
"Preferences": "Præferencer",
- "Player preferences": "",
+ "Player preferences": "Afspillerindstillinger",
"Always loop: ": "Altid gentag: ",
"Autoplay: ": "Auto afspil: ",
"Play next by default: ": "Afspil næste som standard: ",
@@ -74,118 +74,118 @@
"youtube": "youtube",
"reddit": "reddit",
"Default captions: ": "Standard undertekster: ",
- "Fallback captions: ": "",
- "Show related videos: ": "",
- "Show annotations by default: ": "",
- "Visual preferences": "",
- "Player style: ": "",
- "Dark mode: ": "",
- "Theme: ": "",
- "dark": "",
- "light": "",
- "Thin mode: ": "",
- "Subscription preferences": "",
- "Show annotations by default for subscribed channels: ": "",
- "Redirect homepage to feed: ": "",
- "Number of videos shown in feed: ": "",
- "Sort videos by: ": "",
- "published": "",
- "published - reverse": "",
- "alphabetically": "",
- "alphabetically - reverse": "",
- "channel name": "",
- "channel name - reverse": "",
- "Only show latest video from channel: ": "",
- "Only show latest unwatched video from channel: ": "",
- "Only show unwatched: ": "",
- "Only show notifications (if there are any): ": "",
- "Enable web notifications": "",
- "`x` uploaded a video": "",
- "`x` is live": "",
- "Data preferences": "",
- "Clear watch history": "",
- "Import/export data": "",
- "Change password": "",
- "Manage subscriptions": "",
- "Manage tokens": "",
- "Watch history": "",
- "Delete account": "",
- "Administrator preferences": "",
- "Default homepage: ": "",
- "Feed menu: ": "",
- "Top enabled: ": "",
- "CAPTCHA enabled: ": "",
- "Login enabled: ": "",
- "Registration enabled: ": "",
- "Report statistics: ": "",
- "Save preferences": "",
- "Subscription manager": "",
- "Token manager": "",
- "Token": "",
+ "Fallback captions: ": "Alternative undertekster: ",
+ "Show related videos: ": "Vis relaterede videoer: ",
+ "Show annotations by default: ": "Vis annotationer som standard: ",
+ "Visual preferences": "Visuelle præferencer",
+ "Player style: ": "Afspiller stil: ",
+ "Dark mode: ": "Mørk tilstand: ",
+ "Theme: ": "Tema: ",
+ "dark": "mørk",
+ "light": "lys",
+ "Thin mode: ": "Tynd tilstand: ",
+ "Subscription preferences": "Abonnements præferencer",
+ "Show annotations by default for subscribed channels: ": "Vis annotationer som standard for abonnerede kanaler: ",
+ "Redirect homepage to feed: ": "Omdiriger startside til feed: ",
+ "Number of videos shown in feed: ": "Antal videoer vist i feed: ",
+ "Sort videos by: ": "Sorter videoer efter: ",
+ "published": "offentliggjort",
+ "published - reverse": "offentliggjort - omvendt",
+ "alphabetically": "alfabetisk",
+ "alphabetically - reverse": "alfabetisk - omvendt",
+ "channel name": "kanalnavn",
+ "channel name - reverse": "kanalnavn - omvendt",
+ "Only show latest video from channel: ": "Vis kun seneste video fra kanal: ",
+ "Only show latest unwatched video from channel: ": "Vis kun seneste usete video fra kanal: ",
+ "Only show unwatched: ": "Vis kun usete: ",
+ "Only show notifications (if there are any): ": "Vis kun notifikationer (hvis der er nogle): ",
+ "Enable web notifications": "Aktiver webnotifikationer",
+ "`x` uploaded a video": "`x` uploadede en video",
+ "`x` is live": "`x` er live",
+ "Data preferences": "Data præferencer",
+ "Clear watch history": "Ryd afspilningshistorik",
+ "Import/export data": "Importer/exporter data",
+ "Change password": "Skift adgangskode",
+ "Manage subscriptions": "Administrer abonnementer",
+ "Manage tokens": "Administrer tokens",
+ "Watch history": "Afspilningshistorik",
+ "Delete account": "Slet konto",
+ "Administrator preferences": "Administrator præferencer",
+ "Default homepage: ": "Standard startside: ",
+ "Feed menu: ": "Feed menu: ",
+ "Top enabled: ": "Top aktiveret: ",
+ "CAPTCHA enabled: ": "CAPTCHA aktiveret: ",
+ "Login enabled: ": "Login aktiveret: ",
+ "Registration enabled: ": "Registrering aktiveret: ",
+ "Report statistics: ": "Indsend statistik: ",
+ "Save preferences": "Gem præferencer",
+ "Subscription manager": "Abonnementsmanager",
+ "Token manager": "Tokenmanager",
+ "Token": "Token",
"`x` subscriptions": {
- "([^.,0-9]|^)1([^.,0-9]|$)": "",
- "": ""
+ "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnementer.([^.,0-9]|^)1([^.,0-9]|$)",
+ "": "`x`"
},
"`x` tokens": {
- "([^.,0-9]|^)1([^.,0-9]|$)": "",
- "": ""
+ "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)",
+ "": "`x` tokens."
},
- "Import/export": "",
- "unsubscribe": "",
- "revoke": "",
- "Subscriptions": "",
+ "Import/export": "Importer/eksporter",
+ "unsubscribe": "opsig abonnement",
+ "revoke": "tilbagekald",
+ "Subscriptions": "Abonnementer",
"`x` unseen notifications": {
- "([^.,0-9]|^)1([^.,0-9]|$)": "",
- "": ""
+ "([^.,0-9]|^)1([^.,0-9]|$)": "`x` usete notifikationer.([^.,0-9]|^)1([^.,0-9]|$)",
+ "": "`x` usete notifikationer."
},
- "search": "",
- "Log out": "",
- "Released under the AGPLv3 by Omar Roth.": "",
- "Source available here.": "",
- "View JavaScript license information.": "",
- "View privacy policy.": "",
- "Trending": "",
- "Public": "",
- "Unlisted": "",
- "Private": "",
- "View all playlists": "",
+ "search": "søg",
+ "Log out": "Log ud",
+ "Released under the AGPLv3 by Omar Roth.": "Offentliggjort under AGPLv3 af Omar Roth.",
+ "Source available here.": "Kilde tilgængelig her.",
+ "View JavaScript license information.": "Vis JavaScriptlicensinformation.",
+ "View privacy policy.": "Vis privatpolitik.",
+ "Trending": "Trending",
+ "Public": "Offentlig",
+ "Unlisted": "Skjult",
+ "Private": "Privat",
+ "View all playlists": "Vis alle afspilningslister",
"Updated `x` ago": "",
- "Delete playlist `x`?": "",
- "Delete playlist": "",
- "Create playlist": "",
- "Title": "",
- "Playlist privacy": "",
- "Editing playlist `x`": "",
- "Watch on YouTube": "",
- "Hide annotations": "",
- "Show annotations": "",
- "Genre: ": "",
- "License: ": "",
- "Family friendly? ": "",
- "Wilson score: ": "",
- "Engagement: ": "",
- "Whitelisted regions: ": "",
- "Blacklisted regions: ": "",
- "Shared `x`": "",
+ "Delete playlist `x`?": "Opdateret `x` siden",
+ "Delete playlist": "Slet afspilningsliste",
+ "Create playlist": "Opret afspilningsliste",
+ "Title": "Titel",
+ "Playlist privacy": "Privatlivsindstillinger for afspilningsliste",
+ "Editing playlist `x`": "Redigerer afspilningsliste `x`",
+ "Watch on YouTube": "Se på YouTube",
+ "Hide annotations": "Skjul annotationer",
+ "Show annotations": "Vis annotationer",
+ "Genre: ": "Genre: ",
+ "License: ": "Licens: ",
+ "Family friendly? ": "Familievenlig? ",
+ "Wilson score: ": "Wilson score: ",
+ "Engagement: ": "Engagement: ",
+ "Whitelisted regions: ": "Whitelistede regioner: ",
+ "Blacklisted regions: ": "Blacklistede regioner: ",
+ "Shared `x`": "Delt `x`",
"`x` views": {
- "([^.,0-9]|^)1([^.,0-9]|$)": "",
- "": ""
+ "([^.,0-9]|^)1([^.,0-9]|$)": "`x` visninger.([^.,0-9]|^)1([^.,0-9]|$)",
+ "": "`x` visninger"
},
- "Premieres in `x`": "",
- "Premieres `x`": "",
- "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "",
- "View YouTube comments": "",
- "View more comments on Reddit": "",
+ "Premieres in `x`": "Har premiere om `x`",
+ "Premieres `x`": "Har premiere om `x`",
+ "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hej! Det ser ud til at du har JavaScript slået fra. Klik her for at se kommentarer, vær opmærksom på at de kan tage længere om at loade.",
+ "View YouTube comments": "Vis YouTube kommentarer",
+ "View more comments on Reddit": "Se flere kommentarer på Reddit",
"View `x` comments": {
- "([^.,0-9]|^)1([^.,0-9]|$)": "",
- "": ""
+ "([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` kommentarer.([^.,0-9]|^)1([^.,0-9]|$)",
+ "": "Vis `x` kommentarer."
},
- "View Reddit comments": "",
- "Hide replies": "",
- "Show replies": "",
- "Incorrect password": "",
- "Quota exceeded, try again in a few hours": "",
- "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "",
+ "View Reddit comments": "Vis Reddit kommentarer",
+ "Hide replies": "Skjul svar",
+ "Show replies": "Vis svar",
+ "Incorrect password": "Forkert adgangskode",
+ "Quota exceeded, try again in a few hours": "Kvota overskredet, prøv igen om et par timer",
+ "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Login fejlet, tjek at totrinsbekræftelse (Authenticator eller SMS) er slået til.",
"Invalid TFA code": "",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "",
"Wrong answer": "",
diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr
index c4138671..ed3a3d48 100644
--- a/spec/helpers_spec.cr
+++ b/spec/helpers_spec.cr
@@ -27,11 +27,11 @@ describe "Helper" do
end
end
- describe "#produce_channel_search_url" do
+ describe "#produce_channel_search_continuation" do
it "correctly produces token for searching a specific channel" do
- produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("/browse_ajax?continuation=4qmFsgI2EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaGEVnWnpaV0Z5WTJnNEFYb0RNVEF3dUFFQVoA&gl=US&hl=en")
+ produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
- produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("/browse_ajax?continuation=4qmFsgJ0EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaGEVnWnpaV0Z5WTJnNEFYb0JNTGdCQUE9PVo-0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr8%3D&gl=US&hl=en")
+ produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("4qmFsgKoARIYVUNYdXFTQmxIQUU2WHcteWVKQTBUdW53GiBFZ1p6WldGeVkyZ3dBVGdCWUFGNkJFZEJRVDI0QVFBPVo-0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr-aAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
end
end
@@ -41,12 +41,6 @@ describe "Helper" do
end
end
- describe "#extract_channel_playlists_cursor" do
- it "correctly extracts a playlists cursor from the given URL" do
- extract_channel_playlists_cursor("4qmFsgLRARIYVUNDajk1NklGNjJGYlQ3R291c3phajl3GrQBRWdsd2JHRjViR2x6ZEhNWUF5QUJNQUk0QVdBQmFnQjZabEZWYkZCaE1XczFVbFpHZDJGV09XNWxWelI0V0RGR2VWSnVWbUZOV0Vwc1ZHcG5lRmd3TVU1aVZXdDRWMWN4YzFGdFNuTmtlbWh4VGpCd1NWTllVa1pTYTJNeFlVUmtlRmt3Y0ZWVWJWRXdWbnBzTkU1V1JqRmhNVGxFVm14dmQwMXFhRzVXZDdnQkFBJTNEJTNE", false).should eq("AIOkY9EQpi_gyn1_QrFuZ1reN81_MMmI1YmlBblw8j7JHItEFG5h7qcJTNd4W9x5Quk_CVZ028gW")
- end
- end
-
describe "#produce_playlist_continuation" do
it "correctly produces ctoken for requesting index `x` of a playlist" do
produce_playlist_continuation("UUCla9fZca4I7KagBtgRGnOw", 100).should eq("4qmFsgJNEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoUQ0FGNkJsQlVPa05IVVElM0QlM0SaAhhVVUNsYTlmWmNhNEk3S2FnQnRnUkduT3c%3D")
diff --git a/src/invidious.cr b/src/invidious.cr
index 8d579f92..463a286a 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -308,9 +308,17 @@ end
Invidious::Routing.get "/", Invidious::Routes::Misc, :home
Invidious::Routing.get "/privacy", Invidious::Routes::Misc, :privacy
Invidious::Routing.get "/licenses", Invidious::Routes::Misc, :licenses
-Invidious::Routing.get "/watch", Invidious::Routes::Watch
+
+Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle
+Invidious::Routing.get "/watch/:id", Invidious::Routes::Watch, :redirect
+Invidious::Routing.get "/shorts/:id", Invidious::Routes::Watch, :redirect
+Invidious::Routing.get "/w/:id", Invidious::Routes::Watch, :redirect
+Invidious::Routing.get "/v/:id", Invidious::Routes::Watch, :redirect
+Invidious::Routing.get "/e/:id", Invidious::Routes::Watch, :redirect
+
Invidious::Routing.get "/embed/", Invidious::Routes::Embed, :redirect
Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed, :show
+
Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Playlists, :index
Invidious::Routing.get "/create_playlist", Invidious::Routes::Playlists, :new
Invidious::Routing.post "/create_playlist", Invidious::Routes::Playlists, :create
@@ -323,12 +331,15 @@ Invidious::Routing.get "/add_playlist_items", Invidious::Routes::Playlists, :add
Invidious::Routing.post "/playlist_ajax", Invidious::Routes::Playlists, :playlist_ajax
Invidious::Routing.get "/playlist", Invidious::Routes::Playlists, :show
Invidious::Routing.get "/mix", Invidious::Routes::Playlists, :mix
+
Invidious::Routing.get "/opensearch.xml", Invidious::Routes::Search, :opensearch
Invidious::Routing.get "/results", Invidious::Routes::Search, :results
Invidious::Routing.get "/search", Invidious::Routes::Search, :search
+
Invidious::Routing.get "/login", Invidious::Routes::Login, :login_page
Invidious::Routing.post "/login", Invidious::Routes::Login, :login
Invidious::Routing.post "/signout", Invidious::Routes::Login, :signout
+
Invidious::Routing.get "/preferences", Invidious::Routes::PreferencesRoute, :show
Invidious::Routing.post "/preferences", Invidious::Routes::PreferencesRoute, :update
Invidious::Routing.get "/toggle_theme", Invidious::Routes::PreferencesRoute, :toggle_theme
@@ -1699,7 +1710,7 @@ get "/channel/:ucid" do |env|
sort_options = {"last", "oldest", "newest"}
sort_by ||= "last"
- items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by)
+ items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
items.uniq! do |item|
if item.responds_to?(:title)
item.title
@@ -1766,7 +1777,7 @@ get "/channel/:ucid/playlists" do |env|
next env.redirect "/channel/#{channel.ucid}"
end
- items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by)
+ items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) }
items.each { |item| item.author = "" }
@@ -2467,7 +2478,7 @@ end
next error_json(500, ex)
end
- items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by)
+ items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
JSON.build do |json|
json.object do
diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr
index 9a129e1e..3109b508 100644
--- a/src/invidious/channels.cr
+++ b/src/invidious/channels.cr
@@ -355,14 +355,22 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
return channel
end
-def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
- if continuation || auto_generated
- url = produce_channel_playlists_url(ucid, continuation, sort_by, auto_generated)
-
- response = YT_POOL.client &.get(url)
+def fetch_channel_playlists(ucid, author, continuation, sort_by)
+ if continuation
+ response_json = request_youtube_api_browse(continuation)
+ result = JSON.parse(response_json)
+ continuationItems = result["onResponseReceivedActions"]?
+ .try &.[0]["appendContinuationItemsAction"]["continuationItems"]
+
+ return [] of SearchItem, nil if !continuationItems
+
+ items = [] of SearchItem
+ continuationItems.as_a.select(&.as_h.has_key?("gridPlaylistRenderer")).each { |item|
+ extract_item(item, author, ucid).try { |t| items << t }
+ }
- continuation = response.body.match(/"continuation":"(?<continuation>[^"]+)"/).try &.["continuation"]?
- initial_data = JSON.parse(response.body).as_a.find(&.["response"]?).try &.as_h
+ continuation = continuationItems.as_a.last["continuationItemRenderer"]?
+ .try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s
else
url = "/channel/#{ucid}/playlists?flow=list&view=1"
@@ -377,13 +385,12 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
end
response = YT_POOL.client &.get(url)
- continuation = response.body.match(/"continuation":"(?<continuation>[^"]+)"/).try &.["continuation"]?
initial_data = extract_initial_data(response.body)
- end
+ return [] of SearchItem, nil if !initial_data
- return [] of SearchItem, nil if !initial_data
- items = extract_items(initial_data)
- continuation = extract_channel_playlists_cursor(continuation, auto_generated) if continuation
+ items = extract_items(initial_data, author, ucid)
+ continuation = response.body.match(/"token":"(?<continuation>[^"]+)"/).try &.["continuation"]?
+ end
return items, continuation
end
@@ -453,6 +460,15 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
end
+# ## NOTE: DEPRECATED
+# Reason -> Unstable
+# The Protobuf object must be provided with an id of the last playlist from the current "page"
+# in order to fetch the next one accurately
+# (if the id isn't included, entries shift around erratically between pages,
+# leading to repetitions and skip overs)
+#
+# Since it's impossible to produce the appropriate Protobuf without an id being provided by the user,
+# it's better to stick to continuation tokens provided by the first request and onward
def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated = false)
object = {
"80226972:embedded" => {
@@ -499,31 +515,6 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
end
-def extract_channel_playlists_cursor(cursor, auto_generated)
- cursor = URI.decode_www_form(cursor)
- .try { |i| Base64.decode(i) }
- .try { |i| IO::Memory.new(i) }
- .try { |i| Protodec::Any.parse(i) }
- .try { |i| i["80226972:0:embedded"]["3:1:base64"].as_h.find { |k, v| k.starts_with? "15:" } }
- .try &.[1]
-
- if cursor.try &.as_h?
- cursor = cursor.try { |i| Protodec::Any.cast_json(i.as_h) }
- .try { |i| Protodec::Any.from_json(i) }
- .try { |i| Base64.urlsafe_encode(i) }
- .try { |i| URI.encode_www_form(i) } || ""
- else
- cursor = cursor.try &.as_s || ""
- end
-
- if !auto_generated
- cursor = URI.decode_www_form(cursor)
- .try { |i| Base64.decode_string(i) }
- end
-
- return cursor
-end
-
# TODO: Add "sort_by"
def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en")
@@ -830,63 +821,87 @@ def get_about_info(ucid, locale)
raise ChannelRedirect.new(channel_id: browse_endpoint["browseId"].to_s)
end
- author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
- author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
- author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
+ auto_generated = false
+ # Check for special auto generated gaming channels
+ if !initdata.has_key?("metadata")
+ auto_generated = true
+ end
+
+ if auto_generated
+ author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s
+ author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s
+ author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s
- ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
+ # Raises a KeyError on failure.
+ banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
+ banner = banners.try &.[-1]?.try &.["url"].as_s?
- # Raises a KeyError on failure.
- banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
- banner = banners.try &.[-1]?.try &.["url"].as_s?
+ description = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"]["simpleText"].as_s
+ description_html = HTML.escape(description).gsub("\n", "<br>")
- # if banner.includes? "channels/c4/default_banner"
- # banner = nil
- # end
+ paid = false
+ is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool
+ allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map { |a| a.as_s }
- description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || ""
- description_html = HTML.escape(description).gsub("\n", "<br>")
+ related_channels = [] of AboutRelatedChannel
+ else
+ author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
+ author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
+ author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
- paid = about.xpath_node(%q(//meta[@itemprop="paid"])).not_nil!["content"] == "True"
- is_family_friendly = about.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True"
- allowed_regions = about.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",")
+ ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
- related_channels = initdata["contents"]["twoColumnBrowseResultsRenderer"]
- .["secondaryContents"]?.try &.["browseSecondaryContentsRenderer"]["contents"][0]?
- .try &.["verticalChannelSectionRenderer"]?.try &.["items"]?.try &.as_a.map do |node|
- renderer = node["miniChannelRenderer"]?
- related_id = renderer.try &.["channelId"]?.try &.as_s?
- related_id ||= ""
+ # Raises a KeyError on failure.
+ banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
+ banner = banners.try &.[-1]?.try &.["url"].as_s?
- related_title = renderer.try &.["title"]?.try &.["simpleText"]?.try &.as_s?
- related_title ||= ""
+ # if banner.includes? "channels/c4/default_banner"
+ # banner = nil
+ # end
- related_author_url = renderer.try &.["navigationEndpoint"]?.try &.["commandMetadata"]?.try &.["webCommandMetadata"]?
- .try &.["url"]?.try &.as_s?
- related_author_url ||= ""
+ description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || ""
+ description_html = HTML.escape(description).gsub("\n", "<br>")
- related_author_thumbnails = renderer.try &.["thumbnail"]?.try &.["thumbnails"]?.try &.as_a?
- related_author_thumbnails ||= [] of JSON::Any
+ paid = about.xpath_node(%q(//meta[@itemprop="paid"])).not_nil!["content"] == "True"
+ is_family_friendly = about.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True"
+ allowed_regions = about.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",")
- related_author_thumbnail = ""
- if related_author_thumbnails.size > 0
- related_author_thumbnail = related_author_thumbnails[-1]["url"]?.try &.as_s?
- related_author_thumbnail ||= ""
- end
+ related_channels = initdata["contents"]["twoColumnBrowseResultsRenderer"]
+ .["secondaryContents"]?.try &.["browseSecondaryContentsRenderer"]["contents"][0]?
+ .try &.["verticalChannelSectionRenderer"]?.try &.["items"]?.try &.as_a.map do |node|
+ renderer = node["miniChannelRenderer"]?
+ related_id = renderer.try &.["channelId"]?.try &.as_s?
+ related_id ||= ""
- AboutRelatedChannel.new({
- ucid: related_id,
- author: related_title,
- author_url: related_author_url,
- author_thumbnail: related_author_thumbnail,
- })
- end
- related_channels ||= [] of AboutRelatedChannel
+ related_title = renderer.try &.["title"]?.try &.["simpleText"]?.try &.as_s?
+ related_title ||= ""
+
+ related_author_url = renderer.try &.["navigationEndpoint"]?.try &.["commandMetadata"]?.try &.["webCommandMetadata"]?
+ .try &.["url"]?.try &.as_s?
+ related_author_url ||= ""
+
+ related_author_thumbnails = renderer.try &.["thumbnail"]?.try &.["thumbnails"]?.try &.as_a?
+ related_author_thumbnails ||= [] of JSON::Any
+
+ related_author_thumbnail = ""
+ if related_author_thumbnails.size > 0
+ related_author_thumbnail = related_author_thumbnails[-1]["url"]?.try &.as_s?
+ related_author_thumbnail ||= ""
+ end
+
+ AboutRelatedChannel.new({
+ ucid: related_id,
+ author: related_title,
+ author_url: related_author_url,
+ author_thumbnail: related_author_thumbnail,
+ })
+ end
+ related_channels ||= [] of AboutRelatedChannel
+ end
total_views = 0_i64
joined = Time.unix(0)
tabs = [] of String
- auto_generated = false
tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a?
if !tabs_json.nil?
@@ -904,7 +919,7 @@ def get_about_info(ucid, locale)
joined = channel_about_meta["joinedDateText"]?.try &.["runs"]?.try &.as_a.reduce("") { |acc, node| acc + node["text"].as_s }
.try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0)
- # Auto-generated channels
+ # Normal Auto-generated channels
# https://support.google.com/youtube/answer/2579942
# For auto-generated channels, channel_about_meta only has ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"]
if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) &&
diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr
index 5d127e1a..f6b6acd1 100644
--- a/src/invidious/helpers/helpers.cr
+++ b/src/invidious/helpers/helpers.cr
@@ -42,7 +42,7 @@ struct ConfigPreferences
property player_style : String = "invidious"
property quality : String = "hd720"
property quality_dash : String = "auto"
- property default_home : String = "Popular"
+ property default_home : String? = "Popular"
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"]
property related_videos : Bool = true
property sort : String = "published"
diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr
index 2c95a373..67f496df 100644
--- a/src/invidious/helpers/utils.cr
+++ b/src/invidious/helpers/utils.cr
@@ -9,6 +9,8 @@ def add_yt_headers(request)
return if request.resource.starts_with? "/sorry/index"
request.headers["x-youtube-client-name"] ||= "1"
request.headers["x-youtube-client-version"] ||= "2.20200609"
+ # Preserve original cookies and add new YT consent cookie for EU servers
+ request.headers["cookie"] = "#{request.headers["cookie"]?}; CONSENT=YES+"
if !CONFIG.cookies.empty?
request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
end
diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr
index 71f6a9b8..073a9986 100644
--- a/src/invidious/playlists.cr
+++ b/src/invidious/playlists.cr
@@ -437,7 +437,8 @@ end
def get_playlist_videos(db, playlist, offset, locale = nil, continuation = nil)
# Show empy playlist if requested page is out of range
- if offset >= playlist.video_count
+ # (e.g, when a new playlist has been created, offset will be negative)
+ if offset >= playlist.video_count || offset < 0
return [] of PlaylistVideo
end
diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr
index 73c14155..1f7fa27d 100644
--- a/src/invidious/routes/playlists.cr
+++ b/src/invidious/routes/playlists.cr
@@ -434,7 +434,7 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute
end
page_count = (playlist.video_count / 100).to_i
- page_count = 1 if page_count == 0
+ page_count += 1 if (playlist.video_count % 100) > 0
if page > page_count
return env.redirect "/playlist?list=#{plid}&page=#{page_count}"
diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr
index 8169e1ed..4f6e9598 100644
--- a/src/invidious/routes/watch.cr
+++ b/src/invidious/routes/watch.cr
@@ -187,4 +187,8 @@ class Invidious::Routes::Watch < Invidious::Routes::BaseRoute
templated "watch"
end
+
+ def redirect(env)
+ return env.redirect "/watch?v=#{env.params.url["id"]}"
+ end
end
diff --git a/src/invidious/search.cr b/src/invidious/search.cr
index cf8fd790..4b216613 100644
--- a/src/invidious/search.cr
+++ b/src/invidious/search.cr
@@ -231,20 +231,32 @@ end
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist
def channel_search(query, page, channel)
- response = YT_POOL.client &.get("/channel/#{channel}?hl=en&gl=US")
- response = YT_POOL.client &.get("/user/#{channel}?hl=en&gl=US") if response.headers["location"]?
- response = YT_POOL.client &.get("/c/#{channel}?hl=en&gl=US") if response.headers["location"]?
+ response = YT_POOL.client &.get("/channel/#{channel}")
+
+ if response.status_code == 404
+ response = YT_POOL.client &.get("/user/#{channel}")
+ response = YT_POOL.client &.get("/c/#{channel}") if response.status_code == 404
+ initial_data = extract_initial_data(response.body)
+ ucid = initial_data["header"]["c4TabbedHeaderRenderer"]?.try &.["channelId"].as_s?
+ raise InfoException.new("Impossible to extract channel ID from page") if !ucid
+ else
+ ucid = channel
+ end
- ucid = response.body.match(/\\"channelId\\":\\"(?<ucid>[^\\]+)\\"/).try &.["ucid"]?
+ continuation = produce_channel_search_continuation(ucid, query, page)
+ response_json = request_youtube_api_browse(continuation)
- return 0, [] of SearchItem if !ucid
+ result = JSON.parse(response_json)
+ continuationItems = result["onResponseReceivedActions"]?
+ .try &.[0]["appendContinuationItemsAction"]["continuationItems"]
- url = produce_channel_search_url(ucid, query, page)
- response = YT_POOL.client &.get(url)
- initial_data = JSON.parse(response.body).as_a.find &.["response"]?
- return 0, [] of SearchItem if !initial_data
- author = initial_data["response"]?.try &.["metadata"]?.try &.["channelMetadataRenderer"]?.try &.["title"]?.try &.as_s
- items = extract_items(initial_data.as_h, author, ucid)
+ return 0, [] of SearchItem if !continuationItems
+
+ items = [] of SearchItem
+ continuationItems.as_a.select(&.as_h.has_key?("itemSectionRenderer")).each { |item|
+ extract_item(item["itemSectionRenderer"]["contents"].as_a[0])
+ .try { |t| items << t }
+ }
return items.size, items
end
@@ -361,17 +373,28 @@ def produce_search_params(page = 1, sort : String = "relevance", date : String =
return params
end
-def produce_channel_search_url(ucid, query, page)
+def produce_channel_search_continuation(ucid, query, page)
+ if page <= 1
+ idx = 0_i64
+ else
+ idx = 30_i64 * (page - 1)
+ end
+
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:base64" => {
"2:string" => "search",
+ "6:varint" => 1_i64,
"7:varint" => 1_i64,
- "15:string" => "#{page}",
+ "12:varint" => 1_i64,
+ "15:base64" => {
+ "3:varint" => idx,
+ },
"23:varint" => 0_i64,
},
"11:string" => query,
+ "35:string" => "browse-feed#{ucid}search",
},
}
@@ -380,7 +403,7 @@ def produce_channel_search_url(ucid, query, page)
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
- return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
+ return continuation
end
def process_search_query(query, page, user, region)
diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr
index 8d078387..910a99d8 100644
--- a/src/invidious/trending.cr
+++ b/src/invidious/trending.cr
@@ -6,24 +6,22 @@ def fetch_trending(trending_type, region, locale)
plid = nil
if trending_type && trending_type != "Default"
- trending_type = trending_type.downcase.capitalize
+ if trending_type == "Music"
+ trending_type = 1
+ elsif trending_type == "Gaming"
+ trending_type = 2
+ elsif trending_type == "Movies"
+ trending_type = 3
+ end
response = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body
initial_data = extract_initial_data(response)
+ url = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][trending_type]["tabRenderer"]["endpoint"]["commandMetadata"]["webCommandMetadata"]["url"]
+ url = "#{url}&gl=#{region}&hl=en"
- tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][0]["tabRenderer"]["content"]["sectionListRenderer"]["subMenu"]["channelListSubMenuRenderer"]["contents"].as_a
- url = tabs.select { |tab| tab["channelListSubMenuAvatarRenderer"]["title"]["simpleText"] == trending_type }[0]?
-
- if url
- url["channelListSubMenuAvatarRenderer"]["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"]
- url = url["channelListSubMenuAvatarRenderer"]["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"].as_s
- url = "#{url}&gl=#{region}&hl=en"
- trending = YT_POOL.client &.get(url).body
- plid = extract_plid(url)
- else
- trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body
- end
+ trending = YT_POOL.client &.get(url).body
+ plid = extract_plid(url)
else
trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body
end
diff --git a/src/invidious/users.cr b/src/invidious/users.cr
index 7a948b76..8fef64a0 100644
--- a/src/invidious/users.cr
+++ b/src/invidious/users.cr
@@ -68,7 +68,7 @@ struct Preferences
property quality : String = CONFIG.default_user_preferences.quality
@[JSON::Field(converter: Preferences::ProcessString)]
property quality_dash : String = CONFIG.default_user_preferences.quality_dash
- property default_home : String = CONFIG.default_user_preferences.default_home
+ property default_home : String? = CONFIG.default_user_preferences.default_home
property feed_menu : Array(String) = CONFIG.default_user_preferences.feed_menu
property related_videos : Bool = CONFIG.default_user_preferences.related_videos
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr
index 61b900e3..68cacc0f 100644
--- a/src/invidious/views/template.ecr
+++ b/src/invidious/views/template.ecr
@@ -26,7 +26,7 @@
<span style="display:none" id="dark_mode_pref"><%= env.get("preferences").as(Preferences).dark_mode %></span>
<div class="pure-g">
<div class="pure-u-1 pure-u-md-2-24"></div>
- <div class="pure-u-1 pure-u-md-20-24">
+ <div class="pure-u-1 pure-u-md-20-24", id="contents">
<div class="pure-g navbar h-box">
<div class="pure-u-1 pure-u-md-4-24">
<a href="/" class="index-link pure-menu-heading">Invidious</a>
@@ -106,7 +106,7 @@
<%= content %>
- <div class="footer">
+ <footer>
<div class="pure-g">
<div class="pure-u-1 pure-u-md-1-3">
<a href="https://github.com/iv-org/invidious">
@@ -140,7 +140,7 @@
<%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %>
</div>
</div>
- </div>
+ </footer>
</div>
<div class="pure-u-1 pure-u-md-2-24"></div>
</div>
diff --git a/src/invidious/views/trending.ecr b/src/invidious/views/trending.ecr
index 42acb15c..3ec62555 100644
--- a/src/invidious/views/trending.ecr
+++ b/src/invidious/views/trending.ecr
@@ -21,7 +21,7 @@
</div>
<div class="pure-u-1-3">
<div class="pure-g" style="text-align:right">
- <% {"Default", "Music", "Gaming", "News", "Movies"}.each do |option| %>
+ <% {"Default", "Music", "Gaming", "Movies"}.each do |option| %>
<div class="pure-u-1 pure-md-1-3">
<% if trending_type == option %>
<b><%= translate(locale, option) %></b>