Files
cyh-tk-backend/src/modules/media/media.service.ts
zhxiao1124 f342312b92 refactor: 重构项目支持TypeORM数据库持久化
新增数据库实体类与服务实现,将内存存储扩展为支持数据库的完整实现,包括:
1. 新增Drama和Episode实体类,添加多个业务字段
2. 重构DramasService实现数据库CRUD与业务逻辑
3. 升级相关服务类为异步方法适配数据库操作
4. 更新测试用例适配异步接口变更
5. 完善分页、数据转换等辅助工具方法
2026-07-01 21:49:36 +08:00

216 lines
6.2 KiB
TypeScript

import { Injectable, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
import { DramasService } from '../dramas/dramas.service';
import { CreateCoverDto } from './dto/create-cover.dto';
import { CreateDirectUploadTaskDto } from './dto/create-direct-upload-task.dto';
import { CreateUploadJobDto } from './dto/create-upload-job.dto';
import {
CoverRecord,
MediaUploadJobRecord,
} from './interfaces/media-records.interface';
import { BytePlusMediaProvider } from './providers/byteplus-media.provider';
interface PaginationInput {
page: number;
pageSize: number;
}
@Injectable()
export class MediaService {
private coverSequence = 1;
private uploadJobSequence = 1;
private readonly covers: CoverRecord[] = [];
private readonly uploadJobs: MediaUploadJobRecord[] = [];
constructor(
private readonly dramasService: DramasService,
private readonly bytePlusMediaProvider: BytePlusMediaProvider,
) {}
createCover(dto: CreateCoverDto): CoverRecord {
const cover: CoverRecord = {
id: this.coverSequence++,
fileName: dto.fileName,
url: dto.url,
createdAt: new Date().toISOString(),
};
this.covers.push(cover);
return cover;
}
createUploadJob(dto: CreateUploadJobDto): MediaUploadJobRecord {
const now = new Date().toISOString();
const job: MediaUploadJobRecord = {
id: this.uploadJobSequence++,
jobId: dto.jobId,
byteplusVid: dto.byteplusVid,
status: dto.status,
rawResponse: dto.rawResponse,
createdAt: now,
updatedAt: now,
};
this.uploadJobs.push(job);
return job;
}
async createDirectUploadTask(
dto: CreateDirectUploadTaskDto,
): Promise<MediaUploadJobRecord> {
await this.dramasService.getEpisodeOrThrow(dto.episodeId);
const now = new Date().toISOString();
const jobId = randomUUID();
const upload = this.bytePlusMediaProvider.createDirectUpload({
jobId,
fileName: dto.fileName,
contentType: dto.contentType,
});
const job: MediaUploadJobRecord = {
id: this.uploadJobSequence++,
jobId,
episodeId: dto.episodeId,
status: 'pending',
fileName: dto.fileName,
contentType: dto.contentType,
fileSize: dto.fileSize,
bucket: upload.bucket,
objectKey: upload.objectKey,
uploadUrl: upload.uploadUrl,
reviewStatus: 'not_submitted',
rawResponse: {
headers: upload.headers,
expiresAt: upload.expiresAt,
},
createdAt: now,
updatedAt: now,
};
this.uploadJobs.push(job);
return job;
}
async completeDirectUploadTask(jobId: string): Promise<MediaUploadJobRecord> {
const job = this.getUploadJobOrThrow(jobId);
if (!job.objectKey || !job.episodeId) {
throw new NotFoundException('Upload task not found');
}
const completed = this.bytePlusMediaProvider.completeUpload({
jobId: job.jobId,
objectKey: job.objectKey,
});
const review = this.bytePlusMediaProvider.submitReview(completed.byteplusVid);
Object.assign(job, {
byteplusVid: completed.byteplusVid,
videoUrl: completed.videoUrl,
width: completed.width,
height: completed.height,
durationSeconds: completed.durationSeconds,
status: review.reviewStatus,
reviewStatus: review.reviewStatus,
reviewMessage: review.reviewMessage,
rawResponse: {
...job.rawResponse,
upload: completed.rawResponse,
review,
},
updatedAt: new Date().toISOString(),
});
await this.dramasService.attachEpisodeMedia({
episodeId: job.episodeId,
byteplusVid: completed.byteplusVid,
videoUrl: completed.videoUrl,
storageBucket: job.bucket ?? '',
objectKey: job.objectKey,
durationSeconds: completed.durationSeconds,
width: completed.width,
height: completed.height,
reviewStatus: review.reviewStatus,
reviewMessage: review.reviewMessage,
});
return job;
}
async retryReview(jobId: string): Promise<MediaUploadJobRecord> {
const job = this.getUploadJobOrThrow(jobId);
if (!job.byteplusVid || !job.episodeId) {
throw new NotFoundException('Upload task not found');
}
const review = this.bytePlusMediaProvider.submitReview(job.byteplusVid);
job.status = review.reviewStatus;
job.reviewStatus = review.reviewStatus;
job.reviewMessage = review.reviewMessage;
job.updatedAt = new Date().toISOString();
await this.dramasService.updateEpisodeReviewStatus(
job.episodeId,
review.reviewStatus,
review.reviewMessage,
);
return job;
}
async syncReviewStatuses() {
const reviewingJobs = this.uploadJobs.filter(
(job) =>
job.byteplusVid &&
job.episodeId &&
(job.reviewStatus === 'reviewing' || job.reviewStatus === 'pending'),
);
for (const job of reviewingJobs) {
const review = this.bytePlusMediaProvider.queryReviewStatus(
job.byteplusVid as string,
);
job.status = review.reviewStatus;
job.reviewStatus = review.reviewStatus;
job.reviewMessage = review.reviewMessage;
job.updatedAt = new Date().toISOString();
await this.dramasService.updateEpisodeReviewStatus(
job.episodeId as number,
review.reviewStatus,
review.reviewMessage,
);
}
return {
status: 'completed',
synced: reviewingJobs.length,
};
}
getUploadTask(jobId: string): MediaUploadJobRecord {
return this.getUploadJobOrThrow(jobId);
}
listUploadJobs(
input: PaginationInput,
): PaginatedResponse<MediaUploadJobRecord> {
const page = Math.max(input.page, 1);
const pageSize = Math.min(Math.max(input.pageSize, 1), 100);
const start = (page - 1) * pageSize;
const records = [...this.uploadJobs].reverse();
return {
list: records.slice(start, start + pageSize),
pagination: {
page,
pageSize,
total: records.length,
},
};
}
private getUploadJobOrThrow(jobId: string) {
const job = this.uploadJobs.find((item) => item.jobId === jobId);
if (!job) {
throw new NotFoundException('Upload task not found');
}
return job;
}
}