Add shared package: types, schemas, plans and API client

Holds everything web, mobile and API must agree on: DTOs, zod input
schemas, plan entitlements (the single place feature gates live),
streaming-service link helpers, feed ordering and a typed API client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-03 09:59:02 -04:00
co-authored by Claude Opus 5.5
parent 3627f29f73
commit 57c1733bab
9 changed files with 351 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
/** 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: 'Android',
};
/** 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 };
}