at main 4.8 kB view raw
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}