|
|
@@ -60,153 +60,37 @@ export class VideoService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Get video detail by videoId.
|
|
|
- * Reads from appVideoDetailKey (JSON).
|
|
|
- */
|
|
|
- async getVideoDetail(videoId: string): Promise<VideoDetailDto | null> {
|
|
|
- try {
|
|
|
- const key = tsCacheKeys.video.detail(videoId);
|
|
|
- const cached = await this.redis.getJson<VideoDetailDto | null>(key);
|
|
|
- return cached ?? null;
|
|
|
- } catch (err) {
|
|
|
- this.logger.error(
|
|
|
- `Error fetching video detail for videoId=${videoId}`,
|
|
|
- err instanceof Error ? err.stack : String(err),
|
|
|
- );
|
|
|
- return null;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
* Get home section videos for a channel.
|
|
|
* Reads from appVideoHomeSectionKey (LIST of videoIds).
|
|
|
* Returns video details for each ID.
|
|
|
*/
|
|
|
async getHomeSectionVideos(
|
|
|
- channelId: string,
|
|
|
- section: VideoHomeSectionKey,
|
|
|
- ): Promise<VideoDetailDto[]> {
|
|
|
- try {
|
|
|
- const key = tsCacheKeys.video.homeSection(channelId, section);
|
|
|
-
|
|
|
- // Use helper to read all video IDs from the LIST
|
|
|
- const videoIds = await this.cacheHelper.getVideoIdList(key);
|
|
|
-
|
|
|
- if (!videoIds || videoIds.length === 0) {
|
|
|
- return [];
|
|
|
- }
|
|
|
-
|
|
|
- // Fetch details for all videoIds
|
|
|
- const details = await this.getVideoDetailsBatch(videoIds);
|
|
|
- return details.filter((d) => d !== null) as VideoDetailDto[];
|
|
|
- } catch (err) {
|
|
|
- this.logger.error(
|
|
|
- `Error fetching home section videos for channelId=${channelId}, section=${section}`,
|
|
|
- err instanceof Error ? err.stack : String(err),
|
|
|
- );
|
|
|
- return [];
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private async getVideoDetailsBatch(
|
|
|
- videoIds: string[],
|
|
|
- ): Promise<(VideoDetailDto | null)[]> {
|
|
|
- if (!videoIds || videoIds.length === 0) {
|
|
|
- return [];
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- const keys = videoIds.map((id) => tsCacheKeys.video.detail(id));
|
|
|
- const results: (VideoDetailDto | null)[] = [];
|
|
|
-
|
|
|
- // Fetch all in parallel
|
|
|
- for (const key of keys) {
|
|
|
- const cached = await this.redis.getJson<VideoDetailDto | null>(key);
|
|
|
- results.push(cached ?? null);
|
|
|
- }
|
|
|
-
|
|
|
- return results;
|
|
|
- } catch (err) {
|
|
|
- this.logger.error(
|
|
|
- `Error fetching video details batch`,
|
|
|
- err instanceof Error ? err.stack : String(err),
|
|
|
- );
|
|
|
- return videoIds.map(() => null);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private async getVideoPayloadsByIds(
|
|
|
- videoIds: string[],
|
|
|
- ): Promise<VideoPayload[]> {
|
|
|
- if (!videoIds || videoIds.length === 0) {
|
|
|
- return [];
|
|
|
- }
|
|
|
-
|
|
|
+ channelId: string
|
|
|
+ ): Promise<any[]> {
|
|
|
try {
|
|
|
- const keys = videoIds.map((id) => tsCacheKeys.video.payload(id));
|
|
|
- const cached = await this.redis.mget(keys);
|
|
|
-
|
|
|
- const payloadMap = new Map<string, VideoPayload>();
|
|
|
- const missing = new Set<string>();
|
|
|
-
|
|
|
- cached.forEach((raw, idx) => {
|
|
|
- const id = videoIds[idx];
|
|
|
- if (!raw) {
|
|
|
- missing.add(id);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const parsed = parseVideoPayload(raw);
|
|
|
- if (!parsed) {
|
|
|
- missing.add(id);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- payloadMap.set(id, parsed);
|
|
|
+ const channel = await this.mongoPrisma.channel.findUnique({
|
|
|
+ where: { channelId },
|
|
|
});
|
|
|
|
|
|
- if (missing.size > 0) {
|
|
|
- const records = await this.mongoPrisma.videoMedia.findMany({
|
|
|
- where: { id: { in: Array.from(missing) } },
|
|
|
- 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,
|
|
|
- },
|
|
|
- });
|
|
|
+ const result: { tag: string; records: VideoListItemDto[] }[] = [];
|
|
|
|
|
|
- if (records.length > 0) {
|
|
|
- const pipelineEntries = records.map((row: RawVideoPayloadRow) => ({
|
|
|
- key: tsCacheKeys.video.payload(row.id),
|
|
|
- value: toVideoPayload(row),
|
|
|
- }));
|
|
|
+ for (const tag of channel.tagNames) {
|
|
|
+ const records = await this.getVideoList({
|
|
|
+ random: true,
|
|
|
+ tag,
|
|
|
+ size: 7,
|
|
|
+ }, 3600 * 24);
|
|
|
|
|
|
- await this.redis.pipelineSetJson(pipelineEntries);
|
|
|
- for (const row of records) {
|
|
|
- payloadMap.set(row.id, toVideoPayload(row));
|
|
|
- missing.delete(row.id);
|
|
|
- }
|
|
|
- }
|
|
|
+ result.push({
|
|
|
+ tag,
|
|
|
+ records,
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- return videoIds
|
|
|
- .map((id) => payloadMap.get(id))
|
|
|
- .filter((payload): payload is VideoPayload => Boolean(payload));
|
|
|
+ return result;
|
|
|
} catch (err) {
|
|
|
this.logger.error(
|
|
|
- `Error fetching video payloads for ids=${videoIds.join(',')}`,
|
|
|
+ `Error fetching home section videos for channelId=${channelId}`,
|
|
|
err instanceof Error ? err.stack : String(err),
|
|
|
);
|
|
|
return [];
|
|
|
@@ -218,7 +102,7 @@ export class VideoService {
|
|
|
* Reads video IDs from Redis cache, fetches full details from MongoDB,
|
|
|
* and returns paginated results.
|
|
|
*/
|
|
|
- async getVideoList(dto: VideoListRequestDto): Promise<VideoListItemDto[]> {
|
|
|
+ async getVideoList(dto: VideoListRequestDto, ttl?: number): Promise<VideoListItemDto[]> {
|
|
|
const { page, size, tag, keyword, random } = dto;
|
|
|
const start = (page - 1) * size;
|
|
|
|
|
|
@@ -226,7 +110,9 @@ export class VideoService {
|
|
|
JSON.stringify(dto),
|
|
|
).toString('base64')}`;
|
|
|
|
|
|
- const ttl = random ? 15 : 300;
|
|
|
+ if(!ttl){
|
|
|
+ ttl = random ? 15 : 300;
|
|
|
+ }
|
|
|
|
|
|
let fallbackRecords: VideoListItemDto[] = [];
|
|
|
try {
|
|
|
@@ -239,19 +125,18 @@ export class VideoService {
|
|
|
status: 'Completed'
|
|
|
};
|
|
|
|
|
|
- if (tag) {
|
|
|
- where.secondTags = {
|
|
|
- has: tag,
|
|
|
- };
|
|
|
- }
|
|
|
+ if(random){
|
|
|
+ if (tag) {
|
|
|
+ where.secondTags = tag;
|
|
|
+ }
|
|
|
|
|
|
- if (keyword) {
|
|
|
- where.title = {
|
|
|
- contains: keyword, mode: 'insensitive'
|
|
|
+ if (keyword) {
|
|
|
+ where.title = {
|
|
|
+ $regex: keyword,
|
|
|
+ $options: 'i',
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- if(random){
|
|
|
fallbackRecords = (await this.mongoPrisma.videoMedia.aggregateRaw({
|
|
|
pipeline: [
|
|
|
{ $match: where },
|
|
|
@@ -269,6 +154,18 @@ export class VideoService {
|
|
|
],
|
|
|
})) as unknown as VideoListItemDto[];
|
|
|
}else{
|
|
|
+ if (tag) {
|
|
|
+ where.secondTags = {
|
|
|
+ has: tag,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (keyword) {
|
|
|
+ where.title = {
|
|
|
+ contains: keyword, mode: 'insensitive'
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
fallbackRecords = (await this.mongoPrisma.videoMedia.findMany({
|
|
|
where,
|
|
|
orderBy: [{ addedTime: 'desc' }, { createdAt: 'desc' }],
|
|
|
@@ -572,7 +469,7 @@ export class VideoService {
|
|
|
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');
|
|
|
+ const cached = await this.readCachedVideoList(tsCacheKeys.video.guess() + encodeURIComponent(tag), 'guess like videos');
|
|
|
|
|
|
if (cached && Array.isArray(cached) && cached.length > 0) {
|
|
|
return cached;
|
|
|
@@ -594,7 +491,7 @@ export class VideoService {
|
|
|
this.mapVideoToDto(v),
|
|
|
);
|
|
|
|
|
|
- this.redis.setJson(tsCacheKeys.video.guess() + tag, items, 3600).catch(err => {
|
|
|
+ this.redis.setJson(tsCacheKeys.video.guess() + encodeURIComponent(tag), items, 3600).catch(err => {
|
|
|
this.logger.warn("Redis setJson video.guess failed", err);
|
|
|
});
|
|
|
|