Compare commits
3 Commits
55d18138b7
...
d9fe81f282
| Author | SHA1 | Date | |
|---|---|---|---|
| d9fe81f282 | |||
| 19b3d5de2b | |||
| e5ce154175 |
@ -484,6 +484,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Matching Modal -->
|
||||||
|
<div id="matching-modal" class="modal-overlay" style="z-index: 150;">
|
||||||
|
<div class="card"
|
||||||
|
style="width: 600px; max-width: 90%; text-align: center; padding: 24px; position: relative; background: #fff;">
|
||||||
|
<h3 style="margin-top:0;">Link Media?</h3>
|
||||||
|
<p style="color:var(--text-secondary); margin-bottom: 24px;">
|
||||||
|
We found a matching file in Shopify. Should these be linked?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: center; gap: 24px; margin-bottom: 24px;">
|
||||||
|
<!-- Drive Side -->
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div style="font-size: 12px; font-weight: 600; margin-bottom: 8px;">Drive File</div>
|
||||||
|
<img id="match-drive-img"
|
||||||
|
style="width: 100%; height: 200px; object-fit: contain; border: 1px solid var(--border); border-radius: 8px; background: #f8fafc;">
|
||||||
|
<div id="match-drive-name"
|
||||||
|
style="font-size: 11px; margin-top: 4px; color: var(--text-secondary); word-break: break-all;">filename.jpg
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Icon -->
|
||||||
|
<div style="display: flex; align-items: center; font-size: 24px; color: var(--text-secondary);">🔗</div>
|
||||||
|
|
||||||
|
<!-- Shopify Side -->
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<div style="font-size: 12px; font-weight: 600; margin-bottom: 8px;">Shopify Media</div>
|
||||||
|
<img id="match-shopify-img"
|
||||||
|
style="width: 100%; height: 200px; object-fit: contain; border: 1px solid var(--border); border-radius: 8px; background: #f8fafc;">
|
||||||
|
<div id="match-shopify-name"
|
||||||
|
style="font-size: 11px; margin-top: 4px; color: var(--text-secondary); word-break: break-all;">filename.jpg
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 12px; justify-content: center;">
|
||||||
|
<button id="btn-match-skip" onclick="controller.skipLink()" class="btn btn-secondary" style="width: 100px;">No,
|
||||||
|
Skip</button>
|
||||||
|
<button id="btn-match-confirm" onclick="controller.confirmLink()" class="btn" style="width: 100px;">Yes,
|
||||||
|
Link</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 12px; font-size: 12px; color: var(--text-secondary);">
|
||||||
|
Match <span id="match-index">1</span> of <span id="match-total">1</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="drop-overlay"
|
<div id="drop-overlay"
|
||||||
style="position: fixed; top:0; left:0; right:0; bottom:0; background: rgba(37, 99, 235, 0.9); z-index: 200; display: none; flex-direction: column; align-items: center; justify-content: center; color: white;">
|
style="position: fixed; top:0; left:0; right:0; bottom:0; background: rgba(37, 99, 235, 0.9); z-index: 200; display: none; flex-direction: column; align-items: center; justify-content: center; color: white;">
|
||||||
<div style="font-size: 48px; margin-bottom: 16px;">☁️</div>
|
<div style="font-size: 48px; margin-bottom: 16px;">☁️</div>
|
||||||
@ -749,8 +795,9 @@
|
|||||||
// Create Media Element
|
// Create Media Element
|
||||||
// RULE: Only create <video> for Shopify-hosted videos (public).
|
// RULE: Only create <video> for Shopify-hosted videos (public).
|
||||||
// Drive videos use static thumbnail + Iframe Preview.
|
// Drive videos use static thumbnail + Iframe Preview.
|
||||||
var mediaEl;
|
var mediaEl;
|
||||||
if (isVideo && item.source === 'shopify_only' && contentUrl) {
|
// Allow Shopify-only OR Synced items with valid contentUrl (Shopify Video URL) to use <video> tag
|
||||||
|
if (isVideo && (item.source === 'shopify_only' || item.source === 'synced') && contentUrl) {
|
||||||
mediaEl = document.createElement('video');
|
mediaEl = document.createElement('video');
|
||||||
mediaEl.src = contentUrl;
|
mediaEl.src = contentUrl;
|
||||||
mediaEl.poster = item.thumbnail || "";
|
mediaEl.poster = item.thumbnail || "";
|
||||||
@ -986,11 +1033,13 @@
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
state.setItems(normalized);
|
state.setItems(normalized);
|
||||||
document.getElementById('loading-ui').style.display = 'none';
|
|
||||||
document.getElementById('main-ui').style.display = 'block';
|
|
||||||
|
|
||||||
ui.logStatus('done', 'Finished loading.', 'success');
|
if (!controller.hasRunMatching) {
|
||||||
setTimeout(() => ui.toggleLog(false), 1000); // Auto hide after 1s
|
controller.hasRunMatching = true;
|
||||||
|
controller.checkMatches(normalized);
|
||||||
|
} else {
|
||||||
|
controller.showGallery();
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
.withFailureHandler(function (err) {
|
.withFailureHandler(function (err) {
|
||||||
@ -1114,7 +1163,151 @@
|
|||||||
})
|
})
|
||||||
.importFromPicker(state.sku, null, item.mimeType, item.filename, url);
|
.importFromPicker(state.sku, null, item.mimeType, item.filename, url);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// --- Compatibility / Matching Logic ---
|
||||||
|
matches: [],
|
||||||
|
currentMatchIndex: 0,
|
||||||
|
hasRunMatching: false,
|
||||||
|
|
||||||
|
checkMatches(items) {
|
||||||
|
// Filter candidates
|
||||||
|
var driveOnly = items.filter(function (i) { return i.status === 'drive_only'; });
|
||||||
|
var shopifyOnly = items.filter(function (i) { return i.source === 'shopify_only'; }); // source check is safer for shopify items
|
||||||
|
|
||||||
|
var newMatches = [];
|
||||||
|
|
||||||
|
driveOnly.forEach(function (d) {
|
||||||
|
// Find match by filename
|
||||||
|
// Note: Backend might return "Orphaned Media" if extraction failed, ignore those.
|
||||||
|
if (!d.filename || d.filename === 'Orphaned Media') return;
|
||||||
|
|
||||||
|
var match = shopifyOnly.find(function (s) {
|
||||||
|
return s.filename === d.filename; // Exact match
|
||||||
|
});
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
newMatches.push({ drive: d, shopify: match });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newMatches.length > 0) {
|
||||||
|
this.matches = newMatches;
|
||||||
|
this.currentMatchIndex = 0;
|
||||||
|
ui.logStatus('info', 'Found ' + newMatches.length + ' potential matches. Starting matching wizard...', 'info');
|
||||||
|
this.startMatching();
|
||||||
|
} else {
|
||||||
|
// No matches, show UI
|
||||||
|
this.showGallery();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startMatching() {
|
||||||
|
document.getElementById('loading-ui').style.display = 'none';
|
||||||
|
document.getElementById('main-ui').style.display = 'none';
|
||||||
|
document.getElementById('matching-modal').style.display = 'flex';
|
||||||
|
this.renderMatch();
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMatch() {
|
||||||
|
var match = this.matches[this.currentMatchIndex];
|
||||||
|
|
||||||
|
// Reset Buttons
|
||||||
|
var btnConfirm = document.getElementById('btn-match-confirm');
|
||||||
|
var btnSkip = document.getElementById('btn-match-skip');
|
||||||
|
if (btnConfirm) {
|
||||||
|
btnConfirm.disabled = false;
|
||||||
|
btnConfirm.innerText = "Yes, Link";
|
||||||
|
}
|
||||||
|
if (btnSkip) {
|
||||||
|
btnSkip.disabled = false;
|
||||||
|
btnSkip.innerText = "No, Skip";
|
||||||
|
}
|
||||||
|
|
||||||
|
var dImg = document.getElementById('match-drive-img');
|
||||||
|
var sImg = document.getElementById('match-shopify-img');
|
||||||
|
|
||||||
|
// Reset visual state safely
|
||||||
|
dImg.style.transition = 'none';
|
||||||
|
dImg.style.opacity = '0';
|
||||||
|
sImg.style.transition = 'none';
|
||||||
|
sImg.style.opacity = '0';
|
||||||
|
|
||||||
|
// Clear source to blank pixel to ensure old image is gone
|
||||||
|
var blank = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
|
||||||
|
dImg.src = blank;
|
||||||
|
sImg.src = blank;
|
||||||
|
|
||||||
|
document.getElementById('match-drive-name').innerText = match.drive.filename;
|
||||||
|
document.getElementById('match-shopify-name').innerText = match.shopify.filename;
|
||||||
|
|
||||||
|
document.getElementById('match-index').innerText = this.currentMatchIndex + 1;
|
||||||
|
document.getElementById('match-total').innerText = this.matches.length;
|
||||||
|
|
||||||
|
// Load new images
|
||||||
|
setTimeout(function () {
|
||||||
|
dImg.style.transition = 'opacity 0.3s ease';
|
||||||
|
sImg.style.transition = 'opacity 0.3s ease';
|
||||||
|
|
||||||
|
dImg.onload = function () { dImg.style.opacity = '1'; };
|
||||||
|
sImg.onload = function () { sImg.style.opacity = '1'; };
|
||||||
|
|
||||||
|
dImg.src = match.drive.thumbnail;
|
||||||
|
sImg.src = match.shopify.thumbnail;
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmLink() {
|
||||||
|
var match = this.matches[this.currentMatchIndex];
|
||||||
|
var _this = this;
|
||||||
|
|
||||||
|
document.getElementById('btn-match-confirm').disabled = true;
|
||||||
|
document.getElementById('btn-match-confirm').innerText = "Linking...";
|
||||||
|
document.getElementById('btn-match-skip').disabled = true;
|
||||||
|
|
||||||
|
// ui.logStatus('link', 'Linking ' + match.drive.filename + '...', 'info');
|
||||||
|
|
||||||
|
google.script.run
|
||||||
|
.withSuccessHandler(function () {
|
||||||
|
// ui.logStatus('link', 'Linked ' + match.drive.filename, 'success');
|
||||||
|
_this.nextMatch();
|
||||||
|
})
|
||||||
|
.withFailureHandler(function (e) {
|
||||||
|
alert("Failed to link: " + e.message);
|
||||||
|
document.getElementById('btn-match-confirm').disabled = false;
|
||||||
|
document.getElementById('btn-match-confirm').innerText = "Yes, Link";
|
||||||
|
document.getElementById('btn-match-skip').disabled = false;
|
||||||
|
})
|
||||||
|
.linkDriveFileToShopifyMedia(state.sku, match.drive.id, match.shopify.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
skipLink() {
|
||||||
|
document.getElementById('btn-match-skip').innerText = "Skipping...";
|
||||||
|
document.getElementById('btn-match-skip').disabled = true;
|
||||||
|
document.getElementById('btn-match-confirm').disabled = true;
|
||||||
|
setTimeout(() => this.nextMatch(), 200);
|
||||||
|
},
|
||||||
|
|
||||||
|
nextMatch() {
|
||||||
|
this.currentMatchIndex++;
|
||||||
|
if (this.currentMatchIndex < this.matches.length) {
|
||||||
|
this.renderMatch();
|
||||||
|
} else {
|
||||||
|
// Done
|
||||||
|
document.getElementById('matching-modal').style.display = 'none';
|
||||||
|
ui.logStatus('info', 'Matching complete. Reloading...', 'info');
|
||||||
|
document.getElementById('loading-ui').style.display = 'block';
|
||||||
|
// Reload to get fresh state. Since hasRunMatching is true, it shouldn't trigger again.
|
||||||
|
this.loadMedia(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showGallery() {
|
||||||
|
document.getElementById('loading-ui').style.display = 'none';
|
||||||
|
document.getElementById('main-ui').style.display = 'block';
|
||||||
|
ui.logStatus('done', 'Finished loading.', 'success');
|
||||||
|
setTimeout(function () { ui.toggleLog(false); }, 1000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Google Picker API ---
|
// --- Google Picker API ---
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import { fillProductFromTemplate } from "./fillProductFromTemplate"
|
|||||||
import { showSidebar, getQueueStatus, setQueueEnabled, deleteEdit, pushEdit } from "./sidebar"
|
import { showSidebar, getQueueStatus, setQueueEnabled, deleteEdit, pushEdit } from "./sidebar"
|
||||||
import { checkRecentSales, reconcileSalesHandler } from "./salesSync"
|
import { checkRecentSales, reconcileSalesHandler } from "./salesSync"
|
||||||
import { installSalesSyncTrigger } from "./triggers"
|
import { installSalesSyncTrigger } from "./triggers"
|
||||||
import { showMediaManager, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess } from "./mediaHandlers"
|
import { showMediaManager, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess, linkDriveFileToShopifyMedia } from "./mediaHandlers"
|
||||||
import { runSystemDiagnostics } from "./verificationSuite"
|
import { runSystemDiagnostics } from "./verificationSuite"
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
@ -64,3 +64,4 @@ import { runSystemDiagnostics } from "./verificationSuite"
|
|||||||
;(global as any).createPhotoSession = createPhotoSession
|
;(global as any).createPhotoSession = createPhotoSession
|
||||||
;(global as any).checkPhotoSession = checkPhotoSession
|
;(global as any).checkPhotoSession = checkPhotoSession
|
||||||
;(global as any).debugFolderAccess = debugFolderAccess
|
;(global as any).debugFolderAccess = debugFolderAccess
|
||||||
|
;(global as any).linkDriveFileToShopifyMedia = linkDriveFileToShopifyMedia
|
||||||
|
|||||||
@ -116,6 +116,17 @@ export function getMediaDiagnostics(sku: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function linkDriveFileToShopifyMedia(sku: string, driveId: string, shopifyId: 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)
|
||||||
|
|
||||||
|
return mediaService.linkDriveFileToShopifyMedia(sku, driveId, shopifyId)
|
||||||
|
}
|
||||||
|
|
||||||
export function saveFileToDrive(sku: string, filename: string, mimeType: string, base64Data: string) {
|
export function saveFileToDrive(sku: string, filename: string, mimeType: string, base64Data: string) {
|
||||||
const config = new Config()
|
const config = new Config()
|
||||||
const driveService = new GASDriveService()
|
const driveService = new GASDriveService()
|
||||||
|
|||||||
@ -72,6 +72,7 @@ describe("MediaService V2 Integration Logic", () => {
|
|||||||
moveTo: jest.fn(),
|
moveTo: jest.fn(),
|
||||||
getMimeType: () => "image/jpeg",
|
getMimeType: () => "image/jpeg",
|
||||||
getBlob: () => ({}),
|
getBlob: () => ({}),
|
||||||
|
getSize: () => 1024,
|
||||||
getId: () => id
|
getId: () => id
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@ -163,4 +163,98 @@ describe("MediaService Robust Sync", () => {
|
|||||||
expect(spyRename).toHaveBeenCalledWith(f1.getId(), expect.stringMatching(/^SKU123_\d+\.jpg$/))
|
expect(spyRename).toHaveBeenCalledWith(f1.getId(), expect.stringMatching(/^SKU123_\d+\.jpg$/))
|
||||||
expect(spyRename).not.toHaveBeenCalledWith(f2.getId(), expect.anything())
|
expect(spyRename).not.toHaveBeenCalledWith(f2.getId(), expect.anything())
|
||||||
})
|
})
|
||||||
|
test("Upload: Handles Video Uploads with correct resource type", () => {
|
||||||
|
const folder = driveService.getOrCreateFolder("SKU_VIDEO", "root")
|
||||||
|
|
||||||
|
// Mock Video Blob
|
||||||
|
const videoBlob = {
|
||||||
|
getName: () => "video.mp4",
|
||||||
|
getBytes: () => [],
|
||||||
|
getContentType: () => "video/mp4",
|
||||||
|
getThumbnail: () => ({ getBytes: () => [] })
|
||||||
|
} as any
|
||||||
|
|
||||||
|
const vidFile = driveService.saveFile(videoBlob, folder.getId())
|
||||||
|
|
||||||
|
const finalState = [{
|
||||||
|
id: vidFile.getId(),
|
||||||
|
driveId: vidFile.getId(),
|
||||||
|
filename: "video.mp4",
|
||||||
|
source: "drive_only"
|
||||||
|
}]
|
||||||
|
|
||||||
|
const spyStaged = jest.spyOn(shopifyService, 'stagedUploadsCreate')
|
||||||
|
const spyCreate = jest.spyOn(shopifyService, 'productCreateMedia')
|
||||||
|
|
||||||
|
mediaService.processMediaChanges("SKU_VIDEO", finalState, "pid")
|
||||||
|
|
||||||
|
// 1. Verify stagedUploadsCreate called with resource="VIDEO" and fileSize
|
||||||
|
expect(spyStaged).toHaveBeenCalledWith(expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
resource: "VIDEO",
|
||||||
|
mimeType: "video/mp4",
|
||||||
|
filename: "video.mp4",
|
||||||
|
fileSize: "0" // 0 because mock bytes are empty
|
||||||
|
})
|
||||||
|
]))
|
||||||
|
|
||||||
|
// 2. Verify productCreateMedia called with mediaContentType="VIDEO"
|
||||||
|
expect(spyCreate).toHaveBeenCalledWith("pid", expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
mediaContentType: "VIDEO"
|
||||||
|
})
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Thumbnail: Uses Shopify thumbnail when synced", () => {
|
||||||
|
const folder = driveService.getOrCreateFolder("SKU_THUMB", "root")
|
||||||
|
|
||||||
|
// Drive File
|
||||||
|
const blob1 = { getName: () => "img1.jpg", getBytes: () => [], getThumbnail: () => ({ getBytes: () => [1, 2, 3] }) } as any
|
||||||
|
const f1 = driveService.saveFile(blob1, folder.getId())
|
||||||
|
driveService.updateFileProperties(f1.getId(), { shopify_media_id: "gid://shopify/Media/123" })
|
||||||
|
|
||||||
|
// Shopify Media with distinct thumbnail
|
||||||
|
shopifyService.getProductMedia = jest.fn().mockReturnValue([
|
||||||
|
{
|
||||||
|
id: "gid://shopify/Media/123",
|
||||||
|
filename: "img1.jpg",
|
||||||
|
preview: { image: { originalSrc: "https://shopify.com/thumb.jpg" } }
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const state = mediaService.getUnifiedMediaState("SKU_THUMB", "pid")
|
||||||
|
|
||||||
|
const item = state.find(s => s.id === f1.getId())
|
||||||
|
expect(item.source).toBe("synced")
|
||||||
|
expect(item.thumbnail).toBe("https://shopify.com/thumb.jpg")
|
||||||
|
// Verify it didn't use the base64 drive thumbnail
|
||||||
|
expect(item.thumbnail).not.toContain("base64")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Video Sync: Uses Shopify contentUrl for synced videos", () => {
|
||||||
|
const folder = driveService.getOrCreateFolder("SKU_VID_SYNC", "root")
|
||||||
|
|
||||||
|
// Drive File (Video)
|
||||||
|
const blob = { getName: () => "vid.mp4", getBytes: () => [], getMimeType: () => "video/mp4", getThumbnail: () => ({ getBytes: () => [] }) } as any
|
||||||
|
const f = driveService.saveFile(blob, folder.getId())
|
||||||
|
driveService.updateFileProperties(f.getId(), { shopify_media_id: "gid://shopify/Media/Vid1" })
|
||||||
|
|
||||||
|
// Shopify Media (Video)
|
||||||
|
shopifyService.getProductMedia = jest.fn().mockReturnValue([
|
||||||
|
{
|
||||||
|
id: "gid://shopify/Media/Vid1",
|
||||||
|
filename: "vid.mp4",
|
||||||
|
mediaContentType: "VIDEO",
|
||||||
|
sources: [{ url: "https://shopify.com/video.mp4", mimeType: "video/mp4" }],
|
||||||
|
preview: { image: { originalSrc: "https://shopify.com/vid_thumb.jpg" } }
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const state = mediaService.getUnifiedMediaState("SKU_VID_SYNC", "pid")
|
||||||
|
const item = state.find(s => s.id === f.getId())
|
||||||
|
|
||||||
|
expect(item.contentUrl).toBe("https://shopify.com/video.mp4")
|
||||||
|
expect(item.thumbnail).toBe("https://shopify.com/vid_thumb.jpg")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -126,12 +126,16 @@ export class MediaService {
|
|||||||
shopifyId: match ? match.id : null,
|
shopifyId: match ? match.id : null,
|
||||||
filename: d.file.getName(),
|
filename: d.file.getName(),
|
||||||
source: match ? 'synced' : 'drive_only',
|
source: match ? 'synced' : 'drive_only',
|
||||||
thumbnail: `data:image/jpeg;base64,${Utilities.base64Encode(d.file.getThumbnail().getBytes())}`,
|
thumbnail: (match && match.preview && match.preview.image && match.preview.image.originalSrc)
|
||||||
|
? match.preview.image.originalSrc
|
||||||
|
: `data:image/jpeg;base64,${Utilities.base64Encode(d.file.getThumbnail().getBytes())}`,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
galleryOrder: d.galleryOrder,
|
galleryOrder: d.galleryOrder,
|
||||||
mimeType: d.file.getMimeType(),
|
mimeType: d.file.getMimeType(),
|
||||||
// Use manual download URL construction which is often more reliable for authenticated sessions than getDownloadUrl()
|
// Prefer Shopify Video URL for playback/hover if available, otherwise Drive Download URL
|
||||||
contentUrl: `https://drive.google.com/uc?export=download&id=${d.file.getId()}`
|
contentUrl: (match && match.sources)
|
||||||
|
? (match.sources.find((s: any) => s.mimeType === 'video/mp4')?.url || match.sources[0]?.url)
|
||||||
|
: `https://drive.google.com/uc?export=download&id=${d.file.getId()}`
|
||||||
})
|
})
|
||||||
// console.log(`[MediaService] File ${d.file.getName()} (${d.file.getId()}): Mime=${d.file.getMimeType()}, ContentUrl=https://drive.google.com/uc?export=download&id=${d.file.getId()}`)
|
// console.log(`[MediaService] File ${d.file.getName()} (${d.file.getId()}): Mime=${d.file.getMimeType()}, ContentUrl=https://drive.google.com/uc?export=download&id=${d.file.getId()}`)
|
||||||
})
|
})
|
||||||
@ -153,12 +157,26 @@ export class MediaService {
|
|||||||
contentUrl = m.image.url
|
contentUrl = m.image.url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract filename from URL (Shopify URLs usually contain the filename)
|
||||||
|
let filename = "Orphaned Media";
|
||||||
|
try {
|
||||||
|
if (contentUrl) {
|
||||||
|
// Clean query params and get last segment
|
||||||
|
const cleanUrl = contentUrl.split('?')[0];
|
||||||
|
const parts = cleanUrl.split('/');
|
||||||
|
const candidate = parts.pop();
|
||||||
|
if (candidate) filename = candidate;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to extract filename from URL", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
unifiedState.push({
|
unifiedState.push({
|
||||||
id: m.id, // Use Shopify ID keys for orphans
|
id: m.id, // Use Shopify ID keys for orphans
|
||||||
driveId: null,
|
driveId: null,
|
||||||
shopifyId: m.id,
|
shopifyId: m.id,
|
||||||
filename: "Orphaned Media", // Shopify doesn't always expose filename cleanly in same way
|
filename: filename,
|
||||||
// Try to get filename if possible or fallback
|
|
||||||
source: 'shopify_only',
|
source: 'shopify_only',
|
||||||
thumbnail: m.preview?.image?.originalSrc || "",
|
thumbnail: m.preview?.image?.originalSrc || "",
|
||||||
status: 'active',
|
status: 'active',
|
||||||
@ -172,6 +190,13 @@ export class MediaService {
|
|||||||
return unifiedState
|
return unifiedState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
linkDriveFileToShopifyMedia(sku: string, driveId: string, shopifyId: string) {
|
||||||
|
console.log(`MediaService: Linking Drive File ${driveId} to Shopify Media ${shopifyId}`);
|
||||||
|
// Verify ownership? Maybe later. For now, trust the ID.
|
||||||
|
this.driveService.updateFileProperties(driveId, { shopify_media_id: shopifyId });
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
processMediaChanges(sku: string, finalState: any[], shopifyProductId: string): string[] {
|
processMediaChanges(sku: string, finalState: any[], shopifyProductId: string): string[] {
|
||||||
const logs: string[] = []
|
const logs: string[] = []
|
||||||
logs.push(`Starting processing for SKU ${sku}`)
|
logs.push(`Starting processing for SKU ${sku}`)
|
||||||
@ -256,7 +281,8 @@ export class MediaService {
|
|||||||
return {
|
return {
|
||||||
filename: f.getName(),
|
filename: f.getName(),
|
||||||
mimeType: f.getMimeType(),
|
mimeType: f.getMimeType(),
|
||||||
resource: "IMAGE",
|
resource: f.getMimeType().startsWith('video/') ? "VIDEO" : "IMAGE",
|
||||||
|
fileSize: f.getSize().toString(),
|
||||||
httpMethod: "POST",
|
httpMethod: "POST",
|
||||||
file: f,
|
file: f,
|
||||||
originalItem: item
|
originalItem: item
|
||||||
@ -269,14 +295,26 @@ export class MediaService {
|
|||||||
filename: u.filename,
|
filename: u.filename,
|
||||||
mimeType: u.mimeType,
|
mimeType: u.mimeType,
|
||||||
resource: u.resource,
|
resource: u.resource,
|
||||||
|
fileSize: u.fileSize,
|
||||||
httpMethod: u.httpMethod
|
httpMethod: u.httpMethod
|
||||||
}))
|
}))
|
||||||
const stagedResp = shopifySvc.stagedUploadsCreate(stagedInput)
|
const stagedResp = shopifySvc.stagedUploadsCreate(stagedInput)
|
||||||
|
|
||||||
|
if (stagedResp.userErrors && stagedResp.userErrors.length > 0) {
|
||||||
|
console.error("[MediaService] stagedUploadsCreate Errors:", JSON.stringify(stagedResp.userErrors))
|
||||||
|
logs.push(`- Upload preparation failed: ${stagedResp.userErrors.map(e => e.message).join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
const targets = stagedResp.stagedTargets
|
const targets = stagedResp.stagedTargets
|
||||||
|
|
||||||
const mediaToCreate = []
|
const mediaToCreate = []
|
||||||
uploads.forEach((u, i) => {
|
uploads.forEach((u, i) => {
|
||||||
const target = targets[i]
|
const target = targets[i]
|
||||||
|
if (!target || !target.url) {
|
||||||
|
logs.push(`- Failed to get upload target for ${u.filename}: Invalid target`)
|
||||||
|
console.warn(`[MediaService] Missing target URL for ${u.filename}. Target:`, JSON.stringify(target))
|
||||||
|
return
|
||||||
|
}
|
||||||
const payload = {}
|
const payload = {}
|
||||||
target.parameters.forEach((p: any) => payload[p.name] = p.value)
|
target.parameters.forEach((p: any) => payload[p.name] = p.value)
|
||||||
payload['file'] = u.file.getBlob()
|
payload['file'] = u.file.getBlob()
|
||||||
@ -284,7 +322,7 @@ export class MediaService {
|
|||||||
mediaToCreate.push({
|
mediaToCreate.push({
|
||||||
originalSource: target.resourceUrl,
|
originalSource: target.resourceUrl,
|
||||||
alt: u.filename,
|
alt: u.filename,
|
||||||
mediaContentType: "IMAGE"
|
mediaContentType: u.resource
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -52,6 +52,7 @@ export class MockDriveService implements IDriveService {
|
|||||||
getThumbnail: () => ({ getBytes: () => [] }),
|
getThumbnail: () => ({ getBytes: () => [] }),
|
||||||
getMimeType: () => (blob as any).getContentType ? (blob as any).getContentType() : "image/jpeg",
|
getMimeType: () => (blob as any).getContentType ? (blob as any).getContentType() : "image/jpeg",
|
||||||
getDownloadUrl: () => `https://drive.google.com/uc?export=download&id=${id}`,
|
getDownloadUrl: () => `https://drive.google.com/uc?export=download&id=${id}`,
|
||||||
|
getSize: () => blob.getBytes ? blob.getBytes().length : 0,
|
||||||
getAppProperty: (key) => {
|
getAppProperty: (key) => {
|
||||||
return (newFile as any)._properties?.[key]
|
return (newFile as any)._properties?.[key]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user