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:
@ -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
|
||||
|
||||
Reference in New Issue
Block a user