diff --git a/public/index.html b/public/index.html
index 2e33adc..fa97458 100644
--- a/public/index.html
+++ b/public/index.html
@@ -47,8 +47,6 @@
diff --git a/public/js/main.js b/public/js/main.js
index ae88532..3efde08 100644
--- a/public/js/main.js
+++ b/public/js/main.js
@@ -2,8 +2,6 @@ document.addEventListener('DOMContentLoaded', () => {
const videoListEl = document.getElementById('video-list');
const loadingSpinner = document.getElementById('loading-spinner');
const refreshBtn = document.getElementById('refresh-btn');
- const clearDownloadCacheBtn = document.getElementById('clear-download-cache-btn');
- const clearTranscodeCacheBtn = document.getElementById('clear-transcode-cache-btn');
const bucketSelect = document.getElementById('bucket-select');
const loginScreen = document.getElementById('login-screen');
const appContainer = document.getElementById('app-container');
@@ -675,26 +673,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
};
- const clearDownloadCache = async () => {
- if (!clearDownloadCacheBtn) return;
- clearDownloadCacheBtn.disabled = true;
- clearDownloadCacheBtn.textContent = '清空中...';
- try {
- const res = await fetch('/api/clear-download-cache', { method: 'POST' });
- if (!res.ok) {
- const data = await res.json().catch(() => ({}));
- throw new Error(data.error || '清空下载缓存失败');
- }
- alert('下载缓存已清空');
- } catch (err) {
- console.error('Clear download cache failed:', err);
- alert(`清空下载缓存失败: ${err.message}`);
- } finally {
- clearDownloadCacheBtn.disabled = false;
- clearDownloadCacheBtn.textContent = '清空下载缓存';
- }
- };
if (transcodeBtn) {
transcodeBtn.addEventListener('click', () => {
@@ -734,7 +713,8 @@ document.addEventListener('DOMContentLoaded', () => {
// Build a tree structure from S3 keys
const tree = {};
- data.videos.forEach(key => {
+ data.videos.forEach(vid => {
+ const key = vid.key;
const parts = key.split('/');
let current = tree;
parts.forEach((part, index) => {
@@ -743,15 +723,22 @@ document.addEventListener('DOMContentLoaded', () => {
}
if (index === parts.length - 1) {
current[part].__file = key;
+ current[part].__hasTranscodeCache = vid.hasTranscodeCache;
}
current = current[part];
});
});
- const createFileItem = (name, key) => {
+ const createFileItem = (name, key, hasTranscodeCache) => {
const li = document.createElement('li');
li.className = 'video-item file-item';
const ext = name.split('.').pop().toUpperCase();
+
+ let cacheButtonHtml = '';
+ if (hasTranscodeCache) {
+ cacheButtonHtml = `
`;
+ }
+
li.innerHTML = `
@@ -760,19 +747,44 @@ document.addEventListener('DOMContentLoaded', () => {
${name}
Video / ${ext}
+ ${cacheButtonHtml}
`;
li.addEventListener('click', (e) => {
e.stopPropagation();
selectVideo(key, li);
});
+
+ const clearBtn = li.querySelector('.clear-video-cache-btn');
+ if (clearBtn) {
+ clearBtn.addEventListener('click', async (e) => {
+ e.stopPropagation();
+ clearBtn.disabled = true;
+ clearBtn.textContent = '清空中...';
+ try {
+ const res = await fetch('/api/clear-video-transcode-cache', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ bucket: selectedBucket, key: key })
+ });
+ if (!res.ok) throw new Error('Failed to clear cache');
+ fetchVideos(selectedBucket);
+ } catch (err) {
+ console.error(err);
+ alert('清空转码缓存失败: ' + err.message);
+ clearBtn.disabled = false;
+ clearBtn.textContent = '清空转码缓存';
+ }
+ });
+ }
+
return li;
};
const renderTree = (node, container) => {
for (const [name, value] of Object.entries(node)) {
- if (name === '__file') continue;
+ if (name === '__file' || name === '__hasTranscodeCache') continue;
- const childKeys = Object.keys(value).filter(key => key !== '__file');
+ const childKeys = Object.keys(value).filter(k => k !== '__file' && k !== '__hasTranscodeCache');
const hasChildren = childKeys.length > 0;
const isFile = typeof value.__file === 'string';
@@ -803,13 +815,13 @@ document.addEventListener('DOMContentLoaded', () => {
li.appendChild(folderHeader);
if (isFile) {
- subListContainer.appendChild(createFileItem(name, value.__file));
+ subListContainer.appendChild(createFileItem(name, value.__file, value.__hasTranscodeCache));
}
renderTree(value, subListContainer);
li.appendChild(subListContainer);
container.appendChild(li);
} else if (isFile) {
- container.appendChild(createFileItem(name, value.__file));
+ container.appendChild(createFileItem(name, value.__file, value.__hasTranscodeCache));
}
}
};
@@ -1100,29 +1112,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Bind events
refreshBtn.addEventListener('click', () => fetchVideos(selectedBucket));
- if (clearDownloadCacheBtn) {
- clearDownloadCacheBtn.addEventListener('click', clearDownloadCache);
- }
- if (clearTranscodeCacheBtn) {
- clearTranscodeCacheBtn.addEventListener('click', async () => {
- clearTranscodeCacheBtn.disabled = true;
- clearTranscodeCacheBtn.textContent = '清空中...';
- try {
- const res = await fetch('/api/clear-transcode-cache', { method: 'POST' });
- if (!res.ok) {
- const data = await res.json().catch(() => ({}));
- throw new Error(data.error || '清空转码缓存失败');
- }
- alert('转码缓存已清空');
- } catch (err) {
- console.error('Clear transcode cache failed:', err);
- alert(`清空转码缓存失败: ${err.message}`);
- } finally {
- clearTranscodeCacheBtn.disabled = false;
- clearTranscodeCacheBtn.textContent = '清空转码缓存';
- }
- });
- }
+
if (stopTranscodeBtn) {
stopTranscodeBtn.addEventListener('click', stopTranscode);
}
diff --git a/server.js b/server.js
index ecbc9ea..39a1f83 100644
--- a/server.js
+++ b/server.js
@@ -7,7 +7,7 @@ const path = require('path');
const http = require('http');
const WebSocket = require('ws');
const ffmpeg = require('fluent-ffmpeg');
-const { S3Client, ListBucketsCommand, ListObjectsV2Command, GetObjectCommand } = require('@aws-sdk/client-s3');
+const { S3Client, ListBucketsCommand, ListObjectsV2Command, GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
const crypto = require('crypto');
const Redis = require('ioredis');
@@ -396,16 +396,36 @@ const ensureS3Downloaded = async (s3Client, bucket, key, tmpInputPath, progressK
}
}
+ let shouldDownload = true;
+ let s3Metadata = null;
+
if (fs.existsSync(tmpInputPath)) {
const stats = fs.statSync(tmpInputPath);
- const totalBytes = stats.size;
+ const localSize = stats.size;
+
+ try {
+ const headCommand = new HeadObjectCommand({ Bucket: bucket, Key: key });
+ s3Metadata = await s3Client.send(headCommand);
+
+ if (s3Metadata.ContentLength === localSize) {
+ shouldDownload = false;
+ console.log(`[Cache] Verified ${key}: Local size matches S3 (${localSize} bytes).`);
+ } else {
+ console.log(`[Cache] Mismatch for ${key}: Local ${localSize} vs S3 ${s3Metadata.ContentLength}. Re-downloading.`);
+ }
+ } catch (err) {
+ console.error(`[Cache] Failed to verify S3 metadata for ${key}:`, err.message);
+ }
+ }
+
+ if (!shouldDownload) {
progressMap[progressKey] = {
status: 'downloaded',
percent: 100,
- downloadedBytes: totalBytes,
- totalBytes,
+ downloadedBytes: s3Metadata.ContentLength,
+ totalBytes: s3Metadata.ContentLength,
streamSessionId,
- details: 'Source already downloaded locally...',
+ details: 'Source cached locally and verified against S3...',
mp4Url: null
};
broadcastWs(progressKey, { type: 'progress', key, progress: progressMap[progressKey] });
@@ -489,39 +509,7 @@ const ensureS3Downloaded = async (s3Client, bucket, key, tmpInputPath, progressK
}
};
-const clearDownloadCache = () => {
- const tmpDir = CACHE_DIR;
- try {
- if (!fs.existsSync(tmpDir)) return;
- const files = fs.readdirSync(tmpDir);
- for (const file of files) {
- if (file.startsWith('s3-input-') && (file.endsWith('.tmp') || file.endsWith('.downloading'))) {
- const filePath = path.join(tmpDir, file);
- fs.rmSync(filePath, { force: true });
- }
- }
- } catch (err) {
- console.error('Failed to clear download cache:', err);
- throw err;
- }
-};
-const clearTranscodeCache = () => {
- const tmpDir = CACHE_DIR;
- try {
- if (!fs.existsSync(tmpDir)) return;
- const files = fs.readdirSync(tmpDir);
- for (const file of files) {
- if (file.startsWith('hls-')) {
- const filePath = path.join(tmpDir, file);
- fs.rmSync(filePath, { recursive: true, force: true });
- }
- }
- } catch (err) {
- console.error('Failed to clear transcode cache:', err);
- throw err;
- }
-};
app.get('/api/buckets', async (req, res) => {
@@ -573,6 +561,15 @@ app.get('/api/videos', async (req, res) => {
if (!key) return false;
const lowerKey = key.toLowerCase();
return videoExtensions.some(ext => lowerKey.endsWith(ext));
+ })
+ .map(key => {
+ const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_');
+ const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-z0-9]/gi, '_'));
+ const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${safeKeySegments.join('-')}`);
+ return {
+ key: key,
+ hasTranscodeCache: fs.existsSync(hlsDir)
+ };
});
res.json({ videos });
@@ -622,22 +619,22 @@ app.get('/api/config', (req, res) => {
});
});
-app.post('/api/clear-download-cache', (req, res) => {
+app.post('/api/clear-video-transcode-cache', async (req, res) => {
try {
- clearDownloadCache();
- res.json({ message: 'Download cache cleared' });
- } catch (error) {
- console.error('Error clearing download cache:', error);
- res.status(500).json({ error: 'Failed to clear download cache', detail: error.message });
- }
-});
+ const { bucket, key } = req.body;
+ if (!bucket || !key) {
+ return res.status(400).json({ error: 'Bucket and key are required' });
+ }
+ const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_');
+ const safeKeySegments = key.split('/').map(segment => segment.replace(/[^a-z0-9]/gi, '_'));
+ const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${safeKeySegments.join('-')}`);
-app.post('/api/clear-transcode-cache', (req, res) => {
- try {
- clearTranscodeCache();
- res.json({ message: 'Transcode cache cleared' });
+ if (fs.existsSync(hlsDir)) {
+ fs.rmSync(hlsDir, { recursive: true, force: true });
+ }
+ res.json({ message: 'Transcode cache cleared for video' });
} catch (error) {
- console.error('Error clearing transcode cache:', error);
+ console.error('Error clearing video transcode cache:', error);
res.status(500).json({ error: 'Failed to clear transcode cache', detail: error.message });
}
});