feat(settings): add Podcast Index fallback source with credential storage
Podcast Index (api.podcastindex.org) ships as a disabled, key-less source and is only consulted as a fallback when primary search results are fewer than 3 — never on the hot path, never when disabled or credential-less. A failed fallback leaves primary results intact. Credentials are user-supplied: enabling the source pops a dialog that asks for the free key+secret, prefilled masked (first 3 chars + "...") when already stored; toggling off never clears them. Secrets prefer the macOS keychain (security CLI, encrypted at rest) with a plaintext config.json fallback when the keychain is unavailable; sources carry only a hasCredentials/credentialStorage marker, and legacy plaintext keys in existing configs are migrated on load. Auth follows the documented scheme: X-Auth-Key, X-Auth-Date (epoch) and Authorization = sha1(key + secret + date). Dead feeds are filtered, feed URLs are used directly, and episode-scope search is a no-op (no endpoint).
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
* Podcast source type definitions for PodTUI
|
||||
*/
|
||||
|
||||
import type { Episode } from "./episode"
|
||||
import type { Podcast } from "./podcast"
|
||||
|
||||
/** Source type enumeration */
|
||||
export enum SourceType {
|
||||
/** RSS feed URL */
|
||||
@@ -22,8 +25,21 @@ export interface PodcastSource {
|
||||
type: SourceType
|
||||
/** Base URL for the source */
|
||||
baseUrl: string
|
||||
/** API key (if required) */
|
||||
/** API key — live only when the keychain is unavailable and the source
|
||||
* uses the plaintext fallback (credentialStorage "plaintext"). Legacy
|
||||
* plaintext keys are migrated to the OS keychain on load and stripped. */
|
||||
apiKey?: string
|
||||
/** API secret (e.g. Podcast Index signature auth) — same lifecycle as
|
||||
* apiKey: held in the OS keychain by default, live on the source only
|
||||
* under the plaintext fallback. */
|
||||
apiSecret?: string
|
||||
/** True when this source's credentials are stored. A source is usable once
|
||||
* enabled. */
|
||||
hasCredentials?: boolean
|
||||
/** Where this source's credentials live: the OS keychain (encrypted at
|
||||
* rest) by default, or config.json as a plaintext fallback when the
|
||||
* keychain is unavailable (e.g. non-macOS). */
|
||||
credentialStorage?: "keychain" | "plaintext"
|
||||
/** Whether source is enabled */
|
||||
enabled: boolean
|
||||
/** Source icon/logo URL */
|
||||
@@ -78,20 +94,39 @@ export enum SearchSortField {
|
||||
POPULARITY = "popularity",
|
||||
}
|
||||
|
||||
/** Search result */
|
||||
export interface SearchResult {
|
||||
/** What a directory search targets: shows or individual episodes. */
|
||||
export type SearchScope = "podcast" | "episode"
|
||||
|
||||
/** Fields shared by every search result. */
|
||||
export interface SearchResultBase {
|
||||
/** Source that returned this result */
|
||||
sourceId: string
|
||||
/** Source display name */
|
||||
sourceName?: string
|
||||
/** Source type */
|
||||
sourceType?: SourceType
|
||||
/** Podcast data */
|
||||
podcast: import("./podcast").Podcast
|
||||
/** Relevance score (0-1) */
|
||||
score?: number
|
||||
}
|
||||
|
||||
/** A show found by directory search. */
|
||||
export interface PodcastSearchResult extends SearchResultBase {
|
||||
kind: "podcast"
|
||||
/** Podcast data */
|
||||
podcast: Podcast
|
||||
}
|
||||
|
||||
/** A single episode found by directory search. `podcast` is its parent show
|
||||
* — used for display context and for subscribing to the show. */
|
||||
export interface EpisodeSearchResult extends SearchResultBase {
|
||||
kind: "episode"
|
||||
podcast: Podcast
|
||||
episode: Episode
|
||||
}
|
||||
|
||||
/** Search result */
|
||||
export type SearchResult = PodcastSearchResult | EpisodeSearchResult
|
||||
|
||||
/** Default podcast sources */
|
||||
export const DEFAULT_SOURCES: PodcastSource[] = [
|
||||
{
|
||||
@@ -105,4 +140,15 @@ export const DEFAULT_SOURCES: PodcastSource[] = [
|
||||
language: "en_us",
|
||||
allowExplicit: true,
|
||||
},
|
||||
{
|
||||
id: "podcastindex",
|
||||
name: "Podcast Index",
|
||||
type: SourceType.API,
|
||||
baseUrl: "https://api.podcastindex.org/api/1.0/search/byterm",
|
||||
enabled: false,
|
||||
description:
|
||||
"Open podcast directory. Fallback when other sources return few results; requires a free API key + secret from podcastindex.org.",
|
||||
language: "en",
|
||||
allowExplicit: true,
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user