tubearr/src/sources/platform-source.ts
John Lightner 4606dce553 feat: Tubearr — full project state through M006/S01
Migrated git root from W:/programming/Projects/ to W:/programming/Projects/Tubearr/.
Previous history preserved in Tubearr-full-backup.bundle at parent directory.

Completed milestones: M001 through M005
Active: M006/S02 (Add Channel UX)
2026-03-24 20:20:10 -05:00

115 lines
3.5 KiB
TypeScript

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<string>;
/** 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<PlatformSourceMetadata>;
/** Fetch recent content items for a channel. */
fetchRecentContent(
channel: Channel,
options?: FetchRecentContentOptions
): Promise<PlatformContentMetadata[]>;
/**
* 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<PlatformContentMetadata[]>;
/**
* Fetch playlists for a channel, with video-to-playlist mappings.
* Optional — not all platforms expose playlist information.
*/
fetchPlaylists?(channel: Channel): Promise<PlaylistDiscoveryResult[]>;
}
// ── 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<Platform, PlatformSource>();
/** 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;
}