CI / check (push) Canceled after 0s
- Feed and profile cards show a progress bar with elapsed/total time, only when the source reports a real position (Plex, Jellyfin, the Android reader). It ticks locally between updates. Apple Music's guessed position is no longer reported. - The Android reader advances MediaSession's stale position to now, stamps when it was read (the server corrects for upload delay), and reports seeks. The server pushes an update to watchers when a position jumps. - When a polled source says where in the song someone is, the next poll is scheduled just after the song ends (never sooner than 5s), instead of waiting out the full interval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
66 lines
2.4 KiB
TypeScript
66 lines
2.4 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
/**
|
|
* Android music apps whose media sessions we share by default (package → display name).
|
|
* Anything else (video, podcasts, browsers) is ignored unless the user opts in, so
|
|
* friends don't see YouTube videos or audiobooks in their feed.
|
|
*/
|
|
export const ANDROID_MUSIC_APPS: Record<string, string> = {
|
|
'com.spotify.music': 'Spotify',
|
|
'com.google.android.apps.youtube.music': 'YouTube Music',
|
|
'com.apple.android.music': 'Apple Music',
|
|
'com.aspiro.tidal': 'TIDAL',
|
|
'deezer.android.app': 'Deezer',
|
|
'com.soundcloud.android': 'SoundCloud',
|
|
'com.amazon.mp3': 'Amazon Music',
|
|
'com.pandora.android': 'Pandora',
|
|
'com.qobuz.music': 'Qobuz',
|
|
'tv.plex.labs.plexamp': 'Plexamp',
|
|
'com.unicornsonlsd.finamp': 'Finamp',
|
|
'app.symfonium.android': 'Symfonium',
|
|
'com.bandcamp.android': 'Bandcamp',
|
|
'com.anghami': 'Anghami',
|
|
'com.jio.media.jiobeats': 'JioSaavn',
|
|
'com.maxmpz.audioplayer': 'Poweramp',
|
|
'org.videolan.vlc': 'VLC',
|
|
'com.kapp.youtube.final': 'YMusic',
|
|
'app.revanced.android.apps.youtube.music': 'YouTube Music',
|
|
};
|
|
|
|
export const registerDeviceSchema = z.object({
|
|
platform: z.enum(['android', 'ios']),
|
|
name: z.string().trim().min(1).max(100),
|
|
});
|
|
|
|
const reportedTrack = z.object({
|
|
title: z.string().trim().min(1).max(500),
|
|
artist: z.string().trim().max(500),
|
|
album: z.string().trim().max(500).nullish(),
|
|
durationMs: z.number().int().positive().max(24 * 3600_000).nullish(),
|
|
});
|
|
|
|
/** What a phone reports when playback changes in a music app. */
|
|
export const devicePlaybackSchema = z
|
|
.object({
|
|
/** Android package name (or iOS bundle id) of the playing app. */
|
|
app: z.string().trim().min(1).max(200),
|
|
/** The app's label as the OS reports it; used for apps we don't know. */
|
|
appName: z.string().trim().max(100).nullish(),
|
|
state: z.enum(['playing', 'paused', 'stopped']),
|
|
track: reportedTrack.nullish(),
|
|
positionMs: z.number().int().min(0).nullish(),
|
|
/** Phone's clock (epoch ms) when positionMs was read; lets the server account for upload delay. */
|
|
positionAt: z.number().int().positive().nullish(),
|
|
})
|
|
.refine((v) => v.state === 'stopped' || v.track, { message: 'track is required unless stopped', path: ['track'] });
|
|
|
|
export type DevicePlaybackInput = z.infer<typeof devicePlaybackSchema>;
|
|
|
|
export interface DeviceDTO {
|
|
id: string;
|
|
platform: 'android' | 'ios';
|
|
name: string;
|
|
lastSeenAt: string | null;
|
|
createdAt: string;
|
|
}
|