[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(storybook): More accurate play list api mocking

* Reusable function for generic behavior
* Append to list and create unique plays on every page request

FoxxMD (Jul 2, 2026, 9:32 PM UTC) bf7fb4f5 32b64ff3

+98 -123
+2 -2
src/core/tests/utils/apiFixtures.ts
··· 297 297 return {data: messages.map((x, index) => ({line: x, time: time + index, levelLabel: 'debug', level: 'debug'})) } 298 298 } 299 299 300 - export const generatePlayApiCommonDetailedList = async (opts: {endDate?: Dayjs} = {}) => { 301 - const queued = normalizePlays(generateArray(7, () => generatePlayWithLifecycle()), { endDate: opts.endDate ?? dayjs() }).map(x => { 300 + export const generatePlayApiCommonDetailedList = async (opts: {endDate?: Dayjs, initialDate?: Dayjs} = {}) => { 301 + const queued = normalizePlays(generateArray(7, () => generatePlayWithLifecycle()), { endDate: opts.initialDate === undefined ? opts.endDate ?? dayjs() : undefined, initialDate: opts.initialDate }).map(x => { 302 302 const jsonPlay = asJsonPlayObject(x); 303 303 return generatePlayApiCommonDetailed({ playOpts: [{ state: 'queued', play: jsonPlay }], inputOpts: [{ play: jsonPlay }] }) 304 304 });
+96 -121
src/stories/plays/ActivityList.stories.tsx
··· 1 1 import '../../client/wdyr.js'; 2 2 import preview from "../../../.storybook/preview.js"; 3 3 import React from 'react'; 4 - import { http, HttpResponse, delay, sse } from 'msw'; 4 + import { http, HttpResponse, delay, sse, StrictRequest, DefaultBodyType, JsonBodyType } from 'msw'; 5 5 6 6 import { fn } from 'storybook/test'; 7 7 import { Container } from '@chakra-ui/react'; ··· 45 45 }); 46 46 47 47 let playData: PlayApiCommonDetailed[] = []; 48 - let livePlayData: PlayApiCommonDetailed[] = []; 48 + 49 + 50 + const handlePlayListRequest = async ({ request, list }: {request: StrictRequest<DefaultBodyType>, list: PlayApiCommonDetailed[]}): Promise<[HttpResponse<JsonBodyType>, PlayApiCommonDetailed[]]> => { 51 + const url = new URL(request.url) 52 + console.log(url.search); 53 + const query = qs.parse(url.search, qsOptions); 54 + console.log(query); 55 + let responseData: PlayApiCommonDetailed[] = []; 56 + if(list.length === 0) { 57 + list = await generatePlayApiCommonDetailedList(); 58 + responseData = list; 59 + } else { 60 + const startDate = dayjs(list[Math.min(Number.parseInt(query.offset as string) - 1, list.length - 1)].playedAt); 61 + const moreData = await generatePlayApiCommonDetailedList({endDate: startDate}); 62 + list = list.concat(moreData); 63 + responseData = moreData; 64 + } 65 + const res: PaginatedResponse<PlayApiCommonDetailed> = { 66 + data: responseData, 67 + meta: { 68 + offset: Number.parseInt(query.offset as string), 69 + limit: responseData.length, 70 + total: list.length, 71 + } 72 + } 73 + return [HttpResponse.json(res), list]; 74 + } 75 + 76 + type PlayRequestMock = Parameters<Parameters<typeof http.get<{ uid: string }>>[1]>[0]; 77 + 78 + const handlePlayRequest = async ({params, request}: PlayRequestMock, list: PlayApiCommonDetailed[]): Promise<[HttpResponse<JsonBodyType>, PlayApiCommonDetailed[]]> => { 79 + if(list.length === 0) { 80 + list = await generatePlayApiCommonDetailedList(); 81 + } 82 + const existing = list.find(x => x.uid === params.uid); 83 + if (existing !== undefined) { 84 + return [HttpResponse.json(existing), list]; 85 + } 86 + return [HttpResponse.json(generatePlayApiCommonDetailed()), list]; 87 + } 49 88 89 + let listLiveData: PlayApiCommonDetailed[] = []; 50 90 export const ListLive = meta.story({ 51 91 component: ListContainerFetchable, 52 92 render: function Render(args, { loaded: { data } }) { return (<ListContainerFetchable {...args}/>) }, ··· 58 98 parameters: { 59 99 msw: { 60 100 handlers: [ 61 - http.get<{ uid: string }>('/api/components/:componentId/plays', async ({ params }) => { 62 - if(livePlayData.length === 0) { 63 - livePlayData = await generatePlayApiCommonDetailedList(); 64 - } 65 - const res: PaginatedResponse<PlayApiCommonDetailed> = { 66 - data: livePlayData, 67 - meta: { 68 - offset: 0, 69 - limit: livePlayData.length 70 - } 71 - } 72 - return HttpResponse.json(res); 101 + http.get<{ uid: string }>('/api/components/:componentId/plays', async ({ params, request }) => { 102 + const [res, newList] = await handlePlayListRequest({request, list: listLiveData}); 103 + await delay(1000); 104 + listLiveData = newList; 105 + return res; 73 106 }), 74 - http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async ({ params }) => { 75 - if(livePlayData.length === 0) { 76 - livePlayData = await generatePlayApiCommonDetailedList(); 77 - } 78 - const existing = livePlayData.find(x => x.uid === params.uid); 79 - if (existing !== undefined) { 80 - return HttpResponse.json(existing); 81 - } 82 - return HttpResponse.json(generatePlayApiCommonDetailed()); 107 + http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async (info) => { 108 + const [res, newList] = await handlePlayRequest(info, listLiveData); 109 + listLiveData = newList; 110 + await delay(1000); 111 + return res; 83 112 }), 84 113 ], 85 114 }, ··· 94 123 ] 95 124 }); 96 125 126 + 127 + let livePlayData: PlayApiCommonDetailed[] = []; 97 128 export const ListLiveFilterable = meta.story({ 98 129 component: ListContainerFilterable, 99 130 render: function Render(args, { loaded: { data } }) { return (<ListContainerFilterable {...args}/>) }, ··· 106 137 msw: { 107 138 handlers: [ 108 139 http.get<{ uid: string }>('/api/components/:componentId/plays', async ({ params, request }) => { 109 - if(livePlayData.length === 0) { 110 - livePlayData = await generatePlayApiCommonDetailedList(); 111 - } 112 - const url = new URL(request.url) 113 - console.log(url.search); 114 - const query = qs.parse(url.search, qsOptions); 115 - console.log(query); 116 - const res: PaginatedResponse<PlayApiCommonDetailed> = { 117 - data: livePlayData, 118 - meta: { 119 - offset: 0, 120 - limit: livePlayData.length, 121 - total: livePlayData.length, 122 - } 123 - } 140 + const [res, newList] = await handlePlayListRequest({request, list: livePlayData}); 124 141 await delay(1000); 125 - return HttpResponse.json(res); 142 + livePlayData = newList; 143 + return res; 126 144 }), 127 - http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async ({ params }) => { 128 - if(livePlayData.length === 0) { 129 - livePlayData = await generatePlayApiCommonDetailedList(); 130 - } 131 - const existing = livePlayData.find(x => x.uid === params.uid); 132 - if (existing !== undefined) { 133 - return HttpResponse.json(existing); 134 - } 135 - return HttpResponse.json(generatePlayApiCommonDetailed()); 145 + http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async (info) => { 146 + const [res, newList] = await handlePlayRequest(info, livePlayData); 147 + livePlayData = newList; 148 + await delay(1000); 149 + return res; 136 150 }), 137 151 ], 138 152 }, ··· 163 177 return HttpResponse.json({data: [], meta: {offset: 0, limit: 100}}); 164 178 }), 165 179 http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async ({ params }) => { 166 - if(livePlayData.length === 0) { 167 - livePlayData = await generatePlayApiCommonDetailedList(); 168 - } 169 - const existing = livePlayData.find(x => x.uid === params.uid); 170 - if (existing !== undefined) { 171 - return HttpResponse.json(existing); 172 - } 173 - return HttpResponse.json(generatePlayApiCommonDetailed()); 180 + return new HttpResponse('', {status: 404}); 174 181 }), 175 182 ], 176 183 }, 177 184 } 178 185 }); 179 186 187 + let liveNoMorePlayData: PlayApiCommonDetailed[] = []; 180 188 export const ListLiveNoMorePlay = meta.story({ 181 189 component: ListContainerFilterable, 182 190 render: function Render(args) { return (<ListContainerFilterable {...args}/>) }, ··· 200 208 return HttpResponse.json({ 201 209 data: [], 202 210 meta: { 203 - limit: livePlayData.length, 211 + limit: liveNoMorePlayData.length, 204 212 offset: query.offset 205 213 } 206 214 }); 207 215 } 208 - if(livePlayData.length === 0) { 209 - livePlayData = await generatePlayApiCommonDetailedList(); 216 + if(liveNoMorePlayData.length === 0) { 217 + liveNoMorePlayData = await generatePlayApiCommonDetailedList(); 210 218 } 211 219 const res: PaginatedResponse<PlayApiCommonDetailed> = { 212 - data: livePlayData, 220 + data: liveNoMorePlayData, 213 221 meta: { 214 222 offset: 0, 215 223 limit: livePlayData.length ··· 217 225 } 218 226 return HttpResponse.json(res); 219 227 }), 220 - http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async ({ params }) => { 221 - if(livePlayData.length === 0) { 222 - livePlayData = await generatePlayApiCommonDetailedList(); 223 - } 224 - const existing = livePlayData.find(x => x.uid === params.uid); 225 - if (existing !== undefined) { 226 - return HttpResponse.json(existing); 227 - } 228 - return HttpResponse.json(generatePlayApiCommonDetailed()); 228 + http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async (info) => { 229 + const [res, newList] = await handlePlayRequest(info, liveNoMorePlayData); 230 + liveNoMorePlayData = newList; 231 + await delay(1000); 232 + return res; 229 233 }), 230 234 ], 231 235 }, 232 236 }, 233 237 }); 234 238 239 + let liveUpdateData: PlayApiCommonDetailed[] = []; 235 240 export const ListLiveUpdates = meta.story({ 236 241 component: ListContainerFilterable, 237 242 render: function Render(args, { loaded: { data } }) { return (<ListContainerFilterable {...args}/>) }, ··· 244 249 msw: { 245 250 handlers: [ 246 251 http.get<{ uid: string }>('/api/components/:componentId/plays', async ({ params, request }) => { 247 - livePlayData = await generatePlayApiCommonDetailedList(); 248 - const url = new URL(request.url) 249 - console.log(url.search); 250 - const query = qs.parse(url.search, qsOptions); 251 - console.log(query); 252 - const res: PaginatedResponse<PlayApiCommonDetailed> = { 253 - data: livePlayData, 254 - meta: { 255 - offset: 0, 256 - limit: livePlayData.length, 257 - total: livePlayData.length, 258 - } 259 - } 252 + const [res, newList] = await handlePlayListRequest({request, list: liveUpdateData}); 260 253 await delay(1000); 261 - return HttpResponse.json(res); 254 + liveUpdateData = newList; 255 + return res; 262 256 }), 263 - http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async ({ params }) => { 264 - if(livePlayData.length === 0) { 265 - livePlayData = await generatePlayApiCommonDetailedList(); 266 - } 267 - const existingIndex = livePlayData.findIndex(x => x.uid === params.uid); 268 - if (existingIndex !== -1) { 269 - const existing = livePlayData[existingIndex]; 270 - return HttpResponse.json(existing); 271 - } 272 - return HttpResponse.json(generatePlayApiCommonDetailed()); 257 + http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async (info) => { 258 + const [res, newList] = await handlePlayRequest(info, liveUpdateData); 259 + liveUpdateData = newList; 260 + await delay(1000); 261 + return res; 273 262 }), 274 263 sse('/api/events?next=true', async ({ params, client }) => { 275 264 setInterval(() => { 276 265 277 266 const index = faker.number.int({min: 0, max: 7}); 278 - let newState: PlayState = livePlayData[index].state; 279 - while(newState === livePlayData[index].state) { 267 + let newState: PlayState = liveUpdateData[index].state; 268 + while(newState === liveUpdateData[index].state) { 280 269 newState = randomPlayState(); 281 270 } 282 - livePlayData[index].state = newState; 283 - livePlayData[index].play.data.track = faker.music.songName(); 284 - livePlayData[index].updatedAt = dayjs().toISOString(); 271 + liveUpdateData[index].state = newState; 272 + liveUpdateData[index].play.data.track = faker.music.songName(); 273 + liveUpdateData[index].updatedAt = dayjs().toISOString(); 285 274 286 275 client.send({ 287 276 //@ts-expect-error 288 277 event: 'playUpdate', 289 - data: {componentId: 1, data: {uid: livePlayData[index].uid}}}) 278 + data: {componentId: 1, data: {uid: liveUpdateData[index].uid}}}) 290 279 }, 2000); 291 280 }) 292 281 ], ··· 315 304 msw: { 316 305 handlers: [ 317 306 http.get<{ uid: string }>('/api/components/:componentId/plays', async ({ params, request }) => { 318 - const url = new URL(request.url) 319 - console.log(url.search); 320 - const query = qs.parse(url.search, qsOptions) as QueryPlaysOptsJson; 321 - if(livePlayInsertData.length === 0) { 322 - livePlayInsertData = await generatePlayApiCommonDetailedList({endDate: dayjs((query.playedAt as CompareDateBetween<string>)?.range[1])}); 323 - } 324 - console.log(query); 325 - const res: PaginatedResponse<PlayApiCommonDetailed> = { 326 - data: livePlayInsertData, 327 - meta: { 328 - offset: 0, 329 - limit: livePlayInsertData.length, 330 - total: livePlayInsertData.length, 331 - } 332 - } 307 + const [res, newList] = await handlePlayListRequest({request, list: livePlayInsertData}); 333 308 await delay(1000); 334 - return HttpResponse.json(res); 309 + livePlayInsertData = newList; 310 + return res; 335 311 }), 336 - http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async ({ params }) => { 337 - const existing = livePlayInsertData.findIndex(x => x.uid === params.uid); 338 - if (existing !== undefined) { 339 - return HttpResponse.json(livePlayInsertData[existing]); 340 - } 341 - return HttpResponse.json(generatePlayApiCommonDetailed()); 312 + http.get<{ uid: string }>('/api/components/:componentId/plays/:uid', async (info) => { 313 + const [res, newList] = await handlePlayRequest(info, livePlayInsertData); 314 + livePlayInsertData = newList; 315 + await delay(1000); 316 + return res; 342 317 }), 343 318 sse('/api/events?next=true', async ({ params, client }) => { 344 319 setInterval(() => {