|
|
@@ -233,17 +233,15 @@ router.post('/books/:id', optionalAuth, async (ctx: Context) => {
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
- * GET /api/book-generator/books/:id/chapters
|
|
|
- * 获取专辑章节列表
|
|
|
+ * 获取专辑章节列表 handler
|
|
|
* 返回章(level=1)的列表,每个章包含其下所有小节的合并音频
|
|
|
* 过滤规则:公开的音频 + 当前用户自己的音频
|
|
|
*/
|
|
|
-router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
|
|
|
+async function getChapters(ctx: Context) {
|
|
|
try {
|
|
|
const bookId = ctx.params.id as string;
|
|
|
const userId = ctx.state.user?.userId || TEST_USER_ID;
|
|
|
|
|
|
- // 获取专辑信息
|
|
|
const book = await prisma.book.findUnique({
|
|
|
where: { id: parseInt(bookId) },
|
|
|
});
|
|
|
@@ -254,20 +252,16 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 判断是否为所有者(userId为null时也视为所有者,这样无需登录也能访问自己的音频)
|
|
|
const isOwner = book.userId === null || book.userId === parseInt(userId);
|
|
|
|
|
|
- // 获取所有章节
|
|
|
const allChapters = await prisma.bookChapter.findMany({
|
|
|
where: { bookId: parseInt(bookId) },
|
|
|
orderBy: { number: 'asc' },
|
|
|
});
|
|
|
|
|
|
- // 构建章(1) -> 节(2) -> 小节(3) 的树形结构
|
|
|
const chapterMap = new Map<number, any>();
|
|
|
const sectionMap = new Map<number, any>();
|
|
|
|
|
|
- // 先遍历,构建映射
|
|
|
allChapters.forEach(c => {
|
|
|
if (c.level === 1) {
|
|
|
chapterMap.set(c.id, {
|
|
|
@@ -283,7 +277,7 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
|
|
|
videoDuration: c.videoDuration || 0,
|
|
|
isPublic: c.isPublic,
|
|
|
level: c.level,
|
|
|
- subsections: [], // 节和小节列表
|
|
|
+ subsections: [],
|
|
|
});
|
|
|
} else if (c.level === 2) {
|
|
|
sectionMap.set(c.id, {
|
|
|
@@ -295,10 +289,7 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
- // 收集所有小节及其音频,按章分组
|
|
|
const subsectionsByChapter = new Map<number, { audioUrl: string; audioDuration: number; title: string; isPublic: boolean }[]>();
|
|
|
-
|
|
|
- // 先把3级小节挂到2级节下
|
|
|
const sectionSubsections = new Map<number, { audioUrl: string; audioDuration: number; title: string; isPublic: boolean }[]>();
|
|
|
allChapters.forEach(c => {
|
|
|
if (c.level === 3 && c.parentId && c.audioUrl && c.audioUrl.trim() !== '') {
|
|
|
@@ -314,7 +305,6 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
- // 把2级节挂到1级章下,并收集小节音频
|
|
|
chapterMap.forEach((chapter, chapterId) => {
|
|
|
subsectionsByChapter.set(chapterId, []);
|
|
|
});
|
|
|
@@ -324,10 +314,8 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
|
|
|
const chapter = chapterMap.get(c.parentId);
|
|
|
const sectionData = sectionMap.get(c.id);
|
|
|
if (chapter && sectionData) {
|
|
|
- // 获取这个小节的所有小节音频
|
|
|
const subs = sectionSubsections.get(c.id) || [];
|
|
|
chapter.subsections.push({ ...sectionData, subsections: subs });
|
|
|
- // 把小节音频按章汇总
|
|
|
subs.forEach(s => {
|
|
|
subsectionsByChapter.get(c.parentId)!.push(s);
|
|
|
});
|
|
|
@@ -335,41 +323,27 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
- // 为每个章设置audioUrl
|
|
|
- // 优先级:1. 章自身已有的合并音频(_merged) > 2. 章自身的audioUrl > 3. 仅汇总子节时长
|
|
|
- // 注意:不再用 subs[0].audioUrl 覆盖章的 audioUrl,因为章的合并音频已在 mergeChapterAudios 中正确写入
|
|
|
chapterMap.forEach((chapter, chapterId) => {
|
|
|
const subs = subsectionsByChapter.get(chapterId) || [];
|
|
|
if (subs.length > 0 && (!chapter.audioUrl || chapter.audioUrl.trim() === '')) {
|
|
|
- // 章自身没有音频(短文章以外的情况),仅汇总子节时长,不覆盖audioUrl
|
|
|
- // 前端播放章级别时会通过 player 接口自动触发合并
|
|
|
chapter.audioDuration = subs.reduce((acc, s) => acc + s.audioDuration, 0);
|
|
|
} else if (subs.length > 0) {
|
|
|
- // 章已有音频(可能是合并后的 _merged 音频),保留它,只汇总时长
|
|
|
chapter.audioDuration = subs.reduce((acc, s) => acc + s.audioDuration, 0);
|
|
|
}
|
|
|
- // 否则保持章自己的audioUrl(短文章直接生成在章上)
|
|
|
});
|
|
|
|
|
|
- // 转换为数组并应用过滤规则
|
|
|
let chapters = Array.from(chapterMap.values());
|
|
|
|
|
|
- // 应用公开过滤(只影响非所有者)
|
|
|
if (!isOwner) {
|
|
|
chapters = chapters.map(chapter => {
|
|
|
- // 检查是否有公开的小节音频
|
|
|
const hasPublicSubsection = chapter.subsections?.some((s: any) =>
|
|
|
s.subsections?.some((sub: any) => sub.isPublic === true && sub.audioUrl)
|
|
|
);
|
|
|
- // 检查章节本身是否有公开音频
|
|
|
const hasOwnPublicAudio = chapter.audioUrl &&
|
|
|
chapter.audioUrl.trim() !== '' &&
|
|
|
chapter.isPublic === true;
|
|
|
if (!hasPublicSubsection && !hasOwnPublicAudio) {
|
|
|
- return {
|
|
|
- ...chapter,
|
|
|
- audioUrl: null,
|
|
|
- };
|
|
|
+ return { ...chapter, audioUrl: null };
|
|
|
}
|
|
|
return chapter;
|
|
|
});
|
|
|
@@ -378,17 +352,18 @@ router.get('/books/:id/chapters', optionalAuth, async (ctx: Context) => {
|
|
|
ctx.body = {
|
|
|
code: 0,
|
|
|
message: 'success',
|
|
|
- data: {
|
|
|
- chapters,
|
|
|
- isOwner,
|
|
|
- },
|
|
|
+ data: { chapters, isOwner },
|
|
|
};
|
|
|
} catch (error) {
|
|
|
console.error('查询失败:', error);
|
|
|
ctx.status = 500;
|
|
|
ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' };
|
|
|
}
|
|
|
-});
|
|
|
+}
|
|
|
+
|
|
|
+// GET /api/book-generator/books/:id/chapters 和 /albums/:id/chapters
|
|
|
+router.get('/books/:id/chapters', optionalAuth, getChapters);
|
|
|
+router.get('/albums/:id/chapters', optionalAuth, getChapters);
|
|
|
|
|
|
/**
|
|
|
* POST /api/book-generator/books/:id/chapters/:chapterId/merge-audio
|
|
|
@@ -478,14 +453,11 @@ router.post('/books/:id/chapters/:chapterId/merge-audio', optionalAuth, async (c
|
|
|
});
|
|
|
|
|
|
/**
|
|
|
- * GET /api/book-generator/books/chapters/:id
|
|
|
- * 获取单个章节详情
|
|
|
+ * 获取单个章节详情 handler
|
|
|
*/
|
|
|
-router.get('/books/chapters/:id', optionalAuth, async (ctx: Context) => {
|
|
|
+async function getChapterDetail(ctx: Context) {
|
|
|
try {
|
|
|
const chapterId = parseInt(ctx.params.id as string);
|
|
|
-
|
|
|
- // 获取章节详情
|
|
|
const chapter = await prisma.bookChapter.findUnique({
|
|
|
where: { id: chapterId },
|
|
|
});
|
|
|
@@ -521,6 +493,10 @@ router.get('/books/chapters/:id', optionalAuth, async (ctx: Context) => {
|
|
|
ctx.status = 500;
|
|
|
ctx.body = { code: 1, message: error instanceof Error ? error.message : '查询失败' };
|
|
|
}
|
|
|
-});
|
|
|
+}
|
|
|
+
|
|
|
+// GET /api/book-generator/books/chapters/:id 和 /albums/chapters/:id
|
|
|
+router.get('/books/chapters/:id', optionalAuth, getChapterDetail);
|
|
|
+router.get('/albums/chapters/:id', optionalAuth, getChapterDetail);
|
|
|
|
|
|
export default router;
|