mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-03 20:13:03 +00:00
264 lines
7.9 KiB
Python
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()
|