Skip to content
robinchoicePublic

About

Versionen für Musik. Ohne Chaos.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Music Hub

Versionsverwaltung für Musikproduktionen. Kein Chaos mehr in Dropbox-Ordnern voller final_v2_WIRKLICH_FINAL.wav.

Was ist das?

Music Hub ist eine kollaborative Plattform für Produzenten, Engineers und Labels. Tracks haben Versionen, Versionen haben Kommentare, und alle Beteiligten sehen was gerade der aktuelle Stand ist — ohne dass jemand eine WAV-Datei per WhatsApp schickt.

Features:

  • Projekte und Tracks mit vollständiger Versionshistorie
  • Wellenform-Ansicht direkt im Browser (WaveSurfer.js)
  • Zeitgestempelte Kommentare auf Versionen
  • Share-Links für externe Reviewer (kein Account nötig)
  • Ablieferung an Kunden mit Korrekturrunden und Bezahlwand (Lightning oder Überweisung direkt ans Studio)
  • Magic-Link-Auth — kein Passwort-Reset-Chaos
  • S3-kompatibler File-Storage (Hetzner Object Storage)

Stack

Layer Tech
Frontend SvelteKit + TypeScript
Backend Hono (Bun)
Datenbank PostgreSQL + Drizzle ORM
Storage S3-kompatibel (Hetzner / MinIO)
Email SMTP (z. B. Proton Mail)
Deploy Coolify, Docker

Monorepo mit Turborepo: apps/web, apps/api, apps/cli, packages/db, packages/shared.

Setup (lokal)

Voraussetzungen: Bun, Docker

# Dependencies
bun install

# Dienste starten (Postgres + MinIO)
docker compose up -d

# .env anlegen
cp .env.example .env

# Dev-Server
bun dev

Web läuft auf http://localhost:5173, API auf http://localhost:3000.

Umgebungsvariablen

Pflicht-Variablen in .env:

DATABASE_URL=postgresql://musichub:musichub@localhost:5435/musichub
# Salt der IP-Hashes in der Hör-Statistik (trotz des Namens), nicht wechseln
MAGIC_LINK_SECRET=<openssl rand -hex 32>
APP_URL=http://localhost:5173

Für E-Mail-Versand (lokal optional, ohne stehen die Magic Links in der Konsole; in Produktion startet die API ohne SMTP_HOST nicht):

SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=noreply@example.com
SMTP_PASS=...
EMAIL_FROM=Music Hub <noreply@example.com>

Datenbankmigrationen

# Schema ändern → Migration generieren
bun db:generate

# Migration anwenden
bun db:migrate

Self-Hosting

Music Hub läuft auf jedem Server mit Docker. Du brauchst:

  • eine Domain, z. B. music.example.com
  • einen S3-kompatiblen Bucket mit HTTPS-Endpunkt, z. B. Hetzner Object Storage, Backblaze B2 oder Cloudflare R2. Browser laden Audio direkt in den Bucket hoch und streamen von dort.
  • einen SMTP-Zugang für die Login-Mails. Ohne SMTP_HOST startet die API nicht, sonst könnte jeder mit Zugriff auf die Logs sich mit den dort stehenden Login-Links als beliebiger Nutzer anmelden.
git clone https://github.com/robinchoice/music-hub.git
cd music-hub
cp .env.production.example .env   # Werte eintragen
docker compose -f docker-compose.prod.yml up -d --build

Die App lauscht dann auf 127.0.0.1:3000. Davor gehört ein Reverse Proxy mit HTTPS, ohne HTTPS funktioniert der Login nicht. Mit Caddy reicht als Caddyfile:

music.example.com {
    reverse_proxy 127.0.0.1:3000
}

Die API migriert die Datenbank beim Start und erlaubt der App-Domain den Zugriff auf den Bucket (CORS). Update: git pull, dann denselben docker compose-Befehl noch einmal. Sichern solltest du das Docker-Volume pgdata und den Bucket.

Deployment

Läuft auf Coolify, gebaut aus robinchoice/music-hub (Branch main).

  • Web (Dockerfile.web): hub.pleasance.org, alte Domain hub.diespaetzles.lol parallel
  • API (Dockerfile.api): intern via API_INTERNAL_URL, führt Migrationen beim Start aus
  • Datenbank: Postgres als eigene Coolify-Ressource

Jeder Push auf main deployt automatisch: GitHub Actions prüft und baut die Images, dann deployt die CI API, Audio-Worker und Web über Coolify. Der Deploy ist erst erfolgreich, wenn /api/v1/health den Commit als revision meldet. Umgebungsvariablen werden in Coolify gepflegt, siehe .env.production.example.

Danach fotografiert der CI-Job screenshots die öffentliche Startseite (Demo mit Beispieldaten) und /login, ohne Anmeldung oder Nutzerdaten. apps/web/screenshots.ts erzeugt Desktop- (1440×900) und Handy-Bilder (390×844, doppelte Dichte) auf Deutsch und Englisch als WebP unter docs/screenshots. Der Bot committet geänderte Bilder mit [skip ci] auf main, damit kein weiterer Deploy startet. Anschließend meldet der Job prototype-deployed mit Repo-Slug und Deploy-Commit an robinchoice/pleasance über den bestehenden Empfänger-Workflow. Dafür nutzt er das bestehende Actions-Secret PLEASANCE_DISPATCH_TOKEN; dessen Token darf nur auf das Empfänger-Repo zugreifen.

Lokal: bun run screenshots [url] in apps/web, ohne URL gegen http://localhost:5173. Nur feste öffentliche Seiten ohne echte Nutzerdaten in PAGES aufnehmen, keine Profile, offenen Tracks oder Token-URLs. Vor dem nächsten Push den Bot-Commit von main holen.

Update-Mails

Mails zu neuen Funktionen an alle, die sie bestellt haben. Ohne Opt-in geht nichts raus.

  • Opt-in: ein nicht vorausgewähltes Kästchen beim Magic Link und bei der Registrierung. Es zählt erst, wenn der Link in der Mail geöffnet wird (Double-Opt-in), users.updates_consent_at hält den Zeitpunkt fest. Im Konto lässt es sich ein- und ausschalten.
  • Abbestellen: Link in jeder Update-Mail (/abbestellen?liste=updates) und One-Click im Mailprogramm (List-Unsubscribe), beides mit users.mail_token wie bei den Versions-Mails, aber unabhängig von deren Schalter.
  • Entwürfe: Agenten legen sie als updates/<slug>.md an, je Sprache # de: Titel bzw. # en: Title, darunter - Feature je Zeile, optional ![](bild.png) aus demselben Ordner. Die API übernimmt jede Datei beim Start einmal als Entwurf, spätere Änderungen an der Datei nicht mehr. Der API-Test prüft, dass alle Dateien lesbar sind.
  • Senden: nur Admins aus ADMIN_EMAILS unter /admin/updates, mit Vorschau und Testmail an sich selbst je Sprache. Gesendet wird erst mit Titel und Feature auf Deutsch und Englisch, jede Person bekommt die Sprache ihres Kontos (users.locale). Updates von vor Englisch (ohne content.en) lesen alle auf Deutsch. Danach lässt sich das Update nicht mehr ändern.
  • Versand: die API, eine Mail alle UPDATE_MAIL_INTERVAL_MS (Standard 6000) über alle Instanzen, mit drei Versuchen pro Empfänger. Die Logs nennen keine Adressen.

Ablieferung mit Bezahlwand

Ein Studio hängt an seinen Link zum Reinhören für ein Projekt (/album/<token>) einen Auftrag: Kunde, Preis je Song, inklusive Korrekturrunden (Standard 2) und Preis jeder weiteren Runde. Im Projekt unter „Auftrag“, nur für Owner, oder per CLI musichub deliveries.

  • Kunde: hört ohne Konto die MP3-Vorschau, kommentiert mit Zeitstempel und gibt je Song die gehörte Version frei oder fordert Änderungen an. Jede Anforderung ist eine Runde für diese Version, das Studio lädt danach eine neue hoch. Über die freien Runden hinaus muss der Kunde die Zusatzrunde ausdrücklich bestätigen („Zahlungspflichtig beauftragen · 40 €“), sie kommt auf die Schlussrechnung. Das Studio kann einem Song Runden schenken.
  • Bezahlen: erst wenn alle Songs freigegeben sind, die Summe aus Songpreisen und Zusatzrunden, jede zum Preis, den der Kunde bestätigt hat. Schon eingegangene Zahlungen werden angerechnet, ein Auftrag ohne Betrag ist mit der letzten Freigabe bezahlt. Danach lädt der Kunde je Song die WAV der freigegebenen Version und, wenn eingestellt, die Spuren als ZIP. Vorher verlässt kein Original den Server.
  • Kommt Geld für eine ersetzte oder abgelaufene Zahlung, zählt es trotzdem: Lightning-Rechnungen fragt die API noch zehn Minuten nach Ablauf bei der Wallet ab, jede andere Zahlung bestätigt das Studio im Auftrag von Hand. Zu viel Bezahltes zahlt das Studio selbst zurück.
  • Geld läuft nie über Music Hub. Lightning: Rechnung aus der eigenen Wallet des Studios über Nostr Wallet Connect, nur mit Verbindungen, die empfangen, aber nicht zahlen dürfen; Kurs EUR→sats von mempool.space. Überweisung: an die IBAN des Studios mit GiroCode (EPC-QR), der Kunde meldet „Ich habe überwiesen“, das Studio bestätigt den Eingang. Zahlungswege richtet jedes Konto unter „Konto“ › „Zahlungswege“ ein.
  • Tarif: Aufträge anlegen nur mit dem Tarif Studio (users.plan = 'solo', optional bis plan_until) oder als Admin, siehe „Tarife“.
  • Server: PAYMENT_SECRETS_KEY (32 Bytes base64, openssl rand -base64 32) verschlüsselt die Wallet-Verbindungen. Ohne ihn gibt es nur Überweisung. Ein neuer Key macht gespeicherte Verbindungen unlesbar, die Studios müssen sie neu eintragen.

Tarife

Gratis, Pro (9 € im Monat, 90 € im Jahr, 100 GB) und Studio (24 € bzw. 240 €, 500 GB, dazu Aufträge). Preise und Grenzen stehen in packages/shared/src/constants/plans.ts, die Abrechnung in apps/api/src/lib/billing.ts, die Karte „Tarif“ im Konto in PlanSettings.svelte, in der CLI musichub plan.

  • Regel „bezahlt bis“: Jede Zahlung schiebt nur users.plan_until, egal woher das Geld kommt. Ein laufender Tarif hebt das Speicherlimit an, nie darunter (greatest aus eigenem storage_limit und Tarif). Endet er, bleiben alle Dateien, nur neue Uploads über der eigenen Grenze gehen nicht mehr. Zahlende Konten zählen nicht gegen MAX_USERS.
  • Stripe (Karte, PayPal, SEPA): Checkout als Abo mit dem Preis aus PLAN_PRICES, im Dashboard muss nichts angelegt werden. Der Webhook invoice.paid setzt plan_until aufs Ende der bezahlten Periode plus 3 Tage, customer.subscription.deleted vergisst nur das Abo. Kündigen über das Kundenportal von Stripe („Abo kündigen oder verwalten“).
  • Lightning: eine Rechnung unseres BTCPay Servers für einen Monat oder ein Jahr im Voraus, ohne Abo. Bezahlt schiebt sie plan_until um die Periode, bei laufendem Tarif ab dessen Ende. Der Webhook sagt nur, welche Rechnung; den Status fragt die API bei BTCPay ab, ebenso beim Öffnen des Kontos für offene Rechnungen des letzten Tages. Spät eingegangenes Geld zählt.
  • Während ein Tarif läuft, gibt es nur „mit Lightning verlängern“ desselben Tarifs. Wechsel und Kauf neben einem Abo oder einem Tarif ohne Ende (vom Admin) gehen nicht, das klärt der Admin im Lagebild. Werden doch zwei Rechnungen für verschiedene Tarife bezahlt, läuft der größere bis zum späteren Ende.
  • Verbraucherrecht: Kaufen geht nur mit dem Häkchen „Ich verlange ausdrücklich, dass mein Tarif sofort beginnt …“ (PLAN_CONSENT, gespeichert als consented_at). Jeder neue Vertrag (Lightning-Zahlung oder erste Rechnung eines Abos, nicht die Verlängerung) bekommt eine Bestätigungsmail mit AGB und Widerrufsbelehrung. „Verträge hier kündigen“ (§ 312k BGB) kündigt das Abo zum Ende der bezahlten Zeit, ein Jahresabo nach seiner ersten Verlängerung spätestens in einem Monat. „Widerrufen“ (§ 356a BGB) steht 14 Tage an der Zahlung und beendet, was sie gekauft hat. Erstattungen macht ein Admin von Hand, die API mailt ADMIN_EMAILS Betrag und Rechnung.
  • Zahlungen stehen in plan_payments (eine Zeile je Rechnung, external_id eindeutig, doppelte Webhooks ändern nichts) und bleiben bei gelöschtem Konto für die Buchhaltung.
  • Ohne die Variablen gibt es nichts zu kaufen, die Karte „Tarif“ erscheint dann nur bei laufendem Tarif. Einschalten:
    1. Vorher: Das Bucket-Backup darf nicht mehr über das Volume von VPS 1 laufen (AGENTS.md, „Fallen“). Robin gibt die AGB, die Widerrufsbelehrung und den Datenschutz-Abschnitt „13. Tarife und Zahlungen“ frei (packages/shared/src/terms.ts, Entwurf nicht anwaltlich geprüft). Sie gehen mit dem ersten gesetzten Zahlungsweg live: /agb, /agb.md, der Abschnitt auf /datenschutz und die Bestätigungsmail.
    2. Stripe: STRIPE_SECRET_KEY (geheimer Key, sk_live_…) und einen Webhook auf https://hub.pleasance.org/api/v1/billing/stripe mit den Ereignissen invoice.paid und customer.subscription.deleted, dessen Signatur-Secret als STRIPE_WEBHOOK_SECRET. Kundenportal im Dashboard aktivieren (Kündigen erlauben).
    3. BTCPay: BTCPAY_URL (von VPS 1 aus erreichbar), BTCPAY_STORE_ID, einen Greenfield-API-Key mit btcpay.store.cancreateinvoice und btcpay.store.canviewinvoices als BTCPAY_API_KEY, einen Webhook auf https://hub.pleasance.org/api/v1/billing/btcpay (Ereignis „Invoice settled“, gern alle Rechnungsereignisse) mit seinem Secret als BTCPAY_WEBHOOK_SECRET.
    4. Alle Werte nur in Coolify bei der API (secret-to-coolify), nicht beim Worker.
    5. In Stripe unter Rechnungen und Belegen den Hinweis „Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.“ hinterlegen.

Für Agenten

Fremde Agenten lesen die öffentlichen Inhalte ohne HTML, nach llms.txt. Quelle ist publicPages in apps/api/src/lib/public-pages.ts, die Routen stehen in apps/api/src/routes/agents.ts. Der Web-Server leitet die Pfade an die API weiter.

  • /llms.txt: Einstieg mit Name, Tagline und allen öffentlichen Seiten als Links auf ihr Markdown.
  • /llms-full.txt: alle Seiten in einer Datei.
  • <seite>.md: jede Seite als Markdown, die Startseite als /index.md. Fragt ein Agent mit Accept: text/markdown nach einer Seite, die es als Markdown gibt, bekommt er unter derselben URL gleich das Markdown, alle anderen das HTML.
  • Sprache: ?lang=de|en, sonst Accept-Language, sonst Deutsch. Links behalten ein ?lang=.
  • Nur Startseite, Datenschutz und Impressum, nichts aus Projekten, offenen Tracks oder Profilen.
  • Ausprobieren: curl localhost:5173/llms.txt bei laufendem bun run dev.

Secret-Scan

Das Repo ist öffentlich, Code, Historie und CI-Logs. Der Workflow .github/workflows/secrets.yml läuft bei jedem Push auf jedem Branch und bei PRs aus Forks, ohne paths-ignore, und scannt mit gitleaks (Docker-Image, Version per Digest fest) die neuen Commits. Ein Fund macht den Lauf rot, der Wert steht dank --redact nicht im Log.

  • Regeln: die Standardregeln von gitleaks über .gitleaks.toml, bisher ohne Ausnahmen.
  • Fehlalarm: den Fingerprint aus dem Log als Zeile in .gitleaksignore eintragen.
  • Echter Fund: Der Scan läuft erst nach dem Push, das Secret ist also schon öffentlich. Sofort rotieren, Entfernen aus dem Code oder der Historie reicht nicht.

Login in der Savor-Vorschau

Agenten zeigen Änderungen hinter dem Login in der Savor-Vorschau mit dem Testkonto admin@pleasance.org. Dafür gibt bun apps/api/src/login-link.ts <email> im API-Container einen einmaligen Login-Link aus, 15 Minuten gültig wie ein gemailter, den der Agent per browser_navigate öffnet. Browser-Sessions in Benutzung verlängern sich höchstens einmal am Tag auf 365 Tage, also bleibt die Vorschau angemeldet. Auch Sessions des Plugins gelten 365 Tage und verlängern sich gleitend. Für die CLI nimmt der Agent denselben Link per Pipe, siehe „CLI“.

CLI

Music Hub hat eine eigene Befehlszeile in apps/cli, damit Agenten, Skripte und Tests die App über Befehle bedienen statt über die UI. Sie spricht nur mit der HTTP-API wie Web und Plugin, nie mit der Datenbank. So gelten dieselben Rechte, Rate-Limits und MAX_USERS. Die Argumente liest parseArgs aus node:util, ohne weitere Abhängigkeit.

  • Bauen: bun run build in apps/cli (auch Teil von bun --bun run build) kompiliert apps/cli/build/musichub, eine einzelne Datei mit Bun darin (gut 100 MB). Die Adresse ist eingebacken, https://hub.pleasance.org, oder CLI_URL aus der Umgebung beim Bauen. --url wählt eine andere, etwa http://localhost:5173 gegen bun run dev, danach gilt die Adresse des letzten Logins. Ohne Bauen: bun apps/cli/src/main.ts <befehl>.
  • Anmelden: musichub login nutzt den Geräte-Login des Plugins (routes/device.ts): Die CLI zeigt einen Link auf /device mit Code, wer im Browser angemeldet ist, bestätigt ihn. Konten entstehen dabei nicht. musichub login --link <link> löst einen Login-Link ein, --link - liest ihn von stdin (/auth/verify mit cookie: false gibt den Token zurück). Danach liegt der Token dieses Geräts mit 0600 in $XDG_CONFIG_HOME/musichub/session.json (sonst ~/.config, unter macOS ~/Library/Application Support, unter Windows %APPDATA%), zusammen mit der Adresse, an andere Adressen geht er nie. Er ist eine gewöhnliche Session als Bearer mit client = 'cli': gehasht in sessions, 365 Tage, verlängert sich bei Nutzung, jedes Gerät hat seine eigene. whoami zeigt das Konto, logout widerruft den Token auf dem Server und löscht die Datei, ein neuer Login widerruft den alten.
  • Befehle: projects [--archived] listet die eigenen Projekte mit Rollen und Anzahl der Tracks. board [--project <Name oder ID>] listet die Tracks nach Produktionsphase wie das Board im Web, board move <Phase> --project <Name oder ID> --track <Name oder ID> schiebt einen in eine andere Phase (sketch, recording, mixing, mastering, final, released; nur wer hochladen darf). share --project <Name oder ID> gibt den eigenen Link zum Reinhören für ein ganzes Projekt aus und legt ihn beim ersten Mal an. deliveries verwaltet Aufträge auf diesem Link, nur als Besitzer des Projekts: list [--project <Name oder ID>] listet sie mit Status, Freigaben und Summe, show <ID> zeigt Songs, Runden und Zahlungen. create --project <Name oder ID> --client <Name> --price <Euro> legt einen an (braucht den Tarif Studio), der Preis gilt je Song; dazu --track <Name> (mehrfach, sonst alle Songs), --email, --reference, --rounds (inklusive Runden je Song, Standard 2), --extra <Euro> (Zusatzrunde) und --no-stems. confirm-payment <ID> bestätigt den Eingang der offenen Überweisung, cancel <ID> storniert einen offenen Auftrag. plan zeigt Tarif, Speicher und Zahlungen, gekauft wird im Web. Beträge nimmt die CLI in Euro wie 40 oder 12,50.
  • Ausgabe: Text für Menschen, mit --json genau ein JSON-Wert auf stdout, auch Fehler als { "error": "…" } und die Hilfe als Liste der Befehle. Exit-Codes: 0 erledigt, 1 Fehler (auch Absagen der API wie ein volles Music Hub), 2 falscher Aufruf, 3 nicht angemeldet. Hilfe und Meldungen stehen auf Englisch in apps/cli/src/texts.ts, auch die API antwortet der CLI auf Englisch.
  • Eigene Befehle: Datei in apps/cli/src/commands nach dem Beispiel projects.ts und in commands in src/cli.ts eintragen. Ein Befehl nennt Hilfezeilen, Optionen und run. Das ruft über ctx.api die API auf, liest die Antwort mit zod und gibt { data, text } zurück, data für --json. currentUser(ctx) verlangt den Login, Fehler enden als Failure mit einem Code aus EXIT. Die Tests in src/cli.test.ts rufen die CLI im Prozess gegen die echten API-Routen mit der Testdatenbank auf.
  • Agenten: nur mit dem Testkonto admin@pleasance.org, nie mit Kundenkonten. Den Token holen sie über den Login-Link aus dem Kontor, per Pipe, so steht der Link nicht in ihrem Kontext: login-link musichub | apps/cli/build/musichub login --link -. Danach bleibt die CLI auf diesem Rechner angemeldet.
  • Verteilen: Für andere Plattformen bun run build --target=bun-darwin-arm64 in apps/cli (ebenso bun-linux-x64, bun-linux-arm64, bun-windows-x64), das überschreibt build/musichub. Die CI baut die Datei bei jedem Lauf zur Probe, veröffentlicht sie aber nicht.

Projektstruktur

music-hub/
├── apps/
│   ├── api/          # Hono API (Bun)
│   ├── cli/          # Befehlszeile gegen die API, siehe „CLI“
│   └── web/          # SvelteKit Frontend
├── packages/
│   ├── db/           # Drizzle Schema + Migrationen
│   └── shared/       # Zod-Schemas, gemeinsame Typen
├── Dockerfile.api
├── Dockerfile.web
└── docker-compose.yml

Tests und Prozessende

bun run check && bun run test && bun --bun run build prüft Typen, die any-Regeln von Biome, API, CLI und Browser und baut Web und CLI. Die API- und Browser-Suite laufen nacheinander, damit Browser-Build und API-Tests auf kleinen Rechnern nicht konkurrieren. Jede Suite bekommt eine eigene leere Datenbank auf dem Server aus DATABASE_URL; der Nutzer braucht CREATEDB (lokaler Compose- und CI-Nutzer haben es). packages/db/test.ts entfernt nur diese temporäre Datenbank nach Erfolg, Fehler oder SIGINT/SIGTERM. Direkte Aufrufe von bun test oder playwright test umgehen die Isolation. Playwright nutzt 31100/31101 unter dem ephemeren Linux-Portbereich; parallele Worktrees wählen andere Ports unter 32768 per TEST_API_PORT und TEST_WEB_PORT.

SIGTERM/SIGINT lässt die API laufende Anfragen und Mailjobs beenden, schließt SSE und danach Postgres. Datei-Uploads laufen direkt nach S3; ihre Bestätigung kann der Client nach dem Neustart senden. Der Worker stoppt ffmpeg, bevor er den Job zur Wiederaufnahme freigibt. Compose gewährt beiden 60 Sekunden. Die Prozesse prüfen formatierte Env-Werte beim Start mit zod; Fehler nennen nur die Feldnamen.

About

Versionen für Musik. Ohne Chaos.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages