Files
musicshare/packages/shared/src/dto.ts
T
chrisandClaude Opus 5.5 85e1643c29
CI / check (push) Canceled after 0s
Email confirmation, change email/password, show/hide passwords
- Sign-up emails a confirmation link (24h); unconfirmed accounts see a
  banner on the web with "send it again", and status in Settings.
- Change email (needs the password): the new address gets a confirmation
  link and only replaces the old one once clicked; the old address gets a
  heads-up with a reset-password link. Pending changes can be cancelled.
- Change password (needs the current one): signs out other sessions.
- Password fields have Show/Hide on web and mobile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
2026-10-06 13:05:26 -04:00

151 lines
4.3 KiB
TypeScript

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<Record<StreamingService, string>>;
}
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;
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;
}
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;
}
/** 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<T extends FeedItemDTO>(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;
/** 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<Record<'lastfm' | 'listenbrainz', string>>;
}
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 };