/** Services a listener can open a song in. */ export const STREAMING_SERVICES = [ 'spotify', 'apple_music', 'youtube_music', 'deezer', 'tidal', ] as const; export type StreamingService = (typeof STREAMING_SERVICES)[number]; /** Sources we can read "now playing" from. */ export const NOW_PLAYING_PROVIDERS = [ 'spotify', 'lastfm', 'listenbrainz', 'plex', 'jellyfin', 'apple_music', 'android_media_session', ] as const; export type NowPlayingProvider = (typeof NOW_PLAYING_PROVIDERS)[number]; export const SERVICE_LABELS: Record = { spotify: 'Spotify', apple_music: 'Apple Music', youtube_music: 'YouTube Music', deezer: 'Deezer', tidal: 'TIDAL', lastfm: 'Last.fm', listenbrainz: 'ListenBrainz', plex: 'Plex', jellyfin: 'Jellyfin', android_media_session: 'your phone', }; /** Brand colours, for the badge shown next to a service's name. */ export const SERVICE_COLORS: Record = { spotify: '#1DB954', apple_music: '#FA2D48', youtube_music: '#FF0033', deezer: '#A238FF', tidal: '#000000', lastfm: '#D51007', listenbrainz: '#EB743B', plex: '#E5A00D', jellyfin: '#AA5CC3', android_media_session: '#5b4cf0', }; /** Fallback when we have no direct link: a search on that service. */ export function searchUrl(service: StreamingService, title: string, artist: string): string { const q = encodeURIComponent(`${artist} ${title}`); switch (service) { case 'spotify': return `https://open.spotify.com/search/${q}`; case 'apple_music': return `https://music.apple.com/search?term=${q}`; case 'youtube_music': return `https://music.youtube.com/search?q=${q}`; case 'deezer': return `https://www.deezer.com/search/${q}`; case 'tidal': return `https://listen.tidal.com/search?q=${q}`; } } /** * The URL to open a track in the given service. All direct links are https * universal/app links, so they open the native app when installed. */ export function openUrl( track: { title: string; artist: string; links: Partial> }, service: StreamingService, ): { url: string; exact: boolean } { const direct = track.links[service]; if (direct) return { url: direct, exact: true }; return { url: searchUrl(service, track.title, track.artist), exact: false }; } /** Public profile page for scrobbling services (attribution links). */ export function sourceProfileUrl(provider: string, username: string): string | null { const u = encodeURIComponent(username); if (provider === 'lastfm') return `https://www.last.fm/user/${u}`; if (provider === 'listenbrainz') return `https://listenbrainz.org/user/${u}/`; return null; } /** Where users re-link Spotify on each scrobbling service. */ export const SPOTIFY_LINK_PAGES = { lastfm: 'https://www.last.fm/settings/applications', listenbrainz: 'https://listenbrainz.org/settings/music-services/details/', } as const; const QUIET_AFTER_MS = 7 * 86_400_000; /** * A scrobbler that used to report plays but has gone silent for a week. Usually * Spotify's link to it expired (Spotify makes users re-approve every 6 months). */ export function isQuietScrobbler( c: { provider: string; status: string; lastActivityAt: string | null }, now = Date.now(), ): c is { provider: 'lastfm' | 'listenbrainz'; status: string; lastActivityAt: string } { return ( (c.provider === 'lastfm' || c.provider === 'listenbrainz') && c.status === 'active' && c.lastActivityAt !== null && now - new Date(c.lastActivityAt).getTime() > QUIET_AFTER_MS ); }