import type { Entitlements, Plan } from './plans'; import type { NowPlayingProvider, StreamingService } from './services'; export interface TrackDTO { id: string; title: string; artist: string; album: string | null; artworkUrl: string | null; durationMs: number | null; isrc: string | null; links: Partial>; } export interface UserSummaryDTO { id: string; username: string; displayName: string | null; avatarUrl: string | null; } export interface MeDTO extends UserSummaryDTO { email: string; bio: string | null; preferredService: StreamingService | null; plan: Plan; entitlements: Entitlements; } export interface NowPlayingDTO { user: UserSummaryDTO; track: TrackDTO; isPlaying: boolean; source: NowPlayingProvider; progressMs: number | null; updatedAt: string; } /** A feed entry. pinnedAt is set when the viewer pinned this friend. */ export interface FeedItemDTO extends NowPlayingDTO { pinnedAt: string | null; } /** Pinned first (oldest pin on top, so order is stable), then live listeners, then most recent. */ export function sortFeed(items: T[]): T[] { return [...items].sort( (a, b) => Number(Boolean(b.pinnedAt)) - Number(Boolean(a.pinnedAt)) || (a.pinnedAt && b.pinnedAt ? a.pinnedAt.localeCompare(b.pinnedAt) : 0) || Number(b.isPlaying) - Number(a.isPlaying) || b.updatedAt.localeCompare(a.updatedAt), ); } export interface FeaturedSongDTO { track: TrackDTO; comment: string | null; updatedAt: string; } export interface RecentPlayDTO { track: TrackDTO; source: NowPlayingProvider; playedAt: string; } export interface ProfileDTO { user: UserSummaryDTO & { bio: string | null }; followerCount: number; followingCount: number; isFollowing: boolean; isPinned: boolean; isSelf: boolean; featured: FeaturedSongDTO | null; nowPlaying: NowPlayingDTO | null; /** Most recent first, not including the track playing right now. */ recent: RecentPlayDTO[]; } export interface ConnectionDTO { provider: NowPlayingProvider; externalUserId: string; displayName: string | null; status: 'active' | 'error'; lastError: string | null; lastPolledAt: string | null; /** Secret URL the user can paste into Plex/Jellyfin for instant updates. */ webhookUrl: string | null; } export interface TrackSearchResultDTO { deezerId: string; title: string; artist: string; album: string | null; artworkUrl: string | null; durationMs: number | null; } /** Messages pushed over the realtime websocket. */ export type RealtimeMessage = { type: 'now_playing'; data: NowPlayingDTO };