fix(search): surface iTunes stubs and resolve feeds for delisted shows

Shows that left Apple Podcasts (e.g. Daily Wire's in 2021) come back from
the iTunes Search API as metadata-only stub records with feedUrl null.
mapItunesResult dropped them, so The Ben Shapiro Show — the #1 hit for
'ben shapiro' — never appeared in search while sibling shows did.

- Keep feedUrl-less results (feedUrl "" + directoryUrl pointing at the
  Apple page) so delisted shows stay findable.
- Resolve the real feed from the Apple page at subscribe time
  (itunes-feed-resolver: anchor on the collection's adamId, forward-scan
  for the embedded feedUrl; Apple serves page variants where the
  showOffer block sits thousands of chars after the adamId).
- addFeed refuses feedless stubs whose feed can't be resolved instead of
  adding a broken feed; SearchPage surfaces the failure via toast.
- Tests: stub mapping, extractor variants, and an end-to-end subscribe
  over a local HTTP server.
This commit is contained in:
2026-08-10 22:38:03 -04:00
parent e73e608b9f
commit 0b0637b9dc
8 changed files with 364 additions and 11 deletions

View File

@@ -9,7 +9,7 @@
* search. This test pins the empty-result contract.
*/
import { test, expect } from "bun:test";
import { searchSourceByType } from "../src/utils/source-searcher";
import { searchSourceByType, mapItunesResult } from "../src/utils/source-searcher";
import { SourceType } from "../src/types/source";
import type { PodcastSource } from "../src/types/source";
@@ -29,6 +29,16 @@ const customSource: PodcastSource = {
enabled: true,
};
const itunesSource: PodcastSource = {
id: "itunes",
name: "Apple Podcasts",
type: SourceType.API,
baseUrl: "https://itunes.apple.com/search",
enabled: true,
country: "US",
language: "en_us",
};
test("RSS sources return no directory search results", async () => {
const results = await searchSourceByType("blocked and reported", rssSource);
expect(results).toEqual([]);
@@ -38,3 +48,54 @@ test("custom sources return no directory search results", async () => {
const results = await searchSourceByType("anything", customSource);
expect(results).toEqual([]);
});
// ── iTunes stub records (delisted shows) ────────────────────────────────────
// Shows that left Apple Podcasts (e.g. The Daily Wire's in 2021) remain in
// the directory as metadata-only records with feedUrl null. They must stay
// findable — earlier they were dropped entirely, so "ben shapiro" surfaced
// nothing while the show is the #1 iTunes hit.
test("iTunes results without a feedUrl (delisted shows) are kept, not dropped", () => {
const stub = mapItunesResult(
{
collectionId: 1047335260,
collectionName: "The Ben Shapiro Show",
artistName: "The Daily Wire",
feedUrl: null,
collectionViewUrl:
"https://podcasts.apple.com/us/podcast/the-ben-shapiro-show/id1047335260",
},
itunesSource,
);
expect(stub).not.toBeNull();
expect(stub!.title).toBe("The Ben Shapiro Show");
// Empty feed marks "unavailable from this directory"; the Apple page URL
// is carried for feed resolution at subscribe time.
expect(stub!.feedUrl).toBe("");
expect(stub!.directoryUrl).toBe(
"https://podcasts.apple.com/us/podcast/the-ben-shapiro-show/id1047335260",
);
});
test("iTunes results with a feedUrl keep it and carry no directory fallback", () => {
const normal = mapItunesResult(
{
collectionId: 1487234816,
collectionName: "Morning Wire",
artistName: "The Daily Wire",
feedUrl: "https://feeds.megaphone.fm/BVDWV8747925072",
},
itunesSource,
);
expect(normal).not.toBeNull();
expect(normal!.feedUrl).toBe("https://feeds.megaphone.fm/BVDWV8747925072");
expect(normal!.directoryUrl).toBeUndefined();
});
test("iTunes results without a collection name stay dropped", () => {
const dropped = mapItunesResult(
{ collectionId: 1, feedUrl: "https://example.com/feed.xml" },
itunesSource,
);
expect(dropped).toBeNull();
});