v1.7.8: sticky save button + Spotify Premium chiarito

Impostazioni: il bottone "Salva impostazioni" e' ora sticky in fondo
al viewport, sempre visibile senza dover scrollare in coda alla pagina.

Spotify: aggiornata guida in-app e landing per riflettere il nuovo
requisito Premium per creare le API keys (fino a oggi era gratis con
account Spotify Free; Spotify ha cambiato policy). Singoli brani e
tutte le altre fonti continuano a funzionare senza Premium.

Inclusi anche file gia deployati lato server in sessioni precedenti
(webhook LS, landing icone PNG, download.html Gatekeeper instructions).
This commit is contained in:
luzadev committed 2026-06-09 15:42:42 +02:00
1 parent c2599a72a1
commit 6e96e8c6b7
12 files changed
+146 -37

No files matched your search

+10 -2
View File
@@ -42,7 +42,15 @@ from core.upgrader import (
SPOTIFY_GUIDE_TEXT = """\
1) Vai su https://developer.spotify.com/dashboard e accedi (anche con account free).
NOTE: Spotify oggi consente di creare le chiavi API SOLO con un account
Spotify Premium attivo. Senza Premium puoi comunque scaricare:
- YouTube, SoundCloud, TikTok, Instagram, Facebook (senza setup)
- Brani Spotify SINGOLI (incolla il link e basta, l'app fa il resto)
Le chiavi Spotify servono solo per playlist e album da Spotify.
Procedura (se hai Spotify Premium):
1) Vai su https://developer.spotify.com/dashboard e accedi.
2) Clicca "Create app".
3) Compila:
- App name: MusicTools (o quello che vuoi)
@@ -55,7 +63,7 @@ SPOTIFY_GUIDE_TEXT = """\
- Client Secret (clicca "View client secret")
5) Incollali qui sotto e premi "Salva".
Note: credenziali gratuite, no Premium. Limite 100 richieste/minuto.
Limite tecnico Spotify: 100 richieste/minuto.
"""
+1 -1
View File
@@ -5,7 +5,7 @@ import os
import sys
from pathlib import Path
VERSION = "v1.7.7"
VERSION = "v1.7.8"
APP_NAME = "MusicTools"
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+7
View File
@@ -91,6 +91,13 @@ a { color: inherit; text-decoration: none; }
font-size: 16px;
}
/* Quando il logo e' un'immagine, niente gradient ne' radius forzato */
.brand-logo-img {
background: none;
border-radius: 0;
object-fit: contain;
}
.nav-links {
display: flex;
gap: 26px;
+35 -2
View File
@@ -91,6 +91,30 @@
.dl-support { text-align: center; color: var(--text-muted); font-size: 14px; padding-top: 24px; border-top: 1px solid var(--divider); margin-top: 24px; }
.dl-support a { color: var(--accent); }
.dl-code {
display: block;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 14px;
margin: 8px 0 14px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 13px;
color: var(--accent);
user-select: all;
word-break: break-all;
}
.dl-sub {
margin: 8px 0 0;
padding-left: 20px;
color: var(--text-muted);
font-size: 14.5px;
line-height: 1.6;
}
.dl-sub li { margin-bottom: 4px; }
.dl-sub strong { color: var(--text); }
.dl-sub code { background: var(--bg-card-2); padding: 1px 6px; border-radius: 4px; font-size: 12.5px; }
.dl-status {
max-width: 460px;
margin: -16px auto 24px;
@@ -177,8 +201,17 @@
<ol>
<li>Apri il file <code>.dmg</code> scaricato — si monta come volume "MusicTools"</li>
<li>Trascina <strong>MusicTools.app</strong> sull'alias <strong>Applicazioni</strong> dentro il DMG</li>
<li>Doppio click su <strong>Fix Gatekeeper.command</strong> (dentro il DMG): autorizza l'app in 1 click. Si chiude da sola appena finito.</li>
<li>Apri MusicTools dalle Applicazioni, inserisci email e chiave per attivare</li>
<li>
Autorizza l'app (una sola volta, perche' non e' firmata con Developer ID Apple).<br>
<strong>Modo piu' semplice:</strong> apri il <strong>Terminale</strong> (cmd+spazio &rarr; "Terminale"), incolla questa riga e premi Invio:
<p class="dl-code">xattr -dr com.apple.quarantine /Applications/MusicTools.app</p>
<strong>In alternativa</strong> (senza Terminale):
<ul class="dl-sub">
<li><strong>Tasto destro</strong> su <code>Fix Gatekeeper.command</code> nel DMG &rarr; <strong>Apri</strong> &rarr; <strong>Apri</strong>. Se compare il blocco "non e' in grado di verificare", clicca <strong>Annulla</strong> e procedi col passo successivo.</li>
<li>Apri <strong>Impostazioni di Sistema</strong> &rarr; <strong>Privacy e sicurezza</strong>. In fondo vedi "Fix Gatekeeper.command e' stato bloccato": clicca <strong>Apri comunque</strong>.</li>
</ul>
</li>
<li>Apri <strong>MusicTools</strong> dalle Applicazioni, inserisci email e chiave per attivare</li>
</ol>
</div>
+30 -20
View File
@@ -11,7 +11,9 @@
<meta property="og:type" content="website" />
<meta property="og:url" content="https://musictools.djluza.com" />
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32.png" />
<link rel="icon" type="image/png" sizes="192x192" href="assets/favicon-192.png" />
<link rel="apple-touch-icon" sizes="192x192" href="assets/favicon-192.png" />
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
@@ -20,7 +22,7 @@
<header class="nav">
<div class="container nav-row">
<a href="#top" class="brand">
<span class="brand-logo">♫</span>
<img class="brand-logo brand-logo-img" src="assets/icon.png" alt="MusicTools" />
<span class="brand-name">MusicTools</span>
</a>
<nav class="nav-links">
@@ -285,17 +287,19 @@
</div>
<div class="spotify-note">
<h3>Per sfruttare al massimo MusicTools serve un account Spotify</h3>
<h3>Playlist e album Spotify: serve un account Spotify Premium</h3>
<p>
L'app raggiunge il <strong>100% delle sue funzioni</strong> quando colleghi
le tue <strong>chiavi API Spotify</strong>. Sono <strong>gratuite</strong>:
basta un account Spotify (anche <strong>quello gratis</strong>, niente
Premium richiesto) e bastano <strong>2 minuti</strong> per generarle dalla
Developer Dashboard. La guida passo-passo e' integrata nell'app.
MusicTools funziona <strong>al 100% subito</strong> con YouTube,
SoundCloud, TikTok, Instagram, Facebook e i singoli brani Spotify
(anche con account Spotify gratuito).<br>
Per scaricare <strong>playlist e album interi da Spotify</strong> servono
le tue <strong>chiavi API Spotify</strong>, che oggi Spotify rilascia solo
a chi ha un <strong>account Premium</strong> attivo (le crei tu dal
Developer Dashboard, in 2 minuti, con la guida integrata nell'app).
</p>
<div class="spotify-note-grid">
<div class="sn-col sn-with">
<div class="sn-title">✓ Con chiavi API Spotify (consigliato)</div>
<div class="sn-title">✓ Con Spotify Premium + chiavi API</div>
<ul>
<li>Scarichi <strong>playlist intere</strong> e <strong>album</strong> da Spotify</li>
<li>Brani con <strong>metadati e copertine</strong> ufficiali</li>
@@ -303,9 +307,9 @@
</ul>
</div>
<div class="sn-col sn-without">
<div class="sn-title">Senza chiavi (modalita' base)</div>
<div class="sn-title">Senza Premium (modalita' base)</div>
<ul>
<li>Da Spotify scarichi <strong>un brano alla volta</strong></li>
<li>Da Spotify scarichi <strong>un brano alla volta</strong> incollando il link</li>
<li>YouTube, SoundCloud, TikTok, IG, FB: <strong>nessuna limitazione</strong></li>
<li>Ricerca per "Artista - Titolo" sempre attiva</li>
</ul>
@@ -376,8 +380,15 @@
</details>
<details class="faq-item">
<summary>Devo creare un account Spotify Premium o YouTube Premium?</summary>
<p>No. Per Spotify ti basta un account gratuito (serve solo per ottenere le credenziali API dal loro dashboard, è gratis). YouTube non richiede account.</p>
<summary>Mi serve un abbonamento Spotify Premium?</summary>
<p>
Solo se vuoi scaricare <strong>playlist e album interi</strong> da Spotify:
in quel caso servono le chiavi API Spotify, che oggi Spotify rilascia
solo agli account Premium. Con un account Spotify gratuito (o senza
account Spotify) <strong>puoi comunque scaricare</strong> da YouTube,
SoundCloud, TikTok, IG, FB e i singoli brani Spotify incollando il link.
</p>
<p>YouTube non richiede alcun account.</p>
</details>
<details class="faq-item">
@@ -387,12 +398,11 @@
scarichi senza limiti, link diretti e ricerca per titolo. Senza setup.
</p>
<p>
<strong>Da Spotify</strong>: senza chiavi puoi scaricare <strong>un
brano alla volta</strong> incollando il link al singolo brano (l'app
estrae artista e titolo dai metadati pubblici di Spotify e cerca su
YouTube). Per <strong>album e playlist</strong> servono le chiavi API
— sono gratis (anche con account Spotify free) e la procedura guidata
nell'app ci mette 2 minuti.
<strong>Da Spotify</strong>: puoi scaricare <strong>un brano alla volta</strong>
incollando il link al singolo brano (l'app estrae artista e titolo
dai metadati pubblici di Spotify e cerca su YouTube). Per <strong>album
e playlist</strong> servono le chiavi API, che Spotify rilascia solo
agli account Premium.
</p>
</details>
@@ -452,7 +462,7 @@
<footer class="footer">
<div class="container footer-row">
<div class="footer-brand">
<span class="brand-logo">♫</span>
<img class="brand-logo brand-logo-img" src="assets/icon.png" alt="MusicTools" />
<span>MusicTools</span>
<span class="footer-by">by LuZa</span>
</div>
+50 -11
View File
@@ -73,18 +73,31 @@ async function handleOrderCreated(payload, attrs, t) {
const planCode = planByVariantId(variantId);
const plan = planCode ? getPlan(planCode) : null;
// Subscriptions: il record vero arriva con subscription_created.
// Se per qualche motivo arriva prima (ordine pagato ma sottoscrizione
// non ancora generata), accettiamo l'evento ma non creiamo nulla.
if (plan && plan.is_subscription) {
return { status: 200, body: { ok: true, deferred: "wait_for_subscription_created" } };
// Se l'order e' gia stato processato (lo riconosciamo dall'order_id),
// non duplichiamo licenza ne' email — ack 200 e basta.
const existing = await one(
"SELECT id FROM licenses WHERE order_id=? LIMIT 1",
[orderId],
);
if (existing) {
return { status: 200, body: { ok: true, duplicate: true } };
}
// Annual one-time (o piano non mappato -> trattalo come annual di default
// cosi' non lasciamo l'utente senza licenza se ha pagato).
// Piano non mappato -> fallback annual cosi' non blocchiamo l'utente
// che ha gia pagato (la licenza sara' usabile anche se non sappiamo
// i limiti esatti del piano comprato).
const planForRecord = plan?.code || "annual";
const limit = plan?.daily_limit ?? null;
const expiresAt = t + ANNUAL_TTL_SECONDS;
const planMeta = plan || getPlan("annual");
const limit = planMeta?.daily_limit ?? null;
// Annual one-time: settiamo expires_at = now + 365d.
// Subscription mensili: NON settiamo current_period_end qui — lo fara'
// subscription_created/subscription_payment_success quando arriva.
// Se quell'evento non arrivasse mai (perche' non abilitato in LS
// dashboard), la licenza resta utilizzabile (current_period_end NULL
// = no scadenza imposta lato server: la sottoscrizione vivra' o
// morira' su LS, e i webhook successivi aggiorneranno lo status).
const expiresAt = planMeta?.is_subscription ? null : t + ANNUAL_TTL_SECONDS;
const key = generateLicenseKey();
try {
@@ -103,7 +116,10 @@ async function handleOrderCreated(payload, attrs, t) {
}
try {
await sendLicenseEmail(email, key, { plan: planForRecord, expiresAt });
await sendLicenseEmail(email, key, {
plan: planForRecord,
expiresAt: expiresAt,
});
} catch (e) {
console.error("[email] send failed:", e?.message || e);
}
@@ -141,7 +157,7 @@ async function handleSubscriptionCreated(payload, attrs, t) {
const key = generateLicenseKey();
// Idempotenza: se ricevo lo stesso subscription_created due volte,
// riuso la licenza esistente (non ne creo un'altra).
// riuso la licenza esistente.
const existing = await one(
"SELECT id, license_key FROM licenses WHERE subscription_id=? LIMIT 1",
[subId],
@@ -156,6 +172,29 @@ async function handleSubscriptionCreated(payload, attrs, t) {
return { status: 200, body: { ok: true, duplicate: true } };
}
// Caso normale ora: la licenza e' gia stata creata da order_created
// (con subscription_id NULL). La aggiorno con subscription_id e
// current_period_end senza generare una nuova chiave ne' una nuova email.
const byOrder = orderId
? await one(
`SELECT id FROM licenses
WHERE order_id=? AND subscription_id IS NULL LIMIT 1`,
[orderId],
)
: null;
if (byOrder) {
await exec(
`UPDATE licenses
SET subscription_id=?, current_period_end=?, plan=?, daily_limit=?,
updated_at=?
WHERE id=?`,
[subId, periodEnd, planCode, plan.daily_limit, t, byOrder.id],
);
return { status: 200, body: { ok: true, attached_subscription: subId } };
}
// Fallback: nessuna licenza esistente -> ne creiamo una nuova
// (puo' succedere se order_created non e' arrivato per qualche motivo).
try {
await exec(
`INSERT INTO licenses
+13 -1
View File
@@ -873,11 +873,23 @@ input[type="number"]::-webkit-inner-spin-button {
white-space: pre-line;
}
/* Bottone "Salva impostazioni" — sticky in fondo al viewport per restare
sempre visibile mentre l'utente scrolla la tab Impostazioni. Niente
ricerca del pulsante in coda. */
/* Bottone "Salva impostazioni" — sticky in fondo al viewport per restare
sempre visibile mentre l'utente scrolla la tab Impostazioni. */
.save-row {
display: flex;
align-items: center;
gap: 16px;
margin: 20px 0 32px;
position: sticky;
bottom: -24px;
padding: 16px 32px;
margin: 24px -32px -32px;
background: var(--bg);
border-top: 1px solid var(--divider);
box-shadow: 0 -8px 16px rgba(0, 0, 0, 0.25);
z-index: 5;
}
.save-status {