diff --git a/.env.example b/.env.example index 9aab16d..64361d3 100644 --- a/.env.example +++ b/.env.example @@ -37,5 +37,6 @@ MAX_PARALLEL_DOWNLOADS=2 # external services (all optional — backend degrades gracefully if unset) # ML_SERVICE_URL=http://ml:9000 # ACOUSTID_API_KEY= -MUSICBRAINZ_USER_AGENT=mcma-backend/0.1.0 ( https://github.com/your/repo ) +# Sent to MusicBrainz/AcoustID as part of the User-Agent (MCMA/ ( )). +# MUSICBRAINZ_OWNER_EMAIL=you@example.com # YOUTUBE_COOKIES_PATH=/data/cookies.txt diff --git a/app/core/config.py b/app/core/config.py index 3a1d675..729ae45 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -5,12 +5,25 @@ development). Access the cached singleton via :func:`get_settings`. """ from functools import lru_cache +from importlib.metadata import PackageNotFoundError, version from pathlib import Path from typing import Literal from pydantic import Field, SecretStr, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict +# App identity for outbound API calls (e.g. the MusicBrainz/AcoustID +# User-Agent). Name is fixed; version comes from the installed package. +APP_NAME = "MCMA" +_PROJECT_URL = "https://git.ollyhearn.ru/olly/mcma-backend" + + +def app_version() -> str: + try: + return version("mcma-backend") + except PackageNotFoundError: + return "0.0.0" + class Settings(BaseSettings): model_config = SettingsConfigDict( @@ -77,7 +90,12 @@ class Settings(BaseSettings): ml_service_url: str | None = None acoustid_api_key: SecretStr | None = None acoustid_api_url: str = "https://api.acoustid.org/v2/lookup" - musicbrainz_user_agent: str = "mcma-backend/0.1.0 ( https://github.com/your/repo )" + # MusicBrainz/AcoustID require a meaningful User-Agent identifying the + # application and a way to contact its maintainer (see + # https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting). Self-hosted + # deployments should set their own contact email; see + # ``musicbrainz_user_agent`` below for how it's used. + musicbrainz_owner_email: str | None = None youtube_cookies_path: Path | None = None # -- enrichment ------------------------------------------------------- @@ -96,6 +114,16 @@ class Settings(BaseSettings): def is_prod(self) -> bool: return self.environment == "prod" + @property + def musicbrainz_user_agent(self) -> str: + """User-Agent sent to MusicBrainz/AcoustID: ``MCMA/ ( )``. + + Falls back to the project URL if the deployment hasn't set + ``musicbrainz_owner_email``. + """ + contact = self.musicbrainz_owner_email or _PROJECT_URL + return f"{APP_NAME}/{app_version()} ( {contact} )" + @lru_cache def get_settings() -> Settings: diff --git a/app/main.py b/app/main.py index 9875ccf..a796230 100644 --- a/app/main.py +++ b/app/main.py @@ -10,7 +10,7 @@ from app.api.health import router as health_router from app.api.middleware import CorrelationIdMiddleware from app.api.rest import subsonic_router from app.api.v1 import api_v1_router -from app.core.config import get_settings +from app.core.config import app_version, get_settings from app.core.logging import configure_logging, get_logger from app.infrastructure.cache import close_redis from app.infrastructure.db import dispose_engine @@ -34,7 +34,7 @@ def create_app() -> FastAPI: app = FastAPI( title="mcma-backend", - version="0.1.0", + version=app_version(), summary="Self-hosted, offline-first music service.", lifespan=lifespan, )