mirror of
https://github.com/hectorqin/reader.git
synced 2026-11-11 21:41:02 +08:00
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.
61 lines
4.6 KiB
TypeScript
61 lines
4.6 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} from 'node:path';
|
|
import {Db} from '../src/db/index.ts';
|
|
import {MediaLibraries} from '../src/media/libraries.ts';
|
|
import {MediaScanner} from '../src/media/scanner.ts';
|
|
import {MediaPlayback} from '../src/media/playback.ts';
|
|
|
|
test('manual music parenting preserves item and part identities across rescans and rejects stale or invalid moves',async t=>{
|
|
const root=await mkdtemp(join(tmpdir(),'media-parent-')),db=new Db(':memory:');
|
|
db.run("INSERT INTO users(id,username,password_hash,role,created_at,updated_at) VALUES('admin','admin','x','admin',0,0)");
|
|
const actor={id:'admin',role:'admin'} as const,libraries=new MediaLibraries(db);
|
|
const scanner=new MediaScanner(db,libraries,async()=>({status:'unsupported'}));
|
|
t.after(async()=>{await scanner.close();db.close();await rm(root,{recursive:true,force:true});});
|
|
for(const name of ['A','B']){
|
|
await mkdir(join(root,name));await writeFile(join(root,name,'track.mp3'),'fixture');
|
|
await writeFile(join(root,name,'track.nfo'),`<album><title>${name}曲目</title><album>${name}专辑</album><artist>${name}歌手</artist></album>`);
|
|
}
|
|
const library=await libraries.create(actor,{name:'music',kind:'music',root,access:'all'});
|
|
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,tracks=catalog.list(actor,library.id,{kind:'track'}).items;
|
|
const first=catalog.detail(actor,tracks[0]!.id),other=catalog.detail(actor,tracks[1]!.id);
|
|
assert.notEqual(first.parentId,other.parentId);
|
|
const parts=first.editions.flatMap(edition=>edition.parts.map(part=>part.id));
|
|
const playback=new MediaPlayback(db,libraries),session=playback.create(actor,parts[0]!);
|
|
playback.update(actor,session.id,{sequence:0,revision:session.revision,position:15});
|
|
const progress=playback.progress(actor,parts[0]!);
|
|
const moved=catalog.reparentMusic(actor,first.id,other.parentId!,first.parentId);
|
|
assert.equal(moved.parentId,other.parentId);assert.deepEqual(moved.editions.flatMap(edition=>edition.parts.map(part=>part.id)),parts);
|
|
assert.throws(()=>catalog.reparentMusic(actor,first.id,first.parentId!,first.parentId),{code:'MEDIA_PARENT_CHANGED'});
|
|
assert.throws(()=>catalog.reparentMusic(actor,first.id,other.id,other.parentId),{code:'MEDIA_PARENT_TARGET'});
|
|
assert.throws(()=>catalog.reparentMusic({id:'admin',role:'member'},first.id,first.parentId!,other.parentId),{statusCode:403});
|
|
const another=await libraries.create(actor,{name:'other',kind:'music',root,access:'restricted'});
|
|
db.run("INSERT INTO media_items(id,library_id,kind,local_key,title,metadata_json) VALUES('foreign-album',?,'album','foreign','other','{}')",another.id);
|
|
assert.throws(()=>catalog.reparentMusic(actor,first.id,'foreign-album',other.parentId),{code:'MEDIA_PARENT_TARGET'});
|
|
const album=catalog.detail(actor,other.parentId!),oldAlbum=catalog.detail(actor,first.parentId!);
|
|
catalog.reparentMusic(actor,album.id,oldAlbum.parentId!,album.parentId);
|
|
await scan();
|
|
assert.equal(catalog.detail(actor,first.id).parentId,other.parentId);
|
|
assert.equal(catalog.detail(actor,album.id).parentId,oldAlbum.parentId);
|
|
assert.deepEqual(catalog.detail(actor,first.id).editions.flatMap(edition=>edition.parts.map(part=>part.id)),parts);
|
|
assert.deepEqual(playback.progress(actor,parts[0]!),progress);
|
|
const beforeCount=catalog.list(actor,library.id,{kind:'album'}).total;
|
|
const regrouped=catalog.createMusicParent(actor,first.id,'新建专辑',other.parentId);
|
|
const newAlbum=catalog.detail(actor,regrouped.parentId!);
|
|
assert.equal(newAlbum.title,'新建专辑');assert.equal(newAlbum.parentId,oldAlbum.parentId);
|
|
assert.throws(()=>catalog.createMusicParent(actor,first.id,'重复提交',other.parentId),{code:'MEDIA_PARENT_CHANGED'});
|
|
assert.equal(catalog.list(actor,library.id,{kind:'album'}).total,beforeCount+1,'stale creation does not leave a new album');
|
|
const artistCount=catalog.list(actor,library.id,{kind:'artist'}).total;
|
|
const regroupedAlbum=catalog.createMusicParent(actor,newAlbum.id,'新建歌手',newAlbum.parentId);
|
|
assert.equal(catalog.detail(actor,regroupedAlbum.parentId!).title,'新建歌手');
|
|
assert.equal(catalog.list(actor,library.id,{kind:'artist'}).total,artistCount+1);
|
|
await scan();
|
|
assert.equal(catalog.detail(actor,first.id).parentId,newAlbum.id);
|
|
assert.equal(catalog.detail(actor,newAlbum.id).parentId,regroupedAlbum.parentId);
|
|
assert.deepEqual(playback.progress(actor,parts[0]!),progress);
|
|
});
|