Fix missing thumbnails: Add error handling to image/video elements with fallback to YouTube defaults
Some checks are pending
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / build (push) Blocked by required conditions

This commit is contained in:
KV-Tube Deployer 2026-03-24 23:23:11 +07:00
parent 16e146ce11
commit 86913861f2
9 changed files with 94 additions and 50 deletions

View file

@ -4,9 +4,9 @@
version: '3.8'
services:
kv-tube-app:
kv-tube:
image: git.khoavo.myds.me/vndangkhoa/kv-tube:v4.0.8
container_name: kv-tube-app
container_name: kv-tube
platform: linux/amd64
restart: unless-stopped
ports:

View file

@ -51,6 +51,10 @@ function VideoCard({ video, hideChannelAvatar }: { video: VideoData; hideChannel
style={{ objectFit: 'cover', backgroundColor: 'var(--yt-hover)' }}
className="videocard-thumb"
priority={false}
onError={(e) => {
e.target.onError = null; // Prevent infinite loop
e.target.src = 'https://i.ytimg.com/vi/default/hqdefault.jpg'; // Fallback to YouTube's default thumbnail
}}
/>
{video.duration && !video.is_mix && (
<div className="duration-badge" style={{ position: 'absolute', bottom: '8px', right: '8px' }}>

View file

@ -149,6 +149,10 @@ export default async function LibraryPage() {
alt={video.title}
className="videocard-thumb"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
onError={(e) => {
e.target.onError = null; // Prevent infinite loop
e.target.src = 'https://i.ytimg.com/vi/default/hqdefault.jpg'; // Fallback to YouTube's default thumbnail
}}
/>
{video.duration && (
<div className="duration-badge">{video.duration}</div>

View file

@ -123,6 +123,10 @@ export default async function SubscriptionsPage() {
src={video.thumbnail}
alt={video.title}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
onError={(e) => {
e.target.onError = null; // Prevent infinite loop
e.target.src = 'https://i.ytimg.com/vi/default/hqdefault.jpg'; // Fallback to YouTube's default thumbnail
}}
/>
{video.duration && (
<div className="duration-badge">{video.duration}</div>

View file

@ -87,6 +87,10 @@ async function SearchResults({ query }: { query: string }) {
alt={v.title}
style={{ width: '100%', height: '100%', objectFit: 'cover', backgroundColor: '#272727' }}
className="search-result-thumb"
onError={(e) => {
e.target.onError = null; // Prevent infinite loop
e.target.src = 'https://i.ytimg.com/vi/default/hqdefault.jpg'; // Fallback to YouTube's default thumbnail
}}
/>
{v.duration && (
<span className="duration-badge" style={{ position: 'absolute', bottom: '8px', right: '8px' }}>
@ -110,7 +114,15 @@ async function SearchResults({ query }: { query: string }) {
<div style={{ width: '24px', height: '24px', borderRadius: '50%', background: 'var(--yt-avatar-bg)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '11px', color: '#fff', overflow: 'hidden', fontWeight: 600 }}>
{v.avatar_url ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={v.avatar_url} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<img
src={v.avatar_url}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
onError={(e) => {
e.target.onError = null; // Prevent infinite loop
e.target.src = 'https://i.ytimg.com/img/channels/c_ip_m_default.jpg'; // Fallback to YouTube's default channel avatar
}}
/>
) : firstLetter}
</div>
<span style={{ fontSize: '12px', color: 'var(--yt-text-secondary)' }}>{v.uploader}</span>

View file

@ -121,6 +121,10 @@ export default function Comments({ videoId }: CommentsProps) {
fill
sizes="40px"
style={{ objectFit: 'cover' }}
onError={(e) => {
e.target.onError = null; // Prevent infinite loop
e.target.src = 'https://i.ytimg.com/img/channels/c_ip_m_default.jpg'; // Fallback to YouTube's default channel avatar
}}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', minWidth: 0, gap: '4px' }}>

View file

@ -99,6 +99,10 @@ export default function PlaylistPanel({ videos, currentVideoId, listId, title }:
fill
sizes="100px"
style={{ objectFit: 'cover' }}
onError={(e) => {
e.target.onError = null; // Prevent infinite loop
e.target.src = 'https://i.ytimg.com/vi/default/hqdefault.jpg'; // Fallback to YouTube's default thumbnail
}}
/>
{video.duration && (
<div style={{

View file

@ -49,7 +49,15 @@ export default async function RelatedVideos({ videoId, title, uploader }: { vide
return (
<Link key={video.id} href={`/watch?v=${video.id}`} className={`related-video-item fade-in-up ${staggerClass}`} style={{ opacity: 0 }}>
<div className="related-thumb-container">
<img src={video.thumbnail} alt={video.title} className="related-thumb-img" />
<img
src={video.thumbnail}
alt={video.title}
className="related-thumb-img"
onError={(e) => {
e.target.onError = null; // Prevent infinite loop
e.target.src = 'https://i.ytimg.com/vi/default/hqdefault.jpg'; // Fallback to YouTube's default thumbnail
}}
/>
{video.duration && (
<div className="duration-badge">
{video.duration}

View file

@ -568,6 +568,10 @@ export default function VideoPlayer({ videoId, title }: VideoPlayerProps) {
x5-video-player-fullscreen="true"
preload="auto"
poster={`https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`}
onError={(e) => {
e.target.onError = null; // Prevent infinite loop
e.target.poster = 'https://i.ytimg.com/vi/default/hqdefault.jpg'; // Fallback to YouTube's default thumbnail
}}
/>
<audio ref={audioRef} style={{ display: 'none' }} />