@zeppeliorg/yt-sdl
v0.0.1
Published
Zero-dependency YouTube search + audio extract/download
Maintainers
Readme
@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-dlsRequirements
- Node.js 18+ — built-in global
fetch,vm, andstream/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:
signatureCipher— thesparam 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).- The
nparam — every stream URL also carries annvalue 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 handsato a couple of helper methods beforea.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
ytInitialDataJSON blob embedded in YouTube's search results page. Ifsearch()starts throwing "YouTube may have changed its page structure", that's what happened. - Signed CDN URLs from
getAudioUrl/searchAndGetUrlexpire 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 (
.m4aor.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
