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.
This commit is contained in:
Ben Miller
2025-12-29 02:37:55 -07:00
parent 4b156cb371
commit 7ef5ef2913
3 changed files with 59 additions and 9 deletions

View File

@ -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);
});
},

View File

@ -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,

View File

@ -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 {