Bug: codesign --deep ad-hoc dava a ffmpeg un identifier auto-generato (ffmpeg-<hash>) diverso da com.djluza.musictools del bundle, quindi macOS trattava ffmpeg come app distinta per TCC -> permesso microfono del main NON ereditato dal subprocess -> registrazione muta o errore. Fix: in adhoc_codesign() ora firmiamo OGNI Mach-O nested con --identifier com.djluza.musictools (stesso del bundle), poi firmiamo il bundle .app. Cosi' TCC vede un unico "team" coerente e il permesso microfono si propaga a ffmpeg/ffprobe/yt-dlp. Verificato in dev mode: registrazione BlackHole funziona quando ffmpeg eredita correttamente il TCC. Anche fix collaterale: bridge._gate() salta la chiamata consume_quota al server quando il piano corrente ha daily_limit=None (Annual). Per i piani unlimited non c'e' motivo di chiamare il server -> niente piu' errori online se il server e' temporaneamente irraggiungibile durante una registrazione su un piano Annual.
109 lines
3.8 KiB
Python
109 lines
3.8 KiB
Python
"""Gestione configurazione persistente (JSON)."""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
VERSION = "v1.7.12"
|
|
|
|
|
|
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"),
|
|
"output_dir": str(_project_dir / "MUSICA"),
|
|
"theme": "dark",
|
|
# ---- 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)
|