diff options
| -rw-r--r-- | assets/js/player.js | 6 | ||||
| -rw-r--r-- | assets/js/themes.js | 2 | ||||
| -rw-r--r-- | config/config.example.yml | 22 | ||||
| -rw-r--r-- | locales/ar.json | 61 | ||||
| -rw-r--r-- | locales/nb-NO.json | 9 | ||||
| -rw-r--r-- | src/invidious/config.cr | 4 | ||||
| -rw-r--r-- | src/invidious/exceptions.cr | 8 | ||||
| -rw-r--r-- | src/invidious/helpers/i18n.cr | 2 | ||||
| -rw-r--r-- | src/invidious/helpers/tokens.cr | 2 | ||||
| -rw-r--r-- | src/invidious/helpers/utils.cr | 12 | ||||
| -rw-r--r-- | src/invidious/playlists.cr | 2 | ||||
| -rw-r--r-- | src/invidious/routes/preferences.cr | 2 | ||||
| -rw-r--r-- | src/invidious/videos.cr | 134 | ||||
| -rw-r--r-- | src/invidious/views/watch.ecr | 10 | ||||
| -rw-r--r-- | src/invidious/yt_backend/extractors.cr | 18 | ||||
| -rw-r--r-- | src/invidious/yt_backend/youtube_api.cr | 14 |
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). |
