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

@shafin5556/ytic

v1.0.0

Published

YouTube internal-protocol toolkit: search, video intelligence, transcripts/captions, comments, direct stream resolution and an internal browser streaming player — no API key, no scraping HTML, no third-party services. Pure InnerTube, zero runtime dependen

Readme

ytic

YouTube internal-protocol toolkit for Node.js — search, video intelligence, transcripts/captions, comments, direct stream resolution, and an internal browser streaming player.

  • No API key. Talks directly to YouTube's private InnerTube API the apps use.
  • No HTML scraping, no headless browser, no third-party services.
  • Zero runtime dependencies. Pure Node stdlib + global fetch.
  • Fully dynamic / incognito. API key, client version and visitorData are fetched fresh per session — nothing hardcoded.
  • TypeScript-first. Full typings for every result.

⚠️ Disclaimer. This library communicates with YouTube's private endpoints for interoperability/research. It is not affiliated with or endorsed by YouTube/Google. You are responsible for complying with YouTube's Terms of Service and the laws of your jurisdiction. Use responsibly and at your own risk.


Table of contents


Install

npm install @shafin5556/ytic

Requires Node.js ≥ 18 (uses the built-in global fetch). Works in CommonJS and ESM, JavaScript and TypeScript.

// ESM / TypeScript
import { SearchEngine, VideoIntelligence } from "@shafin5556/ytic";
// CommonJS
const { SearchEngine, VideoIntelligence } = require("@shafin5556/ytic");

Quick start

import { SearchEngine, VideoIntelligence, CommentScraper, WebPlayer } from "@shafin5556/ytic";

// 1) Search
const results = await new SearchEngine().search("lofi hip hop", 5);
console.log(results[0].title, results[0].url);

// 2) Video intelligence
const vi = new VideoIntelligence();
const meta = await vi.metadata(results[0].videoId!);
const transcript = await vi.transcript(results[0].videoId!);
console.log(meta.title, "·", transcript.length, "transcript segments");

// 3) Comments (with replies)
const comments = await new CommentScraper().scrape(results[0].videoId!, { limit: 50, withReplies: true });

// 4) Stream it in the browser (no download)
const handle = await new WebPlayer().serve(results[0].videoId!, { openBrowser: true });
console.log("Playing at", handle.url);

Every method is async and returns plain JSON-serializable objects.


CLI

Installing the package adds a ytic binary (use npx @shafin5556/ytic ... without a global install):

ytic search "lofi beats" --limit 10
ytic search "news" --filter upload_date --json

ytic video   https://www.youtube.com/watch?v=VIDEOID
ytic transcript VIDEOID --translate bn
ytic timeline   VIDEOID --translate bn

ytic comments VIDEOID --limit 50 --replies
ytic streams  VIDEOID --json

ytic play VIDEOID                 # browser streaming player (opens a local page)
ytic play VIDEOID --no-browser    # just print the URL

Global flags: --hl <lang> (interface language, default en), --gl <country> (default US), --json (machine-readable output), -h/--help.

Search filters: relevance, upload_date, view_count, rating, video, channel, playlist, short, long, live, 4k, hd, subtitles.


API reference

All classes accept either an InnerTube instance (to share one session) or an options object { hl?, gl?, timeout? }.

// Share a single session across modules (recommended):
import { InnerTube, SearchEngine, VideoIntelligence } from "@shafin5556/ytic";
const yt = new InnerTube({ hl: "en", gl: "US" });
const search = new SearchEngine(yt);
const vi = new VideoIntelligence(yt);

InnerTube

The low-level client. You rarely need it directly, but it powers everything.

const yt = new InnerTube({ hl?: string; gl?: string; timeout?: number });

await yt.config();                      // resolve {apiKey, visitorData, ...} (cached)
await yt.call(endpoint, payload, client?); // raw InnerTube POST
await yt.player(videoId, client?);      // /player for a given client
await yt.getText(url, headers?);        // raw GET (used for caption fetches)

Available client profiles: "WEB", "IOS", "ANDROID", "ANDROID_VR" (see How it works).

SearchEngine

const engine = new SearchEngine(optsOrClient?);

await engine.search(query, limit?, filter?): Promise<SearchResult[]>;
for await (const r of engine.iterSearch(query, filter?)) { ... } // lazy, auto-paginated

SearchResult:

interface SearchResult {
  rank: number;
  type: "video" | "channel" | "playlist";
  videoId?: string;
  title: string;
  url: string;
  thumbnailUrl?: string;
  channelName: string;
  channelId?: string;
  channelUrl?: string;
  duration?: string;
  durationSeconds?: number;
  views?: number;
  viewsText: string;
  published: string;
  description: string;
  badges: string[];
  live: boolean;
}

VideoIntelligence

const vi = new VideoIntelligence(optsOrClient?);

await vi.metadata(idOrUrl): Promise<VideoMetadata>;
await vi.captionTracks(idOrUrl): Promise<CaptionTrack[]>;
await vi.transcript(idOrUrl, { language?, translateTo? }): Promise<TranscriptSegment[]>;
await vi.banglaTranscript(idOrUrl): Promise<TranscriptSegment[]>;
await vi.speakerTimeline(idOrUrl, { translateTo?, groupGap?, window? }): Promise<TimelineBlock[]>;
await vi.audioTracks(idOrUrl): Promise<AudioTrack[]>;     // incl. dubbed languages
await vi.streamSummary(idOrUrl): Promise<StreamSummary>;
await vi.chapters(idOrUrl): Promise<Chapter[]>;           // from API or description timestamps
await vi.relatedVideos(idOrUrl, limit?): Promise<RelatedVideo[]>;
await vi.analyze(idOrUrl, translateTo?): Promise<{...}>;  // everything in one object

transcript() resolution order: a native track in the requested language → on-the-fly &tlang= translation (when the track is translatable) → the best available track. Captions are fetched directly from the IOS client's timedtext baseUrl (POToken-free), parsed from json3.

Speaker timeline note: YouTube's public captions carry no speaker diarization, so blocks are grouped by silence gaps / time windows and labelled Segment 1, 2, … rather than by real speaker identity.

CommentScraper

const scraper = new CommentScraper({ delay?: number, ...opts });

await scraper.scrape(idOrUrl, { limit?, withReplies?, repliesLimit? }): Promise<Comment[]>;
for await (const c of scraper.iterComments(idOrUrl, { withReplies?, repliesLimit? })) { ... }

Comment:

interface Comment {
  commentId: string;
  text: string;
  author: string;          // e.g. "@handle"
  authorChannelId: string;
  authorAvatar: string;
  likeCount?: number;
  published: string;
  replyCount: number;
  isReply: boolean;
  isPinned: boolean;
  isHearted: boolean;
  replies: Comment[];
}

Understands both the modern entity format (commentEntityPayload) and the legacy commentRenderer.

StreamResolver

const r = new StreamResolver(optsOrClient?);
const streams = await r.resolve(idOrUrl): Promise<ResolvedStreams>;

StreamResolver.bestProgressive(streams);
StreamResolver.bestVideo(streams, maxHeight?);
StreamResolver.bestAudio(streams, lang?);

ResolvedStreams contains progressive, video and audio arrays of StreamFormat (itag, direct url, codecs, width/height/fps/bitrate, initRange/indexRange for DASH, etc.). Streams are resolved via the ANDROID_VR client so the URLs support full random-access byte ranges (real seeking) without a POToken.

WebPlayer

A local DASH streaming player. It resolves streams, builds an MPD manifest, and proxies byte-range requests to YouTube — the browser streams directly from YouTube's servers; nothing touches disk.

const player = new WebPlayer(optsOrClient?);
const handle = await player.serve(idOrUrl, {
  port?: number,         // default 8675 (auto-scans next 20 if busy)
  openBrowser?: boolean, // default true
  audioLang?: string,    // preferred audio language
});
// handle: { url, port, server, close() }
await handle.close();

// one-liner:
import { serveWeb } from "@shafin5556/ytic";
const handle = await serveWeb(idOrUrl, { openBrowser: false });

The page (dash.js, pinned to v4.7.4) supports quality/resolution switching, audio-track selection, captions, and full seeking.


How it works

youtube.com  ──►  bootstrap: scrape INNERTUBE_API_KEY + visitorData (fresh, incognito)
                         │
   ┌─────────────────────┼─────────────────────────────────────────┐
   ▼                     ▼                                           ▼
/youtubei/v1/search   /youtubei/v1/next                      /youtubei/v1/player
 (WEB client)          (WEB client)                          (per-task client)
 search results        comments, chapters,                   ┌─ IOS:        captions (POToken-free)
                       related, like counts                  └─ ANDROID_VR: stream URLs (seekable!)
  • Dynamic config. On first call, ytic loads youtube.com, scrapes the live API key, client version and visitorData, and uses a clean cookie state — every run is a brand-new anonymous identity.
  • Right client for the job. Search/comments use WEB; captions use IOS (its timedtext URLs need no POToken); media uses ANDROID_VR, whose stream URLs are unsigned and allow arbitrary byte-range requests (so seeking and full playback work). IOS/WEB stream URLs are now POToken-gated and return 403 for any range past the first ~100 KB.
  • Throttle-resistant proxy. The WebPlayer fetches each requested range from googlevideo in 1 MiB sub-chunks, which sidesteps YouTube's sustained-transfer throttling without deciphering the n parameter.

Notes & limitations

  • Dubbed audio in the WebPlayer. Multi-language dubbed audio tracks are only reliably exposed by the IOS/WEB clients, which are POToken-gated (so they 403 on seek). The WebPlayer therefore streams the original audio (always present) to guarantee seeking. You can still enumerate all dubbed tracks via VideoIntelligence.audioTracks().
  • Age-restricted / members-only / region-locked / DRM videos may not resolve streams.
  • Live streams expose metadata and captions but the WebPlayer targets VOD (on-demand) DASH.
  • No speaker diarization in transcripts (see note above).
  • It depends on YouTube's private API, which can change. Client version strings are easy to bump in CLIENTS if a profile ages out.

Error handling

Network/API failures throw InnerTubeError. Parsing helpers fail soft (return empty arrays / undefined) so a missing field never crashes a whole pipeline.

import { InnerTubeError } from "@shafin5556/ytic";
try {
  await new VideoIntelligence().metadata("badid");
} catch (e) {
  if (e instanceof InnerTubeError) console.error("InnerTube failed:", e.message);
}

Tune the per-request timeout: new InnerTube({ timeout: 30000 }).


Contributing & publishing

npm install        # install dev deps
npm run build      # compile TypeScript -> dist/
npm run dev        # watch mode
node examples/search.mjs "query"

To publish (after bumping version in package.json):

npm login
npm publish    # access:public is set in package.json; prepublishOnly rebuilds

Published as the scoped package @shafin5556/ytic (npm's name-similarity filter rejects the bare ytic). The CLI command is still ytic.


License

MIT. Not affiliated with YouTube or Google.