Files
musicdownload/core/config.py
T
luciano ed1059bf8c feat: sidebar scroll + log persistente (v1.11.1)
- Sidebar diventata troppo lunga (14+ tab): overflow-y auto sulla nav,
  brand e footer restano sticky. Scrollbar 6px tema-coerente
- Nuovo core/applog.py: TimedRotatingFileHandler daily con 7 giorni
  di retention. NON tocca sys.stdout/stderr (causa segfault WebKit
  su macOS); cattura tracebacks via sys.excepthook + threading.excepthook
- bridge._log mirror su logger persistente (ui.<view>) → tutto quello
  che vedi nei log delle tab e' anche su file
- Bridge: get_log_file_path() + open_log_folder() per la nuova sezione
  Impostazioni con path visibile e bottone "Apri cartella"
2026-08-27 11:27:57 +02:00

127 lines
4.6 KiB
Python

"""Gestione configurazione persistente (JSON)."""
import json
import os
import sys
from pathlib import Path
VERSION = "v1.11.1"
APP_NAME = "MusicTools"
_LEGACY_NAME = "MusicDownload"
# Endpoint dell'API di licenza/aggiornamenti. Cambialo qui per puntare
# a un ambiente di staging.
LICENSE_API_URL = "https://musictools.djluza.com"
# Quanti giorni puo' restare l'app offline prima di richiedere una
# nuova validazione contro il server.
LICENSE_GRACE_DAYS = 14
# Ogni quanti giorni l'app rivalida la licenza in background quando online.
LICENSE_REVALIDATE_DAYS = 7
def _get_config_dir() -> Path:
"""Ritorna la directory per config.json.
Frozen (bundle PyInstaller):
- macOS: ~/Library/Application Support/MusicTools/
- Windows: %APPDATA%/MusicTools/
Dev (non frozen):
- directory del progetto
Se la cartella nuova non esiste ma quella legacy 'MusicDownload' si',
viene migrata (copia di config.json) cosi' l'utente non perde le
impostazioni dopo il rename.
"""
if getattr(sys, "frozen", False):
if sys.platform == "win32":
base = Path(os.environ.get("APPDATA", Path.home()))
else:
base = Path.home() / "Library" / "Application Support"
config_dir = base / APP_NAME
legacy_dir = base / _LEGACY_NAME
# Migrazione one-shot: se non esiste la nuova ma esiste la vecchia
if not config_dir.exists() and legacy_dir.exists():
try:
config_dir.mkdir(parents=True, exist_ok=True)
legacy_cfg = legacy_dir / "config.json"
if legacy_cfg.exists():
(config_dir / "config.json").write_bytes(legacy_cfg.read_bytes())
except OSError:
pass
config_dir.mkdir(parents=True, exist_ok=True)
return config_dir
return Path(__file__).resolve().parent.parent
CONFIG_PATH = _get_config_dir() / "config.json"
# Default: i path relativi al progetto (se non frozen)
_project_dir = Path(__file__).resolve().parent.parent
DEFAULTS = {
"client_id": "",
"client_secret": "",
"bitrate": "320K",
"hq_threshold": 310,
"cookies_path": str(_project_dir / "cookies.txt"),
"cookies_browser": "", # "" | chrome | safari | firefox | edge | brave
"output_dir": str(_project_dir / "MUSICA"),
"theme": "dark",
# ---- Beatport ----
"beatport_last_genre": "melodic-house-techno", # ultimo genere Top 100 caricato
# ---- Traxsource ----
"traxsource_last_genre": "tech-house",
# ---- Music Search (Spotify + YouTube) ----
"spotify_search_last_query": "",
"spotify_search_artist_mode": False,
"youtube_search_last_query": "",
# ---- Dedup (audio duplicati via Chromaprint) ----
"dedup_last_folder": "",
"dedup_recursive": True,
"dedup_method": "fingerprint", # "fingerprint" | "filename"
# ---- Cataloga (AcoustID -> <Anno>/<Genere>/) ----
"catalog_last_source": "",
"catalog_last_target": "",
"catalog_recursive": True,
"acoustid_app_key": "", # opzionale: se vuoto usa default in core/acoustid.py
# ---- Licenza ----
"license_key": "", # chiave fornita all'utente via email
"license_email": "", # email associata all'acquisto
"license_token": "", # JWT firmato dal server (claims offline)
"license_activated_at": 0, # epoch della prima attivazione
"last_validated_at": 0, # epoch dell'ultima revalidate online riuscita
"device_id": "", # UUID generato al primo avvio
# ---- Piano (snapshot dei claims JWT) ----
"plan_code": "", # "basic" | "pro" | "premium" | "annual"
"plan_name": "", # nome user-facing
"plan_features": [], # ["audio", "video", "record", "metadata", "upgrade"]
"plan_daily_limit": None, # None = unlimited (annual), altrimenti int
"plan_is_subscription": False,
"plan_expires_at": 0, # solo per annual one-time
"plan_period_end": 0, # solo per subscription mensili
}
def load_config() -> dict:
"""Carica la configurazione da config.json, con fallback ai default."""
config = dict(DEFAULTS)
if CONFIG_PATH.exists():
try:
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
stored = json.load(f)
config.update(stored)
except (json.JSONDecodeError, OSError):
pass
return config
def save_config(config: dict) -> None:
"""Salva la configurazione su config.json."""
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)