fix(extractors): parse collab lockup views/published across metadataRows

Channel collaboration videos list co-authors in earlier metadataRows, so
views and published timestamps were missed when only the first row was
read. Flatten every row's metadataParts with nil-safe lookups (matching
the playlist branch style) and keep the existing views/"ago" filters.

Adds a focused LockupViewModelParser regression spec covering the
split-row collab shape and a normal single-row listing.

Fixes: #5740
This commit is contained in:
LaptopsPlural 2026-09-09 19:00:40 +00:00
parent 049d591d29
commit 4468145e2b
2 changed files with 99 additions and 1 deletions

View File

@ -0,0 +1,93 @@
require "../../parsers_helper.cr"
Spectator.describe "LockupViewModelParser channel videos" do
# Minimal lockupViewModel shape used by channel video listings.
def lockup_video(metadata_rows : Array(JSON::Any)) : JSON::Any
JSON.parse({
"lockupViewModel" => {
"contentType" => "LOCKUP_CONTENT_TYPE_VIDEO",
"contentId" => "dQw4w9WgXcQ",
"contentImage" => {
"thumbnailViewModel" => {
"image" => {
"sources" => [{"url" => "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg"}],
},
"overlays" => [{
"thumbnailBottomOverlayViewModel" => {
"badges" => [{
"thumbnailBadgeViewModel" => {"text" => "12:34"},
}],
},
}],
},
},
"metadata" => {
"lockupMetadataViewModel" => {
"title" => {"content" => "Google Maps is unreasonably fast. Let me explain"},
"metadata" => {
"contentMetadataViewModel" => {
"metadataRows" => metadata_rows.map(&.raw),
},
},
},
},
},
}.to_json)
end
it "parses views and published from the first metadata row (non-collab)" do
rows = [
JSON.parse({
"metadataParts" => [
{"text" => {"content" => "1.5M views"}},
{"text" => {"content" => "3 days ago"}},
],
}.to_json),
]
result = parse_item(lockup_video(rows), "Veritasium", "UCHnyfMqiRRG1u-2MsSQLbXA")
expect(result).to be_a(SearchVideo)
video = result.as(SearchVideo)
expect(video.title).to eq("Google Maps is unreasonably fast. Let me explain")
expect(video.id).to eq("dQw4w9WgXcQ")
expect(video.views).to eq(1_500_000)
expect(video.published).to be_close(Time.utc - 3.days, 2.seconds)
expect(video.length_seconds).to eq(754)
end
it "parses views and published when collab authors occupy earlier metadata rows" do
# Collaboration listings place co-authors in the first row(s); views/"ago" follow later.
# Rows without metadataParts must be skipped without raising.
rows = [
JSON.parse("{}"),
JSON.parse({
"metadataParts" => [
{
"text" => {"content" => "Veritasium"},
"icon" => {"name" => "CHECK_CIRCLE_FILLED"},
},
{
"text" => {"content" => "and Linus Tech Tips"},
"icon" => {"name" => "CHECK_CIRCLE_FILLED"},
},
],
}.to_json),
JSON.parse({
"metadataParts" => [
{"text" => {"content" => "1.5M views"}},
{"text" => {"content" => "3 days ago"}},
],
}.to_json),
]
result = parse_item(lockup_video(rows), "Veritasium", "UCHnyfMqiRRG1u-2MsSQLbXA")
expect(result).to be_a(SearchVideo)
video = result.as(SearchVideo)
expect(video.views).to eq(1_500_000)
expect(video.published).to be_close(Time.utc - 3.days, 2.seconds)
expect(video.author).to eq("Veritasium")
expect(video.ucid).to eq("UCHnyfMqiRRG1u-2MsSQLbXA")
end
end

View File

@ -662,7 +662,12 @@ private module Parsers
metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
title = metadata.dig("title", "content").as_s
# Contains the views of the video and the published time of the video.
metadata_parts = metadata.dig("metadata", "contentMetadataViewModel", "metadataRows", 0, "metadataParts").try &.as_a
# For collaboration videos the first metadataRows entry lists co-authors,
# with views/"ago" in a later row — search every row (nil-safe).
metadata_rows = metadata.dig?("metadata", "contentMetadataViewModel", "metadataRows").try &.as_a
metadata_parts = metadata_rows.try &.compact_map { |row|
row["metadataParts"]?.try &.as_a
}.try &.flatten
view_count_text = metadata_parts.try &.find { |item| item["icon"]?.nil? && item.dig?("text", "content").try &.as_s.includes?("views") }
.try &.dig("text", "content").as_s