Files
reader/server/test/media-narrators.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

54 lines
3.9 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, basename } from 'node:path';
import { Db } from '../src/db/index.ts';
import { MediaLibraries } from '../src/media/libraries.ts';
import { MediaScanner } from '../src/media/scanner.ts';
test('narrator directory tracks asset credits across edition changes, overrides and legacy rescans',async t=>{
const root=await mkdtemp(join(tmpdir(),'media-narrators-'));await mkdir(join(root,'Book'));
const db=new Db(':memory:'),libraries=new MediaLibraries(db),actor={id:'admin',role:'admin'} as const;
const scanner=new MediaScanner(db,libraries,async file=>({status:'ready',info:{duration:90,format:'m4b',streams:[],chapters:[],tags:{album:'同一作品',narrator:basename(file).startsWith('a')?'甲':'乙'}}}));
t.after(async()=>{await scanner.close();db.close();await rm(root,{recursive:true,force:true});});
for(const file of ['a1.m4b','a2.m4b','b.m4b'])await writeFile(join(root,'Book',file),file);
const library=await libraries.create(actor,{name:'有声',kind:'audiobook',root,access:'restricted'});
const scan=async()=>{const job=scanner.start(actor,library.id);await scanner.wait(library.id);assert.equal(scanner.job(actor,job.id).state,'complete');};
await scan();const catalog=scanner.catalog;
assert.throws(()=>catalog.narrators({id:'other',role:'member'},library.id),{statusCode:404});
const list=()=>catalog.narrators(actor,library.id);
assert.equal(list().total,2);
assert.equal(catalog.narrators(actor,library.id,{search:'%'}).total,0,'search is literal');
assert.equal(catalog.narrators(actor,library.id,{offset:1,limit:1}).items.length,1);
const forName=(name:string)=>catalog.narrators(actor,library.id,{name}).items as ReturnType<typeof catalog.detail>[];
const work=forName('甲')[0]!;
assert.equal(work.editions.length,1);assert.equal(work.editions[0]!.parts.length,2);
assert.equal(forName('乙')[0]!.editions.length,1);assert.equal(forName('乙')[0]!.editions[0]!.parts.length,1);
assert.notEqual(work.editions[0]!.id,forName('乙')[0]!.editions[0]!.id);
const source=work.editions[0]!;
const split=catalog.splitEdition(actor,source.id,{expectedRevision:source.revision,assetIds:[source.parts[0]!.assetId],label:'拆分'});
assert.equal(forName('甲')[0]!.editions.length,2);
const left=split.editions.find(e=>e.id===source.id)!,right=split.editions.find(e=>e.label==='拆分')!;
catalog.mergeEditions(actor,left.id,{expectedRevision:left.revision,targetEditionId:right.id,targetRevision:right.revision});
assert.equal(forName('甲')[0]!.editions.length,1);
catalog.override(actor,work.id,{narrator:'人工演播者'});
assert.equal(forName('甲').length,0);assert.equal(forName('人工演播者')[0]!.editions.length,2);
catalog.override(actor,work.id,{narrator:null});
assert.equal(list().total,2);
db.run('DELETE FROM media_asset_narrators');
assert.equal(list().total,0,'legacy records must not borrow a work-level credit');
await scan();assert.equal(list().total,2);assert.equal(forName('甲')[0]!.editions.length,1);
await mkdir(join(root,'Other'));await writeFile(join(root,'Other','b.m4b'),'other');await scan();
const target=catalog.list(actor,library.id,{kind:'audiobook'}).items.find(item=>item.id!==work.id)!;
const edition=forName('甲')[0]!.editions[0]!;
catalog.reassignEdition(actor,edition.id,target.id,work.id);
assert.equal(forName('甲')[0]!.id,target.id);
await scan();assert.equal(forName('甲')[0]!.id,target.id,'manual reassignment retains the credited assets');
assert.equal(catalog.narrators(actor,'').total,2);
assert.equal(catalog.narrators({id:'member',role:'member'},'').total,0);
assert.equal(catalog.narrators({id:'member',role:'member'},'',{name:'甲'}).total,0);
libraries.setAccess(actor,library.id,'all',[]);
assert.equal(catalog.narrators({id:'member',role:'member'},'').total,2);
});