| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- import {
- Injectable,
- NotFoundException,
- BadRequestException,
- } from '@nestjs/common';
- import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
- import { ImageUploadService } from '../image-upload/image-upload.service';
- import {
- VideoMediaListQueryDto,
- UpdateVideoMediaManageDto,
- UpdateVideoMediaStatusDto,
- BatchUpdateVideoMediaStatusDto,
- } from './video-media.dto';
- @Injectable()
- export class VideoMediaService {
- constructor(
- private readonly prisma: MongoPrismaService,
- private readonly imageUploadService: ImageUploadService,
- ) {}
- async findAll(query: VideoMediaListQueryDto): Promise<any> {
- const page = query.page ?? 1;
- const pageSize = query.size ?? 20;
- const skip = (page - 1) * pageSize;
- const take = pageSize;
- const where: any = {};
- if (query.keyword) {
- where.OR = [
- { title: { contains: query.keyword, mode: 'insensitive' } },
- { filename: { contains: query.keyword, mode: 'insensitive' } },
- ];
- }
- if (query.categoryId) {
- where.categoryId = query.categoryId;
- }
- if (query.tagId) {
- where.tagIds = { has: query.tagId };
- }
- if (typeof query.listStatus === 'number') {
- where.listStatus = query.listStatus;
- }
- // filter by editedFrom and editedTo
- if (
- typeof query.editedFrom === 'number' ||
- typeof query.editedTo === 'number'
- ) {
- where.editedAt = {};
- if (typeof query.editedFrom === 'number') {
- where.editedAt.gte = BigInt(query.editedFrom);
- }
- if (typeof query.editedTo === 'number') {
- where.editedAt.lte = BigInt(query.editedTo);
- }
- }
- const [total, rows] = await Promise.all([
- this.prisma.videoMedia.count({ where }),
- this.prisma.videoMedia.findMany({
- where,
- skip,
- take,
- orderBy: { addedTime: 'desc' },
- }),
- ]);
- return {
- total,
- page,
- pageSize,
- items: rows.map((row) => ({
- id: row.id,
- title: row.title,
- filename: row.filename,
- videoTime: row.videoTime,
- size: row.size?.toString?.() ?? '0',
- coverImg: row.coverImg ?? '',
- categoryId: row.categoryId ?? null,
- tagIds: row.tagIds ?? [],
- listStatus: row.listStatus ?? 0,
- editedAt: Number(row.editedAt ?? 0),
- updatedAt: row.updatedAt ?? null,
- tags: row.tags ?? [],
- tagsFlat: row.tagsFlat ?? '',
- secondTags: row.secondTags ?? [],
- // NOTE: We keep list DTO backward compatible.
- // If you later want to show tag names in list, we can add e.g. `tagsFlat` or `tagNames` here.
- })),
- };
- }
- async findOne(id: string): Promise<any> {
- const video = await this.prisma.videoMedia.findUnique({
- where: { id },
- });
- if (!video) {
- throw new NotFoundException('Video not found');
- }
- const [category, tags] = await Promise.all([
- video.categoryId
- ? this.prisma.category.findUnique({
- where: { id: video.categoryId },
- })
- : null,
- video.tagIds && video.tagIds.length
- ? this.prisma.tag.findMany({
- where: { id: { in: video.tagIds } },
- orderBy: { seq: 'asc' },
- })
- : [],
- ]);
- return {
- id: video.id,
- title: video.title,
- filename: video.filename,
- videoTime: video.videoTime,
- size: video.size?.toString?.() ?? '0',
- coverImg: video.coverImg ?? '',
- type: video.type,
- formatType: video.formatType,
- contentType: video.contentType,
- country: video.country,
- status: video.status,
- desc: video.desc ?? '',
- categoryId: video.categoryId ?? null,
- tagIds: video.tagIds ?? [],
- listStatus: video.listStatus ?? 0,
- editedAt: Number(video.editedAt ?? 0),
- updatedAt: video.updatedAt ?? null,
- categoryName: category?.name ?? null,
- // Existing DTO: tags as {id, name}[]
- tags: video.tags ?? [],
- tagsFlat: video.tagsFlat ?? '',
- secondTags: video.secondTags ?? [],
- };
- }
- async updateManage(id: string, dto: UpdateVideoMediaManageDto) {
- const video = await this.prisma.videoMedia.findUnique({
- where: { id },
- });
- if (!video) {
- throw new NotFoundException('Video not found');
- }
- const updateData: any = {};
- if (typeof dto.title === 'string') {
- updateData.title = dto.title.trim();
- }
- let categoryId: string | null | undefined = dto.categoryId;
- const tagIds: string[] | undefined = dto.tagIds;
- if (dto.categoryId === null) {
- categoryId = null;
- }
- if (typeof categoryId !== 'undefined' || typeof tagIds !== 'undefined') {
- const { finalCategoryId, finalTagIds, tags, tagsFlat } =
- await this.validateCategoryAndTags(categoryId, tagIds);
- updateData.categoryId = finalCategoryId;
- updateData.tagIds = finalTagIds;
- updateData.tags = tags; // NEW: store denormalised tag names (lowercased)
- updateData.tagsFlat = tagsFlat; // existing: text for search
- }
- if (typeof dto.listStatus === 'number') {
- if (dto.listStatus !== 0 && dto.listStatus !== 1) {
- throw new BadRequestException('Invalid listStatus value');
- }
- updateData.listStatus = dto.listStatus;
- }
- updateData.editedAt = BigInt(Date.now());
- updateData.updatedAt = new Date();
- await this.prisma.videoMedia.update({
- where: { id },
- data: updateData,
- });
- return this.findOne(id);
- }
- async updateStatus(id: string, dto: UpdateVideoMediaStatusDto) {
- const video = await this.prisma.videoMedia.findUnique({
- where: { id },
- });
- if (!video) {
- throw new NotFoundException('Video not found');
- }
- if (dto.listStatus !== 0 && dto.listStatus !== 1) {
- throw new BadRequestException('Invalid listStatus value');
- }
- const editedAt = BigInt(Date.now());
- const updatedAt = new Date();
- await this.prisma.videoMedia.update({
- where: { id },
- data: {
- listStatus: dto.listStatus,
- editedAt,
- updatedAt,
- },
- });
- return {
- id,
- listStatus: dto.listStatus,
- editedAt: editedAt.toString(),
- };
- }
- async batchUpdateStatus(dto: BatchUpdateVideoMediaStatusDto) {
- if (!dto.ids?.length) {
- throw new BadRequestException('ids cannot be empty');
- }
- if (dto.listStatus !== 0 && dto.listStatus !== 1) {
- throw new BadRequestException('Invalid listStatus value');
- }
- const editedAt = BigInt(Date.now());
- const updatedAt = new Date();
- const result = await this.prisma.videoMedia.updateMany({
- where: { id: { in: dto.ids } },
- data: {
- listStatus: dto.listStatus,
- editedAt,
- updatedAt,
- },
- });
- return {
- affected: result.count,
- listStatus: dto.listStatus,
- editedAt: editedAt.toString(),
- };
- }
- /**
- * Upload and update VideoMedia cover image.
- */
- async updateCover(id: string, file: Express.Multer.File) {
- // Ensure video exists
- const video = await this.prisma.videoMedia.findUnique({
- where: { id },
- });
- if (!video) {
- throw new NotFoundException('Video not found');
- }
- // Upload image
- const { key, imgSource } = await this.imageUploadService.uploadCoverImage(
- 'video-cover',
- file,
- );
- const editedAt = BigInt(Date.now());
- const updatedAt = new Date();
- // Update VideoMedia record
- const updated = await this.prisma.videoMedia.update({
- where: { id },
- data: {
- coverImg: key,
- imgSource,
- editedAt,
- updatedAt,
- },
- });
- return {
- id: updated.id,
- coverImg: updated.coverImg,
- imgSource: updated.imgSource,
- editedAt: editedAt.toString(),
- };
- }
- private async validateCategoryAndTags(
- categoryId: string | null | undefined,
- tagIds: string[] | undefined,
- ): Promise<{
- finalCategoryId: string | null;
- finalTagIds: string[];
- tags: string[]; // NEW: denormalised tag names (lowercased)
- tagsFlat: string; // NEW: concatenated names for search
- }> {
- let finalCategoryId: string | null =
- typeof categoryId === 'undefined' ? (undefined as any) : categoryId;
- let finalTagIds: string[] = [];
- let tags: string[] = []; // NEW
- let tagsFlat = '';
- // Normalize tagIds: remove duplicates
- if (Array.isArray(tagIds)) {
- const unique = [...new Set(tagIds)];
- if (unique.length > 5) {
- throw new BadRequestException('Tag count cannot exceed 5');
- }
- finalTagIds = unique;
- }
- // If tags are provided but categoryId is null/undefined -> error
- if (finalTagIds.length > 0 && !finalCategoryId) {
- throw new BadRequestException(
- 'Category is required when tags are provided.',
- );
- }
- // Validate category if present
- if (typeof finalCategoryId !== 'undefined' && finalCategoryId !== null) {
- const category = await this.prisma.category.findUnique({
- where: { id: finalCategoryId },
- });
- if (!category) {
- throw new BadRequestException('Category not found');
- }
- if (category.status !== 1) {
- throw new BadRequestException('Category is disabled');
- }
- }
- if (finalTagIds.length > 0) {
- const tagEntities = await this.prisma.tag.findMany({
- where: { id: { in: finalTagIds } },
- });
- if (tagEntities.length !== finalTagIds.length) {
- throw new BadRequestException('Some tags do not exist');
- }
- const distinctCategoryIds = [
- ...new Set(tagEntities.map((t) => t.categoryId.toString())),
- ];
- if (distinctCategoryIds.length > 1) {
- throw new BadRequestException(
- 'All tags must belong to the same category',
- );
- }
- const tagCategoryId = distinctCategoryIds[0];
- if (finalCategoryId && tagCategoryId !== finalCategoryId) {
- throw new BadRequestException(
- 'Tags do not belong to the specified category',
- );
- }
- // If categoryId was not provided but tags exist, infer from tags
- if (!finalCategoryId) {
- finalCategoryId = tagCategoryId;
- }
- // Build tags & tagsFlat: lowercased names
- const tagNames = tagEntities
- .map((t) => t.name?.trim())
- .filter(Boolean) as string[];
- tags = tagNames.map((name) => name.toLowerCase()); // NEW
- tagsFlat = tags.join(' '); // e.g. "funny hot 2025"
- }
- if (typeof finalCategoryId === 'undefined') {
- finalCategoryId = null;
- }
- return {
- finalCategoryId,
- finalTagIds,
- tags,
- tagsFlat,
- };
- }
- }
|