npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zeppeliorg/yt-sdl

v0.0.1

Published

Zero-dependency YouTube search + audio extract/download

Readme

npm version License Downloads

Donation site

@zeppeliorg/yt-dls

Zero-dependencies, no external binaries. Search YouTube (by parsing YouTube's own public search results page) and get either a direct audio stream URL or a downloaded audio file — no API key, no yt-dlp, no ffmpeg. Extraction works by parsing YouTube's watch page + player JS directly, using only Node's built-in fetch and vm modules.

Why not just shell out to yt-dlp? So this package has zero runtime requirements beyond Node itself — nothing to install on the host, nothing that can be missing from PATH, works the same on Termux/Android as anywhere else.

npm install @zeppeliorg/yt-dls

Requirements

  • Node.js 18+ — built-in global fetch, vm, and stream/promises. That's it. No other install steps.

Usage

Just search

const { search } = require("@zeppeliorg/yt-dls");

const results = await search("Led Zeppelin", { limit: 3 });
console.log(results[0].title, results[0].url);

Fast path — direct audio URL, no download

Best for quick playback links (e.g. Telegram inline query results). The URL is Google's signed CDN link — time-limited (expires in a few hours), so use it right away rather than caching it long-term.

const { searchAndGetUrl } = require("@zeppeliorg/yt-dls");

const track = await searchAndGetUrl("Led Zeppelin");
if (track) {
  console.log(track.title, track.audioUrl);
}

Reliable path — actually download the audio

Better when you need the file to still be valid a bit later (e.g. queued for upload) instead of a link that might expire.

const { searchAndDownload } = require("@zeppeliorg/yt-dls");
const fs = require("fs");

const track = await searchAndDownload("Led Zeppelin");
if (track) {
  // track.filePath is .m4a or .webm — whatever YouTube actually served,
  // since there's no ffmpeg here to re-encode to .mp3
  console.log(track.filePath, track.ext);
  fs.unlink(track.filePath, () => {}); // clean up when you're done —
  // there's also a built-in 10s auto-delete safety net either way
}

Lower-level functions

const { search, getAudioUrl, downloadAudio } = require("@zeppeliorg/yt-dls");

const results = await search("some song", { limit: 5 });
const { url: audioUrl } = await getAudioUrl(results[0].url);
const { filePath, ext } = await downloadAudio(results[0].url, {
  fileName: "Made In Heaven"
});

API

| Function | Description | | --- | --- | | search(query, opts?) | Returns up to opts.limit (default 5) results: {videoId, title, url, thumbnail, author, timestamp, seconds} | | getAudioUrl(videoUrl, opts?) | Resolves a direct playable audio URL. Returns {url, mimeType, bitrate}. opts.timeoutMs (default 8000) | | downloadAudio(videoUrl, opts?) | Downloads the audio (native container — .m4a or .webm, no re-encoding). Returns {filePath, ext, mimeType}. opts.timeoutMs (default 60000), opts.autoDeleteMs (default 10000, safety-net cleanup) | | searchAndGetUrl(query, opts?) | search + getAudioUrl for the top result in one call | | searchAndDownload(query, opts?) | search + downloadAudio for the top result in one call |

Using it in a Telegram bot (xzcgram example)

const { searchAndGetUrl } = require("@zeppeliorg/yt-dls");

bot.command("play", async (ctx) => {
  const query = ctx.args.join(" ");
  if (!query) return ctx.replyQuote("Usage: /play <song name>");

  const track = await searchAndGetUrl(query).catch(() => null);
  if (!track) return ctx.replyQuote("Not found.");

  await ctx.replyWithButtons(
    `🎵 <b>${track.title}</b>\n${track.author.name}`,
    [[{ text: "Watch in YouTube", url: track.url }]],
    { media: track.audioUrl, parseMode: "html" }
  );
});

How extraction works (and why it's fragile)

YouTube signs its stream URLs in two ways:

  1. signatureCipher — the s param has to be run through a "decipher" function that's minified and inlined into YouTube's player JS (base.js), and shuffles the signature string around (reverse, splice, swap elements).
  2. The n param — every stream URL also carries an n value that has to be run through a second, unrelated "n-transform" function, also defined in the same player JS.

This package fetches the current player JS, locates both functions by matching the shape of the minified code (since their names are randomized every time YouTube rebuilds the player), and executes them in a vm sandbox using only Node's built-ins.

This is inherently more fragile than yt-dlp. yt-dlp survives YouTube's changes because a large team updates it quickly, often within hours of YouTube shipping a new player. Here, if YouTube reshapes its player JS in a way the pattern-matching doesn't expect, you'll get a clear thrown error (e.g. "Could not locate the signature-decipher function") rather than silent garbage — but the regexes in index.js (buildDecipher, buildNTransform) will need a manual update to match the new shape. If you hit this, open the current player JS in a readable form and look for:

  • a function that does a=a.split("") then hands a to a couple of helper methods before a.join("") → that's the decipher
  • a call site right after .get("n") → that names the n-transform

Other known limitations

  • The search step parses the ytInitialData JSON blob embedded in YouTube's search results page. If search() starts throwing "YouTube may have changed its page structure", that's what happened.
  • Signed CDN URLs from getAudioUrl/searchAndGetUrl expire after a few hours — don't cache them long-term.
  • No re-encoding step (no ffmpeg), so downloaded audio stays in whatever native container YouTube served (.m4a or .webm), not forced to .mp3.
  • Not affiliated with YouTube/Google. Respect YouTube's Terms of Service and applicable copyright law for how you use downloaded audio.

License

MIT