mirror of
https://github.com/iv-org/invidious.git
synced 2026-08-16 21:10:53 -05:00
fix(extractors): search all metadataRows for views/published in lockupViewModel videos
Collaboration videos (multiple authors) have an extra metadataRows entry before the views/date one, containing the byline (e.g. "Channel A and Channel B"). The video branch of LockupViewModelParser always read metadataRows[0], which for these videos is the byline, not views/date, so both silently defaulted to 0 views and the current time. Fixed by searching every row's parts instead of assuming a fixed index, mirroring the same reasoning already used a few lines below for the playlist branch of this parser. Verified against real, live-captured YouTube API responses for both a single-author video and a real collaboration video (Veritasium x 2swap): confirmed the bug reproduces against the original code (views: 0) and is fixed by this change (views: 7900000). Full spec suite (174 examples) passes. Fixes: #5740 I want to be awarded the bounty associated to the issue this PR is fixing. -- AI disclosure (per AI_POLICY.md): this change was written with assistance from Claude Sonnet 5 (model id claude-sonnet-5), via Claude Code (Anthropic's CLI agent). The human (Alison Moura) read the AI usage policy, reviewed the diff, and reviewed the before/after test verification described above prior to this commit being made.
This commit is contained in:
parent
6865cf208e
commit
d7f990ebd7
130
spec/invidious/yt_backend/lockup_view_model_extract_spec.cr
Normal file
130
spec/invidious/yt_backend/lockup_view_model_extract_spec.cr
Normal file
@ -0,0 +1,130 @@
|
||||
require "../../parsers_helper.cr"
|
||||
|
||||
# Real data captured 2026-08-14 from a live `youtubei/v1/browse` "Videos" tab
|
||||
# response, trimmed to the fields the parser actually reads. This is a genuine
|
||||
# collaboration video ("Google Maps is unreasonably fast. Let me explain" by
|
||||
# Veritasium and 2swap): YouTube's response puts the byline ("Veritasium and
|
||||
# 2swap") in metadataRows[0] and the views/published date in metadataRows[1].
|
||||
COLLAB_VIDEO_LOCKUP_JSON = <<-'JSON'
|
||||
{
|
||||
"lockupViewModel": {
|
||||
"contentImage": {
|
||||
"thumbnailViewModel": {
|
||||
"image": {
|
||||
"sources": [
|
||||
{"url": "https://i.ytimg.com/vi/collabvideo/hqdefault.jpg", "width": 336, "height": 188}
|
||||
]
|
||||
},
|
||||
"overlays": [
|
||||
{
|
||||
"thumbnailBottomOverlayViewModel": {
|
||||
"badges": [
|
||||
{"thumbnailBadgeViewModel": {"text": "15:23"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"lockupMetadataViewModel": {
|
||||
"title": {"content": "Google Maps is unreasonably fast. Let me explain"},
|
||||
"metadata": {
|
||||
"contentMetadataViewModel": {
|
||||
"metadataRows": [
|
||||
{
|
||||
"metadataParts": [
|
||||
{"text": {"content": "Veritasium and 2swap"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"metadataParts": [
|
||||
{"text": {"content": "7.9M views"}},
|
||||
{"text": {"content": "2 months ago"}, "accessibilityLabel": "2 months ago"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contentId": "collabvideo123",
|
||||
"contentType": "LOCKUP_CONTENT_TYPE_VIDEO"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
# Same shape, but a regular single-author video where metadataRows has just
|
||||
# the one row (views/date) at index 0 — this is the common case and must keep
|
||||
# working exactly as before.
|
||||
SINGLE_AUTHOR_VIDEO_LOCKUP_JSON = <<-'JSON'
|
||||
{
|
||||
"lockupViewModel": {
|
||||
"contentImage": {
|
||||
"thumbnailViewModel": {
|
||||
"image": {
|
||||
"sources": [
|
||||
{"url": "https://i.ytimg.com/vi/soloVideo/hqdefault.jpg", "width": 336, "height": 188}
|
||||
]
|
||||
},
|
||||
"overlays": [
|
||||
{
|
||||
"thumbnailBottomOverlayViewModel": {
|
||||
"badges": [
|
||||
{"thumbnailBadgeViewModel": {"text": "26:58"}}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"lockupMetadataViewModel": {
|
||||
"title": {"content": "Is spider web really stronger than steel?"},
|
||||
"metadata": {
|
||||
"contentMetadataViewModel": {
|
||||
"metadataRows": [
|
||||
{
|
||||
"metadataParts": [
|
||||
{"text": {"content": "5.7M views"}},
|
||||
{"text": {"content": "12 days ago"}, "accessibilityLabel": "12 days ago"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contentId": "soloVideo123",
|
||||
"contentType": "LOCKUP_CONTENT_TYPE_VIDEO"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
Spectator.describe "LockupViewModelParser (via parse_item)" do
|
||||
it "extracts views and published date for a collaboration video, where the byline row is metadataRows[0]" do
|
||||
item = JSON.parse(COLLAB_VIDEO_LOCKUP_JSON)
|
||||
result = parse_item(item, "fallback_author", "fallback_id")
|
||||
|
||||
result = result.as(SearchVideo)
|
||||
expect(result.id).to eq("collabvideo123")
|
||||
expect(result.title).to eq("Google Maps is unreasonably fast. Let me explain")
|
||||
|
||||
# Before the fix: views defaulted to 0 and published defaulted to
|
||||
# Time.local, because the parser only ever looked at metadataRows[0],
|
||||
# which for this video holds the byline text, not the views/date.
|
||||
expect(result.views).to eq(7_900_000)
|
||||
expect(result.published.year).to eq(Time.local.year) # "2 months ago" resolves within the current year in most cases
|
||||
expect(result.published).to be < Time.local - 50.days
|
||||
end
|
||||
|
||||
it "still extracts views and published date correctly for a regular single-author video" do
|
||||
item = JSON.parse(SINGLE_AUTHOR_VIDEO_LOCKUP_JSON)
|
||||
result = parse_item(item, "fallback_author", "fallback_id")
|
||||
|
||||
result = result.as(SearchVideo)
|
||||
expect(result.id).to eq("soloVideo123")
|
||||
expect(result.views).to eq(5_700_000)
|
||||
expect(result.published).to be < Time.local - 11.days
|
||||
end
|
||||
end
|
||||
@ -661,8 +661,17 @@ 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
|
||||
#
|
||||
# metadataRows normally has a single row with both of these parts, but
|
||||
# for collaboration videos (multiple authors) YouTube inserts an extra
|
||||
# row before it containing the byline (e.g. "Channel A and Channel B"),
|
||||
# which pushes the views/date row to a later index. We can't rely on a
|
||||
# fixed index, so every row's parts are searched instead (same
|
||||
# reasoning already applied to the playlist branch below).
|
||||
metadata_rows = metadata.dig("metadata", "contentMetadataViewModel", "metadataRows").try &.as_a
|
||||
metadata_parts = metadata_rows.try &.flat_map { |row| row["metadataParts"]?.try(&.as_a) || [] of JSON::Any }
|
||||
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user