764 lines
22 KiB
JavaScript
764 lines
22 KiB
JavaScript
/**
|
|
* Tour Pages Service
|
|
*
|
|
* Extends the factory service with reversed video generation for back navigation transitions.
|
|
* Reversed videos are always generated for forward navigation elements to support back navigation.
|
|
*/
|
|
|
|
const Tour_pagesDBApi = require('../db/api/tour_pages');
|
|
const AssetsDBApi = require('../db/api/assets');
|
|
const Asset_variantsDBApi = require('../db/api/asset_variants');
|
|
const { createEntityService } = require('../factories/service.factory');
|
|
const { downloadToBuffer, uploadBuffer } = require('./file');
|
|
const videoProcessing = require('./videoProcessing');
|
|
const { logger } = require('../utils/logger');
|
|
|
|
const projectRegenInProgress = new Set();
|
|
const singleReverseGenerationInProgress = new Set();
|
|
const reverseGenerationPromiseByStorageKey = new Map();
|
|
|
|
// Create base service from factory
|
|
const BaseService = createEntityService(Tour_pagesDBApi, {
|
|
entityName: 'tour_pages',
|
|
});
|
|
|
|
/**
|
|
* Tour Pages Service with reversed video generation
|
|
*/
|
|
class TourPagesService extends BaseService {
|
|
/**
|
|
* Create tour page - generate reversed videos if needed
|
|
*/
|
|
static async create(data, currentUser) {
|
|
// Process reversed videos and get updated ui_schema_json
|
|
const updatedData =
|
|
await TourPagesService.processReversedVideosAndUpdateSchema(
|
|
data,
|
|
currentUser,
|
|
);
|
|
|
|
return super.create(updatedData, currentUser);
|
|
}
|
|
|
|
/**
|
|
* Update tour page - generate reversed videos if needed
|
|
*/
|
|
static async update(data, id, currentUser) {
|
|
// Fetch existing page to get projectId (not included in update request body)
|
|
const existingPage = await Tour_pagesDBApi.findBy({ id });
|
|
const projectId =
|
|
existingPage?.projectId || data.projectId || data.project_id;
|
|
|
|
// Process reversed videos and get updated ui_schema_json
|
|
const updatedData =
|
|
await TourPagesService.processReversedVideosAndUpdateSchema(
|
|
{ ...data, projectId, id },
|
|
currentUser,
|
|
);
|
|
|
|
return super.update(updatedData, id, currentUser);
|
|
}
|
|
|
|
/**
|
|
* Check if element is a back navigation button
|
|
* @private
|
|
*/
|
|
static isBackElement(element) {
|
|
return (
|
|
element.type === 'navigation_prev' ||
|
|
(element.type?.startsWith?.('navigation') && element.navType === 'back')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if element is a forward navigation button with a target and transition
|
|
* @private
|
|
*/
|
|
static isForwardElementWithTarget(element) {
|
|
const isForward =
|
|
element.type === 'navigation_next' ||
|
|
(element.type?.startsWith?.('navigation') &&
|
|
element.navType !== 'back' &&
|
|
element.type !== 'navigation_prev');
|
|
// Check for target (slug or legacy ID) and transition video
|
|
const hasTarget = element.targetPageSlug || element.targetPageId;
|
|
return isForward && hasTarget && element.transitionVideoUrl;
|
|
}
|
|
|
|
/**
|
|
* Process reversed videos and update ui_schema_json with reversed URLs.
|
|
* Returns data with updated ui_schema_json.
|
|
*
|
|
* Generates reversed videos for all navigation elements with transitions.
|
|
*
|
|
* @param {Object} data - Page data with ui_schema_json
|
|
* @param {Object} currentUser - Current user for permissions
|
|
* @param {Object} options - Processing options
|
|
* @param {boolean} options._skipHistoryModeCheck - Skip initial regeneration check
|
|
*/
|
|
static async processReversedVideosAndUpdateSchema(
|
|
data,
|
|
currentUser,
|
|
options = {},
|
|
) {
|
|
let uiSchema = data.ui_schema_json;
|
|
const wasString = typeof uiSchema === 'string';
|
|
|
|
// Parse if string
|
|
if (wasString) {
|
|
try {
|
|
uiSchema = JSON.parse(uiSchema);
|
|
} catch {
|
|
return data; // Return original data if parsing fails
|
|
}
|
|
}
|
|
|
|
if (!uiSchema?.elements || !Array.isArray(uiSchema.elements)) {
|
|
logger.debug({ hasElements: false }, 'No elements in ui_schema_json');
|
|
return data;
|
|
}
|
|
|
|
// Get project ID
|
|
const projectId = data.projectId || data.project_id || data.project;
|
|
|
|
logger.info(
|
|
{ projectId },
|
|
'Processing reversed videos for navigation elements',
|
|
);
|
|
|
|
let wasModified = false;
|
|
|
|
for (const element of uiSchema.elements) {
|
|
const isBack = TourPagesService.isBackElement(element);
|
|
const isForward = TourPagesService.isForwardElementWithTarget(element);
|
|
|
|
// Always generate reversed videos for navigation elements with transitions
|
|
const needsReversed = isBack || isForward;
|
|
|
|
logger.debug(
|
|
{
|
|
elementType: element.type,
|
|
navType: element.navType,
|
|
isBack,
|
|
isForward,
|
|
needsReversed,
|
|
hasTransitionVideo: Boolean(element.transitionVideoUrl),
|
|
targetPageSlug: element.targetPageSlug,
|
|
targetPageId: element.targetPageId,
|
|
},
|
|
'Evaluating element for reversed video',
|
|
);
|
|
|
|
if (!needsReversed) continue;
|
|
if (!element.transitionVideoUrl) continue;
|
|
|
|
// Skip if already has a manually set reverseVideoUrl (separate_video mode)
|
|
if (
|
|
element.transitionReverseMode === 'separate_video' &&
|
|
element.reverseVideoUrl
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const storageKey = element.transitionVideoUrl;
|
|
|
|
try {
|
|
// Fast path: check if reversed variant already exists
|
|
const reversedUrl =
|
|
await TourPagesService.getExistingReversedVariant(storageKey);
|
|
|
|
if (reversedUrl && reversedUrl !== element.reverseVideoUrl) {
|
|
element.reverseVideoUrl = reversedUrl;
|
|
wasModified = true;
|
|
logger.info(
|
|
{
|
|
elementType: element.type,
|
|
isBack,
|
|
isForward,
|
|
storageKey,
|
|
},
|
|
'Added existing reversed video URL to element',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
// Enqueue async generation - doesn't block save request
|
|
if (projectId) {
|
|
TourPagesService.enqueueSingleReverseGeneration({
|
|
projectId,
|
|
pageId: data.id,
|
|
storageKey,
|
|
currentUser,
|
|
});
|
|
}
|
|
} catch (err) {
|
|
logger.error({ err, storageKey }, 'Failed to process reversed variant');
|
|
// Continue without reversed video. Background generation can still populate later.
|
|
}
|
|
}
|
|
|
|
// Always check all project pages for missing reversed videos on any page save.
|
|
// Uses async queue to avoid blocking the save response.
|
|
// Elements that already have reverseVideoUrl are skipped (no duplicate work).
|
|
if (projectId && !options._skipHistoryModeCheck) {
|
|
TourPagesService.enqueueProjectReversedVideosRegeneration(
|
|
projectId,
|
|
currentUser,
|
|
data.id,
|
|
);
|
|
}
|
|
|
|
if (wasModified) {
|
|
return {
|
|
...data,
|
|
// Return same type as input: string if input was string, object otherwise
|
|
ui_schema_json: wasString ? JSON.stringify(uiSchema) : uiSchema,
|
|
};
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Get existing reversed variant URL if already generated.
|
|
* Does not trigger generation.
|
|
*
|
|
* @param {string} storageKey - Original video storage key
|
|
* @returns {Promise<string|null>}
|
|
*/
|
|
static async getExistingReversedVariant(storageKey) {
|
|
const asset = await AssetsDBApi.findBy({ storage_key: storageKey });
|
|
|
|
if (!asset) {
|
|
logger.warn({ storageKey }, 'Asset not found for transition');
|
|
return null;
|
|
}
|
|
|
|
const variants = asset.asset_variants_asset || [];
|
|
const reversedVariant = variants.find((v) => v.variant_type === 'reversed');
|
|
|
|
return reversedVariant?.storage_key || null;
|
|
}
|
|
|
|
/**
|
|
* Enqueue one reversed video generation task without blocking request cycle.
|
|
* Updates all matching project elements after generation succeeds.
|
|
*
|
|
* @param {Object} task
|
|
* @param {string} task.projectId
|
|
* @param {string} task.storageKey
|
|
* @param {Object} task.currentUser
|
|
* @param {string} [task.pageId]
|
|
*/
|
|
static enqueueSingleReverseGeneration({
|
|
projectId,
|
|
storageKey,
|
|
currentUser,
|
|
pageId,
|
|
}) {
|
|
if (!projectId || !storageKey) return;
|
|
|
|
const taskKey = `${projectId}:${storageKey}`;
|
|
if (singleReverseGenerationInProgress.has(taskKey)) {
|
|
return;
|
|
}
|
|
|
|
singleReverseGenerationInProgress.add(taskKey);
|
|
|
|
setImmediate(async () => {
|
|
const log = logger.child({
|
|
projectId,
|
|
storageKey,
|
|
pageId,
|
|
operation: 'singleReverseGeneration',
|
|
});
|
|
|
|
try {
|
|
log.info('Starting background reversed variant generation');
|
|
const reversedUrl = await TourPagesService.getOrGenerateReversedVariant(
|
|
storageKey,
|
|
currentUser,
|
|
);
|
|
|
|
if (!reversedUrl) {
|
|
log.warn(
|
|
'Background reversed variant generation finished without result',
|
|
);
|
|
return;
|
|
}
|
|
|
|
await TourPagesService.applyReversedUrlToProjectElements(
|
|
projectId,
|
|
storageKey,
|
|
reversedUrl,
|
|
currentUser,
|
|
);
|
|
} catch (err) {
|
|
log.error({ err }, 'Background reversed generation failed');
|
|
} finally {
|
|
singleReverseGenerationInProgress.delete(taskKey);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Enqueue project-wide reverse regeneration (history mode) without blocking save request.
|
|
*
|
|
* @param {string} projectId
|
|
* @param {Object} currentUser
|
|
* @param {string} [excludePageId]
|
|
*/
|
|
static enqueueProjectReversedVideosRegeneration(
|
|
projectId,
|
|
currentUser,
|
|
excludePageId,
|
|
) {
|
|
if (!projectId) return;
|
|
if (projectRegenInProgress.has(projectId)) return;
|
|
|
|
projectRegenInProgress.add(projectId);
|
|
|
|
setImmediate(async () => {
|
|
try {
|
|
await TourPagesService.regenerateProjectReversedVideos(
|
|
projectId,
|
|
currentUser,
|
|
excludePageId,
|
|
);
|
|
} catch (err) {
|
|
logger.error(
|
|
{ err, projectId },
|
|
'Background project regeneration failed',
|
|
);
|
|
} finally {
|
|
projectRegenInProgress.delete(projectId);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Write generated reversed URL into all matching navigation elements in dev pages.
|
|
*
|
|
* @param {string} projectId
|
|
* @param {string} storageKey
|
|
* @param {string} reversedUrl
|
|
* @param {Object} currentUser
|
|
*/
|
|
static async applyReversedUrlToProjectElements(
|
|
projectId,
|
|
storageKey,
|
|
reversedUrl,
|
|
currentUser,
|
|
) {
|
|
if (!projectId || !storageKey || !reversedUrl) return;
|
|
|
|
const log = logger.child({
|
|
projectId,
|
|
storageKey,
|
|
operation: 'applyReversedUrlToProjectElements',
|
|
});
|
|
|
|
const { rows: pages } = await Tour_pagesDBApi.findAll({
|
|
projectId,
|
|
environment: 'dev',
|
|
});
|
|
|
|
let pagesUpdated = 0;
|
|
|
|
for (const page of pages) {
|
|
const uiSchema =
|
|
typeof page.ui_schema_json === 'string'
|
|
? JSON.parse(page.ui_schema_json || '{}')
|
|
: page.ui_schema_json || {};
|
|
|
|
if (!uiSchema.elements || !Array.isArray(uiSchema.elements)) continue;
|
|
|
|
let pageModified = false;
|
|
|
|
for (const element of uiSchema.elements) {
|
|
const shouldAttachReverse =
|
|
TourPagesService.isBackElement(element) ||
|
|
TourPagesService.isForwardElementWithTarget(element);
|
|
|
|
if (!shouldAttachReverse) continue;
|
|
if (element.transitionVideoUrl !== storageKey) continue;
|
|
if (element.reverseVideoUrl) continue;
|
|
|
|
element.reverseVideoUrl = reversedUrl;
|
|
pageModified = true;
|
|
}
|
|
|
|
if (!pageModified) continue;
|
|
|
|
await Tour_pagesDBApi.partialUpdate(
|
|
page.id,
|
|
{ ui_schema_json: JSON.stringify(uiSchema) },
|
|
{ currentUser },
|
|
);
|
|
pagesUpdated++;
|
|
}
|
|
|
|
if (pagesUpdated > 0) {
|
|
log.info({ pagesUpdated }, 'Applied reversed URL to project elements');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get existing reversed variant URL or generate one
|
|
* @param {string} storageKey - Original video storage key
|
|
* @param {Object} currentUser - Current user for permissions
|
|
* @returns {Promise<string|null>} Reversed video storage key or null
|
|
*/
|
|
static async getOrGenerateReversedVariant(storageKey, currentUser) {
|
|
const existingVariant =
|
|
await TourPagesService.getExistingReversedVariant(storageKey);
|
|
|
|
if (existingVariant) {
|
|
return existingVariant;
|
|
}
|
|
|
|
if (reverseGenerationPromiseByStorageKey.has(storageKey)) {
|
|
return reverseGenerationPromiseByStorageKey.get(storageKey);
|
|
}
|
|
|
|
const generationPromise = (async () => {
|
|
// Find the asset by storage key
|
|
const asset = await AssetsDBApi.findBy({ storage_key: storageKey });
|
|
|
|
if (!asset) {
|
|
logger.warn({ storageKey }, 'Asset not found for transition');
|
|
return null;
|
|
}
|
|
|
|
// Generate reversed video
|
|
return TourPagesService.generateReversedVariant(asset, currentUser);
|
|
})();
|
|
|
|
reverseGenerationPromiseByStorageKey.set(storageKey, generationPromise);
|
|
|
|
try {
|
|
return await generationPromise;
|
|
} finally {
|
|
reverseGenerationPromiseByStorageKey.delete(storageKey);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate reversed video variant for an asset
|
|
* @returns {Promise<string|null>} Reversed video storage key or null
|
|
*/
|
|
static async generateReversedVariant(asset, currentUser) {
|
|
const log = logger.child({ assetId: asset.id });
|
|
log.info('Generating reversed video variant');
|
|
|
|
try {
|
|
// Check if FFmpeg is available
|
|
const ffmpegAvailable = await videoProcessing.isFFmpegAvailable();
|
|
if (!ffmpegAvailable) {
|
|
log.error('FFmpeg is not available on this server');
|
|
return null;
|
|
}
|
|
|
|
// Download original video to buffer
|
|
const originalBuffer = await downloadToBuffer(asset.storage_key);
|
|
|
|
// Generate reversed video
|
|
const reversedBuffer = await videoProcessing.reverseVideo(
|
|
originalBuffer,
|
|
asset.original_file_name || 'video.mp4',
|
|
);
|
|
|
|
// Upload reversed video to storage
|
|
const reversedKey = `assets/${asset.id}/reversed.mp4`;
|
|
|
|
const result = await uploadBuffer(reversedKey, reversedBuffer, {
|
|
contentType: 'video/mp4',
|
|
});
|
|
|
|
// Create variant record
|
|
await Asset_variantsDBApi.create(
|
|
{
|
|
assetId: asset.id,
|
|
variant_type: 'reversed',
|
|
cdn_url: result.url,
|
|
storage_key: reversedKey,
|
|
size_mb: reversedBuffer.length / (1024 * 1024),
|
|
},
|
|
{ currentUser },
|
|
);
|
|
|
|
log.info(
|
|
{ reversedKey, size: reversedBuffer.length },
|
|
'Reversed variant created',
|
|
);
|
|
|
|
return reversedKey;
|
|
} catch (err) {
|
|
log.error({ err }, 'Failed to generate reversed variant');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Regenerate reversed videos for all forward navigation elements in project.
|
|
* Called when history-mode back navigation is first enabled in a project.
|
|
*
|
|
* @param {string} projectId - Project ID
|
|
* @param {Object} currentUser - Current user for permissions
|
|
* @param {string} excludePageId - Optional page ID to exclude (the page being saved)
|
|
*/
|
|
static async regenerateProjectReversedVideos(
|
|
projectId,
|
|
currentUser,
|
|
excludePageId,
|
|
) {
|
|
const log = logger.child({ projectId, operation: 'regenerateReversed' });
|
|
log.info('Starting project-wide reversed video regeneration');
|
|
|
|
try {
|
|
// Only process dev environment pages (constructor works with dev)
|
|
const { rows: pages } = await Tour_pagesDBApi.findAll({
|
|
projectId,
|
|
environment: 'dev',
|
|
});
|
|
|
|
let pagesUpdated = 0;
|
|
let elementsProcessed = 0;
|
|
|
|
for (const page of pages) {
|
|
// Skip the page that triggered this regeneration (it's already being processed)
|
|
if (excludePageId && page.id === excludePageId) {
|
|
continue;
|
|
}
|
|
|
|
const uiSchema =
|
|
typeof page.ui_schema_json === 'string'
|
|
? JSON.parse(page.ui_schema_json || '{}')
|
|
: page.ui_schema_json || {};
|
|
|
|
if (!uiSchema.elements || !Array.isArray(uiSchema.elements)) {
|
|
continue;
|
|
}
|
|
|
|
let pageModified = false;
|
|
|
|
for (const element of uiSchema.elements) {
|
|
// Process both forward elements AND back elements with their own transition
|
|
const isForward =
|
|
TourPagesService.isForwardElementWithTarget(element);
|
|
const isBackWithTransition =
|
|
TourPagesService.isBackElement(element) &&
|
|
element.transitionVideoUrl;
|
|
|
|
log.debug(
|
|
{
|
|
pageId: page.id,
|
|
elementType: element.type,
|
|
navType: element.navType,
|
|
isForward,
|
|
isBackWithTransition,
|
|
hasTransitionVideo: Boolean(element.transitionVideoUrl),
|
|
hasReverseVideo: Boolean(element.reverseVideoUrl),
|
|
targetPageSlug: element.targetPageSlug,
|
|
targetPageId: element.targetPageId,
|
|
},
|
|
'Checking element in regeneration',
|
|
);
|
|
|
|
// Skip if neither forward nor back-with-transition
|
|
if (!isForward && !isBackWithTransition) {
|
|
continue;
|
|
}
|
|
|
|
if (!element.transitionVideoUrl) continue;
|
|
if (element.reverseVideoUrl) continue; // Already has reversed
|
|
|
|
try {
|
|
const reversedUrl =
|
|
await TourPagesService.getOrGenerateReversedVariant(
|
|
element.transitionVideoUrl,
|
|
currentUser,
|
|
);
|
|
|
|
if (reversedUrl) {
|
|
element.reverseVideoUrl = reversedUrl;
|
|
pageModified = true;
|
|
elementsProcessed++;
|
|
}
|
|
} catch (err) {
|
|
log.error(
|
|
{ err, pageId: page.id, elementType: element.type },
|
|
'Failed to generate reversed video for element',
|
|
);
|
|
}
|
|
}
|
|
|
|
if (pageModified) {
|
|
// Use partialUpdate to only update ui_schema_json field
|
|
// This avoids the regular update's getFieldMapping which sets other fields to null
|
|
await Tour_pagesDBApi.partialUpdate(
|
|
page.id,
|
|
{ ui_schema_json: JSON.stringify(uiSchema) },
|
|
{ currentUser },
|
|
);
|
|
pagesUpdated++;
|
|
}
|
|
}
|
|
|
|
log.info(
|
|
{ pagesUpdated, elementsProcessed },
|
|
'Completed project-wide reversed video regeneration',
|
|
);
|
|
} catch (err) {
|
|
log.error({ err }, 'Failed to regenerate project reversed videos');
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Populate reverseVideoUrl for navigation elements from existing variants.
|
|
* This eagerly looks up reversed variants that were generated asynchronously,
|
|
* ensuring the frontend has the reversed URL even if the page wasn't re-saved
|
|
* after variant generation completed.
|
|
*
|
|
* @param {Object|Array} pages - Single page or array of pages
|
|
* @returns {Promise<Object|Array>} Pages with populated reverseVideoUrl fields
|
|
*/
|
|
static async populateReverseVideoUrls(pages) {
|
|
if (!pages) return pages;
|
|
|
|
const isArray = Array.isArray(pages);
|
|
const pageList = isArray ? pages : [pages];
|
|
|
|
// Collect all storage keys that need lookup
|
|
const storageKeysToLookup = new Set();
|
|
|
|
for (const page of pageList) {
|
|
if (!page?.ui_schema_json) continue;
|
|
|
|
const uiSchema =
|
|
typeof page.ui_schema_json === 'string'
|
|
? JSON.parse(page.ui_schema_json || '{}')
|
|
: page.ui_schema_json || {};
|
|
|
|
if (!uiSchema?.elements || !Array.isArray(uiSchema.elements)) continue;
|
|
|
|
for (const element of uiSchema.elements) {
|
|
if (!element.transitionVideoUrl) continue;
|
|
if (element.reverseVideoUrl) continue; // Already has reversed URL
|
|
|
|
// Only auto_reverse mode needs lookup; separate_video has manual URL
|
|
if (element.transitionReverseMode === 'separate_video') continue;
|
|
|
|
storageKeysToLookup.add(element.transitionVideoUrl);
|
|
}
|
|
}
|
|
|
|
if (storageKeysToLookup.size === 0) {
|
|
return pages;
|
|
}
|
|
|
|
// Batch lookup all reversed variants
|
|
const reversedUrlByStorageKey = new Map();
|
|
|
|
for (const storageKey of storageKeysToLookup) {
|
|
try {
|
|
const reversedUrl =
|
|
await TourPagesService.getExistingReversedVariant(storageKey);
|
|
if (reversedUrl) {
|
|
reversedUrlByStorageKey.set(storageKey, reversedUrl);
|
|
}
|
|
} catch {
|
|
// Ignore lookup errors - element will just have no reverseVideoUrl
|
|
}
|
|
}
|
|
|
|
if (reversedUrlByStorageKey.size === 0) {
|
|
return pages;
|
|
}
|
|
|
|
// Apply reversed URLs to pages
|
|
const modifiedPages = pageList.map((page) => {
|
|
if (!page?.ui_schema_json) return page;
|
|
|
|
const uiSchema =
|
|
typeof page.ui_schema_json === 'string'
|
|
? JSON.parse(page.ui_schema_json || '{}')
|
|
: page.ui_schema_json || {};
|
|
|
|
if (!uiSchema?.elements || !Array.isArray(uiSchema.elements)) return page;
|
|
|
|
let modified = false;
|
|
|
|
for (const element of uiSchema.elements) {
|
|
if (!element.transitionVideoUrl) continue;
|
|
if (element.reverseVideoUrl) continue;
|
|
if (element.transitionReverseMode === 'separate_video') continue;
|
|
|
|
const reversedUrl = reversedUrlByStorageKey.get(
|
|
element.transitionVideoUrl,
|
|
);
|
|
if (reversedUrl) {
|
|
element.reverseVideoUrl = reversedUrl;
|
|
modified = true;
|
|
}
|
|
}
|
|
|
|
if (!modified) return page;
|
|
|
|
// Return modified page with updated ui_schema_json
|
|
const plainPage = page.get ? page.get({ plain: true }) : { ...page };
|
|
return {
|
|
...plainPage,
|
|
ui_schema_json: JSON.stringify(uiSchema),
|
|
};
|
|
});
|
|
|
|
return isArray ? modifiedPages : modifiedPages[0];
|
|
}
|
|
|
|
/**
|
|
* Check reverse video generation status for given storage keys.
|
|
* Returns which keys have reversed variants ready.
|
|
*
|
|
* @param {string[]} storageKeys - Array of original video storage keys to check
|
|
* @returns {Promise<Object>} Status object with ready keys and their reversed URLs
|
|
*/
|
|
static async checkReverseVideoStatus(storageKeys) {
|
|
if (
|
|
!storageKeys ||
|
|
!Array.isArray(storageKeys) ||
|
|
storageKeys.length === 0
|
|
) {
|
|
return { ready: {}, pending: [], allReady: true };
|
|
}
|
|
|
|
const ready = {};
|
|
const pending = [];
|
|
|
|
for (const storageKey of storageKeys) {
|
|
if (!storageKey) continue;
|
|
|
|
try {
|
|
const reversedUrl =
|
|
await TourPagesService.getExistingReversedVariant(storageKey);
|
|
if (reversedUrl) {
|
|
ready[storageKey] = reversedUrl;
|
|
} else {
|
|
pending.push(storageKey);
|
|
}
|
|
} catch {
|
|
pending.push(storageKey);
|
|
}
|
|
}
|
|
|
|
return {
|
|
ready,
|
|
pending,
|
|
allReady: pending.length === 0,
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = TourPagesService;
|