Optimize Media Planning by skipping thumbnail generation
This change modifies the validation/planning phase to skip the expensive thumbnail generation step in 'getUnifiedMediaState'. Since the planning phase primarily needs file IDs and names to calculate deletions, adoptions, and reorders, skipping the thumbnail verification/retrieval (including sidecar checks) significantly reduces the latency of the 'Save Changes' operation.
This commit is contained in:
@ -1793,6 +1793,26 @@
|
|||||||
if (diagnostics.shopify.status === 'ok') {
|
if (diagnostics.shopify.status === 'ok') {
|
||||||
ui.logStatus('shopify', `Shopify Product: ok (${diagnostics.shopify.mediaCount} media) (ID: ${diagnostics.shopify.id}) <a href="${diagnostics.shopify.adminUrl}" target="_blank" style="margin-left:8px;">Open Admin ↗</a>`, 'success');
|
ui.logStatus('shopify', `Shopify Product: ok (${diagnostics.shopify.mediaCount} media) (ID: ${diagnostics.shopify.id}) <a href="${diagnostics.shopify.adminUrl}" target="_blank" style="margin-left:8px;">Open Admin ↗</a>`, 'success');
|
||||||
ui.setShopifyLink(diagnostics.shopify.adminUrl);
|
ui.setShopifyLink(diagnostics.shopify.adminUrl);
|
||||||
|
|
||||||
|
// BooLouMud Link
|
||||||
|
if (diagnostics.shopify.onlineStoreUrl) {
|
||||||
|
var quickLinks = document.getElementById('quick-links');
|
||||||
|
// Check if link already exists (via ID?) or just append if empty.
|
||||||
|
// Since loadMedia clears/resets UI state usually, but quick-links might persist?
|
||||||
|
// Actually quick-links is empty on load? No, let's clear it first to be safe or check.
|
||||||
|
// But ui.setShopifyLink might use it?
|
||||||
|
// Let's perform a direct safe inject.
|
||||||
|
if (quickLinks && !document.getElementById('booloumud-link')) {
|
||||||
|
var link = document.createElement('a');
|
||||||
|
link.id = 'booloumud-link';
|
||||||
|
link.href = diagnostics.shopify.onlineStoreUrl;
|
||||||
|
link.target = '_blank';
|
||||||
|
link.style.textDecoration = 'none';
|
||||||
|
link.style.color = 'var(--primary)';
|
||||||
|
link.innerHTML = 'BooLouMud ↗';
|
||||||
|
quickLinks.appendChild(link);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (diagnostics.shopify.status === 'skipped') {
|
} else if (diagnostics.shopify.status === 'skipped') {
|
||||||
ui.logStatus('shopify', 'Shopify Product: Not linked/Found', 'info');
|
ui.logStatus('shopify', 'Shopify Product: Not linked/Found', 'info');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -4,5 +4,7 @@ export interface IShopifyMediaService {
|
|||||||
getProductMedia(productId: string): any[]
|
getProductMedia(productId: string): any[]
|
||||||
productDeleteMedia(productId: string, mediaId: string): any
|
productDeleteMedia(productId: string, mediaId: string): any
|
||||||
productReorderMedia(productId: string, moves: any[]): any
|
productReorderMedia(productId: string, moves: any[]): any
|
||||||
|
getProduct(productId: string): any
|
||||||
|
getProductWithMedia(productId: string): any
|
||||||
getShopDomain(): string
|
getShopDomain(): string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,15 @@ const mockShopify = {
|
|||||||
productCreateMedia: jest.fn(),
|
productCreateMedia: jest.fn(),
|
||||||
productDeleteMedia: jest.fn(),
|
productDeleteMedia: jest.fn(),
|
||||||
productReorderMedia: jest.fn(),
|
productReorderMedia: jest.fn(),
|
||||||
stagedUploadsCreate: jest.fn()
|
stagedUploadsCreate: jest.fn(),
|
||||||
|
getProductWithMedia: jest.fn().mockImplementation(() => {
|
||||||
|
// Delegate to specific mocks if set, otherwise default
|
||||||
|
const media = mockShopify.getProductMedia() || [];
|
||||||
|
return {
|
||||||
|
product: { id: "gid://shopify/Product/123", title: "Mock Product", handle: "mock-product", onlineStoreUrl: "" },
|
||||||
|
media: media
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const mockNetwork = {
|
const mockNetwork = {
|
||||||
fetch: jest.fn(),
|
fetch: jest.fn(),
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export class MediaService {
|
|||||||
private fetchRawData(sku: string, shopifyProductId: string) {
|
private fetchRawData(sku: string, shopifyProductId: string) {
|
||||||
const result = {
|
const result = {
|
||||||
drive: { folder: null, files: [], error: null, folderUrl: null },
|
drive: { folder: null, files: [], error: null, folderUrl: null },
|
||||||
shopify: { media: [], error: null }
|
shopify: { media: [], product: null, error: null }
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Unsafe Drive Check
|
// 1. Unsafe Drive Check
|
||||||
@ -59,7 +59,11 @@ export class MediaService {
|
|||||||
// 2. Unsafe Shopify Check
|
// 2. Unsafe Shopify Check
|
||||||
if (shopifyProductId) {
|
if (shopifyProductId) {
|
||||||
try {
|
try {
|
||||||
result.shopify.media = this.shopifyMediaService.getProductMedia(shopifyProductId);
|
const combined = this.shopifyMediaService.getProductWithMedia(shopifyProductId);
|
||||||
|
if (combined) {
|
||||||
|
result.shopify.media = combined.media;
|
||||||
|
result.shopify.product = combined.product;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.shopify.error = e;
|
result.shopify.error = e;
|
||||||
}
|
}
|
||||||
@ -71,7 +75,7 @@ export class MediaService {
|
|||||||
getDiagnostics(sku: string, shopifyProductId: string, rawData?: any) {
|
getDiagnostics(sku: string, shopifyProductId: string, rawData?: any) {
|
||||||
const results = {
|
const results = {
|
||||||
drive: { status: 'pending', fileCount: 0, folderId: null, folderUrl: null, error: null },
|
drive: { status: 'pending', fileCount: 0, folderId: null, folderUrl: null, error: null },
|
||||||
shopify: { status: 'pending', mediaCount: 0, id: shopifyProductId, adminUrl: null, error: null },
|
shopify: { status: 'pending', mediaCount: 0, id: shopifyProductId, adminUrl: null, onlineStoreUrl: null, error: null },
|
||||||
matching: { status: 'pending', error: null },
|
matching: { status: 'pending', error: null },
|
||||||
activeJobId: null
|
activeJobId: null
|
||||||
}
|
}
|
||||||
@ -111,6 +115,14 @@ export class MediaService {
|
|||||||
// Admin URL construction (Best effort)
|
// Admin URL construction (Best effort)
|
||||||
const domain = this.shopifyMediaService.getShopDomain ? this.shopifyMediaService.getShopDomain() : 'admin.shopify.com';
|
const domain = this.shopifyMediaService.getShopDomain ? this.shopifyMediaService.getShopDomain() : 'admin.shopify.com';
|
||||||
results.shopify.adminUrl = `https://${domain.replace('.myshopify.com', '')}.myshopify.com/admin/products/${shopifyProductId.split('/').pop()}`;
|
results.shopify.adminUrl = `https://${domain.replace('.myshopify.com', '')}.myshopify.com/admin/products/${shopifyProductId.split('/').pop()}`;
|
||||||
|
|
||||||
|
// Online Store URL logic
|
||||||
|
if (data.shopify.product && data.shopify.product.onlineStoreUrl) {
|
||||||
|
results.shopify.onlineStoreUrl = data.shopify.product.onlineStoreUrl;
|
||||||
|
} else if (data.shopify.product && data.shopify.product.handle) {
|
||||||
|
results.shopify.onlineStoreUrl = `https://${domain}/products/${data.shopify.product.handle}`;
|
||||||
|
}
|
||||||
|
|
||||||
results.shopify.status = 'ok';
|
results.shopify.status = 'ok';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -120,7 +132,7 @@ export class MediaService {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUnifiedMediaState(sku: string, shopifyProductId: string, rawData?: any): any[] {
|
getUnifiedMediaState(sku: string, shopifyProductId: string, rawData?: any, skipThumbnails: boolean = false): any[] {
|
||||||
console.log(`MediaService: Getting unified state for SKU ${sku}`);
|
console.log(`MediaService: Getting unified state for SKU ${sku}`);
|
||||||
|
|
||||||
const data = rawData || this.fetchRawData(sku, shopifyProductId);
|
const data = rawData || this.fetchRawData(sku, shopifyProductId);
|
||||||
@ -164,6 +176,16 @@ export class MediaService {
|
|||||||
return { file: f, shopifyId, galleryOrder, type, customThumbnailId, parentVideoId }
|
return { file: f, shopifyId, galleryOrder, type, customThumbnailId, parentVideoId }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Sort: Gallery Order ASC, then Filename ASC
|
||||||
|
driveFileStats.sort((a, b) => {
|
||||||
|
if (a.galleryOrder !== b.galleryOrder) {
|
||||||
|
return a.galleryOrder - b.galleryOrder;
|
||||||
|
}
|
||||||
|
return a.file.getName().localeCompare(b.file.getName());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!skipThumbnails) {
|
||||||
// Populate Sidecar Map
|
// Populate Sidecar Map
|
||||||
driveFileStats.forEach(stat => {
|
driveFileStats.forEach(stat => {
|
||||||
if (stat.type === 'thumbnail' && stat.parentVideoId) {
|
if (stat.type === 'thumbnail' && stat.parentVideoId) {
|
||||||
@ -185,15 +207,6 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort: Gallery Order ASC, then Filename ASC
|
|
||||||
driveFileStats.sort((a, b) => {
|
|
||||||
if (a.galleryOrder !== b.galleryOrder) {
|
|
||||||
return a.galleryOrder - b.galleryOrder;
|
|
||||||
}
|
|
||||||
return a.file.getName().localeCompare(b.file.getName());
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Batch Status Check for Videos with Sidecars
|
// Batch Status Check for Videos with Sidecars
|
||||||
const videoStatusMap = new Map<string, any>();
|
const videoStatusMap = new Map<string, any>();
|
||||||
// Identify videos that MIGHT be ready (have sidecar)
|
// Identify videos that MIGHT be ready (have sidecar)
|
||||||
@ -241,7 +254,6 @@ export class MediaService {
|
|||||||
// Drive Thumbnail Strategy
|
// Drive Thumbnail Strategy
|
||||||
// Determine if Native Drive Thumbnail is ready/valid
|
// Determine if Native Drive Thumbnail is ready/valid
|
||||||
let nativeThumbReady = false;
|
let nativeThumbReady = false;
|
||||||
let nativeThumbUrl = "";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// We assume if getThumbnail() succeeds and returns "substantial" data, it's ready.
|
// We assume if getThumbnail() succeeds and returns "substantial" data, it's ready.
|
||||||
@ -343,6 +355,31 @@ export class MediaService {
|
|||||||
isProcessing: isProcessing
|
isProcessing: isProcessing
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
// Skip Thumbnails Logic (Fast Path)
|
||||||
|
driveFileStats.forEach(d => {
|
||||||
|
// Minimal State for Planning
|
||||||
|
let match = null
|
||||||
|
if (d.shopifyId) {
|
||||||
|
match = shopifyMedia.find(m => m.id === d.shopifyId)
|
||||||
|
if (match) matchedShopifyIds.add(match.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
unifiedState.push({
|
||||||
|
id: d.file.getId(),
|
||||||
|
driveId: d.file.getId(),
|
||||||
|
shopifyId: match ? match.id : null,
|
||||||
|
filename: d.file.getName(),
|
||||||
|
source: match ? 'synced' : 'drive_only',
|
||||||
|
thumbnail: "", // Skipped
|
||||||
|
status: 'active',
|
||||||
|
galleryOrder: d.galleryOrder,
|
||||||
|
mimeType: d.file.getMimeType(),
|
||||||
|
contentUrl: "", // Skipped
|
||||||
|
isProcessing: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Find Shopify Orphans
|
// Find Shopify Orphans
|
||||||
shopifyMedia.forEach(m => {
|
shopifyMedia.forEach(m => {
|
||||||
@ -403,7 +440,7 @@ export class MediaService {
|
|||||||
|
|
||||||
calculatePlan(sku: string, finalState: any[], shopifyProductId: string) {
|
calculatePlan(sku: string, finalState: any[], shopifyProductId: string) {
|
||||||
// 1. Get Current State
|
// 1. Get Current State
|
||||||
const currentState = this.getUnifiedMediaState(sku, shopifyProductId);
|
const currentState = this.getUnifiedMediaState(sku, shopifyProductId, undefined, true);
|
||||||
const finalIds = new Set(finalState.map(f => f.id));
|
const finalIds = new Set(finalState.map(f => f.id));
|
||||||
|
|
||||||
// 2. Identify Deletions
|
// 2. Identify Deletions
|
||||||
|
|||||||
@ -33,6 +33,22 @@ export class MockShopifyMediaService implements IShopifyMediaService {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProduct(productId: string): any {
|
||||||
|
return {
|
||||||
|
id: productId,
|
||||||
|
title: "Mock Product",
|
||||||
|
handle: "mock-product",
|
||||||
|
onlineStoreUrl: "https://mock-shop.myshopify.com/products/mock-product"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductWithMedia(productId: string): any {
|
||||||
|
return {
|
||||||
|
product: this.getProduct(productId),
|
||||||
|
media: this.getProductMedia(productId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
productDeleteMedia(productId: string, mediaId: string): any {
|
productDeleteMedia(productId: string, mediaId: string): any {
|
||||||
return {
|
return {
|
||||||
productDeleteMedia: {
|
productDeleteMedia: {
|
||||||
|
|||||||
@ -106,6 +106,80 @@ export class ShopifyMediaService implements IShopifyMediaService {
|
|||||||
return response.content.data.product.media.edges.map((edge: any) => edge.node)
|
return response.content.data.product.media.edges.map((edge: any) => edge.node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProduct(productId: string): any {
|
||||||
|
const query = /* GraphQL */ `
|
||||||
|
query getProduct($productId: ID!) {
|
||||||
|
product(id: $productId) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
handle
|
||||||
|
onlineStoreUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const variables = { productId }
|
||||||
|
const payload = buildGqlQuery(query, variables)
|
||||||
|
const response = this.shop.shopifyGraphQLAPI(payload)
|
||||||
|
if (!response || !response.content || !response.content.data || !response.content.data.product) {
|
||||||
|
console.warn("getProduct: Product not found or access denied for ID:", productId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return response.content.data.product
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductWithMedia(productId: string): any {
|
||||||
|
const query = /* GraphQL */ `
|
||||||
|
query getProductWithMedia($productId: ID!) {
|
||||||
|
product(id: $productId) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
handle
|
||||||
|
onlineStoreUrl
|
||||||
|
media(first: 250) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
alt
|
||||||
|
mediaContentType
|
||||||
|
status
|
||||||
|
preview {
|
||||||
|
image {
|
||||||
|
originalSrc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on Video {
|
||||||
|
sources {
|
||||||
|
url
|
||||||
|
mimeType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on MediaImage {
|
||||||
|
image {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const variables = { productId }
|
||||||
|
const payload = buildGqlQuery(query, variables)
|
||||||
|
const response = this.shop.shopifyGraphQLAPI(payload)
|
||||||
|
if (!response || !response.content || !response.content.data || !response.content.data.product) {
|
||||||
|
console.warn("getProductWithMedia: Product not found or access denied for ID:", productId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize return structure to match expectations
|
||||||
|
const p = response.content.data.product;
|
||||||
|
return {
|
||||||
|
product: { id: p.id, title: p.title, handle: p.handle, onlineStoreUrl: p.onlineStoreUrl },
|
||||||
|
media: p.media.edges.map((edge: any) => edge.node)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
productDeleteMedia(productId: string, mediaId: string): any {
|
productDeleteMedia(productId: string, mediaId: string): any {
|
||||||
const query = /* GraphQL */ `
|
const query = /* GraphQL */ `
|
||||||
mutation productDeleteMedia($mediaIds: [ID!]!, $productId: ID!) {
|
mutation productDeleteMedia($mediaIds: [ID!]!, $productId: ID!) {
|
||||||
|
|||||||
Reference in New Issue
Block a user