Files
musicshare/packages/shared/src/services.ts
T
chrisandClaude Opus 5.5 804493aee0 Create-account links for Last.fm and ListenBrainz, and Link Spotify for ListenBrainz
ListenBrainz accounts are MusicBrainz accounts, so its sign-up link goes to MusicBrainz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:02:40 -04:00

132 lines
4.4 KiB
TypeScript

/** 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<StreamingService | NowPlayingProvider, string> = {
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<StreamingService | NowPlayingProvider, string> = {
spotify: '#1DB954',
apple_music: '#FA2D48',
youtube_music: '#FF0033',
deezer: '#A238FF',
tidal: '#000000',
lastfm: '#D51007',
listenbrainz: '#EB743B',
plex: '#E5A00D',
jellyfin: '#AA5CC3',
android_media_session: '#5b4cf0',
};
/**
* The services to offer for a song, main one first: the ones the viewer uses,
* or all of them if they haven't said.
*/
export function viewerServices(
viewer: { preferredService: StreamingService | null; services: StreamingService[] | null } | null | undefined,
): StreamingService[] {
const chosen = viewer?.services?.length ? viewer.services : STREAMING_SERVICES;
const main = viewer?.preferredService;
return main ? [main, ...chosen.filter((s) => s !== main)] : [...chosen];
}
/** 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<Record<StreamingService, string>> },
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;
/**
* Where to make a free account (neither lets other apps create one). ListenBrainz accounts are
* MusicBrainz accounts: register there, then sign in to ListenBrainz once to start it.
*/
export const SIGNUP_PAGES = {
lastfm: 'https://www.last.fm/join',
listenbrainz: 'https://musicbrainz.org/register',
} 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
);
}