wiki-plugin-shoppe 0.0.23 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/scripts/shoppe-sign.js +1 -1
- package/server/server.js +109 -22
package/package.json
CHANGED
|
@@ -185,7 +185,7 @@ function createZip() {
|
|
|
185
185
|
execSync(`powershell -NoProfile -Command "${psCmd}"`, { stdio: 'pipe' });
|
|
186
186
|
} else {
|
|
187
187
|
execSync(
|
|
188
|
-
`zip -r "${outputZip}" . -x "*/shoppe-key.json"`,
|
|
188
|
+
`zip -r "${outputZip}" . -x "*/shoppe-key.json" -x "*.mp4" -x "*.mov" -x "*.mkv" -x "*.webm" -x "*.avi"`,
|
|
189
189
|
{ cwd: SHOPPE_DIR, stdio: 'pipe' }
|
|
190
190
|
);
|
|
191
191
|
}
|
package/server/server.js
CHANGED
|
@@ -1125,26 +1125,10 @@ async function processArchive(zipPath) {
|
|
|
1125
1125
|
await sanoraUploadImage(tenant, title, coverBuf, coverFile);
|
|
1126
1126
|
}
|
|
1127
1127
|
|
|
1128
|
-
//
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
const videoBuf = fs.readFileSync(path.join(entryPath, videoFilename));
|
|
1133
|
-
const localHash = crypto.createHash('sha256').update(videoBuf).digest('hex');
|
|
1134
|
-
|
|
1135
|
-
const existing = existingLucilleVideos[title];
|
|
1136
|
-
if (existing && existing.contentHash && existing.contentHash === localHash) {
|
|
1137
|
-
console.log(`[shoppe] ⏩ video unchanged, skipping upload: ${title}`);
|
|
1138
|
-
results.videos.push({ title, price, skipped: true });
|
|
1139
|
-
} else {
|
|
1140
|
-
await lucilleRegisterVideo(tenant, title, description, tags, effectiveLucilleUrl);
|
|
1141
|
-
await lucilleUploadVideo(tenant, title, videoBuf, videoFilename, effectiveLucilleUrl);
|
|
1142
|
-
results.videos.push({ title, price });
|
|
1143
|
-
console.log(`[shoppe] 🎬 video: ${title}`);
|
|
1144
|
-
}
|
|
1145
|
-
} else {
|
|
1146
|
-
results.warnings.push(`video "${title}": no video file found (expected .mp4/.mov/.mkv/.webm/.avi)`);
|
|
1147
|
-
}
|
|
1128
|
+
// Register video metadata in Lucille (file upload happens separately via upload-info endpoint)
|
|
1129
|
+
await lucilleRegisterVideo(tenant, title, description, tags, effectiveLucilleUrl);
|
|
1130
|
+
results.videos.push({ title, price });
|
|
1131
|
+
console.log(`[shoppe] 🎬 video registered: ${title} (upload file separately)`);
|
|
1148
1132
|
} catch (err) {
|
|
1149
1133
|
console.warn(`[shoppe] ⚠️ video ${entry}: ${err.message}`);
|
|
1150
1134
|
results.warnings.push(`video "${entry}": ${err.message}`);
|
|
@@ -1262,7 +1246,8 @@ async function getShoppeGoods(tenant) {
|
|
|
1262
1246
|
shipping: product.shipping || 0,
|
|
1263
1247
|
image: product.image ? `${getSanoraUrl()}/images/${product.image}` : null,
|
|
1264
1248
|
url: (bucketName && redirects[bucketName]) || defaultUrl,
|
|
1265
|
-
...(lucillePlayerUrl && { lucillePlayerUrl })
|
|
1249
|
+
...(lucillePlayerUrl && { lucillePlayerUrl }),
|
|
1250
|
+
...(product.category === 'video' && { shoppeId: tenant.uuid })
|
|
1266
1251
|
};
|
|
1267
1252
|
const bucket = goods[bucketName];
|
|
1268
1253
|
if (bucket) bucket.push(item);
|
|
@@ -1539,12 +1524,35 @@ function renderCards(items, category) {
|
|
|
1539
1524
|
}
|
|
1540
1525
|
return items.map(item => {
|
|
1541
1526
|
const isVideo = !!item.lucillePlayerUrl;
|
|
1527
|
+
const isUnuploadedVideo = item.shoppeId && !item.lucillePlayerUrl;
|
|
1542
1528
|
const imgHtml = item.image
|
|
1543
1529
|
? `<div class="card-img${isVideo ? ' card-video-play' : ''}"><img src="${item.image}" alt="" loading="lazy"></div>`
|
|
1544
|
-
:
|
|
1530
|
+
: isUnuploadedVideo
|
|
1531
|
+
? `<div class="card-img-placeholder card-video-upload"><span style="font-size:44px">🎬</span></div>`
|
|
1532
|
+
: `<div class="card-img-placeholder">${CATEGORY_EMOJI[category] || '🎁'}</div>`;
|
|
1545
1533
|
const priceHtml = (item.price > 0 || category === 'product')
|
|
1546
1534
|
? `<div class="price">$${(item.price / 100).toFixed(2)}${item.shipping ? ` <span class="shipping">+ $${(item.shipping / 100).toFixed(2)} shipping</span>` : ''}</div>`
|
|
1547
1535
|
: '';
|
|
1536
|
+
if (isUnuploadedVideo) {
|
|
1537
|
+
const safeTitle = item.title.replace(/'/g, "\\'");
|
|
1538
|
+
return `
|
|
1539
|
+
<div class="card" id="video-card-${item.shoppeId}-${item.title.replace(/[^a-z0-9]/gi,'_')}">
|
|
1540
|
+
${imgHtml}
|
|
1541
|
+
<div class="card-body">
|
|
1542
|
+
<div class="card-title">${item.title}</div>
|
|
1543
|
+
${item.description ? `<div class="card-desc">${item.description}</div>` : ''}
|
|
1544
|
+
${priceHtml}
|
|
1545
|
+
<div class="video-upload-area" id="upload-area-${item.shoppeId}-${item.title.replace(/[^a-z0-9]/gi,'_')}">
|
|
1546
|
+
<label class="upload-btn-label">
|
|
1547
|
+
📁 Upload Video
|
|
1548
|
+
<input type="file" accept="video/*" style="display:none"
|
|
1549
|
+
onchange="startVideoUpload(this,'${item.shoppeId}','${safeTitle}')">
|
|
1550
|
+
</label>
|
|
1551
|
+
<div class="upload-progress" style="display:none"></div>
|
|
1552
|
+
</div>
|
|
1553
|
+
</div>
|
|
1554
|
+
</div>`;
|
|
1555
|
+
}
|
|
1548
1556
|
const clickHandler = isVideo
|
|
1549
1557
|
? `playVideo('${item.lucillePlayerUrl}')`
|
|
1550
1558
|
: `window.open('${item.url}','_blank')`;
|
|
@@ -1622,6 +1630,12 @@ function generateShoppeHTML(tenant, goods) {
|
|
|
1622
1630
|
.video-modal-content iframe { width: 100%; height: 100%; border: none; display: block; }
|
|
1623
1631
|
.video-modal-close { position: absolute; top: 10px; right: 12px; z-index: 2; background: rgba(0,0,0,0.5); border: none; color: #fff; font-size: 20px; line-height: 1; padding: 4px 10px; border-radius: 6px; cursor: pointer; }
|
|
1624
1632
|
.video-modal-close:hover { background: rgba(0,0,0,0.8); }
|
|
1633
|
+
.card-video-upload { cursor: default !important; }
|
|
1634
|
+
.upload-btn-label { display: inline-block; background: #0066cc; color: white; border-radius: 8px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; margin-top: 8px; }
|
|
1635
|
+
.upload-btn-label:hover { background: #0052a3; }
|
|
1636
|
+
.upload-progress { margin-top: 8px; font-size: 12px; color: #555; }
|
|
1637
|
+
.upload-progress-bar { height: 4px; background: #e0e0e0; border-radius: 2px; margin-top: 4px; overflow: hidden; }
|
|
1638
|
+
.upload-progress-bar-fill { height: 100%; background: #0066cc; border-radius: 2px; transition: width 0.2s; }
|
|
1625
1639
|
</style>
|
|
1626
1640
|
</head>
|
|
1627
1641
|
<body>
|
|
@@ -1668,6 +1682,56 @@ function generateShoppeHTML(tenant, goods) {
|
|
|
1668
1682
|
document.getElementById('video-iframe').src = '';
|
|
1669
1683
|
}
|
|
1670
1684
|
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeVideo(); });
|
|
1685
|
+
|
|
1686
|
+
async function startVideoUpload(input, shoppeId, title) {
|
|
1687
|
+
const file = input.files[0];
|
|
1688
|
+
if (!file) return;
|
|
1689
|
+
|
|
1690
|
+
const areaId = 'upload-area-' + shoppeId + '-' + title.replace(/[^a-z0-9]/gi,'_');
|
|
1691
|
+
const area = document.getElementById(areaId);
|
|
1692
|
+
const progressDiv = area.querySelector('.upload-progress');
|
|
1693
|
+
const label = area.querySelector('.upload-btn-label');
|
|
1694
|
+
|
|
1695
|
+
label.style.display = 'none';
|
|
1696
|
+
progressDiv.style.display = 'block';
|
|
1697
|
+
progressDiv.innerHTML = 'Getting upload credentials…';
|
|
1698
|
+
|
|
1699
|
+
try {
|
|
1700
|
+
const infoRes = await fetch('/plugin/shoppe/' + shoppeId + '/video/' + encodeURIComponent(title) + '/upload-info');
|
|
1701
|
+
if (!infoRes.ok) throw new Error('Could not get upload credentials (' + infoRes.status + ')');
|
|
1702
|
+
const { uploadUrl, timestamp, signature } = await infoRes.json();
|
|
1703
|
+
|
|
1704
|
+
progressDiv.innerHTML = 'Uploading… 0%<div class="upload-progress-bar"><div class="upload-progress-bar-fill" id="fill-' + areaId + '" style="width:0%"></div></div>';
|
|
1705
|
+
|
|
1706
|
+
const form = new FormData();
|
|
1707
|
+
form.append('video', file, file.name);
|
|
1708
|
+
|
|
1709
|
+
await new Promise((resolve, reject) => {
|
|
1710
|
+
const xhr = new XMLHttpRequest();
|
|
1711
|
+
xhr.upload.onprogress = e => {
|
|
1712
|
+
if (e.lengthComputable) {
|
|
1713
|
+
const pct = Math.round(e.loaded / e.total * 100);
|
|
1714
|
+
progressDiv.querySelector('div').textContent = '';
|
|
1715
|
+
progressDiv.firstChild.textContent = 'Uploading… ' + pct + '%';
|
|
1716
|
+
const fill = document.getElementById('fill-' + areaId);
|
|
1717
|
+
if (fill) fill.style.width = pct + '%';
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
xhr.onload = () => xhr.status === 200 ? resolve() : reject(new Error('Upload failed: ' + xhr.status));
|
|
1721
|
+
xhr.onerror = () => reject(new Error('Network error during upload'));
|
|
1722
|
+
xhr.open('PUT', uploadUrl);
|
|
1723
|
+
xhr.setRequestHeader('x-pn-timestamp', timestamp);
|
|
1724
|
+
xhr.setRequestHeader('x-pn-signature', signature);
|
|
1725
|
+
xhr.send(form);
|
|
1726
|
+
});
|
|
1727
|
+
|
|
1728
|
+
progressDiv.innerHTML = '✅ Uploaded! Reloading…';
|
|
1729
|
+
setTimeout(() => location.reload(), 1500);
|
|
1730
|
+
} catch (err) {
|
|
1731
|
+
progressDiv.innerHTML = '❌ ' + err.message;
|
|
1732
|
+
label.style.display = 'inline-block';
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1671
1735
|
</script>
|
|
1672
1736
|
</body>
|
|
1673
1737
|
</html>`;
|
|
@@ -2473,6 +2537,29 @@ async function startServer(params) {
|
|
|
2473
2537
|
}
|
|
2474
2538
|
});
|
|
2475
2539
|
|
|
2540
|
+
// GET /plugin/shoppe/:id/video/:title/upload-info (owner only)
|
|
2541
|
+
// Returns a pre-signed lucille upload URL so the browser can PUT the video file directly to lucille.
|
|
2542
|
+
app.get('/plugin/shoppe/:identifier/video/:title/upload-info', owner, async (req, res) => {
|
|
2543
|
+
try {
|
|
2544
|
+
const tenant = getTenantByIdentifier(req.params.identifier);
|
|
2545
|
+
if (!tenant) return res.status(404).json({ error: 'tenant not found' });
|
|
2546
|
+
if (!tenant.lucilleKeys) return res.status(400).json({ error: 'tenant has no lucille user — re-register' });
|
|
2547
|
+
|
|
2548
|
+
const title = req.params.title;
|
|
2549
|
+
const lucilleBase = getLucilleUrl().replace(/\/$/, '');
|
|
2550
|
+
const { uuid: lucilleUuid, pubKey, privateKey } = tenant.lucilleKeys;
|
|
2551
|
+
|
|
2552
|
+
const timestamp = Date.now().toString();
|
|
2553
|
+
sessionless.getKeys = () => ({ pubKey, privateKey });
|
|
2554
|
+
const signature = await sessionless.sign(timestamp + pubKey);
|
|
2555
|
+
|
|
2556
|
+
const uploadUrl = `${lucilleBase}/user/${lucilleUuid}/video/${encodeURIComponent(title)}/file`;
|
|
2557
|
+
res.json({ uploadUrl, timestamp, signature });
|
|
2558
|
+
} catch (err) {
|
|
2559
|
+
res.status(500).json({ error: err.message });
|
|
2560
|
+
}
|
|
2561
|
+
});
|
|
2562
|
+
|
|
2476
2563
|
// Goods JSON (public)
|
|
2477
2564
|
app.get('/plugin/shoppe/:identifier/goods', async (req, res) => {
|
|
2478
2565
|
try {
|