v0.1.8
Menu
Download

YouTube Downloader

You paste a YouTube link. RuForge downloads the file and plays it locally. There is no in-app YouTube player.

The downloader tab is built around the URL bar. Paste, drop a link, or copy a watch URL elsewhere and paste when you focus the bar. extractYouTubeUrlFromText in youtubeUrl.ts pulls the first valid watch or playlist URL out of messy clipboard text (tracking params stripped, playlist preferred when list= is present).

Single videos go straight to metadata fetch. Playlist URLs expand first: get_video_info returns isPlaylist plus playlistItems, then buildPlaylistEnqueuePlan in useDownloaderView.ts builds one row per video with per-item audio toggles, duplicate badges, and a numbered output folder name before anything hits the queue.

After download, YouTube is out of the loop. scan_gallery reads .info.json sidecars for title, id, chapters, and playlist_index. The player loads the file from disk via convertFileSrc. Media stack cards group playlist folders so you browse a batch like a stack, not a flat pile of files.

Age-restricted or members-only videos need cookies. Settings → Downloads exposes a cookie file path and optional browser profile string. The Explorer tab (child webview) is there to log in on youtube.com when you do not already have a cookie export. yt-dlp gets the same cookie args on simulate and on actual download.

In the repo

/** Canonical playlist id from `list=` or `/playlist?list=`, or null. */
export function extractYouTubePlaylistId(input: string): string | null {
  const url = parseYoutubeUrl(input);
  if (!url || !isYoutubeHost(url.hostname)) return null;

  const host = url.hostname.replace(/^www\./i, "").toLowerCase();
  if (host === "youtu.be") return null;

  const list = url.searchParams.get("list")?.trim();
  if (list && YT_PLAYLIST_ID_RE.test(list)) return list;

  if (/^\/playlist\/?$/i.test(url.pathname)) {
    const fromPath = url.searchParams.get("list")?.trim();
    if (fromPath && YT_PLAYLIST_ID_RE.test(fromPath)) return fromPath;
  }

  return null;
}

/** Stable https playlist URL, or null if not a playlist link. */
export function canonicalYouTubePlaylistUrl(input: string): string | null {
  const id = extractYouTubePlaylistId(input);
  if (!id) return null;
  return `https://www.youtube.com/playlist?list=${id}`;
}
export function extractYouTubeUrlFromText(text: string): string | null {
  const trimmed = text.trim();
  if (!trimmed) return null;

  const direct = canonicalYouTubeDownloaderUrl(trimmed);
  if (direct) return direct;

  const matches = trimmed.match(YT_URL_IN_TEXT_RE);
  if (!matches) return null;

  for (const candidate of matches) {
    const playlist = canonicalYouTubePlaylistUrl(candidate);
    if (playlist) return playlist;
  }

  for (const candidate of matches) {
    const watch = canonicalYouTubeWatchUrl(candidate);
    if (watch) {
      if (extractYouTubePlaylistId(candidate)) {
        const pl = canonicalYouTubePlaylistUrl(candidate);
        if (pl) return pl;
      }
      return watch;
    }
  }

  return null;
}
    if (!videoInfo?.isPlaylist || !videoInfo.playlistItems?.length) return null;
    return buildPlaylistEnqueuePlan(
      videoInfo.playlistItems,
      entries,
      playlistItemAudioOverrides,
      heroAudioOnly,
      settings.skipDuplicatesAutomatically,
    );
  }, [
    videoInfo,
    entries,
    playlistItemAudioOverrides,
    heroAudioOnly,
    settings.skipDuplicatesAutomatically,
  ]);

  const playlistDuplicateSummary = useMemo(() => {
    if (!playlistEnqueuePlan || playlistEnqueuePlan.duplicates.length === 0) {
      return null;
    }
    const n = playlistEnqueuePlan.duplicates.length;
    const total = playlistEnqueuePlan.totalResolved;
    return `${n} of ${total} already in library`;
  }, [playlistEnqueuePlan]);

Where it shows up

  • youtubeUrl.ts for parse, canonical URLs, and clipboard extraction
  • useDownloaderView.ts for hero metadata, playlist plan, and enqueue
  • DownloaderView.tsx for the URL bar UI, duplicate banner, and Download button
  • .info.json sidecars written by yt-dlp; read back in gallery.rs