Files
romm/backend/utils/nginx.py
zurdi bd38eddd11 feat: add multi-file ROM fixture for testing and enhance soundtrack player functionality
- Introduced a new fixture `multi_file_rom` in `conftest.py` to create a ROM with multiple files for testing purposes.
- Updated `test_manual.py` and `test_soundtrack.py` to utilize the new fixture for testing manual uploads and soundtrack functionalities.
- Enhanced the audio tag extraction logic in `audio_tags.py` to handle oversized audio files and added a function to check allowed audio file extensions.
- Modified `nginx.py` to support inline file serving for audio files, allowing for better streaming capabilities.
- Improved error handling in the Vue components for soundtrack management, including user feedback for playback errors and metadata loading issues.
- Refactored the soundtrack player store to use local storage for volume and mute settings, simplifying state management.
- Added new localization strings for soundtrack player actions and error messages in both English (US and GB) locale files.
2026-04-20 08:33:35 +00:00

85 lines
2.7 KiB
Python

import dataclasses
from collections.abc import Collection
from typing import Any
from urllib.parse import quote
from anyio import Path
from fastapi.responses import Response
@dataclasses.dataclass(frozen=True)
class ZipContentLine:
"""Dataclass for lines returned in the response body, for usage with the `mod_zip` module.
Reference:
https://github.com/evanmiller/mod_zip?tab=readme-ov-file#usage
"""
crc32: str | None
size_bytes: int
encoded_location: str
filename: str
def __str__(self) -> str:
crc32 = self.crc32 or "-"
return f"{crc32} {self.size_bytes} {self.encoded_location} {self.filename}"
class ZipResponse(Response):
"""Response class for returning a ZIP archive with multiple files, using the `mod_zip` module."""
def __init__(
self,
*,
content_lines: Collection[ZipContentLine],
filename: str,
**kwargs: Any,
):
if kwargs.get("content"):
raise ValueError(
"Argument 'content' must not be provided, as it is generated from 'content_lines'"
)
kwargs["content"] = "\n".join(str(line) for line in content_lines)
kwargs.setdefault("headers", {}).update(
{
"Content-Disposition": f"attachment; filename*=UTF-8''{filename}; filename=\"{filename}\"",
"X-Archive-Files": "zip",
"X-Archive-Charset": "UTF-8",
}
)
super().__init__(**kwargs)
class FileRedirectResponse(Response):
"""Response class for serving a file download by using the X-Accel-Redirect header."""
def __init__(
self,
*,
download_path: Path,
filename: str | None = None,
disposition: str = "attachment",
**kwargs: Any,
):
"""
Arguments:
- download_path: Path to the file to be served.
- filename: Name of the file to be served. If not provided, the file name from the
download_path is used.
- disposition: "attachment" (default) forces a download; "inline" lets the
browser render/stream the file (required for <audio> to issue Range
requests and seek). Nginx itself still handles the Range parsing.
"""
media_type = kwargs.pop("media_type", "application/octet-stream")
filename = filename or download_path.name
kwargs.setdefault("headers", {}).update(
{
"Content-Disposition": f"{disposition}; filename*=UTF-8''{quote(filename)}; filename=\"{quote(filename)}\"",
"X-Accel-Redirect": quote(str(download_path)),
}
)
super().__init__(media_type=media_type, **kwargs)