docs: rewrite the deploy documentation and fix the stale references
The root README was a design handoff for a prototype that is not in this repo, web-player/README.md was the sv template, and deploy/README.md only covered development. Replace all three and document the production deploy: its prerequisites, what up.sh does and why nginx sits in front. The docs under docs/ still used the pre-monorepo project paths, Ardalis.Result instead of ErrorOr and the player.html test console that no longer exists.
This commit is contained in:
@@ -1,75 +1,47 @@
|
|||||||
# Handoff: Musify (App prototype)
|
# Musify
|
||||||
|
|
||||||
## Overview
|
Music streaming platform: upload your own tracks (or import them from YouTube),
|
||||||
Musify is a music streaming app: no ads, upload your own files, import from other platforms, adaptive-bitrate HLS streaming, offline downloads, and user playlists. This bundle covers the **app prototype** only (`Musify.dc.html`) — the in-app experience (sidebar nav, home, search, library, upload, import, downloads, playlists, and a persistent bottom player). The marketing landing page is a separate deliverable and is not included here.
|
process them asynchronously (audio transcoded to `.m4a`, thumbnails generated)
|
||||||
|
and stream them without the bytes ever going through the API.
|
||||||
|
|
||||||
## About the Design File
|
```
|
||||||
`Musify.dc.html` is a **design reference built in HTML** — an interactive prototype showing the intended look, layout, and behavior. It is **not production code to copy directly**. The task is to **recreate this design in your real codebase** using your framework's component model and state management (stores, hooks, etc.) and your project's existing styling approach — not to embed the HTML as-is.
|
backend/ .NET 10 — Domain / Application / Infrastructure / Api / Worker / StreamingGateway
|
||||||
|
web-player/ SvelteKit 2 + Svelte 5 + Tailwind 4 web client
|
||||||
|
deploy/ Docker Compose stacks and the scripts that bootstrap them
|
||||||
|
docs/ What each piece does and why (in Spanish)
|
||||||
|
```
|
||||||
|
|
||||||
## Fidelity
|
## Run it
|
||||||
**High-fidelity (hifi)**: colors, typography, spacing, and interactions are final-intent. Recreate pixel-close; adapt only where your framework's conventions differ (e.g. CSS scoping, component boundaries, routing).
|
|
||||||
|
|
||||||
## Design System
|
Development (Windows, Docker Desktop):
|
||||||
- **Background**: near-black `#08080a` throughout (not pure gray — avoid `#111`/`#222` grays; use this near-black plus translucent white overlays for elevation, e.g. `rgba(255,255,255,0.035)`–`rgba(255,255,255,0.09)`).
|
|
||||||
- **Accent color**: **dynamic** — derived from the currently-playing track's "hue" (`oklch(72% 0.17 <hue>)`), so the whole UI (nav highlight, progress bar, buttons, profile avatar) subtly recolors per song. There is no fixed brand accent in the app; drive this from real track metadata/album art in production (e.g. extracted dominant color), or fall back to a fixed brand purple if per-track theming isn't desired.
|
|
||||||
- **Typography**: headings in **Sora** (600–900 weight), body/UI text in **Manrope** (400–700 weight). Both loaded from Google Fonts.
|
|
||||||
- **Text color**: primary text `#f5f4f2`; secondary/subtitle text `rgba(245,244,242,0.7–0.85)` (kept fairly bright, not dimmed gray); tertiary/meta text down to `~0.4–0.55`.
|
|
||||||
- **Corners**: generous radii — 6–14px on small controls/rows, 100px (pill) on avatars/buttons/badges.
|
|
||||||
- **Cover art / track thumbnails**: gradient placeholders (`linear-gradient(135deg, oklch(70% 0.15 hue), oklch(36% 0.13 hue+28))`) with a diagonal repeating-stripe overlay — stand-ins for real album art (network image).
|
|
||||||
- **Icons**: all hand-built from divs (no icon font/SVG library) — simple geometric shapes (house, magnifying glass, bars, triangles, circles). Recreate as inline SVG or an icon library for maintainability.
|
|
||||||
- **Layout philosophy**: content areas fill the available width (search results, library list, playlist grids, "acceso rápido" grid all stretch to 100%/auto-fill rather than being capped at a narrow max-width) — avoid re-introducing narrow `max-width` caps when porting.
|
|
||||||
|
|
||||||
## Layout
|
```powershell
|
||||||
CSS grid, `grid-template-columns: 264px 1fr`, `grid-template-rows: 1fr 92px`, `grid-template-areas: 'sidebar main' 'player player'`, fixed to viewport (`position:fixed; inset:0`).
|
./deploy/up.ps1 # infrastructure; the apps run from the IDE
|
||||||
|
./deploy/up.ps1 -Apps # everything in Docker
|
||||||
|
```
|
||||||
|
|
||||||
### Sidebar (264px, left)
|
Production, on a server with a domain:
|
||||||
- Wordmark "Musify" (Sora 800, 20px) top-left — no icon/logo mark, text only.
|
|
||||||
- Nav list: Inicio (house icon), Buscar (magnifying-glass icon), Tu biblioteca (bars icon), Playlists (list icon); then a divider; then Subir música, Importar, Descargas. Active item: `rgba(255,255,255,0.08)` background pill, icon/text tinted with the current accent color; inactive: `rgba(245,244,242,0.5–0.62)`.
|
|
||||||
- Footer: profile row — circular avatar (gradient, offset hue from current track) + name "Alex Rivera" + "@alexrivera" handle, in a subtle rounded card, clickable (wire to a real account menu).
|
|
||||||
|
|
||||||
### Main content (right of sidebar, scrollable)
|
```sh
|
||||||
Seven views, toggled by sidebar nav (single active view at a time — replace with real routing):
|
./deploy/up.sh # fill in deploy/.env.prod and the TLS certs when it asks
|
||||||
|
```
|
||||||
|
|
||||||
1. **Inicio (Home)** — blurred color-wash hero (current track's gradient), greeting + headline "Tu música. Sin límites."; "Acceso rápido" grid (auto-fill, min 260px per card) of 6 track rows; "Recomendado para ti" horizontal scroll rail of 168×168 cards with a hidden-but-functional scrollbar.
|
Both are idempotent and print what they did. See
|
||||||
2. **Buscar (Search)** — full-width search input filtering a track list live by title/artist; empty state message when no match. List and input both stretch to 100% width.
|
[deploy/README.md](deploy/README.md) for the details, the port map and the
|
||||||
3. **Tu biblioteca (Library)** — full-width flat list of all tracks (built-in + uploaded), "Subida"/"Importada" source badges where applicable.
|
production prerequisites.
|
||||||
4. **Subir música (Upload)** — dashed drop-zone (click simulates an upload adding a fake track), list of uploaded tracks below.
|
|
||||||
5. **Importar (Import)** — 3 cards for external sources (cloud storage, external streaming account, other device), each with a "Conectar"/"Conectado" toggle pill.
|
|
||||||
6. **Descargas (Downloads)** — storage usage bar (used/2GB), list of tracks marked offline with per-track download progress (0–100%, simulated) and a circular toggle button.
|
|
||||||
7. **Playlists** — grid of playlist cards (2×2 color-mosaic thumbnail from track hues + name + track count) plus a "create playlist" input+button. Clicking a card opens **Playlist Detail**: editable title (inline text input), 2×2 mosaic art, track list with per-track remove (×), and an "Añadir canciones" list of tracks not yet in the playlist (click to add).
|
|
||||||
|
|
||||||
### Player bar (bottom, full width, 92px)
|
## Components
|
||||||
Same background treatment as sidebar. Fluid three-column layout (`minmax(200px,1fr) minmax(0,3fr) minmax(200px,1fr)`) so the transport/seek area grows on wide screens:
|
|
||||||
- Left: current track art (52px) + title/artist.
|
|
||||||
- Center: transport controls (prev/play-pause/next, hand-drawn triangles) and a seek bar — click-to-seek with a smooth animated fill (`transition: width 0.6s cubic-bezier(0.22,0.61,0.36,1)`), current/total time labels, now stretching up to 900px wide.
|
|
||||||
- Right: a thin custom volume slider styled to match the seek bar (not the default OS slider) — track is a gradient split at the volume %, small circular thumb via `::-webkit-slider-thumb`/`::-moz-range-thumb`.
|
|
||||||
|
|
||||||
## Interactions & Behavior
|
| Service | Port (dev) | Role |
|
||||||
- **View switching**: clicking a sidebar item sets `state.view` and conditionally renders one of the 7 main-content sections; no real routing — replace with SvelteKit routes / React Router / etc.
|
|---|---|---|
|
||||||
- **Playback simulation**: a 300ms interval advances `progress` when `playing`; on reaching track duration it auto-advances to the next track and loops. Clicking a track toggles play/pause if it's already current, else switches to it and starts playing. Replace with a real `<audio>`/HLS.js player.
|
| `Musify.Api` | 5111 | REST API, issues presigned upload URLs and stream tickets |
|
||||||
- **Seek**: click position on the progress bar computes a fraction of its width and sets `progress` to that fraction × duration; the fill animates smoothly rather than jumping.
|
| `Musify.StreamingGateway` | 8081 | YARP proxy that validates the stream ticket and serves the audio |
|
||||||
- **Volume**: native `<input type="range">`, custom-styled; value drives a linear-gradient split on the track itself.
|
| `Musify.Worker` | — | MassTransit consumers: transcoding, thumbnails, YouTube downloads |
|
||||||
- **Offline downloads**: clicking the toggle starts a simulated download (progress increments ~14% every 300ms until 100%/"done"); clicking again while done removes it. Storage-used bar is derived from count of "done" downloads × ~0.18GB (placeholder math — replace with real file sizes).
|
| `web-player` | 5173 / 3000 | Web client (SvelteKit, server-side OIDC session) |
|
||||||
- **Upload**: clicking the drop-zone appends a fake track to `state.uploads` (no real file picker/parsing) — wire up a real file input + upload pipeline.
|
| Zitadel | 8080 | Identity (OIDC/OAuth2) |
|
||||||
- **Import**: toggling a service just flips a boolean per source — no real OAuth/integration.
|
| PostgreSQL / RabbitMQ / SeaweedFS / Jaeger | 59000 / 5672 / 8333 / 16686 | Metadata, messaging, object storage, traces |
|
||||||
- **Playlists**: create (name input + button) → adds a playlist and opens it; rename via inline text input on the detail header; delete button removes it and returns to the list; add/remove track buttons mutate `trackIds` on the playlist.
|
|
||||||
|
|
||||||
## State Management (what to model)
|
More in [docs/](docs/README.md): [architecture](docs/architecture.md),
|
||||||
- `currentTrackId`, `playing`, `progress` (seconds), `volume` (0–100), `network` (currently cycled manually — in production drive from actual measured/adaptive HLS bitrate).
|
[authentication](docs/authentication.md), [storage](docs/storage.md),
|
||||||
- `view` (active section) — replace with real routing.
|
[streaming](docs/streaming.md), [media processing](docs/media-processing.md),
|
||||||
- `tracks` (built-in + uploaded, merged) — replace with real catalog/uploaded-file data from your backend.
|
[development](docs/development.md).
|
||||||
- `offline: Record<trackId, {status, percent}>` — replace simulated progress with real download-manager state.
|
|
||||||
- `playlists: {id, name, trackIds}[]`, `activePlaylistId`.
|
|
||||||
|
|
||||||
## Design Tokens
|
|
||||||
- **Colors**: background `#08080a`; text `#f5f4f2`; text-secondary `rgba(245,244,242,0.7–0.85)`; text-tertiary `rgba(245,244,242,0.4–0.55)`; surfaces `rgba(255,255,255,0.035–0.09)`. Accent computed at runtime: `oklch(72% 0.17 <trackHue>)`.
|
|
||||||
- **Typography**: Sora 600/700/800/900 for headings; Manrope 400/500/600/700 for body/UI.
|
|
||||||
- **Radii**: 6–14px (small UI), 100px (pills/avatars).
|
|
||||||
- **Spacing**: main content padding `44px 48px`; sidebar padding `26px 18px`.
|
|
||||||
|
|
||||||
## Assets
|
|
||||||
No external images — all art (track covers, avatar) is CSS-gradient placeholders. Replace with real album art / user avatar images before shipping. Icons are hand-built divs; consider swapping for an SVG icon set in your production build.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
- `Musify.dc.html` — app prototype (self-contained; open directly in a browser).
|
|
||||||
- `support.js` — runtime helper used only by the HTML prototype format; **not needed** in your production port, it has no equivalent purpose in a real app.
|
|
||||||
|
|||||||
+120
-154
@@ -1,192 +1,158 @@
|
|||||||
# Development deploy
|
# Deploy
|
||||||
|
|
||||||
Everything the backend needs for development, in one place. A single
|
Everything needed to run Musify, in development and in production, from the
|
||||||
`docker-compose.yml` brings up **PostgreSQL, Zitadel, RabbitMQ, SeaweedFS and
|
same set of compose files.
|
||||||
Jaeger**; the .NET apps run from the IDE (or, optionally, in Docker too).
|
|
||||||
|
|
||||||
## Quick start
|
| File | Role |
|
||||||
|
|---|---|
|
||||||
|
| `compose.yml` | Every service. No published ports, no domains, no environment-specific values. |
|
||||||
|
| `compose.dev.yml` | Development overlay: publishes the ports to the host, adds pgAdmin. |
|
||||||
|
| `compose.prod.yml` | Production overlay: adds the nginx edge and the mounted secrets. |
|
||||||
|
| `.env` / `.env.prod` | The values compose interpolates. Not versioned; templates in `.env.example` / `.env.prod.example`. |
|
||||||
|
| `up.ps1` | Development orchestrator (Windows). |
|
||||||
|
| `up.sh` | Production orchestrator (the server). |
|
||||||
|
|
||||||
|
The four applications (`api`, `worker`, `gateway`, `web-player`) sit behind the
|
||||||
|
`apps` compose profile, so a plain `up` starts infrastructure only — the usual
|
||||||
|
development setup, with the apps running from the IDE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# From the repo root (Musify/)
|
./deploy/up.ps1 # infrastructure only, apps from the IDE
|
||||||
./deploy/up.ps1
|
./deploy/up.ps1 -Apps # build and run the apps in Docker too
|
||||||
```
|
```
|
||||||
|
|
||||||
`up.ps1` does the full bootstrap:
|
`up.ps1` does the whole bootstrap, with no manual steps afterwards:
|
||||||
|
|
||||||
1. Creates `deploy/.env` from `.env.example` if missing.
|
1. Creates `deploy/.env` from `.env.example` if it is missing.
|
||||||
2. Starts the stack (`docker compose up -d`) and waits for Postgres to be healthy.
|
2. Generates the RS256 stream-ticket keys in `deploy/keys/` and the Worker
|
||||||
3. Creates the `webapi-storage` bucket in SeaweedFS (`seaweedfs-init` service).
|
scratch directory `deploy/.tmp/`.
|
||||||
4. Generates the RS256 stream-ticket keys in `D:\weed` if missing.
|
3. Starts the stack and waits for Postgres.
|
||||||
5. Creates the Worker temp directory (`D:\tempsFilesDev`).
|
4. Applies the EF Core migrations.
|
||||||
6. Applies the EF Core migrations to `musify_db`.
|
5. Provisions Zitadel: project, API OIDC app and web player OIDC app, then
|
||||||
7. **Provisions Zitadel automatically** (project + OIDC app with JWT token) and
|
writes the client ids into `deploy/.env`, `Musify.Api` user-secrets and
|
||||||
wires the ClientId into `Musify.Api` user-secrets and `.env`.
|
`web-player/.env`.
|
||||||
|
6. Prints a summary with every URL and credential.
|
||||||
|
|
||||||
When it finishes it prints a summary with URLs and credentials. **No manual steps.**
|
Other switches: `-Tools` (pgAdmin), `-SkipMigrations`, `-Down` (stop, keep
|
||||||
|
data), `-Destroy` (stop and wipe the volumes).
|
||||||
|
|
||||||
### Without the script (infra only)
|
### Ports
|
||||||
|
|
||||||
```powershell
|
| Service | Port | Used by |
|
||||||
docker compose -f deploy/docker-compose.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
The compose file still creates the bucket on its own; run migrations with
|
|
||||||
`dotnet ef database update` (see [../docs/development.md](../docs/development.md)).
|
|
||||||
|
|
||||||
## Services and ports
|
|
||||||
|
|
||||||
| Service | Port(s) | Used for |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| PostgreSQL | `59000` → 5432 | API + Worker (EF Core) and the Zitadel database |
|
| PostgreSQL | `59000` → 5432 | API + Worker (EF Core) and Zitadel |
|
||||||
| Zitadel | `8080` | OIDC/OAuth2 (console at `/ui/console`) |
|
| Zitadel | `8080` | OIDC/OAuth2, console at `/ui/console` |
|
||||||
| RabbitMQ | `5672` / `15672` | MassTransit / management UI |
|
| RabbitMQ | `5672` / `15672` | MassTransit / management UI |
|
||||||
| SeaweedFS S3 | `8333` | S3 (presigned PUT/GET) |
|
| SeaweedFS | `8333` / `8888` / `9333` | S3 / filer (→ gateway) / master |
|
||||||
| SeaweedFS filer | `8888` | serves files → StreamingGateway |
|
| Jaeger | `16686` / `4317` / `4318` | traces UI / OTLP gRPC / OTLP HTTP |
|
||||||
| SeaweedFS master | `9333` | admin/UI |
|
| pgAdmin (`-Tools`) | `5050` | Postgres UI |
|
||||||
| Jaeger | `16686` / `4317` / `4318` | OTLP traces (UI / gRPC / HTTP) |
|
| API, gateway, web player (`-Apps`) | `5111` / `8081` / `3000` | |
|
||||||
|
|
||||||
Ports are published to the host, so apps run locally connect without changing
|
### Running the apps from the IDE
|
||||||
`AppSettings.Development.json` (Postgres, RabbitMQ, S3, OTLP).
|
|
||||||
|
|
||||||
## Authentication (Zitadel) — automatic
|
|
||||||
|
|
||||||
There are **no manual steps**. `up.ps1` runs `zitadel/provision.ps1`, which
|
|
||||||
idempotently:
|
|
||||||
|
|
||||||
1. Uses a **service account** with a **PAT** that Zitadel writes on init
|
|
||||||
(`zitadel/.output/admin-sa.pat`).
|
|
||||||
2. Creates (or reuses) the **project** `Musify` and an **OIDC app** (PKCE,
|
|
||||||
`devMode`, **access token = JWT** — required for `JwtBearer` to validate).
|
|
||||||
3. Captures the generated **ClientId** and writes it to:
|
|
||||||
- **`Musify.Api` user-secrets** (for running the apps locally), and
|
|
||||||
- `deploy/.env` as `AUTH_CLIENT_ID` (for the full-docker path).
|
|
||||||
|
|
||||||
The values stay stable while the Postgres volume persists. After `up.ps1 -Destroy`
|
|
||||||
the next `up.ps1` re-provisions (new ClientId, re-wired).
|
|
||||||
|
|
||||||
- Console: `http://host.docker.internal:8080/ui/console`
|
|
||||||
- Admin login: `admin@zitadel.host.docker.internal` / `Password1!` (see `.env`).
|
|
||||||
- Test login: open Scalar (`http://localhost:5111/scalar/v1`) and authorize.
|
|
||||||
|
|
||||||
### Why `host.docker.internal`
|
|
||||||
|
|
||||||
`ZITADEL_EXTERNAL_DOMAIN=host.docker.internal` forms the token *issuer* and
|
|
||||||
resolves the **same** from the browser, local apps and containers, so token
|
|
||||||
validation works both ways (local and full-docker). Zitadel routes by the `Host`
|
|
||||||
header, so the domain must be identical everywhere. If you only run the apps
|
|
||||||
locally you can use `localhost`. Changing the domain requires a re-init:
|
|
||||||
`up.ps1 -Destroy; up.ps1`.
|
|
||||||
|
|
||||||
## Running the apps
|
|
||||||
|
|
||||||
### Recommended: from the IDE (best for debugging)
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet run --project backend/Musify.Api # :5111
|
dotnet run --project backend/Musify.Api # :5111
|
||||||
dotnet run --project backend/Musify.StreamingGateway # :8081
|
dotnet run --project backend/Musify.StreamingGateway # :8081
|
||||||
dotnet run --project backend/Musify.Worker # background
|
dotnet run --project backend/Musify.Worker # background
|
||||||
|
npm --prefix web-player run dev # :5173
|
||||||
```
|
```
|
||||||
|
|
||||||
Host requirements: **.NET 10 SDK**, **ffmpeg** on PATH (Worker audio transcode).
|
Host requirements: .NET 10 SDK, Node 22, ffmpeg and yt-dlp on PATH (the Worker
|
||||||
|
transcodes and downloads audio), Docker Desktop.
|
||||||
|
|
||||||
### Optional: everything in Docker
|
### Why `host.docker.internal`
|
||||||
|
|
||||||
```powershell
|
`PUBLIC_AUTH_URL` and `S3_PUBLIC_URL` point at `host.docker.internal`, which
|
||||||
docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.apps.yml up -d --build
|
resolves to the same address from the browser, from apps running on the host
|
||||||
|
and from inside the containers. That keeps the OIDC token issuer and the
|
||||||
|
presigned S3 URLs valid whichever way the apps are running. Zitadel routes by
|
||||||
|
the `Host` header, so the domain has to be identical everywhere; changing it
|
||||||
|
requires a re-init (`./up.ps1 -Destroy; ./up.ps1`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
One command on the server, from the repository root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./deploy/up.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Builds and runs `api`, `worker` and `gateway` as containers (the Worker already
|
It refuses to start until the two things it cannot generate are in place:
|
||||||
ships ffmpeg). The stream-ticket keys are mounted from `D:\weed` (configurable via
|
|
||||||
`STREAM_KEYS_DIR` in `.env`). Auth works in this mode too (the API downloads the
|
|
||||||
Zitadel JWKS via `host.docker.internal`).
|
|
||||||
|
|
||||||
> Note: on Linux the apps look for `appsettings*.json` in lowercase. The `Api`/
|
1. **`deploy/.env.prod`** — copied from `.env.prod.example` on the first run.
|
||||||
> `Worker` projects use `AppSettings*.json` (PascalCase), so their Dockerfiles
|
Replace every `example.com` with your domain and every `CHANGE_ME` with a
|
||||||
> create lowercase copies at publish time. On Windows it did not matter
|
random value (`openssl rand -hex 24`; the Zitadel masterkey needs exactly 32
|
||||||
> (case-insensitive).
|
characters). `chmod 600` it.
|
||||||
|
2. **`deploy/nginx/certs/origin.pem` and `origin.key`** — the origin
|
||||||
|
certificate nginx serves. The stack assumes Cloudflare in *Full (strict)*
|
||||||
|
mode in front of it, so a Cloudflare Origin Certificate is enough; any
|
||||||
|
certificate valid for the five hostnames works.
|
||||||
|
|
||||||
## Useful commands
|
DNS: point `example.com`, `www`, `api`, `auth`, `stream` and `s3` at the server.
|
||||||
|
|
||||||
```powershell
|
Then `up.sh` generates the stream keys, starts the infrastructure and the edge,
|
||||||
./deploy/up.ps1 -SkipMigrations # bring up without touching the DB
|
provisions Zitadel (it needs `curl` and `jq` on the server), applies the
|
||||||
./deploy/up.ps1 -Down # stop and remove containers (keep data)
|
migrations and finally builds and starts the applications. It is idempotent —
|
||||||
./deploy/up.ps1 -Destroy # stop and remove containers + volumes (wipe data)
|
run it again to deploy a new version, or `./deploy/up.sh --no-build` to restart
|
||||||
|
without rebuilding.
|
||||||
|
|
||||||
docker compose -f deploy/docker-compose.yml logs -f zitadel
|
Only nginx publishes ports (80/443); everything else is reachable only on the
|
||||||
docker compose -f deploy/docker-compose.yml ps
|
internal compose network.
|
||||||
```
|
|
||||||
|
|
||||||
## Project layout — what each file does
|
### Optional secrets
|
||||||
|
|
||||||
|
`deploy/secrets/` is mounted read-only at `/secrets` in the Worker. Drop a
|
||||||
|
`youtube_cookies.txt` there if you want yt-dlp to use a cookie jar — it is
|
||||||
|
referenced from `YTDLP_ADDITIONAL_ARGUMENTS` in `.env.prod`, which you can
|
||||||
|
empty if you do not have one.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
deploy/
|
deploy/
|
||||||
├─ docker-compose.yml # infrastructure stack (single source of truth)
|
├─ compose.yml # all services, environment-independent
|
||||||
├─ docker-compose.apps.yml # optional overlay: the 3 .NET apps in Docker
|
├─ compose.dev.yml # dev overlay: host ports, pgAdmin
|
||||||
├─ .env # local config (secrets/ports); not versioned
|
├─ compose.prod.yml # prod overlay: nginx edge, secrets
|
||||||
├─ .env.example # config template (versioned)
|
├─ .env.example / .env.prod.example
|
||||||
├─ up.ps1 # orchestrator: up / down / destroy
|
├─ up.ps1 # dev orchestrator
|
||||||
├─ README.md # this file
|
├─ up.sh # prod orchestrator
|
||||||
|
├─ keys/ # RS256 stream-ticket key pair (generated)
|
||||||
|
├─ secrets/ # mounted into the Worker (prod, optional)
|
||||||
|
├─ nginx/
|
||||||
|
│ ├─ templates/default.conf.template # vhosts, rendered with the domain by envsubst
|
||||||
|
│ ├─ snippets/proxy.conf # proxy headers shared by every vhost
|
||||||
|
│ └─ certs/ # origin.pem + origin.key (not versioned)
|
||||||
├─ scripts/
|
├─ scripts/
|
||||||
│ ├─ common.ps1 # shared helpers (Read-DotEnv, Write-Step)
|
│ ├─ common.ps1 / lib.sh # shared helpers
|
||||||
│ ├─ stream-keys.ps1 # generates the RS256 stream-ticket key pair
|
│ ├─ stream-keys.ps1 / .sh # RS256 key pair generation
|
||||||
│ └─ migrate.ps1 # applies EF Core migrations
|
│ └─ migrate.ps1 / .sh # EF Core migrations
|
||||||
├─ seaweedfs/
|
|
||||||
│ └─ s3.conf # SeaweedFS S3 identities/credentials
|
|
||||||
└─ zitadel/
|
└─ zitadel/
|
||||||
├─ provision.ps1 # creates project + OIDC app, wires the ClientId
|
├─ provision.ps1 / .sh # project + OIDC apps, wires the client ids
|
||||||
└─ .output/ # PAT + init secrets written by Zitadel (not versioned)
|
└─ .output/ # PAT written by Zitadel on init (not versioned)
|
||||||
|
|
||||||
backend/
|
|
||||||
├─ Musify.Api.Dockerfile # API image
|
|
||||||
├─ Musify.Worker.Dockerfile # Worker image (includes ffmpeg)
|
|
||||||
├─ Musify.StreamingGateway.Dockerfile # Gateway image
|
|
||||||
└─ .dockerignore
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compose
|
The application images are built from `backend/Musify.*.Dockerfile` (build
|
||||||
|
context `backend/`, so restore can copy the central props and every csproj) and
|
||||||
|
`web-player/Dockerfile`.
|
||||||
|
|
||||||
- **`docker-compose.yml`** — the infra stack (`postgres`, `zitadel`, `rabbitmq`,
|
## Notes
|
||||||
`seaweedfs`, the one-shot `seaweedfs-init` bucket creator, and `jaeger`). This is
|
|
||||||
the single source of truth for services, ports and volumes. Values come from `.env`.
|
|
||||||
- **`docker-compose.apps.yml`** — an optional overlay that adds the `api`, `worker`
|
|
||||||
and `gateway` services. It merges with the infra file (same `musify-dev` project
|
|
||||||
and network) and shares the backend env via YAML anchors. Used to run the whole
|
|
||||||
stack in Docker; for active development prefer running the apps from the IDE.
|
|
||||||
|
|
||||||
### Config
|
- **S3 credentials** are rendered into the SeaweedFS identity file from
|
||||||
|
`S3_ACCESS_KEY` / `S3_SECRET_KEY` when the container starts, so they cannot
|
||||||
- **`.env`** — the actual values used by compose (Postgres/RabbitMQ/Zitadel/S3
|
drift from the ones the apps are given.
|
||||||
credentials, ports, host paths, `AUTH_CLIENT_ID`). Ignored by git (`*.env`).
|
- **Zitadel** runs `start-from-init`, which is idempotent: the same command
|
||||||
- **`.env.example`** — the versioned template. Copy it to `.env` (or let `up.ps1`
|
works on an empty volume and on an existing one.
|
||||||
do it) and adjust.
|
- **`latest` Zitadel images** require the standalone Login UI v2 container,
|
||||||
|
which this stack does not run. The provisioning scripts turn that requirement
|
||||||
### Scripts (PowerShell)
|
off so the built-in `/ui/login` is used.
|
||||||
|
- **`AppSettings*.json` are PascalCase** in `Musify.Api` and `Musify.Worker`;
|
||||||
- **`up.ps1`** — the orchestrator. Parses `.env`, starts the stack, waits for
|
on case-sensitive Linux the host looks for `appsettings*.json`, so their
|
||||||
Postgres, then calls the sub-scripts in order (keys → migrations → provisioning)
|
Dockerfiles create lowercase copies at publish time.
|
||||||
and prints the summary. Also handles `-Down` / `-Destroy` / `-SkipMigrations`.
|
|
||||||
- **`scripts/common.ps1`** — shared helpers dot-sourced by the others: `Read-DotEnv`
|
|
||||||
(parse `.env` into a hashtable) and `Write-Step` (step logging).
|
|
||||||
- **`scripts/stream-keys.ps1`** — generates the RS256 key pair that the API uses to
|
|
||||||
sign stream tickets and the Gateway uses to verify them. No-op if the keys exist.
|
|
||||||
- **`scripts/migrate.ps1`** — sets the connection string in user-secrets and runs
|
|
||||||
`dotnet ef database update` (startup project = `Musify.Infrastructure`).
|
|
||||||
- **`zitadel/provision.ps1`** — reads the service-account PAT, creates/reuses the
|
|
||||||
Zitadel project and OIDC app (JWT token), and wires the ClientId into
|
|
||||||
`Musify.Api` user-secrets and `.env`. Idempotent; can be run standalone.
|
|
||||||
|
|
||||||
### Service configs
|
|
||||||
|
|
||||||
- **`seaweedfs/s3.conf`** — the S3 identities (access/secret keys) SeaweedFS loads.
|
|
||||||
Must match `InfrastructureStorage:*` in the app settings and `S3_*` in `.env`.
|
|
||||||
- **`zitadel/.output/`** — created at runtime; holds the PAT Zitadel writes on init
|
|
||||||
(`admin-sa.pat`). Not versioned; wiped by `up.ps1 -Destroy`.
|
|
||||||
|
|
||||||
### Dockerfiles (in `backend/`)
|
|
||||||
|
|
||||||
- **`Musify.Api.Dockerfile`** / **`Musify.Worker.Dockerfile`** /
|
|
||||||
**`Musify.StreamingGateway.Dockerfile`** — multi-stage builds for each host. The
|
|
||||||
Worker image also installs ffmpeg. Built by the apps overlay; build context is
|
|
||||||
`backend/` so restore can copy the central props and all csproj files.
|
|
||||||
- **`.dockerignore`** — keeps `bin/`, `obj/`, IDE folders and `DesignSettings.json`
|
|
||||||
out of the build context.
|
|
||||||
|
|||||||
+8
-6
@@ -13,12 +13,13 @@ Esta carpeta explica **qué hace cada pieza, su propósito y por qué está dise
|
|||||||
- [streaming.md](streaming.md) — Streaming del audio (`.m4a` por HTTP range) con stream-ticket (RS256) y el reverse proxy (StreamingGateway).
|
- [streaming.md](streaming.md) — Streaming del audio (`.m4a` por HTTP range) con stream-ticket (RS256) y el reverse proxy (StreamingGateway).
|
||||||
- [media-processing.md](media-processing.md) — El worker y los workflows de MassTransit (transcode de audio, miniaturas).
|
- [media-processing.md](media-processing.md) — El worker y los workflows de MassTransit (transcode de audio, miniaturas).
|
||||||
- [development.md](development.md) — Cómo levantar todo en local: contenedores, puertos y comandos.
|
- [development.md](development.md) — Cómo levantar todo en local: contenedores, puertos y comandos.
|
||||||
|
- [../deploy/README.md](../deploy/README.md) — El deploy en detalle: compose, scripts y producción.
|
||||||
|
|
||||||
## Mapa de componentes (alto nivel)
|
## Mapa de componentes (alto nivel)
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────┐
|
||||||
Cliente ───► │ WebApi (:5111) API REST + emite tickets │
|
Cliente ───► │ Musify.Api (:5111) API REST + emite tickets │
|
||||||
└─────────────────────────────────────────────┘
|
└─────────────────────────────────────────────┘
|
||||||
│ EF Core │ presigned (subidas) │ MassTransit (eventos)
|
│ EF Core │ presigned (subidas) │ MassTransit (eventos)
|
||||||
▼ ▼ ▼
|
▼ ▼ ▼
|
||||||
@@ -27,19 +28,20 @@ Esta carpeta explica **qué hace cada pieza, su propósito y por qué está dise
|
|||||||
▲
|
▲
|
||||||
Cliente ───► StreamingGateway (:8081) ┘ sirve audio .m4a (valida ticket, proxy al filer)
|
Cliente ───► StreamingGateway (:8081) ┘ sirve audio .m4a (valida ticket, proxy al filer)
|
||||||
|
|
||||||
Login: Cliente ◄──► Zitadel (:8080, OIDC) → WebApi valida el JWT
|
Login: Cliente ◄──► Zitadel (:8080, OIDC) → Musify.Api valida el JWT
|
||||||
```
|
```
|
||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- **.NET 10**, C#. Minimal APIs (WebApi), Worker Service (Worker), ASP.NET + YARP (StreamingGateway).
|
- **.NET 10**, C#. Minimal APIs (`Musify.Api`), Worker Service (`Musify.Worker`), ASP.NET + YARP (`Musify.StreamingGateway`).
|
||||||
- **CQRS** con [Mediator] (source-generator) y resultados con **Ardalis.Result**.
|
- **CQRS** con [Mediator] (source-generator) y resultados con **ErrorOr**.
|
||||||
- **EF Core 10** + **PostgreSQL** (metadatos).
|
- **EF Core 10** + **PostgreSQL** (metadatos).
|
||||||
- **MassTransit 8** + **RabbitMQ** (procesado asíncrono con routing slips/sagas).
|
- **MassTransit 8** + **RabbitMQ** (procesado asíncrono con routing slips/sagas).
|
||||||
- **SeaweedFS** (almacenamiento de objetos compatible con S3, vía AWSSDK.S3).
|
- **SeaweedFS** (almacenamiento de objetos compatible con S3, vía AWSSDK.S3).
|
||||||
- **ffmpeg** (transcode de audio a AAC/`.m4a`), **SixLabors.ImageSharp** (miniaturas).
|
- **ffmpeg** (transcode de audio a AAC/`.m4a`), **SixLabors.ImageSharp** (miniaturas).
|
||||||
- **Zitadel** (OIDC/OAuth2) para identidad.
|
- **Zitadel** (OIDC/OAuth2) para identidad.
|
||||||
|
|
||||||
## Repositorios git
|
## Estructura del repositorio
|
||||||
|
|
||||||
Cada proyecto de nivel superior es su propio repo git: `MusifyBackend`, `Musify.WebApi`, `Musify.StreamingGateway`, `Musify.Worker`. La raíz del workspace (`docs/`, `deploy/`, `player.html`) no está versionada.
|
Un solo repo: `backend/` (los seis proyectos .NET), `web-player/` (cliente web
|
||||||
|
SvelteKit), `deploy/` (stacks de Docker Compose y scripts) y `docs/`.
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
|
|
||||||
## Principios
|
## Principios
|
||||||
|
|
||||||
- **Clean architecture / capas** en `MusifyBackend`: el dominio y la aplicación no dependen de infraestructura; la infraestructura implementa las interfaces (`Application/Contracts`).
|
- **Clean architecture / capas** en `backend/`: el dominio y la aplicación no dependen de infraestructura; la infraestructura implementa las interfaces (`Application/Contracts`).
|
||||||
- **CQRS**: cada operación es un `Command` o `Query` con su handler (Mediator). Los handlers devuelven `Ardalis.Result` (Ok/NotFound/Unauthorized/Conflict/Invalid…), que la WebApi traduce a códigos HTTP.
|
- **CQRS**: cada operación es un `Command` o `Query` con su handler (Mediator). Los handlers devuelven `Ardalis.Result` (Ok/NotFound/Unauthorized/Conflict/Invalid…), que la API traduce a códigos HTTP.
|
||||||
- **Los bytes pesados no pasan por la API**: subidas con URLs prefirmadas (cliente → S3) y reproducción vía un reverse proxy dedicado (gateway → S3). La WebApi solo mueve metadatos y emite permisos.
|
- **Los bytes pesados no pasan por la API**: subidas con URLs prefirmadas (cliente → S3) y reproducción vía un reverse proxy dedicado (gateway → S3). La API solo mueve metadatos y emite permisos.
|
||||||
- **Procesado asíncrono**: crear una pista no transcodifica en caliente; publica un evento y un worker hace el trabajo pesado con compensación (sagas/routing slips de MassTransit).
|
- **Procesado asíncrono**: crear una pista no transcodifica en caliente; publica un evento y un worker hace el trabajo pesado con compensación (sagas/routing slips de MassTransit).
|
||||||
|
|
||||||
## Componentes
|
## Componentes
|
||||||
|
|
||||||
| Componente | Responsabilidad | Por qué separado |
|
| Componente | Responsabilidad | Por qué separado |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **WebApi** | API REST (usuarios, pistas, playlists), autoriza, emite presigned URLs y stream-tickets | Punto de entrada fino; no procesa medios ni sirve bytes |
|
| **Musify.Api** | API REST (usuarios, pistas, playlists), autoriza, emite presigned URLs y stream-tickets | Punto de entrada fino; no procesa medios ni sirve bytes |
|
||||||
| **MusifyBackend** | Dominio + casos de uso + infraestructura compartida (EF, S3, MassTransit, ffmpeg) | Lógica reutilizada por WebApi y Worker |
|
| **Domain/Application/Infrastructure** | Dominio + casos de uso + infraestructura compartida (EF, S3, MassTransit, ffmpeg) | Lógica reutilizada por la API y el Worker |
|
||||||
| **StreamingGateway** | Reverse proxy (YARP) que valida el ticket y sirve el audio (`.m4a`) desde SeaweedFS | Escala con el tráfico de bytes, independiente de la API; SeaweedFS queda privado detrás |
|
| **StreamingGateway** | Reverse proxy (YARP) que valida el ticket y sirve el audio (`.m4a`) desde SeaweedFS | Escala con el tráfico de bytes, independiente de la API; SeaweedFS queda privado detrás |
|
||||||
| **Worker** | Consume eventos de MassTransit: transcodifica audio a `.m4a` y genera miniaturas | Trabajo CPU/IO intensivo fuera del request HTTP |
|
| **Worker** | Consume eventos de MassTransit: transcodifica audio a `.m4a` y genera miniaturas | Trabajo CPU/IO intensivo fuera del request HTTP |
|
||||||
| **PostgreSQL** | Metadatos (usuarios, pistas, playlists, upload intents) | — |
|
| **PostgreSQL** | Metadatos (usuarios, pistas, playlists, upload intents) | — |
|
||||||
@@ -23,15 +23,15 @@
|
|||||||
## Flujos principales
|
## Flujos principales
|
||||||
|
|
||||||
### 1. Subida de una pista
|
### 1. Subida de una pista
|
||||||
1. Cliente pide `POST /tracks/upload-urls` → la WebApi crea **UploadIntents** (reservas) y devuelve **URLs prefirmadas PUT** (imagen + audio).
|
1. Cliente pide `POST /tracks/upload-urls` → la API crea **UploadIntents** (reservas) y devuelve **URLs prefirmadas PUT** (imagen + audio).
|
||||||
2. Cliente sube los ficheros **directo a SeaweedFS** con esas URLs (no pasan por la API).
|
2. Cliente sube los ficheros **directo a SeaweedFS** con esas URLs (no pasan por la API).
|
||||||
3. Cliente llama `POST /tracks` con los `intentId`s → la WebApi valida los intents, crea el `Track` (+ `UserHasTrack`) y publica `CreateTrackResourcesEvent`.
|
3. Cliente llama `POST /tracks` con los `intentId`s → la API valida los intents, crea el `Track` (+ `UserHasTrack`) y publica `CreateTrackResourcesEvent`.
|
||||||
4. El **Worker** consume el evento: mueve los originales a su sitio, genera miniaturas y transcodifica el audio a `.m4a`, y actualiza el estado del track.
|
4. El **Worker** consume el evento: mueve los originales a su sitio, genera miniaturas y transcodifica el audio a `.m4a`, y actualiza el estado del track.
|
||||||
|
|
||||||
Ver detalle en [media-processing.md](media-processing.md) y [storage.md](storage.md).
|
Ver detalle en [media-processing.md](media-processing.md) y [storage.md](storage.md).
|
||||||
|
|
||||||
### 2. Reproducción (streaming)
|
### 2. Reproducción (streaming)
|
||||||
1. Cliente pide `GET /tracks/{id}/stream` (autenticado) → la WebApi autoriza y devuelve `{ manifestUrl, ticket }` (URL al `.m4a` + ticket RS256 acotado a la carpeta de esa pista).
|
1. Cliente pide `GET /tracks/{id}/stream` (autenticado) → la API autoriza y devuelve `{ manifestUrl, ticket }` (URL al `.m4a` + ticket RS256 acotado a la carpeta de esa pista).
|
||||||
2. El reproductor abre la URL del audio añadiendo `?t=<ticket>` (o cabecera `X-Stream-Ticket`) y descarga por HTTP range.
|
2. El reproductor abre la URL del audio añadiendo `?t=<ticket>` (o cabecera `X-Stream-Ticket`) y descarga por HTTP range.
|
||||||
3. El **StreamingGateway** valida el ticket y reenvía al filer de SeaweedFS; los bytes van storage → cliente.
|
3. El **StreamingGateway** valida el ticket y reenvía al filer de SeaweedFS; los bytes van storage → cliente.
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Ver [streaming.md](streaming.md).
|
|||||||
|
|
||||||
### 3. Autenticación
|
### 3. Autenticación
|
||||||
1. Cliente hace login OIDC contra **Zitadel** y obtiene un access token (JWT).
|
1. Cliente hace login OIDC contra **Zitadel** y obtiene un access token (JWT).
|
||||||
2. La WebApi valida la firma/iss/aud del JWT.
|
2. La Musify.Api valida la firma/iss/aud del JWT.
|
||||||
3. En cada token validado, se **sincroniza** el usuario local (crea/actualiza nombre y foto) — provisioning just-in-time.
|
3. En cada token validado, se **sincroniza** el usuario local (crea/actualiza nombre y foto) — provisioning just-in-time.
|
||||||
|
|
||||||
Ver [authentication.md](authentication.md).
|
Ver [authentication.md](authentication.md).
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Proveedor: Zitadel (OIDC/OAuth2)
|
## Proveedor: Zitadel (OIDC/OAuth2)
|
||||||
|
|
||||||
El login y la emisión de tokens los hace **Zitadel** (no la WebApi). La WebApi es un **resource server**: solo **valida** el access token (JWT) que le llega en `Authorization: Bearer`.
|
El login y la emisión de tokens los hace **Zitadel** (no la API). La API es un **resource server**: solo **valida** el access token (JWT) que le llega en `Authorization: Bearer`.
|
||||||
|
|
||||||
- Config en la sección `Authentication` (`AuthenticationConfiguration`): metadata address, issuer, audience, client id, endpoints de authorize/token, scopes.
|
- Config en la sección `Authentication` (`AuthenticationConfiguration`): metadata address, issuer, audience, client id, endpoints de authorize/token, scopes.
|
||||||
- `JwtBearerOptionsSetup` configura la validación (issuer, audience, claves vía metadata OIDC).
|
- `JwtBearerOptionsSetup` configura la validación (issuer, audience, claves vía metadata OIDC).
|
||||||
|
|||||||
+48
-50
@@ -1,92 +1,90 @@
|
|||||||
# Entorno de desarrollo
|
# Entorno de desarrollo
|
||||||
|
|
||||||
Cómo levantar todo en local y probarlo.
|
Cómo levantar todo en local y probarlo. El detalle completo del deploy (y el de
|
||||||
|
producción) está en [deploy/README.md](../deploy/README.md).
|
||||||
|
|
||||||
## Dependencias (Docker)
|
## Arranque
|
||||||
|
|
||||||
Todo (PostgreSQL, Zitadel, RabbitMQ, SeaweedFS, Jaeger) vive en un solo stack en
|
|
||||||
[`deploy/`](../deploy/README.md). Bootstrap completo (levanta + bucket + claves + migraciones):
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
./deploy/up.ps1
|
./deploy/up.ps1 # infraestructura; las apps se ejecutan desde el IDE
|
||||||
|
./deploy/up.ps1 -Apps # todo en Docker (api, worker, gateway, web player)
|
||||||
```
|
```
|
||||||
|
|
||||||
O solo la infraestructura:
|
`up.ps1` hace el bootstrap completo: crea `deploy/.env`, genera las claves RS256
|
||||||
|
de stream-ticket en `deploy/keys/`, levanta el stack, aplica las migraciones y
|
||||||
|
provisiona Zitadel (proyecto + apps OIDC), dejando los client ids en
|
||||||
|
`deploy/.env`, en los user-secrets de `Musify.Api` y en `web-player/.env`.
|
||||||
|
|
||||||
|
Otros modificadores: `-Tools` (pgAdmin), `-SkipMigrations`, `-Down`, `-Destroy`.
|
||||||
|
|
||||||
|
## Aplicaciones
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
docker compose -f deploy\docker-compose.yml up -d
|
dotnet run --project backend/Musify.Api # :5111
|
||||||
```
|
dotnet run --project backend/Musify.StreamingGateway # :8081
|
||||||
|
dotnet run --project backend/Musify.Worker # background
|
||||||
Config (usuarios/claves/puertos) en `deploy/.env`. Ver [deploy/README.md](../deploy/README.md)
|
npm --prefix web-player run dev # :5173
|
||||||
para el detalle y el paso de configuración de Zitadel (crear la app OIDC y copiar el ClientId).
|
|
||||||
|
|
||||||
## Aplicaciones (.NET)
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet run --project Musify.WebApi\WebApi # :5111
|
|
||||||
dotnet run --project Musify.StreamingGateway\StreamingGateway # :8081
|
|
||||||
dotnet run --project Musify.Worker\Worker # background
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mapa de puertos
|
## Mapa de puertos
|
||||||
|
|
||||||
| Servicio | Puerto |
|
| Servicio | Puerto |
|
||||||
|---|---|
|
|---|---|
|
||||||
| WebApi | 5111 (`/scalar/v1`, `/openapi/v1.json`) |
|
| Musify.Api | 5111 (`/scalar/v1`, `/openapi/v1.json`) |
|
||||||
| StreamingGateway | 8081 (`/health`, `/media/...`) |
|
| Musify.StreamingGateway | 8081 (`/health`, `/media/...`) |
|
||||||
|
| web-player | 5173 (`npm run dev`) o 3000 (contenedor) |
|
||||||
| SeaweedFS S3 / filer / master | 8333 / 8888 / 9333 |
|
| SeaweedFS S3 / filer / master | 8333 / 8888 / 9333 |
|
||||||
| Zitadel | 8080 |
|
| Zitadel | 8080 |
|
||||||
| PostgreSQL | 59000 (→ 5432 en el contenedor) |
|
| PostgreSQL | 59000 (→ 5432 en el contenedor) |
|
||||||
| RabbitMQ AMQP / management | 5672 / 15672 |
|
| RabbitMQ AMQP / management | 5672 / 15672 |
|
||||||
| pgAdmin | 5050 |
|
| Jaeger UI / OTLP | 16686 / 4317 |
|
||||||
|
| pgAdmin (`-Tools`) | 5050 |
|
||||||
|
|
||||||
DB: `musify_db`, usuario `postgres`/`postgres`. Bucket S3: `webapi-storage` (credenciales `admin_access_key`/`admin_secret_key`).
|
DB: `musify_db`, usuario `postgres`/`postgres`. Bucket S3: `webapi-storage`
|
||||||
|
(credenciales `admin_access_key`/`admin_secret_key`). Todos los valores salen de
|
||||||
|
`deploy/.env`.
|
||||||
|
|
||||||
## Migraciones
|
## Migraciones
|
||||||
|
|
||||||
No hay migración automática al arrancar. `deploy/up.ps1` las aplica en el bootstrap;
|
No hay migración automática al arrancar; `up.ps1` las aplica en el bootstrap.
|
||||||
para hacerlo a mano tras cambios de esquema:
|
A mano, tras cambios de esquema:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
dotnet ef database update --project backend\Musify.Infrastructure --startup-project backend\Musify.Infrastructure --context Database
|
dotnet ef database update --project backend/Musify.Infrastructure --startup-project backend/Musify.Infrastructure --context Database
|
||||||
```
|
```
|
||||||
|
|
||||||
La cadena de conexión para `dotnet ef` se lee de `DesignSettings.json` / user-secrets del proyecto Infrastructure
|
`Musify.Infrastructure` es a la vez el proyecto de migraciones y el de arranque:
|
||||||
(`up.ps1` la deja en user-secrets automáticamente).
|
tiene el `IDesignTimeDbContextFactory` y el paquete `EFCore.Design`. La cadena de
|
||||||
|
conexión se lee de sus user-secrets (`up.ps1` la deja puesta).
|
||||||
|
|
||||||
## Stream-tickets (claves RS256)
|
## Stream-tickets (claves RS256)
|
||||||
|
|
||||||
La WebApi firma los stream-tickets con una clave privada y el gateway valida con la pública:
|
La API firma los stream-tickets con la clave privada y el gateway los valida con
|
||||||
|
la pública. `up.ps1` las genera en `deploy/keys/`; a mano:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out stream_private.pem
|
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out deploy/keys/stream_private.pem
|
||||||
openssl rsa -in stream_private.pem -pubout -out stream_public.pem
|
openssl rsa -in deploy/keys/stream_private.pem -pubout -out deploy/keys/stream_public.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
- WebApi: `StreamTicket:PrivateKeyPath` → `stream_private.pem`.
|
- API: `StreamTicket:PrivateKeyPath`.
|
||||||
- Gateway: `StreamTicket:PublicKeyPath` → `stream_public.pem`.
|
- Gateway: `StreamTicket:PublicKeyPath`.
|
||||||
|
|
||||||
## Consola de pruebas: `player.html`
|
|
||||||
|
|
||||||
Un único archivo HTML para probar todo sin front:
|
|
||||||
|
|
||||||
1. Sírvelo o ábrelo directo (el CORS está abierto en Development).
|
|
||||||
2. Pega tu **access token** de Zitadel y la API base (`http://localhost:5111`).
|
|
||||||
3. Permite: crear/actualizar/eliminar playlists, listar canciones, añadir/quitar canciones de una playlist, ver las canciones de una playlist y **reproducir** (pide el ticket y abre el `.m4a` con `?t=<ticket>`).
|
|
||||||
|
|
||||||
> Nota: `player.html` todavía usa dash.js; tras el cambio a `.m4a` debe actualizarse para reproducir el fichero directo (elemento `<audio>` por HTTP range).
|
|
||||||
|
|
||||||
Para obtener un token: login OAuth2 en Scalar (`/scalar/v1`) o cópialo de DevTools.
|
|
||||||
|
|
||||||
## Config (AppSettings)
|
## Config (AppSettings)
|
||||||
|
|
||||||
Convención: `AppSettings.json` = plantilla con valores vacíos/por defecto; `AppSettings.Development.json` = valores reales de dev. Secretos sensibles (claves, connection strings) van idealmente en user-secrets.
|
Convención: `AppSettings.json` es la plantilla con valores vacíos o neutros;
|
||||||
|
`AppSettings.Development.json` tiene los valores reales de dev; en Docker todo
|
||||||
|
se sobreescribe con variables de entorno (`Seccion__Clave`) desde
|
||||||
|
`deploy/compose.yml`. Los valores de `Authentication` los deja el provisioning
|
||||||
|
en user-secrets, no en el repo.
|
||||||
|
|
||||||
Secciones clave: `Authentication` (Zitadel), `Database`, `MassTransit`, `InfrastructureStorage` (S3), `ApplicationStorage` (bucket), `Track`, `PlayList`, `UploadIntent`, `AudioTranscoder`, `Workers`, `StreamGateway`/`StreamTicket`.
|
Secciones clave: `Authentication` (Zitadel), `Database`, `MassTransit`,
|
||||||
|
`InfrastructureStorage` (S3), `ApplicationStorage` (bucket), `Track`, `PlayList`,
|
||||||
|
`UploadIntent`, `AudioTranscoder`, `YtDlp`, `Workers`, `StreamGateway` /
|
||||||
|
`StreamTicket`.
|
||||||
|
|
||||||
## Requisitos del host
|
## Requisitos del host
|
||||||
|
|
||||||
- .NET 10 SDK.
|
- .NET 10 SDK y Node 22.
|
||||||
- **ffmpeg** en el PATH (transcode de audio del Worker).
|
- **ffmpeg** y **yt-dlp** en el PATH (transcode y descarga de audio del Worker).
|
||||||
- Docker Desktop.
|
- Docker Desktop.
|
||||||
- Directorio temporal de trabajo del Worker (`Workers:Routes:TemporaryFilesDirectory`, p. ej. `D:\tempsFilesDev`).
|
|
||||||
|
|||||||
+18
-11
@@ -1,8 +1,9 @@
|
|||||||
# Proyectos
|
# Proyectos
|
||||||
|
|
||||||
Cada carpeta de nivel superior es una solución/repo independiente.
|
Los seis proyectos .NET viven en `backend/` (solución `Musify.slnx`); el cliente
|
||||||
|
web en `web-player/`.
|
||||||
|
|
||||||
## Musify.WebApi/WebApi (`:5111`)
|
## Musify.Api (`:5111`)
|
||||||
|
|
||||||
API REST pública (Minimal APIs, .NET 10). Es la fachada fina del sistema.
|
API REST pública (Minimal APIs, .NET 10). Es la fachada fina del sistema.
|
||||||
|
|
||||||
@@ -11,12 +12,12 @@ API REST pública (Minimal APIs, .NET 10). Es la fachada fina del sistema.
|
|||||||
- **`CurrentUser`**: abstracción que se enlaza como parámetro de endpoint (`IBindableFromHttpContext`) y expone los claims básicos (`Id` como `long?`, `RequiredId`, nombre, etc.) sin tener que leer `ClaimsPrincipal` a mano.
|
- **`CurrentUser`**: abstracción que se enlaza como parámetro de endpoint (`IBindableFromHttpContext`) y expone los claims básicos (`Id` como `long?`, `RequiredId`, nombre, etc.) sin tener que leer `ClaimsPrincipal` a mano.
|
||||||
- **Emite**: presigned URLs de subida (vía `IStorageService`) y **stream-tickets** (vía `IStreamTicketService`, RS256).
|
- **Emite**: presigned URLs de subida (vía `IStorageService`) y **stream-tickets** (vía `IStreamTicketService`, RS256).
|
||||||
- **OpenAPI + Scalar** para documentación/prueba; OAuth2 (PKCE) configurado para login desde Scalar.
|
- **OpenAPI + Scalar** para documentación/prueba; OAuth2 (PKCE) configurado para login desde Scalar.
|
||||||
- **CORS** totalmente abierto **solo en Development** (para el `player.html` y front local).
|
- **CORS** totalmente abierto **solo en Development** (para el front local).
|
||||||
- Traduce `Ardalis.Result` → HTTP en `ResultHttpExtensions`.
|
- Traduce `ErrorOr` → HTTP en `ResultHttpExtensions`.
|
||||||
|
|
||||||
**Propósito/porqué**: separar la cara pública (autorización + metadatos) del procesado y del servido de bytes.
|
**Propósito/porqué**: separar la cara pública (autorización + metadatos) del procesado y del servido de bytes.
|
||||||
|
|
||||||
## MusifyBackend (Domain + Application + Infrastructure)
|
## Musify.Domain + Musify.Application + Musify.Infrastructure
|
||||||
|
|
||||||
El núcleo, en tres proyectos (clean architecture):
|
El núcleo, en tres proyectos (clean architecture):
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ Casos de uso (CQRS con Mediator):
|
|||||||
- `Shared/` → helpers transversales (`StorageKey.Combine`, `ImageSize`).
|
- `Shared/` → helpers transversales (`StorageKey.Combine`, `ImageSize`).
|
||||||
- `Pagination/` → `PaginatedResponse<T>`.
|
- `Pagination/` → `PaginatedResponse<T>`.
|
||||||
|
|
||||||
**Porqué**: la lógica de negocio no conoce EF, S3 ni RabbitMQ; solo interfaces. Reutilizable por WebApi y Worker.
|
**Porqué**: la lógica de negocio no conoce EF, S3 ni RabbitMQ; solo interfaces. Reutilizable por la API y el Worker.
|
||||||
|
|
||||||
### Infrastructure
|
### Infrastructure
|
||||||
Implementaciones concretas:
|
Implementaciones concretas:
|
||||||
@@ -40,20 +41,26 @@ Implementaciones concretas:
|
|||||||
- `MassTransit/` → consumers, activities y routing slips (ver [media-processing.md](media-processing.md)).
|
- `MassTransit/` → consumers, activities y routing slips (ver [media-processing.md](media-processing.md)).
|
||||||
- `Jobs/` → `UploadIntentExpirationJob`, `TemporalUploadsCleanUpJob`.
|
- `Jobs/` → `UploadIntentExpirationJob`, `TemporalUploadsCleanUpJob`.
|
||||||
|
|
||||||
## Musify.StreamingGateway/StreamingGateway (`:8081`)
|
## Musify.StreamingGateway (`:8081`)
|
||||||
|
|
||||||
Reverse proxy (ASP.NET + **YARP**) que sirve el audio (`.m4a`) desde SeaweedFS. Valida el stream-ticket (RS256, clave pública) y, si el objeto pedido cae bajo el prefijo autorizado, reenvía al filer.
|
Reverse proxy (ASP.NET + **YARP**) que sirve el audio (`.m4a`) desde SeaweedFS. Valida el stream-ticket (RS256, clave pública) y, si el objeto pedido cae bajo el prefijo autorizado, reenvía al filer.
|
||||||
|
|
||||||
**Porqué un proyecto aparte**: escala con el tráfico de bytes independientemente de la API, tiene superficie mínima (solo sabe validar un JWT y proxiar) y permite que SeaweedFS quede en red privada. Ver [streaming.md](streaming.md).
|
**Porqué un proyecto aparte**: escala con el tráfico de bytes independientemente de la API, tiene superficie mínima (solo sabe validar un JWT y proxiar) y permite que SeaweedFS quede en red privada. Ver [streaming.md](streaming.md).
|
||||||
|
|
||||||
## Musify.Worker/Worker
|
## Musify.Worker
|
||||||
|
|
||||||
Worker Service (host de fondo) que consume los eventos de MassTransit y ejecuta el procesado pesado: transcode de audio a `.m4a` (ffmpeg), generación de miniaturas (ImageSharp), transferencias de ficheros en el bucket, y los jobs de expiración/limpieza de upload intents.
|
Worker Service (host de fondo) que consume los eventos de MassTransit y ejecuta el procesado pesado: transcode de audio a `.m4a` (ffmpeg), generación de miniaturas (ImageSharp), transferencias de ficheros en el bucket, y los jobs de expiración/limpieza de upload intents.
|
||||||
|
|
||||||
**Porqué aparte**: el trabajo CPU/IO no debe ocurrir dentro de un request HTTP; se escala y reinicia por separado. (Antes se llamaba `Musify.PictureWorker`; se renombró porque hace más que imágenes.)
|
**Porqué aparte**: el trabajo CPU/IO no debe ocurrir dentro de un request HTTP; se escala y reinicia por separado. (Antes se llamaba `Musify.PictureWorker`; se renombró porque hace más que imágenes.)
|
||||||
|
|
||||||
## Raíz del workspace (no versionada)
|
## web-player (`:5173` en dev, `:3000` en contenedor)
|
||||||
|
|
||||||
- **`deploy/seaweedfs/`** — `docker-compose.yml` + `s3.conf` para levantar SeaweedFS.
|
Cliente web (SvelteKit 2 + Svelte 5 + Tailwind 4). La sesión OIDC vive en el
|
||||||
- **`player.html`** — consola de pruebas (un solo archivo): login con token, CRUD de playlists, añadir/quitar canciones, listar y reproducir. Ver [development.md](development.md). (Pendiente: hoy usa dash.js; con el cambio a `.m4a` debe reproducir el fichero directo por `<audio>`/range.)
|
servidor (cookie cifrada); las llamadas al backend salen del servidor con el
|
||||||
|
access token, y las subidas se hacen contra las URLs prefirmadas de S3.
|
||||||
|
|
||||||
|
## Otras carpetas
|
||||||
|
|
||||||
|
- **`deploy/`** — stacks de Docker Compose (dev y prod) y los scripts de
|
||||||
|
arranque, migraciones y provisioning. Ver [../deploy/README.md](../deploy/README.md).
|
||||||
- **`docs/`** — esta documentación.
|
- **`docs/`** — esta documentación.
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ Expone:
|
|||||||
- **filer HTTP** (`:8888`) — sirve los ficheros por ruta; lo usa el **StreamingGateway**.
|
- **filer HTTP** (`:8888`) — sirve los ficheros por ruta; lo usa el **StreamingGateway**.
|
||||||
- master (`:9333`) y volume server — internos.
|
- master (`:9333`) y volume server — internos.
|
||||||
|
|
||||||
Bucket de la app: **`webapi-storage`** (`ApplicationStorage:Bucket`). Importante: la WebApi y el Worker usan **el mismo bucket** (el worker procesa lo que sube la API).
|
Bucket de la app: **`webapi-storage`** (`ApplicationStorage:Bucket`). Importante: la API y el Worker usan **el mismo bucket** (el worker procesa lo que sube la API).
|
||||||
|
|
||||||
## Estructura de claves (layout)
|
## Estructura de claves (layout)
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Objetivo
|
## Objetivo
|
||||||
|
|
||||||
Reproducir el audio **sin que los bytes pasen por la WebApi**, pero con control de acceso. La WebApi solo emite un permiso corto; el reverse proxy sirve los bytes desde SeaweedFS.
|
Reproducir el audio **sin que los bytes pasen por la API**, pero con control de acceso. La API solo emite un permiso corto; el reverse proxy sirve los bytes desde SeaweedFS.
|
||||||
|
|
||||||
## Formato: fichero único AAC (.m4a) por HTTP range
|
## Formato: fichero único AAC (.m4a) por HTTP range
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ Se autoriza **el prefijo de la carpeta** de la pista con un *stream-ticket* de c
|
|||||||
|
|
||||||
## Flujo
|
## Flujo
|
||||||
|
|
||||||
1. `GET /tracks/{id}/stream` (autenticado) → la WebApi autoriza y devuelve:
|
1. `GET /tracks/{id}/stream` (autenticado) → la API autoriza y devuelve:
|
||||||
```json
|
```json
|
||||||
{ "manifestUrl": "http://<gateway>/media/Tracks/ProcessedAudios/<folder>/audio.m4a",
|
{ "manifestUrl": "http://<gateway>/media/Tracks/ProcessedAudios/<folder>/audio.m4a",
|
||||||
"ticket": "<JWT RS256>", "expiresInSeconds": 3600 }
|
"ticket": "<JWT RS256>", "expiresInSeconds": 3600 }
|
||||||
@@ -27,10 +27,10 @@ Se autoriza **el prefijo de la carpeta** de la pista con un *stream-ticket* de c
|
|||||||
|
|
||||||
## El ticket (RS256)
|
## El ticket (RS256)
|
||||||
|
|
||||||
- Lo firma la WebApi con clave **privada** (`StreamTicketService`); el gateway valida con la **pública** (`TicketValidator`). Así el gateway **no puede emitir** tickets, solo verificarlos.
|
- Lo firma la API con clave **privada** (`StreamTicketService`); el gateway valida con la **pública** (`TicketValidator`). Así el gateway **no puede emitir** tickets, solo verificarlos.
|
||||||
- Claims: `sub`, `prefix`, `aud = media-gateway`, `iss = musify-webapi`, `exp` (TTL ~1h).
|
- Claims: `sub`, `prefix`, `aud = media-gateway`, `iss = musify-webapi`, `exp` (TTL ~1h).
|
||||||
|
|
||||||
**Por qué RS256 (asimétrica)**: separa responsabilidades — emisor (WebApi) y verificador (gateway) no comparten secreto; el gateway, aunque se comprometa, no puede crear permisos.
|
**Por qué RS256 (asimétrica)**: separa responsabilidades — emisor (la API) y verificador (gateway) no comparten secreto; el gateway, aunque se comprometa, no puede crear permisos.
|
||||||
|
|
||||||
## El gateway (validación + proxy)
|
## El gateway (validación + proxy)
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ Se autoriza **el prefijo de la carpeta** de la pista con un *stream-ticket* de c
|
|||||||
|
|
||||||
## ¿Un ticket por canción?
|
## ¿Un ticket por canción?
|
||||||
|
|
||||||
Sí. Cada ticket está acotado a **una** carpeta (una pista). Para reproducir otra, se pide otro ticket. Es barato (la WebApi solo firma un JWT) y es lo más seguro: cada permiso abre solo lo que vas a reproducir.
|
Sí. Cada ticket está acotado a **una** carpeta (una pista). Para reproducir otra, se pide otro ticket. Es barato (la API solo firma un JWT) y es lo más seguro: cada permiso abre solo lo que vas a reproducir.
|
||||||
|
|
||||||
- Si algún día molesta (muchísimos cambios de pista), el mismo mecanismo del claim `prefix` permite ampliar el alcance: a nivel de playlist o de toda la biblioteca (`Tracks/ProcessedAudios/`). Trade-off seguridad ↔ comodidad. Por defecto, **por canción**.
|
- Si algún día molesta (muchísimos cambios de pista), el mismo mecanismo del claim `prefix` permite ampliar el alcance: a nivel de playlist o de toda la biblioteca (`Tracks/ProcessedAudios/`). Trade-off seguridad ↔ comodidad. Por defecto, **por canción**.
|
||||||
|
|
||||||
|
|||||||
+23
-30
@@ -1,42 +1,35 @@
|
|||||||
# sv
|
# web-player
|
||||||
|
|
||||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
Musify's web client: SvelteKit 2 (`adapter-node`), Svelte 5 runes, Tailwind 4,
|
||||||
|
TypeScript. Authentication is server-side OIDC against Zitadel (`openid-client`
|
||||||
|
and `jose`) with an encrypted session cookie; the backend is called from `load`
|
||||||
|
functions and form actions through a typed `openapi-fetch` client.
|
||||||
|
|
||||||
## Creating a project
|
## Development
|
||||||
|
|
||||||
If you're seeing this, you've probably already done this step. Congrats!
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# create a new project
|
npm install
|
||||||
npx sv create my-app
|
npm run dev # :5173
|
||||||
```
|
```
|
||||||
|
|
||||||
To recreate this project with the same configuration:
|
Configuration lives in `.env` (template in `.env.example`).
|
||||||
|
`deploy/zitadel/provision.ps1` fills in the Zitadel values automatically, so the
|
||||||
|
usual flow is to bring the stack up first (`./deploy/up.ps1`) and then run the
|
||||||
|
dev server.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# recreate this project
|
npm run check # svelte-check — must be clean before closing a change
|
||||||
npx sv@0.16.2 create --template minimal --types ts --add tailwindcss="plugins:none" --no-download-check --install npm web-player
|
npm run lint # prettier --check . && eslint .
|
||||||
|
npm run format # prettier --write .
|
||||||
|
npm run build # production build into build/
|
||||||
|
npm run gen:api # regenerate src/lib/api/schema.d.ts from the running API
|
||||||
```
|
```
|
||||||
|
|
||||||
## Developing
|
## Production
|
||||||
|
|
||||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
Built and run as a container by the production stack — see
|
||||||
|
[../deploy/README.md](../deploy/README.md). There the same variables come from
|
||||||
|
`deploy/.env.prod` through compose instead of from `.env`.
|
||||||
|
|
||||||
```sh
|
Conventions and the design system are documented in [CLAUDE.md](CLAUDE.md) and
|
||||||
npm run dev
|
in the `musify-web` skill.
|
||||||
|
|
||||||
# or start the server and open the app in a new browser tab
|
|
||||||
npm run dev -- --open
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
To create a production version of your app:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
You can preview the production build with `npm run preview`.
|
|
||||||
|
|
||||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user