优化页面布局,修复故障视频拉动进度条的故障
This commit is contained in:
@@ -47,8 +47,6 @@
|
||||
</label>
|
||||
<div class="banner-actions">
|
||||
<button id="refresh-btn" class="action-btn" title="刷新列表">刷新列表</button>
|
||||
<button id="clear-download-cache-btn" class="action-btn" title="清空下载缓存">清空下载缓存</button>
|
||||
<button id="clear-transcode-cache-btn" class="action-btn" title="清空转码缓存">清空转码缓存</button>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<button id="logout-btn" class="action-btn danger">退出登录</button>
|
||||
|
||||
@@ -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 = `<button class="action-btn danger clear-video-cache-btn" data-key="${key}" style="padding: 0.3rem 0.6rem; font-size: 0.75rem; margin-left: auto;" title="清空转码缓存">清空转码缓存</button>`;
|
||||
}
|
||||
|
||||
li.innerHTML = `
|
||||
<div class="video-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 8-6 4 6 4V8Z"/><rect width="14" height="12" x="2" y="6" rx="2" ry="2"/></svg>
|
||||
@@ -760,19 +747,44 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
<div class="video-title" title="${key}">${name}</div>
|
||||
<div class="video-meta">Video / ${ext}</div>
|
||||
</div>
|
||||
${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);
|
||||
}
|
||||
|
||||
97
server.js
97
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 });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user