Compare commits
2 Commits
a6af3765a8
...
df5999c338
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df5999c338 | ||
|
|
3b65c306de |
@@ -139,8 +139,12 @@
|
|||||||
<div id="now-playing" class="now-playing hidden">
|
<div id="now-playing" class="now-playing hidden">
|
||||||
<h3>Now Playing</h3>
|
<h3>Now Playing</h3>
|
||||||
<p id="current-video-title">video.mp4</p>
|
<p id="current-video-title">video.mp4</p>
|
||||||
<button id="transcode-btn" class="play-btn hidden">Start Transcode</button>
|
<div style="display: flex; gap: 0.5rem; margin-top: 1rem; align-items: center; flex-wrap: wrap;">
|
||||||
<button id="stop-transcode-btn" class="play-btn stop-btn hidden">Stop Transcode</button>
|
<button id="transcode-btn" class="play-btn hidden">Start Transcode</button>
|
||||||
|
<button id="stop-transcode-btn" class="play-btn stop-btn hidden">Stop Transcode</button>
|
||||||
|
<button id="clear-playing-download-cache-btn" class="action-btn danger hidden">清空下载缓存</button>
|
||||||
|
<button id="clear-playing-transcode-cache-btn" class="action-btn danger hidden">清空转码缓存</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const currentVideoTitle = document.getElementById('current-video-title');
|
const currentVideoTitle = document.getElementById('current-video-title');
|
||||||
const transcodeBtn = document.getElementById('transcode-btn');
|
const transcodeBtn = document.getElementById('transcode-btn');
|
||||||
const stopTranscodeBtn = document.getElementById('stop-transcode-btn');
|
const stopTranscodeBtn = document.getElementById('stop-transcode-btn');
|
||||||
|
const clearPlayingDownloadBtn = document.getElementById('clear-playing-download-cache-btn');
|
||||||
|
const clearPlayingTranscodeBtn = document.getElementById('clear-playing-transcode-cache-btn');
|
||||||
const themeSelector = document.getElementById('theme-selector');
|
const themeSelector = document.getElementById('theme-selector');
|
||||||
const videoListHeader = document.getElementById('video-list-header');
|
const videoListHeader = document.getElementById('video-list-header');
|
||||||
const logoutBtn = document.getElementById('logout-btn');
|
const logoutBtn = document.getElementById('logout-btn');
|
||||||
@@ -724,21 +726,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (index === parts.length - 1) {
|
if (index === parts.length - 1) {
|
||||||
current[part].__file = key;
|
current[part].__file = key;
|
||||||
current[part].__hasTranscodeCache = vid.hasTranscodeCache;
|
current[part].__hasTranscodeCache = vid.hasTranscodeCache;
|
||||||
|
current[part].__hasDownloadCache = vid.hasDownloadCache;
|
||||||
}
|
}
|
||||||
current = current[part];
|
current = current[part];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const createFileItem = (name, key, hasTranscodeCache) => {
|
const createFileItem = (name, key, hasTranscodeCache, hasDownloadCache) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'video-item file-item';
|
li.className = 'video-item file-item';
|
||||||
const ext = name.split('.').pop().toUpperCase();
|
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 = `
|
li.innerHTML = `
|
||||||
<div class="video-icon">
|
<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>
|
<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>
|
||||||
@@ -747,44 +745,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
<div class="video-title" title="${key}">${name}</div>
|
<div class="video-title" title="${key}">${name}</div>
|
||||||
<div class="video-meta">Video / ${ext}</div>
|
<div class="video-meta">Video / ${ext}</div>
|
||||||
</div>
|
</div>
|
||||||
${cacheButtonHtml}
|
|
||||||
`;
|
`;
|
||||||
li.addEventListener('click', (e) => {
|
li.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
selectVideo(key, li);
|
selectVideo(key, li, hasTranscodeCache, hasDownloadCache);
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
return li;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTree = (node, container) => {
|
const renderTree = (node, container) => {
|
||||||
for (const [name, value] of Object.entries(node)) {
|
for (const [name, value] of Object.entries(node)) {
|
||||||
if (name === '__file' || name === '__hasTranscodeCache') continue;
|
if (name === '__file' || name === '__hasTranscodeCache' || name === '__hasDownloadCache') continue;
|
||||||
|
|
||||||
const childKeys = Object.keys(value).filter(k => k !== '__file' && k !== '__hasTranscodeCache');
|
const childKeys = Object.keys(value).filter(k => k !== '__file' && k !== '__hasTranscodeCache' && k !== '__hasDownloadCache');
|
||||||
const hasChildren = childKeys.length > 0;
|
const hasChildren = childKeys.length > 0;
|
||||||
const isFile = typeof value.__file === 'string';
|
const isFile = typeof value.__file === 'string';
|
||||||
|
|
||||||
@@ -815,13 +789,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
li.appendChild(folderHeader);
|
li.appendChild(folderHeader);
|
||||||
if (isFile) {
|
if (isFile) {
|
||||||
subListContainer.appendChild(createFileItem(name, value.__file, value.__hasTranscodeCache));
|
subListContainer.appendChild(createFileItem(name, value.__file, value.__hasTranscodeCache, value.__hasDownloadCache));
|
||||||
}
|
}
|
||||||
renderTree(value, subListContainer);
|
renderTree(value, subListContainer);
|
||||||
li.appendChild(subListContainer);
|
li.appendChild(subListContainer);
|
||||||
container.appendChild(li);
|
container.appendChild(li);
|
||||||
} else if (isFile) {
|
} else if (isFile) {
|
||||||
container.appendChild(createFileItem(name, value.__file, value.__hasTranscodeCache));
|
container.appendChild(createFileItem(name, value.__file, value.__hasTranscodeCache, value.__hasDownloadCache));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -834,7 +808,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle video selection
|
// Handle video selection
|
||||||
const selectVideo = (key, listItemNode) => {
|
const selectVideo = (key, listItemNode, hasTranscodeCache = false, hasDownloadCache = false) => {
|
||||||
document.querySelectorAll('.video-item').forEach(n => n.classList.remove('active'));
|
document.querySelectorAll('.video-item').forEach(n => n.classList.remove('active'));
|
||||||
listItemNode.classList.add('active');
|
listItemNode.classList.add('active');
|
||||||
|
|
||||||
@@ -868,6 +842,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
stopTranscodeBtn.disabled = false;
|
stopTranscodeBtn.disabled = false;
|
||||||
stopTranscodeBtn.textContent = 'Stop Transcode';
|
stopTranscodeBtn.textContent = 'Stop Transcode';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clearPlayingDownloadBtn) {
|
||||||
|
if (hasDownloadCache) clearPlayingDownloadBtn.classList.remove('hidden');
|
||||||
|
else clearPlayingDownloadBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
if (clearPlayingTranscodeBtn) {
|
||||||
|
if (hasTranscodeCache) clearPlayingTranscodeBtn.classList.remove('hidden');
|
||||||
|
else clearPlayingTranscodeBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
if (playBtn) {
|
if (playBtn) {
|
||||||
playBtn.classList.add('hidden');
|
playBtn.classList.add('hidden');
|
||||||
}
|
}
|
||||||
@@ -1116,6 +1100,52 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (stopTranscodeBtn) {
|
if (stopTranscodeBtn) {
|
||||||
stopTranscodeBtn.addEventListener('click', stopTranscode);
|
stopTranscodeBtn.addEventListener('click', stopTranscode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clearPlayingDownloadBtn) {
|
||||||
|
clearPlayingDownloadBtn.addEventListener('click', async () => {
|
||||||
|
if (!currentVideoKey) return;
|
||||||
|
clearPlayingDownloadBtn.disabled = true;
|
||||||
|
clearPlayingDownloadBtn.textContent = '清空中...';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/clear-video-download-cache', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ bucket: selectedBucket, key: currentVideoKey })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('清空失败');
|
||||||
|
clearPlayingDownloadBtn.classList.add('hidden');
|
||||||
|
fetchVideos(selectedBucket);
|
||||||
|
} catch (err) {
|
||||||
|
alert('清空下载缓存失败: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
clearPlayingDownloadBtn.disabled = false;
|
||||||
|
clearPlayingDownloadBtn.textContent = '清空下载缓存';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearPlayingTranscodeBtn) {
|
||||||
|
clearPlayingTranscodeBtn.addEventListener('click', async () => {
|
||||||
|
if (!currentVideoKey) return;
|
||||||
|
clearPlayingTranscodeBtn.disabled = true;
|
||||||
|
clearPlayingTranscodeBtn.textContent = '清空中...';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/clear-video-transcode-cache', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ bucket: selectedBucket, key: currentVideoKey })
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('清空失败');
|
||||||
|
clearPlayingTranscodeBtn.classList.add('hidden');
|
||||||
|
fetchVideos(selectedBucket);
|
||||||
|
} catch (err) {
|
||||||
|
alert('清空转码缓存失败: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
clearPlayingTranscodeBtn.disabled = false;
|
||||||
|
clearPlayingTranscodeBtn.textContent = '清空转码缓存';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
if (loginBtn) {
|
if (loginBtn) {
|
||||||
loginBtn.addEventListener('click', login);
|
loginBtn.addEventListener('click', login);
|
||||||
}
|
}
|
||||||
|
|||||||
24
server.js
24
server.js
@@ -566,9 +566,11 @@ app.get('/api/videos', async (req, res) => {
|
|||||||
const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_');
|
const safeBucket = bucket.replace(/[^a-z0-9]/gi, '_');
|
||||||
const safeKeySegments = key.split('/').map(segment => segment.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('-')}`);
|
const hlsDir = path.join(CACHE_DIR, `hls-${safeBucket}-${safeKeySegments.join('-')}`);
|
||||||
|
const tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
|
||||||
return {
|
return {
|
||||||
key: key,
|
key: key,
|
||||||
hasTranscodeCache: fs.existsSync(hlsDir)
|
hasTranscodeCache: fs.existsSync(hlsDir),
|
||||||
|
hasDownloadCache: fs.existsSync(tmpInputPath)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -639,6 +641,26 @@ app.post('/api/clear-video-transcode-cache', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/clear-video-download-cache', async (req, res) => {
|
||||||
|
try {
|
||||||
|
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 tmpInputPath = path.join(CACHE_DIR, `s3-input-${safeBucket}-${safeKeySegments.join('-')}.tmp`);
|
||||||
|
|
||||||
|
if (fs.existsSync(tmpInputPath)) {
|
||||||
|
fs.rmSync(tmpInputPath, { force: true });
|
||||||
|
}
|
||||||
|
res.json({ message: 'Download cache cleared for video' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing video download cache:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to clear download cache', detail: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/stop-transcode', (req, res) => {
|
app.post('/api/stop-transcode', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { key } = req.body;
|
const { key } = req.body;
|
||||||
|
|||||||
Reference in New Issue
Block a user