Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2599a72a1 | ||
|
|
2cc6c5e9c6 | ||
|
|
891562dc88 | ||
|
|
9d21ca051f | ||
|
|
05ea4c6181 |
No files matched your search
@@ -33,17 +33,38 @@ jobs:
|
|||||||
- name: Build app
|
- name: Build app
|
||||||
run: python build_macos.py
|
run: python build_macos.py
|
||||||
|
|
||||||
- name: Package .app -> zip
|
- name: Create DMG
|
||||||
run: |
|
run: |
|
||||||
cd dist
|
STAGING="dmg_staging"
|
||||||
ditto -c -k --sequesterRsrc --keepParent MusicTools.app ../MusicTools-macOS.zip
|
rm -rf "$STAGING"
|
||||||
ls -lh ../MusicTools-macOS.zip
|
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
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: MusicTools-macOS
|
name: MusicTools-macOS
|
||||||
path: MusicTools-macOS.zip
|
path: MusicTools-macOS.dmg
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
@@ -66,6 +87,14 @@ jobs:
|
|||||||
- name: Build exe
|
- name: Build exe
|
||||||
run: python build_windows.py
|
run: python build_windows.py
|
||||||
|
|
||||||
|
- name: Unblock files (rimuovi MOTW pre-zip)
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
# Per ridurre l'incidenza del bug "Failed to resolve
|
||||||
|
# Python.Runtime.Loader.Initialize" causato da Mark of the Web.
|
||||||
|
Get-ChildItem -Path dist/MusicTools -Recurse -Include *.dll,*.exe,*.pyd `
|
||||||
|
| Unblock-File
|
||||||
|
|
||||||
- name: Package -> zip
|
- name: Package -> zip
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
@@ -107,5 +136,5 @@ jobs:
|
|||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
files: |
|
files: |
|
||||||
artifacts/macos/MusicTools-macOS.zip
|
artifacts/macos/MusicTools-macOS.dmg
|
||||||
artifacts/windows/MusicTools-Windows.zip
|
artifacts/windows/MusicTools-Windows.zip
|
||||||
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 |
+21
-1
@@ -106,6 +106,9 @@ def run_pyinstaller():
|
|||||||
"--windowed",
|
"--windowed",
|
||||||
"--onedir",
|
"--onedir",
|
||||||
"--noconfirm",
|
"--noconfirm",
|
||||||
|
# Forza rebuild pulita (senza, residui di build vecchi possono
|
||||||
|
# confondere il bundling di pythonnet/clr_loader).
|
||||||
|
"--clean",
|
||||||
]
|
]
|
||||||
if icon_path.exists():
|
if icon_path.exists():
|
||||||
cmd.extend(["--icon", str(icon_path)])
|
cmd.extend(["--icon", str(icon_path)])
|
||||||
@@ -115,11 +118,28 @@ def run_pyinstaller():
|
|||||||
# Hidden imports per pywebview (EdgeChromium su Windows)
|
# Hidden imports per pywebview (EdgeChromium su Windows)
|
||||||
"--hidden-import", "webview",
|
"--hidden-import", "webview",
|
||||||
"--hidden-import", "webview.platforms.edgechromium",
|
"--hidden-import", "webview.platforms.edgechromium",
|
||||||
|
"--hidden-import", "webview.platforms.winforms",
|
||||||
"--hidden-import", "clr",
|
"--hidden-import", "clr",
|
||||||
|
"--hidden-import", "clr_loader",
|
||||||
|
"--hidden-import", "clr_loader.netfx",
|
||||||
|
"--hidden-import", "clr_loader.types",
|
||||||
|
"--hidden-import", "pythonnet",
|
||||||
"--hidden-import", "requests",
|
"--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", "webview",
|
||||||
|
"--collect-all", "pythonnet",
|
||||||
|
"--collect-all", "clr_loader",
|
||||||
"--collect-all", "mutagen",
|
"--collect-all", "mutagen",
|
||||||
|
# collect-data prende anche i file .json/.dll non-Python che
|
||||||
|
# collect-all potrebbe saltare (Python.Runtime.runtimeconfig.json
|
||||||
|
# in particolare).
|
||||||
|
"--collect-data", "pythonnet",
|
||||||
|
"--collect-data", "clr_loader",
|
||||||
|
"--copy-metadata", "pythonnet",
|
||||||
|
"--copy-metadata", "clr_loader",
|
||||||
# Binari bundled
|
# Binari bundled
|
||||||
*add_binaries,
|
*add_binaries,
|
||||||
# Entry point
|
# Entry point
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
VERSION = "v1.7.2"
|
VERSION = "v1.7.7"
|
||||||
|
|
||||||
|
|
||||||
APP_NAME = "MusicTools"
|
APP_NAME = "MusicTools"
|
||||||
|
|||||||
@@ -601,6 +601,69 @@ a { color: inherit; text-decoration: none; }
|
|||||||
text-align: center;
|
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
|
FAQ
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|||||||
@@ -175,10 +175,10 @@
|
|||||||
<div class="dl-steps">
|
<div class="dl-steps">
|
||||||
<h2>Installazione macOS</h2>
|
<h2>Installazione macOS</h2>
|
||||||
<ol>
|
<ol>
|
||||||
<li>Apri il file <code>.zip</code> scaricato — viene estratto in <code>MusicTools.app</code></li>
|
<li>Apri il file <code>.dmg</code> scaricato — si monta come volume "MusicTools"</li>
|
||||||
<li>Trascina <strong>MusicTools.app</strong> nella cartella <strong>Applicazioni</strong></li>
|
<li>Trascina <strong>MusicTools.app</strong> sull'alias <strong>Applicazioni</strong> dentro il DMG</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>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>Inserisci email e chiave per attivare</li>
|
<li>Apri MusicTools dalle Applicazioni, inserisci email e chiave per attivare</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,39 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div class="use-policy">
|
||||||
<h3>Uso responsabile</h3>
|
<h3>Uso responsabile</h3>
|
||||||
<p>
|
<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>
|
<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>
|
||||||
|
|
||||||
|
<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">
|
<details class="faq-item">
|
||||||
<summary>Posso registrare l'audio di Spotify mentre suona?</summary>
|
<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>
|
<p>Sì, con BlackHole (macOS) o la scheda audio virtuale di Windows. La tab Registra ti guida nel setup passo-passo.</p>
|
||||||
|
|||||||
@@ -4,6 +4,46 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Windows: forza il caricamento del runtime pythonnet PRIMA di importare
|
||||||
|
# webview. Con PyInstaller il lazy-loader fallisce con
|
||||||
|
# "Failed to resolve Python.Runtime.Loader.Initialize" perche'
|
||||||
|
# Python.Runtime.dll viene bundlata ma il loader non sa risolverla
|
||||||
|
# senza l'inizializzazione esplicita. Fix conferma:
|
||||||
|
# https://github.com/pythonnet/pythonnet/issues/2178
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# ============================================================
|
||||||
|
# Fix Windows: rimuovi Mark of the Web (MOTW) dalle DLL bundled.
|
||||||
|
# Quando l'utente scarica il .zip dal browser, Windows aggiunge
|
||||||
|
# uno stream NTFS Zone.Identifier a TUTTI i file estratti.
|
||||||
|
# .NET Framework 4.x rifiuta silenziosamente di risolvere i
|
||||||
|
# symbol export degli assembly managed con MOTW -> il loader
|
||||||
|
# netfx di pythonnet fallisce con "Failed to resolve
|
||||||
|
# Python.Runtime.Loader.Initialize".
|
||||||
|
# Soluzione: cancelliamo lo stream :Zone.Identifier da ogni
|
||||||
|
# .dll/.exe del bundle al primo avvio (operazione idempotente
|
||||||
|
# e sicura: l'utente ha gia eseguito l'exe esplicitamente).
|
||||||
|
# Vedi: https://github.com/r0x0r/pywebview/issues/1215
|
||||||
|
# ============================================================
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
_kernel32 = ctypes.windll.kernel32
|
||||||
|
_bundle = Path(sys._MEIPASS) # type: ignore[attr-defined]
|
||||||
|
for _f in _bundle.rglob("*"):
|
||||||
|
if _f.suffix.lower() in (".dll", ".exe", ".pyd"):
|
||||||
|
_kernel32.DeleteFileW(f"{_f}:Zone.Identifier")
|
||||||
|
except Exception as _e:
|
||||||
|
print(f"[bootstrap] MOTW cleanup failed: {_e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
os.environ["PYTHONNET_RUNTIME"] = "netfx"
|
||||||
|
from pythonnet import load as _pythonnet_load
|
||||||
|
_pythonnet_load("netfx")
|
||||||
|
import clr # noqa: F401 - inizializza il loader
|
||||||
|
except Exception as _e:
|
||||||
|
print(f"[bootstrap] preload pythonnet failed: {_e}")
|
||||||
|
|
||||||
import webview
|
import webview
|
||||||
|
|
||||||
from api.bridge import Api
|
from api.bridge import Api
|
||||||
|
|||||||
+2
-1
@@ -5,4 +5,5 @@ mutagen>=1.47.0
|
|||||||
pyobjc-core>=10.0 ; sys_platform == "darwin"
|
pyobjc-core>=10.0 ; sys_platform == "darwin"
|
||||||
pyobjc-framework-Cocoa>=10.0 ; sys_platform == "darwin"
|
pyobjc-framework-Cocoa>=10.0 ; sys_platform == "darwin"
|
||||||
pyobjc-framework-WebKit>=10.0 ; sys_platform == "darwin"
|
pyobjc-framework-WebKit>=10.0 ; sys_platform == "darwin"
|
||||||
pythonnet>=3.0 ; sys_platform == "win32"
|
pythonnet==3.0.5 ; sys_platform == "win32"
|
||||||
|
clr-loader==0.2.7.post0 ; sys_platform == "win32"
|
||||||
@@ -89,7 +89,13 @@ export async function download(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const name = path.basename(abs);
|
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}"`);
|
res.setHeader("Content-Disposition", `attachment; filename="${name}"`);
|
||||||
fs.createReadStream(abs).pipe(res);
|
fs.createReadStream(abs).pipe(res);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user