import { BookTaskDetailService } from '@/define/db/service/book/bookTaskDetailService' import { BookTaskService } from '@/define/db/service/book/bookTaskService' import { OptionRealmService } from '@/define/db/service/optionService' import { BookService } from '@/define/db/service/book/bookService' import { TaskListService } from '@/define/db/service/book/taskListService' import { TaskModal } from '@/define/model/task' import { Book } from '@/define/model/book/book' import { getProjectPath } from '../../option/optionCommonService' import path from 'path' import { isEmpty } from 'lodash' import axios from 'axios' import { define } from '@/define/define' import { CheckFolderExistsOrCreate, CopyFileOrFolder } from '@/define/Tools/file' import { DownloadFile } from '@/define/Tools/common' import { MappingTaskTypeToVideoModel } from '@/define/enum/video' import { BookBackTaskType } from '@/define/enum/bookEnum' import { t } from '@/i18n' export class BookBasicHandle { bookTaskDetailService!: BookTaskDetailService bookTaskService!: BookTaskService optionRealmService!: OptionRealmService bookService!: BookService taskListService!: TaskListService constructor() { // 初始化 } async InitBookBasicHandle() { // 如果 bookTaskDetailService 已经初始化,则直接返回 if (!this.bookTaskDetailService) { this.bookTaskDetailService = await BookTaskDetailService.getInstance() } if (!this.bookTaskService) { this.bookTaskService = await BookTaskService.getInstance() } if (!this.optionRealmService) { this.optionRealmService = await OptionRealmService.getInstance() } if (!this.bookService) { this.bookService = await BookService.getInstance() } if (!this.taskListService) { this.taskListService = await TaskListService.getInstance() } } /** * 检查所有的服务是否都已初始化 * @returns */ CheckInit() { if (this.bookTaskDetailService && this.bookTaskService && this.optionRealmService && this.bookService && this.taskListService) { return true } return false } /** 执行事务的方法 */ async transaction(callback: (realm: any) => void) { this.CheckInit() || await this.InitBookBasicHandle() this.bookService.transaction(() => { callback(this.bookService.realm) }) } /** * 下载视频文件并处理路径映射 * * 此方法负责从远程URL下载视频文件到本地,并处理文件的存储路径、转存服务等。 * 支持多种视频来源的处理,包括MidJourney、可灵等不同平台的视频文件。 * 会自动处理文件转存(除MJ官方CDN和可灵视频外),并更新数据库中的路径信息。 * * @param {string[]} videoUrls - 需要下载的视频URL列表 * @param {TaskModal.Task} task - 当前执行的任务对象,包含任务类型等信息 * @param {Book.SelectBookTaskDetail} bookTaskDetail - 小说任务详情对象,包含分镜信息 * @param {string} preffix - 文件名前缀,用于区分不同来源的视频文件 * * @returns {Promise<{outVideoPath: string, subVideoPath: string[]}>} 返回下载结果 * - outVideoPath: 主输出视频的本地路径(第一个视频的副本) * - subVideoPath: 所有子视频的路径信息数组(JSON字符串格式) * * @throws {Error} 当服务未初始化时 * @throws {Error} 当文件下载失败时 * @throws {Error} 当数据库操作失败时 * * @example * ```typescript * const result = await this.DownloadVideoUrls( * ['http://example.com/video1.mp4', 'http://example.com/video2.mp4'], * task, * bookTaskDetail, * 'MJ' * ); * console.log('主视频路径:', result.outVideoPath); * console.log('所有视频路径:', result.subVideoPath); * ``` * * @description * 处理流程: * 1. 初始化服务并获取项目路径 * 2. 遍历每个视频URL进行下载 * 3. 根据任务类型决定是否使用转存服务 * 4. 创建本地存储目录并下载文件 * 5. 将第一个视频复制为主输出视频 * 6. 更新数据库中的路径信息 * * @note * - MidJourney官方CDN (cdn.midjourney.com) 的视频不支持转存 * - 可灵视频 (KLING_VIDEO, KLING_VIDEO_EXTEND) 不使用转存服务 * - 转存服务需要全局machineId配置 * - 视频文件按时间戳和索引命名以避免冲突 */ async DownloadVideoUrls(videoUrls: string[], task: TaskModal.Task, bookTaskDetail: Book.SelectBookTaskDetail, preffix: string, videoIds?: string[]): Promise<{ outVideoPath: string, subVideoPath: string[] }> { this.CheckInit() || await this.InitBookBasicHandle() if (videoIds != undefined && videoIds.length != videoUrls.length) { throw new Error(t("视频ID数量与视频链接数量不匹配")) } let bookTask = await this.bookTaskService.GetBookTaskDataById( bookTaskDetail.bookTaskId as string, true ) let tempVideoUrls = bookTaskDetail.subVideoPath || [] let newVideoUrls: string[] = [] let outVideoPath: string = '' const project_path = await getProjectPath() // 开始下载所有视频 for (let i = 0; i < videoUrls.length; i++) { const videoUrl = videoUrls[i] // 处理文件地址和下载 let videoPath = path.join( bookTask.imageFolder as string, `video/subVideo/${bookTaskDetail.name}/${new Date().getTime()}_${i}.mp4` ) let remoteUrl = videoUrl // 开始处理下载 mj 官方的图片不支持转存 if (global.machineId && !isEmpty(global.machineId) && !videoUrl.startsWith('https://cdn.midjourney.com') && task.type != BookBackTaskType.KLING_VIDEO && task.type != BookBackTaskType.KLING_VIDEO_EXTEND ) { // 转存一下视频文件 // 获取当前url的文件名 let fileName = preffix + "_" + path.basename(videoUrl) let transferRes = await axios.post(define.lms_url + `/lms/FileUpload/UrlUpload/${global.machineId}`, { url: videoUrl, fileName: fileName }) if (transferRes.status == 200 && transferRes.data.code == 1) { remoteUrl = transferRes.data.data.url } } if (isEmpty(remoteUrl)) { remoteUrl = videoUrl } await CheckFolderExistsOrCreate(path.dirname(videoPath)) await DownloadFile(remoteUrl, videoPath) // 处理返回数据信息 // 开始修改信息 // 将信息添加到里面 let a = { localPath: path.relative(project_path, videoPath), remotePath: remoteUrl, taskId: bookTaskDetail.videoMessage?.taskId, videoId: videoIds != undefined && videoIds[i] ? videoIds[i] : "", index: i, type: MappingTaskTypeToVideoModel(task.type as string) } newVideoUrls.push(JSON.stringify(a)) if (i == 0) { outVideoPath = path.join( bookTask.imageFolder as string, 'video', bookTaskDetail.name + path.extname(videoPath) ) await CopyFileOrFolder(videoPath, outVideoPath as string) } } // 开始处理数据 // 将原有的视频路径合并到新数组中 newVideoUrls.push(...tempVideoUrls) await this.bookTaskDetailService.ModifyBookTaskDetailById(bookTaskDetail.id as string, { subVideoPath: newVideoUrls, generateVideoPath: outVideoPath != '' ? outVideoPath : '' }) return { outVideoPath: outVideoPath, subVideoPath: newVideoUrls } } }