import { GASSpreadsheetService } from "./services/GASSpreadsheetService" import { GASDriveService } from "./services/GASDriveService" import { ShopifyMediaService } from "./services/ShopifyMediaService" import { GASNetworkService } from "./services/GASNetworkService" import { MediaService } from "./services/MediaService" import { Shop } from "./shopifyApi" import { Config } from "./config" import { Product } from "./Product" export function showMediaManager() { const html = HtmlService.createHtmlOutputFromFile("MediaManager") .setTitle("Media Manager") .setWidth(1100) .setHeight(750); SpreadsheetApp.getUi().showModalDialog(html, "Media Manager"); } export function getSelectedSku(): string | null { const ss = new GASSpreadsheetService() const sheet = SpreadsheetApp.getActiveSheet() if (sheet.getName() !== "product_inventory") return null const row = sheet.getActiveRange().getRow() if (row <= 1) return null // Header const sku = ss.getCellValueByColumnName("product_inventory", row, "sku") return sku ? String(sku) : null } export function getPickerConfig() { const config = new Config() return { apiKey: config.googlePickerApiKey, token: ScriptApp.getOAuthToken(), email: Session.getEffectiveUser().getEmail(), parentId: config.productPhotosFolderId // Root folder to start picker in? Optionally could be SKU folder } } export function getMediaForSku(sku: string): any[] { const driveService = new GASDriveService() try { const config = new Config() // Moved inside try block to catch init errors const folder = driveService.getOrCreateFolder(sku, config.productPhotosFolderId) const files = driveService.getFiles(folder.getId()) return files.map(f => { let thumb = "" try { const bytes = f.getThumbnail().getBytes() thumb = "data:image/png;base64," + Utilities.base64Encode(bytes) } catch (e) { console.log(`Failed to get thumbnail for ${f.getName()}`) // Fallback or empty } return { id: f.getId(), name: f.getName(), thumbnailLink: thumb } }) } catch (e) { console.error(e) return [] } } export function saveFileToDrive(sku: string, filename: string, mimeType: string, base64Data: string) { const config = new Config() const driveService = new GASDriveService() const folder = driveService.getOrCreateFolder(sku, config.productPhotosFolderId) const blob = Utilities.newBlob(Utilities.base64Decode(base64Data), mimeType, filename) driveService.saveFile(blob, folder.getId()) // Auto-sync after upload? // syncMediaForSku(sku) // Optional: auto-sync } // Picker Callback specific handler if needed, or we just rely on frontend passing back file ID // Implementing a "copy from Picker" handler // Implementing a "copy from Picker" handler export function importFromPicker(sku: string, fileId: string, mimeType: string, name: string, imageUrl: string | null) { const driveService = new GASDriveService() let config; try { config = new Config(); } catch(e) { console.error("Config init failed in importFromPicker", e); throw new Error("Configuration Error: " + e.message); } console.log(`importFromPicker starting for SKU: ${sku}`); // STEP 1: Acquire/Create File in Root (Safe Zone) let finalFile: GoogleAppsScript.Drive.File; try { if (fileId && !imageUrl) { // Case A: Existing Drive File (Copy it) // Note: makeCopy(name) w/o folder argument copies to the same parent as original usually, or root? // Actually explicitly copying to Root is safer for "new" file. const source = DriveApp.getFileById(fileId); finalFile = source.makeCopy(name); // Default location console.log(`Step 1 Success: Drive File copied to Root/Default. ID: ${finalFile.getId()}`); } else if (imageUrl) { // Case B: URL (Photos) -> Blob -> File // Handling high-res parameter if (imageUrl.includes("googleusercontent.com") && !imageUrl.includes("=d")) { imageUrl += "=d"; // Download param } const response = UrlFetchApp.fetch(imageUrl, { headers: { Authorization: `Bearer ${ScriptApp.getOAuthToken()}` }, muteHttpExceptions: true }); console.log(`Download Response Code: ${response.getResponseCode()}`); if (response.getResponseCode() !== 200) { const errorBody = response.getContentText().substring(0, 500); throw new Error(`Request failed for ${imageUrl} returned code ${response.getResponseCode()}. Truncated server response: ${errorBody}`); } 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)}`); } 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()}`); } catch (createErr) { console.warn("DriveApp.createFile failed with clean blob. Trying Advanced Drive API...", createErr); try { // Fallback to Advanced Drive Service (v3 usually, or v2) // Note: v2 uses 'insert' & 'title', v3 uses 'create' & 'name' // We try v3 first as it's the modern default. if (typeof Drive === 'undefined') { throw new Error("Advanced Drive Service is not enabled. Please enable 'Drive API' in Apps Script Services."); } const drive = Drive as any; let insertedFile; if (drive.Files.create) { // v3 const fileResource = { name: fileName, mimeType: blob.getContentType() }; insertedFile = drive.Files.create(fileResource, blob); } else if (drive.Files.insert) { // v2 fallback const fileResource = { title: fileName, mimeType: blob.getContentType() }; insertedFile = drive.Files.insert(fileResource, blob); } else { throw new Error("Unknown Drive API version (neither create nor insert found)."); } finalFile = DriveApp.getFileById(insertedFile.id); console.log(`Step 1 Success (Advanced API): Photo downloaded to Root. ID: ${finalFile.getId()}`); } catch (advErr) { const metadata = `Type: ${blob.getContentType()}, Size: ${blob.getBytes().length}`; console.error(`All file creation methods failed. Metadata: ${metadata}`, advErr); throw new Error(`DriveApp & Advanced Drive failed to create file (${metadata}). Error: ${advErr.message}`); } } } else { throw new Error("No File ID and No Image URL provided."); } } catch (e) { console.error("Step 1 Failed (File Creation)", e); throw e; // Re-throw modified error } // STEP 2: Get Target Folder let folder: GoogleAppsScript.Drive.Folder; try { folder = driveService.getOrCreateFolder(sku, config.productPhotosFolderId); console.log(`Step 2 Success: Target folder found/created. Name: ${folder.getName()}`); } catch (e) { console.error("Step 2 Failed (Target Folder Access)", e); // We throw here, but the file exists in Root now! throw new Error(`File saved to Drive Root, but failed to put in SKU folder: ${e.message}`); } // STEP 3: Move File to Folder try { finalFile.moveTo(folder); console.log(`Step 3 Success: File moved to target folder.`); } catch (e) { console.error("Step 3 Failed (Move)", e); throw new Error(`File created (ID: ${finalFile.getId()}), but failed to move to folder: ${e.message}`); } } export function syncMediaForSku(sku: string) { const config = new Config() const driveService = new GASDriveService() const shop = new Shop() const shopifyMediaService = new ShopifyMediaService(shop) const networkService = new GASNetworkService() const mediaService = new MediaService(driveService, shopifyMediaService, networkService, config) // Need Shopify Product ID // We can get it from the Product class or Sheet const product = new Product(sku) if (!product.shopify_id) { product.MatchToShopifyProduct(shop) } if (!product.shopify_id) { throw new Error("Product not found on Shopify. Please sync product first.") } mediaService.syncMediaForSku(sku, product.shopify_id) // Update thumbnail in sheet // TODO: Implement thumbnail update in sheet if desired } export function debugScopes() { const token = ScriptApp.getOAuthToken(); console.log("Current Token exists: " + (token ? "YES" : "NO")); // We can't see exact scopes easily from server side without a library, // but we can check if the specific Photos pickup works? // No, let's just confirm the code is running the latest version. } export function debugFolderAccess() { const config = new Config() const ui = SpreadsheetApp.getUi(); if (!config.productPhotosFolderId) { ui.alert("Config Error", "No productPhotosFolderId found in vars.", ui.ButtonSet.OK); return; } const id = config.productPhotosFolderId.trim(); const info = [`Configured ID: '${id}'`]; try { info.push(`User: ${Session.getActiveUser().getEmail()}`); info.push(`Effective: ${Session.getEffectiveUser().getEmail()}`); const folder = DriveApp.getFolderById(id); info.push(`Success! Found Folder: ${folder.getName()}`); info.push(`URL: ${folder.getUrl()}`); info.push("Access seems OK from Menu context."); } catch (e) { info.push("FAILED to access as FOLDER."); info.push(`Error: ${e.message}`); // Try as file try { const file = DriveApp.getFileById(id); info.push(`\nWAIT! This ID belongs to a FILE, not a FOLDER!`); info.push(`File Name: ${file.getName()}`); info.push(`Mime: ${file.getMimeType()}`); } catch (e2) { info.push(`\nNot a File either: ${e2.message}`); } // Try Advanced Drive API try { const drive = (typeof Drive !== 'undefined') ? (Drive as any) : undefined; if (!drive) { info.push("\nAdvanced Drive Service (Drive) is NOT enabled. Please enable it in 'Services' > 'Drive API'."); } else { const advItem = drive.Files.get(id, { supportsAllDrives: true }); info.push(`\nSuccess via Advanced Drive API!`); info.push(`Title: ${advItem.title}`); info.push(`Mime: ${advItem.mimeType}`); info.push(`Note: If this works but DriveApp fails, this is likely a Shared Drive or permissions issue.`); } } catch (e3) { info.push(`\nAdvanced Drive API Failed: ${e3.message}`); } } ui.alert("Folder Access Debug", info.join("\n\n"), ui.ButtonSet.OK); } export function createPhotoSession() { const url = 'https://photospicker.googleapis.com/v1/sessions'; const token = ScriptApp.getOAuthToken(); const options = { method: 'post' as const, contentType: 'application/json', headers: { Authorization: `Bearer ${token}` }, payload: JSON.stringify({}) // Default session }; try { const response = UrlFetchApp.fetch(url, options); const data = JSON.parse(response.getContentText()); return data; // { id: "...", pickerUri: "..." } } catch (e) { console.error("Failed to create photo session", e); throw new Error("Failed to create photo session: " + e.message); } } export function checkPhotoSession(sessionId: string) { // Use pageSize=100 or check documentation. Default is usually fine. // We need to poll until we get mediaItems. const url = `https://photospicker.googleapis.com/v1/mediaItems?sessionId=${sessionId}&pageSize=10`; const token = ScriptApp.getOAuthToken(); const options = { method: 'get' as const, headers: { Authorization: `Bearer ${token}` }, muteHttpExceptions: true }; try { const response = UrlFetchApp.fetch(url, options); const text = response.getContentText(); console.log(`Polling session ${sessionId}: ${response.getResponseCode()}`); if (response.getResponseCode() !== 200) { // 400 Bad Request often means "Picker session not ready" or "Empty" if using the wrong check. // But documentation says FAILED_PRECONDITION (400?) if user hasn't finished. console.log("Polling response: " + response.getResponseCode() + " " + text); return { status: 'waiting' }; // Treat as waiting } const data = JSON.parse(text); // data.mediaItems might be undefined if nothing picked yet? // Or API waits? Actually checking documentation: it returns empty list or hangs? // It usually returns immediatley. If empty, user hasn't picked. if (data.mediaItems && data.mediaItems.length > 0) { return { status: 'complete', mediaItems: data.mediaItems }; } return { status: 'waiting' }; } catch (e) { console.error("Failed to check photo session", e); return { status: 'error', message: e.message }; } }