Files
reader/server/test/media-embedded-artwork.test.ts
hectorqin 73f9eb6e20 feat(media): add isolated media libraries and playback
Support local and OpenList libraries, metadata, independent media navigation, themes, and audio/video playback. Preserve approved v2 prototypes and current documentation; archive superseded review artifacts and keep generated validation output outside version control.
2026-09-27 18:22:51 +08:00

40 lines
3.5 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp,mkdir,readFile,rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { execFile } from 'node:child_process';
import { Db } from '../src/db/index.ts';
import { MediaLibraries } from '../src/media/libraries.ts';
import { MediaScanner } from '../src/media/scanner.ts';
import { MediaArtwork } from '../src/media/artwork.ts';
for(const [extension,codec] of [['mp3','libmp3lame'],['flac','flac'],['m4a','aac'],['m4b','aac']] as const) test(`real ${extension} embedded artwork survives scanning and authenticated cover retrieval`, {skip:!process.env.MEDIA_TEST_FFMPEG||!process.env.MEDIA_FFPROBE_PATH},async t=>{
const root=await mkdtemp(join(tmpdir(),'embedded-artwork-')),media=join(root,'media');await mkdir(media);
const db=new Db(':memory:'),libraries=new MediaLibraries(db),scanner=new MediaScanner(db,libraries);
t.after(async()=>{await scanner.close();db.close();await rm(root,{recursive:true,force:true});});
const previous=process.env.MEDIA_FFMPEG_PATH;process.env.MEDIA_FFMPEG_PATH=process.env.MEDIA_TEST_FFMPEG;
t.after(()=>{if(previous===undefined)delete process.env.MEDIA_FFMPEG_PATH;else process.env.MEDIA_FFMPEG_PATH=previous;});
const run=promisify(execFile),image=join(root,'cover.png'),file=join(media,`song.${extension}`);
await run(process.env.MEDIA_TEST_FFMPEG!,['-nostdin','-loglevel','error','-f','lavfi','-i','color=c=red:s=32x32','-frames:v','1',image],{windowsHide:true,timeout:30000});
await run(process.env.MEDIA_TEST_FFMPEG!,['-nostdin','-loglevel','error','-f','lavfi','-i','sine=duration=1','-i',image,'-map','0:a','-map','1:v','-c:a',codec,'-c:v','copy','-disposition:v','attached_pic',file],{windowsHide:true,timeout:30000});
const before=await readFile(file),actor={id:'admin',role:'admin'} as const;
const lib=await libraries.create(actor,{name:'music',kind:extension==='m4b'?'audiobook':'music',root:media,access:'all'});
const job=scanner.start(actor,lib.id);await scanner.wait(lib.id);assert.equal(scanner.job(actor,job.id).state,'complete');
const album=scanner.catalog.list(actor,lib.id,{kind:extension==='m4b'?'audiobook':'album'}).items[0]!;
assert.equal(typeof album.metadata.embeddedCoverAssetId,'string',JSON.stringify(db.all('SELECT probe_status,technical_json FROM media_assets')));
const artwork=new MediaArtwork(db,libraries);
const result=await artwork.cover(actor,album.id);assert.equal(result.contentType,'image/png');assert.deepEqual(result.bytes,await readFile(image));
assert.deepEqual((await artwork.cover(actor,album.id)).bytes,result.bytes);assert.deepEqual(await readFile(file),before);
const asset=db.get<{id:string;technical_json:string}>('SELECT id,technical_json FROM media_assets')!;
const legacy=JSON.parse(asset.technical_json);for(const stream of legacy.streams)delete stream.attachedPicture;
db.run('UPDATE media_assets SET technical_json=? WHERE id=?',JSON.stringify(legacy),asset.id);
db.run("UPDATE media_items SET metadata_json=json_remove(metadata_json,'$.embeddedCoverAssetId')");
scanner.start(actor,lib.id);await scanner.wait(lib.id);
assert.equal(scanner.catalog.detail(actor,album.id).metadata.embeddedCoverAssetId,asset.id);
assert.deepEqual((await artwork.cover(actor,album.id)).bytes,result.bytes);
db.run('UPDATE media_libraries SET access=? WHERE id=?','restricted',lib.id);
await assert.rejects(artwork.cover({id:'member',role:'member'},album.id),{statusCode:404});
});