summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--assets/css/default.css2
-rw-r--r--locales/bn_BD.json10
-rw-r--r--locales/da.json4
-rw-r--r--locales/sr.json414
-rw-r--r--locales/sr_Cyrl.json98
-rw-r--r--spec/helpers_spec.cr14
-rw-r--r--src/invidious/comments.cr12
-rw-r--r--src/invidious/helpers/utils.cr2
-rw-r--r--src/invidious/routes/playlists.cr2
-rw-r--r--src/invidious/search.cr51
-rw-r--r--src/invidious/trending.cr24
-rw-r--r--src/invidious/views/components/item.ecr5
-rw-r--r--src/invidious/views/trending.ecr2
13 files changed, 531 insertions, 109 deletions
diff --git a/assets/css/default.css b/assets/css/default.css
index a76ecd48..2552263d 100644
--- a/assets/css/default.css
+++ b/assets/css/default.css
@@ -641,7 +641,7 @@ body.dark-theme {
}
#filters > summary {
- display: inline-block;
+ display: block;
margin-bottom: 15px;
}
diff --git a/locales/bn_BD.json b/locales/bn_BD.json
index 8356424f..0662a87c 100644
--- a/locales/bn_BD.json
+++ b/locales/bn_BD.json
@@ -59,11 +59,11 @@
"Autoplay: ": "স্বয়ংক্রিয় চালু: ",
"Play next by default: ": "ডিফল্টভাবে পরবর্তী চালাও: ",
"Autoplay next video: ": "পরবর্তী ভিডিও স্বয়ংক্রিয়ভাবে চালাও: ",
- "Listen by default: ": "",
- "Proxy videos: ": "",
- "Default speed: ": "",
- "Preferred video quality: ": "",
- "Player volume: ": "",
+ "Listen by default: ": "সহজাতভাবে শোনো: ",
+ "Proxy videos: ": "ভিডিও প্রক্সি করো: ",
+ "Default speed: ": "সহজাত গতি: ",
+ "Preferred video quality: ": "পছন্দের ভিডিও মান: ",
+ "Player volume: ": "প্লেয়ার শব্দের মাত্রা: ",
"Default comments: ": "",
"youtube": "",
"reddit": "",
diff --git a/locales/da.json b/locales/da.json
index 1944e47b..ac60862c 100644
--- a/locales/da.json
+++ b/locales/da.json
@@ -44,7 +44,7 @@
"Export data as JSON": "Exporter data som JSON",
"Delete account?": "Slet konto?",
"History": "Historik",
- "An alternative front-end to YouTube": "",
+ "An alternative front-end to YouTube": "En alternativ forside til YouTube",
"JavaScript license information": "JavaScript licens information",
"source": "kilde",
"Log in": "Log på",
@@ -73,7 +73,7 @@
"Default comments: ": "Standard kommentarer: ",
"youtube": "youtube",
"reddit": "reddit",
- "Default captions: ": "",
+ "Default captions: ": "Standard undertekster: ",
"Fallback captions: ": "",
"Show related videos: ": "",
"Show annotations by default: ": "",
diff --git a/locales/sr.json b/locales/sr.json
new file mode 100644
index 00000000..dd8fb2fc
--- /dev/null
+++ b/locales/sr.json
@@ -0,0 +1,414 @@
+{
+ "`x` subscribers": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "`x` пратилаца.([^.,0-9]|^)1([^.,0-9]|$)",
+ "": "`x` пратилаца."
+ },
+ "`x` videos": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "`x` видео записа.([^.,0-9]|^)1([^.,0-9]|$)",
+ "": "`x` видео записа."
+ },
+ "`x` playlists": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "`x` списака извођења.([^.,0-9]|^)1([^.,0-9]|$)",
+ "": "`x` списака извођења."
+ },
+ "LIVE": "УЖИВО",
+ "Shared `x` ago": "Подељено пре `x`",
+ "Unsubscribe": "Прекини праћење",
+ "Subscribe": "Прати",
+ "View channel on YouTube": "Погледај канал на YouTube-у",
+ "View playlist on YouTube": "Погледај списак извођења на YouTube-у",
+ "newest": "најновије",
+ "oldest": "најстарије",
+ "popular": "гласовито",
+ "last": "последње",
+ "Next page": "Следећа страница",
+ "Previous page": "Претходна страница",
+ "Clear watch history?": "Избрисати повест прегледања?",
+ "New password": "Нова запорка",
+ "New passwords must match": "Нове запорке морају бити истоветне",
+ "Cannot change password for Google accounts": "Није могуће променити запорку за Google налоге",
+ "Authorize token?": "Овласти токен?",
+ "Authorize token for `x`?": "Овласти токен за `x`?",
+ "Yes": "Да",
+ "No": "Не",
+ "Import and Export Data": "Увоз и извоз података",
+ "Import": "Увези",
+ "Import Invidious data": "Увези податке са Invidious-а",
+ "Import YouTube subscriptions": "Увези праћења са YouTube-а",
+ "Import FreeTube subscriptions (.db)": "Увези праћења са FreeTube-а (.db)",
+ "Import NewPipe subscriptions (.json)": "Увези праћења са NewPipe-а (.json)",
+ "Import NewPipe data (.zip)": "Увези податке са NewPipe-а (.zip)",
+ "Export": "Извези",
+ "Export subscriptions as OPML": "Извези праћења као OPML датотеку",
+ "Export subscriptions as OPML (for NewPipe & FreeTube)": "Извези праћења као OPML датотеку (за NewPipe и FreeTube)",
+ "Export data as JSON": "Извези податке као JSON датотеку",
+ "Delete account?": "Избрисати рачун?",
+ "History": "Повест",
+ "An alternative front-end to YouTube": "Заменски кориснички слој за YouTube",
+ "JavaScript license information": "Извештај о JavaScript одобрењу",
+ "source": "извор",
+ "Log in": "Пријави се",
+ "Log in/register": "Пријави се/Отвори налог",
+ "Log in with Google": "Пријави се помоћу Google-а",
+ "User ID": "Кориснички ИД",
+ "Password": "Запорка",
+ "Time (h:mm:ss):": "Време (ч:мм:сс):",
+ "Text CAPTCHA": "Знаковни CAPTCHA",
+ "Image CAPTCHA": "Сликовни CAPTCHA",
+ "Sign In": "Пријава",
+ "Register": "Отвори налог",
+ "E-mail": "Е-пошта",
+ "Google verification code": "Google-ов оверни кôд",
+ "Preferences": "Подешавања",
+ "Player preferences": "Подешавања репродуктора",
+ "Always loop: ": "Увек понављај: ",
+ "Autoplay: ": "Самопуштање: ",
+ "Play next by default: ": "Увек подразумевано пуштај следеће: ",
+ "Autoplay next video: ": "Самопуштање следећег видео записа: ",
+ "Listen by default: ": "Увек подразумевано укључен само звук: ",
+ "Proxy videos: ": "Приказ видео записа преко посредника: ",
+ "Default speed: ": "",
+ "Preferred video quality: ": "",
+ "Player volume: ": "",
+ "Default comments: ": "",
+ "youtube": "",
+ "reddit": "",
+ "Default captions: ": "",
+ "Fallback captions: ": "",
+ "Show related videos: ": "",
+ "Show annotations by default: ": "",
+ "Visual preferences": "",
+ "Player style: ": "",
+ "Dark mode: ": "",
+ "Theme: ": "",
+ "dark": "",
+ "light": "",
+ "Thin mode: ": "",
+ "Subscription preferences": "",
+ "Show annotations by default for subscribed channels: ": "",
+ "Redirect homepage to feed: ": "",
+ "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": "",
+ "`x` subscriptions": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "`x` tokens": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "Import/export": "",
+ "unsubscribe": "",
+ "revoke": "",
+ "Subscriptions": "",
+ "`x` unseen notifications": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "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": "",
+ "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`": "",
+ "`x` views": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "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": "",
+ "View `x` comments": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "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.": "",
+ "Invalid TFA code": "",
+ "Login failed. This may be because two-factor authentication is not turned on for your account.": "",
+ "Wrong answer": "",
+ "Erroneous CAPTCHA": "",
+ "CAPTCHA is a required field": "",
+ "User ID is a required field": "",
+ "Password is a required field": "",
+ "Wrong username or password": "",
+ "Please sign in using 'Log in with Google'": "",
+ "Password cannot be empty": "",
+ "Password cannot be longer than 55 characters": "",
+ "Please log in": "",
+ "Invidious Private Feed for `x`": "",
+ "channel:`x`": "",
+ "Deleted or invalid channel": "",
+ "This channel does not exist.": "",
+ "Could not get channel info.": "",
+ "Could not fetch comments": "",
+ "View `x` replies": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "`x` ago": "",
+ "Load more": "",
+ "`x` points": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "Could not create mix.": "",
+ "Empty playlist": "",
+ "Not a playlist.": "",
+ "Playlist does not exist.": "",
+ "Could not pull trending pages.": "",
+ "Hidden field \"challenge\" is a required field": "",
+ "Hidden field \"token\" is a required field": "",
+ "Erroneous challenge": "",
+ "Erroneous token": "",
+ "No such user": "",
+ "Token is expired, please try again": "",
+ "English": "",
+ "English (auto-generated)": "",
+ "Afrikaans": "",
+ "Albanian": "",
+ "Amharic": "",
+ "Arabic": "",
+ "Armenian": "",
+ "Azerbaijani": "",
+ "Bangla": "",
+ "Basque": "",
+ "Belarusian": "",
+ "Bosnian": "",
+ "Bulgarian": "",
+ "Burmese": "",
+ "Catalan": "",
+ "Cebuano": "",
+ "Chinese (Simplified)": "",
+ "Chinese (Traditional)": "",
+ "Corsican": "",
+ "Croatian": "",
+ "Czech": "",
+ "Danish": "",
+ "Dutch": "",
+ "Esperanto": "",
+ "Estonian": "",
+ "Filipino": "",
+ "Finnish": "",
+ "French": "",
+ "Galician": "",
+ "Georgian": "",
+ "German": "",
+ "Greek": "",
+ "Gujarati": "",
+ "Haitian Creole": "",
+ "Hausa": "",
+ "Hawaiian": "",
+ "Hebrew": "",
+ "Hindi": "",
+ "Hmong": "",
+ "Hungarian": "",
+ "Icelandic": "",
+ "Igbo": "",
+ "Indonesian": "",
+ "Irish": "",
+ "Italian": "",
+ "Japanese": "",
+ "Javanese": "",
+ "Kannada": "",
+ "Kazakh": "",
+ "Khmer": "",
+ "Korean": "",
+ "Kurdish": "",
+ "Kyrgyz": "",
+ "Lao": "",
+ "Latin": "",
+ "Latvian": "",
+ "Lithuanian": "",
+ "Luxembourgish": "",
+ "Macedonian": "",
+ "Malagasy": "",
+ "Malay": "",
+ "Malayalam": "",
+ "Maltese": "",
+ "Maori": "",
+ "Marathi": "",
+ "Mongolian": "",
+ "Nepali": "",
+ "Norwegian Bokmål": "",
+ "Nyanja": "",
+ "Pashto": "",
+ "Persian": "",
+ "Polish": "",
+ "Portuguese": "",
+ "Punjabi": "",
+ "Romanian": "",
+ "Russian": "",
+ "Samoan": "",
+ "Scottish Gaelic": "",
+ "Serbian": "",
+ "Shona": "",
+ "Sindhi": "",
+ "Sinhala": "",
+ "Slovak": "",
+ "Slovenian": "",
+ "Somali": "",
+ "Southern Sotho": "",
+ "Spanish": "",
+ "Spanish (Latin America)": "",
+ "Sundanese": "",
+ "Swahili": "",
+ "Swedish": "",
+ "Tajik": "",
+ "Tamil": "",
+ "Telugu": "",
+ "Thai": "",
+ "Turkish": "",
+ "Ukrainian": "",
+ "Urdu": "",
+ "Uzbek": "",
+ "Vietnamese": "",
+ "Welsh": "",
+ "Western Frisian": "",
+ "Xhosa": "",
+ "Yiddish": "",
+ "Yoruba": "",
+ "Zulu": "",
+ "`x` years": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "`x` months": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "`x` weeks": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "`x` days": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "`x` hours": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "`x` minutes": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "`x` seconds": {
+ "([^.,0-9]|^)1([^.,0-9]|$)": "",
+ "": ""
+ },
+ "Fallback comments: ": "",
+ "Popular": "",
+ "Top": "",
+ "About": "",
+ "Rating: ": "",
+ "Language: ": "",
+ "View as playlist": "",
+ "Default": "",
+ "Music": "",
+ "Gaming": "",
+ "News": "",
+ "Movies": "",
+ "Download": "",
+ "Download as: ": "",
+ "%A %B %-d, %Y": "",
+ "(edited)": "",
+ "YouTube comment permalink": "",
+ "permalink": "",
+ "`x` marked it with a ❤": "",
+ "Audio mode": "",
+ "Video mode": "",
+ "Videos": "",
+ "Playlists": "",
+ "Community": "",
+ "relevance": "",
+ "rating": "",
+ "date": "",
+ "views": "",
+ "content_type": "",
+ "duration": "",
+ "features": "",
+ "sort": "",
+ "hour": "",
+ "today": "",
+ "week": "",
+ "month": "",
+ "year": "",
+ "video": "",
+ "channel": "",
+ "playlist": "",
+ "movie": "",
+ "show": "",
+ "hd": "",
+ "subtitles": "",
+ "creative_commons": "",
+ "3d": "",
+ "live": "",
+ "4k": "",
+ "location": "",
+ "hdr": "",
+ "filter": "",
+ "Current version: ": ""
+}
diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json
index 0ca9a8a0..adb25544 100644
--- a/locales/sr_Cyrl.json
+++ b/locales/sr_Cyrl.json
@@ -105,61 +105,61 @@
"Default homepage: ": "Подразумевана главна страница: ",
"Feed menu: ": "Мени довода: ",
"Top enabled: ": "",
- "CAPTCHA enabled: ": "",
- "Login enabled: ": "",
- "Registration enabled: ": "",
+ "CAPTCHA enabled: ": "CAPTCHA укључена?: ",
+ "Login enabled: ": "Пријава укључена?: ",
+ "Registration enabled: ": "Регистрација укључена?: ",
"Report statistics: ": "",
- "Save preferences": "",
- "Subscription manager": "",
- "Token manager": "",
- "Token": "",
- "`x` subscriptions.": "",
- "`x` tokens.": "",
- "Import/export": "",
- "unsubscribe": "",
- "revoke": "",
- "Subscriptions": "",
- "`x` unseen notifications.": "",
- "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": "",
- "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: ": "",
+ "Save preferences": "Сачувај подешавања",
+ "Subscription manager": "Управљање праћењима",
+ "Token manager": "Управљање токенима",
+ "Token": "Токен",
+ "`x` subscriptions.": "`x`праћења.",
+ "`x` tokens.": "`x`токена.",
+ "Import/export": "Увези/извези",
+ "unsubscribe": "укини праћење",
+ "revoke": "опозови",
+ "Subscriptions": "Праћења",
+ "`x` unseen notifications.": "`x` непрочитаних обавештења.",
+ "search": "претрага",
+ "Log out": "Одјавите се",
+ "Released under the AGPLv3 by Omar Roth.": "Издао Омар Рот (Omar Roth) под условима AGPLv3 лиценце.",
+ "Source available here.": "Изворни код доступан овде.",
+ "View JavaScript license information.": "Прикажи информације о JavaScript лиценци.",
+ "View privacy policy.": "Прикажи извештај о приватности.",
+ "Trending": "У тренду",
+ "Public": "Јавно",
+ "Unlisted": "По позиву",
+ "Private": "Приватно",
+ "View all playlists": "Прикажи све плејлисте",
+ "Updated `x` ago": "Ажурирано пре `x`",
+ "Delete playlist `x`?": "Избриши плејлисту `x`?",
+ "Delete playlist": "Избриши плејлисту",
+ "Create playlist": "Направи плејлисту",
+ "Title": "Наслов",
+ "Playlist privacy": "Видљивост плејлисте",
+ "Editing playlist `x`": "Уређујете плејлисту `x`",
+ "Watch on YouTube": "Гледајте на YouTube-у",
+ "Hide annotations": "Сакриј анотације",
+ "Show annotations": "Прикажи анотације",
+ "Genre: ": "Жанр: ",
+ "License: ": "Лиценца: ",
"Family friendly? ": "",
"Wilson score: ": "",
- "Engagement: ": "",
- "Whitelisted regions: ": "",
- "Blacklisted regions: ": "",
+ "Engagement: ": "Ангажовање: ",
+ "Whitelisted regions: ": "Дозвољене области: ",
+ "Blacklisted regions: ": "Забрањене области: ",
"Shared `x`": "",
- "`x` views.": "",
- "Premieres in `x`": "",
+ "`x` views.": "`x` прегледа.",
+ "Premieres in `x`": "Емитује се уживо за `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": "",
+ "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Здраво! Изгледа да је искључен JavaScript. Кликните овде да бисте приказали коментаре. Требаће мало дуже да се учитају.",
+ "View YouTube comments": "Прикажи коментаре са YouTube-а",
+ "View more comments on Reddit": "Прикажи још коментара на Reddit-у",
"View `x` comments.": "",
- "View Reddit comments": "",
- "Hide replies": "",
- "Show replies": "",
- "Incorrect password": "",
+ "View Reddit comments": "Прикажи коментаре са Reddit-а",
+ "Hide replies": "Сакриј одговоре",
+ "Show replies": "Прикажи одговоре",
+ "Incorrect password": "Неисправна лозинка",
"Quota exceeded, try again in a few hours": "",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "",
"Invalid TFA code": "",
diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr
index a58c1e5a..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
@@ -65,14 +65,6 @@ describe "Helper" do
end
end
- describe "#extract_comment_cursor" do
- it "correctly extracts a comment cursor from a given continuation" do
- extract_comment_cursor("EiYSC2tKUVA3a2l3NUZrwAEByAEB4AEBogINKP___________wFAABgGMpwFCoYFQURTSl9pM1RqN1VlZ3dBd1daZkk4TmNiZ0djLVp0NFZEaW1BUGZwWHlPNDhuYUFxa3BsOXZYTk41OWpGXzNGRkVZeVpJOHRGWWpla0w1Z2ktcjhLdGFhcmduMDFxTUpsQ19QN2NaLWU5VGxxbTgzeUN6QVFHSUVtMGlMbUs5ZmVNOUVmNVo2S24xclpPRmlOdkxJS3JIUlJhWS10dkFNdzBDb0R3UWxiSXdpNDAzNkNCQ0ZXY2syemh1VHBsdEVUa2RmRHVrYVdkNnR1X1F4dkdnMGRkeEMydnNuVnlsQ1lJSUliWjAwMk1UTmpsbWJ5ejNKeGVybHJoa1drNW9kODZhOS16RVBPMjRHVzRKZnJlZEFvdGtzRmtCUUx5RWNRbkxRdHVyMHNwbGNmLUswZUttTlZkbk1DY1JVUF9LaU8tdVk4Qmg4RmtCa2RwMTFhVW10R0tzMWM0VjZXVkwwc29TallQc0VGLUF0LWlEVENJVXRNT1RLZklMblJ2V2NJclJvWndUNHA2MXFFMnhuN01CSFVJMzJJRjhJN2pKanh4a2o3ekMtUXBuT0xFdUNGOGJlN29kekFDa2VfTzVZNnpHM1FzN0lDM3NvV0NFbVJiLXlPNzB0ZDlXS3lXc25UNTJqM0FVT3hiQW16NU1EeU9qUVN3SERLNlFmaVh6N3ZjbGZnWEgxSUlqVmFCVUc3bkhlZkFOMlNoZ1BnN1hwaHBrV0FUdUtnRjNtRnBNRmViTFp2bHVPQ1k1WkgxVTh5LWV1ZnN5UUhxQkZJVlh0Mkg1NEFVa0xZeGdORmJTY0dfaEE4dEswV0JwdkdGUmE0V2dmT3NsNjlRSmRISTBKbWlOeS1rdyIPIgtrSlFQN2tpdzVGazAAKCg%3D").should eq("ADSJ_i3Tj7UegwAwWZfI8NcbgGc-Zt4VDimAPfpXyO48naAqkpl9vXNN59jF_3FFEYyZI8tFYjekL5gi-r8Ktaargn01qMJlC_P7cZ-e9Tlqm83yCzAQGIEm0iLmK9feM9Ef5Z6Kn1rZOFiNvLIKrHRRaY-tvAMw0CoDwQlbIwi4036CBCFWck2zhuTpltETkdfDukaWd6tu_QxvGg0ddxC2vsnVylCYIIIbZ002MTNjlmbyz3JxerlrhkWk5od86a9-zEPO24GW4JfredAotksFkBQLyEcQnLQtur0splcf-K0eKmNVdnMCcRUP_KiO-uY8Bh8FkBkdp11aUmtGKs1c4V6WVL0soSjYPsEF-At-iDTCIUtMOTKfILnRvWcIrRoZwT4p61qE2xn7MBHUI32IF8I7jJjxxkj7zC-QpnOLEuCF8be7odzACke_O5Y6zG3Qs7IC3soWCEmRb-yO70td9WKyWsnT52j3AUOxbAmz5MDyOjQSwHDK6QfiXz7vclfgXH1IIjVaBUG7nHefAN2ShgPg7XphpkWATuKgF3mFpMFebLZvluOCY5ZH1U8y-eufsyQHqBFIVXt2H54AUkLYxgNFbScG_hA8tK0WBpvGFRa4WgfOsl69QJdHI0JmiNy-kw")
-
- extract_comment_cursor("EiYSC2tKUVA3a2l3NUZrwAEByAEB4AEBogINKP___________wFAABgGMo4DCvgCQURTSl9pMEhLLWg2SGRybURYZV93VXA3b1VuVmhFZlJtcUNndUxPaEtTNnlONURSdTAxZ2RQUVBEQkw3ZFVJci1fNDRPc3dVUDF0WjE1YVczMUJjN1JNb2ZCdzc0cDhyVnFLcWVzUDFPZnhOXzhDRlV2ZHo0aDlvalM1UzFJbjEzVGVXQkx5TmxlcHhRSy00Ymhwd1I0Q3FIN2I1YlBvMkw2ZE8xdklXc3VsRmJQQXpQb29XTkhPdGlHdlRsbmFybEl2VFBPb3BzcTFsd3RUanhSZ25yU0d2SlhscHFPeUpZb0tyR01Cam5nREk2ZFMxcTU2UEt1ajlvbTc4WTFvckhiZzhaOEZrNG54NUFDd2lCSjYtLTBoOXhpNnpSMi1oeTRnTTlGWnFIeHU1QlgwQzBCczJ0WEJ4V1BoTWVPVUtPVjh6UVFaOTNXdTlhc284THdPMVVJZmtkdWgxSTVMY0NaWUlPLXd1c1UxcnN5MWV5ekQtZ0NBTiIPIgtrSlFQN2tpdzVGazAAKCg%3D").should eq("ADSJ_i0HK-h6HdrmDXe_wUp7oUnVhEfRmqCguLOhKS6yN5DRu01gdPQPDBL7dUIr-_44OswUP1tZ15aW31Bc7RMofBw74p8rVqKqesP1OfxN_8CFUvdz4h9ojS5S1In13TeWBLyNlepxQK-4bhpwR4CqH7b5bPo2L6dO1vIWsulFbPAzPooWNHOtiGvTlnarlIvTPOopsq1lwtTjxRgnrSGvJXlpqOyJYoKrGMBjngDI6dS1q56PKuj9om78Y1orHbg8Z8Fk4nx5ACwiBJ6--0h9xi6zR2-hy4gM9FZqHxu5BX0C0Bs2tXBxWPhMeOUKOV8zQQZ93Wu9aso8LwO1UIfkduh1I5LcCZYIO-wusU1rsy1eyzD-gCAN")
- end
- end
-
describe "#produce_comment_continuation" do
it "correctly produces a continuation token for comments" do
produce_comment_continuation("_cE8xSu6swE", "ADSJ_i2qvJeFtL0htmS5_K5Ctj3eGFVBMWL9Wd42o3kmUL6_mAzdLp85-liQZL0mYr_16BhaggUqX652Sv9JqV6VXinShSP-ZT6rL4NolPBaPXVtJsO5_rA_qE3GubAuLFw9uzIIXU2-HnpXbdgPLWTFavfX206hqWmmpHwUOrmxQV_OX6tYkM3ux3rPAKCDrT8eWL7MU3bLiNcnbgkW8o0h8KYLL_8BPa8LcHbTv8pAoNkjerlX1x7K4pqxaXPoyz89qNlnh6rRx6AXgAzzoHH1dmcyQ8CIBeOHg-m4i8ZxdX4dP88XWrIFg-jJGhpGP8JUMDgZgavxVx225hUEYZMyrLGler5em4FgbG62YWC51moLDLeYEA").should eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyjAMK9gJBRFNKX2kycXZKZUZ0TDBodG1TNV9LNUN0ajNlR0ZWQk1XTDlXZDQybzNrbVVMNl9tQXpkTHA4NS1saVFaTDBtWXJfMTZCaGFnZ1VxWDY1MlN2OUpxVjZWWGluU2hTUC1aVDZyTDROb2xQQmFQWFZ0SnNPNV9yQV9xRTNHdWJBdUxGdzl1eklJWFUyLUhucFhiZGdQTFdURmF2ZlgyMDZocVdtbXBId1VPcm14UVZfT1g2dFlrTTN1eDNyUEFLQ0RyVDhlV0w3TVUzYkxpTmNuYmdrVzhvMGg4S1lMTF84QlBhOExjSGJUdjhwQW9Oa2plcmxYMXg3SzRwcXhhWFBveXo4OXFObG5oNnJSeDZBWGdBenpvSEgxZG1jeVE4Q0lCZU9IZy1tNGk4WnhkWDRkUDg4WFdySUZnLWpKR2hwR1A4SlVNRGdaZ2F2eFZ4MjI1aFVFWVpNeXJMR2xlcjVlbTRGZ2JHNjJZV0M1MW1vTERMZVlFQSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D")
diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr
index e7e87203..5d72503e 100644
--- a/src/invidious/comments.cr
+++ b/src/invidious/comments.cr
@@ -226,7 +226,7 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so
if body["continuations"]?
continuation = body["continuations"][0]["nextContinuationData"]["continuation"].as_s
- json.field "continuation", cursor.try &.starts_with?("E") ? continuation : extract_comment_cursor(continuation)
+ json.field "continuation", continuation
end
end
end
@@ -580,16 +580,6 @@ def content_to_comment_html(content)
return comment_html
end
-def extract_comment_cursor(continuation)
- cursor = URI.decode_www_form(continuation)
- .try { |i| Base64.decode(i) }
- .try { |i| IO::Memory.new(i) }
- .try { |i| Protodec::Any.parse(i) }
- .try { |i| i["6:2:embedded"]["1:0:string"].as_s }
-
- return cursor
-end
-
def produce_comment_continuation(video_id, cursor = "", sort_by = "top")
object = {
"2:embedded" => {
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/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/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/views/components/item.ecr b/src/invidious/views/components/item.ecr
index ea7d356c..9dfa047e 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -141,7 +141,10 @@
<b style="flex: 1;">
<a style="width:100%" href="/channel/<%= item.ucid %>"><%= item.author %></a>
</b>
- <a title="Audio mode" href="/watch?v=<%= item.id %>&amp;listen=1">
+ <a title="<%=translate(locale, "Watch on YouTube")%>" href="https://www.youtube.com/watch?v=<%= item.id %>" style="margin-right: 5px;">
+ <i class="icon ion-logo-youtube"></i>
+ </a>
+ <a title="<%=translate(locale, "Audio mode")%>" href="/watch?v=<%= item.id %>&amp;listen=1">
<i class="icon ion-md-headset"></i>
</a>
</p>
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>