Add Apple Music, Last.fm sign-in, and a fallback for Spotify's account cap

- Apple Music: sign MusicKit developer tokens (ES256 via node:crypto),
  accept the Music User Token from the iOS app, and poll recently played.
  Apple has no now-playing API or play timestamps, so a new top song is
  shown as playing for its length; connecting records a baseline so an
  old song isn't announced as current
- Last.fm web auth ("Sign in with Last.fm") instead of typing a
  username; session key stored encrypted
- Spotify accounts rejected by the 5-user development cap are sent back
  with error=spotify_limited instead of a 500, so apps can offer
  Spotify via Last.fm

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-04 07:12:19 -04:00
co-authored by Claude Opus 5.5
parent 0b7907146d
commit ca97f1c3ac
11 changed files with 545 additions and 6 deletions
+7
View File
@@ -23,7 +23,13 @@ const schema = z.object({
SPOTIFY_CLIENT_ID: optional,
SPOTIFY_CLIENT_SECRET: optional,
LASTFM_API_KEY: optional,
/** Enables "Sign in with Last.fm" (web auth). Without it, users type their username. */
LASTFM_SHARED_SECRET: optional,
ODESLI_API_KEY: optional,
/** Apple Music (MusicKit): Team ID, MusicKit key ID, and the key's .p8 contents (newlines may be written as \n). */
APPLE_TEAM_ID: optional,
APPLE_MUSICKIT_KEY_ID: optional,
APPLE_MUSICKIT_PRIVATE_KEY: optional,
/** Identifies this app to Plex. Keep it stable: changing it invalidates users' Plex authorizations. */
PLEX_CLIENT_ID: z.string().default('musicshare-dev'),
/**
@@ -40,3 +46,4 @@ const schema = z.object({
export const env = schema.parse(process.env);
export const spotifyConfigured = Boolean(env.SPOTIFY_CLIENT_ID && env.SPOTIFY_CLIENT_SECRET);
export const appleMusicConfigured = Boolean(env.APPLE_TEAM_ID && env.APPLE_MUSICKIT_KEY_ID && env.APPLE_MUSICKIT_PRIVATE_KEY);