{"id":50391015,"url":"https://github.com/hallelx2/tytube","last_synced_at":"2026-05-30T18:01:36.381Z","repository":{"id":350945202,"uuid":"1208883130","full_name":"hallelx2/tytube","owner":"hallelx2","description":"TypeScript port of pytube. Extract YouTube metadata, list streams, and read captions without the YouTube Data API. Works in Node, Bun, and the browser.","archived":false,"fork":false,"pushed_at":"2026-04-12T21:42:32.000Z","size":69,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-12T23:25:18.022Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hallelx2.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-12T21:36:02.000Z","updated_at":"2026-04-12T21:42:36.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hallelx2/tytube","commit_stats":null,"previous_names":["hallelx2/tytube"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/hallelx2/tytube","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hallelx2%2Ftytube","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hallelx2%2Ftytube/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hallelx2%2Ftytube/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hallelx2%2Ftytube/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hallelx2","download_url":"https://codeload.github.com/hallelx2/tytube/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hallelx2%2Ftytube/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33703065,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-30T18:01:35.451Z","updated_at":"2026-05-30T18:01:36.350Z","avatar_url":"https://github.com/hallelx2.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# tytube\n\n**A TypeScript port of [pytube](https://github.com/pytube/pytube). Extract YouTube metadata, list streams, and read captions — without the YouTube Data API.**\n\n[![npm version](https://img.shields.io/npm/v/@hallelx/tytube?color=cb3837\u0026logo=npm\u0026label=npm)](https://www.npmjs.com/package/@hallelx/tytube)\n[![npm downloads](https://img.shields.io/npm/dm/@hallelx/tytube?color=cb3837\u0026logo=npm)](https://www.npmjs.com/package/@hallelx/tytube)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/@hallelx/tytube?label=minzip)](https://bundlephobia.com/package/@hallelx/tytube)\n[![CI](https://img.shields.io/github/actions/workflow/status/hallelx2/tytube/ci.yml?branch=main\u0026logo=github\u0026label=CI)](https://github.com/hallelx2/tytube/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6?logo=typescript\u0026logoColor=white)](tsconfig.json)\n[![Node](https://img.shields.io/node/v/@hallelx/tytube?logo=node.js\u0026logoColor=white)](package.json)\n[![Bun](https://img.shields.io/badge/Bun-ready-fbf0df?logo=bun\u0026logoColor=black)](https://bun.sh)\n[![Tests](https://img.shields.io/badge/tests-45_passing-success?logo=vitest\u0026logoColor=white)](tests)\n\n`youtube` \u0026nbsp;·\u0026nbsp; `metadata` \u0026nbsp;·\u0026nbsp; `streams` \u0026nbsp;·\u0026nbsp; `captions` \u0026nbsp;·\u0026nbsp; `playlist` \u0026nbsp;·\u0026nbsp; `channel` \u0026nbsp;·\u0026nbsp; `search` \u0026nbsp;·\u0026nbsp; `no-api-key` \u0026nbsp;·\u0026nbsp; `pytube` \u0026nbsp;·\u0026nbsp; `bun` \u0026nbsp;·\u0026nbsp; `node` \u0026nbsp;·\u0026nbsp; `esm`\n\n\u003c/div\u003e\n\n---\n\n\u003e **Status: pre-release.** Metadata extraction, stream discovery, and caption listing all work end-to-end against live YouTube. Full media downloads require a working `n`-parameter cipher against YouTube's current `base.js` and are not yet implemented in v0.1 — see [Limitations](#limitations).\n\n## Why tytube?\n\nExisting JS YouTube libraries either wrap a Python binary (`youtube-dl-exec`), depend on the YouTube Data API v3 (rate limits + API key required), or break under non-Node runtimes. tytube is a **pure TypeScript** rewrite of pytube's API surface that runs natively under **Bun**, has **zero runtime dependencies**, and ships **ESM + CJS + .d.ts** out of the box.\n\n- **No Python**, no `yt-dlp` shell-out, no Data API key.\n- **Zero runtime dependencies** — just `fetch` and (optionally) `node:fs`.\n- **1:1 pytube API parity** — if you know pytube, you know tytube.\n- **First-class Bun support** — built and tested on Bun matrix CI.\n- **Strict TypeScript** with full `.d.ts` declarations and `noUncheckedIndexedAccess`.\n\n## Install\n\n```bash\nbun add @hallelx/tytube     # Bun\nnpm install @hallelx/tytube # Node\npnpm add @hallelx/tytube    # pnpm\nyarn add @hallelx/tytube    # Yarn\n```\n\n## Quick start\n\n```ts\nimport { YouTube } from '@hallelx/tytube';\n\nconst yt = new YouTube('https://youtu.be/lpFcNQpH81Q');\nawait yt.prefetch();\n\nconsole.log(await yt.title());     // \"Build a Language Learning Mobile App: …\"\nconsole.log(await yt.author());    // \"Andreas Trolle\"\nconsole.log(await yt.length());    // 31126 (seconds)\nconsole.log(await yt.views());     // 16372\n\nconst streams = await yt.streams();\nconsole.log(`Found ${streams.length} streams`);\nfor (const stream of streams) {\n  console.log(stream.toString());\n}\n\nconst captions = await yt.captions();\nfor (const c of captions) {\n  console.log(`${c.code}: ${c.name}`);\n}\n```\n\nAfter `prefetch()`, the `*Sync` getters work without further I/O:\n\n```ts\nconsole.log(yt.titleSync);\nconsole.log(yt.authorSync);\nconsole.log(yt.streamsSync.length);\n```\n\n## Playlists, channels, and search\n\n```ts\nimport { Playlist, Channel, Search } from '@hallelx/tytube';\n\n// Playlist iteration — async iterable, lazily paged\nconst playlist = new Playlist('https://www.youtube.com/playlist?list=PL...');\nconsole.log(await playlist.title());\nfor await (const video of playlist) {\n  console.log(await video.title());\n}\n\n// Channel videos\nconst channel = new Channel('https://www.youtube.com/c/SomeChannel');\nconsole.log(await channel.channelName());\nfor await (const video of channel) {\n  console.log(await video.title());\n}\n\n// Search results\nconst search = new Search('typescript tutorial');\nconst results = await search.results();\nfor (const yt of results) {\n  console.log(await yt.title());\n}\n```\n\n## CLI\n\n```bash\nbunx @hallelx/tytube https://youtu.be/lpFcNQpH81Q --list\nbunx @hallelx/tytube https://youtu.be/lpFcNQpH81Q --list-captions\nbunx @hallelx/tytube https://youtu.be/lpFcNQpH81Q -c en -t ./captions\nbunx @hallelx/tytube --version\n```\n\n\u003e Download flags (`-r`, `--itag`, `-a`, `-f`) are wired up but currently constrained by the cipher limitation below.\n\n## API parity with pytube\n\nAll metadata getters are async because they may trigger network I/O on first call. After `prefetch()`, sync `*Sync` getters work for hot-path access.\n\n| pytube                                  | tytube                                          |\n| --------------------------------------- | ----------------------------------------------- |\n| `YouTube(url)`                          | `new YouTube(url)`                              |\n| `yt.title`                              | `await yt.title()` \u0026nbsp;·\u0026nbsp; `yt.titleSync` |\n| `yt.author`                             | `await yt.author()`                             |\n| `yt.length`                             | `await yt.length()`                             |\n| `yt.views`                              | `await yt.views()`                              |\n| `yt.streams`                            | `await yt.streams()`                            |\n| `yt.captions`                           | `await yt.captions()`                           |\n| `yt.thumbnail_url`                      | `await yt.thumbnailUrl()`                       |\n| `yt.channel_url`                        | `await yt.channelUrl()`                         |\n| `yt.publish_date`                       | `await yt.publishDate()`                        |\n| `yt.metadata`                           | `await yt.metadata()`                           |\n| `Playlist(url).videos`                  | `for await (const v of new Playlist(url))`     |\n| `Channel(url).videos`                   | `for await (const v of new Channel(url))`      |\n| `Search(q).results`                     | `await new Search(q).results()`                 |\n| `stream.download(output_path=…)`        | `await stream.download({ outputPath: '…' })`   |\n| `caption.generate_srt_captions()`       | `await caption.generateSrtCaptions()`           |\n\nFiltering streams works the same way:\n\n```ts\nconst streams = await yt.streams();\n\nstreams.getHighestResolution();\nstreams.getLowestResolution();\nstreams.getAudioOnly('mp4');\nstreams.getByItag(22);\nstreams.getByResolution('720p');\nstreams.filter({ progressive: true, subtype: 'mp4' }).orderBy('resolution').last();\nstreams.filter({ onlyAudio: true }).orderBy('abr').last();\n```\n\n## Runtime support\n\n| Runtime              | Status                                                                                  |\n| -------------------- | --------------------------------------------------------------------------------------- |\n| Node.js ≥18          | ✅ Full support                                                                          |\n| Bun ≥1.0             | ✅ Full support — built and CI-tested on Bun                                            |\n| Deno                 | ✅ Works with `--allow-net` (uses global `fetch`)                                       |\n| Browser              | ⚠️ Requires a CORS proxy — YouTube does not send CORS headers                          |\n| Cloudflare Workers   | ⚠️ Cipher path requires `Function()` which is blocked under default CSP                 |\n\n## Limitations\n\n**Media downloads currently fail with HTTP 403.** YouTube ciphers the `n` parameter on stream URLs, and decoding `n` requires running JavaScript extracted from YouTube's `base.js`. tytube's cipher extractor uses pytube's regex patterns, which YouTube's late-2025 `base.js` has drifted beyond. This is the same problem pytube has hit repeatedly — YouTube rotates `base.js` and breaks the regex.\n\n**The metadata extraction path works completely** because it parses `ytInitialPlayerResponse` directly from the watch HTML, which doesn't depend on cipher decryption.\n\nIf you need actual media downloads today, use yt-dlp via `child_process.spawn` as a temporary bridge while we iterate on the cipher. We're tracking the cipher fix as the headline issue for v0.2.\n\n## Contributing\n\n```bash\ngit clone https://github.com/hallelx2/tytube\ncd tytube\nbun install\nbunx vitest run    # 45 tests, all passing\nbunx tsup          # build dist/\n```\n\nThe package mirrors pytube's module layout 1:1 in [`src/`](src) so contributors can read both side-by-side. The Python pytube source lives in the parent directory at `../pytube/pytube/` as a read-only reference.\n\n## Acknowledgements\n\n- The original [pytube](https://github.com/pytube/pytube) project, whose architecture and regex patterns this port closely follows.\n- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for keeping the InnerTube client configurations current.\n\n## License\n\n[MIT](LICENSE), with portions derived from pytube (originally The Unlicense).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhallelx2%2Ftytube","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhallelx2%2Ftytube","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhallelx2%2Ftytube/lists"}