diff --git a/media_server/services/thumbnail_service.py b/media_server/services/thumbnail_service.py
index dc13808..cb6c3f2 100644
--- a/media_server/services/thumbnail_service.py
+++ b/media_server/services/thumbnail_service.py
@@ -51,6 +51,9 @@ class ThumbnailService:
absolute_path = str(file_path.resolve())
return hashlib.sha256(absolute_path.encode()).hexdigest()
+ # Sentinel value indicating "no thumbnail available" is cached
+ NO_THUMBNAIL = b""
+
@staticmethod
def get_cached_thumbnail(file_path: Path, size: str) -> Optional[bytes]:
"""Get cached thumbnail if valid.
@@ -60,11 +63,24 @@ class ThumbnailService:
size: Thumbnail size ("small" or "medium").
Returns:
- Thumbnail bytes if cached and valid, None otherwise.
+ Thumbnail bytes if cached, empty bytes if cached as "no thumbnail",
+ None if not cached.
"""
cache_dir = ThumbnailService.get_cache_dir()
cache_key = ThumbnailService.get_cache_key(file_path)
- cache_path = cache_dir / cache_key / f"{size}.jpg"
+ cache_folder = cache_dir / cache_key
+ cache_path = cache_folder / f"{size}.jpg"
+ no_thumb_path = cache_folder / ".no_thumbnail"
+
+ # Check negative cache first (no thumbnail available)
+ if no_thumb_path.exists():
+ try:
+ file_mtime = file_path.stat().st_mtime
+ cache_mtime = no_thumb_path.stat().st_mtime
+ if file_mtime <= cache_mtime:
+ return ThumbnailService.NO_THUMBNAIL
+ except (OSError, PermissionError):
+ pass
if not cache_path.exists():
return None
@@ -109,6 +125,20 @@ class ThumbnailService:
except (OSError, PermissionError) as e:
logger.error(f"Error caching thumbnail: {e}")
+ @staticmethod
+ def cache_no_thumbnail(file_path: Path) -> None:
+ """Cache that a file has no thumbnail (negative cache)."""
+ cache_dir = ThumbnailService.get_cache_dir()
+ cache_key = ThumbnailService.get_cache_key(file_path)
+ cache_folder = cache_dir / cache_key
+ cache_folder.mkdir(parents=True, exist_ok=True)
+
+ try:
+ (cache_folder / ".no_thumbnail").touch()
+ logger.debug(f"Cached no-thumbnail for {file_path.name}")
+ except (OSError, PermissionError) as e:
+ logger.error(f"Error caching no-thumbnail marker: {e}")
+
@staticmethod
def generate_audio_thumbnail(file_path: Path, size: str) -> Optional[bytes]:
"""Generate thumbnail from audio file (extract album art).
@@ -277,10 +307,10 @@ class ThumbnailService:
if size not in THUMBNAIL_SIZES:
size = "medium"
- # Check cache first
+ # Check cache first (returns bytes, empty bytes for negative cache, or None)
cached = ThumbnailService.get_cached_thumbnail(file_path, size)
- if cached:
- return cached
+ if cached is not None:
+ return cached if cached else None
# Generate thumbnail based on file type
suffix = file_path.suffix.lower()
@@ -299,9 +329,11 @@ class ThumbnailService:
# Video files - already async
thumbnail_data = await ThumbnailService.generate_video_thumbnail(file_path, size)
- # Cache if generated successfully
+ # Cache result (positive or negative)
if thumbnail_data:
ThumbnailService.cache_thumbnail(file_path, size, thumbnail_data)
+ else:
+ ThumbnailService.cache_no_thumbnail(file_path)
return thumbnail_data
diff --git a/media_server/static/css/styles.css b/media_server/static/css/styles.css
index fabfd81..2423522 100644
--- a/media_server/static/css/styles.css
+++ b/media_server/static/css/styles.css
@@ -1214,8 +1214,8 @@
.view-toggle-btn:hover {
color: var(--text-primary);
- background: var(--border);
- transform: none;
+ background: transparent !important;
+ transform: none !important;
}
.view-toggle-btn.active {
@@ -1223,6 +1223,10 @@
background: var(--bg-primary);
}
+.browser-refresh-btn {
+ margin-left: 0.35rem;
+}
+
.view-toggle-btn svg {
fill: currentColor;
}
diff --git a/media_server/static/index.html b/media_server/static/index.html
index b77e79e..7d36213 100644
--- a/media_server/static/index.html
+++ b/media_server/static/index.html
@@ -164,6 +164,9 @@
+