Implement artwork download and cache

This commit is contained in:
diyaa
2026-05-30 09:43:14 +02:00
parent 8caf29f186
commit 7b1952794c
23 changed files with 1261 additions and 66 deletions
+133 -1
View File
@@ -14,6 +14,7 @@ import { Test } from '@nestjs/testing';
import { AppModule } from '../../src/app.module';
import { AssetsController } from '../../src/modules/assets/assets.controller';
import { AssetDownloadQueryDto } from '../../src/modules/assets/assets.dto';
import { ArtworkController } from '../../src/modules/artwork/artwork.controller';
import { AppConfigService } from '../../src/modules/config/config.service';
import { DevicesController } from '../../src/modules/devices/devices.controller';
import { HealthController } from '../../src/modules/health/health.controller';
@@ -62,6 +63,7 @@ function createPrismaMock() {
const devices = new Map<string, any>();
const tracks = new Map<string, any>();
const audioAssets = new Map<string, any>();
const artworkAssets = new Map<string, any>();
const uploadSessions = new Map<string, any>();
const libraryEvents = new Map<bigint, any>();
let nextLibraryEventId = 1n;
@@ -133,6 +135,9 @@ function createPrismaMock() {
primaryAudioAsset: track.primaryAudioAssetId
? audioAssets.get(track.primaryAudioAssetId) ?? null
: null,
artworkAsset: track.artworkAssetId
? artworkAssets.get(track.artworkAssetId) ?? null
: null,
}));
}),
findUnique: jest.fn().mockImplementation(async ({ where }) => {
@@ -209,6 +214,11 @@ function createPrismaMock() {
return updated;
}),
},
artworkAsset: {
findUnique: jest.fn().mockImplementation(async ({ where }) => {
return artworkAssets.get(where.id) ?? null;
}),
},
uploadSession: {
create: jest.fn().mockImplementation(async ({ data }) => {
const now = new Date();
@@ -266,6 +276,7 @@ function createPrismaMock() {
devices,
tracks,
audioAssets,
artworkAssets,
uploadSessions,
libraryEvents,
},
@@ -275,6 +286,7 @@ function createPrismaMock() {
describe('Velody API wiring (e2e)', () => {
let app: INestApplication;
let assetsController: AssetsController;
let artworkController: ArtworkController;
let healthController: HealthController;
let devicesController: DevicesController;
let libraryController: LibraryController;
@@ -315,6 +327,7 @@ describe('Velody API wiring (e2e)', () => {
await app.init();
assetsController = moduleRef.get(AssetsController);
artworkController = moduleRef.get(ArtworkController);
healthController = moduleRef.get(HealthController);
devicesController = moduleRef.get(DevicesController);
libraryController = moduleRef.get(LibraryController);
@@ -490,6 +503,107 @@ describe('Velody API wiring (e2e)', () => {
).rejects.toBeInstanceOf(NotFoundException);
});
it('downloads artwork bytes for the owning device user with the stored image mime type', async () => {
const registerResponse = await devicesController.register({
platform: 'IPHONE',
deviceName: 'Artwork iPhone',
appVersion: '0.1.0',
});
const artworkId = randomUUID();
const trackId = randomUUID();
const bytes = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P8z8DwHwAFgwJ/lBi4NwAAAABJRU5ErkJggg==',
'base64',
);
const storageKey = join('library', 'artwork', `${artworkId}.png`);
prismaState.artworkAssets.set(artworkId, {
id: artworkId,
sha256: sha256Hex(bytes),
storageKey,
mimeType: 'image/png',
width: 1,
height: 1,
fileSizeBytes: BigInt(bytes.length),
createdAt: new Date('2026-05-29T08:00:00.000Z'),
track: {
userId: prismaState.defaultUser.id,
},
});
prismaState.tracks.set(trackId, {
id: trackId,
userId: prismaState.defaultUser.id,
primaryAudioAssetId: null,
artworkAssetId: artworkId,
title: 'Artwork Track',
artist: 'Velody',
album: null,
albumArtist: null,
genre: null,
discNumber: null,
trackNumber: null,
year: null,
durationMs: null,
status: 'ACTIVE',
deletedAt: null,
createdAt: new Date('2026-05-29T08:00:00.000Z'),
updatedAt: new Date('2026-05-29T08:02:00.000Z'),
});
const filePath = join(storageRoot, storageKey);
await mkdir(dirname(filePath), { recursive: true });
await writeFile(filePath, bytes);
const headers = new Map<string, string>();
const responseMock = {
setHeader(name: string, value: string) {
headers.set(name.toLowerCase(), String(value));
},
} as any;
const streamable = await artworkController.download(
artworkId,
{ deviceId: registerResponse.deviceId },
responseMock,
);
const downloadedBytes = await streamToBuffer(streamable.getStream());
expect(downloadedBytes.equals(bytes)).toBe(true);
expect(headers.get('content-type')).toBe('image/png');
expect(headers.get('content-length')).toBe(String(bytes.length));
});
it('returns not found when the requested artwork file is missing', async () => {
const registerResponse = await devicesController.register({
platform: 'IPHONE',
deviceName: 'Artwork iPhone',
appVersion: '0.1.0',
});
const artworkId = randomUUID();
prismaState.artworkAssets.set(artworkId, {
id: artworkId,
sha256: 'sha-missing-artwork',
storageKey: join('library', 'artwork', `${artworkId}.png`),
mimeType: 'image/png',
width: 1,
height: 1,
fileSizeBytes: BigInt(10),
createdAt: new Date('2026-05-29T08:00:00.000Z'),
track: {
userId: prismaState.defaultUser.id,
},
});
await expect(
artworkController.download(
artworkId,
{ deviceId: registerResponse.deviceId },
{ setHeader() {} } as any,
),
).rejects.toBeInstanceOf(NotFoundException);
});
it('rejects an invalid asset download device id query', async () => {
const validationPipe = new ValidationPipe({
whitelist: true,
@@ -523,6 +637,7 @@ describe('Velody API wiring (e2e)', () => {
const secondaryDeviceId = randomUUID();
const primaryTrackId = randomUUID();
const primaryAssetId = randomUUID();
const primaryArtworkId = randomUUID();
const secondaryTrackId = randomUUID();
const secondaryAssetId = randomUUID();
@@ -552,6 +667,16 @@ describe('Velody API wiring (e2e)', () => {
sourceDeviceId: primaryDevice.deviceId,
createdAt: new Date('2026-05-29T08:00:00.000Z'),
});
prismaState.artworkAssets.set(primaryArtworkId, {
id: primaryArtworkId,
sha256: 'artwork-sha-default',
storageKey: `library/artwork/${primaryArtworkId}.png`,
mimeType: 'image/png',
width: 512,
height: 512,
fileSizeBytes: BigInt(128),
createdAt: new Date('2026-05-29T08:00:30.000Z'),
});
prismaState.audioAssets.set(secondaryAssetId, {
id: secondaryAssetId,
userId: secondUserId,
@@ -571,7 +696,7 @@ describe('Velody API wiring (e2e)', () => {
id: primaryTrackId,
userId: prismaState.defaultUser.id,
primaryAudioAssetId: primaryAssetId,
artworkAssetId: null,
artworkAssetId: primaryArtworkId,
title: 'Default User Track',
artist: 'Velody',
album: null,
@@ -621,6 +746,13 @@ describe('Velody API wiring (e2e)', () => {
assetId: primaryAssetId,
createdAt: '2026-05-29T08:00:00.000Z',
updatedAt: '2026-05-29T08:02:00.000Z',
artwork: {
artworkId: primaryArtworkId,
sha256: 'artwork-sha-default',
mimeType: 'image/png',
width: 512,
height: 512,
},
},
],
});