import type { Entitlements, Plan } from './plans'; import type { NotificationPrefs } from './notifications'; 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; emailVerified: boolean; /** A new address waiting to be confirmed (the link was sent there). */ pendingEmail: string | null; bio: string | null; preferredService: StreamingService | null; /** The services they use; null until chosen, meaning all of them. */ services: StreamingService[] | null; notifyPrefs: NotificationPrefs; role: 'user' | 'admin'; /** When onboarding was finished; null means show it. */ onboardedAt: string | null; /** ISO time sharing resumes, or null when sharing. Far future means "until turned back on". */ sharingPausedUntil: string | null; plan: Plan; entitlements: Entitlements; } /** One emoji on a play, and who left it (newest first). */ export interface ReactionDTO { emoji: string; count: number; users: UserSummaryDTO[]; } export interface NowPlayingDTO { user: UserSummaryDTO; track: TrackDTO; isPlaying: boolean; source: NowPlayingProvider; /** The app it's playing in, when a phone reported it (e.g. "Spotify"). */ sourceApp: string | null; /** Link to the source for attribution, e.g. the user's Last.fm profile (required by Last.fm's terms). */ sourceUrl: string | null; progressMs: number | null; updatedAt: string; /** The play this is, for reactions (null for very old rows). */ listenId: string | null; reactions: ReactionDTO[]; } /** 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 { listenId: string; track: TrackDTO; source: NowPlayingProvider; playedAt: string; reactions: ReactionDTO[]; } export interface ProfileDTO { user: UserSummaryDTO & { bio: string | null }; followerCount: number; followingCount: number; isFollowing: boolean; isPinned: boolean; isSelf: boolean; /** They follow the viewer. */ followsYou: boolean; /** The viewer has blocked them (activity is hidden; they can be unblocked). */ isBlocked: boolean; featured: FeaturedSongDTO | null; nowPlaying: NowPlayingDTO | null; /** Most recent first, not including the track playing right now. */ recent: RecentPlayDTO[]; /** Profile links for sources whose data appears here (Last.fm requires attribution). */ attribution: Partial>; } export interface AdminReportDTO { id: string; reason: string; note: string | null; status: 'open' | 'resolved'; createdAt: string; reporter: UserSummaryDTO | null; reported: UserSummaryDTO & { suspended: boolean }; } export interface AdminStatsDTO { users: number; listeningNow: number; openReports: number; suspended: number; } /** A row in a followers/following list. */ export interface FollowListItemDTO extends UserSummaryDTO { /** Whether the viewer follows this person (for Follow/Following buttons). */ isFollowing: boolean; } export interface FollowListDTO { users: FollowListItemDTO[]; /** Pass back as ?cursor= for the next page; null when there are no more. */ nextCursor: string | null; } export interface ConnectionDTO { provider: NowPlayingProvider; externalUserId: string; displayName: string | null; status: 'active' | 'error'; lastError: string | null; lastPolledAt: string | null; /** Last time this connection saw something playing. */ lastActivityAt: 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 } /** Something new in the viewer's notifications. */ | { type: 'notification'; data: { unread: number } };