import type { Platform, PlatformSourceMetadata, PlatformContentMetadata, PlaylistDiscoveryResult, Channel, } from '../types/index'; // ── Options ── /** Options for fetchRecentContent, loaded from platform settings. */ export interface FetchRecentContentOptions { /** Maximum items to enumerate in the discovery phase. Default: 50 */ limit?: number; /** Set of platformContentIds already known — skips enrichment for these. */ existingIds?: Set; /** Milliseconds to wait between per-item enrichment calls. Default: 1000 */ rateLimitDelay?: number; } // ── Interface ── /** * Extensible plugin contract for platform integrations. * Each platform (YouTube, SoundCloud, etc.) implements this interface * to provide channel resolution and content fetching via yt-dlp. */ export interface PlatformSource { /** Resolve a platform URL to channel metadata (name, platformId, image). */ resolveChannel(url: string): Promise; /** Fetch recent content items for a channel. */ fetchRecentContent( channel: Channel, options?: FetchRecentContentOptions ): Promise; /** * Fetch ALL content for a channel (no playlist-items limit). * Used by back-catalog import. Optional — platforms that don't support * full catalog fetch fall back to fetchRecentContent with a high limit. */ fetchAllContent?(channel: Channel): Promise; /** * Fetch playlists for a channel, with video-to-playlist mappings. * Optional — not all platforms expose playlist information. */ fetchPlaylists?(channel: Channel): Promise; } // ── Registry ── /** * Maps Platform enum values to PlatformSource implementations. * Resolves the correct source from a URL via pattern matching. */ export class PlatformRegistry { private readonly sources = new Map(); /** Register a platform source implementation. */ register(platform: Platform, source: PlatformSource): void { this.sources.set(platform, source); } /** Get the source for a known platform. */ get(platform: Platform): PlatformSource | undefined { return this.sources.get(platform); } /** * Detect the platform from a URL and return the corresponding source. * Returns null if the URL doesn't match any registered platform. */ getForUrl(url: string): { platform: Platform; source: PlatformSource } | null { const platform = detectPlatformFromUrl(url); if (!platform) return null; const source = this.sources.get(platform); if (!source) return null; return { platform, source }; } } // ── URL Detection ── const YOUTUBE_PATTERNS = [ /^https?:\/\/(www\.)?youtube\.com\/@/, /^https?:\/\/(www\.)?youtube\.com\/channel\//, /^https?:\/\/(www\.)?youtube\.com\/c\//, /^https?:\/\/(www\.)?youtube\.com\/user\//, /^https?:\/\/youtu\.be\//, ]; const SOUNDCLOUD_PATTERNS = [ /^https?:\/\/(www\.)?soundcloud\.com\/[^/]+\/?$/, /^https?:\/\/(www\.)?soundcloud\.com\/[^/]+$/, ]; function detectPlatformFromUrl(url: string): Platform | null { for (const pattern of YOUTUBE_PATTERNS) { if (pattern.test(url)) return 'youtube' as Platform; } // SoundCloud: match artist pages, reject track/set URLs if (/^https?:\/\/(www\.)?soundcloud\.com\//.test(url)) { // Reject track and set URLs if (/\/(tracks|sets)\//.test(url)) return null; // Accept artist-level URLs return 'soundcloud' as Platform; } return null; }