Skip to content

Commit a115b90

Browse files
committed
Preview a track as its whole album, starting on that track
Feed stories like "x bought a track" now show the album the track belongs to, with the purchased track loaded. Standalone singles still show on their own.
1 parent 42b0a0b commit a115b90

3 files changed

Lines changed: 81 additions & 8 deletions

File tree

‎src/bclient.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export interface TralbumDetailsResponse {
6262
price: number;
6363
is_purchasable: boolean;
6464
bandcamp_url?: string;
65+
album_id?: number | null;
6566
tracks?: TralbumTrack[];
6667
}
6768

‎src/components/player/loader.ts‎

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -232,12 +232,7 @@ export async function loadAlbumIntoDrawer(
232232
fetchCaching = enableFetchCaching;
233233

234234
try {
235-
const tralbumDetails = await getTralbumDetails(
236-
albumId,
237-
convertToApiType(albumType),
238-
null,
239-
createFetchFunction(enableFetchCaching)
240-
);
235+
const { tralbumDetails, startTrackId } = await fetchTralbumForDrawer(albumId, albumType, enableFetchCaching);
241236

242237
if (requestedAlbumId !== albumId) {
243238
log.debug(`Album ${albumId} arrived after the drawer moved on`);
@@ -273,8 +268,8 @@ export async function loadAlbumIntoDrawer(
273268
}
274269

275270
attachTrackListHandlers();
276-
loadTrack(0);
277-
void loadTrackState(albumId);
271+
loadTrack(startTrackIndex(tralbumDetails, startTrackId));
272+
void loadTrackState(String(tralbumDetails.id));
278273

279274
log.info(`Album loaded: ${tralbumDetails.title} by ${tralbumDetails.tralbum_artist}`);
280275
} catch (error) {
@@ -283,6 +278,32 @@ export async function loadAlbumIntoDrawer(
283278
}
284279
}
285280

281+
// A track that belongs to an album is shown as its whole album, starting on that track.
282+
async function fetchTralbumForDrawer(
283+
id: string,
284+
type: string,
285+
enableFetchCaching: boolean
286+
): Promise<{ tralbumDetails: TralbumDetailsResponse; startTrackId?: number }> {
287+
const fetchFn = createFetchFunction(enableFetchCaching);
288+
const details = await getTralbumDetails(id, convertToApiType(type), null, fetchFn);
289+
if (details.type !== 't' || !details.album_id) return { tralbumDetails: details };
290+
291+
try {
292+
const album = await getTralbumDetails(details.album_id, 'a', null, fetchFn);
293+
return { tralbumDetails: album, startTrackId: details.id };
294+
} catch (error) {
295+
log.warn(`Failed to load album ${details.album_id} for track ${id}, showing the track alone: ${error}`);
296+
return { tralbumDetails: details };
297+
}
298+
}
299+
300+
function startTrackIndex(tralbumDetails: TralbumDetailsResponse, trackId?: number): number {
301+
if (!trackId) return 0;
302+
303+
const index = tralbumDetails.tracks?.findIndex(track => track.track_id === trackId) ?? -1;
304+
return Math.max(index, 0);
305+
}
306+
286307
export function getLoadedAlbumId(): string | null {
287308
return requestedAlbumId;
288309
}

‎test/playerLoader.test.ts‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ vi.mock('../src/bclient', () => {
3030
return {
3131
getTralbumDetails: vi.fn(async (albumId: string | number) => {
3232
const id = Number(albumId);
33+
34+
// Track 7772 is the second track of album 777; track 8000 is a standalone single
35+
if (id === 7772 || id === 8000) {
36+
return {
37+
id,
38+
type: 't',
39+
album_id: id === 7772 ? 777 : null,
40+
title: `Track ${id}`,
41+
tralbum_artist: 'Test Artist',
42+
is_purchasable: true,
43+
price: 1.0,
44+
currency: 'USD',
45+
tracks: [playableTrack(id)]
46+
};
47+
}
48+
49+
if (id === 777) {
50+
return {
51+
id,
52+
type: 'a',
53+
title: 'Test Album 777',
54+
tralbum_artist: 'Test Artist',
55+
is_purchasable: true,
56+
price: 10.0,
57+
currency: 'USD',
58+
tracks: [playableTrack(7771), playableTrack(7772), playableTrack(7773)]
59+
};
60+
}
3361
// Album 456 has a playable first track and nothing playable after it
3462
const tracks =
3563
id === 456
@@ -375,6 +403,29 @@ describe('PlayerLoader - Main Player Logic', () => {
375403
});
376404
});
377405

406+
describe('Previewing a track', () => {
407+
it('should show the whole album and start on that track', async () => {
408+
await player.loadAlbumIntoDrawer('7772', 'track', false);
409+
410+
expect(player.getCurrentAlbumData()?.id).toBe(777);
411+
expect(player.getCurrentTrackIndex()).toBe(1);
412+
expect((document.querySelector('audio') as HTMLAudioElement).src).toContain('track7772.mp3');
413+
});
414+
415+
it('should still toggle the drawer for the track that was previewed', async () => {
416+
await player.loadAlbumIntoDrawer('7772', 'track', false);
417+
418+
expect(player.getLoadedAlbumId()).toBe('7772');
419+
});
420+
421+
it('should show a single on its own', async () => {
422+
await player.loadAlbumIntoDrawer('8000', 'track', false);
423+
424+
expect(player.getCurrentAlbumData()?.id).toBe(8000);
425+
expect(player.getCurrentTrackIndex()).toBe(0);
426+
});
427+
});
428+
378429
describe('Persistent audio element with state management', () => {
379430
it('should create persistent audio element only once', async () => {
380431
discography.updateDiscographyOrder();

0 commit comments

Comments
 (0)