fix: remove fake RSS placeholder source; migrate persisted configs
This commit is contained in:
@@ -288,7 +288,15 @@ function createFeedStore() {
|
|||||||
const loadedFeeds = await loadFeedsFromFile();
|
const loadedFeeds = await loadFeedsFromFile();
|
||||||
if (loadedFeeds.length > 0) setFeeds(loadedFeeds);
|
if (loadedFeeds.length > 0) setFeeds(loadedFeeds);
|
||||||
const loadedSources = await loadSourcesFromFile<PodcastSource>();
|
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();
|
await refreshAllFeeds();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -367,7 +375,7 @@ function createFeedStore() {
|
|||||||
/** Remove a source */
|
/** Remove a source */
|
||||||
const removeSource = (sourceId: string) => {
|
const removeSource = (sourceId: string) => {
|
||||||
// Don't remove default sources
|
// Don't remove default sources
|
||||||
if (sourceId === "itunes" || sourceId === "rss") return false;
|
if (sourceId === "itunes") return false;
|
||||||
|
|
||||||
setSources((prev) => {
|
setSources((prev) => {
|
||||||
const updated = prev.filter((s) => s.id !== sourceId);
|
const updated = prev.filter((s) => s.id !== sourceId);
|
||||||
|
|||||||
@@ -105,12 +105,4 @@ export const DEFAULT_SOURCES: PodcastSource[] = [
|
|||||||
language: "en_us",
|
language: "en_us",
|
||||||
allowExplicit: true,
|
allowExplicit: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "rss",
|
|
||||||
name: "RSS Feed",
|
|
||||||
type: SourceType.RSS,
|
|
||||||
baseUrl: "",
|
|
||||||
enabled: true,
|
|
||||||
description: "Add podcasts via RSS feed URL",
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,95 +4,12 @@ import type { PodcastSource, SearchResult } from "../types/source"
|
|||||||
|
|
||||||
type SearcherResult = SearchResult[]
|
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 =>
|
const slugify = (input: string): string =>
|
||||||
input
|
input
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, "-")
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
.replace(/^-+|-+$/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 = {
|
type ItunesResult = {
|
||||||
collectionId?: number
|
collectionId?: number
|
||||||
collectionName?: string
|
collectionName?: string
|
||||||
@@ -173,23 +90,28 @@ const searchAPISource = async (
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchCustomSource = async (
|
/**
|
||||||
query: string,
|
* RSS-type sources have no directory search backend: a feed URL identifies one
|
||||||
source: PodcastSource
|
* show, and no API exists to search across "the RSS directory". Return no
|
||||||
): Promise<SearcherResult> => {
|
* results rather than fabricating them.
|
||||||
await delay(300, 650)
|
*/
|
||||||
return makeResults(query, source, 13)
|
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 (
|
export const searchSourceByType = async (
|
||||||
query: string,
|
query: string,
|
||||||
source: PodcastSource
|
source: PodcastSource
|
||||||
): Promise<SearcherResult> => {
|
): Promise<SearcherResult> => {
|
||||||
if (source.type === SourceType.RSS) {
|
if (source.type === SourceType.RSS) {
|
||||||
return searchRSSSource(query, source)
|
return searchRSSSource()
|
||||||
}
|
}
|
||||||
if (source.type === SourceType.CUSTOM) {
|
if (source.type === SourceType.CUSTOM) {
|
||||||
return searchCustomSource(query, source)
|
return searchCustomSource()
|
||||||
}
|
}
|
||||||
return searchAPISource(query, source)
|
return searchAPISource(query, source)
|
||||||
}
|
}
|
||||||
|
|||||||
40
tests/source-searcher.test.ts
Normal file
40
tests/source-searcher.test.ts
Normal 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([]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user