Improve video gallery filtering and thumbnail display
- Filter out videos smaller than 1KB (test files) - Verify videos exist in R2 before displaying in gallery - Add thumbnail error handling with placeholder icons - Improve video preview with proper metadata loading - Update API list endpoint to exclude small/test files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
76b9485d2c
commit
50e44f7c24
|
|
@ -347,8 +347,15 @@ async function handlePublicList(bucket, kv, corsHeaders) {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter to only shareable videos
|
// Filter to only shareable videos and exclude small/test files
|
||||||
const shareableVideos = videos.filter(v => v.visibility === 'shareable');
|
const validVideoExtensions = ['.mp4', '.mkv', '.mov', '.avi', '.webm', '.flv', '.wmv'];
|
||||||
|
const shareableVideos = videos.filter(v => {
|
||||||
|
if (v.visibility !== 'shareable') return false;
|
||||||
|
if (v.name.startsWith('live/')) return false;
|
||||||
|
if (v.size < 1024) return false; // Exclude files smaller than 1KB
|
||||||
|
const ext = v.name.substring(v.name.lastIndexOf('.')).toLowerCase();
|
||||||
|
return validVideoExtensions.includes(ext);
|
||||||
|
});
|
||||||
|
|
||||||
return new Response(JSON.stringify({ count: shareableVideos.length, videos: shareableVideos }), {
|
return new Response(JSON.stringify({ count: shareableVideos.length, videos: shareableVideos }), {
|
||||||
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
headers: { 'Content-Type': 'application/json', ...corsHeaders }
|
||||||
|
|
@ -502,7 +509,7 @@ async function handlePublicGallery(bucket, kv, corsHeaders) {
|
||||||
|
|
||||||
// Filter to only shareable videos AND exclude HLS live stream files
|
// Filter to only shareable videos AND exclude HLS live stream files
|
||||||
const validVideoExtensions = ['.mp4', '.mkv', '.mov', '.avi', '.webm', '.flv', '.wmv'];
|
const validVideoExtensions = ['.mp4', '.mkv', '.mov', '.avi', '.webm', '.flv', '.wmv'];
|
||||||
const shareableVideos = videos.filter(v => {
|
const candidateVideos = videos.filter(v => {
|
||||||
// Must be shareable
|
// Must be shareable
|
||||||
if (v.visibility !== 'shareable') return false;
|
if (v.visibility !== 'shareable') return false;
|
||||||
|
|
||||||
|
|
@ -511,9 +518,28 @@ async function handlePublicGallery(bucket, kv, corsHeaders) {
|
||||||
|
|
||||||
// Only include actual video files (not HLS chunks)
|
// Only include actual video files (not HLS chunks)
|
||||||
const ext = v.key.substring(v.key.lastIndexOf('.')).toLowerCase();
|
const ext = v.key.substring(v.key.lastIndexOf('.')).toLowerCase();
|
||||||
|
|
||||||
|
// Filter out test files and files that are too small to be real videos (< 1KB)
|
||||||
|
if (v.size < 1024) return false;
|
||||||
|
|
||||||
return validVideoExtensions.includes(ext);
|
return validVideoExtensions.includes(ext);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Verify each video actually exists and is accessible by trying to read first byte
|
||||||
|
const shareableVideos = [];
|
||||||
|
for (const video of candidateVideos) {
|
||||||
|
try {
|
||||||
|
// Try to actually fetch the first 1 byte to verify the video exists
|
||||||
|
const testObject = await bucket.get(video.key, { range: { offset: 0, length: 1 } });
|
||||||
|
if (testObject && testObject.body) {
|
||||||
|
shareableVideos.push(video);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Video doesn't exist or isn't accessible, skip it
|
||||||
|
console.log(`Skipping inaccessible video: ${video.key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const videoItems = shareableVideos
|
const videoItems = shareableVideos
|
||||||
.map(obj => {
|
.map(obj => {
|
||||||
const sizeInMB = (obj.size / (1024 * 1024)).toFixed(2);
|
const sizeInMB = (obj.size / (1024 * 1024)).toFixed(2);
|
||||||
|
|
@ -522,8 +548,8 @@ async function handlePublicGallery(bucket, kv, corsHeaders) {
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="video-item">
|
<div class="video-item">
|
||||||
<div class="video-thumbnail" onclick="window.location.href='/watch/${encodedKey}'">
|
<div class="video-thumbnail" onclick="playVideo('${encodedKey}')">
|
||||||
<video preload="metadata">
|
<video id="thumb-${encodedKey}" preload="metadata" muted>
|
||||||
<source src="/${obj.key}#t=0.5" type="${getContentType(obj.key)}">
|
<source src="/${obj.key}#t=0.5" type="${getContentType(obj.key)}">
|
||||||
</video>
|
</video>
|
||||||
<div class="play-overlay">
|
<div class="play-overlay">
|
||||||
|
|
@ -531,10 +557,10 @@ async function handlePublicGallery(bucket, kv, corsHeaders) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="video-info">
|
<div class="video-info">
|
||||||
<h3 onclick="window.location.href='/watch/${encodedKey}'" style="cursor: pointer;">${obj.key}</h3>
|
<h3 onclick="playVideo('${encodedKey}')" style="cursor: pointer;">${obj.key}</h3>
|
||||||
<p>Size: ${sizeInMB} MB | Uploaded: ${uploadDate}</p>
|
<p>Size: ${sizeInMB} MB | Uploaded: ${uploadDate}</p>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button onclick="window.location.href='/watch/${encodedKey}'">Watch</button>
|
<button onclick="playVideo('${encodedKey}')">Watch</button>
|
||||||
<button onclick="copyLink('${obj.key}')">Copy Link</button>
|
<button onclick="copyLink('${obj.key}')">Copy Link</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -685,10 +711,38 @@ async function handlePublicGallery(bucket, kv, corsHeaders) {
|
||||||
${emptyState}
|
${emptyState}
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
function playVideo(encodedKey) {
|
||||||
|
window.location.href = '/watch/' + encodedKey;
|
||||||
|
}
|
||||||
|
|
||||||
function copyLink(filename) {
|
function copyLink(filename) {
|
||||||
const url = window.location.origin + '/' + filename;
|
const url = window.location.origin + '/' + filename;
|
||||||
navigator.clipboard.writeText(url).then(() => alert('Link copied!'));
|
navigator.clipboard.writeText(url).then(() => alert('Link copied!'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load thumbnails on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const videos = document.querySelectorAll('.video-thumbnail video');
|
||||||
|
videos.forEach(video => {
|
||||||
|
// Set up error handling for videos that fail to load
|
||||||
|
video.addEventListener('error', function() {
|
||||||
|
// Hide the video element and show a placeholder
|
||||||
|
const thumbnail = this.closest('.video-thumbnail');
|
||||||
|
const overlay = thumbnail.querySelector('.play-overlay');
|
||||||
|
this.style.display = 'none';
|
||||||
|
thumbnail.style.background = 'linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%)';
|
||||||
|
|
||||||
|
// Add a video icon placeholder
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 3rem; opacity: 0.3;';
|
||||||
|
icon.textContent = '🎬';
|
||||||
|
thumbnail.insertBefore(icon, overlay);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try to load the video
|
||||||
|
video.load();
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue