Explicit and Age-Restricted Tracks
How Orchard plays age-restricted songs that most YouTube Music clients cannot, and why the explicit flag never changes stream quality.
Most third-party YouTube Music clients cannot play age-restricted songs. Orchard can, on both desktop and Android. This page explains what the problem is, why it defeated the usual approach for so long, and how Orchard solves it.
Tracked as issue #3.
The symptom
A song fails with “Sign in to confirm your age” while everything else in the catalog plays fine. In practice this lands hardest on songs with profanity in the title, which is why the pattern users noticed was that anything with “fuck” in the name refused to play.
The original report was Evil Neuro’s “Crazy Fucking Robot Body”. Later reports in the same thread followed the same shape. As one reporter put it, YouTube is clearly not happy with the F word.
Two different things that look identical
The single most important distinction on this page, and the one that most clients get wrong:
| Concept | What it is | Effect on playback |
|---|---|---|
| Explicit | A catalog metadata flag. A lyrics advisory, the “E” badge. | None. It is a content label. |
| Age-restricted | A playability decision made by YouTube’s player API. | Playback is refused without an authenticated session. |
They correlate, which is what makes them easy to conflate. Plenty of explicit tracks are not age-gated, and the two are decided by different systems.
Orchard treats the explicit flag as advisory only. The rule, stated in
YouTubeStreamResolver.kt:
Catalog metadata’s “explicit” flag is only a lyrics advisory and must never select a lower-quality stream.
Why that matters is covered in Never downgrade on a guess below.
Why this was hard
Age-restricted playback requires an authenticated request to YouTube’s InnerTube player API, signed correctly, with a signature timestamp matching the player build YouTube is currently serving. The library most clients build on, youtubei.js, did not support authenticated InnerTube player requests.
This is not an Orchard-specific gap. The same limitation affects ArchiveTune and Metrolist, and issue #3 was originally closed as not planned for exactly that reason.
It was reopened once it became clear the approach yt-dlp
uses could be adapted. Android was fixed first, in
7e3c349,
with credit to Julian-FF2000. The desktop port landed in
afb6d80,
721 lines, closing the issue on 2026-08-11.
The desktop port was genuinely difficult. Tracks that had always been broken started working while the one song from the original report kept failing, and early fixes for the others broke the home feed.
How Orchard does it
Four pieces have to be right at the same time. Any one of them wrong produces a URL that looks completely plausible and still fails.
1. Session authentication
Player requests are signed with a SAPISIDHASH, a SHA-1 over the current epoch second, the
relevant cookie value, and the origin.
Android emits all applicable hashes rather than only the first, deriving SAPISIDHASH,
SAPISID1PHASH, and SAPISID3PHASH from SAPISID, __Secure-1PAPISID, and
__Secure-3PAPISID respectively. Requests also carry the account index, the data sync id
(as X-Goog-PageId), and visitor data.
The player request sets contentCheckOk: true and racyCheckOk: true, which is how a
client states it is willing to receive restricted content.
2. A signature timestamp matching the live player build
This is the trap that silently breaks everything, documented in YouTubeChallengeSolver.kt:
The player request and the decipher step must use this same build. Mixing the current timestamp with a pinned older script produces a plausible googlevideo URL whose signature the CDN rejects with HTTP 403.
So Orchard discovers the player build at runtime by fetching
https://www.youtube.com/iframe_api, extracting the current player URL, and reading the
signature timestamp out of that exact script. The pinned DEFAULT_PLAYER_URL and
DEFAULT_SIGNATURE_TIMESTAMP constants exist only as a network-failure fallback, and are
explicitly commented as such.
The build is cached and can be invalidated. On desktop, an error matching “needs to be reloaded” triggers one forced account refresh and a single retry against a freshly loaded player.
3. Deciphering both challenges
Stream URLs carry two obfuscated values, and both have to be solved with the same player script:
- The signature (
sig), solved from the player JS. - The
nparameter, which throttles playback if left unsolved.
Android solves these in a headless WebView running the real player JS, and caches solved n
values.
The n challenge has a second form that is easy to miss. In progressive formats it is a
query parameter. In HLS manifests it is path-shaped, /n/<challenge>/. From
decipherManifestUrl:
Direct formats put this value in a query parameter, but manifests encode it as
/n/<challenge>/; requesting that path unchanged lets the playlist load while its media chunks are rejected with HTTP 403.
A manifest that loads while every chunk 403s is a confusing failure, and it comes from solving one form of the challenge and not the other.
4. Two playback paths
| Path | Client | Format | Role |
|---|---|---|---|
| Authenticated direct | WEB_REMIX | itag 18, progressive MP4 | Primary. Ordinary progressive file, so playback starts immediately. |
| Safari HLS | WEB with a Safari user agent | HLS manifest | Fallback. Slower to start, broadly compatible. |
The Safari path has to be consistent end to end: the manifest is requested with a Safari identity, and media chunks are then fetched with that same identity. On Android the HLS stream is routed through Media3 with the matching Safari user agent, and HLS manifests are deliberately kept out of the progressive stream cache.
Orchard is signed-in-only for this. Without a session it reports “Sign in to YouTube to play age-restricted tracks” rather than failing obscurely.
Never downgrade on a guess
Getting age-restricted playback working created a second, subtler problem, fixed in
f272f4d.
The first Android implementation routed every track flagged explicit straight down the authenticated path, on the assumption that explicit implies age-gated. That path returns itag 18, a modest-quality progressive stream chosen for reliability rather than fidelity.
The result was a silent quality downgrade applied to a large slice of a normal music library, most of which was never age-restricted at all.
The fix inverts the order. Explicit no longer routes anything:
- Every track resolves through the normal, full-quality ladder first. On Android that
ladder is
ANDROID_VR, then a retry with a fresh visitor identity, thenANDROID,IOS,WEB_REMIX, andTVHTML5. - If any client in that ladder returns a real age gate, the video id is recorded.
- Only after the normal stream actually fails does the playback service consume that recorded evidence and retry through authenticated itag 18.
From the source: this keeps “authenticated itag 18 a real fallback”. Nothing is downgraded on a guess, and the authenticated path is reserved for tracks that provably need it.
Desktop music-video fallback
Desktop carries one extra route for tracks that resist everything above. When a track is age-gated, Orchard can find the same recording published as a music video and play its audio.
Candidates must match on normalized title, match on artist where both are known, be an actual video, and land within 5 seconds of the target duration. The closest duration match wins. Playback then reports the original track id, so the queue and your history stay correct even though a different video id supplied the audio.
There is also a narrow proactive heuristic, isAgeGateRiskTrack, which looks for titles
beginning with “fuck” and tries the music-video route first for those. It applies only when
the track is not flagged explicit, because a properly flagged explicit track goes down
the normal path where the real age-gate detection lives.
Some releases have no alternate video at all. That was the situation in the original issue report, and it is why a fallback alone was never a complete answer.
If an age-restricted track still fails
- Sign in. This path requires an authenticated session and there is no way around that.
- Check the track is not simply unavailable for your account or region. Try it on the YouTube Music website with the same account.
- Restart Orchard if failures started suddenly. A stale cached player build recovers on its own, though a restart forces it.
- Report it with the video id. YouTube changes this machinery periodically, and it has broken on both platforms before.
More general playback help in Troubleshooting.
A standing warning
This is the most fragile surface in Orchard. It depends on the current shape of YouTube’s player script, its challenge formats, its client version strings, and its session signing. All of those have changed before, and the workaround has stopped working on both platforms at least once already.
When age-restricted playback breaks after working fine, an upstream change is the most likely cause. See Privacy and Data for what an authenticated session means for your account.
Source map
| Path | Role |
|---|---|
electron/playback/authenticatedYouTubePlayback.js | Desktop WEB_REMIX direct and Safari HLS |
electron/playback/playbackErrors.js | Age-gate, bot-check, and guest-fallback detection |
electron/playback/playbackRouting.js | When to attempt an authenticated retry |
electron/playback/musicVideoFallback.js | Duration-matched music-video search |
electron/auth/youtubeClientSession.js | Desktop session refresh |
src/app/playback/hlsPlayback.js | Renderer HLS playback |
mobile/.../playback/YouTubeChallengeSolver.kt | Player discovery, sig and n deciphering |
mobile/.../playback/YouTubeStreamResolver.kt | Client ladder and age-gate recording |
mobile/.../playback/MediaItemMapper.kt | Direct and HLS fallback item construction |
mobile/.../auth/YouTubeSessionAuth.kt | SAPISIDHASH signing |