feat(media): implement video processing polling and fallback

This commit adds robust handling for Google Drive videos that are still processing (lacking thumbnails).  Changes include:  1. Backend (MediaService.ts): Implement try/catch around thumbnail generation. If it fails, return a placeholder and flag the item as 'isProcessing'. 2. Frontend (MediaManager.html):     - Add polling logic to check for updates on processing items every 15s.     - Add UI support for processing state: slate background, centered animated hourglass emoji.     - Implement sand animation (toggling hourglass state) and rotation animation (180deg flip on poll event).     - Fix badges and positioning issues.
This commit is contained in:
Ben Miller
2025-12-29 09:12:37 -07:00
parent 7ef5ef2913
commit f6831cdc8f
2 changed files with 670 additions and 514 deletions

File diff suppressed because it is too large Load Diff

View File

@ -111,6 +111,8 @@ export class MediaService {
// Match Logic (Strict ID Match Only) // Match Logic (Strict ID Match Only)
driveFileStats.forEach(d => { driveFileStats.forEach(d => {
let match = null let match = null
let isProcessing = false
let thumbnail = "";
// 1. ID Match // 1. ID Match
if (d.shopifyId) { if (d.shopifyId) {
@ -120,22 +122,37 @@ export class MediaService {
// NO Filename Fallback matching per new design "Strict Linkage" // NO Filename Fallback matching per new design "Strict Linkage"
if (match && match.preview && match.preview.image && match.preview.image.originalSrc) {
thumbnail = match.preview.image.originalSrc;
} else {
try {
// Try to get Drive thumbnail
thumbnail = `data:image/jpeg;base64,${Utilities.base64Encode(d.file.getThumbnail().getBytes())}`;
} catch (e) {
console.warn(`Failed to get thumbnail for ${d.file.getName()} (likely processing): ${e}`);
// Return a generic placeholder (Gray 1x1 pixel) + Flag as processing
// thumbnail = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
// Better placeholder: https://ssl.gstatic.com/docs/doclist/images/icon_10_movie_list.png (Video Icon) or just gray
thumbnail = "https://ssl.gstatic.com/docs/doclist/images/icon_128_video_blue.png"; // Official Video Icon
isProcessing = true;
}
}
unifiedState.push({ unifiedState.push({
id: d.file.getId(), // Use Drive ID as primary key id: d.file.getId(), // Use Drive ID as primary key
driveId: d.file.getId(), driveId: d.file.getId(),
shopifyId: match ? match.id : null, shopifyId: match ? match.id : null,
filename: d.file.getName(), filename: d.file.getName(),
source: match ? 'synced' : 'drive_only', source: match ? 'synced' : 'drive_only',
thumbnail: (match && match.preview && match.preview.image && match.preview.image.originalSrc) thumbnail: thumbnail,
? match.preview.image.originalSrc
: `data:image/jpeg;base64,${Utilities.base64Encode(d.file.getThumbnail().getBytes())}`,
status: 'active', status: 'active',
galleryOrder: d.galleryOrder, galleryOrder: d.galleryOrder,
mimeType: d.file.getMimeType(), mimeType: d.file.getMimeType(),
// Prefer Shopify Video URL for playback/hover if available, otherwise Drive Download URL // Prefer Shopify Video URL for playback/hover if available, otherwise Drive Download URL
contentUrl: (match && match.sources) contentUrl: (match && match.sources)
? (match.sources.find((s: any) => s.mimeType === 'video/mp4')?.url || match.sources[0]?.url) ? (match.sources.find((s: any) => s.mimeType === 'video/mp4')?.url || match.sources[0]?.url)
: `https://drive.google.com/uc?export=download&id=${d.file.getId()}` : `https://drive.google.com/uc?export=download&id=${d.file.getId()}`,
isProcessing: isProcessing
}) })
// console.log(`[MediaService] File ${d.file.getName()} (${d.file.getId()}): Mime=${d.file.getMimeType()}, ContentUrl=https://drive.google.com/uc?export=download&id=${d.file.getId()}`) // console.log(`[MediaService] File ${d.file.getName()} (${d.file.getId()}): Mime=${d.file.getMimeType()}, ContentUrl=https://drive.google.com/uc?export=download&id=${d.file.getId()}`)
}) })