import { useEffect } from 'react'; import { AppState } from 'react-native'; import AppleMusic, { type MusicAppPlayback } from '@/modules/apple-music'; import { api } from './api'; import { reportPlayback } from './device'; /** True on iOS builds that include the native module (not Android, web, or Expo Go). */ export const appleMusicAvailable = AppleMusic !== null; export class AppleMusicPermissionError extends Error {} /** * The whole Apple Music setup: one system prompt. Asks for access, exchanges * our developer token for the user's Music User Token, and hands it to the * server, which then follows recently played in the background. */ export async function connectAppleMusic() { if (!AppleMusic) throw new Error('Apple Music is only available in the iPhone app'); let status = AppleMusic.authorizationStatus(); if (status === 'notDetermined') status = await AppleMusic.requestAuthorization(); if (status !== 'authorized') { throw new AppleMusicPermissionError( status === 'restricted' ? 'Apple Music access is restricted on this iPhone (Screen Time or a device profile).' : 'Turn on Media & Apple Music for musicshare in the Settings app, then try again.', ); } const { token } = await api.appleMusicDeveloperToken(); const musicUserToken = await AppleMusic.getUserToken(token); return api.connectAppleMusic(musicUserToken); } const HEARTBEAT_MS = 4 * 60_000; // the server expires silent phones after 9 minutes let lastSent: { key: string; at: number } | null = null; function report(playback: MusicAppPlayback) { const key = 'title' in playback ? `${playback.state}|${playback.title}|${playback.artist}` : 'stopped'; const now = Date.now(); if (lastSent?.key === key && (playback.state !== 'playing' || now - lastSent.at < HEARTBEAT_MS)) return; if (!lastSent && key === 'stopped') return; // nothing was shared yet; no need to say "stopped" lastSent = { key, at: now }; reportPlayback( 'title' in playback && playback.state !== 'stopped' ? { app: 'com.apple.Music', appName: 'Apple Music', state: playback.state, track: { title: playback.title, artist: playback.artist, album: playback.album || null, durationMs: playback.durationMs > 0 ? Math.round(playback.durationMs) : null, }, } : { app: 'com.apple.Music', appName: 'Apple Music', state: 'stopped' }, ).catch(() => { lastSent = null; // retry on the next change }); } /** * While the app is open, mirror the Music app exactly. iOS doesn't let us watch * it in the background; then the server's recently-played polling takes over. */ export function useAppleMusicLive(enabled: boolean) { useEffect(() => { if (!enabled || !AppleMusic || AppleMusic.authorizationStatus() !== 'authorized') return; const music = AppleMusic; const check = () => music.getNowPlaying().then(report).catch(() => {}); check(); const changes = music.addListener('onNowPlayingChange', report); const foreground = AppState.addEventListener('change', (s) => s === 'active' && check()); const heartbeat = setInterval(check, HEARTBEAT_MS); return () => { changes.remove(); foreground.remove(); clearInterval(heartbeat); }; }, [enabled]); }