mirror of
https://github.com/Dvorinka/SpotifyRecAlg.git
synced 2026-06-04 20:43:04 +00:00
first commit
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user