Add media browser feature with UI improvements
- Refactored index.html: Split into separate HTML (309 lines), CSS (908 lines), and JS (1,286 lines) files - Implemented media browser with folder configuration, recursive navigation, and thumbnail display - Added metadata extraction using mutagen library (title, artist, album, duration, bitrate, codec) - Implemented thumbnail generation and caching with SHA256 hash-based keys and LRU eviction - Added platform-specific file playback (os.startfile on Windows, xdg-open on Linux, open on macOS) - Implemented path validation security to prevent directory traversal attacks - Added smooth thumbnail loading with fade-in animation and loading spinner - Added i18n support for browser (English and Russian) - Updated dependencies: mutagen>=1.47.0, pillow>=10.0.0 - Added comprehensive media browser documentation to README Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
194
media_server/services/metadata_service.py
Normal file
194
media_server/services/metadata_service.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""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"}
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user