Files
SpotifyRecAlg/swingmusic/services/audio_quality_store.py
T
Tomas Dvorak 6e8fedf534 first commit
2026-04-13 17:46:58 +02:00

264 lines
7.9 KiB
Python

"""Lightweight persistence and helpers for audio quality preferences."""
from __future__ import annotations
import json
from copy import deepcopy
from typing import Any
from sqlalchemy import text
from swingmusic.db.engine import DbEngine
DEFAULT_AUDIO_SETTINGS: dict[str, Any] = {
"streaming_quality": "high",
"adaptive_quality": True,
"network_aware_quality": True,
"device_specific_quality": True,
"download_format": "flac",
"download_bitrate": None,
"download_sample_rate": "44.1kHz",
"download_bit_depth": "16bit",
"enable_loudness_normalization": True,
"target_loudness": -14.0,
"enable_adaptive_eq": True,
"enable_spatial_audio_processing": False,
"spatial_audio_format": "stereo",
"enable_crossfade": False,
"crossfade_duration": 2.0,
"enable_gapless_playback": True,
"enable_replaygain": True,
"prioritize_fidelity": True,
"prioritize_file_size": False,
"prioritize_compatibility": False,
"custom_ffmpeg_params": {},
"enable_experimental_codecs": False,
"cache_transcoded_files": True,
}
AUDIO_PRESETS: dict[str, dict[str, Any]] = {
"audiophile": {
"streaming_quality": "lossless",
"download_format": "flac",
"download_sample_rate": "96kHz",
"download_bit_depth": "24bit",
"prioritize_fidelity": True,
},
"portable": {
"streaming_quality": "high",
"download_format": "aac_256",
"adaptive_quality": True,
"network_aware_quality": True,
},
"data_saver": {
"streaming_quality": "data_saver",
"download_format": "mp3_128",
"prioritize_file_size": True,
"prioritize_fidelity": False,
},
"studio": {
"streaming_quality": "lossless",
"download_format": "wav",
"download_sample_rate": "192kHz",
"download_bit_depth": "32bit",
"prioritize_fidelity": True,
},
"gaming": {
"streaming_quality": "medium",
"download_format": "mp3_256",
"enable_crossfade": False,
"enable_gapless_playback": True,
},
"podcast": {
"streaming_quality": "medium",
"download_format": "aac_128",
"target_loudness": -16.0,
"enable_adaptive_eq": True,
},
}
SUPPORTED_FORMATS = [
"flac",
"alac",
"wav",
"mp3_320",
"mp3_256",
"mp3_192",
"mp3_128",
"aac_256",
"aac_192",
"aac_128",
"ogg_vorbis",
"ogg_opus",
]
class AudioQualityStore:
def __init__(self):
self._ensure_schema()
def _ensure_schema(self):
with DbEngine.manager(commit=True) as session:
session.execute(
text(
"""
CREATE TABLE IF NOT EXISTS audio_quality_settings (
user_id INTEGER PRIMARY KEY,
settings_json TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"""
)
)
def _normalize_settings(self, incoming: dict[str, Any]) -> dict[str, Any]:
settings = deepcopy(DEFAULT_AUDIO_SETTINGS)
for key, value in incoming.items():
if key not in settings:
continue
settings[key] = value
if settings["streaming_quality"] not in {
"lossless",
"high",
"medium",
"low",
"data_saver",
}:
settings["streaming_quality"] = DEFAULT_AUDIO_SETTINGS["streaming_quality"]
if not isinstance(settings["custom_ffmpeg_params"], dict):
settings["custom_ffmpeg_params"] = {}
return settings
def get_settings(self, user_id: int) -> dict[str, Any]:
with DbEngine.manager() as session:
row = (
session.execute(
text(
"""
SELECT settings_json
FROM audio_quality_settings
WHERE user_id = :user_id
"""
),
{"user_id": int(user_id)},
)
.mappings()
.first()
)
if not row:
return deepcopy(DEFAULT_AUDIO_SETTINGS)
try:
raw = json.loads(row["settings_json"])
if not isinstance(raw, dict):
return deepcopy(DEFAULT_AUDIO_SETTINGS)
return self._normalize_settings(raw)
except json.JSONDecodeError:
return deepcopy(DEFAULT_AUDIO_SETTINGS)
def save_settings(self, user_id: int, settings: dict[str, Any]) -> dict[str, Any]:
normalized = self._normalize_settings(settings)
with DbEngine.manager(commit=True) as session:
session.execute(
text(
"""
INSERT INTO audio_quality_settings (user_id, settings_json, updated_at)
VALUES (:user_id, :settings_json, CURRENT_TIMESTAMP)
ON CONFLICT(user_id) DO UPDATE SET
settings_json = excluded.settings_json,
updated_at = CURRENT_TIMESTAMP
"""
),
{
"user_id": int(user_id),
"settings_json": json.dumps(normalized),
},
)
return normalized
def update_settings(self, user_id: int, patch: dict[str, Any]) -> dict[str, Any]:
current = self.get_settings(user_id)
current.update(patch)
return self.save_settings(user_id, current)
def apply_preset(
self, user_id: int, preset_name: str
) -> tuple[dict[str, Any] | None, bool]:
preset = AUDIO_PRESETS.get(preset_name)
if preset is None:
return None, False
settings = self.update_settings(user_id, preset)
return settings, True
def get_presets(self) -> list[dict[str, Any]]:
return [{"key": key, "settings": value} for key, value in AUDIO_PRESETS.items()]
def get_supported_formats(self) -> list[str]:
return SUPPORTED_FORMATS[:]
def get_network_status(self) -> dict[str, Any]:
# Keep deterministic and cheap. A dedicated bandwidth probe can be added later.
return {
"speed": 0,
"quality": "unknown",
"metered": False,
"latency_ms": None,
}
def get_device_info(self, user_agent: str) -> dict[str, Any]:
ua = (user_agent or "").lower()
if any(token in ua for token in ("iphone", "android", "mobile")):
device_type = "mobile"
elif any(token in ua for token in ("ipad", "tablet")):
device_type = "tablet"
else:
device_type = "desktop"
if "windows" in ua:
os_name = "windows"
elif "mac os" in ua or "macintosh" in ua:
os_name = "macos"
elif "linux" in ua:
os_name = "linux"
elif "android" in ua:
os_name = "android"
elif "iphone" in ua or "ipad" in ua:
os_name = "ios"
else:
os_name = "unknown"
return {
"type": device_type,
"os": os_name,
"supports_lossless": device_type in {"desktop", "tablet"},
"supports_spatial_audio": device_type != "unknown",
}
def get_optimal_streaming_quality(
self, user_id: int, context: dict[str, Any] | None = None
) -> str:
settings = self.get_settings(user_id)
preferred = settings.get("streaming_quality", "high")
context = context or {}
battery_low = bool(context.get("battery_low"))
network_quality = str(context.get("network_quality") or "")
if battery_low and preferred == "lossless":
return "high"
if network_quality in {"poor", "slow"}:
return "medium" if preferred in {"lossless", "high"} else preferred
return preferred
audio_quality_store = AudioQualityStore()