diff --git a/locales/en-US.json b/locales/en-US.json index fa28e7f8b..bf79d3d8d 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -122,8 +122,6 @@ "Redirect homepage to feed: ": "Redirect homepage to feed: ", "preferences_max_results_label": "Number of videos shown in feed: ", "preferences_sort_label": "Sort videos by: ", - "preferences_default_playlist": "Default playlist: ", - "preferences_default_playlist_none": "No default playlist set", "published": "published", "published - reverse": "published - reverse", "alphabetically": "alphabetically", @@ -504,5 +502,6 @@ "carousel_go_to": "Go to slide `x`", "timeline_parse_error_placeholder_heading": "Unable to parse item", "timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:", - "timeline_parse_error_show_technical_details": "Show technical details" + "timeline_parse_error_show_technical_details": "Show technical details", + "label_mix": "Mix" } diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr index 2796a8dc6..ffc6074d9 100644 --- a/src/invidious/helpers/serialized_yt_data.cr +++ b/src/invidious/helpers/serialized_yt_data.cr @@ -171,6 +171,8 @@ struct SearchPlaylist property videos : Array(SearchPlaylistVideo) property thumbnail : String? property author_verified : Bool + @[DB::Field(ignore: true)] + property is_mix : Bool = false def to_json(locale : String?, json : JSON::Builder) json.object do diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 85f6caa55..6a9844c9e 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -321,6 +321,7 @@ private module Parsers video_count = HelperExtractors.get_video_count(item_contents) playlist_thumbnail = HelperExtractors.get_thumbnails(item_contents) + is_mix = plid.starts_with? "RD" SearchPlaylist.new({ title: title, id: plid, @@ -330,6 +331,7 @@ private module Parsers videos: [] of SearchPlaylistVideo, thumbnail: playlist_thumbnail, author_verified: author_verified, + is_mix: is_mix, }) end @@ -382,6 +384,7 @@ private module Parsers # TODO: item_contents["publishedTimeText"]? + is_mix = plid.starts_with? "RD" SearchPlaylist.new({ title: title, id: plid, @@ -391,6 +394,7 @@ private module Parsers videos: videos, thumbnail: playlist_thumbnail, author_verified: author_verified, + is_mix: is_mix, }) end @@ -656,27 +660,34 @@ private module Parsers thumbnail = thumbnail_view_model.dig("image", "sources", 0, "url").as_s - # This complicated sequences tries to extract the following data structure: - # "overlays": [{ - # "thumbnailOverlayBadgeViewModel": { - # "thumbnailBadges": [{ - # "thumbnailBadgeViewModel": { - # "text": "430 episodes", - # "badgeStyle": "THUMBNAIL_OVERLAY_BADGE_STYLE_DEFAULT" - # } - # }] - # } - # }] - # - # NOTE: this simplistic `.to_i` conversion might not work on larger - # playlists and hasn't been tested. - video_count = thumbnail_view_model.dig("overlays").as_a + # Parse overlays badges + overlays = thumbnail_view_model.dig("overlays").as_a .compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a) .flatten - .find(nil, &.dig?("thumbnailBadgeViewModel", "text").try { |node| - {"episodes", "videos"}.any? { |str| node.as_s.ends_with?(str) } - }) - .try &.dig("thumbnailBadgeViewModel", "text").as_s.to_i(strict: false) + + # Detect Mix playlists via icon imageName at any index or text case-insensitively + is_mix = overlays.any? { |badge| + sources = badge.dig?("thumbnailBadgeViewModel", "icon", "sources").try &.as_a || [] of JSON::Any + has_mix_icon = sources.any? { |s| s.dig?("clientResource", "imageName").try &.as_s == "MIX" } + text = badge.dig?("thumbnailBadgeViewModel", "text").try &.as_s || "" + has_mix_text = text.downcase == "mix" + has_mix_icon || has_mix_text + } + + # Fallback: RD-prefixed playlist IDs are Mixes + is_mix ||= playlist_id.starts_with? "RD" + + # Robustly extract digits from any badge text for video_count; fallback to -1 if not found + video_count = nil.as(Int32?) + overlays.each do |badge| + t = badge.dig?("thumbnailBadgeViewModel", "text").try &.as_s + next unless t + digits = t.gsub(/\D/, "") + unless digits.empty? + video_count = digits.to_i + break + end + end metadata = item_contents.dig("metadata", "lockupMetadataViewModel") title = metadata.dig("title", "content").as_s @@ -699,6 +710,7 @@ private module Parsers videos: [] of SearchPlaylistVideo, thumbnail: thumbnail, author_verified: false, + is_mix: is_mix, }) end