- Fix CORS: set allow_credentials=False (token auth, not cookies) - Add threading.Lock for position cache thread safety - Add shutdown_executor() for clean ThreadPoolExecutor cleanup - Dedicated ThreadPoolExecutors for script/callback execution - Fix Mutagen file handle leaks with try/finally close - Reduce idle WebSocket polling (0.5s → 2.0s when no clients) - Add :focus-visible styles for playback control buttons - Add aria-label to icon-only header buttons - Dynamic album art alt text for screen readers - Persist MDI icon cache to localStorage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
203 lines
7.1 KiB
Python
203 lines
7.1 KiB
Python
"""Metadata extraction service for media files."""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MetadataService:
|
|
"""Service for extracting metadata from media files."""
|
|
|
|
@staticmethod
|
|
def extract_audio_metadata(file_path: Path) -> dict:
|
|
"""Extract metadata from an audio file.
|
|
|
|
Args:
|
|
file_path: Path to the audio file.
|
|
|
|
Returns:
|
|
Dictionary with audio metadata.
|
|
"""
|
|
try:
|
|
import mutagen
|
|
from mutagen import File as MutagenFile
|
|
|
|
audio = MutagenFile(str(file_path), easy=True)
|
|
if audio is None:
|
|
return {"error": "Unable to read audio file"}
|
|
|
|
try:
|
|
metadata = {
|
|
"type": "audio",
|
|
"filename": file_path.name,
|
|
"path": str(file_path),
|
|
}
|
|
|
|
# Extract duration
|
|
if hasattr(audio.info, "length"):
|
|
metadata["duration"] = round(audio.info.length, 2)
|
|
|
|
# Extract bitrate
|
|
if hasattr(audio.info, "bitrate"):
|
|
metadata["bitrate"] = audio.info.bitrate
|
|
|
|
# Extract sample rate
|
|
if hasattr(audio.info, "sample_rate"):
|
|
metadata["sample_rate"] = audio.info.sample_rate
|
|
elif hasattr(audio.info, "samplerate"):
|
|
metadata["sample_rate"] = audio.info.samplerate
|
|
|
|
# Extract channels
|
|
if hasattr(audio.info, "channels"):
|
|
metadata["channels"] = audio.info.channels
|
|
|
|
# Extract tags (use easy=True for consistent tag names)
|
|
if audio is not None and hasattr(audio, "tags") and audio.tags:
|
|
# Easy tags provide lists, so we take the first item
|
|
tags = audio.tags
|
|
|
|
if "title" in tags:
|
|
metadata["title"] = tags["title"][0] if isinstance(tags["title"], list) else tags["title"]
|
|
|
|
if "artist" in tags:
|
|
metadata["artist"] = tags["artist"][0] if isinstance(tags["artist"], list) else tags["artist"]
|
|
|
|
if "album" in tags:
|
|
metadata["album"] = tags["album"][0] if isinstance(tags["album"], list) else tags["album"]
|
|
|
|
if "albumartist" in tags:
|
|
metadata["album_artist"] = tags["albumartist"][0] if isinstance(tags["albumartist"], list) else tags["albumartist"]
|
|
|
|
if "date" in tags:
|
|
metadata["date"] = tags["date"][0] if isinstance(tags["date"], list) else tags["date"]
|
|
|
|
if "genre" in tags:
|
|
metadata["genre"] = tags["genre"][0] if isinstance(tags["genre"], list) else tags["genre"]
|
|
|
|
if "tracknumber" in tags:
|
|
metadata["track_number"] = tags["tracknumber"][0] if isinstance(tags["tracknumber"], list) else tags["tracknumber"]
|
|
|
|
# If no title tag, use filename
|
|
if "title" not in metadata:
|
|
metadata["title"] = file_path.stem
|
|
|
|
return metadata
|
|
finally:
|
|
if hasattr(audio, 'close'):
|
|
audio.close()
|
|
|
|
except ImportError:
|
|
logger.error("mutagen library not installed, cannot extract metadata")
|
|
return {"error": "mutagen library not installed"}
|
|
except Exception as e:
|
|
logger.error(f"Error extracting audio metadata from {file_path}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"filename": file_path.name,
|
|
"title": file_path.stem,
|
|
}
|
|
|
|
@staticmethod
|
|
def extract_video_metadata(file_path: Path) -> dict:
|
|
"""Extract basic metadata from a video file.
|
|
|
|
Args:
|
|
file_path: Path to the video file.
|
|
|
|
Returns:
|
|
Dictionary with video metadata.
|
|
"""
|
|
try:
|
|
import mutagen
|
|
from mutagen import File as MutagenFile
|
|
|
|
video = MutagenFile(str(file_path))
|
|
if video is None:
|
|
return {
|
|
"type": "video",
|
|
"filename": file_path.name,
|
|
"title": file_path.stem,
|
|
}
|
|
|
|
try:
|
|
metadata = {
|
|
"type": "video",
|
|
"filename": file_path.name,
|
|
"path": str(file_path),
|
|
}
|
|
|
|
# Extract duration
|
|
if hasattr(video.info, "length"):
|
|
metadata["duration"] = round(video.info.length, 2)
|
|
|
|
# Extract bitrate
|
|
if hasattr(video.info, "bitrate"):
|
|
metadata["bitrate"] = video.info.bitrate
|
|
|
|
# Extract video-specific properties if available
|
|
if hasattr(video.info, "width"):
|
|
metadata["width"] = video.info.width
|
|
|
|
if hasattr(video.info, "height"):
|
|
metadata["height"] = video.info.height
|
|
|
|
# Try to extract title from tags
|
|
if hasattr(video, "tags") and video.tags:
|
|
tags = video.tags
|
|
if hasattr(tags, "get"):
|
|
title = tags.get("title") or tags.get("TITLE") or tags.get("\xa9nam")
|
|
if title:
|
|
metadata["title"] = title[0] if isinstance(title, list) else str(title)
|
|
|
|
# If no title tag, use filename
|
|
if "title" not in metadata:
|
|
metadata["title"] = file_path.stem
|
|
|
|
return metadata
|
|
finally:
|
|
if hasattr(video, 'close'):
|
|
video.close()
|
|
|
|
except ImportError:
|
|
logger.error("mutagen library not installed, cannot extract metadata")
|
|
return {
|
|
"error": "mutagen library not installed",
|
|
"type": "video",
|
|
"filename": file_path.name,
|
|
"title": file_path.stem,
|
|
}
|
|
except Exception as e:
|
|
logger.debug(f"Error extracting video metadata from {file_path}: {e}")
|
|
# Return basic metadata
|
|
return {
|
|
"type": "video",
|
|
"filename": file_path.name,
|
|
"title": file_path.stem,
|
|
}
|
|
|
|
@staticmethod
|
|
def extract_metadata(file_path: Path) -> dict:
|
|
"""Extract metadata from a media file (auto-detect type).
|
|
|
|
Args:
|
|
file_path: Path to the media file.
|
|
|
|
Returns:
|
|
Dictionary with media metadata.
|
|
"""
|
|
from .browser_service import AUDIO_EXTENSIONS, VIDEO_EXTENSIONS
|
|
|
|
suffix = file_path.suffix.lower()
|
|
|
|
if suffix in AUDIO_EXTENSIONS:
|
|
return MetadataService.extract_audio_metadata(file_path)
|
|
elif suffix in VIDEO_EXTENSIONS:
|
|
return MetadataService.extract_video_metadata(file_path)
|
|
else:
|
|
return {
|
|
"error": "Unsupported file type",
|
|
"filename": file_path.name,
|
|
}
|