mirror of
https://github.com/iv-org/invidious.git
synced 2026-09-07 17:42:46 -05:00
Upstream has no way to tell a Short from a long-form upload in a feed: `ChannelVideo#to_json` reports `"type": "shortVideo"` for every row, and `channel_videos.length_seconds` is 0 for anything absent from the channel's Videos tab — Shorts and stream VODs alike. 531 of 1586 rows (34%) were in that state. YouTube's per-channel uploads playlists supply the signal, addressed by replacing the "UC" of the channel ID: UULF long-form, UUSH Shorts, UULV live. Measured over 75 channels, 15 newest entries each: 1114 long-form, 679 Shorts, 299 live, one Short leaking into a UULF feed. ClassifyChannelVideosJob labels `kind` from those feeds, falling back to a capped `HEAD /shorts/<id>` probe (200 = Short, 303 = not) for rows older than a 15-entry window. `feed_kinds` restricts the subscription feed via a predicate in each user's materialized view. A separate job rather than an edit to `fetch_channel`, and `kind` is absent from the insert's `ON CONFLICT DO UPDATE` set, so a channel refresh cannot overwrite a label. Unclassified entries are always shown and an empty `feed_kinds` admits everything, so the feed cannot end up blank. Also folds in three pre-existing ameba Performance/ChainedCallWithNoBang fixes in arik_settings.cr and admin_settings.cr, which a newer ameba release started flagging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
166 lines
5.8 KiB
Crystal
166 lines
5.8 KiB
Crystal
require "../spec_helper"
|
|
require "../../src/invidious/arik_feed_kinds"
|
|
|
|
Spectator.describe Invidious::ArikFeedKinds do
|
|
alias Kinds = Invidious::ArikFeedKinds
|
|
|
|
UCID = "UC7qUL2EsTHpNcgsz7woW9Iw"
|
|
|
|
describe ".uploads_playlist_id" do
|
|
it "swaps the UC prefix for the kind's prefix" do
|
|
expect(Kinds.uploads_playlist_id(UCID, "video")).to eq("UULF7qUL2EsTHpNcgsz7woW9Iw")
|
|
expect(Kinds.uploads_playlist_id(UCID, "short")).to eq("UUSH7qUL2EsTHpNcgsz7woW9Iw")
|
|
expect(Kinds.uploads_playlist_id(UCID, "live")).to eq("UULV7qUL2EsTHpNcgsz7woW9Iw")
|
|
end
|
|
|
|
it "keeps the ID length, so the suffix is never truncated" do
|
|
plid = Kinds.uploads_playlist_id(UCID, "short").not_nil!
|
|
expect(plid.size).to eq(UCID.size + 2)
|
|
expect(plid.ends_with?(UCID[2..])).to be_true
|
|
end
|
|
|
|
it "refuses an ID that is not a channel ID" do
|
|
expect(Kinds.uploads_playlist_id("PL7qUL2EsTHpNcgsz7woW9Iw", "short")).to be_nil
|
|
expect(Kinds.uploads_playlist_id("UC", "short")).to be_nil
|
|
expect(Kinds.uploads_playlist_id("UCtooshort", "short")).to be_nil
|
|
expect(Kinds.uploads_playlist_id("UC7qUL2EsTHpNcgsz7woW9I/", "short")).to be_nil
|
|
end
|
|
|
|
it "refuses a kind it does not know" do
|
|
expect(Kinds.uploads_playlist_id(UCID, "premiere")).to be_nil
|
|
expect(Kinds.uploads_playlist_id(UCID, "")).to be_nil
|
|
end
|
|
end
|
|
|
|
describe ".feed_resource" do
|
|
it "addresses the playlist feed, not the channel feed" do
|
|
expect(Kinds.feed_resource(UCID, "short"))
|
|
.to eq("/feeds/videos.xml?playlist_id=UUSH7qUL2EsTHpNcgsz7woW9Iw")
|
|
end
|
|
|
|
it "is nil for an unusable channel or kind" do
|
|
expect(Kinds.feed_resource("nonsense", "short")).to be_nil
|
|
expect(Kinds.feed_resource(UCID, "nonsense")).to be_nil
|
|
end
|
|
end
|
|
|
|
describe ".kind_from_probe_status" do
|
|
it "reads YouTube's answer: 200 is a Short, a redirect is not" do
|
|
expect(Kinds.kind_from_probe_status(200)).to eq("short")
|
|
expect(Kinds.kind_from_probe_status(303)).to eq("video")
|
|
expect(Kinds.kind_from_probe_status(301)).to eq("video")
|
|
expect(Kinds.kind_from_probe_status(302)).to eq("video")
|
|
end
|
|
|
|
it "gives no answer for a status that carries none" do
|
|
expect(Kinds.kind_from_probe_status(429)).to be_nil
|
|
expect(Kinds.kind_from_probe_status(500)).to be_nil
|
|
expect(Kinds.kind_from_probe_status(404)).to be_nil
|
|
end
|
|
end
|
|
|
|
describe ".visible?" do
|
|
it "hides a kind the feed does not admit" do
|
|
expect(Kinds.visible?("short", ["video"])).to be_false
|
|
expect(Kinds.visible?("live", ["video"])).to be_false
|
|
expect(Kinds.visible?("video", ["video"])).to be_true
|
|
expect(Kinds.visible?("live", ["video", "live"])).to be_true
|
|
end
|
|
|
|
it "shows an unclassified entry" do
|
|
expect(Kinds.visible?(nil, ["video"])).to be_true
|
|
end
|
|
|
|
it "shows an entry whose stored kind is not one we know" do
|
|
expect(Kinds.visible?("premiere", ["video"])).to be_true
|
|
end
|
|
|
|
it "shows everything when nothing is configured" do
|
|
expect(Kinds.visible?("short", [] of String)).to be_true
|
|
end
|
|
end
|
|
|
|
describe ".json_type" do
|
|
it "reports the kind in the vocabulary clients use for search results" do
|
|
expect(Kinds.json_type("video")).to eq("video")
|
|
expect(Kinds.json_type("short")).to eq("shortVideo")
|
|
expect(Kinds.json_type("live")).to eq("stream")
|
|
end
|
|
|
|
it "calls an unclassified entry a video, not a Short" do
|
|
expect(Kinds.json_type(nil)).to eq("video")
|
|
expect(Kinds.json_type("premiere")).to eq("video")
|
|
end
|
|
end
|
|
|
|
describe ".clean_kinds" do
|
|
it "accepts the known kinds and fixes their order" do
|
|
cleaned, errors = Kinds.clean_kinds(["live", "video"])
|
|
expect(cleaned).to eq(["video", "live"])
|
|
expect(errors).to be_empty
|
|
end
|
|
|
|
it "drops blanks and duplicates, and normalizes case" do
|
|
cleaned, errors = Kinds.clean_kinds(["VIDEO", " video ", "", " "])
|
|
expect(cleaned).to eq(["video"])
|
|
expect(errors).to be_empty
|
|
end
|
|
|
|
it "reports anything that is not a kind" do
|
|
cleaned, errors = Kinds.clean_kinds(["video", "premiere"])
|
|
expect(cleaned).to eq(["video"])
|
|
expect(errors.size).to eq(1)
|
|
expect(errors[0]).to contain("premiere")
|
|
end
|
|
end
|
|
|
|
describe ".decode_kinds" do
|
|
it "returns nothing for a missing row, so the config stays authoritative" do
|
|
kinds, error = Kinds.decode_kinds(nil)
|
|
expect(kinds).to be_nil
|
|
expect(error).to be_nil
|
|
end
|
|
|
|
it "decodes a stored list" do
|
|
kinds, error = Kinds.decode_kinds(%(["video", "live"]))
|
|
expect(kinds).to eq(["video", "live"])
|
|
expect(error).to be_nil
|
|
end
|
|
|
|
it "refuses a malformed row instead of raising" do
|
|
kinds, error = Kinds.decode_kinds(%({"video": true}))
|
|
expect(kinds).to be_nil
|
|
expect(error).not_to be_nil
|
|
|
|
kinds, error = Kinds.decode_kinds(%(["premiere"]))
|
|
expect(kinds).to be_nil
|
|
expect(error.not_nil!).to contain("premiere")
|
|
end
|
|
end
|
|
|
|
describe ".view_predicate" do
|
|
it "admits NULL alongside the configured kinds" do
|
|
predicate = Kinds.view_predicate(["video"], "cv.kind")
|
|
expect(predicate).to contain("cv.kind IS NULL")
|
|
expect(predicate).to contain("cv.kind IN ('video')")
|
|
expect(predicate).to start_with(" AND (")
|
|
end
|
|
|
|
it "lists every configured kind" do
|
|
predicate = Kinds.view_predicate(["video", "live"], "cv.kind")
|
|
expect(predicate).to contain("'video', 'live'")
|
|
end
|
|
|
|
it "is empty when the feed admits everything" do
|
|
expect(Kinds.view_predicate([] of String, "cv.kind")).to eq("")
|
|
expect(Kinds.view_predicate(["video", "short", "live"], "cv.kind")).to eq("")
|
|
end
|
|
|
|
it "only ever emits the kinds it validated" do
|
|
cleaned, _ = Kinds.clean_kinds(["video'; DROP TABLE users; --"])
|
|
expect(cleaned).to be_empty
|
|
expect(Kinds.view_predicate(cleaned, "cv.kind")).to eq("")
|
|
end
|
|
end
|
|
end
|