Feature: Robust Google Photos Integration & Media Hardening

- Implemented Google Photos Picker with Session API.
- Fixed 403 Forbidden errors by adding OAuth headers to download requests.
- Implemented MediaHandler resilience:
  - 3-Step Import (Save to Root -> Verify Folder -> Move).
  - Advanced Drive API Fallback (v3/v2) for file creation.
  - Blob Sanitization (Utilities.newBlob) to fix server errors.
- Enabled Advanced Drive Service in ppsscript.json.
- Updated Documentation (MEMORY.md, ARCHITECTURE.md) with findings.
This commit is contained in:
2025-12-26 01:51:04 -07:00
parent 95094b1674
commit 50ddfc9e15
7 changed files with 403 additions and 22 deletions

View File

@ -162,8 +162,21 @@
</div>
<button onclick="openPicker()" class="btn btn-secondary" style="margin-top: 8px; font-size: 13px;">
Import from Google Drive / Photos
Import from Google Drive
</button>
<button onclick="startPhotoSession()" class="btn btn-secondary" style="margin-top: 4px; font-size: 13px;">
Import from Google Photos
</button>
<div id="photos-session-ui"
style="display:none; margin-top:12px; padding:12px; background:#f0f9ff; border-radius:8px; border:1px solid #bae6fd;">
<div style="font-weight:500; font-size:13px; margin-bottom:4px;">Pick Photos</div>
<a id="photos-session-link" href="#" target="_blank"
style="font-size:13px; color:#0284c7; text-decoration:none; display:block; margin-bottom:8px;">
Active Session (Click to Open) ↗
</a>
<div id="photos-session-status" style="font-size:11px; color:#64748b;">Waiting for selection...</div>
</div>
</div>
<div class="card">
@ -308,13 +321,11 @@
.setIncludeFolders(true)
.setSelectFolderEnabled(false);
const photosView = new google.picker.PhotosView();
const picker = new google.picker.PickerBuilder()
.addView(view)
.addView(photosView)
.setOAuthToken(config.token)
.setDeveloperKey(config.apiKey)
.setOrigin(google.script.host.origin)
.setCallback(pickerCallback)
.build();
@ -323,13 +334,103 @@
function pickerCallback(data) {
if (data.action == google.picker.Action.PICKED) {
const fileId = data.docs[0].id;
const mimeType = data.docs[0].mimeType;
const doc = data.docs[0];
const fileId = doc.id;
const mimeType = doc.mimeType;
const name = doc.name;
const url = doc.url; // Often the link to the file in Drive/Photos
// For Photos, we might need the direct image URL, which is often in thumbnails or requires specific handling
// doc.thumbnails contains 's75-c' style URLs. We can strip the size to get full size?
// Actually, for Photos API items, 'url' might be the user-facing URL.
// Let's pass the 'thumbnails' closest to original if possible, or just pass the whole doc object to backend?
// Simpler: pass specific fields.
const imageUrl = (doc.thumbnails && doc.thumbnails.length > 0) ? doc.thumbnails[doc.thumbnails.length - 1].url : null;
google.script.run
.withSuccessHandler(() => loadMedia(currentSku))
.importFromPicker(currentSku, fileId, mimeType);
}
.importFromPicker(currentSku, fileId, mimeType, name, imageUrl);
}
}
// --- Photos Session Logic (New API) ---
let pollingTimer = null;
function startPhotoSession() {
// Reset UI
document.getElementById('photos-session-ui').style.display = 'block';
document.getElementById('photos-session-status').innerText = "Creating session...";
document.getElementById('photos-session-link').style.display = 'none';
google.script.run
.withSuccessHandler(onSessionCreated)
.withFailureHandler(e => {
alert('Failed to start session: ' + e.message);
document.getElementById('photos-session-ui').style.display = 'none';
})
.createPhotoSession();
}
function onSessionCreated(session) {
if (!session || !session.pickerUri) {
alert("Failed to get picker URI");
return;
}
const link = document.getElementById('photos-session-link');
link.href = session.pickerUri;
link.style.display = 'block';
link.innerText = "Click here to pick photos ↗";
document.getElementById('photos-session-status').innerText = "Waiting for you to pick photos...";
// Open automatically? Browsers block it. User must click.
// Start polling
if (pollingTimer) clearInterval(pollingTimer);
pollingTimer = setInterval(() => pollSession(session.id), 2000); // Poll every 2s
}
function pollSession(sessionId) {
google.script.run
.withSuccessHandler(result => {
console.log("Poll result:", result);
if (result.status === 'complete') {
clearInterval(pollingTimer);
document.getElementById('photos-session-status').innerText = "Importing photos...";
processPickedPhotos(result.mediaItems);
} else if (result.status === 'error') {
document.getElementById('photos-session-status').innerText = "Error: " + result.message;
}
})
.checkPhotoSession(sessionId);
}
function processPickedPhotos(items) {
// Reuse importFromPicker logic logic?
// We can call importFromPicker for each item.
let processed = 0;
items.forEach(item => {
// console.log("Processing item:", item);
// The new Picker API returns baseUrl nested inside mediaFile
const imageUrl = (item.mediaFile && item.mediaFile.baseUrl) ? item.mediaFile.baseUrl : item.baseUrl;
google.script.run
.withSuccessHandler(() => {
processed++;
if (processed === items.length) {
document.getElementById('photos-session-status').innerText = "Done!";
loadMedia(currentSku);
setTimeout(() => {
document.getElementById('photos-session-ui').style.display = 'none';
}, 3000);
}
})
.importFromPicker(currentSku, null, item.mimeType, item.filename, imageUrl);
});
}
// Start