新增数据库实体类与服务实现,将内存存储扩展为支持数据库的完整实现,包括: 1. 新增Drama和Episode实体类,添加多个业务字段 2. 重构DramasService实现数据库CRUD与业务逻辑 3. 升级相关服务类为异步方法适配数据库操作 4. 更新测试用例适配异步接口变更 5. 完善分页、数据转换等辅助工具方法
216 lines
6.2 KiB
TypeScript
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;
|
|
}
|
|
}
|