From c813949d48084f2adf3d9c95a0893b4eb1917572 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Mon, 10 Aug 2026 15:53:25 -0400 Subject: [PATCH] fix: remove fake RSS placeholder source; migrate persisted configs --- src/stores/feed.ts | 12 +++- src/types/source.ts | 8 --- src/utils/source-searcher.ts | 106 +++++----------------------------- tests/source-searcher.test.ts | 40 +++++++++++++ 4 files changed, 64 insertions(+), 102 deletions(-) create mode 100644 tests/source-searcher.test.ts diff --git a/src/stores/feed.ts b/src/stores/feed.ts index 47bc211..d62d5f4 100644 --- a/src/stores/feed.ts +++ b/src/stores/feed.ts @@ -288,7 +288,15 @@ function createFeedStore() { const loadedFeeds = await loadFeedsFromFile(); if (loadedFeeds.length > 0) setFeeds(loadedFeeds); const loadedSources = await loadSourcesFromFile(); - if (loadedSources && loadedSources.length > 0) setSources(loadedSources); + // The default "rss" placeholder source fabricated fake search results + // and was removed from DEFAULT_SOURCES; drop it from persisted configs + // too. User-added custom feeds keep their own ids and are untouched. + const migratedSources = + loadedSources?.filter((source) => source.id !== "rss") ?? []; + if (migratedSources.length > 0) { + setSources(migratedSources); + saveSources(migratedSources); + } await refreshAllFeeds(); })(); @@ -367,7 +375,7 @@ function createFeedStore() { /** Remove a source */ const removeSource = (sourceId: string) => { // Don't remove default sources - if (sourceId === "itunes" || sourceId === "rss") return false; + if (sourceId === "itunes") return false; setSources((prev) => { const updated = prev.filter((s) => s.id !== sourceId); diff --git a/src/types/source.ts b/src/types/source.ts index 54abb73..4269d94 100644 --- a/src/types/source.ts +++ b/src/types/source.ts @@ -105,12 +105,4 @@ export const DEFAULT_SOURCES: PodcastSource[] = [ language: "en_us", allowExplicit: true, }, - { - id: "rss", - name: "RSS Feed", - type: SourceType.RSS, - baseUrl: "", - enabled: true, - description: "Add podcasts via RSS feed URL", - }, ] diff --git a/src/utils/source-searcher.ts b/src/utils/source-searcher.ts index 90c7e04..9278018 100644 --- a/src/utils/source-searcher.ts +++ b/src/utils/source-searcher.ts @@ -4,95 +4,12 @@ import type { PodcastSource, SearchResult } from "../types/source" type SearcherResult = SearchResult[] -const delay = async (min = 200, max = 500) => - new Promise((resolve) => setTimeout(resolve, min + Math.random() * max)) - -const hashString = (input: string): number => { - let hash = 0 - for (let i = 0; i < input.length; i += 1) { - hash = (hash << 5) - hash + input.charCodeAt(i) - hash |= 0 - } - return Math.abs(hash) -} - const slugify = (input: string): string => input .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") -const sourceLabel = (source: PodcastSource): string => - source.name || source.id - -const buildPodcast = ( - idBase: string, - title: string, - description: string, - author: string, - categories: string[], - source: PodcastSource -): Podcast => ({ - id: idBase, - title, - description, - feedUrl: `https://example.com/${slugify(title)}/feed.xml`, - author, - categories, - lastUpdated: new Date(), - isSubscribed: false, -}) - -const makeResults = (query: string, source: PodcastSource, seedOffset = 0): SearcherResult => { - const seed = hashString(`${source.id}:${query}`) + seedOffset - const baseTitles = [ - "Daily Briefing", - "Studio Sessions", - "Signal & Noise", - "The Long Play", - "Off the Record", - ] - const descriptors = [ - "Deep dives into", - "A fast-paced look at", - "Smart conversations about", - "A weekly roundup of", - "Curated stories on", - ] - const categories = ["Technology", "Business", "Science", "Culture", "News"] - - return baseTitles.map((base, index) => { - const title = `${query} ${base}` - const desc = `${descriptors[index % descriptors.length]} ${query.toLowerCase()} from ${sourceLabel(source)}.` - const author = `${sourceLabel(source)} Network` - const cat = [categories[(seed + index) % categories.length]] - const podcast = buildPodcast( - `search-${source.id}-${seed + index}`, - title, - desc, - author, - cat, - source - ) - - return { - sourceId: source.id, - sourceName: source.name, - sourceType: source.type, - podcast, - score: 1 - index * 0.08, - } - }) -} - -const searchRSSSource = async ( - query: string, - source: PodcastSource -): Promise => { - await delay(200, 450) - return makeResults(query, source, 1) -} - type ItunesResult = { collectionId?: number collectionName?: string @@ -173,23 +90,28 @@ const searchAPISource = async ( })) } -const searchCustomSource = async ( - query: string, - source: PodcastSource -): Promise => { - await delay(300, 650) - return makeResults(query, source, 13) -} +/** + * RSS-type sources have no directory search backend: a feed URL identifies one + * show, and no API exists to search across "the RSS directory". Return no + * results rather than fabricating them. + */ +const searchRSSSource = async (): Promise => [] + +/** + * Custom sources are RSS feeds added by URL (SourceManager) — same + * no-backend story, so they contribute nothing to directory search. + */ +const searchCustomSource = async (): Promise => [] export const searchSourceByType = async ( query: string, source: PodcastSource ): Promise => { if (source.type === SourceType.RSS) { - return searchRSSSource(query, source) + return searchRSSSource() } if (source.type === SourceType.CUSTOM) { - return searchCustomSource(query, source) + return searchCustomSource() } return searchAPISource(query, source) } diff --git a/tests/source-searcher.test.ts b/tests/source-searcher.test.ts new file mode 100644 index 0000000..9a4efe9 --- /dev/null +++ b/tests/source-searcher.test.ts @@ -0,0 +1,40 @@ +/** + * Search source dispatch regression test. + * + * RSS-type and custom sources have no directory search backend: a feed URL + * identifies one show, and no API exists to search "the RSS directory". They + * must return no results. Earlier the dispatcher fabricated fake podcasts + * (" Daily Briefing" by " Network", with dead + * https://example.com/... feed URLs) from the query, which polluted every + * search. This test pins the empty-result contract. + */ +import { test, expect } from "bun:test"; +import { searchSourceByType } from "../src/utils/source-searcher"; +import { SourceType } from "../src/types/source"; +import type { PodcastSource } from "../src/types/source"; + +const rssSource: PodcastSource = { + id: "rss", + name: "RSS Feed", + type: SourceType.RSS, + baseUrl: "", + enabled: true, +}; + +const customSource: PodcastSource = { + id: "my-feed", + name: "My Feed", + type: SourceType.CUSTOM, + baseUrl: "https://example.com/feed.rss", + enabled: true, +}; + +test("RSS sources return no directory search results", async () => { + const results = await searchSourceByType("blocked and reported", rssSource); + expect(results).toEqual([]); +}); + +test("custom sources return no directory search results", async () => { + const results = await searchSourceByType("anything", customSource); + expect(results).toEqual([]); +});