Fix artwork upload pipeline

This commit is contained in:
diyaa
2026-05-31 01:20:56 +02:00
parent 7b1952794c
commit 18ed79e3c4
24 changed files with 1652 additions and 56 deletions
+288 -12
View File
@@ -5,12 +5,13 @@ import { dirname, join } from 'node:path';
import { Readable } from 'node:stream';
import {
ForbiddenException,
INestApplication,
NotFoundException,
ValidationPipe,
VersioningType,
} from '@nestjs/common';
import type { NestExpressApplication } from '@nestjs/platform-express';
import { Test } from '@nestjs/testing';
import { API_JSON_BODY_LIMIT } from '../../src/app.factory';
import { AppModule } from '../../src/app.module';
import { AssetsController } from '../../src/modules/assets/assets.controller';
import { AssetDownloadQueryDto } from '../../src/modules/assets/assets.dto';
@@ -218,6 +219,33 @@ function createPrismaMock() {
findUnique: jest.fn().mockImplementation(async ({ where }) => {
return artworkAssets.get(where.id) ?? null;
}),
findFirst: jest.fn().mockImplementation(async ({ where }) => {
return (
[...artworkAssets.values()].find(
(asset) =>
asset.userId === where.userId &&
asset.sha256 === where.sha256,
) ?? null
);
}),
create: jest.fn().mockImplementation(async ({ data }) => {
const record = {
id: randomUUID(),
createdAt: new Date(),
...data,
};
artworkAssets.set(record.id, record);
return record;
}),
update: jest.fn().mockImplementation(async ({ where, data }) => {
const current = artworkAssets.get(where.id);
const updated = {
...current,
...data,
};
artworkAssets.set(where.id, updated);
return updated;
}),
},
uploadSession: {
create: jest.fn().mockImplementation(async ({ data }) => {
@@ -284,7 +312,7 @@ function createPrismaMock() {
}
describe('Velody API wiring (e2e)', () => {
let app: INestApplication;
let app: NestExpressApplication;
let assetsController: AssetsController;
let artworkController: ArtworkController;
let healthController: HealthController;
@@ -314,7 +342,12 @@ describe('Velody API wiring (e2e)', () => {
.useValue(prismaMock)
.compile();
app = moduleRef.createNestApplication();
app = moduleRef.createNestApplication({ bodyParser: false });
app.useBodyParser('json', { limit: API_JSON_BODY_LIMIT });
app.useBodyParser('urlencoded', {
extended: true,
limit: API_JSON_BODY_LIMIT,
});
app.setGlobalPrefix('api');
app.enableVersioning({ type: VersioningType.URI });
app.useGlobalPipes(
@@ -515,10 +548,16 @@ describe('Velody API wiring (e2e)', () => {
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P8z8DwHwAFgwJ/lBi4NwAAAABJRU5ErkJggg==',
'base64',
);
const storageKey = join('library', 'artwork', `${artworkId}.png`);
const storageKey = join(
'users',
prismaState.defaultUser.id,
'artwork',
`${sha256Hex(bytes)}.png`,
);
prismaState.artworkAssets.set(artworkId, {
id: artworkId,
userId: prismaState.defaultUser.id,
sha256: sha256Hex(bytes),
storageKey,
mimeType: 'image/png',
@@ -526,9 +565,7 @@ describe('Velody API wiring (e2e)', () => {
height: 1,
fileSizeBytes: BigInt(bytes.length),
createdAt: new Date('2026-05-29T08:00:00.000Z'),
track: {
userId: prismaState.defaultUser.id,
},
tracks: [{ userId: prismaState.defaultUser.id }],
});
prismaState.tracks.set(trackId, {
id: trackId,
@@ -583,16 +620,20 @@ describe('Velody API wiring (e2e)', () => {
prismaState.artworkAssets.set(artworkId, {
id: artworkId,
userId: prismaState.defaultUser.id,
sha256: 'sha-missing-artwork',
storageKey: join('library', 'artwork', `${artworkId}.png`),
storageKey: join(
'users',
prismaState.defaultUser.id,
'artwork',
'sha-missing-artwork.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,
},
tracks: [{ userId: prismaState.defaultUser.id }],
});
await expect(
@@ -638,6 +679,8 @@ describe('Velody API wiring (e2e)', () => {
const primaryTrackId = randomUUID();
const primaryAssetId = randomUUID();
const primaryArtworkId = randomUUID();
const placeholderTrackId = randomUUID();
const placeholderAssetId = randomUUID();
const secondaryTrackId = randomUUID();
const secondaryAssetId = randomUUID();
@@ -669,13 +712,29 @@ describe('Velody API wiring (e2e)', () => {
});
prismaState.artworkAssets.set(primaryArtworkId, {
id: primaryArtworkId,
userId: prismaState.defaultUser.id,
sha256: 'artwork-sha-default',
storageKey: `library/artwork/${primaryArtworkId}.png`,
storageKey: `users/${prismaState.defaultUser.id}/artwork/artwork-sha-default.png`,
mimeType: 'image/png',
width: 512,
height: 512,
fileSizeBytes: BigInt(128),
createdAt: new Date('2026-05-29T08:00:30.000Z'),
tracks: [{ userId: prismaState.defaultUser.id }],
});
prismaState.audioAssets.set(placeholderAssetId, {
id: placeholderAssetId,
userId: prismaState.defaultUser.id,
trackId: placeholderTrackId,
sha256: 'sha-no-artwork',
storageKey: 'users/default/audio/sha-no-artwork.mp3',
originalFilename: 'no-artwork.mp3',
mimeType: 'audio/mpeg',
fileExtension: 'mp3',
fileSizeBytes: BigInt(64),
durationMs: 201000,
sourceDeviceId: primaryDevice.deviceId,
createdAt: new Date('2026-05-29T08:00:45.000Z'),
});
prismaState.audioAssets.set(secondaryAssetId, {
id: secondaryAssetId,
@@ -730,6 +789,25 @@ describe('Velody API wiring (e2e)', () => {
createdAt: new Date('2026-05-29T08:01:00.000Z'),
updatedAt: new Date('2026-05-29T08:03:00.000Z'),
});
prismaState.tracks.set(placeholderTrackId, {
id: placeholderTrackId,
userId: prismaState.defaultUser.id,
primaryAudioAssetId: placeholderAssetId,
artworkAssetId: null,
title: 'No Artwork Track',
artist: 'Velody',
album: null,
albumArtist: null,
genre: null,
discNumber: null,
trackNumber: null,
year: null,
durationMs: 201000,
status: 'ACTIVE',
deletedAt: null,
createdAt: new Date('2026-05-29T08:00:45.000Z'),
updatedAt: new Date('2026-05-29T08:02:30.000Z'),
});
const response = await libraryController.getTracks({
deviceId: primaryDevice.deviceId,
@@ -754,6 +832,17 @@ describe('Velody API wiring (e2e)', () => {
height: 512,
},
},
{
trackId: placeholderTrackId,
title: 'No Artwork Track',
artist: 'Velody',
durationSeconds: 201,
sha256: 'sha-no-artwork',
assetId: placeholderAssetId,
createdAt: '2026-05-29T08:00:45.000Z',
updatedAt: '2026-05-29T08:02:30.000Z',
artwork: null,
},
],
});
});
@@ -832,7 +921,194 @@ describe('Velody API wiring (e2e)', () => {
});
expect(duplicatePrepare.status).toBe('exists');
expect(duplicatePrepare.uploadId).toBeDefined();
expect(prismaState.audioAssets.size).toBe(1);
expect(prismaState.libraryEvents.size).toBe(1);
});
it('supports upload finalize with embedded artwork and exposes remote artwork metadata', async () => {
const registerResponse = await devicesController.register({
platform: 'MACOS',
deviceName: 'Artwork Upload Mac',
appVersion: '0.1.0',
});
const bytes = sampleMp3Bytes('e2e-upload-artwork');
const artworkBytes = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P8z8DwHwAFgwJ/lBi4NwAAAABJRU5ErkJggg==',
'base64',
);
const sha256 = sha256Hex(bytes);
const artworkSha256 = sha256Hex(artworkBytes);
const prepareResponse = await uploadsController.prepare({
deviceId: registerResponse.deviceId,
sha256,
originalFilename: 'e2e-upload-artwork.mp3',
sizeBytes: bytes.length,
});
expect(prepareResponse.status).toBe('upload_required');
const uploadResponse = await uploadsService.uploadFile(
prepareResponse.uploadId!,
createUploadRequest(bytes),
);
expect(uploadResponse.status).toBe('COMPLETED');
const finalizeResponse = await uploadsController.finalize(
prepareResponse.uploadId!,
{
title: 'Uploaded Artwork Track',
artist: 'Velody',
album: 'Milestone 8.1',
durationMs: 222000,
artwork: {
dataBase64: artworkBytes.toString('base64'),
sha256: artworkSha256,
mimeType: 'image/png',
width: 1,
height: 1,
},
},
);
expect(finalizeResponse.trackId).toBeDefined();
expect(prismaState.artworkAssets.size).toBe(1);
const remoteLibrary = await libraryController.getTracks({
deviceId: registerResponse.deviceId,
});
expect(remoteLibrary.tracks).toEqual([
expect.objectContaining({
trackId: finalizeResponse.trackId,
artwork: {
artworkId: expect.any(String),
sha256: artworkSha256,
mimeType: 'image/png',
width: 1,
height: 1,
},
}),
]);
const artworkAsset = [...prismaState.artworkAssets.values()][0];
const storedArtworkBytes = await readFile(
join(
storageRoot,
'users',
prismaState.defaultUser.id,
'artwork',
`${artworkSha256}.png`,
),
);
expect(storedArtworkBytes.equals(artworkBytes)).toBe(true);
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(
artworkAsset.id,
{ deviceId: registerResponse.deviceId },
responseMock,
);
const downloadedArtworkBytes = await streamToBuffer(streamable.getStream());
expect(downloadedArtworkBytes.equals(artworkBytes)).toBe(true);
expect(headers.get('content-type')).toBe('image/png');
});
it('attaches artwork through the deduped prepare exists path and returns non-null remote artwork metadata', async () => {
const registerResponse = await devicesController.register({
platform: 'MACOS',
deviceName: 'Deduped Artwork Upload Mac',
appVersion: '0.1.0',
});
const bytes = sampleMp3Bytes('e2e-deduped-artwork');
const artworkBytes = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P8z8DwHwAFgwJ/lBi4NwAAAABJRU5ErkJggg==',
'base64',
);
const sha256 = sha256Hex(bytes);
const artworkSha256 = sha256Hex(artworkBytes);
const firstPrepare = await uploadsController.prepare({
deviceId: registerResponse.deviceId,
sha256,
originalFilename: 'e2e-deduped-artwork.mp3',
sizeBytes: bytes.length,
});
expect(firstPrepare.status).toBe('upload_required');
const uploadResponse = await uploadsService.uploadFile(
firstPrepare.uploadId!,
createUploadRequest(bytes),
);
expect(uploadResponse.status).toBe('COMPLETED');
const firstFinalize = await uploadsController.finalize(
firstPrepare.uploadId!,
{
title: 'Deduped Artwork Track',
artist: 'Velody',
album: 'Milestone 8.1',
durationMs: 222000,
},
);
const secondPrepare = await uploadsController.prepare({
deviceId: registerResponse.deviceId,
sha256,
originalFilename: 'e2e-deduped-artwork.mp3',
sizeBytes: bytes.length,
});
expect(secondPrepare.status).toBe('exists');
expect(secondPrepare.uploadId).toBeDefined();
expect(prismaState.audioAssets.size).toBe(1);
const secondFinalize = await uploadsController.finalize(
secondPrepare.uploadId!,
{
title: 'Deduped Artwork Track',
artist: 'Velody',
album: 'Milestone 8.1',
durationMs: 222000,
artwork: {
dataBase64: artworkBytes.toString('base64'),
sha256: artworkSha256,
mimeType: 'image/png',
width: 1,
height: 1,
},
},
);
expect(secondFinalize.trackId).toBe(firstFinalize.trackId);
expect(secondFinalize.assetId).toBe(firstFinalize.assetId);
expect(prismaState.artworkAssets.size).toBe(1);
const remoteLibrary = await libraryController.getTracks({
deviceId: registerResponse.deviceId,
});
expect(remoteLibrary.tracks).toEqual([
expect.objectContaining({
trackId: firstFinalize.trackId,
artwork: {
artworkId: expect.any(String),
sha256: artworkSha256,
mimeType: 'image/png',
width: 1,
height: 1,
},
}),
]);
});
});