summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--assets/js/player.js6
-rw-r--r--assets/js/themes.js2
-rw-r--r--config/config.example.yml22
-rw-r--r--locales/ar.json61
-rw-r--r--locales/nb-NO.json9
-rw-r--r--src/invidious/config.cr4
-rw-r--r--src/invidious/exceptions.cr8
-rw-r--r--src/invidious/helpers/i18n.cr2
-rw-r--r--src/invidious/helpers/tokens.cr2
-rw-r--r--src/invidious/helpers/utils.cr12
-rw-r--r--src/invidious/playlists.cr2
-rw-r--r--src/invidious/routes/preferences.cr2
-rw-r--r--src/invidious/videos.cr134
-rw-r--r--src/invidious/views/watch.ecr10
-rw-r--r--src/invidious/yt_backend/extractors.cr18
-rw-r--r--src/invidious/yt_backend/youtube_api.cr14
16 files changed, 198 insertions, 110 deletions
diff --git a/assets/js/player.js b/assets/js/player.js
index 66d1682f..a5ea08ec 100644
--- a/assets/js/player.js
+++ b/assets/js/player.js
@@ -86,7 +86,7 @@ if (location.pathname.startsWith('/embed/')) {
});
}
-// Detect mobile users and initalize mobileUi for better UX
+// Detect mobile users and initialize mobileUi for better UX
// Detection code taken from https://stackoverflow.com/a/20293441
function isMobile() {
@@ -119,7 +119,7 @@ if (isMobile()) {
operations_bar_element.className += " mobile-operations-bar"
player.addChild(operations_bar)
- // Playback menu doesn't work when its initalized outside of the primary control bar
+ // Playback menu doesn't work when it's initialized outside of the primary control bar
playback_element = document.getElementsByClassName("vjs-playback-rate")[0]
operations_bar_element.append(playback_element)
@@ -138,7 +138,7 @@ if (isMobile()) {
player.on('error', function (event) {
if (player.error().code === 2 || player.error().code === 4) {
setTimeout(function (event) {
- console.log('An error occured in the player, reloading...');
+ console.log('An error occurred in the player, reloading...');
var currentTime = player.currentTime();
var playbackRate = player.playbackRate();
diff --git a/assets/js/themes.js b/assets/js/themes.js
index 470f10bf..0214a7f0 100644
--- a/assets/js/themes.js
+++ b/assets/js/themes.js
@@ -77,7 +77,7 @@ function update_mode (mode) {
// If preference for dark mode indicated
set_mode(true);
}
- else if (mode === 'false' /* for backwards compaibility */ || mode === 'light') {
+ else if (mode === 'false' /* for backwards compatibility */ || mode === 'light') {
// If preference for light mode indicated
set_mode(false);
}
diff --git a/config/config.example.yml b/config/config.example.yml
index d1c1f300..59cb486b 100644
--- a/config/config.example.yml
+++ b/config/config.example.yml
@@ -163,7 +163,7 @@ https_only: false
#use_quic: false
##
-## Additionnal cookies to be sent when requesting the youtube API.
+## Additional cookies to be sent when requesting the youtube API.
##
## Accepted values: a string in the format "name1=value1; name2=value2..."
## Default: <none>
@@ -188,7 +188,7 @@ https_only: false
##
## Path to log file. Can be absolute or relative to the invidious
-## binary. This is overriden if "-o OUTPUT" or "--output=OUTPUT"
+## binary. This is overridden if "-o OUTPUT" or "--output=OUTPUT"
## are passed on the command line.
##
## Accepted values: a filesystem path or 'STDOUT'
@@ -197,7 +197,7 @@ https_only: false
#output: STDOUT
##
-## Logging Verbosity. This is overriden if "-l LEVEL" or
+## Logging Verbosity. This is overridden if "-l LEVEL" or
## "--log-level=LEVEL" are passed on the command line.
##
## Accepted values: All, Trace, Debug, Info, Warn, Error, Fatal, Off
@@ -306,7 +306,7 @@ https_only: false
##
## Notes:
## - Setting this to 0 will disable the channel videos crawl job.
-## - This setting is overriden if "-c THREADS" or
+## - This setting is overridden if "-c THREADS" or
## "--channel-threads=THREADS" are passed on the command line.
##
## Accepted values: a positive integer
@@ -328,7 +328,7 @@ full_refresh: false
##
## Notes:
## - Setting this to 0 will disable the channel videos crawl job.
-## - This setting is overriden if "-f THREADS" or
+## - This setting is overridden if "-f THREADS" or
## "--feed-threads=THREADS" are passed on the command line.
##
## Accepted values: a positive integer
@@ -371,7 +371,7 @@ feed_threads: 1
# -----------------------------
-# Miscellanous
+# Miscellaneous
# -----------------------------
##
@@ -433,7 +433,7 @@ feed_threads: 1
#cache_annotations: false
##
-## Source code URL. If your instance is running a modfied source
+## Source code URL. If your instance is running a modified source
## code, you MUST publish it somewhere and set this option.
##
## Accepted values: a string
@@ -520,9 +520,9 @@ default_user_preferences:
#region: US
##
- ## Top 3 prefered languages for video captions.
+ ## Top 3 preferred languages for video captions.
##
- ## Note: overridin the default (no preferred
+ ## Note: overriding the default (no preferred
## caption language) is not recommended, in order
## to not penalize people using other languages.
##
@@ -594,7 +594,7 @@ default_user_preferences:
#feed_menu: ["Popular", "Trending", "Subscriptions", "Playlists"]
##
- ## Default feed to diplay on the home page.
+ ## Default feed to display on the home page.
##
## Note: setting this option to "Popular" has no
## effect when 'popular_enabled' is set to false.
@@ -812,7 +812,7 @@ default_user_preferences:
# -----------------------------
- # Miscellanous
+ # Miscellaneous
# -----------------------------
##
diff --git a/locales/ar.json b/locales/ar.json
index b2845acf..c7220be6 100644
--- a/locales/ar.json
+++ b/locales/ar.json
@@ -154,7 +154,7 @@
"Shared `x`": "شارك منذ `x`",
"Premieres in `x`": "يعرض فى `x`",
"Premieres `x`": "يعرض `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.": "اهلا! يبدو ان الجافاسكريبت معطلة. اضغط هنا لعرض التعليقات, ضع فى إعتبارك انها ستأخذ وقت اطول للعرض.",
+ "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": "عرض المزيد من التعليقات على\\من موقع Reddit",
"View `x` comments": {
@@ -164,25 +164,25 @@
"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.": "غير قادر على تسجيل الدخول, تأكد من تشغيل المصادقة الثنائية 2FA.",
+ "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.": "غير قادر على تسجيل الدخول، تأكد من تشغيل المصادقة الثنائية 2FA.",
"Invalid TFA code": "كود مصادقة ثنائية 2FA غير صحيح",
- "Login failed. This may be because two-factor authentication is not turned on for your account.": "لم يتم تسجيل الدخول. هذا ربما بسبب ان المصادقة الثنائية 2FA معطلة فى حسابك.",
+ "Login failed. This may be because two-factor authentication is not turned on for your account.": "فشل تسجيل الدخول. قد يكون هذا بسبب أن المصادقة الثنائية 2FA معطلة في حسابك.",
"Wrong answer": "إجابة خاطئة",
"Erroneous CAPTCHA": "الكابتشا CAPTCHA غير صاحلة",
"CAPTCHA is a required field": "مكان الكابتشا CAPTCHA مطلوب",
- "User ID is a required field": "مكان إسم المستخدم مطلوب",
- "Password is a required field": "مكان الرقم السرى مطلوب",
- "Wrong username or password": "إسم المستخدم او الرقم السرى غير صحيح",
+ "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": "الرقم السرى لا يتعدى 55 حرف",
+ "Password cannot be empty": "لا يمكن أن تكون كلمة السر فارغة",
+ "Password cannot be longer than 55 characters": "يجب أن لا تتعدى كلمة السر 55 حرفًا",
"Please log in": "الرجاء تسجيل الدخول",
"Invidious Private Feed for `x`": "تغذية Invidious خاصة ل 'x'",
"channel:`x`": "قناة:`x`",
"Deleted or invalid channel": "قناة ممسوحة او غير صالحة",
- "This channel does not exist.": "القناة غير موجودة.",
+ "This channel does not exist.": "هذه القناة غير موجودة.",
"Could not get channel info.": "لم يستطع الحصول على معلومات القناة.",
"Could not fetch comments": "لم يتمكن من إحضار التعليقات",
"`x` ago": "`x` منذ",
@@ -192,22 +192,22 @@
"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": "تحدى غير صالح",
+ "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)": "إنجليزى (تم إنشائة تلقائى)",
+ "Token is expired, please try again": "الرمز منتهى الصلاحية، الرجاء المحاولة مرة اخرى",
+ "English": "إنجليزي",
+ "English (auto-generated)": "إنجليزي (تم إنشائه تلقائيًا)",
"Afrikaans": "الأفريكانية",
"Albanian": "الألبانية",
"Amharic": "الأمهرية",
"Arabic": "العربية",
- "Armenian": "الأرميني",
- "Azerbaijani": "أذربيجان",
+ "Armenian": "الأرمينية",
+ "Azerbaijani": "أذربيجانية",
"Bangla": "البنغالية",
- "Basque": "الباسكي",
+ "Basque": "الباسكية",
"Belarusian": "البيلاروسية",
"Bosnian": "البوسنية",
"Bulgarian": "البلغارية",
@@ -318,18 +318,18 @@
"News": "الأخبار",
"Movies": "الأفلام",
"Download": "نزّل",
- "Download as: ": "نزله ك:. ",
+ "Download as: ": "نزله كـ: ",
"%A %B %-d, %Y": "%A %-d %B %Y",
- "(edited)": "(تم تعديلة)",
+ "(edited)": "(معدّل)",
"YouTube comment permalink": "رابط التعليق على اليوتيوب",
"permalink": "الرابط",
- "`x` marked it with a ❤": "`x` اعجب بهذا",
- "Audio mode": "الوضع الصوتى",
+ "`x` marked it with a ❤": "`x` أعجب بهذا",
+ "Audio mode": "الوضع الصوتي",
"Video mode": "وضع الفيديو",
"Videos": "الفيديوهات",
"Playlists": "قوائم التشغيل",
"Community": "المجتمع",
- "relevance": "ملاءم",
+ "relevance": "ملاؤم",
"rating": "تقييم",
"date": "التاريخ",
"views": "مشاهدات",
@@ -339,9 +339,9 @@
"sort": "فرز",
"hour": "ساعة",
"today": "اليوم",
- "week": "إسبوع",
- "month": "شهر",
- "year": "سنة",
+ "week": "هذا الأسبوع",
+ "month": "هذا الشهر",
+ "year": "هذه السنة",
"video": "فيديو",
"channel": "قناة",
"playlist": "قائمة التشغيل",
@@ -353,7 +353,7 @@
"3d": "ثلاثي الأبعاد",
"live": "مباشر",
"4k": "4k",
- "location": "الاماكن",
+ "location": "الأماكن",
"hdr": "وضع التباين العالي",
"filter": "معامل الفرز",
"Current version: ": "الإصدار الحالي: ",
@@ -398,5 +398,6 @@
"360": "360°",
"download_subtitles": "ترجمات - 'x' (.vtt)",
"invidious": "الخيالي",
- "preferences_save_player_pos_label": "احفظ وقت الفيديو الحالي: "
+ "preferences_save_player_pos_label": "احفظ وقت الفيديو الحالي: ",
+ "crash_page_you_found_a_bug": "يبدو أنك قد وجدت خطأً برمجيًّا في Invidious!"
}
diff --git a/locales/nb-NO.json b/locales/nb-NO.json
index d1ad9c7a..20993b5f 100644
--- a/locales/nb-NO.json
+++ b/locales/nb-NO.json
@@ -430,5 +430,12 @@
"generic_count_minutes": "{{count}} minutt",
"generic_count_minutes_plural": "{{count}} minutter",
"generic_count_years": "{{count}} år",
- "generic_count_years_plural": "{{count}} år"
+ "generic_count_years_plural": "{{count}} år",
+ "crash_page_read_the_faq": "lest de <a href=\"`x`\">Ofte stilte spørsmålene (OSS/FAQ)</a>",
+ "crash_page_search_issue": "søkt etter <a href=\"`x`\">eksisterende utfordringer på Github</a>",
+ "crash_page_you_found_a_bug": "Det ser ut til at du fant en feil i Invidious!",
+ "crash_page_refresh": "forsøkt å <a href=\"`x`\">laste siden på nytt</a>",
+ "crash_page_switch_instance": "forsøkt et <a href=\"`x`\">annet eksemplar</a>",
+ "crash_page_before_reporting": "Før du rapporterer en feil, sikre at du har:",
+ "crash_page_report_issue": "Hvis intet av det overnevnte hjalp, <a href=\"`x`\">lag en ny utfordring på Github</a> (fortrinnsvis på engelsk) og ta med følgende tekstbit i meldingen dit (IKKE oversett denne teksten):"
}
diff --git a/src/invidious/config.cr b/src/invidious/config.cr
index c4a8bf83..72e145da 100644
--- a/src/invidious/config.cr
+++ b/src/invidious/config.cr
@@ -91,8 +91,8 @@ class Config
@[YAML::Field(converter: Preferences::FamilyConverter)]
property force_resolve : Socket::Family = Socket::Family::UNSPEC # Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729)
- property port : Int32 = 3000 # Port to listen for connections (overrided by command line argument)
- property host_binding : String = "0.0.0.0" # Host to bind (overrided by command line argument)
+ property port : Int32 = 3000 # Port to listen for connections (overridden by command line argument)
+ property host_binding : String = "0.0.0.0" # Host to bind (overridden by command line argument)
property pool_size : Int32 = 100 # Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
property use_quic : Bool = false # Use quic transport for youtube api
diff --git a/src/invidious/exceptions.cr b/src/invidious/exceptions.cr
new file mode 100644
index 00000000..391a574d
--- /dev/null
+++ b/src/invidious/exceptions.cr
@@ -0,0 +1,8 @@
+# Exception used to hold the name of the missing item
+# Should be used in all parsing functions
+class BrokenTubeException < InfoException
+ getter element : String
+
+ def initialize(@element)
+ end
+end
diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr
index 3cf9ad1c..6571dbe6 100644
--- a/src/invidious/helpers/i18n.cr
+++ b/src/invidious/helpers/i18n.cr
@@ -135,7 +135,7 @@ def translate_count(locale : String, key : String, count : Int, format = NumberF
# Try #2: Fallback to english
translation = translate_count("en-US", key, count)
else
- # Return key if we're already in english, as the tranlation is missing
+ # Return key if we're already in english, as the translation is missing
LOGGER.warn("i18n: Missing translation key \"#{key}\"")
return key
end
diff --git a/src/invidious/helpers/tokens.cr b/src/invidious/helpers/tokens.cr
index 9b664646..a44988cd 100644
--- a/src/invidious/helpers/tokens.cr
+++ b/src/invidious/helpers/tokens.cr
@@ -44,7 +44,7 @@ def sign_token(key, hash)
# TODO: figure out which "key" variable is used
# Ameba reports a warning for "Lint/ShadowingOuterLocalVar" on this
- # variable, but its preferrable to not touch that (works fine atm).
+ # variable, but it's preferable to not touch that (works fine atm).
hash.each do |key, value|
next if key == "signature"
diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr
index 3ab9a0fc..a58a21b1 100644
--- a/src/invidious/helpers/utils.cr
+++ b/src/invidious/helpers/utils.cr
@@ -161,11 +161,11 @@ def short_text_to_number(short_text : String) : Int32
end
def number_to_short_text(number)
- seperated = number_with_separator(number).gsub(",", ".").split("")
- text = seperated.first(2).join
+ separated = number_with_separator(number).gsub(",", ".").split("")
+ text = separated.first(2).join
- if seperated[2]? && seperated[2] != "."
- text += seperated[2]
+ if separated[2]? && separated[2] != "."
+ text += separated[2]
end
text = text.rchop(".0")
@@ -323,8 +323,8 @@ def fetch_random_instance
instance_list.each do |data|
# TODO Check if current URL is onion instance and use .onion types if so.
if data[1]["type"] == "https"
- # Instances can have statisitics disabled, which is an requirement of version validation.
- # as_nil? doesn't exist. Thus we'll have to handle the error rasied if as_nil fails.
+ # Instances can have statistics disabled, which is an requirement of version validation.
+ # as_nil? doesn't exist. Thus we'll have to handle the error raised if as_nil fails.
begin
data[1]["stats"].as_nil
next
diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr
index 88888a65..aefa34cc 100644
--- a/src/invidious/playlists.cr
+++ b/src/invidious/playlists.cr
@@ -401,7 +401,7 @@ def fetch_playlist(plid : String)
end
def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32, video_id = nil)
- # Show empy playlist if requested page is out of range
+ # 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
return [] of PlaylistVideo
diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr
index 9c740cf2..930c588b 100644
--- a/src/invidious/routes/preferences.cr
+++ b/src/invidious/routes/preferences.cr
@@ -136,7 +136,7 @@ module Invidious::Routes::PreferencesRoute
notifications_only ||= "off"
notifications_only = notifications_only == "on"
- # Convert to JSON and back again to take advantage of converters used for compatability
+ # Convert to JSON and back again to take advantage of converters used for compatibility
preferences = Preferences.from_json({
annotations: annotations,
annotations_subscribed: annotations_subscribed,
diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr
index d77d56d2..446e8e03 100644
--- a/src/invidious/videos.cr
+++ b/src/invidious/videos.cr
@@ -446,7 +446,7 @@ struct Video
end
json.field "author", rv["author"]
- json.field "authorUrl", rv["author_url"]?
+ json.field "authorUrl", "/channel/#{rv["ucid"]?}"
json.field "authorId", rv["ucid"]?
if rv["author_thumbnail"]?
json.field "authorThumbnails" do
@@ -455,7 +455,7 @@ struct Video
qualities.each do |quality|
json.object do
- json.field "url", rv["author_thumbnail"]?.try &.gsub(/s\d+-/, "s#{quality}-")
+ json.field "url", rv["author_thumbnail"].gsub(/s\d+-/, "s#{quality}-")
json.field "width", quality
json.field "height", quality
end
@@ -465,7 +465,7 @@ struct Video
end
json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i
- json.field "viewCountText", rv["short_view_count_text"]?
+ json.field "viewCountText", rv["short_view_count"]?
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
end
end
@@ -802,23 +802,50 @@ class VideoRedirect < Exception
end
end
-def parse_related(r : JSON::Any) : JSON::Any?
- # TODO: r["endScreenPlaylistRenderer"], etc.
- return if !r["endScreenVideoRenderer"]?
- r = r["endScreenVideoRenderer"].as_h
-
- return if !r["lengthInSeconds"]?
-
- rv = {} of String => JSON::Any
- rv["author"] = r["shortBylineText"]["runs"][0]?.try &.["text"] || JSON::Any.new("")
- rv["ucid"] = r["shortBylineText"]["runs"][0]?.try &.["navigationEndpoint"]["browseEndpoint"]["browseId"] || JSON::Any.new("")
- rv["author_url"] = JSON::Any.new("/channel/#{rv["ucid"]}")
- rv["length_seconds"] = JSON::Any.new(r["lengthInSeconds"].as_i.to_s)
- rv["title"] = r["title"]["simpleText"]
- rv["short_view_count_text"] = JSON::Any.new(r["shortViewCountText"]?.try &.["simpleText"]?.try &.as_s || "")
- rv["view_count"] = JSON::Any.new(r["title"]["accessibility"]?.try &.["accessibilityData"]["label"].as_s.match(/(?<views>[1-9](\d+,?)*) views/).try &.["views"].gsub(/\D/, "") || "")
- rv["id"] = r["videoId"]
- JSON::Any.new(rv)
+# Use to parse both "compactVideoRenderer" and "endScreenVideoRenderer".
+# The former is preferred as it has more videos in it. The second has
+# the same 11 first entries as the compact rendered.
+#
+# TODO: "compactRadioRenderer" (Mix) and
+def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
+ return nil if !related["videoId"]?
+
+ # The compact renderer has video length in seconds, where the end
+ # screen rendered has a full text version ("42:40")
+ length = related["lengthInSeconds"]?.try &.as_i.to_s
+ length ||= related.dig?("lengthText", "simpleText").try do |box|
+ decode_length_seconds(box.as_s).to_s
+ end
+
+ # Both have "short", so the "long" option shouldn't be required
+ channel_info = (related["shortBylineText"]? || related["longBylineText"]?)
+ .try &.dig?("runs", 0)
+
+ author = channel_info.try &.dig?("text")
+ ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
+
+ # "4,088,033 views", only available on compact renderer
+ # and when video is not a livestream
+ view_count = related.dig?("viewCountText", "simpleText")
+ .try &.as_s.gsub(/\D/, "")
+
+ short_view_count = related.try do |r|
+ HelperExtractors.get_short_view_count(r).to_s
+ end
+
+ LOGGER.trace("parse_related_video: Found \"watchNextEndScreenRenderer\" container")
+
+ # TODO: when refactoring video types, make a struct for related videos
+ # or reuse an existing type, if that fits.
+ return {
+ "id" => related["videoId"],
+ "title" => related["title"]["simpleText"],
+ "author" => author || JSON::Any.new(""),
+ "ucid" => JSON::Any.new(ucid || ""),
+ "length_seconds" => JSON::Any.new(length || "0"),
+ "view_count" => JSON::Any.new(view_count || "0"),
+ "short_view_count" => JSON::Any.new(short_view_count || "0"),
+ }
end
def extract_video_info(video_id : String, proxy_region : String? = nil, context_screen : String? = nil)
@@ -871,30 +898,61 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
params[f] = player_response[f] if player_response[f]?
end
- params["relatedVideos"] = (
- player_response
- .dig?("playerOverlays", "playerOverlayRenderer", "endScreen", "watchNextEndScreenRenderer", "results")
- .try &.as_a.compact_map { |r| parse_related r } || \
- player_response
- .dig?("webWatchNextResponseExtensionData", "relatedVideoArgs")
- .try &.as_s.split(",").map { |r|
- r = HTTP::Params.parse(r).to_h
- JSON::Any.new(Hash.zip(r.keys, r.values.map { |v| JSON::Any.new(v) }))
- }
- ).try { |a| JSON::Any.new(a) } || JSON::Any.new([] of JSON::Any)
-
# Top level elements
- primary_results = player_response
- .dig?("contents", "twoColumnWatchNextResults", "results", "results", "contents")
+ main_results = player_response.dig?("contents", "twoColumnWatchNextResults")
+
+ raise BrokenTubeException.new("twoColumnWatchNextResults") if !main_results
+
+ primary_results = main_results.dig?("results", "results", "contents")
+ secondary_results = main_results
+ .dig?("secondaryResults", "secondaryResults", "results")
+
+ raise BrokenTubeException.new("results") if !primary_results
+ raise BrokenTubeException.new("secondaryResults") if !secondary_results
video_primary_renderer = primary_results
- .try &.as_a.find(&.["videoPrimaryInfoRenderer"]?)
- .try &.["videoPrimaryInfoRenderer"]
+ .as_a.find(&.["videoPrimaryInfoRenderer"]?)
+ .try &.["videoPrimaryInfoRenderer"]
video_secondary_renderer = primary_results
- .try &.as_a.find(&.["videoSecondaryInfoRenderer"]?)
- .try &.["videoSecondaryInfoRenderer"]
+ .as_a.find(&.["videoSecondaryInfoRenderer"]?)
+ .try &.["videoSecondaryInfoRenderer"]
+
+ raise BrokenTubeException.new("videoPrimaryInfoRenderer") if !video_primary_renderer
+ raise BrokenTubeException.new("videoSecondaryInfoRenderer") if !video_secondary_renderer
+
+ # Related videos
+
+ LOGGER.debug("extract_video_info: parsing related videos...")
+
+ related = [] of JSON::Any
+
+ # Parse "compactVideoRenderer" items (under secondary results)
+ secondary_results.as_a.each do |element|
+ if item = element["compactVideoRenderer"]?
+ related_video = parse_related_video(item)
+ related << JSON::Any.new(related_video) if related_video
+ end
+ end
+
+ # If nothing was found previously, fall back to end screen renderer
+ if related.empty?
+ # Container for "endScreenVideoRenderer" items
+ player_overlays = player_response.dig?(
+ "playerOverlays", "playerOverlayRenderer",
+ "endScreen", "watchNextEndScreenRenderer", "results"
+ )
+
+ player_overlays.try &.as_a.each do |element|
+ if item = element["endScreenVideoRenderer"]?
+ related_video = parse_related_video(item)
+ related << JSON::Any.new(related_video) if related_video
+ end
+ end
+ end
+
+ params["relatedVideos"] = JSON::Any.new(related)
# Likes/dislikes
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr
index 00f5f8b7..2e0aee99 100644
--- a/src/invidious/views/watch.ecr
+++ b/src/invidious/views/watch.ecr
@@ -321,11 +321,11 @@ we're going to need to do it here in order to allow for translations.
</div>
<div class="pure-u-10-24" style="text-align:right">
- <% if views = rv["short_view_count_text"]?.try &.delete(", views watching") %>
- <% if !views.empty? %>
- <b class="width:100%"><%= translate_count(locale, "generic_views_count", views.to_i? || 0) %></b>
- <% end %>
- <% end %>
+ <b class="width:100%"><%=
+ views = rv["view_count"]?.try &.to_i?
+ views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) }
+ translate_count(locale, "generic_views_count", views || 0, NumberFormatting::Short)
+ %></b>
</div>
</h5>
</a>
diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr
index 66b3cdef..ce39bc28 100644
--- a/src/invidious/yt_backend/extractors.cr
+++ b/src/invidious/yt_backend/extractors.cr
@@ -505,7 +505,7 @@ end
#
# Mostly used to extract out repeated structures to deal with code
# repetition.
-private module HelperExtractors
+module HelperExtractors
# Retrieves the amount of videos present within the given InnerTube data.
#
# Returns a 0 when it's unable to do so
@@ -519,6 +519,20 @@ private module HelperExtractors
end
end
+ # Retrieves the amount of views/viewers a video has.
+ # Seems to be used on related videos only
+ #
+ # Returns "0" when unable to parse
+ def self.get_short_view_count(container : JSON::Any) : String
+ box = container["shortViewCountText"]?
+ return "0" if !box
+
+ # Simpletext: "4M views"
+ # runs: {"text": "1.1K"},{"text":" watching"}
+ return box["simpleText"]?.try &.as_s.sub(" views", "") ||
+ box.dig?("runs", 0, "text").try &.as_s || "0"
+ end
+
# Retrieve lowest quality thumbnail from InnerTube data
#
# TODO allow configuration of image quality (-1 is highest)
@@ -554,7 +568,7 @@ def extract_item(item : JSON::Any, author_fallback : String? = "",
# Cycles through all of the item parsers and attempt to parse the raw YT JSON data.
# Each parser automatically validates the data given to see if the data is
- # applicable to itself. If not nil is returned and the next parser is attemped.
+ # applicable to itself. If not nil is returned and the next parser is attempted.
ITEM_PARSERS.each do |parser|
LOGGER.trace("extract_item: Attempting to parse item using \"#{parser.parser_name}\" (cycling...)")
diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr
index 426c076a..5bbd9213 100644
--- a/src/invidious/yt_backend/youtube_api.cr
+++ b/src/invidious/yt_backend/youtube_api.cr
@@ -90,7 +90,7 @@ module YoutubeAPI
property client_type : ClientType
# Region to provide to youtube, e.g to alter search results
- # (this is passed as the `gl` parmeter).
+ # (this is passed as the `gl` parameter).
property region : String | Nil
# ISO code of country where the proxy is located.
@@ -205,7 +205,7 @@ module YoutubeAPI
# :ditto:
def browse(
browse_id : String,
- *, # Force the following paramters to be passed by name
+ *, # Force the following parameters to be passed by name
params : String,
client_config : ClientConfig | Nil = nil
)
@@ -215,7 +215,7 @@ module YoutubeAPI
"context" => self.make_context(client_config),
}
- # Append the additionnal parameters if those were provided
+ # Append the additional parameters if those were provided
# (this is required for channel info, playlist and community, e.g)
if params != ""
data["params"] = params
@@ -292,14 +292,14 @@ module YoutubeAPI
# and POST data in order to get a JSON reply.
#
# The requested data is a video ID (`v=` parameter), with some
- # additional paramters, formatted as a base64 string.
+ # additional parameters, formatted as a base64 string.
#
# An optional ClientConfig parameter can be passed, too (see
# `struct ClientConfig` above for more details).
#
def player(
video_id : String,
- *, # Force the following paramters to be passed by name
+ *, # Force the following parameters to be passed by name
params : String,
client_config : ClientConfig | Nil = nil
)
@@ -309,7 +309,7 @@ module YoutubeAPI
"context" => self.make_context(client_config),
}
- # Append the additionnal parameters if those were provided
+ # Append the additional parameters if those were provided
if params != ""
data["params"] = params
end
@@ -363,7 +363,7 @@ module YoutubeAPI
# order to get non-US results.
#
# The requested data is a search string, with some additional
- # paramters, formatted as a base64 string.
+ # parameters, formatted as a base64 string.
#
# An optional ClientConfig parameter can be passed, too (see
# `struct ClientConfig` above for more details).