feat: Add custom video thumbnails for Drive uploads

- Implemented custom thumbnail injection in GASDriveService.getResumableUploadUrl.
- Fetches thumbnails from Google Photos using w320 size to avoid API limits.
- Added strict < 2MB size check for thumbnails.
- Updated mediaHandlers and MediaManager to pass sourceUrl to the backend.
- This allows Drive to display a visual cue immediately for video files still processing.
This commit is contained in:
Ben Miller
2025-12-30 00:38:57 -07:00
parent ebc1a39ce3
commit f1ab3b7b84
3 changed files with 48 additions and 5 deletions

View File

@ -1364,7 +1364,7 @@
})
.withFailureHandler(e => ui.logStatus('error', `Ticket failed: ${e.message}`, 'error'))
.getUploadUrl(state.sku, filename, mimeType);
.getUploadUrl(state.sku, filename, mimeType, fetchUrl);
})
.withFailureHandler(e => {
ui.logStatus('error', `Cannot transfer ${filename}: ${e.message}`, 'error');

View File

@ -128,7 +128,7 @@ export function linkDriveFileToShopifyMedia(sku: string, driveId: string, shopif
}
// NEW: Resumable Upload Ticket
export function getUploadUrl(sku: string, filename: string, mimeType: string) {
export function getUploadUrl(sku: string, filename: string, mimeType: string, sourceUrl?: string) {
const config = new Config()
const driveService = new GASDriveService()
@ -136,7 +136,7 @@ export function getUploadUrl(sku: string, filename: string, mimeType: string) {
const folder = driveService.getOrCreateFolder(sku, config.productPhotosFolderId)
// Generate Ticket
return driveService.getResumableUploadUrl(filename, mimeType, folder.getId())
return driveService.getResumableUploadUrl(filename, mimeType, folder.getId(), sourceUrl)
}
// Deprecated (but kept for fallback/legacy small files if needed)

View File

@ -100,16 +100,59 @@ export class GASDriveService implements IDriveService {
}
}
getResumableUploadUrl(filename: string, mimeType: string, folderId: string): string {
getResumableUploadUrl(filename: string, mimeType: string, folderId: string, sourceUrl?: string): string {
const token = ScriptApp.getOAuthToken();
// Metadata for the file to be created
const metadata = {
const metadata: any = {
name: filename,
mimeType: mimeType,
parents: [folderId]
};
// feature: video-thumbnails-for-processing
// If this is a video from Google Photos, fetch a thumbnail and set as contentHint.
if (sourceUrl && sourceUrl.includes('googleusercontent.com') && mimeType.startsWith('video/')) {
try {
console.log(`[GASDriveService] Fetching thumbnail for ${filename}...`);
// =w800-h600 gives a decent sized jpeg thumbnail
// Use same auth token as we use for the video fetch
let thumbUrl = sourceUrl;
if (!thumbUrl.includes('=')) {
thumbUrl += '=w320-h320';
} else {
thumbUrl = thumbUrl.replace(/=.*$/, '') + '=w320-h320';
}
const thumbResp = UrlFetchApp.fetch(thumbUrl, {
headers: { Authorization: `Bearer ${token}` },
muteHttpExceptions: true
});
if (thumbResp.getResponseCode() === 200) {
const thumbBlob = thumbResp.getBlob();
const base64Thumb = Utilities.base64EncodeWebSafe(thumbBlob.getBytes());
// Drive API Limit check (2MB)
if (base64Thumb.length < 2 * 1024 * 1024) {
metadata.contentHints = {
thumbnail: {
image: base64Thumb,
mimeType: 'image/jpeg'
}
};
console.log(`[GASDriveService] Custom thumbnail injected (${base64Thumb.length} chars).`);
} else {
console.warn(`[GASDriveService] Thumbnail too large (${base64Thumb.length} chars). Skipping injection.`);
}
} else {
console.warn(`[GASDriveService] Thumbnail fetch failed: ${thumbResp.getResponseCode()}`);
}
} catch (e) {
console.warn(`[GASDriveService] Thumbnail generation failed: ${e.message}`);
}
}
const params = {
method: 'post' as const,
contentType: 'application/json',