fix: remove fake RSS placeholder source; migrate persisted configs

This commit is contained in:
2026-08-10 15:53:25 -04:00
parent eb220386ce
commit c813949d48
4 changed files with 64 additions and 102 deletions

View File

@@ -288,7 +288,15 @@ function createFeedStore() {
const loadedFeeds = await loadFeedsFromFile();
if (loadedFeeds.length > 0) setFeeds(loadedFeeds);
const loadedSources = await loadSourcesFromFile<PodcastSource>();
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);

View File

@@ -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",
},
]

View File

@@ -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<SearcherResult> => {
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<SearcherResult> => {
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<SearcherResult> => []
/**
* Custom sources are RSS feeds added by URL (SourceManager) — same
* no-backend story, so they contribute nothing to directory search.
*/
const searchCustomSource = async (): Promise<SearcherResult> => []
export const searchSourceByType = async (
query: string,
source: PodcastSource
): Promise<SearcherResult> => {
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)
}

View File

@@ -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
* ("<query> Daily Briefing" by "<Source> 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([]);
});