Add phone playback reporting and pause sharing to the API
Phones that read system media sessions report playback themselves:
- POST /me/devices issues a per-phone token that can only call
POST /devices/now-playing; revocable, hashed at rest, 10 per user
- Reports name the app ("via Spotify"); unknown apps use the OS label
- Push-reported playback expires after 9 minutes of silence, since
phones send a heartbeat every 4 minutes while playing
Users can pause sharing (PUT /me/sharing: 1h, 8h, indefinitely). While
paused, connections aren't polled, phone reports are dropped, nothing is
added to history, and the current track switches to "played … ago".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -50,3 +50,11 @@ export async function watchUsers(userIds: string[]) {
|
||||
);
|
||||
await pollSoon(rows.map((r) => r.id));
|
||||
}
|
||||
|
||||
export async function connectionIdsForUser(userId: string): Promise<string[]> {
|
||||
const rows = await db
|
||||
.select({ id: schema.connections.id })
|
||||
.from(schema.connections)
|
||||
.where(and(eq(schema.connections.userId, userId), eq(schema.connections.status, 'active')));
|
||||
return rows.map((r) => r.id);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ export const toUserSummary = (u: Pick<User, 'id' | 'username' | 'displayName' |
|
||||
avatarUrl: u.avatarUrl,
|
||||
});
|
||||
|
||||
/** Phones report changes and a heartbeat every few minutes; if they go quiet (app killed, no signal), stop showing "listening now". */
|
||||
export const DEVICE_STALE_MS = 9 * 60_000;
|
||||
const PUSH_SOURCES = new Set<NowPlayingProvider>(['android_media_session']);
|
||||
|
||||
export async function getNowPlaying(userIds: string[]): Promise<NowPlayingDTO[]> {
|
||||
if (userIds.length === 0) return [];
|
||||
const rows = await db
|
||||
@@ -26,8 +30,9 @@ export async function getNowPlaying(userIds: string[]): Promise<NowPlayingDTO[]>
|
||||
.map(({ np, user }) => ({
|
||||
user: toUserSummary(user),
|
||||
track: tracks.get(np.trackId)!,
|
||||
isPlaying: np.isPlaying,
|
||||
isPlaying: np.isPlaying && !(PUSH_SOURCES.has(np.source) && Date.now() - np.updatedAt.getTime() > DEVICE_STALE_MS),
|
||||
source: np.source,
|
||||
sourceApp: np.sourceApp,
|
||||
progressMs: np.progressMs,
|
||||
updatedAt: np.updatedAt.toISOString(),
|
||||
}))
|
||||
@@ -68,6 +73,19 @@ export async function getRecentPlays(userId: string, limit = 10): Promise<Recent
|
||||
return plays.map((p) => ({ track: tracks.get(p.trackId)!, source: p.source, playedAt: p.startedAt.toISOString() }));
|
||||
}
|
||||
|
||||
export const isSharingPaused = (user: Pick<User, 'sharingPausedUntil'>) =>
|
||||
Boolean(user.sharingPausedUntil && user.sharingPausedUntil > new Date());
|
||||
|
||||
/** Starting a pause: whatever was playing becomes "played … ago" for friends. */
|
||||
export async function pauseSharing(userId: string) {
|
||||
const updated = await db
|
||||
.update(schema.nowPlaying)
|
||||
.set({ isPlaying: false, updatedAt: new Date() })
|
||||
.where(and(eq(schema.nowPlaying.userId, userId), eq(schema.nowPlaying.isPlaying, true)))
|
||||
.returning({ userId: schema.nowPlaying.userId });
|
||||
if (updated.length) await publishNowPlaying(userId);
|
||||
}
|
||||
|
||||
export async function publishNowPlaying(userId: string) {
|
||||
const [dto] = await getNowPlaying([userId]);
|
||||
if (dto) await redis.publish(nowPlayingChannel(userId), JSON.stringify(dto));
|
||||
@@ -78,6 +96,7 @@ export async function applyPollResult(
|
||||
userId: string,
|
||||
source: NowPlayingProvider,
|
||||
result: PollResult | null,
|
||||
opts: { sourceApp?: string | null } = {},
|
||||
): Promise<boolean> {
|
||||
const [current] = await db.select().from(schema.nowPlaying).where(eq(schema.nowPlaying.userId, userId));
|
||||
const now = new Date();
|
||||
@@ -102,6 +121,7 @@ export async function applyPollResult(
|
||||
const values = {
|
||||
trackId: track.id,
|
||||
source,
|
||||
sourceApp: opts.sourceApp ?? null,
|
||||
isPlaying: true,
|
||||
progressMs: result.progressMs,
|
||||
updatedAt: now,
|
||||
|
||||
Reference in New Issue
Block a user