Migrating from v4
v5 is a rewrite with no compatibility layer. The module exports a Ytdl class instead of a callable ytdl function, and one instance holds the session YouTube gives it. Create it once and reuse it:
import { Ytdl } from "@distube/ytdl-core";
const yt = new Ytdl();Requirements and imports
Section titled “Requirements and imports”The package is ESM-only and has no default export. import ytdl from "@distube/ytdl-core" throws, and require("@distube/ytdl-core") returns the module namespace, not a function, so ytdl(url) fails. Use the named Ytdl import shown above.
4.15.1 declared Node.js >=14.0, and 4.16.0 raised that to >=20.18.1. v5 needs ^22.22.2 || ^24.15.0 || >=26.0.0, or Bun.
Functions
Section titled “Functions”| 4.x | v5 |
|---|---|
ytdl(url, options) |
yt.download(url, options) |
ytdl.downloadFromInfo(info, options) |
yt.download(info, options) |
ytdl.getInfo(url) |
yt.getInfo(url) |
ytdl.getBasicInfo(url) |
yt.getInfo(url); there is no lighter lookup |
ytdl.chooseFormat(formats, options) |
chooseFormat(formats, options) |
ytdl.filterFormats(formats, "audioonly") |
formats.filter((format) => format.hasAudio && !format.hasVideo) |
ytdl.getVideoID(input), getURLVideoID |
parseVideoId(input), which returns undefined instead of throwing |
ytdl.validateID(id), validateURL(url) |
parseVideoId(input) !== undefined |
ytdl.createAgent(cookies) |
new Ytdl({ cookie }) with the cookie export as text |
ytdl.createProxyAgent({ uri }, cookies) |
new Ytdl({ fetch, cookie }) with a proxied fetch |
ytdl.cache and ytdl.version are gone. Every other 4.15.1 export is in the table. v5 exports Ytdl, chooseFormat, parseVideoId, parsePlaylistId, Quality, Client, DEFAULT_CLIENTS, CaptionFormat, createPoTokenProvider, YtdlError and YtdlErrorCode.
Options
Section titled “Options”| 4.x | v5 |
|---|---|
quality: "highestaudio" |
quality: Quality.HighestAudio; every string has a Quality value |
filter: "audioonly" |
filter: { hasAudio: true, hasVideo: false }; see the string mapping |
format |
format, unchanged |
range: { start, end } |
range, unchanged |
begin |
startTimeMs in milliseconds, for SABR formats only; see Start time. 4.x also used &begin= to seek live and direct formats, and v5 has no equivalent for those |
dlChunkSize |
chunkSize; there is no unchunked mode |
liveBuffer, highWaterMark |
unchanged |
lang |
new Ytdl({ lang }) |
agent, requestOptions |
new Ytdl({ fetch }) |
fetch (4.16) |
new Ytdl({ fetch }). v5 calls it like the standard fetch(input, init), not with undici request options, so set a dispatcher or proxy inside it; see Proxies |
requestCallback |
none; 4.15.1 never called it |
rewriteRequest |
none; change the URL or init in a wrapper around fetch |
IPv6Block |
a fetch bound to one local address; see Proxies |
playerClients |
new Ytdl({ clients }) with Client values; see Clients |
Stream events
Section titled “Stream events”The stream is a plain Node Readable. Its 4.x events become options or calls:
// 4.xytdl(url) .on("info", (info, format) => {}) .on("progress", (chunk, downloaded, total) => {});
// v5const info = await yt.getInfo(url);const format = chooseFormat(info.formats, { quality: Quality.HighestAudio });yt.download(info, { format, onProgress: ({ chunkBytes, downloaded, total }) => {} });To read the same format again later, for example to seek, keep a Media from yt.open(info, format) instead of the info. stream.destroy() still stops a download, and signal cancels one with an AbortController.
Formats
Section titled “Formats”A format keeps the field names 4.x copied from YouTube, such as itag, mimeType, bitrate, initRange, audioQuality, audioTrack and colorInfo, with numeric strings parsed to numbers. Two differ: url is already solved, and signatureCipher is gone. ytdl-core’s own additions are listed in Choose a format. DRM and OTF formats are listed with isDrm and isOtf, and chooseFormat skips them.
4.x videoFormat |
v5 VideoFormat |
|---|---|
contentLength, lastModified, audioSampleRate, approxDurationMs |
numbers, not strings; lastModified is microseconds |
initRange, indexRange |
{ start, end } as numbers |
url |
optional; a format without one streams over SABR |
signatureCipher |
gone, the URL is solved |
isDashMPD |
gone; live DASH formats have isLive and no isHLS |
quality, qualityLabel |
string, not a literal union |
container, codecs, videoCodec, audioCodec, hasVideo, hasAudio, isLive, isHLS |
unchanged |
| none | isDrc, isDrm, isOtf, client, captionTrack, xtags, qualityOrdinal, loudnessDb |
getInfo returns a VideoInfo with eleven fields where 4.x returned about a hundred. Keys 4.x copied from YouTube’s watch page (iv_*, ad*, fmt_list, html5player, fexp, cver and the rest) are gone, and so is full.
4.x videoInfo |
v5 VideoInfo |
|---|---|
videoDetails |
videoDetails, reshaped below |
formats |
formats |
related_videos |
relatedVideos, a list of VideoSummary |
player_response |
playerResponse, typed unknown; narrow it before you read it |
player_response.captions |
captionTracks; see Captions |
html5player |
gone |
| none | client, hlsManifestUrl, sabr, visitorData, expiresAt, playableAt |
4.x videoDetails |
v5 videoDetails |
|---|---|
videoId |
id |
video_url |
url |
shortDescription, description |
description, undefined when empty |
lengthSeconds, viewCount |
numbers, not strings |
likes |
likes, undefined instead of null |
title, publishDate |
unchanged |
published |
typed in 4.x but never filled; use publishDate |
uploadDate, category, keywords, isLive |
unchanged |
age_restricted |
isAgeRestricted |
isLiveContent |
none; true for past broadcasts too, isLive is only while live |
externalChannelId |
author.id |
ownerGplusProfileUrl |
gone |
isUnlisted, isFamilySafe, availableCountries |
unchanged, undefined when YouTube omits them |
thumbnails, storyboards |
unchanged; Thumbnail width and height are optional |
chapters |
chapters, each { title, startTime } instead of start_time |
author |
an Author object, below |
channelId |
author.id |
ownerChannelName |
author.name |
ownerProfileUrl |
author.url |
liveBroadcastDetails |
none; use isLive and isUpcoming |
media |
gone |
averageRating, allowRatings, isOwnerViewing, isCrawlable, isPrivate, isUnpluggedCorpus, hasYpcMetadata, embed, thumbnail |
gone |
| none | isUpcoming |
4.x Author |
v5 Author |
|---|---|
id, name |
unchanged |
channel_url |
https://www.youtube.com/channel/${author.id} |
user_url |
url, the profile URL YouTube sends, else the channel URL |
subscriber_count |
subscriberCount |
verified |
verified, undefined when unknown |
thumbnails |
thumbnails, always a list |
avatar |
thumbnails[0]?.url |
user, external_channel_url |
gone |
4.x relatedVideo |
v5 VideoSummary |
|---|---|
id, title, isLive, thumbnails |
unchanged |
length_seconds |
lengthSeconds |
view_count, short_view_count_text |
viewCount, a number |
author (an Author or a string), ucid, author_thumbnail |
author: { id, name, url, thumbnails, verified, badges } |
published, video_thumbnail, richThumbnails |
gone |
From @distube/ytsr and @distube/ytpl
Section titled “From @distube/ytsr and @distube/ytpl”Both packages are deprecated. Ytdl#search and Ytdl#getPlaylist cover what they did, on the same instance as getInfo.
| ytsr / ytpl | v5 |
|---|---|
ytsr(query, { type: "video" }) |
yt.search(query) or { type: SearchType.Video } |
ytsr(query, { type: "playlist" }) |
yt.search(query, { type: SearchType.Playlist }) |
safeSearch |
search(query, { safeSearch: true }) |
limit |
limit; getPlaylist without it collects every entry instead of 100 |
hl, gl |
new Ytdl({ lang, country }) |
utcOffsetMinutes |
new Ytdl({ utcOffsetMinutes }) |
requestOptions |
new Ytdl({ fetch }) |
ytsr result items |
the array search returns |
ytsr results (estimated total), continuation |
gone |
ytsr Video |
VideoSummary: name is title, duration text is lengthSeconds, views is viewCount, upcoming is upcomingAt, thumbnail is thumbnails[0]?.url |
ytsr video author |
author: channelID is id, avatars and bestAvatar are thumbnails, ownerBadges is badges |
ytsr Playlist |
PlaylistSummary: name is title, length is videoCount, owner is author, publishedAt is updatedText |
ytpl(id) |
yt.getPlaylist(id) |
| ytpl channel ID or channel URL | getPlaylist expands the channel’s uploads |
ytpl items |
videos, VideoSummary values |
ytpl total_items, views |
videoCount, viewCount as a number |
ytpl author.channel_url |
author.url |
ytpl visibility, last_updated |
gone |
ytpl.validateID, ytpl.getPlaylistID |
parsePlaylistId(input), undefined instead of throwing; it reads playlist input only |
Errors
Section titled “Errors”Failures are YtdlError values with a code, such as PRIVATE, AGE_RESTRICTED or NO_FORMAT, instead of messages to match. See Errors.