diff --git a/web-player/src/lib/server/mappers.test.ts b/web-player/src/lib/server/mappers.test.ts index b33e468..0c2a789 100644 --- a/web-player/src/lib/server/mappers.test.ts +++ b/web-player/src/lib/server/mappers.test.ts @@ -5,6 +5,8 @@ import { toCount, toDatedTrack, toLikedTrack, + toCoauthorInvitation, + toLibraryTrack, toListeningStats, toMix, toPage, @@ -25,7 +27,7 @@ const trackDto: components['schemas']['TrackApplicationResponse'] = { tags: [], isExplicit: true, isPublished: true, - coauthors: [] + coauthors: [{ id: 'u2', name: 'Guest' }] }; describe('toTrack', () => { @@ -37,7 +39,8 @@ describe('toTrack', () => { ownerUserId: '42', explicit: true, duration: 183.5, - listensCount: 12 + listensCount: 12, + coauthors: [{ id: 'u2', name: 'Guest' }] }); }); it('falls back to 0 for invalid numbers', () => { @@ -71,7 +74,8 @@ describe('toMix', () => { ownerUserId: null, explicit: false, duration: 60, - listensCount: 3 + listensCount: 3, + coauthors: [] } ]); }); @@ -168,3 +172,35 @@ describe('toListeningStats', () => { }); }); }); + +describe('toLibraryTrack', () => { + it('keeps the coauthor invites with their status', () => { + const track = toLibraryTrack({ + ...trackDto, + albums: [], + coauthorInvites: [ + { userId: 'u2', name: 'Guest', status: 'Accepted' }, + { userId: 'u3', name: 'Other', status: 'Pending' } + ] + }); + expect(track.coauthorInvites).toEqual([ + { userId: 'u2', name: 'Guest', status: 'Accepted' }, + { userId: 'u3', name: 'Other', status: 'Pending' } + ]); + expect(track.coauthors).toEqual([{ id: 'u2', name: 'Guest' }]); + }); +}); + +describe('toCoauthorInvitation', () => { + it('copies the invitation fields', () => { + const dto = { + id: 'i1', + trackId: 't1', + trackTitle: 'Song', + artist: null, + ownerUserId: '42', + invitedAt: '2026-01-02T00:00:00Z' + }; + expect(toCoauthorInvitation(dto)).toEqual(dto); + }); +}); diff --git a/web-player/src/lib/server/mappers.ts b/web-player/src/lib/server/mappers.ts index 0146fbb..69ed4fe 100644 --- a/web-player/src/lib/server/mappers.ts +++ b/web-player/src/lib/server/mappers.ts @@ -1,6 +1,8 @@ import type { components } from '$lib/api/schema'; import type { Album, + CoauthorInvitation, + CoauthorInvite, LibraryTrack, TrackStatusCounts, LikedTrack, @@ -13,6 +15,8 @@ import type { type TrackDto = components['schemas']['TrackApplicationResponse']; type UserTrackDto = components['schemas']['UserTrackApplicationResponse']; +type CoauthorInviteDto = components['schemas']['TrackCoauthorInviteReference']; +type CoauthorInvitationDto = components['schemas']['CoauthorInvitationResponse']; type StatusCountsDto = components['schemas']['TrackStatusCountsResponse']; type AlbumDto = components['schemas']['AlbumApplicationResponse']; type PlaylistDto = components['schemas']['PlayListApplicationResponse']; @@ -34,7 +38,8 @@ export function toTrack(dto: TrackDto): Track { ownerUserId: dto.ownerUserId, explicit: dto.isExplicit, duration: Number(dto.duration) || 0, - listensCount: Number(dto.listensCount) || 0 + listensCount: Number(dto.listensCount) || 0, + coauthors: dto.coauthors.map(({ id, name }) => ({ id, name })) }; } @@ -42,6 +47,21 @@ export function toDatedTrack(dto: TrackDto): Track & { date: string } { return { ...toTrack(dto), date: dto.createdAt }; } +export function toCoauthorInvite({ userId, name, status }: CoauthorInviteDto): CoauthorInvite { + return { userId, name, status }; +} + +export function toCoauthorInvitation(dto: CoauthorInvitationDto): CoauthorInvitation { + return { + id: dto.id, + trackId: dto.trackId, + trackTitle: dto.trackTitle, + artist: dto.artist, + ownerUserId: dto.ownerUserId, + invitedAt: dto.invitedAt + }; +} + export function toLibraryTrack(dto: UserTrackDto): LibraryTrack { return { ...toDatedTrack(dto), @@ -49,7 +69,8 @@ export function toLibraryTrack(dto: UserTrackDto): LibraryTrack { genres: dto.tags, albums: dto.albums, albumTitle: dto.albums.length > 0 ? dto.albums.map((album) => album.title).join(', ') : null, - published: dto.isPublished + published: dto.isPublished, + coauthorInvites: dto.coauthorInvites.map(toCoauthorInvite) }; } @@ -75,7 +96,8 @@ export function toMixTrack(item: MixItemDto): Track { ownerUserId: null, explicit: false, duration: Number(item.durationSeconds) || 0, - listensCount: Number(item.listensCount) || 0 + listensCount: Number(item.listensCount) || 0, + coauthors: [] }; } diff --git a/web-player/src/lib/types.ts b/web-player/src/lib/types.ts index df54b79..ffa136e 100644 --- a/web-player/src/lib/types.ts +++ b/web-player/src/lib/types.ts @@ -7,6 +7,28 @@ export interface SessionUser { export type FollowList = 'followers' | 'following'; +export interface TrackCoauthor { + id: string; + name: string; +} + +export type CoauthorStatus = 'Pending' | 'Accepted' | 'Rejected'; + +export interface CoauthorInvite { + userId: string; + name: string; + status: CoauthorStatus; +} + +export interface CoauthorInvitation { + id: string; + trackId: string; + trackTitle: string; + artist: string | null; + ownerUserId: string; + invitedAt: string; +} + export interface Track { id: string; title: string; @@ -15,6 +37,7 @@ export interface Track { explicit: boolean; duration: number; listensCount: number; + coauthors: TrackCoauthor[]; } export interface GenreOption { @@ -31,6 +54,7 @@ export interface LibraryTrack extends Track { albumTitle: string | null; albums: { id: string; title: string }[]; published: boolean; + coauthorInvites: CoauthorInvite[]; } export interface TrackStatusCounts {