From 830c5da3c3684f4a6df29bfa82bdd861d10d39f9 Mon Sep 17 00:00:00 2001 From: Joshua Wakefield Date: Wed, 9 Sep 2026 14:36:32 -0400 Subject: [PATCH] Allow playback without a YouTube Premium account Remove the client-side Premium gate from the audio engine. Free and signed-out users can now stream; the stream server already supports them via the existing ?ephemeral=1 session-cache path. --- src/lib/audio-engine.ts | 50 +++++------------------------------------ 1 file changed, 6 insertions(+), 44 deletions(-) diff --git a/src/lib/audio-engine.ts b/src/lib/audio-engine.ts index 4ec8a0f..bee2028 100644 --- a/src/lib/audio-engine.ts +++ b/src/lib/audio-engine.ts @@ -13,9 +13,7 @@ import { prefetchStream, saveTrackMeta, streamUrlFor } from "@/lib/stream"; import { AudioGraph, canSelectOutputDevice } from "@/lib/audio-graph"; import { usePlaybackStore, type QueueTrack } from "@/lib/store/playback"; import { eqGains, usePlaybackSettings } from "@/lib/store/playback-settings"; -import { usePremiumStore } from "@/lib/store/premium"; import { useSettingsStore } from "@/lib/store/settings"; -import { openPremiumGate } from "@/lib/store/premium-gate"; import { resolveStreamId, useTrackSourceStore } from "@/lib/store/track-source"; import { pickThumbnail } from "@/components/shared/thumbnail"; import { IS_MAC } from "@/lib/platform"; @@ -232,8 +230,7 @@ export function useAudioEngine() { if (!pendingRef.current && remaining <= cf + CROSSFADE_PRELOAD_LEAD_SEC) { const standby = standbyRef.current; - // Same gate as the resolve effect: no stream without Premium. - if (!standby || usePremiumStore.getState().status !== "premium") { + if (!standby) { return; } const p: PendingNext = { @@ -438,13 +435,6 @@ export function useAudioEngine() { videoId ? resolveStreamId(videoId, s.byVideoId) : undefined, ); - // Reactive Premium check for the gate below. Subscribing (rather than - // calling isPremium() inside the effect) makes the resolve effect - // re-run when the status lands after sign-in / the launch-time probe. - // Without this, a track gated during the "still checking" window would - // sit silent until the user re-picked it. - const premiumOk = usePremiumStore((s) => s.status === "premium"); - useEffect(() => { const el = audioRef.current; if (!el) return; @@ -484,25 +474,6 @@ export function useAudioEngine() { usePlaybackStore.getState().setStreamUrl(undefined); return; } - // Premium gate: signed-out / Free accounts browse but don't stream. - // Every entry path (track clicks, media keys, tray, floating window, - // restored queues) funnels through this effect, so one check here - // guarantees no yt-dlp spawn and no cache write happens without - // Premium. A deliberate play attempt (playing=true) gets the - // explainer dialog; the silent preload of a restored queue - // (playing=false) just parks the track. - if (!premiumOk) { - el.removeAttribute("src"); - el.load(); - const store = usePlaybackStore.getState(); - store.setStreamUrl(undefined); - store.setStatus("idle"); - if (store.playing) { - store.setPlaying(false); - openPremiumGate(); - } - return; - } // Drop the previous track's src immediately. Otherwise a paused→playing // transition committed together with the track change (playNow/goTo set // playing: true) makes the [playing] effect below re-play the OLD src @@ -582,25 +553,16 @@ export function useAudioEngine() { // videoId/streamVideoId alone wouldn't change. Repeating a *single* // track (repeat-one, or repeat-all on a 1-track queue) keeps the same // index, so the store replays it via pendingSeek instead — see - // `next()` in store/playback.ts. `premiumOk` so that gaining Premium - // (sign-in, status re-check) re-resolves a track the gate parked. - // `retryNonce` so the error handler can force a fresh stream-URL fetch - // for the current track after a transient failure without changing id. - }, [streamVideoId, videoId, index, premiumOk, retryNonce]); + // `next()` in store/playback.ts. `retryNonce` so the error handler + // can force a fresh stream-URL fetch for the current track after a + // transient failure without changing id. + }, [streamVideoId, videoId, index, retryNonce]); // Play / pause follow store. const playing = usePlaybackStore((s) => s.playing); useEffect(() => { const el = audioRef.current; if (!el) return; - if (playing && !premiumOk) { - // Resume attempts (play button, Space, SMTC play) on a gated track - // never reach the resolve effect (its deps don't include - // `playing`), so intercept them here. - usePlaybackStore.getState().setPlaying(false); - openPremiumGate(); - return; - } if (!el.src) return; if (playing) { graphRef.current?.resume(); @@ -614,7 +576,7 @@ export function useAudioEngine() { cancelFade(); el.pause(); } - }, [playing, premiumOk]); + }, [playing]); // Volume / mute follow store. const volume = usePlaybackStore((s) => s.volume);