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 }); } });