Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 6 additions & 44 deletions src/lib/audio-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -614,7 +576,7 @@ export function useAudioEngine() {
cancelFade();
el.pause();
}
}, [playing, premiumOk]);
}, [playing]);

// Volume / mute follow store.
const volume = usePlaybackStore((s) => s.volume);
Expand Down