v1.7.0: piani Basic/Pro/Premium/Annual + quota giornaliera
Pricing model: subscription mensili (Basic 2.99 / Pro 5.99 / Premium 9.99) piu' Annual 49.90 one-time. Feature gating per piano (Basic = solo audio, Pro+ = tutto). Quota giornaliera (10/30/150) con reset mezzanotte Europe/Rome. Server: tabella daily_usage, endpoint /api/usage/consume e /status, webhook LS gestisce subscription_* events + mappatura variant_id -> plan via .env (LS_VARIANT_BASIC|PRO|PREMIUM|ANNUAL). Client: gating tab in sidebar in base ai features del piano, contatore "X/Y oggi" + bottone Upgrade, modal blocco con CTA verso landing.
This commit is contained in:
1 parent
bcd4f22b1e
commit
61459099f9
15 files changed
+1395
-108
No files matched your search
@@ -123,6 +123,7 @@ class Api:
|
||||
"config": safe_cfg,
|
||||
"spotify_guide": SPOTIFY_GUIDE_TEXT,
|
||||
"license": license_status,
|
||||
"plan": license_mod.get_plan(cfg),
|
||||
"purchase_url": "https://musictools.djluza.com",
|
||||
}
|
||||
|
||||
@@ -181,6 +182,63 @@ class Api:
|
||||
webbrowser.open("https://musictools.djluza.com")
|
||||
return {"ok": True}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Quota giornaliera
|
||||
# ------------------------------------------------------------------
|
||||
def get_quota_status(self) -> dict:
|
||||
"""Ritorna lo stato quota corrente (lettura senza incremento)."""
|
||||
try:
|
||||
data = license_mod.get_quota_status()
|
||||
return {"ok": True, "quota": data}
|
||||
except license_mod.LicenseNetworkError as e:
|
||||
return {"ok": False, "offline": True, "error": str(e)}
|
||||
except license_mod.LicenseError as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
def _gate(self, feature: str) -> Optional[dict]:
|
||||
"""Verifica feature + quota prima di un'azione.
|
||||
|
||||
Ritorna None se l'azione e' permessa; altrimenti un dict di
|
||||
errore pronto per la UI (con eventuale info quota per il modal
|
||||
"limite raggiunto").
|
||||
"""
|
||||
# 1) Feature gate locale (dai claims JWT in config)
|
||||
if not license_mod.has_feature(feature):
|
||||
plan = license_mod.get_plan()
|
||||
plan_name = plan.get("name") or "il tuo piano"
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "feature_not_in_plan",
|
||||
"feature": feature,
|
||||
"error": (
|
||||
f"Questa funzione non e' inclusa in {plan_name}. "
|
||||
"Passa a un piano superiore per sbloccarla."
|
||||
),
|
||||
"plan": plan,
|
||||
}
|
||||
# 2) Quota gate server-side
|
||||
try:
|
||||
res = license_mod.consume_quota(feature)
|
||||
except license_mod.LicenseNetworkError as e:
|
||||
return {
|
||||
"ok": False, "reason": "offline", "offline": True,
|
||||
"error": f"Impossibile contattare il server: {e}",
|
||||
}
|
||||
except license_mod.LicenseError as e:
|
||||
return {"ok": False, "reason": "license_invalid", "error": str(e)}
|
||||
|
||||
if not res.get("allowed", True):
|
||||
return {
|
||||
"ok": False,
|
||||
"reason": "quota_exceeded",
|
||||
"feature": feature,
|
||||
"error": res.get("error") or "Limite giornaliero raggiunto.",
|
||||
"quota": res,
|
||||
}
|
||||
# OK: notifico la UI dello stato quota aggiornato
|
||||
self._emit("quota:update", res)
|
||||
return None
|
||||
|
||||
def check_update(self) -> dict:
|
||||
"""Controlla aggiornamenti interrogando l'API djluza.com.
|
||||
|
||||
@@ -326,6 +384,10 @@ class Api:
|
||||
if not os.path.exists(path):
|
||||
return {"ok": False, "error": "File non trovato"}
|
||||
|
||||
gate = self._gate("metadata")
|
||||
if gate:
|
||||
return gate
|
||||
|
||||
data = payload.get("data") or {}
|
||||
cover_path = (payload.get("cover_path") or "").strip() or None
|
||||
remove_cover = bool(payload.get("remove_cover", False))
|
||||
@@ -360,6 +422,10 @@ class Api:
|
||||
if not filename.lower().endswith(".mp3"):
|
||||
filename += ".mp3"
|
||||
|
||||
gate = self._gate("record")
|
||||
if gate:
|
||||
return gate
|
||||
|
||||
out_path = os.path.join(output_dir, filename)
|
||||
|
||||
def cb(status, payload_evt):
|
||||
@@ -412,6 +478,10 @@ class Api:
|
||||
if not output_dir:
|
||||
return {"ok": False, "error": "Cartella output non impostata"}
|
||||
|
||||
gate = self._gate("audio")
|
||||
if gate:
|
||||
return gate
|
||||
|
||||
self._download_thread = threading.Thread(
|
||||
target=self._download_worker,
|
||||
args=(list(urls), output_dir),
|
||||
@@ -441,6 +511,10 @@ class Api:
|
||||
if safe:
|
||||
output_dir = os.path.join(output_dir, safe)
|
||||
|
||||
gate = self._gate("audio")
|
||||
if gate:
|
||||
return gate
|
||||
|
||||
self._download_thread = threading.Thread(
|
||||
target=self._tracks_worker,
|
||||
args=(list(tracks), output_dir),
|
||||
@@ -627,6 +701,10 @@ class Api:
|
||||
if not directory:
|
||||
return {"ok": False, "error": "Cartella non impostata"}
|
||||
|
||||
gate = self._gate("upgrade")
|
||||
if gate:
|
||||
return gate
|
||||
|
||||
cfg = load_config()
|
||||
cookies_path = cfg.get("cookies_path", "")
|
||||
|
||||
@@ -700,6 +778,11 @@ class Api:
|
||||
if not output_dir:
|
||||
return {"ok": False, "error": "Cartella output non impostata"}
|
||||
|
||||
# Anche "audio_only" video count come video: usa codec/yt-dlp diversi
|
||||
gate = self._gate("video")
|
||||
if gate:
|
||||
return gate
|
||||
|
||||
self._video_thread = threading.Thread(
|
||||
target=self._video_worker,
|
||||
args=(list(urls), output_dir, quality, audio_only),
|
||||
|
||||
Reference in new issue
Block a user