music on atproto
plyr.fm
1/**
2 * playback helper - guards queue operations with gated content checks.
3 *
4 * all playback actions should go through this module to prevent
5 * gated tracks from interrupting current playback.
6 */
7
8import { browser } from '$app/environment';
9import { queue } from './queue.svelte';
10import { toast } from './toast.svelte';
11import { API_URL, getAtprotofansSupportUrl } from './config';
12import type { Track } from './types';
13
14interface GatedCheckResult {
15 allowed: boolean;
16 requiresAuth?: boolean;
17 artistDid?: string;
18 artistHandle?: string;
19}
20
21/**
22 * check if a track can be played by the current user.
23 * returns immediately for non-gated tracks.
24 * for gated tracks, makes a HEAD request to verify access.
25 */
26async function checkAccess(track: Track): Promise<GatedCheckResult> {
27 // non-gated tracks are always allowed
28 if (!track.gated) {
29 return { allowed: true };
30 }
31
32 // gated track - check access via HEAD request
33 try {
34 const response = await fetch(`${API_URL}/audio/${track.file_id}`, {
35 method: 'HEAD',
36 credentials: 'include'
37 });
38
39 if (response.ok) {
40 return { allowed: true };
41 }
42
43 if (response.status === 401) {
44 return {
45 allowed: false,
46 requiresAuth: true,
47 artistDid: track.artist_did,
48 artistHandle: track.artist_handle
49 };
50 }
51
52 if (response.status === 402) {
53 return {
54 allowed: false,
55 requiresAuth: false,
56 artistDid: track.artist_did,
57 artistHandle: track.artist_handle
58 };
59 }
60
61 // unexpected status - allow and let Player handle any errors
62 return { allowed: true };
63 } catch {
64 // network error - allow and let Player handle any errors
65 return { allowed: true };
66 }
67}
68
69/**
70 * show appropriate toast for denied access (from HEAD request).
71 */
72function showDeniedToast(result: GatedCheckResult): void {
73 if (result.requiresAuth) {
74 toast.info('sign in to play supporter-only tracks');
75 } else if (result.artistDid) {
76 toast.info('this track is for supporters only', 5000, {
77 label: 'become a supporter',
78 href: getAtprotofansSupportUrl(result.artistDid)
79 });
80 } else {
81 toast.info('this track is for supporters only');
82 }
83}
84
85/**
86 * show toast for gated track (using server-resolved status).
87 */
88function showGatedToast(track: Track, isAuthenticated: boolean): void {
89 if (!isAuthenticated) {
90 toast.info('sign in to play supporter-only tracks');
91 } else if (track.artist_did) {
92 toast.info('this track is for supporters only', 5000, {
93 label: 'become a supporter',
94 href: getAtprotofansSupportUrl(track.artist_did)
95 });
96 } else {
97 toast.info('this track is for supporters only');
98 }
99}
100
101/**
102 * check if track is accessible using server-resolved gated status.
103 * shows toast if denied. no network call - instant feedback.
104 * use this for queue adds and other non-playback operations.
105 */
106export function guardGatedTrack(track: Track, isAuthenticated: boolean): boolean {
107 if (!track.gated) return true;
108 showGatedToast(track, isAuthenticated);
109 return false;
110}
111
112/**
113 * play a single track now.
114 * checks gated access before modifying queue state.
115 * shows toast if access denied - does NOT interrupt current playback.
116 */
117export async function playTrack(track: Track): Promise<boolean> {
118 if (!browser) return false;
119
120 const result = await checkAccess(track);
121 if (!result.allowed) {
122 showDeniedToast(result);
123 return false;
124 }
125
126 queue.playNow(track);
127 return true;
128}
129
130/**
131 * set the queue and optionally start playing at a specific index.
132 * checks gated access for the starting track before modifying queue state.
133 */
134export async function playQueue(tracks: Track[], startIndex = 0): Promise<boolean> {
135 if (!browser || tracks.length === 0) return false;
136
137 const startTrack = tracks[startIndex];
138 if (!startTrack) return false;
139
140 const result = await checkAccess(startTrack);
141 if (!result.allowed) {
142 showDeniedToast(result);
143 return false;
144 }
145
146 queue.setQueue(tracks, startIndex);
147 return true;
148}
149
150/**
151 * add tracks to queue and optionally start playing.
152 * if playNow is true, checks gated access for the first added track.
153 */
154export async function addToQueue(tracks: Track[], playNow = false): Promise<boolean> {
155 if (!browser || tracks.length === 0) return false;
156
157 if (playNow) {
158 const result = await checkAccess(tracks[0]);
159 if (!result.allowed) {
160 showDeniedToast(result);
161 return false;
162 }
163 }
164
165 queue.addTracks(tracks, playNow);
166 return true;
167}
168
169/**
170 * go to a specific index in the queue.
171 * checks gated access before changing position.
172 */
173export async function goToIndex(index: number): Promise<boolean> {
174 if (!browser) return false;
175
176 const track = queue.tracks[index];
177 if (!track) return false;
178
179 const result = await checkAccess(track);
180 if (!result.allowed) {
181 showDeniedToast(result);
182 return false;
183 }
184
185 queue.goTo(index);
186 return true;
187}