瀏覽代碼

new search

FC_DAN\c9837 3 月之前
父節點
當前提交
25504b01f9

+ 5 - 0
apps/box-app-api/src/feature/auth/auth.service.ts

@@ -21,6 +21,8 @@ type LoginResult = {
   uid: string;
   channelId: string;
   startupAds: any | null; // keep as any until you wire your Ad payload DTO
+  cdn_img: string;
+  cdn_video: string;
 };
 
 @Injectable()
@@ -108,6 +110,9 @@ export class AuthService {
       uid,
       channelId: finalChannelId,
       startupAds,
+      cdn_img: "https://vm.rvakc.xyz/res/decode/",
+      cdn_video: "https://vm.rvakc.xyz/api/web/media/m3u8/"
+      
     };
   }
 

+ 12 - 5
apps/box-app-api/src/feature/video/dto/video-list-request.dto.ts

@@ -1,6 +1,6 @@
 // apps/box-app-api/src/feature/video/dto/video-list-request.dto.ts
 import { ApiProperty } from '@nestjs/swagger';
-import { IsNumber, IsString, IsOptional, Min, Max } from 'class-validator';
+import { IsNumber, IsString, IsOptional, Min, Max, IsBoolean } from 'class-validator';
 
 export class VideoListRequestDto {
   @ApiProperty({
@@ -24,13 +24,12 @@ export class VideoListRequestDto {
   size: number;
 
   @ApiProperty({
-    description: '分类ID',
-    example: '6507f1f77bcf86cd799439011',
+    description: '关键词',
     required: false,
   })
   @IsOptional()
   @IsString()
-  categoryId?: string;
+  keyword?: string;
 
   @ApiProperty({
     required: false,
@@ -39,5 +38,13 @@ export class VideoListRequestDto {
   })
   @IsOptional()
   @IsString()
-  tagName?: string;
+  tag?: string;
+
+  @ApiProperty({
+    required: false,
+    description: '是否随机',
+  })
+  @IsOptional()
+  @IsBoolean()
+  random?: boolean;  
 }

+ 4 - 18
apps/box-app-api/src/feature/video/dto/video-list-response.dto.ts

@@ -18,32 +18,18 @@ export class VideoListItemDto {
     required: false,
     description: '视频时长(秒)',
   })
-  duration?: number;
+  videoTime?: number;
 
-  @ApiProperty({ description: '分类ID' })
-  categoryId: string;
-
-  @ApiProperty({ description: '分类名称' })
-  name: string;
-
-  @ApiProperty({
-    required: false,
-    description: '分类副标题',
-  })
-  subtitle?: string;
+  @ApiProperty({ description: '视频' })
+  preFileName: string;
 
   @ApiProperty({
     description: '关联的标签名列表',
     type: String,
     isArray: true,
   })
-  tags: string[];
+  secondTags: string[];
 
-  @ApiProperty({
-    description: '最后更新时间(ISO 8601格式)',
-    example: '2024-12-06T10:30:00.000Z',
-  })
-  updateAt: string;
 }
 
 export class VideoListResponseDto {

+ 5 - 11
apps/box-app-api/src/feature/video/video.controller.ts

@@ -52,13 +52,8 @@ export class VideoController {
    */
   @Get('search')
   @ApiOperation({
-    summary: '基于 secondTags 搜索视频',
-    description: '过滤视频;支持多个标签,用逗号分隔,tag 为空则返回全部。',
-  })
-  @ApiQuery({
-    name: 'tags',
-    required: false,
-    description: '逗号分隔的 secondTags 标签列表(精确匹配)',
+    summary: '搜索视频',
+    description: '',
   })
   @ApiResponse({
     status: 200,
@@ -67,10 +62,9 @@ export class VideoController {
     isArray: true,
   })
   async searchBySecondTags(
-    @Query('tags') tags?: string,
-  ): Promise<{ total: number; list: VideoItemDto[] }> {
-    const list = await this.videoService.searchVideosBySecondTags(tags);
-    return { total: list.length, list };
+    @Body() req: VideoListRequestDto,
+  ): Promise<VideoItemDto[]> {
+    return await this.videoService.getVideoList(req);
   }
 
   /**

+ 84 - 654
apps/box-app-api/src/feature/video/video.service.ts

@@ -36,6 +36,7 @@ import {
   RECOMMENDED_CATEGORY_NAME,
 } from '../homepage/homepage.constants';
 import { CategoryDto } from '../homepage/dto/homepage.dto';
+import { VideoListItemDto } from './dto/video-list-response.dto';
 
 /**
  * VideoService provides read-only access to video data from Redis cache.
@@ -217,692 +218,85 @@ export class VideoService {
    * Reads video IDs from Redis cache, fetches full details from MongoDB,
    * and returns paginated results.
    */
-  async getVideoList(dto: VideoListRequestDto): Promise<VideoListResponseDto> {
-    const { page, size, tagName } = dto;
-    const categoryId = dto.categoryId;
-    let key: string;
-    let tagId: string | undefined;
-
-    // If tagName is provided but categoryId is not, fallback to searchVideosByTagName
-    if (tagName && !categoryId) {
-      this.logger.debug(
-        `tagName provided without categoryId, falling back to searchVideosByTagName`,
-      );
-      return this.searchVideosByTagName({ page, size, tagName });
-    }
-
-    // Validate categoryId is provided when no tagName
-    if (!categoryId) {
-      this.logger.debug(`categoryId is required for getVideoList`);
-      return {
-        page,
-        size,
-        total: 0,
-        tagName,
-        items: [],
-      };
-    }
-
-    // Step 1: Resolve the Redis key
-    if (!tagName) {
-      // No tag filter - use category list
-      key = tsCacheKeys.video.categoryList(categoryId);
-    } else {
-      // Tag filter - need to find tag ID first
-      try {
-        const tagKey = tsCacheKeys.tag.metadataByCategory(categoryId);
-        const tags = await this.cacheHelper.getTagListForCategory(tagKey);
-
-        if (!tags || tags.length === 0) {
-          this.logger.debug(
-            `No tags found for categoryId=${categoryId}, tagName=${tagName}`,
-          );
-          return {
-            page,
-            size,
-            total: 0,
-            tagName,
-            items: [],
-          };
-        }
-
-        const tag = tags.find((t) => t.name === tagName || t.id === tagName);
-        if (!tag) {
-          this.logger.debug(
-            `Tag not found: categoryId=${categoryId}, tagName=${tagName}`,
-          );
-          return {
-            page,
-            size,
-            total: 0,
-            tagName,
-            items: [],
-          };
-        }
-
-        tagId = tag.id;
-        key = tsCacheKeys.video.tagList(categoryId, tagId);
-      } catch (err) {
-        this.logger.error(
-          `Error fetching tag for categoryId=${categoryId}, tagName=${tagName}`,
-          err instanceof Error ? err.stack : String(err),
-        );
-        return {
-          page,
-          size,
-          total: 0,
-          tagName,
-          items: [],
-        };
-      }
-    }
-
-    type VideoPayloadWithTags = RawVideoPayloadRow & { tagIds?: string[] };
-
-    // Step 2: Compute pagination indices
+  async getVideoList(dto: VideoListRequestDto): Promise<VideoListItemDto[]> {
+    const { page, size, tag, keyword, random } = dto;
     const start = (page - 1) * size;
-    const stop = start + size - 1;
 
-    let total = 0;
-    let pageVideoIds: string[] = [];
-    let fallbackRecords: VideoPayloadWithTags[] = [];
-    let videoTagMap = new Map<string, string[]>();
+    const cacheKey = `video:list:${Buffer.from(
+      JSON.stringify(dto),
+    ).toString('base64')}`;
 
-    let listKeyExists: boolean;
-    try {
-      listKeyExists = (await this.redis.exists(key)) > 0;
-    } catch (err) {
-      this.logger.error(
-        `Error checking list key existence for key=${key}`,
-        err instanceof Error ? err.stack : String(err),
-      );
-      return {
-        page,
-        size,
-        total: 0,
-        tagName,
-        items: [],
-      };
-    }
+    const ttl = random ? 15 : 300;
 
-    if (listKeyExists) {
-      try {
-        total = await this.redis.llen(key);
-      } catch (err) {
-        this.logger.error(
-          `Error getting list length for key=${key}`,
-          err instanceof Error ? err.stack : String(err),
-        );
-        return {
-          page,
-          size,
-          total: 0,
-          tagName,
-          items: [],
-        };
-      }
-
-      if (total === 0) {
-        this.logger.debug(`Empty video list for key=${key}`);
-        return {
-          page,
-          size,
-          total: 0,
-          tagName,
-          items: [],
-        };
+    let fallbackRecords: VideoListItemDto[] = [];
+    try {
+      const cache = await this.redis.getJson<VideoListItemDto[]>(cacheKey);
+      if (cache) {
+        return cache;
       }
 
-      if (start >= total) {
-        this.logger.debug(
-          `Page out of range: page=${page}, size=${size}, total=${total}, key=${key}`,
-        );
-        return {
-          page,
-          size,
-          total,
-          tagName,
-          items: [],
-        };
-      }
+      let where: any = {
+        status: 'Completed'
+      };
 
-      try {
-        pageVideoIds = await this.redis.lrange(key, start, stop);
-      } catch (err) {
-        this.logger.error(
-          `Error fetching video IDs from key=${key}`,
-          err instanceof Error ? err.stack : String(err),
-        );
-        return {
-          page,
-          size,
-          total,
-          tagName,
-          items: [],
+      if (tag) {
+        where.secondTags = {
+          has: tag,
         };
       }
 
-      if (!pageVideoIds || pageVideoIds.length === 0) {
-        this.logger.debug(`No video IDs found for key=${key}`);
-        return {
-          page,
-          size,
-          total,
-          tagName,
-          items: [],
-        };
+      if (keyword) {
+        where.title = {
+          contains: keyword, mode: 'insensitive'
+        }
       }
-    } else {
-      this.logger.debug(`Cache miss for video list key=${key}`);
-      try {
-        const where = tagId
-          ? {
-              categoryIds: { has: categoryId },
-              status: 'Completed',
-              tagIds: { has: tagId },
-            }
-          : {
-              categoryIds: { has: categoryId },
-              status: 'Completed',
-            };
 
+      if(random){
+        fallbackRecords = (await this.mongoPrisma.videoMedia.aggregateRaw({
+          pipeline: [
+            { $match: where },
+            { $sample: { size } },
+            {
+              $project: {
+                id: 1,
+                title: 1,
+                coverImg: 1,
+                videoTime: 1,
+                secondTags: 1,
+                preFileName: 1,
+              },
+            },
+          ],
+        })) as unknown as VideoListItemDto[];
+      }else{
         fallbackRecords = (await this.mongoPrisma.videoMedia.findMany({
           where,
           orderBy: [{ addedTime: 'desc' }, { createdAt: 'desc' }],
+          skip: start,
+          take: size,
           select: {
             id: true,
             title: true,
             coverImg: true,
-            coverImgNew: true,
             videoTime: true,
-            country: true,
-            firstTag: true,
             secondTags: true,
             preFileName: true,
-            desc: true,
-            size: true,
-            updatedAt: true,
-            filename: true,
-            fieldNameFs: true,
-            ext: true,
-            tagIds: true,
           },
-        })) as VideoPayloadWithTags[];
-      } catch (err) {
-        this.logger.error(
-          `Error fetching videos from MongoDB for fallback key=${key}`,
-          err instanceof Error ? err.stack : String(err),
-        );
-        return {
-          page,
-          size,
-          total: 0,
-          tagName,
-          items: [],
-        };
-      }
-
-      const allVideoIds = fallbackRecords.map((video) => video.id);
-      total = allVideoIds.length;
-
-      try {
-        await this.cacheHelper.saveVideoIdList(key, allVideoIds);
-      } catch (err) {
-        this.logger.error(
-          `Error saving video ID list for key=${key}`,
-          err instanceof Error ? err.stack : String(err),
-        );
+        })) as VideoListItemDto[];
       }
 
-      const entries = fallbackRecords.map((video) => ({
-        key: tsCacheKeys.video.payload(video.id),
-        value: toVideoPayload(video),
-      }));
-
-      try {
-        await this.redis.pipelineSetJson(entries);
-      } catch (err) {
-        this.logger.error(
-          `Error writing payload cache for fallback key=${key}`,
-          err instanceof Error ? err.stack : String(err),
-        );
-      }
-
-      if (total === 0) {
-        this.logger.debug(`No videos found for fallback key=${key}`);
-        return {
-          page,
-          size,
-          total: 0,
-          tagName,
-          items: [],
-        };
+      if (fallbackRecords.length > 0) {
+        await this.redis.setJson(cacheKey, fallbackRecords, ttl);
       }
 
-      if (start >= total) {
-        this.logger.debug(
-          `Page out of range: page=${page}, size=${size}, total=${total}, key=${key}`,
-        );
-        return {
-          page,
-          size,
-          total,
-          tagName,
-          items: [],
-        };
-      }
-
-      const slicedIds = allVideoIds.slice(start, stop + 1);
-      pageVideoIds = slicedIds;
-      videoTagMap = new Map(
-        fallbackRecords.map((video) => [
-          video.id,
-          Array.isArray(video.tagIds) ? video.tagIds : [],
-        ]),
-      );
-    }
-
-    if (!pageVideoIds.length) {
-      return {
-        page,
-        size,
-        total,
-        tagName,
-        items: [],
-      };
-    }
-
-    if (!videoTagMap.size) {
-      try {
-        const tagRows = await this.mongoPrisma.videoMedia.findMany({
-          where: { id: { in: pageVideoIds } },
-          select: { id: true, tagIds: true },
-        });
-
-        for (const row of tagRows) {
-          videoTagMap.set(row.id, Array.isArray(row.tagIds) ? row.tagIds : []);
-        }
-      } catch (err) {
-        this.logger.error(
-          `Error fetching video tag IDs for ids=${pageVideoIds.join(',')}`,
-          err instanceof Error ? err.stack : String(err),
-        );
-      }
-    }
-
-    const allTagIds = new Set<string>();
-    for (const ids of videoTagMap.values()) {
-      if (Array.isArray(ids)) {
-        for (const tid of ids) {
-          allTagIds.add(tid);
-        }
-      }
-    }
-
-    let tagsById = new Map<string, string>();
-    if (allTagIds.size > 0) {
-      try {
-        const tagsList = await this.mongoPrisma.tag.findMany({
-          where: { id: { in: Array.from(allTagIds) } },
-          select: { id: true, name: true },
-        });
-        tagsById = new Map(tagsList.map((tag) => [tag.id, tag.name]));
-      } catch (err) {
-        this.logger.error(
-          `Error fetching tags from MongoDB`,
-          err instanceof Error ? err.stack : String(err),
-        );
-      }
-    }
-
-    let category;
-    try {
-      category = await this.mongoPrisma.category.findUnique({
-        where: { id: categoryId },
-      });
+      return fallbackRecords;
     } catch (err) {
       this.logger.error(
-        `Error fetching category for categoryId=${categoryId}`,
+        `Error fetching videos from MongoDB`,
         err instanceof Error ? err.stack : String(err),
       );
+      return [];
     }
-
-    const payloads = await this.getVideoPayloadsByIds(pageVideoIds);
-    const payloadMap = new Map(
-      payloads.map((payload) => [payload.id, payload]),
-    );
-
-    const items = pageVideoIds
-      .map((videoId) => {
-        const payload = payloadMap.get(videoId);
-        if (!payload) {
-          this.logger.debug(`Video payload missing for videoId=${videoId}`);
-          return null;
-        }
-
-        const tags: string[] = [];
-        const videoTagIds = videoTagMap.get(videoId);
-        if (videoTagIds && Array.isArray(videoTagIds)) {
-          for (const tid of videoTagIds) {
-            const tagName = tagsById.get(tid);
-            if (tagName) {
-              tags.push(tagName);
-            }
-          }
-        }
-
-        return {
-          id: payload.id,
-          title: payload.title ?? '',
-          coverImg: payload.coverImg ?? undefined,
-          duration: payload.videoTime ?? undefined,
-          categoryId,
-          name: category?.name ?? '',
-          subtitle: category?.subtitle ?? undefined,
-          tags,
-          updateAt: payload.updatedAt ?? new Date().toISOString(),
-        };
-      })
-      .filter((item): item is NonNullable<typeof item> => item !== null);
-
-    return {
-      page,
-      size,
-      total,
-      tagName,
-      items,
-    };
-  }
-
-  async searchVideosByTagName(
-    dto: VideoSearchByTagRequestDto,
-  ): Promise<VideoListResponseDto> {
-    const { page, size, tagName } = dto;
-
-    // Step 1: Load all categories
-    let categories: Array<{
-      id: string;
-      name: string;
-      subtitle?: string;
-    }>;
-    try {
-      const categoriesKey = tsCacheKeys.category.all();
-      categories = await this.redis.getJson<
-        Array<{
-          id: string;
-          name: string;
-          subtitle?: string;
-        }>
-      >(categoriesKey);
-
-      if (!categories || categories.length === 0) {
-        // Fallback to MongoDB if Redis cache is empty
-        this.logger.debug(
-          'Categories not found in Redis, fetching from MongoDB',
-        );
-        const categoriesFromDb = await this.mongoPrisma.category.findMany({
-          select: {
-            id: true,
-            name: true,
-            subtitle: true,
-          },
-        });
-        categories = categoriesFromDb.map((c) => ({
-          id: c.id,
-          name: c.name ?? '',
-          subtitle: c.subtitle ?? undefined,
-        }));
-      }
-    } catch (err) {
-      this.logger.error(
-        'Error loading categories for tag search',
-        err instanceof Error ? err.stack : String(err),
-      );
-      return {
-        page,
-        size,
-        total: 0,
-        tagName,
-        items: [],
-      };
-    }
-
-    if (!categories || categories.length === 0) {
-      this.logger.debug('No categories found');
-      return {
-        page,
-        size,
-        total: 0,
-        tagName,
-        items: [],
-      };
-    }
-
-    // Step 2 & 3: For each category, find matching tags and collect (categoryId, tagId) pairs
-    const categoryTagPairs: Array<{ categoryId: string; tagId: string }> = [];
-
-    for (const category of categories) {
-      try {
-        const tagKey = tsCacheKeys.tag.metadataByCategory(category.id);
-        const tagsMetadata =
-          await this.cacheHelper.getTagListForCategory(tagKey);
-
-        const matchingTags = (tagsMetadata ?? []).filter(
-          (t) => t.name === tagName,
-        );
-
-        for (const tag of matchingTags) {
-          categoryTagPairs.push({
-            categoryId: category.id,
-            tagId: tag.id,
-          });
-        }
-      } catch (err) {
-        this.logger.debug(
-          `Error fetching tags for categoryId=${category.id}`,
-          err instanceof Error ? err.stack : String(err),
-        );
-        // Continue with next category
-      }
-    }
-
-    if (categoryTagPairs.length === 0) {
-      this.logger.debug(`No categories found with tag: ${tagName}`);
-      return {
-        page,
-        size,
-        total: 0,
-        tagName,
-        items: [],
-      };
-    }
-
-    // Step 4: For each (categoryId, tagId) pair, read all video IDs
-    const allVideoIds: string[] = [];
-
-    for (const pair of categoryTagPairs) {
-      try {
-        const key = tsCacheKeys.video.tagList(pair.categoryId, pair.tagId);
-        const videoIds = await this.redis.lrange(key, 0, -1);
-        if (videoIds && videoIds.length > 0) {
-          allVideoIds.push(...videoIds);
-        }
-      } catch (err) {
-        this.logger.debug(
-          `Error reading video IDs for categoryId=${pair.categoryId}, tagId=${pair.tagId}`,
-          err instanceof Error ? err.stack : String(err),
-        );
-        // Continue with next pair
-      }
-    }
-
-    if (allVideoIds.length === 0) {
-      this.logger.debug(`No videos found for tag: ${tagName}`);
-      return {
-        page,
-        size,
-        total: 0,
-        tagName,
-        items: [],
-      };
-    }
-
-    // Step 5: Deduplicate and compute total
-    const uniqueVideoIds = Array.from(new Set(allVideoIds));
-    const total = uniqueVideoIds.length;
-
-    // Step 6: Apply in-memory pagination
-    const start = (page - 1) * size;
-    const end = start + size;
-    const pagedIds = uniqueVideoIds.slice(start, end);
-
-    if (pagedIds.length === 0) {
-      return {
-        page,
-        size,
-        total,
-        tagName,
-        items: [],
-      };
-    }
-
-    // Step 7: Fetch videos from MongoDB
-    let videos: Awaited<
-      ReturnType<typeof this.mongoPrisma.videoMedia.findMany>
-    >;
-    try {
-      videos = await this.mongoPrisma.videoMedia.findMany({
-        where: {
-          id: { in: pagedIds },
-        },
-      });
-    } catch (err) {
-      this.logger.error(
-        `Error fetching videos from MongoDB for tag search`,
-        err instanceof Error ? err.stack : String(err),
-      );
-      return {
-        page,
-        size,
-        total,
-        tagName,
-        items: [],
-      };
-    }
-
-    if (!videos || videos.length === 0) {
-      return {
-        page,
-        size,
-        total,
-        tagName,
-        items: [],
-      };
-    }
-
-    // Fetch category data for each video
-    const categoryIdSet = new Set<string>();
-    for (const video of videos) {
-      if (video.categoryIds && Array.isArray(video.categoryIds)) {
-        for (const cid of video.categoryIds) {
-          categoryIdSet.add(cid);
-        }
-      }
-    }
-    const categoryIdsList = Array.from(categoryIdSet);
-    const categoriesMap = new Map(
-      categories
-        .filter((c) => categoryIdsList.includes(c.id))
-        .map((c) => [c.id, c]),
-    );
-
-    // Fetch all tags for mapping tag IDs to names
-    const allTagIds = Array.from(
-      new Set(
-        videos.flatMap((v) =>
-          v.tagIds && Array.isArray(v.tagIds) ? v.tagIds : [],
-        ),
-      ),
-    );
-
-    const tagsById = new Map<string, string>();
-    try {
-      if (allTagIds.length > 0) {
-        const tags = await this.mongoPrisma.tag.findMany({
-          where: {
-            id: { in: allTagIds },
-          },
-          select: {
-            id: true,
-            name: true,
-          },
-        });
-
-        for (const tag of tags) {
-          if (tag.name) {
-            tagsById.set(tag.id, tag.name);
-          }
-        }
-      }
-    } catch (err) {
-      this.logger.error(
-        'Error fetching tags for search results',
-        err instanceof Error ? err.stack : String(err),
-      );
-      // Continue without tag names
-    }
-
-    // Step 8: Map to VideoListItemDto (maintain order of pagedIds)
-    const videoMap = new Map(videos.map((v) => [v.id, v]));
-
-    const items = pagedIds
-      .map((videoId) => {
-        const video = videoMap.get(videoId);
-        if (!video) {
-          return null;
-        }
-
-        // Use first category ID from categoryIds array
-        const firstCategoryId =
-          Array.isArray(video.categoryIds) && video.categoryIds.length > 0
-            ? video.categoryIds[0]
-            : undefined;
-        const category = firstCategoryId
-          ? categoriesMap.get(firstCategoryId)
-          : undefined;
-
-        // Map tag IDs to tag names
-        const tags: string[] = [];
-        if (video.tagIds && Array.isArray(video.tagIds)) {
-          for (const tid of video.tagIds) {
-            const tagName = tagsById.get(tid);
-            if (tagName) {
-              tags.push(tagName);
-            }
-          }
-        }
-
-        return {
-          id: video.id,
-          title: video.title ?? '',
-          coverImg: video.coverImg ?? undefined,
-          duration: video.videoTime ?? undefined,
-          categoryId: firstCategoryId ?? '',
-          name: category?.name ?? '',
-          subtitle: category?.subtitle ?? undefined,
-          tags,
-          updateAt: video.updatedAt?.toString() ?? new Date().toISOString(),
-        };
-      })
-      .filter((item): item is NonNullable<typeof item> => item !== null);
-
-    return {
-      page,
-      size,
-      total,
-      tagName,
-      items,
-    };
   }
 
   /**
@@ -1175,6 +569,42 @@ export class VideoService {
     return videos.filter((video) => this.matchesSecondTags(video, tagSet));
   }
 
+  async getGuessLikeVideos(tag: string): Promise<VideoItemDto[]> {
+    try {
+      // Try to fetch from Redis cache first
+      const cached = this.readCachedVideoList(tsCacheKeys.video.guess() + tag, 'guess like videos');
+
+      if (cached && Array.isArray(cached) && cached.length > 0) {
+        return cached;
+      }
+
+      // Fallback to MongoDB if cache miss
+      this.logger.warn(
+        '[getGuessLikeVideos] Cache miss, falling back to MongoDB',
+      );
+
+      const videos = await this.mongoPrisma.videoMedia.aggregateRaw({
+        pipeline: [
+          { $match: { status: 'Completed' } },
+          { $sample: { size: 20 } },
+        ],
+      });
+
+      const items = (Array.isArray(videos) ? videos : []).map((v: any) =>
+        this.mapVideoToDto(v),
+      );
+
+      this.redis.setJson(tsCacheKeys.video.guess() + tag, items, 3600).catch(err => {
+        this.logger.warn("Redis setJson video.guess failed", err);
+      });
+
+      return items;
+    } catch (error) {
+      this.logger.warn('Error fetching guess like videos, returning empty');
+      return [];
+    }
+  }
+
   private matchesSecondTags(
     video: VideoItemDto,
     filters: Set<string>,

+ 1 - 0
libs/common/src/cache/cache-keys.ts

@@ -103,4 +103,5 @@ export const CacheKeys = {
   appRecommendedVideos: 'box:app:video:recommended',
   appVideoLatest: 'box:app:video:latest',
   appVideoList: 'box:app:video:list',
+  appGuessList: 'box:app:video:guess:',
 };

+ 2 - 0
libs/common/src/cache/ts-cache-key.provider.ts

@@ -214,6 +214,7 @@ export interface TsCacheKeyBuilder {
     recommended(): string;
     latest(): string;
     list(): string;
+    guess(): string;
   };
 
   /**
@@ -272,6 +273,7 @@ export function createTsCacheKeyBuilder(): TsCacheKeyBuilder {
       recommended: () => CacheKeys.appRecommendedVideos,
       latest: () => CacheKeys.appVideoLatest,
       list: () => CacheKeys.appVideoList,
+      guess: () => CacheKeys.appGuessList,
     },
     videoList: {
       homePage: (page) => CacheKeys.appHomeVideoPage(page),