feat(media): implement integrated media manager with sidebar and picker

- Implement DriveService and ShopifyMediaService for backend operations
- Create MediaSidebar.html with premium UI and auto-polling
- Integrate Google Picker API for robust file selection
- Orchestrate sync logic via MediaService (Drive -> Staged Upload -> Shopify)
- Add secure config handling for API keys and tokens
- Update ppsscript.json with required OAuth scopes
- Update MEMORY.md and README.md with architecture details
This commit is contained in:
Ben Miller
2025-12-25 15:10:17 -07:00
parent 2417359595
commit 95094b1674
21 changed files with 973 additions and 16 deletions

View File

@ -0,0 +1,103 @@
import { IDriveService } from "../interfaces/IDriveService"
import { IShopifyMediaService } from "../interfaces/IShopifyMediaService"
import { INetworkService } from "../interfaces/INetworkService"
import { Config } from "../config"
export class MediaService {
private driveService: IDriveService
private shopifyMediaService: IShopifyMediaService
private networkService: INetworkService
private config: Config
constructor(
driveService: IDriveService,
shopifyMediaService: IShopifyMediaService,
networkService: INetworkService,
config: Config
) {
this.driveService = driveService
this.shopifyMediaService = shopifyMediaService
this.networkService = networkService
this.config = config
}
syncMediaForSku(sku: string, shopifyProductId: string) {
console.log(`MediaService: Syncing media for SKU ${sku}`)
// 1. Get files from Drive
const folder = this.driveService.getOrCreateFolder(sku, this.config.productPhotosFolderId)
const files = this.driveService.getFiles(folder.getId())
if (files.length === 0) {
console.log("No files found in Drive.")
return
}
console.log(`Found ${files.length} files in Drive folder ${folder.getId()}`)
// Sort files by name to ensure consistent order (01.jpg, 02.jpg)
files.sort((a, b) => a.getName().localeCompare(b.getName()))
// TODO: optimization - check if file already exists on Shopify by filename/size/hash
// For now, we will just upload everything that is new, or we rely on Shopify to dedupe?
// Shopify does NOT dedupe automatically if we create new media entries.
// We should probably list current media on the product and compare filenames.
// But filenames in Shopify are sanitized.
// Pro trick: Use 'alt' text to store the original filename/Drive ID.
// 2. Prepare Staged Uploads
// collecting files needing upload
const filesToUpload = files; // uploading all for MVP simplicity, assume clean state or overwrite logic later
if (filesToUpload.length === 0) return
const stagedUploadInput = filesToUpload.map(f => ({
filename: f.getName(),
mimeType: f.getMimeType(),
resource: "IMAGE", // or VIDEO
httpMethod: "POST"
}))
const response = this.shopifyMediaService.stagedUploadsCreate(stagedUploadInput)
if (response.userErrors && response.userErrors.length > 0) {
console.error("Staged upload errors:", response.userErrors)
throw new Error("Staged upload failed")
}
const stagedTargets = response.stagedTargets
if (!stagedTargets || stagedTargets.length !== filesToUpload.length) {
throw new Error("Failed to create staged upload targets")
}
const mediaToCreate = []
// 3. Upload files to Targets
for (let i = 0; i < filesToUpload.length; i++) {
const file = filesToUpload[i]
const target = stagedTargets[i]
console.log(`Uploading ${file.getName()} to ${target.url}`)
const payload = {}
target.parameters.forEach(p => payload[p.name] = p.value)
payload['file'] = file.getBlob()
this.networkService.fetch(target.url, {
method: "post",
payload: payload
})
mediaToCreate.push({
originalSource: target.resourceUrl,
alt: file.getName(), // Storing filename in Alt for basic deduping later
mediaContentType: "IMAGE" // TODO: Detect video
})
}
// 4. Create Media on Shopify
console.log("Creating media on Shopify...")
const result = this.shopifyMediaService.productCreateMedia(shopifyProductId, mediaToCreate)
console.log("Media created successfully")
}
}