|
|
|
|
@@ -2,14 +2,19 @@ import {
|
|
|
|
|
ConflictException,
|
|
|
|
|
Injectable,
|
|
|
|
|
NotFoundException,
|
|
|
|
|
Optional,
|
|
|
|
|
UnprocessableEntityException,
|
|
|
|
|
} from '@nestjs/common';
|
|
|
|
|
import { InjectDataSource } from '@nestjs/typeorm';
|
|
|
|
|
import { DataSource } from 'typeorm';
|
|
|
|
|
import { PaginatedResponse } from '../../common/dto/paginated-response.dto';
|
|
|
|
|
import { ConfigureEpisodesDto } from './dto/configure-episodes.dto';
|
|
|
|
|
import { CreateDramaDto } from './dto/create-drama.dto';
|
|
|
|
|
import { CreateEpisodeDto } from './dto/create-episode.dto';
|
|
|
|
|
import { UpdateDramaDto } from './dto/update-drama.dto';
|
|
|
|
|
import { UpdateEpisodeDto } from './dto/update-episode.dto';
|
|
|
|
|
import { Drama } from './entities/drama.entity';
|
|
|
|
|
import { Episode } from './entities/episode.entity';
|
|
|
|
|
import { PublishStatus } from './drama-status.enum';
|
|
|
|
|
import {
|
|
|
|
|
DramaRecord,
|
|
|
|
|
@@ -28,7 +33,37 @@ export class DramasService {
|
|
|
|
|
private readonly dramas: DramaRecord[] = [];
|
|
|
|
|
private readonly episodes: EpisodeRecord[] = [];
|
|
|
|
|
|
|
|
|
|
createDrama(dto: CreateDramaDto): DramaRecord {
|
|
|
|
|
constructor(
|
|
|
|
|
@Optional()
|
|
|
|
|
@InjectDataSource()
|
|
|
|
|
readonly dataSource?: DataSource,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async createDrama(dto: CreateDramaDto): Promise<DramaRecord> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const repository = this.dataSource.getRepository(Drama);
|
|
|
|
|
const entity = repository.create({
|
|
|
|
|
tiktokAlbumId: dto.tiktokAlbumId,
|
|
|
|
|
title: dto.title,
|
|
|
|
|
description: dto.description,
|
|
|
|
|
coverId: dto.coverId,
|
|
|
|
|
coverUrl: dto.coverUrl,
|
|
|
|
|
portraitCoverUrl: dto.portraitCoverUrl,
|
|
|
|
|
landscapeCoverUrl: dto.landscapeCoverUrl,
|
|
|
|
|
type: dto.type,
|
|
|
|
|
tags: dto.tags ?? [],
|
|
|
|
|
region: dto.region,
|
|
|
|
|
language: dto.language,
|
|
|
|
|
year: dto.year,
|
|
|
|
|
status: dto.status ?? PublishStatus.Draft,
|
|
|
|
|
sortOrder: dto.sortOrder ?? 0,
|
|
|
|
|
isRecommended: dto.isRecommended ?? false,
|
|
|
|
|
onlineVersion: dto.onlineVersion ?? 1,
|
|
|
|
|
});
|
|
|
|
|
const saved = await repository.save(entity);
|
|
|
|
|
return this.toDramaRecord(saved, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const now = new Date().toISOString();
|
|
|
|
|
const drama: DramaRecord = {
|
|
|
|
|
id: this.dramaSequence++,
|
|
|
|
|
@@ -58,11 +93,41 @@ export class DramasService {
|
|
|
|
|
return drama;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listAdminDramas(pagination: PaginationInput): PaginatedResponse<DramaRecord> {
|
|
|
|
|
return this.paginate(this.sortDramas(this.dramas.map((drama) => this.withEpisodeCounts(drama))), pagination);
|
|
|
|
|
async listAdminDramas(
|
|
|
|
|
pagination: PaginationInput,
|
|
|
|
|
): Promise<PaginatedResponse<DramaRecord>> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const { page, pageSize, skip, take } = this.resolvePagination(pagination);
|
|
|
|
|
const [records, total] = await this.dataSource
|
|
|
|
|
.getRepository(Drama)
|
|
|
|
|
.findAndCount({
|
|
|
|
|
order: { sortOrder: 'DESC', id: 'DESC' },
|
|
|
|
|
skip,
|
|
|
|
|
take,
|
|
|
|
|
});
|
|
|
|
|
const list = await Promise.all(
|
|
|
|
|
records.map((drama) => this.toDramaRecordWithCounts(drama)),
|
|
|
|
|
);
|
|
|
|
|
return { list, pagination: { page, pageSize, total } };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.paginate(
|
|
|
|
|
this.sortDramas(this.dramas.map((drama) => this.withEpisodeCounts(drama))),
|
|
|
|
|
pagination,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getDramaOrThrow(id: number): DramaRecord {
|
|
|
|
|
async getDramaOrThrow(id: number): Promise<DramaRecord> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const drama = await this.dataSource
|
|
|
|
|
.getRepository(Drama)
|
|
|
|
|
.findOne({ where: { id } });
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
}
|
|
|
|
|
return this.toDramaRecordWithCounts(drama);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const drama = this.dramas.find((item) => item.id === id);
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
@@ -71,7 +136,18 @@ export class DramasService {
|
|
|
|
|
return this.withEpisodeCounts(drama);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateDrama(id: number, dto: UpdateDramaDto): DramaRecord {
|
|
|
|
|
async updateDrama(id: number, dto: UpdateDramaDto): Promise<DramaRecord> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const repository = this.dataSource.getRepository(Drama);
|
|
|
|
|
const drama = await repository.findOne({ where: { id } });
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
}
|
|
|
|
|
repository.merge(drama, { ...dto, tags: dto.tags ?? drama.tags });
|
|
|
|
|
const saved = await repository.save(drama);
|
|
|
|
|
return this.toDramaRecordWithCounts(saved);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const drama = this.dramas.find((item) => item.id === id);
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
@@ -85,30 +161,78 @@ export class DramasService {
|
|
|
|
|
return this.withEpisodeCounts(drama);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createEpisode(dto: CreateEpisodeDto): EpisodeRecord {
|
|
|
|
|
async createEpisode(dto: CreateEpisodeDto): Promise<EpisodeRecord> {
|
|
|
|
|
const dramaId = dto.dramaId ?? dto.albumId;
|
|
|
|
|
const episodeNo = dto.episodeNo ?? dto.seq;
|
|
|
|
|
if (!dramaId || !episodeNo) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const freeType = dto.freeType ?? (dto.isPaid ? 'paid' : 'free');
|
|
|
|
|
const price = dto.price ?? dto.unlockPrice ?? 0;
|
|
|
|
|
const publishStatus = dto.publishStatus ?? dto.status ?? PublishStatus.Draft;
|
|
|
|
|
const reviewStatus =
|
|
|
|
|
dto.reviewStatus ??
|
|
|
|
|
(publishStatus === PublishStatus.Published ? 'approved' : 'not_submitted');
|
|
|
|
|
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const dramaRepository = this.dataSource.getRepository(Drama);
|
|
|
|
|
const drama = await dramaRepository.findOne({ where: { id: dramaId } });
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const episodeRepository = this.dataSource.getRepository(Episode);
|
|
|
|
|
const duplicate = await episodeRepository.findOne({
|
|
|
|
|
where: { dramaId, episodeNo },
|
|
|
|
|
});
|
|
|
|
|
if (duplicate) {
|
|
|
|
|
throw new ConflictException(
|
|
|
|
|
'Episode number already exists in this drama',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const entity = episodeRepository.create({
|
|
|
|
|
dramaId,
|
|
|
|
|
tiktokEpisodeId: dto.tiktokEpisodeId,
|
|
|
|
|
episodeNo,
|
|
|
|
|
title: dto.title,
|
|
|
|
|
description: dto.description,
|
|
|
|
|
byteplusVid: dto.byteplusVid,
|
|
|
|
|
coverId: dto.coverId,
|
|
|
|
|
coverUrl: dto.coverUrl ?? '',
|
|
|
|
|
videoUrl: dto.videoUrl,
|
|
|
|
|
trialDurationSeconds: dto.trialDurationSeconds,
|
|
|
|
|
durationSeconds: dto.durationSeconds ?? 0,
|
|
|
|
|
width: dto.width,
|
|
|
|
|
height: dto.height,
|
|
|
|
|
isPaid: freeType === 'paid',
|
|
|
|
|
freeType,
|
|
|
|
|
unlockPrice: price,
|
|
|
|
|
price,
|
|
|
|
|
publishAt: dto.publishAt ? new Date(dto.publishAt) : undefined,
|
|
|
|
|
nextReleaseAt: dto.nextReleaseAt ? new Date(dto.nextReleaseAt) : undefined,
|
|
|
|
|
status: publishStatus,
|
|
|
|
|
reviewStatus,
|
|
|
|
|
publishStatus,
|
|
|
|
|
});
|
|
|
|
|
const saved = await episodeRepository.save(entity);
|
|
|
|
|
return this.toEpisodeRecord(saved);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const drama = this.dramas.find((item) => item.id === dramaId);
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const duplicate = this.episodes.some(
|
|
|
|
|
(item) =>
|
|
|
|
|
item.dramaId === dramaId && item.episodeNo === episodeNo,
|
|
|
|
|
(item) => item.dramaId === dramaId && item.episodeNo === episodeNo,
|
|
|
|
|
);
|
|
|
|
|
if (duplicate) {
|
|
|
|
|
throw new ConflictException('Episode number already exists in this drama');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const now = new Date().toISOString();
|
|
|
|
|
const freeType = dto.freeType ?? (dto.isPaid ? 'paid' : 'free');
|
|
|
|
|
const price = dto.price ?? dto.unlockPrice ?? 0;
|
|
|
|
|
const publishStatus = dto.publishStatus ?? dto.status ?? PublishStatus.Draft;
|
|
|
|
|
const episode: EpisodeRecord = {
|
|
|
|
|
id: this.episodeSequence++,
|
|
|
|
|
dramaId,
|
|
|
|
|
@@ -133,9 +257,7 @@ export class DramasService {
|
|
|
|
|
publishAt: dto.publishAt,
|
|
|
|
|
nextReleaseAt: dto.nextReleaseAt,
|
|
|
|
|
status: publishStatus,
|
|
|
|
|
reviewStatus:
|
|
|
|
|
dto.reviewStatus ??
|
|
|
|
|
(publishStatus === PublishStatus.Published ? 'approved' : 'not_submitted'),
|
|
|
|
|
reviewStatus,
|
|
|
|
|
publishStatus,
|
|
|
|
|
createdAt: now,
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
@@ -145,10 +267,69 @@ export class DramasService {
|
|
|
|
|
return episode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
configureEpisodes(
|
|
|
|
|
async configureEpisodes(
|
|
|
|
|
dramaId: number,
|
|
|
|
|
dto: ConfigureEpisodesDto,
|
|
|
|
|
): PaginatedResponse<EpisodeRecord> {
|
|
|
|
|
): Promise<PaginatedResponse<EpisodeRecord>> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const dramaRepository = this.dataSource.getRepository(Drama);
|
|
|
|
|
const drama = await dramaRepository.findOne({ where: { id: dramaId } });
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const episodeRepository = this.dataSource.getRepository(Episode);
|
|
|
|
|
const existing = await episodeRepository.find({
|
|
|
|
|
where: { dramaId },
|
|
|
|
|
order: { episodeNo: 'ASC' },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (dto.episodeCount < existing.length) {
|
|
|
|
|
const removable = existing.slice(dto.episodeCount);
|
|
|
|
|
const hasLockedEpisode = removable.some(
|
|
|
|
|
(episode) =>
|
|
|
|
|
episode.byteplusVid ||
|
|
|
|
|
episode.publishStatus !== PublishStatus.Draft ||
|
|
|
|
|
episode.reviewStatus !== 'not_submitted',
|
|
|
|
|
);
|
|
|
|
|
if (hasLockedEpisode) {
|
|
|
|
|
throw new UnprocessableEntityException(
|
|
|
|
|
'Only empty draft tail episodes can be removed',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
await episodeRepository.remove(removable);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (
|
|
|
|
|
let episodeNo = existing.length + 1;
|
|
|
|
|
episodeNo <= dto.episodeCount;
|
|
|
|
|
episodeNo++
|
|
|
|
|
) {
|
|
|
|
|
await this.createEpisode({
|
|
|
|
|
dramaId,
|
|
|
|
|
episodeNo,
|
|
|
|
|
title: `第${episodeNo}集`,
|
|
|
|
|
coverUrl: drama.coverUrl,
|
|
|
|
|
durationSeconds: 0,
|
|
|
|
|
freeType: 'free',
|
|
|
|
|
price: 0,
|
|
|
|
|
reviewStatus: 'not_submitted',
|
|
|
|
|
publishStatus: PublishStatus.Draft,
|
|
|
|
|
status: PublishStatus.Draft,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const list = await episodeRepository.find({
|
|
|
|
|
where: { dramaId },
|
|
|
|
|
order: { episodeNo: 'ASC' },
|
|
|
|
|
});
|
|
|
|
|
const records = list.map((episode) => this.toEpisodeRecord(episode));
|
|
|
|
|
return {
|
|
|
|
|
list: records,
|
|
|
|
|
pagination: { page: 1, pageSize: records.length, total: records.length },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const drama = this.dramas.find((item) => item.id === dramaId);
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
@@ -179,8 +360,12 @@ export class DramasService {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let episodeNo = existing.length + 1; episodeNo <= dto.episodeCount; episodeNo++) {
|
|
|
|
|
this.createEpisode({
|
|
|
|
|
for (
|
|
|
|
|
let episodeNo = existing.length + 1;
|
|
|
|
|
episodeNo <= dto.episodeCount;
|
|
|
|
|
episodeNo++
|
|
|
|
|
) {
|
|
|
|
|
await this.createEpisode({
|
|
|
|
|
dramaId,
|
|
|
|
|
episodeNo,
|
|
|
|
|
title: `第${episodeNo}集`,
|
|
|
|
|
@@ -199,28 +384,79 @@ export class DramasService {
|
|
|
|
|
.sort((left, right) => left.episodeNo - right.episodeNo);
|
|
|
|
|
return {
|
|
|
|
|
list,
|
|
|
|
|
pagination: {
|
|
|
|
|
page: 1,
|
|
|
|
|
pageSize: list.length,
|
|
|
|
|
total: list.length,
|
|
|
|
|
},
|
|
|
|
|
pagination: { page: 1, pageSize: list.length, total: list.length },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listAdminEpisodes(
|
|
|
|
|
async listAdminEpisodes(
|
|
|
|
|
dramaId: number,
|
|
|
|
|
pagination: PaginationInput,
|
|
|
|
|
): PaginatedResponse<EpisodeRecord> {
|
|
|
|
|
this.getDramaOrThrow(dramaId);
|
|
|
|
|
): Promise<PaginatedResponse<EpisodeRecord>> {
|
|
|
|
|
await this.getDramaOrThrow(dramaId);
|
|
|
|
|
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const { page, pageSize, skip, take } = this.resolvePagination(pagination);
|
|
|
|
|
const [records, total] = await this.dataSource
|
|
|
|
|
.getRepository(Episode)
|
|
|
|
|
.findAndCount({
|
|
|
|
|
where: { dramaId },
|
|
|
|
|
order: { episodeNo: 'ASC' },
|
|
|
|
|
skip,
|
|
|
|
|
take,
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
list: records.map((episode) => this.toEpisodeRecord(episode)),
|
|
|
|
|
pagination: { page, pageSize, total },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const records = this.episodes
|
|
|
|
|
.filter((episode) => episode.dramaId === dramaId)
|
|
|
|
|
.sort((left, right) => left.episodeNo - right.episodeNo);
|
|
|
|
|
return this.paginate(records, pagination);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateEpisode(id: number, dto: UpdateEpisodeDto): EpisodeRecord {
|
|
|
|
|
const episode = this.getEpisodeOrThrow(id);
|
|
|
|
|
const freeType = dto.freeType ?? (dto.isPaid === undefined ? episode.freeType : dto.isPaid ? 'paid' : 'free');
|
|
|
|
|
async updateEpisode(id: number, dto: UpdateEpisodeDto): Promise<EpisodeRecord> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const repository = this.dataSource.getRepository(Episode);
|
|
|
|
|
const episode = await repository.findOne({ where: { id } });
|
|
|
|
|
if (!episode) {
|
|
|
|
|
throw new NotFoundException('Episode not found');
|
|
|
|
|
}
|
|
|
|
|
const freeType =
|
|
|
|
|
dto.freeType ??
|
|
|
|
|
(dto.isPaid === undefined
|
|
|
|
|
? episode.freeType
|
|
|
|
|
: dto.isPaid
|
|
|
|
|
? 'paid'
|
|
|
|
|
: 'free');
|
|
|
|
|
repository.merge(episode, {
|
|
|
|
|
...dto,
|
|
|
|
|
dramaId: dto.dramaId ?? dto.albumId ?? episode.dramaId,
|
|
|
|
|
episodeNo: dto.episodeNo ?? dto.seq ?? episode.episodeNo,
|
|
|
|
|
freeType,
|
|
|
|
|
isPaid: freeType === 'paid',
|
|
|
|
|
price: dto.price ?? dto.unlockPrice ?? episode.price,
|
|
|
|
|
unlockPrice: dto.unlockPrice ?? dto.price ?? episode.unlockPrice,
|
|
|
|
|
status: dto.status ?? dto.publishStatus ?? episode.status,
|
|
|
|
|
publishStatus: dto.publishStatus ?? dto.status ?? episode.publishStatus,
|
|
|
|
|
publishAt: dto.publishAt ? new Date(dto.publishAt) : episode.publishAt,
|
|
|
|
|
nextReleaseAt: dto.nextReleaseAt
|
|
|
|
|
? new Date(dto.nextReleaseAt)
|
|
|
|
|
: episode.nextReleaseAt,
|
|
|
|
|
});
|
|
|
|
|
const saved = await repository.save(episode);
|
|
|
|
|
return this.toEpisodeRecord(saved);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const episode = await this.getMemEpisodeOrThrow(id);
|
|
|
|
|
const freeType =
|
|
|
|
|
dto.freeType ??
|
|
|
|
|
(dto.isPaid === undefined
|
|
|
|
|
? episode.freeType
|
|
|
|
|
: dto.isPaid
|
|
|
|
|
? 'paid'
|
|
|
|
|
: 'free');
|
|
|
|
|
Object.assign(episode, {
|
|
|
|
|
...dto,
|
|
|
|
|
dramaId: dto.dramaId ?? dto.albumId ?? episode.dramaId,
|
|
|
|
|
@@ -238,7 +474,7 @@ export class DramasService {
|
|
|
|
|
return episode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attachEpisodeMedia(input: {
|
|
|
|
|
async attachEpisodeMedia(input: {
|
|
|
|
|
episodeId: number;
|
|
|
|
|
byteplusVid: string;
|
|
|
|
|
videoUrl: string;
|
|
|
|
|
@@ -249,8 +485,29 @@ export class DramasService {
|
|
|
|
|
height: number;
|
|
|
|
|
reviewStatus: EpisodeRecord['reviewStatus'];
|
|
|
|
|
reviewMessage?: string;
|
|
|
|
|
}): EpisodeRecord {
|
|
|
|
|
const episode = this.getEpisodeOrThrow(input.episodeId);
|
|
|
|
|
}): Promise<EpisodeRecord> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const repository = this.dataSource.getRepository(Episode);
|
|
|
|
|
const episode = await repository.findOne({
|
|
|
|
|
where: { id: input.episodeId },
|
|
|
|
|
});
|
|
|
|
|
if (!episode) {
|
|
|
|
|
throw new NotFoundException('Episode not found');
|
|
|
|
|
}
|
|
|
|
|
episode.byteplusVid = input.byteplusVid;
|
|
|
|
|
episode.videoUrl = input.videoUrl;
|
|
|
|
|
episode.storageBucket = input.storageBucket;
|
|
|
|
|
episode.objectKey = input.objectKey;
|
|
|
|
|
episode.durationSeconds = input.durationSeconds;
|
|
|
|
|
episode.width = input.width;
|
|
|
|
|
episode.height = input.height;
|
|
|
|
|
episode.reviewStatus = input.reviewStatus;
|
|
|
|
|
episode.reviewMessage = input.reviewMessage;
|
|
|
|
|
const saved = await repository.save(episode);
|
|
|
|
|
return this.toEpisodeRecord(saved);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const episode = await this.getMemEpisodeOrThrow(input.episodeId);
|
|
|
|
|
episode.byteplusVid = input.byteplusVid;
|
|
|
|
|
episode.videoUrl = input.videoUrl;
|
|
|
|
|
episode.storageBucket = input.storageBucket;
|
|
|
|
|
@@ -264,21 +521,49 @@ export class DramasService {
|
|
|
|
|
return episode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateEpisodeReviewStatus(
|
|
|
|
|
async updateEpisodeReviewStatus(
|
|
|
|
|
episodeId: number,
|
|
|
|
|
reviewStatus: EpisodeRecord['reviewStatus'],
|
|
|
|
|
reviewMessage?: string,
|
|
|
|
|
): EpisodeRecord {
|
|
|
|
|
const episode = this.getEpisodeOrThrow(episodeId);
|
|
|
|
|
): Promise<EpisodeRecord> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const repository = this.dataSource.getRepository(Episode);
|
|
|
|
|
const episode = await repository.findOne({ where: { id: episodeId } });
|
|
|
|
|
if (!episode) {
|
|
|
|
|
throw new NotFoundException('Episode not found');
|
|
|
|
|
}
|
|
|
|
|
episode.reviewStatus = reviewStatus;
|
|
|
|
|
episode.reviewMessage = reviewMessage;
|
|
|
|
|
const saved = await repository.save(episode);
|
|
|
|
|
return this.toEpisodeRecord(saved);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const episode = await this.getMemEpisodeOrThrow(episodeId);
|
|
|
|
|
episode.reviewStatus = reviewStatus;
|
|
|
|
|
episode.reviewMessage = reviewMessage;
|
|
|
|
|
episode.updatedAt = new Date().toISOString();
|
|
|
|
|
return episode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listPublishedDramas(
|
|
|
|
|
async listPublishedDramas(
|
|
|
|
|
pagination: PaginationInput,
|
|
|
|
|
): PaginatedResponse<DramaRecord> {
|
|
|
|
|
): Promise<PaginatedResponse<DramaRecord>> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const { page, pageSize, skip, take } = this.resolvePagination(pagination);
|
|
|
|
|
const [records, total] = await this.dataSource
|
|
|
|
|
.getRepository(Drama)
|
|
|
|
|
.findAndCount({
|
|
|
|
|
where: { status: PublishStatus.Published },
|
|
|
|
|
order: { sortOrder: 'DESC', id: 'DESC' },
|
|
|
|
|
skip,
|
|
|
|
|
take,
|
|
|
|
|
});
|
|
|
|
|
const list = await Promise.all(
|
|
|
|
|
records.map((drama) => this.toDramaRecordWithCounts(drama)),
|
|
|
|
|
);
|
|
|
|
|
return { list, pagination: { page, pageSize, total } };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.paginate(
|
|
|
|
|
this.sortDramas(
|
|
|
|
|
this.dramas
|
|
|
|
|
@@ -289,7 +574,17 @@ export class DramasService {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getPublishedDramaOrThrow(id: number): DramaRecord {
|
|
|
|
|
async getPublishedDramaOrThrow(id: number): Promise<DramaRecord> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const drama = await this.dataSource
|
|
|
|
|
.getRepository(Drama)
|
|
|
|
|
.findOne({ where: { id, status: PublishStatus.Published } });
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
}
|
|
|
|
|
return this.toDramaRecordWithCounts(drama);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const drama = this.dramas.find(
|
|
|
|
|
(item) => item.id === id && item.status === PublishStatus.Published,
|
|
|
|
|
);
|
|
|
|
|
@@ -300,10 +595,37 @@ export class DramasService {
|
|
|
|
|
return this.withEpisodeCounts(drama);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listPublishedEpisodes(
|
|
|
|
|
async listPublishedEpisodes(
|
|
|
|
|
dramaId: number,
|
|
|
|
|
pagination: PaginationInput,
|
|
|
|
|
): PaginatedResponse<EpisodeRecord> {
|
|
|
|
|
): Promise<PaginatedResponse<EpisodeRecord>> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const drama = await this.dataSource
|
|
|
|
|
.getRepository(Drama)
|
|
|
|
|
.findOne({ where: { id: dramaId, status: PublishStatus.Published } });
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
}
|
|
|
|
|
const { page, pageSize, skip, take } = this.resolvePagination(pagination);
|
|
|
|
|
const [records, total] = await this.dataSource
|
|
|
|
|
.getRepository(Episode)
|
|
|
|
|
.findAndCount({
|
|
|
|
|
where: {
|
|
|
|
|
dramaId,
|
|
|
|
|
status: PublishStatus.Published,
|
|
|
|
|
publishStatus: PublishStatus.Published,
|
|
|
|
|
reviewStatus: 'approved',
|
|
|
|
|
},
|
|
|
|
|
order: { episodeNo: 'ASC' },
|
|
|
|
|
skip,
|
|
|
|
|
take,
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
list: records.map((episode) => this.toEpisodeRecord(episode)),
|
|
|
|
|
pagination: { page, pageSize, total },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const drama = this.dramas.find((item) => item.id === dramaId);
|
|
|
|
|
if (!drama || drama.status !== PublishStatus.Published) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
@@ -324,7 +646,30 @@ export class DramasService {
|
|
|
|
|
return this.paginate(episodes, pagination);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getPublishedEpisodeOrThrow(episodeId: number): EpisodeRecord {
|
|
|
|
|
async getPublishedEpisodeOrThrow(episodeId: number): Promise<EpisodeRecord> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const episode = await this.dataSource.getRepository(Episode).findOne({
|
|
|
|
|
where: {
|
|
|
|
|
id: episodeId,
|
|
|
|
|
status: PublishStatus.Published,
|
|
|
|
|
publishStatus: PublishStatus.Published,
|
|
|
|
|
reviewStatus: 'approved',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
if (!episode) {
|
|
|
|
|
throw new NotFoundException('Episode not found');
|
|
|
|
|
}
|
|
|
|
|
const drama = await this.dataSource
|
|
|
|
|
.getRepository(Drama)
|
|
|
|
|
.findOne({
|
|
|
|
|
where: { id: episode.dramaId, status: PublishStatus.Published },
|
|
|
|
|
});
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Episode not found');
|
|
|
|
|
}
|
|
|
|
|
return this.toEpisodeRecord(episode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const episode = this.episodes.find((item) => item.id === episodeId);
|
|
|
|
|
if (
|
|
|
|
|
!episode ||
|
|
|
|
|
@@ -343,16 +688,39 @@ export class DramasService {
|
|
|
|
|
return episode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getEpisodeOrThrow(episodeId: number): EpisodeRecord {
|
|
|
|
|
const episode = this.episodes.find((item) => item.id === episodeId);
|
|
|
|
|
if (!episode) {
|
|
|
|
|
throw new NotFoundException('Episode not found');
|
|
|
|
|
async getEpisodeOrThrow(episodeId: number): Promise<EpisodeRecord> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const episode = await this.dataSource
|
|
|
|
|
.getRepository(Episode)
|
|
|
|
|
.findOne({ where: { id: episodeId } });
|
|
|
|
|
if (!episode) {
|
|
|
|
|
throw new NotFoundException('Episode not found');
|
|
|
|
|
}
|
|
|
|
|
return this.toEpisodeRecord(episode);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return episode;
|
|
|
|
|
return this.getMemEpisodeOrThrow(episodeId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
submitReview(albumId: number) {
|
|
|
|
|
async submitReview(albumId: number) {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const dramaRepository = this.dataSource.getRepository(Drama);
|
|
|
|
|
const drama = await dramaRepository.findOne({ where: { id: albumId } });
|
|
|
|
|
if (!drama) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
}
|
|
|
|
|
const episodeRepository = this.dataSource.getRepository(Episode);
|
|
|
|
|
const episodes = await episodeRepository.find({
|
|
|
|
|
where: { dramaId: albumId },
|
|
|
|
|
});
|
|
|
|
|
for (const episode of episodes) {
|
|
|
|
|
episode.reviewStatus = 'approved';
|
|
|
|
|
episode.reviewMessage = 'Mock review approved';
|
|
|
|
|
}
|
|
|
|
|
await episodeRepository.save(episodes);
|
|
|
|
|
return { albumId, reviewStatus: 'approved' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const album = this.dramas.find((item) => item.id === albumId);
|
|
|
|
|
if (!album) {
|
|
|
|
|
throw new NotFoundException('Drama not found');
|
|
|
|
|
@@ -366,28 +734,167 @@ export class DramasService {
|
|
|
|
|
episode.updatedAt = new Date().toISOString();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
albumId,
|
|
|
|
|
reviewStatus: 'approved',
|
|
|
|
|
};
|
|
|
|
|
return { albumId, reviewStatus: 'approved' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listAllDramas() {
|
|
|
|
|
async listAllDramas(): Promise<DramaRecord[]> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const records = await this.dataSource
|
|
|
|
|
.getRepository(Drama)
|
|
|
|
|
.find({ order: { sortOrder: 'DESC', id: 'DESC' } });
|
|
|
|
|
return Promise.all(records.map((drama) => this.toDramaRecordWithCounts(drama)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.sortDramas(this.dramas.map((drama) => this.withEpisodeCounts(drama)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listAllEpisodes() {
|
|
|
|
|
async listAllEpisodes(): Promise<EpisodeRecord[]> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const records = await this.dataSource
|
|
|
|
|
.getRepository(Episode)
|
|
|
|
|
.find({ order: { id: 'ASC' } });
|
|
|
|
|
return records.map((episode) => this.toEpisodeRecord(episode));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [...this.episodes].sort((left, right) => left.id - right.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateEpisodePublishStatus(episodeId: number, publishStatus: PublishStatus) {
|
|
|
|
|
const episode = this.getEpisodeOrThrow(episodeId);
|
|
|
|
|
async updateEpisodePublishStatus(
|
|
|
|
|
episodeId: number,
|
|
|
|
|
publishStatus: PublishStatus,
|
|
|
|
|
): Promise<EpisodeRecord> {
|
|
|
|
|
if (this.hasDatabase()) {
|
|
|
|
|
const repository = this.dataSource.getRepository(Episode);
|
|
|
|
|
const episode = await repository.findOne({ where: { id: episodeId } });
|
|
|
|
|
if (!episode) {
|
|
|
|
|
throw new NotFoundException('Episode not found');
|
|
|
|
|
}
|
|
|
|
|
episode.publishStatus = publishStatus;
|
|
|
|
|
episode.status = publishStatus;
|
|
|
|
|
const saved = await repository.save(episode);
|
|
|
|
|
return this.toEpisodeRecord(saved);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const episode = await this.getMemEpisodeOrThrow(episodeId);
|
|
|
|
|
episode.publishStatus = publishStatus;
|
|
|
|
|
episode.status = publishStatus;
|
|
|
|
|
episode.updatedAt = new Date().toISOString();
|
|
|
|
|
return episode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private hasDatabase(): this is this & { dataSource: DataSource } {
|
|
|
|
|
return Boolean(this.dataSource?.isInitialized);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getMemEpisodeOrThrow(episodeId: number): EpisodeRecord {
|
|
|
|
|
const episode = this.episodes.find((item) => item.id === episodeId);
|
|
|
|
|
if (!episode) {
|
|
|
|
|
throw new NotFoundException('Episode not found');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return episode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async toDramaRecordWithCounts(entity: Drama): Promise<DramaRecord> {
|
|
|
|
|
const episodeRepository = this.dataSource!.getRepository(Episode);
|
|
|
|
|
const dramaId = Number(entity.id);
|
|
|
|
|
const totalEpisodes = await episodeRepository.count({ where: { dramaId } });
|
|
|
|
|
const publishedEpisodes = await episodeRepository.count({
|
|
|
|
|
where: {
|
|
|
|
|
dramaId,
|
|
|
|
|
publishStatus: PublishStatus.Published,
|
|
|
|
|
reviewStatus: 'approved',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return this.toDramaRecord(entity, totalEpisodes, publishedEpisodes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toDramaRecord(
|
|
|
|
|
entity: Drama,
|
|
|
|
|
totalEpisodes: number,
|
|
|
|
|
publishedEpisodes: number,
|
|
|
|
|
): DramaRecord {
|
|
|
|
|
return {
|
|
|
|
|
id: Number(entity.id),
|
|
|
|
|
tiktokAlbumId: entity.tiktokAlbumId,
|
|
|
|
|
title: entity.title,
|
|
|
|
|
description: entity.description,
|
|
|
|
|
coverId: entity.coverId,
|
|
|
|
|
coverUrl: entity.coverUrl,
|
|
|
|
|
portraitCoverUrl: entity.portraitCoverUrl,
|
|
|
|
|
landscapeCoverUrl: entity.landscapeCoverUrl,
|
|
|
|
|
type: entity.type,
|
|
|
|
|
tags: entity.tags ?? [],
|
|
|
|
|
region: entity.region,
|
|
|
|
|
language: entity.language,
|
|
|
|
|
year: entity.year,
|
|
|
|
|
status: entity.status,
|
|
|
|
|
sortOrder: entity.sortOrder,
|
|
|
|
|
isRecommended: entity.isRecommended,
|
|
|
|
|
onlineVersion: entity.onlineVersion,
|
|
|
|
|
totalEpisodes,
|
|
|
|
|
publishedEpisodes,
|
|
|
|
|
createdAt: this.toIso(entity.createdAt),
|
|
|
|
|
updatedAt: this.toIso(entity.updatedAt),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toEpisodeRecord(entity: Episode): EpisodeRecord {
|
|
|
|
|
const dramaId = Number(entity.dramaId);
|
|
|
|
|
return {
|
|
|
|
|
id: Number(entity.id),
|
|
|
|
|
dramaId,
|
|
|
|
|
albumId: dramaId,
|
|
|
|
|
tiktokEpisodeId: entity.tiktokEpisodeId,
|
|
|
|
|
episodeNo: entity.episodeNo,
|
|
|
|
|
seq: entity.episodeNo,
|
|
|
|
|
title: entity.title,
|
|
|
|
|
description: entity.description,
|
|
|
|
|
byteplusVid: entity.byteplusVid,
|
|
|
|
|
coverId: entity.coverId,
|
|
|
|
|
coverUrl: entity.coverUrl,
|
|
|
|
|
videoUrl: entity.videoUrl,
|
|
|
|
|
storageBucket: entity.storageBucket,
|
|
|
|
|
objectKey: entity.objectKey,
|
|
|
|
|
trialDurationSeconds: entity.trialDurationSeconds,
|
|
|
|
|
durationSeconds: entity.durationSeconds,
|
|
|
|
|
width: entity.width,
|
|
|
|
|
height: entity.height,
|
|
|
|
|
isPaid: entity.isPaid,
|
|
|
|
|
freeType: entity.freeType,
|
|
|
|
|
unlockPrice: entity.unlockPrice,
|
|
|
|
|
price: entity.price,
|
|
|
|
|
publishAt: this.toIsoOrUndefined(entity.publishAt),
|
|
|
|
|
nextReleaseAt: this.toIsoOrUndefined(entity.nextReleaseAt),
|
|
|
|
|
status: entity.status,
|
|
|
|
|
reviewStatus: entity.reviewStatus,
|
|
|
|
|
reviewMessage: entity.reviewMessage,
|
|
|
|
|
publishStatus: entity.publishStatus,
|
|
|
|
|
createdAt: this.toIso(entity.createdAt),
|
|
|
|
|
updatedAt: this.toIso(entity.updatedAt),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toIso(value?: Date | string): string {
|
|
|
|
|
if (!value) {
|
|
|
|
|
return new Date().toISOString();
|
|
|
|
|
}
|
|
|
|
|
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private toIsoOrUndefined(value?: Date | string): string | undefined {
|
|
|
|
|
if (!value) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private resolvePagination(input: PaginationInput) {
|
|
|
|
|
const page = Math.max(input.page, 1);
|
|
|
|
|
const pageSize = Math.min(Math.max(input.pageSize, 1), 100);
|
|
|
|
|
return { page, pageSize, skip: (page - 1) * pageSize, take: pageSize };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sortDramas(records: DramaRecord[]) {
|
|
|
|
|
return [...records].sort((left, right) => {
|
|
|
|
|
if (right.sortOrder !== left.sortOrder) {
|
|
|
|
|
|