Add Play All, home navigation, and UI improvements
- Add Play All button with M3U playlist generation (local temp file with absolute paths) - Replace folder combobox with root folder cards and home icon breadcrumb - Fix compact grid card sizing (64x64 thumbnails, align-items: start) - Add loading spinner when browsing folders - Cache browse items to avoid re-fetching on view mode switch - Remove unused browser-controls CSS - Add localization keys for Play All and Home (en/ru) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from urllib.parse import unquote
|
||||
@@ -48,6 +49,13 @@ class PlayRequest(BaseModel):
|
||||
path: str = Field(..., description="Full path to the media file")
|
||||
|
||||
|
||||
class PlayFolderRequest(BaseModel):
|
||||
"""Request model for playing all media files in a folder."""
|
||||
|
||||
folder_id: str = Field(..., description="Media folder ID")
|
||||
path: str = Field(default="", description="Path relative to folder root")
|
||||
|
||||
|
||||
# Folder Management Endpoints
|
||||
@router.get("/folders")
|
||||
async def list_folders(_: str = Depends(verify_token)):
|
||||
@@ -423,6 +431,89 @@ async def play_file(
|
||||
raise HTTPException(status_code=500, detail="Failed to play file")
|
||||
|
||||
|
||||
# Play Folder Endpoint (M3U playlist)
|
||||
@router.post("/play-folder")
|
||||
async def play_folder(
|
||||
request: PlayFolderRequest,
|
||||
_: str = Depends(verify_token),
|
||||
):
|
||||
"""Play all media files in a folder by generating an M3U playlist.
|
||||
|
||||
Args:
|
||||
request: Play folder request with folder_id and path.
|
||||
|
||||
Returns:
|
||||
Success message with file count.
|
||||
|
||||
Raises:
|
||||
HTTPException: If folder not found or playback fails.
|
||||
"""
|
||||
try:
|
||||
decoded_path = unquote(request.path)
|
||||
full_path = BrowserService.validate_path(request.folder_id, decoded_path)
|
||||
|
||||
if not full_path.is_dir():
|
||||
raise HTTPException(status_code=400, detail="Path is not a directory")
|
||||
|
||||
# Collect all media files sorted by name
|
||||
media_files = sorted(
|
||||
[f for f in full_path.iterdir() if f.is_file() and BrowserService.is_media_file(f)],
|
||||
key=lambda f: f.name.lower(),
|
||||
)
|
||||
|
||||
if not media_files:
|
||||
raise HTTPException(status_code=404, detail="No media files found in this folder")
|
||||
|
||||
# Generate M3U playlist with absolute paths and EXTINF entries
|
||||
# Written to local temp dir to avoid extra SMB file handle on network shares
|
||||
# Uses utf-8-sig (BOM) so players detect encoding properly
|
||||
m3u_content = "#EXTM3U\r\n"
|
||||
for f in media_files:
|
||||
m3u_content += f"#EXTINF:-1,{f.stem}\r\n"
|
||||
m3u_content += f"{f}\r\n"
|
||||
|
||||
playlist_path = Path(tempfile.gettempdir()) / ".media_server_playlist.m3u"
|
||||
playlist_path.write_text(m3u_content, encoding="utf-8-sig")
|
||||
|
||||
# Open playlist with default player
|
||||
controller = get_media_controller()
|
||||
success = await controller.open_file(playlist_path)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to open playlist")
|
||||
|
||||
# Wait for media player to start
|
||||
await asyncio.sleep(1.5)
|
||||
|
||||
# Broadcast status update
|
||||
try:
|
||||
status = await controller.get_status()
|
||||
status_dict = status.model_dump()
|
||||
await ws_manager.broadcast({
|
||||
"type": "status",
|
||||
"data": status_dict
|
||||
})
|
||||
logger.info(f"Broadcasted status after opening playlist with {len(media_files)} files")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to broadcast status after opening playlist: {e}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Playing {len(media_files)} files",
|
||||
"count": len(media_files),
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except FileNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"Error playing folder: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to play folder")
|
||||
|
||||
|
||||
# Download Endpoint
|
||||
@router.get("/download")
|
||||
async def download_file(
|
||||
|
||||
Reference in New Issue
Block a user