From 7ef5ef2913c0387fb6d73400279208ac1c69d2b0 Mon Sep 17 00:00:00 2001 From: Ben Miller Date: Mon, 29 Dec 2025 02:37:55 -0700 Subject: [PATCH] fix(media): resolve google photos video import treating videos as images This commit fixes a bug where videos imported from the Google Photos Picker were being downloaded as static thumbnails. Changes include: 1. Frontend (MediaManager.html): Correctly access nested 'mediaFile' properties from the Picker API response to ensure valid mimeType and filename are passed to the backend. Restored logic to force 'video/mp4' mimeType if 'mediaMetadata.video' is present. Added debug logging. 2. Backend (mediaHandlers.ts): Restored missing 'else if' block for URL handling that was causing 'No File ID' errors. Implemented logic to append '=dv' parameter for video downloads. Added safeguard to rename downloaded files to '.mp4' if the content type is video but the extension is wrong. --- src/MediaManager.html | 20 ++++++++++++++++++-- src/mediaHandlers.test.ts | 16 ++++++++++++++++ src/mediaHandlers.ts | 32 +++++++++++++++++++++++++------- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/MediaManager.html b/src/MediaManager.html index ed14e2e..6c534a9 100644 --- a/src/MediaManager.html +++ b/src/MediaManager.html @@ -1173,7 +1173,23 @@ processPhotoItems(items) { let done = 0; items.forEach(item => { - const url = (item.mediaFile && item.mediaFile.baseUrl) ? item.mediaFile.baseUrl : item.baseUrl; + console.log("[MediaManager] Processing Item:", JSON.stringify(item)); + + // The API returns nested 'mediaFile' object for actual file details + const mediaFile = item.mediaFile || item; + + const url = mediaFile.baseUrl || item.baseUrl; + const filename = mediaFile.filename || item.filename; + let mimeType = mediaFile.mimeType || item.mimeType; + + console.log(`[MediaManager] Extracted: URL=${url ? 'Yes' : 'No'}, Mime=${mimeType}, Name=${filename}`); + + // Force video mimeType if metadata indicates video (Critical for backend =dv param) + if (item.mediaMetadata && item.mediaMetadata.video) { + console.log("[MediaManager] Metadata indicates VIDEO. Forcing video/mp4."); + mimeType = 'video/mp4'; + } + google.script.run .withSuccessHandler(() => { done++; @@ -1183,7 +1199,7 @@ setTimeout(() => ui.closePhotoSession(), 2000); } }) - .importFromPicker(state.sku, null, item.mimeType, item.filename, url); + .importFromPicker(state.sku, null, mimeType, filename, url); }); }, diff --git a/src/mediaHandlers.test.ts b/src/mediaHandlers.test.ts index e9e3163..bd3f6cf 100644 --- a/src/mediaHandlers.test.ts +++ b/src/mediaHandlers.test.ts @@ -183,6 +183,22 @@ describe("mediaHandlers", () => { expect(mockFile.moveTo).toHaveBeenCalled() }) + test("should append =dv to video URLs from Google Photos", () => { + importFromPicker("SKU123", null, "video/mp4", "video.mp4", "https://lh3.googleusercontent.com/some-id") + expect(UrlFetchApp.fetch).toHaveBeenCalledWith( + "https://lh3.googleusercontent.com/some-id=dv", + expect.anything() + ) + }) + + test("should append =d to image URLs from Google Photos", () => { + importFromPicker("SKU123", null, "image/jpeg", "image.jpg", "https://lh3.googleusercontent.com/some-id") + expect(UrlFetchApp.fetch).toHaveBeenCalledWith( + "https://lh3.googleusercontent.com/some-id=d", + expect.anything() + ) + }) + test("should handle 403 Forbidden on Download", () => { ;(UrlFetchApp.fetch as jest.Mock).mockReturnValue({ getResponseCode: () => 403, diff --git a/src/mediaHandlers.ts b/src/mediaHandlers.ts index 1fbebb6..85d3844 100644 --- a/src/mediaHandlers.ts +++ b/src/mediaHandlers.ts @@ -162,11 +162,25 @@ export function importFromPicker(sku: string, fileId: string, mimeType: string, finalFile = source.makeCopy(name); // Default location console.log(`Step 1 Success: Drive File copied to Root/Default. ID: ${finalFile.getId()}`); } else if (imageUrl) { + console.log(`[importFromPicker] Input: Mime=${mimeType}, Name=${name}, URL=${imageUrl}`); + // Case B: URL (Photos) -> Blob -> File // Handling high-res parameter - if (imageUrl.includes("googleusercontent.com") && !imageUrl.includes("=d")) { - imageUrl += "=d"; // Download param + if (imageUrl.includes("googleusercontent.com")) { + if (mimeType && mimeType.startsWith("video/")) { + // For videos, =dv retrieves the actual video file (download video) + if (!imageUrl.includes("=dv")) { + imageUrl += "=dv"; + } + } else { + // For images, =d retrieves the full download + if (!imageUrl.includes("=d")) { + imageUrl += "=d"; + } + } } + console.log(`[importFromPicker] Fetching URL: ${imageUrl}`); + const response = UrlFetchApp.fetch(imageUrl, { headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` @@ -181,20 +195,24 @@ export function importFromPicker(sku: string, fileId: string, mimeType: string, } const blob = response.getBlob(); console.log(`Blob Content-Type: ${blob.getContentType()}`); - // console.log(`Blob Size: ${blob.getBytes().length} bytes`); // Commented out to save memory if huge - if (blob.getContentType().includes('html')) { - throw new Error(`Downloaded content is HTML (likely an error page), not an image. Body peek: ${response.getContentText().substring(0,200)}`); + let fileName = name || `photo_${Date.now()}.jpg`; + + // Fix Filename Extension if MimeType mismatch + // (e.g. we downloaded a video, but filename is .jpg) + if (blob.getContentType().startsWith('video/') && fileName.match(/\.jpg|\.png|\.jpeg$/i)) { + console.log(`[importFromPicker] Filename extension correction needed for video. Old: ${fileName}`); + fileName = fileName.replace(/\.[^/.]+$/, "") + ".mp4"; + console.log(`[importFromPicker] New Filename: ${fileName}`); } - const fileName = name || `photo_${Date.now()}.jpg`; blob.setName(fileName); try { // Sanitize blob to remove any hidden metadata causing DriveApp issues const cleanBlob = Utilities.newBlob(blob.getBytes(), blob.getContentType(), fileName); finalFile = DriveApp.createFile(cleanBlob); // Creates in Root - console.log(`Step 1 Success: Photo downloaded to Root. ID: ${finalFile.getId()}`); + console.log(`Step 1 Success: File created in Root. ID: ${finalFile.getId()}, Mime: ${finalFile.getMimeType()}`); } catch (createErr) { console.warn("DriveApp.createFile failed with clean blob. Trying Advanced Drive API...", createErr); try {