|
@@ -98,17 +98,20 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
|
|
|
*/
|
|
*/
|
|
|
async buildAll(): Promise<void> {
|
|
async buildAll(): Promise<void> {
|
|
|
const channels = await this.mongoPrisma.channel.findMany();
|
|
const channels = await this.mongoPrisma.channel.findMany();
|
|
|
|
|
+ this.logger.log(
|
|
|
|
|
+ `[BuildAll] Starting video cache rebuild for ${channels.length} channels`,
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
let totalCategories = 0;
|
|
let totalCategories = 0;
|
|
|
let totalTagFilteredLists = 0;
|
|
let totalTagFilteredLists = 0;
|
|
|
let totalTagMetadataLists = 0;
|
|
let totalTagMetadataLists = 0;
|
|
|
-
|
|
|
|
|
- this.logger.log(
|
|
|
|
|
- `🔨 Starting video cache rebuild for ${channels.length} channels...`,
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ let totalVideosProcessed = 0;
|
|
|
|
|
+ let totalTagsProcessed = 0;
|
|
|
|
|
|
|
|
for (const channel of channels) {
|
|
for (const channel of channels) {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ this.logger.debug(`[BuildAll] Processing channel: ${channel.id}`);
|
|
|
|
|
+
|
|
|
// Build video lists for each category
|
|
// Build video lists for each category
|
|
|
const categories = await this.mongoPrisma.category.findMany({
|
|
const categories = await this.mongoPrisma.category.findMany({
|
|
|
where: {
|
|
where: {
|
|
@@ -118,13 +121,15 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
this.logger.debug(
|
|
this.logger.debug(
|
|
|
- ` Channel ${channel.id}: Processing ${categories.length} categories`,
|
|
|
|
|
|
|
+ `[BuildAll] Channel ${channel.id}: Found ${categories.length} categories`,
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
for (const category of categories) {
|
|
for (const category of categories) {
|
|
|
// 1. Build list of all video IDs in this category
|
|
// 1. Build list of all video IDs in this category
|
|
|
- await this.buildCategoryVideoListForCategory(category.id);
|
|
|
|
|
|
|
+ const categoryVideoCount =
|
|
|
|
|
+ await this.buildCategoryVideoListForCategory(category.id);
|
|
|
totalCategories++;
|
|
totalCategories++;
|
|
|
|
|
+ totalVideosProcessed += categoryVideoCount;
|
|
|
|
|
|
|
|
// 2. Build tag-filtered video lists
|
|
// 2. Build tag-filtered video lists
|
|
|
const tags = await this.mongoPrisma.tag.findMany({
|
|
const tags = await this.mongoPrisma.tag.findMany({
|
|
@@ -136,33 +141,36 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
for (const tag of tags) {
|
|
for (const tag of tags) {
|
|
|
- await this.buildTagFilteredVideoListForTag(category.id, tag.id);
|
|
|
|
|
|
|
+ const tagVideoCount = await this.buildTagFilteredVideoListForTag(
|
|
|
|
|
+ category.id,
|
|
|
|
|
+ tag.id,
|
|
|
|
|
+ );
|
|
|
totalTagFilteredLists++;
|
|
totalTagFilteredLists++;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ totalTagsProcessed += tags.length;
|
|
|
|
|
+
|
|
|
// 3. Build tag metadata list for this category
|
|
// 3. Build tag metadata list for this category
|
|
|
- await this.buildTagMetadataListForCategory(category.id);
|
|
|
|
|
|
|
+ const tagMetadataCount = await this.buildTagMetadataListForCategory(
|
|
|
|
|
+ category.id,
|
|
|
|
|
+ );
|
|
|
totalTagMetadataLists++;
|
|
totalTagMetadataLists++;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this.logger.log(
|
|
|
|
|
- ` ✅ Channel ${channel.id}: ${categories.length} categories, ${totalTagFilteredLists} tag filters`,
|
|
|
|
|
|
|
+ this.logger.debug(
|
|
|
|
|
+ `[BuildAll] Channel ${channel.id} complete: ${categories.length} categories`,
|
|
|
);
|
|
);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
this.logger.error(
|
|
this.logger.error(
|
|
|
- ` ❌ Error building video cache for channel ${channel.id}`,
|
|
|
|
|
- err instanceof Error ? err.stack : String(err),
|
|
|
|
|
|
|
+ `[BuildAll] Failed for channel ${channel.id}: ${err instanceof Error ? err.message : String(err)}`,
|
|
|
|
|
+ err instanceof Error ? err.stack : undefined,
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this.logger.log(`
|
|
|
|
|
-📊 Video Cache Rebuild Summary:
|
|
|
|
|
- ├─ Channels processed: ${channels.length}
|
|
|
|
|
- ├─ Category video lists built: ${totalCategories} (box:app:video:category:list:*)
|
|
|
|
|
- ├─ Tag-filtered video lists built: ${totalTagFilteredLists} (box:app:video:tag:list:*)
|
|
|
|
|
- └─ Tag metadata lists built: ${totalTagMetadataLists} (box:app:tag:list:*)
|
|
|
|
|
-`);
|
|
|
|
|
|
|
+ this.logger.log(
|
|
|
|
|
+ `[BuildAll] Rebuild complete: channels=${channels.length} categories=${totalCategories} videos=${totalVideosProcessed} tagLists=${totalTagFilteredLists} tagMetadataLists=${totalTagMetadataLists}`,
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -194,13 +202,25 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
|
|
|
* - If Redis operation fails: log error with stack trace and re-throw
|
|
* - If Redis operation fails: log error with stack trace and re-throw
|
|
|
* - All operations are atomic (DEL + RPUSH + EXPIRE)
|
|
* - All operations are atomic (DEL + RPUSH + EXPIRE)
|
|
|
*/
|
|
*/
|
|
|
- async buildCategoryVideoListForCategory(categoryId: string): Promise<void> {
|
|
|
|
|
|
|
+ async buildCategoryVideoListForCategory(categoryId: string): Promise<number> {
|
|
|
try {
|
|
try {
|
|
|
- // Fetch all videos in this category, ordered by addedTime (provider's timestamp)
|
|
|
|
|
|
|
+ this.logger.debug(`[CategoryList] Building for categoryId=${categoryId}`);
|
|
|
|
|
+
|
|
|
|
|
+ // ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+ // PRISMA QUERY
|
|
|
|
|
+ // ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+ // Filters applied:
|
|
|
|
|
+ // - categoryId: exact match (partition key for category videos)
|
|
|
|
|
+ // - listStatus: 1 = only "on shelf" videos (business rule: publishable)
|
|
|
|
|
+ //
|
|
|
|
|
+ // Ordering: by addedTime DESC (provider's upload timestamp), fallback createdAt DESC
|
|
|
|
|
+ // Reason: preserves provider's intended order for video feeds
|
|
|
|
|
+ //
|
|
|
|
|
+ // Selection: ID only (minimal data for Redis LIST storage)
|
|
|
const videos = await this.mongoPrisma.videoMedia.findMany({
|
|
const videos = await this.mongoPrisma.videoMedia.findMany({
|
|
|
where: {
|
|
where: {
|
|
|
categoryId,
|
|
categoryId,
|
|
|
- listStatus: 1, // Only "on shelf" videos
|
|
|
|
|
|
|
+ listStatus: 1, // ✅ Only "on shelf" videos (matches stats endpoint)
|
|
|
},
|
|
},
|
|
|
orderBy: [{ addedTime: 'desc' }, { createdAt: 'desc' }],
|
|
orderBy: [{ addedTime: 'desc' }, { createdAt: 'desc' }],
|
|
|
select: { id: true }, // Only fetch IDs, not full documents
|
|
select: { id: true }, // Only fetch IDs, not full documents
|
|
@@ -210,21 +230,20 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
|
|
|
const key = tsCacheKeys.video.categoryList(categoryId);
|
|
const key = tsCacheKeys.video.categoryList(categoryId);
|
|
|
|
|
|
|
|
if (videoIds.length === 0) {
|
|
if (videoIds.length === 0) {
|
|
|
- this.logger.debug(
|
|
|
|
|
- `No videos found for category ${categoryId}, clearing cache key`,
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ this.logger.debug(`[CategoryList] Empty category: ${categoryId}`);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Atomic write: DEL existing key, RPUSH video IDs, set TTL if configured
|
|
// Atomic write: DEL existing key, RPUSH video IDs, set TTL if configured
|
|
|
await this.cacheHelper.saveVideoIdList(key, videoIds);
|
|
await this.cacheHelper.saveVideoIdList(key, videoIds);
|
|
|
|
|
|
|
|
this.logger.debug(
|
|
this.logger.debug(
|
|
|
- `Built category video list for categoryId=${categoryId}: ${videoIds.length} videos stored`,
|
|
|
|
|
|
|
+ `[CategoryList] Complete: ${categoryId} → ${videoIds.length} videos`,
|
|
|
);
|
|
);
|
|
|
|
|
+ return videoIds.length;
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
this.logger.error(
|
|
this.logger.error(
|
|
|
- `Failed to build category video list for categoryId=${categoryId}`,
|
|
|
|
|
- err instanceof Error ? err.stack : String(err),
|
|
|
|
|
|
|
+ `[CategoryList] Failed for categoryId=${categoryId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
|
|
|
+ err instanceof Error ? err.stack : undefined,
|
|
|
);
|
|
);
|
|
|
throw err;
|
|
throw err;
|
|
|
}
|
|
}
|
|
@@ -263,14 +282,29 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
|
|
|
async buildTagFilteredVideoListForTag(
|
|
async buildTagFilteredVideoListForTag(
|
|
|
categoryId: string,
|
|
categoryId: string,
|
|
|
tagId: string,
|
|
tagId: string,
|
|
|
- ): Promise<void> {
|
|
|
|
|
|
|
+ ): Promise<number> {
|
|
|
try {
|
|
try {
|
|
|
- // Fetch all videos in this category with this tag
|
|
|
|
|
|
|
+ this.logger.debug(
|
|
|
|
|
+ `[TagList] Building for categoryId=${categoryId}, tagId=${tagId}`,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+ // PRISMA QUERY
|
|
|
|
|
+ // ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+ // Filters applied:
|
|
|
|
|
+ // - categoryId: exact match (partition key)
|
|
|
|
|
+ // - listStatus: 1 = only "on shelf" videos (business rule: publishable)
|
|
|
|
|
+ // - tagIds: { has: tagId } = JSON array contains this tag ID
|
|
|
|
|
+ //
|
|
|
|
|
+ // Ordering: by addedTime DESC (provider's upload timestamp), fallback createdAt DESC
|
|
|
|
|
+ // Reason: preserves provider's intended order, filtered by tag membership
|
|
|
|
|
+ //
|
|
|
|
|
+ // Selection: ID only (minimal data for Redis LIST storage)
|
|
|
const videos = await this.mongoPrisma.videoMedia.findMany({
|
|
const videos = await this.mongoPrisma.videoMedia.findMany({
|
|
|
where: {
|
|
where: {
|
|
|
categoryId,
|
|
categoryId,
|
|
|
- listStatus: 1, // Only "on shelf" videos
|
|
|
|
|
- tagIds: { has: tagId }, // Has this specific tag
|
|
|
|
|
|
|
+ listStatus: 1, // ✅ Only "on shelf" videos (matches stats endpoint)
|
|
|
|
|
+ tagIds: { has: tagId }, // ✅ Has this specific tag (matches stats endpoint)
|
|
|
},
|
|
},
|
|
|
orderBy: [{ addedTime: 'desc' }, { createdAt: 'desc' }],
|
|
orderBy: [{ addedTime: 'desc' }, { createdAt: 'desc' }],
|
|
|
select: { id: true }, // Only fetch IDs
|
|
select: { id: true }, // Only fetch IDs
|
|
@@ -281,7 +315,7 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
|
|
|
|
|
|
|
|
if (videoIds.length === 0) {
|
|
if (videoIds.length === 0) {
|
|
|
this.logger.debug(
|
|
this.logger.debug(
|
|
|
- `No videos found for category ${categoryId} with tag ${tagId}, clearing cache key`,
|
|
|
|
|
|
|
+ `[TagList] Empty tag: categoryId=${categoryId}, tagId=${tagId}`,
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -289,12 +323,13 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
|
|
|
await this.cacheHelper.saveVideoIdList(key, videoIds);
|
|
await this.cacheHelper.saveVideoIdList(key, videoIds);
|
|
|
|
|
|
|
|
this.logger.debug(
|
|
this.logger.debug(
|
|
|
- `Built tag video list for categoryId=${categoryId}, tagId=${tagId}: ${videoIds.length} videos stored`,
|
|
|
|
|
|
|
+ `[TagList] Complete: categoryId=${categoryId}, tagId=${tagId} → ${videoIds.length} videos`,
|
|
|
);
|
|
);
|
|
|
|
|
+ return videoIds.length;
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
this.logger.error(
|
|
this.logger.error(
|
|
|
- `Failed to build tag video list for categoryId=${categoryId}, tagId=${tagId}`,
|
|
|
|
|
- err instanceof Error ? err.stack : String(err),
|
|
|
|
|
|
|
+ `[TagList] Failed for categoryId=${categoryId}, tagId=${tagId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
|
|
|
+ err instanceof Error ? err.stack : undefined,
|
|
|
);
|
|
);
|
|
|
throw err;
|
|
throw err;
|
|
|
}
|
|
}
|
|
@@ -349,14 +384,20 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
|
|
|
* categoryId: string
|
|
* categoryId: string
|
|
|
* }
|
|
* }
|
|
|
*/
|
|
*/
|
|
|
- async buildTagMetadataListForCategory(categoryId: string): Promise<void> {
|
|
|
|
|
|
|
+ async buildTagMetadataListForCategory(categoryId: string): Promise<number> {
|
|
|
try {
|
|
try {
|
|
|
|
|
+ this.logger.debug(`[TagMeta] building for categoryId=${categoryId}`);
|
|
|
|
|
+
|
|
|
// Fetch all enabled tags for this category, ordered by seq
|
|
// Fetch all enabled tags for this category, ordered by seq
|
|
|
const tags = await this.mongoPrisma.tag.findMany({
|
|
const tags = await this.mongoPrisma.tag.findMany({
|
|
|
where: { status: 1, categoryId },
|
|
where: { status: 1, categoryId },
|
|
|
orderBy: [{ seq: 'asc' }, { createAt: 'asc' }],
|
|
orderBy: [{ seq: 'asc' }, { createAt: 'asc' }],
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ this.logger.debug(
|
|
|
|
|
+ `[TagMeta] categoryId=${categoryId}, tags found=${tags.length}`,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
if (tags.length === 0) {
|
|
if (tags.length === 0) {
|
|
|
this.logger.debug(
|
|
this.logger.debug(
|
|
|
`No tags found for category ${categoryId}, clearing cache key`,
|
|
`No tags found for category ${categoryId}, clearing cache key`,
|
|
@@ -375,7 +416,9 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
|
|
|
categoryId: tag.categoryId,
|
|
categoryId: tag.categoryId,
|
|
|
}));
|
|
}));
|
|
|
|
|
|
|
|
- const key = `box:app:tag:list:${categoryId}`; // Direct key construction
|
|
|
|
|
|
|
+ const key = tsCacheKeys.tag.metadataByCategory(categoryId);
|
|
|
|
|
+
|
|
|
|
|
+ this.logger.debug(`[TagMeta] Redis key=${key}`);
|
|
|
|
|
|
|
|
// Atomic write: DEL existing key, RPUSH tag JSON, set TTL if configured
|
|
// Atomic write: DEL existing key, RPUSH tag JSON, set TTL if configured
|
|
|
await this.cacheHelper.saveTagList(key, tagPayloads);
|
|
await this.cacheHelper.saveTagList(key, tagPayloads);
|
|
@@ -383,6 +426,7 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
|
|
|
this.logger.debug(
|
|
this.logger.debug(
|
|
|
`Built tag metadata list for categoryId=${categoryId}: ${tagPayloads.length} tags stored`,
|
|
`Built tag metadata list for categoryId=${categoryId}: ${tagPayloads.length} tags stored`,
|
|
|
);
|
|
);
|
|
|
|
|
+ return tagPayloads.length;
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
this.logger.error(
|
|
this.logger.error(
|
|
|
`Failed to build tag metadata list for categoryId=${categoryId}`,
|
|
`Failed to build tag metadata list for categoryId=${categoryId}`,
|