feat(playlists): библиотека плейлистов приложения

CRUD в редакторе, read-only окно на пульте, отдельный аудиоканал, экспорт/импорт и маппинг из Foundry.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-09-03 10:37:21 +08:00
parent cfb6e62331
commit 278eb78637
46 changed files with 2438 additions and 41 deletions
+24 -4
View File
@@ -279,10 +279,17 @@ function collectSceneAudioPaths(
return paths;
}
export type FoundryPendingAppPlaylist = {
name: string;
tracks: { title: string; absPath: string; mime: string }[];
};
export type FoundryBuiltProject = {
project: Project;
/** Абсолютные пути исходников превью для последующей генерации thumb (sceneId → abs path). */
previewSources: { sceneId: SceneId; absPath: string }[];
/** Неиспользуемые на сценах Foundry-плейлисты → локальная библиотека. */
pendingAppPlaylists: FoundryPendingAppPlaylist[];
};
export async function buildProjectFromFoundryDocuments(
@@ -406,15 +413,28 @@ export async function buildProjectFromFoundryDocuments(
importScenes.map((s) => s.playlist).filter((id): id is string => typeof id === 'string'),
);
const campaignAudios: Project['campaignAudios'] = [];
const pendingAppPlaylists: FoundryPendingAppPlaylist[] = [];
for (const pl of docs.playlists) {
if (usedPlaylistIds.has(pl._id)) continue;
const sounds = [...(pl.sounds ?? [])].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
const tracks: FoundryPendingAppPlaylist['tracks'] = [];
for (const s of sounds) {
if (typeof s.path !== 'string' || !s.path.trim()) continue;
const assetId = await resolveAndImport(ctx, manifest, s.path);
if (!assetId || ctx.assets[assetId]?.type !== 'audio') continue;
campaignAudios.push({ assetId, autoplay: false, loop: true });
const abs = await resolveFoundryAssetPath(manifest, s.path);
if (!abs) continue;
const kind = classifyMediaPath(abs);
if (!kind || kind.type !== 'audio') continue;
const title =
(typeof s.name === 'string' && s.name.trim()) ||
path.basename(s.path).replace(/\.[^.]+$/u, '') ||
'Track';
tracks.push({ title, absPath: abs, mime: kind.mime });
}
if (tracks.length === 0) continue;
pendingAppPlaylists.push({
name: (typeof pl.name === 'string' && pl.name.trim()) || 'Playlist',
tracks,
});
}
const { npcGroups, foundryFolderToGroupId } = buildNpcGroupsFromFoundryFolders(docs.folders);
@@ -538,7 +558,7 @@ export async function buildProjectFromFoundryDocuments(
sceneGraphEdges,
};
return { project, previewSources };
return { project, previewSources, pendingAppPlaylists };
}
/** Actor folders Foundry → дерево групп НПС. */