Compare commits

...
4 Commits
Author SHA1 Message Date
luzadev 891562dc88 v1.7.5: fix avvio Windows ("Failed to resolve Python.Runtime.Loader")
PyInstaller non bundlava i file di supporto di pythonnet/clr_loader
(Python.Runtime.dll, nethost.dll, runtimeconfig.json), per cui al
caricamento di webview.platforms.winforms.py l'import 'clr' moriva.

Aggiunti --collect-all pythonnet/clr_loader + copy-metadata.
Pinnata pythonnet a 3.0.3 (l'ultima compatibile con PyInstaller
senza patch aggiuntive).
2026-06-09 12:11:42 +02:00
luzadev 9d21ca051f v1.7.4: DMG macOS con Fix Gatekeeper integrato
Macos ora viene distribuito come .dmg invece di .zip:
- contiene MusicTools.app + alias Applicazioni + "Fix Gatekeeper.command"
- doppio click sullo script rimuove com.apple.quarantine senza
  Terminal manuale -> niente piu' "App non verificata"

Backend: /api/download serve .dmg con Content-Type
application/x-apple-diskimage. Landing /download aggiornata
con il nuovo flusso (3 step).
2026-06-09 11:53:37 +02:00
luzadev 05ea4c6181 v1.7.3: nuova icona app + landing chiarisce limiti senza chiavi Spotify
Icona: rigenerati icon.icns (macOS multi-size) e icon.ico (Windows
multi-size 16-256) dalla sorgente assets/icon_1024.png.

Landing: nuovo box "Per sfruttare al massimo MusicTools serve un
account Spotify" + FAQ esplicita su cosa funziona senza setup
e dove servono le chiavi.
2026-06-09 11:43:20 +02:00
luzadev 30be62af78 v1.7.2: brani Spotify scaricabili senza API keys + fix download.html
Spotify: per i singoli brani ora basta incollare il link, l'app
estrae name+artist dai meta og:title/og:description pubblici e
risolve via YouTube. Album/playlist continuano a richiedere
le credenziali (gratis, 2 min).

Landing /download: bottoni Mac/Windows ora navigano nella stessa
tab via JS handler + feedback "avvio in corso" invece del fragile
target="_blank".
2026-06-09 11:33:28 +02:00
14 changed files with 354 additions and 34 deletions

No files matched your search

+27 -6
View File
@@ -33,17 +33,38 @@ jobs:
- name: Build app
run: python build_macos.py
- name: Package .app -> zip
- name: Create DMG
run: |
cd dist
ditto -c -k --sequesterRsrc --keepParent MusicTools.app ../MusicTools-macOS.zip
ls -lh ../MusicTools-macOS.zip
STAGING="dmg_staging"
rm -rf "$STAGING"
mkdir -p "$STAGING"
cp -R dist/MusicTools.app "$STAGING/"
cp "assets/Fix Gatekeeper.command" "$STAGING/"
chmod +x "$STAGING/Fix Gatekeeper.command"
ln -s /Applications "$STAGING/Applications"
cat > "$STAGING/LEGGIMI.txt" <<'EOF'
INSTALLAZIONE MusicTools
========================
1) Trascina MusicTools.app sulla cartella "Applicazioni"
(l'alias e' qui dentro nel DMG)
2) Doppio click su "Fix Gatekeeper.command"
Rimuove la quarantena macOS Gatekeeper (una sola volta).
3) Apri MusicTools dalle Applicazioni e attiva con email + chiave.
Problemi? scrivici a info@djluza.com
EOF
hdiutil create -volname "MusicTools" -srcfolder "$STAGING" \
-ov -format UDZO MusicTools-macOS.dmg
ls -lh MusicTools-macOS.dmg
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: MusicTools-macOS
path: MusicTools-macOS.zip
path: MusicTools-macOS.dmg
retention-days: 14
# =========================================================
@@ -107,5 +128,5 @@ jobs:
generate_release_notes: true
fail_on_unmatched_files: true
files: |
artifacts/macos/MusicTools-macOS.zip
artifacts/macos/MusicTools-macOS.dmg
artifacts/windows/MusicTools-Windows.zip
+26 -10
View File
@@ -20,7 +20,12 @@ from core.downloader import (
reset_stop as reset_download_stop,
is_stopped as download_is_stopped,
)
from core.spotify_client import get_access_token, resolve_spotify_url
from core.spotify_client import (
get_access_token,
resolve_spotify_url,
resolve_spotify_track_no_auth,
SpotifyAuthRequired,
)
from core.metadata import read_metadata, write_metadata, SUPPORTED_EXTS
from core.recorder import (
list_input_devices,
@@ -590,14 +595,11 @@ class Api:
view = "download"
token = None
client_id = cfg.get("client_id", "")
client_secret = cfg.get("client_secret", "")
has_credentials = bool(client_id and client_secret)
has_spotify = any("spotify.com" in u for u in urls)
if has_spotify:
client_id = cfg.get("client_id", "")
client_secret = cfg.get("client_secret", "")
if not client_id or not client_secret:
self._log(view, "[ERRORE] Configura Client ID e Client Secret nelle Impostazioni.")
self._emit("download:done", {"ok": False})
return
if has_spotify and has_credentials:
self._log(view, "[INFO] Autenticazione Spotify...")
try:
token = get_access_token(client_id, client_secret)
@@ -605,6 +607,13 @@ class Api:
self._log(view, f"[ERRORE] Autenticazione fallita: {e}")
self._emit("download:done", {"ok": False})
return
elif has_spotify:
self._log(
view,
"[INFO] Spotify API non configurate: i singoli brani "
"verranno comunque scaricati via metadata pubblico. "
"Per album/playlist servono le credenziali (gratuite, 2 min)."
)
for url_idx, url in enumerate(urls):
if download_is_stopped():
@@ -664,12 +673,19 @@ class Api:
if is_spotify:
self._log(view, "[INFO] Recupero informazioni da Spotify...")
try:
label, name, tracks = resolve_spotify_url(token, url)
if token:
label, name, tracks = resolve_spotify_url(token, url)
else:
# Senza API keys: fallback solo per singoli brani.
label, name, tracks = resolve_spotify_track_no_auth(url)
except SpotifyAuthRequired as e:
self._log(view, f"[ERRORE] {e}")
continue
except Exception as e:
self._log(view, f"[ERRORE] {e}")
continue
self._log(view, f"[INFO] {label}: {name} ({len(tracks)} brani)")
dest_dir = os.path.join(output_dir, name)
dest_dir = os.path.join(output_dir, name) if len(tracks) > 1 else output_dir
download_playlist(tracks, dest_dir, bitrate, cookies_path, progress_cb)
else:
self._log(view, f"[INFO] Download diretto via yt-dlp: {url}")
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# Rimuove la quarantena Gatekeeper da MusicTools.app
# Doppio click in macOS apre Terminal ed esegue questo script.
set -e
APP_NAME="MusicTools.app"
# Cerca l'app in /Applications, poi nella stessa cartella del DMG montato.
if [ -d "/Applications/${APP_NAME}" ]; then
APP="/Applications/${APP_NAME}"
elif [ -d "$(dirname "$0")/${APP_NAME}" ]; then
APP="$(dirname "$0")/${APP_NAME}"
else
osascript -e "display alert \"${APP_NAME} non trovata\" message \"Trascina MusicTools.app nella cartella Applicazioni prima di eseguire questo script.\" as critical"
exit 1
fi
echo "Rimuovo la quarantena da: ${APP}"
xattr -dr com.apple.quarantine "${APP}" 2>/dev/null || true
# Notifica visuale
osascript -e "display notification \"MusicTools puo' essere aperta normalmente con doppio click.\" with title \"Fix completato\""
echo ""
echo "Fatto. Ora puoi aprire MusicTools normalmente con doppio click."
echo "Puoi chiudere questa finestra."
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 61 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 207 KiB

+10 -1
View File
@@ -115,11 +115,20 @@ def run_pyinstaller():
# Hidden imports per pywebview (EdgeChromium su Windows)
"--hidden-import", "webview",
"--hidden-import", "webview.platforms.edgechromium",
"--hidden-import", "webview.platforms.winforms",
"--hidden-import", "clr",
"--hidden-import", "clr_loader",
"--hidden-import", "requests",
# Collegamenti completi alle DLL .NET di pywebview/pythonnet
# Collegamenti completi: webview, pythonnet, clr_loader (porta
# con se Python.Runtime.dll + nethost.dll + runtimeconfig.json
# altrimenti winforms.py fallisce con "Failed to resolve
# Python.Runtime.Loader.Initialize ...")
"--collect-all", "webview",
"--collect-all", "pythonnet",
"--collect-all", "clr_loader",
"--collect-all", "mutagen",
"--copy-metadata", "pythonnet",
"--copy-metadata", "clr_loader",
# Binari bundled
*add_binaries,
# Entry point
+1 -1
View File
@@ -5,7 +5,7 @@ import os
import sys
from pathlib import Path
VERSION = "v1.7.1"
VERSION = "v1.7.5"
APP_NAME = "MusicTools"
+80
View File
@@ -175,3 +175,83 @@ def resolve_spotify_url(token: str, url: str) -> tuple[str, str, list[dict]]:
else:
name, tracks = get_playlist_info(token, item_id)
return "Playlist", name, tracks
# ---------------------------------------------------------------------------
# Fallback senza credenziali (solo per singoli brani)
# ---------------------------------------------------------------------------
class SpotifyAuthRequired(Exception):
"""Sollevata quando un URL Spotify richiede le API keys (album/playlist)."""
_OG_TITLE_RE = re.compile(
r'<meta\s+property="og:title"\s+content="([^"]+)"', re.IGNORECASE,
)
_OG_DESC_RE = re.compile(
r'<meta\s+property="og:description"\s+content="([^"]+)"', re.IGNORECASE,
)
# Tag HTML entities che ci interessano nelle og: (Spotify le scrive cosi').
_HTML_ENTITIES = {
"&amp;": "&", "&#x27;": "'", "&apos;": "'",
"&quot;": '"', "&lt;": "<", "&gt;": ">",
}
def _decode_entities(s: str) -> str:
for k, v in _HTML_ENTITIES.items():
s = s.replace(k, v)
return s
def resolve_spotify_track_no_auth(url: str) -> tuple[str, str, list[dict]]:
"""Estrae name/artist da un URL Spotify SENZA credenziali API.
Funziona solo per brani singoli (open.spotify.com/track/<id>): legge
i meta tag og:title e og:description della pagina pubblica.
Per album/playlist non c'e' modo affidabile senza JS rendering -> alza
SpotifyAuthRequired.
Ritorna ("Brano", name, [{name, artist}]).
"""
kind, _ = detect_url_type(url)
if kind != "track":
raise SpotifyAuthRequired(
f"I link a {kind} richiedono le credenziali Spotify (gratuite). "
"Vai in Impostazioni -> Spotify API per configurarle in 2 minuti."
)
try:
resp = requests.get(
url,
headers={"User-Agent": "Mozilla/5.0 (MusicTools)"},
timeout=15,
)
resp.raise_for_status()
except requests.RequestException as e:
raise ValueError(f"Impossibile leggere il brano da Spotify: {e}") from e
html = resp.text
title_m = _OG_TITLE_RE.search(html)
desc_m = _OG_DESC_RE.search(html)
if not title_m:
raise ValueError("Risposta inattesa da Spotify (meta og:title mancante).")
name = _decode_entities(title_m.group(1)).strip()
artist = ""
if desc_m:
# Formato osservato: "Artist Name · Song Title · Song · YYYY"
# oppure con piu' artisti separati da ", ".
parts = [_decode_entities(p).strip() for p in desc_m.group(1).split("·")]
if parts:
artist = parts[0]
if not artist:
raise ValueError(
"Impossibile estrarre l'artista dal brano Spotify senza API keys. "
"Configura le credenziali in Impostazioni."
)
track = {"name": name, "artist": artist}
return "Brano", name, [track]
+63
View File
@@ -601,6 +601,69 @@ a { color: inherit; text-decoration: none; }
text-align: center;
}
/* Box informativo sulle chiavi API Spotify */
.spotify-note {
max-width: 900px;
margin: 48px auto 0;
padding: 28px 32px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 18px;
}
.spotify-note h3 {
margin: 0 0 10px;
font-size: 19px;
font-weight: 800;
}
.spotify-note > p {
margin: 0 0 18px;
color: var(--text-muted);
font-size: 15px;
line-height: 1.55;
}
.spotify-note-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin: 18px 0 14px;
}
@media (max-width: 640px) {
.spotify-note-grid { grid-template-columns: 1fr; }
}
.sn-col {
padding: 18px 20px;
border-radius: 12px;
border: 1px solid var(--border);
}
.sn-with {
background: rgba(29, 185, 84, 0.08);
border-color: rgba(29, 185, 84, 0.3);
}
.sn-without {
background: rgba(120, 120, 120, 0.06);
}
.sn-title {
font-weight: 800;
font-size: 14.5px;
margin-bottom: 8px;
color: var(--text);
}
.sn-col ul {
margin: 0;
padding-left: 18px;
color: var(--text-muted);
font-size: 14px;
line-height: 1.55;
}
.sn-col li { margin-bottom: 4px; }
.sn-col strong { color: var(--text); }
.spotify-note-fine {
margin-top: 12px;
font-size: 12.5px;
color: var(--text-dim);
text-align: center;
}
/* ============================================================
FAQ
============================================================ */
+63 -14
View File
@@ -90,6 +90,24 @@
.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-status {
max-width: 460px;
margin: -16px auto 24px;
padding: 12px 18px;
text-align: center;
background: rgba(29, 185, 84, 0.08);
border: 1px solid rgba(29, 185, 84, 0.25);
color: var(--accent);
border-radius: 12px;
font-size: 14px;
font-weight: 600;
}
.dl-status.err {
background: rgba(239, 68, 68, 0.08);
border-color: rgba(239, 68, 68, 0.3);
color: #f87171;
}
</style>
</head>
<body>
@@ -137,7 +155,7 @@
<div class="dl-os">🍎</div>
<h2>macOS</h2>
<p class="meta">macOS 11 Big Sur o successivi · Apple Silicon e Intel</p>
<a class="btn btn-primary btn-lg btn-block" id="dlMacBtn" target="_blank" rel="noopener">
<a class="btn btn-primary btn-lg btn-block dl-btn" id="dlMacBtn" data-platform="macos">
Scarica per Mac
</a>
</article>
@@ -146,19 +164,21 @@
<div class="dl-os">🪟</div>
<h2>Windows</h2>
<p class="meta">Windows 10 e 11 · 64-bit</p>
<a class="btn btn-primary btn-lg btn-block" id="dlWinBtn" target="_blank" rel="noopener">
<a class="btn btn-primary btn-lg btn-block dl-btn" id="dlWinBtn" data-platform="windows">
Scarica per Windows
</a>
</article>
</div>
<p id="dlStatus" class="dl-status" hidden></p>
<div class="dl-steps">
<h2>Installazione macOS</h2>
<ol>
<li>Apri il file <code>.zip</code> scaricato — viene estratto in <code>MusicTools.app</code></li>
<li>Trascina <strong>MusicTools.app</strong> nella cartella <strong>Applicazioni</strong></li>
<li>Vai in Applicazioni, <strong>tasto destro</strong> su MusicTools → <strong>Apri</strong> → <strong>Apri</strong> (solo la prima volta, e' la procedura standard per app non firmate)</li>
<li>Inserisci email e chiave per attivare</li>
<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>
</ol>
</div>
@@ -184,19 +204,47 @@
return url.searchParams.get(name) || "";
}
function setDownloadLinks(email, key) {
const q = "key=" + encodeURIComponent(key) + "&email=" + encodeURIComponent(email);
document.getElementById("dlMacBtn").href = "/api/download/macos?" + q;
document.getElementById("dlWinBtn").href = "/api/download/windows?" + q;
// Stato corrente — popolato da URL o dal form submit.
const dl = { email: "", key: "" };
function setStatus(text, isError) {
const el = document.getElementById("dlStatus");
if (!text) { el.hidden = true; el.textContent = ""; return; }
el.textContent = text;
el.classList.toggle("err", !!isError);
el.hidden = false;
}
function showDownloadCards(email, key) {
dl.email = email;
dl.key = key;
document.getElementById("dlGrid").hidden = false;
document.getElementById("dlForm").hidden = true;
}
// Se arrivi dall'email con ?key=&email= mostra subito i bottoni
// Click sui bottoni: naviga sulla stessa tab verso /api/download/:platform.
// Il server fa 302 con Content-Disposition: attachment -> il browser
// scarica il file e la pagina /download resta visibile.
document.querySelectorAll(".dl-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.preventDefault();
if (!dl.email || !dl.key) {
setStatus("Inserisci email e chiave prima di scaricare.", true);
return;
}
const platform = btn.dataset.platform;
const q = "key=" + encodeURIComponent(dl.key) +
"&email=" + encodeURIComponent(dl.email);
setStatus("Avvio del download in corso… il file si apre fra qualche secondo.");
window.location.href = "/api/download/" + platform + "?" + q;
});
});
// Se arrivi dall'email con ?key=&email= mostra subito i bottoni.
const urlKey = getParam("key");
const urlEmail = getParam("email");
if (urlKey && urlEmail) {
setDownloadLinks(urlEmail, urlKey);
showDownloadCards(urlEmail, urlKey);
}
// Form submit
@@ -204,10 +252,11 @@
const email = document.getElementById("dlEmail").value.trim();
const key = document.getElementById("dlKey").value.trim();
if (!email || !key) {
alert("Inserisci email e chiave di licenza.");
setStatus("Inserisci email e chiave di licenza.", true);
return;
}
setDownloadLinks(email, key);
setStatus("");
showDownloadCards(email, key);
});
// Enter dentro i campi
+49
View File
@@ -284,6 +284,39 @@
</p>
</div>
<div class="spotify-note">
<h3>Per sfruttare al massimo MusicTools serve un account Spotify</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.
</p>
<div class="spotify-note-grid">
<div class="sn-col sn-with">
<div class="sn-title">✓ Con chiavi API Spotify (consigliato)</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>
<li>Tutte le altre fonti restano sempre attive</li>
</ul>
</div>
<div class="sn-col sn-without">
<div class="sn-title">Senza chiavi (modalita' base)</div>
<ul>
<li>Da Spotify scarichi <strong>un brano alla volta</strong></li>
<li>YouTube, SoundCloud, TikTok, IG, FB: <strong>nessuna limitazione</strong></li>
<li>Ricerca per "Artista - Titolo" sempre attiva</li>
</ul>
</div>
</div>
<p class="spotify-note-fine">
MusicTools non e' affiliato a Spotify. Le chiavi API restano sul tuo
dispositivo, non passano per i nostri server.
</p>
</div>
<div class="use-policy">
<h3>Uso responsabile</h3>
<p>
@@ -347,6 +380,22 @@
<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>
</details>
<details class="faq-item">
<summary>Cosa posso fare senza configurare le chiavi API Spotify?</summary>
<p>
<strong>Da YouTube, SoundCloud, TikTok, Instagram, Facebook</strong>:
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.
</p>
</details>
<details class="faq-item">
<summary>Posso registrare l'audio di Spotify mentre suona?</summary>
<p>Sì, con BlackHole (macOS) o la scheda audio virtuale di Windows. La tab Registra ti guida nel setup passo-passo.</p>
+1 -1
View File
@@ -5,4 +5,4 @@ mutagen>=1.47.0
pyobjc-core>=10.0 ; sys_platform == "darwin"
pyobjc-framework-Cocoa>=10.0 ; sys_platform == "darwin"
pyobjc-framework-WebKit>=10.0 ; sys_platform == "darwin"
pythonnet>=3.0 ; sys_platform == "win32"
pythonnet==3.0.3 ; sys_platform == "win32"
+7 -1
View File
@@ -89,7 +89,13 @@ export async function download(req, res) {
}
const name = path.basename(abs);
res.setHeader("Content-Type", "application/zip");
const ext = path.extname(name).toLowerCase();
const contentType = ext === ".dmg"
? "application/x-apple-diskimage"
: ext === ".exe"
? "application/vnd.microsoft.portable-executable"
: "application/zip";
res.setHeader("Content-Type", contentType);
res.setHeader("Content-Disposition", `attachment; filename="${name}"`);
fs.createReadStream(abs).pipe(res);
}