Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
891562dc88 | ||
|
|
9d21ca051f | ||
|
|
05ea4c6181 | ||
|
|
30be62af78 |
No files matched your search
@@ -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
@@ -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}")
|
||||
|
||||
Executable
+27
@@ -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."
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 207 KiB |
+10
-1
@@ -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
@@ -5,7 +5,7 @@ import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
VERSION = "v1.7.1"
|
||||
VERSION = "v1.7.5"
|
||||
|
||||
|
||||
APP_NAME = "MusicTools"
|
||||
|
||||
@@ -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 = {
|
||||
"&": "&", "'": "'", "'": "'",
|
||||
""": '"', "<": "<", ">": ">",
|
||||
}
|
||||
|
||||
|
||||
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]
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user