Tabbed UI, browse caching, and bottom mini player
- Convert stacked sections to tabbed interface (Player, Browser, Actions, Scripts, Callbacks) with localStorage persistence - Add in-memory directory listing cache (5-min TTL) with nocache bypass for refresh - Defer stat()/duration calls to paginated items only for faster browse - Move mini player from top to bottom with footer padding fix - Always show scrollbar to prevent layout shift between tabs - Add tab localization keys (en/ru) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,17 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from ..config import settings
|
||||
|
||||
# Directory listing cache: {resolved_path_str: (timestamp, all_items)}
|
||||
_dir_cache: dict[str, tuple[float, list[dict]]] = {}
|
||||
DIR_CACHE_TTL = 300 # 5 minutes
|
||||
|
||||
try:
|
||||
from mutagen import File as MutagenFile
|
||||
HAS_MUTAGEN = True
|
||||
@@ -141,6 +146,7 @@ class BrowserService:
|
||||
path: str = "",
|
||||
offset: int = 0,
|
||||
limit: int = 100,
|
||||
nocache: bool = False,
|
||||
) -> dict:
|
||||
"""Browse a directory and return items with metadata.
|
||||
|
||||
@@ -189,49 +195,66 @@ class BrowserService:
|
||||
parent_relative = full_path.parent.relative_to(base_path)
|
||||
parent_path = "/" + str(parent_relative).replace("\\", "/") if str(parent_relative) != "." else "/"
|
||||
|
||||
# List directory contents
|
||||
# List directory contents (with caching)
|
||||
try:
|
||||
all_items = []
|
||||
for item in full_path.iterdir():
|
||||
# Skip hidden files (starting with .)
|
||||
if item.name.startswith("."):
|
||||
continue
|
||||
cache_key = str(full_path)
|
||||
now = time.monotonic()
|
||||
|
||||
# Get file type
|
||||
file_type = BrowserService.get_file_type(item)
|
||||
# Check cache
|
||||
if not nocache and cache_key in _dir_cache:
|
||||
cached_time, cached_items = _dir_cache[cache_key]
|
||||
if now - cached_time < DIR_CACHE_TTL:
|
||||
all_items = cached_items
|
||||
else:
|
||||
del _dir_cache[cache_key]
|
||||
all_items = None
|
||||
else:
|
||||
all_items = None
|
||||
|
||||
# Skip non-media files (but include folders)
|
||||
if file_type == "other" and not item.is_dir():
|
||||
continue
|
||||
# Enumerate directory if not cached
|
||||
if all_items is None:
|
||||
all_items = []
|
||||
for item in full_path.iterdir():
|
||||
if item.name.startswith("."):
|
||||
continue
|
||||
|
||||
# Get file info
|
||||
try:
|
||||
stat = item.stat()
|
||||
size = stat.st_size if item.is_file() else None
|
||||
modified = datetime.fromtimestamp(stat.st_mtime).isoformat()
|
||||
except (OSError, PermissionError):
|
||||
size = None
|
||||
modified = None
|
||||
is_dir = item.is_dir()
|
||||
if is_dir:
|
||||
file_type = "folder"
|
||||
else:
|
||||
suffix = item.suffix.lower()
|
||||
if suffix in AUDIO_EXTENSIONS:
|
||||
file_type = "audio"
|
||||
elif suffix in VIDEO_EXTENSIONS:
|
||||
file_type = "video"
|
||||
else:
|
||||
continue
|
||||
|
||||
all_items.append({
|
||||
"name": item.name,
|
||||
"type": file_type,
|
||||
"size": size,
|
||||
"modified": modified,
|
||||
"is_media": file_type in ("audio", "video"),
|
||||
})
|
||||
all_items.append({
|
||||
"name": item.name,
|
||||
"type": file_type,
|
||||
"is_media": file_type in ("audio", "video"),
|
||||
})
|
||||
|
||||
# Sort: folders first, then by name
|
||||
all_items.sort(key=lambda x: (x["type"] != "folder", x["name"].lower()))
|
||||
all_items.sort(key=lambda x: (x["type"] != "folder", x["name"].lower()))
|
||||
_dir_cache[cache_key] = (now, all_items)
|
||||
|
||||
# Apply pagination
|
||||
total = len(all_items)
|
||||
items = all_items[offset:offset + limit]
|
||||
|
||||
# Extract duration for media files in the current page
|
||||
# Fetch stat + duration only for items on the current page
|
||||
for item in items:
|
||||
item_path = full_path / item["name"]
|
||||
try:
|
||||
stat = item_path.stat()
|
||||
item["size"] = stat.st_size if item["type"] != "folder" else None
|
||||
item["modified"] = datetime.fromtimestamp(stat.st_mtime).isoformat()
|
||||
except (OSError, PermissionError):
|
||||
item["size"] = None
|
||||
item["modified"] = None
|
||||
|
||||
if item["is_media"]:
|
||||
item_path = full_path / item["name"]
|
||||
item["duration"] = BrowserService.get_duration(item_path)
|
||||
else:
|
||||
item["duration"] = None
|
||||
|
||||
Reference in New Issue
Block a user