Files
media-player-server/media_server/routes/links.py
alexei.dolgolyov adf2d936da Consolidate tabs, Quick Access links, mini player nav, link descriptions
- Merge Scripts/Callbacks/Links tabs into single Settings tab with collapsible sections
- Rename Actions tab to Quick Access showing both scripts and configured links
- Add prev/next buttons to mini (secondary) player
- Add optional description field to links (backend + frontend)
- Add CSS chevron indicators on collapsible settings sections
- Persist section collapse/expand state in localStorage
- Fix race condition in Quick Access rendering with generation counter
- Order settings sections: Scripts, Links, Callbacks

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 15:08:09 +03:00

189 lines
5.2 KiB
Python

"""Header quick links management API endpoints."""
import logging
import re
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from ..auth import verify_token
from ..config import LinkConfig, settings
from ..config_manager import config_manager
from ..services.websocket_manager import ws_manager
router = APIRouter(prefix="/api/links", tags=["links"])
logger = logging.getLogger(__name__)
class LinkInfo(BaseModel):
"""Information about a configured link."""
name: str
url: str
icon: str
label: str
description: str
class LinkCreateRequest(BaseModel):
"""Request model for creating or updating a link."""
url: str = Field(..., description="URL to open", min_length=1)
icon: str = Field(default="mdi:link", description="MDI icon name (e.g., 'mdi:led-strip-variant')")
label: str = Field(default="", description="Tooltip text")
description: str = Field(default="", description="Optional description")
def _validate_link_name(name: str) -> None:
"""Validate link name.
Args:
name: Link name to validate.
Raises:
HTTPException: If name is invalid.
"""
if not re.match(r'^[a-zA-Z0-9_]+$', name):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Link name must contain only letters, numbers, and underscores",
)
if len(name) > 64:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Link name must be 64 characters or less",
)
@router.get("/list")
async def list_links(_: str = Depends(verify_token)) -> list[LinkInfo]:
"""List all configured links.
Returns:
List of configured links.
"""
return [
LinkInfo(
name=name,
url=config.url,
icon=config.icon,
label=config.label,
description=config.description,
)
for name, config in settings.links.items()
]
@router.post("/create/{link_name}")
async def create_link(
link_name: str,
request: LinkCreateRequest,
_: str = Depends(verify_token),
) -> dict[str, Any]:
"""Create a new link.
Args:
link_name: Link name (alphanumeric and underscores only).
request: Link configuration.
Returns:
Success response with link name.
"""
_validate_link_name(link_name)
if link_name in settings.links:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Link '{link_name}' already exists. Use PUT /api/links/update/{link_name} to update it.",
)
link_config = LinkConfig(**request.model_dump())
try:
config_manager.add_link(link_name, link_config)
except Exception as e:
logger.error(f"Failed to add link '{link_name}': {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to add link: {str(e)}",
)
await ws_manager.broadcast_links_changed()
logger.info(f"Link '{link_name}' created successfully")
return {"success": True, "link": link_name}
@router.put("/update/{link_name}")
async def update_link(
link_name: str,
request: LinkCreateRequest,
_: str = Depends(verify_token),
) -> dict[str, Any]:
"""Update an existing link.
Args:
link_name: Link name.
request: Updated link configuration.
Returns:
Success response with link name.
"""
_validate_link_name(link_name)
if link_name not in settings.links:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Link '{link_name}' not found. Use POST /api/links/create/{link_name} to create it.",
)
link_config = LinkConfig(**request.model_dump())
try:
config_manager.update_link(link_name, link_config)
except Exception as e:
logger.error(f"Failed to update link '{link_name}': {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to update link: {str(e)}",
)
await ws_manager.broadcast_links_changed()
logger.info(f"Link '{link_name}' updated successfully")
return {"success": True, "link": link_name}
@router.delete("/delete/{link_name}")
async def delete_link(
link_name: str,
_: str = Depends(verify_token),
) -> dict[str, Any]:
"""Delete a link.
Args:
link_name: Link name.
Returns:
Success response with link name.
"""
_validate_link_name(link_name)
if link_name not in settings.links:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Link '{link_name}' not found",
)
try:
config_manager.delete_link(link_name)
except Exception as e:
logger.error(f"Failed to delete link '{link_name}': {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to delete link: {str(e)}",
)
await ws_manager.broadcast_links_changed()
logger.info(f"Link '{link_name}' deleted successfully")
return {"success": True, "link": link_name}