@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
Maintainers
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
visitorDataare 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
- Quick start
- CLI
- API reference
- How it works
- Notes & limitations
- Error handling
- Contributing & publishing
- License
Install
npm install @shafin5556/yticRequires 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 URLGlobal 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-paginatedSearchResult:
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 objecttranscript() 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,
yticloadsyoutube.com, scrapes the live API key, client version andvisitorData, 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
timedtextURLs 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 return403for any range past the first ~100 KB. - Throttle-resistant proxy. The WebPlayer fetches each requested range from
googlevideoin 1 MiB sub-chunks, which sidesteps YouTube's sustained-transfer throttling without deciphering thenparameter.
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
CLIENTSif 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 rebuildsPublished as the scoped package
@shafin5556/ytic(npm's name-similarity filter rejects the bareytic). The CLI command is stillytic.
License
MIT. Not affiliated with YouTube or Google.
