Client (Python/JS):
- core/license.py: attivazione, validazione, deactivate (HTTP client + JWT decode)
- core/config.py: campi license_* + URL endpoint + costanti grace/revalidate
- api/bridge.py: get_init_data ritorna license status; activate_license/
deactivate_license/revalidate_license/open_purchase_page
- check_update riscritto: punta a musictools.djluza.com/api/latest con
Bearer token, URL di download firmato dal server solo se licensed
- webui: schermata di attivazione bloccante all'avvio; sezione Licenza
in Impostazioni con verifica/disattiva
Backend (server/):
- Cloudflare Worker + D1 + R2 (vedi server/README.md)
- Endpoints: /api/license/{activate,validate,deactivate}, /api/latest,
/api/webhook/lemonsqueezy, /api/health
- JWT HS256 con rotazione; max 3 attivazioni/licenza
- Generazione licenze via webhook Lemon Squeezy + email Resend
- Schema D1 in migrations/0001_init.sql
Memory: nuova musictools-commercial-launch.md, rimosso pending obsoleto
NON taggare finche backend non e' deployato (l'app e' bloccante)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
101 lines
3.3 KiB
Python
101 lines
3.3 KiB
Python
"""Gestione configurazione persistente (JSON)."""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
VERSION = "v1.5.2"
|
|
|
|
|
|
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
|
|
}
|
|
|
|
|
|
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)
|