- 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"
127 lines
4.6 KiB
Python
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)
|