YouTube API Integration#
RSSHub's YouTube integration lives in lib/routes/youtube/ and uses a dual-backend architecture: the official Google YouTube Data API v3 is attempted first, with automatic fallback to the unofficial Innertube API via youtubei.js when no API key is configured or a request fails. The fallback logic lives in callApi() in utils.tsx.
Dual-Backend Architecture#
| Backend | File | Trigger |
|---|---|---|
| Google YouTube Data API v3 | api/google.ts | YOUTUBE_KEY env var present |
| Innertube (unofficial) | api/youtubei.ts | Fallback / no API key |
callApi() in utils.tsx is the single dispatch point: if config.youtube?.key is set it calls the Google API backend and catches errors to retry on Innertube; otherwise it goes straight to Innertube .
Google API (api/google.ts): Uses @googleapis/youtube (switched from the monolithic googleapis bundle in PR #22478). An exec() wrapper rotates multiple configured API keys and handles rate-limit fallback . OAuth2 is also supported for the subscriptions feed .
Innertube API (api/youtubei.ts): Initializes an Innertube client lazily (to avoid network calls at import/build time) , then provides matching getDataByChannelId, getDataByUsername, and getDataByPlaylistId exports .
Video Metadata Extraction#
Both backends extract the same normalized fields before returning a Data object:
| Field | Google API source | Innertube source |
|---|---|---|
title | snippet.title | video.title.text |
pubDate | snippet.publishedAt | video.published.text via parseRelativeDate |
image | snippet.thumbnails → getThumbnail() | video.best_thumbnail or video.thumbnails[0] |
description | snippet.description rendered via renderDescription() | description_snippet.toHTML() via renderDescription() |
duration | contentDetails.duration | video.duration.seconds |
author | snippet.videoOwnerChannelTitle | video.author.name |
The renderDescription() helper renders either an embeddable <iframe> (when embed=true) or a <img> thumbnail, defaulting to youtube-nocookie.com for privacy. The embed base URL is configurable via config.youtube.videoEmbedUrl .
getVideoUrl() returns https://www.youtube-nocookie.com/embed/{id}?controls=1&autoplay=1&mute=0, used as the attachment URL in all feed items .
LockupView Migration and youtubei.js Version History#
YouTube periodically changes its internal API response formats. The current relevant transition is from legacy Video/GridVideo wrapper objects to LockupView objects (which use content_id instead of video_id and nest metadata differently).
youtubei.js version timeline:
| Version | PR | Key change |
|---|---|---|
| 16.0.1 → 17.0.1 | PR #21425 | Initial LockupView support added in history module; removal of getTrending (YouTube removed trending feed) |
| 17.0.1 → 17.2.0 | PR #22362 | Bug fix: parse LockupViews in playlist items; adds ContinuationItemView, ChipBarView, ListView |
The Innertube path in youtubei.ts guards against objects that lack a video_id field by filtering with 'video_id' in video before mapping , which provides resilience when the upstream library returns mixed LockupView and legacy Video objects in the same response.
Key Utilities and Supporting Features#
Shorts filtering: getPlaylistWithShortsFilter() converts channel/playlist IDs to the UULF… playlist format (from UC…/UU…) to exclude Shorts from feeds. This is a URL-level trick with no API call required .
Subtitles: api/subtitles.ts provides getSrtAttachmentBatch() for batch SRT extraction (max concurrency 5) and attaches subtitle data URLs as additional attachments on feed items when isJsonFeed=true .
Caching: All Google API calls are wrapped with cache.tryGet() keyed on resource type + ID (e.g., youtube:getPlaylistItems:{id}) with config.cache.routeExpire TTL . Channel ID lookups via Innertube are similarly cached .
Google API dependency: Switched from the monolithic googleapis (v173) to @googleapis/youtube (v33) in PR #22478, reducing bundle size. Imports changed to named exports: import { auth as googleAuth, youtube as googleYoutube } from '@googleapis/youtube' .
Route entry points: Individual routes (channel.ts, user.ts, playlist.ts, live.ts, subscriptions.ts, community.tsx) all call into these shared API modules via callApi() .