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
|
||||
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.
|
||||
Music streaming platform: upload your own tracks (or import them from YouTube),
|
||||
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
|
||||
**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).
|
||||
## Run it
|
||||
|
||||
## Design System
|
||||
- **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.
|
||||
Development (Windows, Docker Desktop):
|
||||
|
||||
## Layout
|
||||
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`).
|
||||
```powershell
|
||||
./deploy/up.ps1 # infrastructure; the apps run from the IDE
|
||||
./deploy/up.ps1 -Apps # everything in Docker
|
||||
```
|
||||
|
||||
### Sidebar (264px, left)
|
||||
- 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).
|
||||
Production, on a server with a domain:
|
||||
|
||||
### Main content (right of sidebar, scrollable)
|
||||
Seven views, toggled by sidebar nav (single active view at a time — replace with real routing):
|
||||
```sh
|
||||
./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.
|
||||
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.
|
||||
3. **Tu biblioteca (Library)** — full-width flat list of all tracks (built-in + uploaded), "Subida"/"Importada" source badges where applicable.
|
||||
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).
|
||||
Both are idempotent and print what they did. See
|
||||
[deploy/README.md](deploy/README.md) for the details, the port map and the
|
||||
production prerequisites.
|
||||
|
||||
### Player bar (bottom, full width, 92px)
|
||||
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`.
|
||||
## Components
|
||||
|
||||
## Interactions & Behavior
|
||||
- **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.
|
||||
- **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.
|
||||
- **Volume**: native `<input type="range">`, custom-styled; value drives a linear-gradient split on the track itself.
|
||||
- **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).
|
||||
- **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.
|
||||
- **Import**: toggling a service just flips a boolean per source — no real OAuth/integration.
|
||||
- **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.
|
||||
| Service | Port (dev) | Role |
|
||||
|---|---|---|
|
||||
| `Musify.Api` | 5111 | REST API, issues presigned upload URLs and stream tickets |
|
||||
| `Musify.StreamingGateway` | 8081 | YARP proxy that validates the stream ticket and serves the audio |
|
||||
| `Musify.Worker` | — | MassTransit consumers: transcoding, thumbnails, YouTube downloads |
|
||||
| `web-player` | 5173 / 3000 | Web client (SvelteKit, server-side OIDC session) |
|
||||
| Zitadel | 8080 | Identity (OIDC/OAuth2) |
|
||||
| PostgreSQL / RabbitMQ / SeaweedFS / Jaeger | 59000 / 5672 / 8333 / 16686 | Metadata, messaging, object storage, traces |
|
||||
|
||||
## State Management (what to model)
|
||||
- `currentTrackId`, `playing`, `progress` (seconds), `volume` (0–100), `network` (currently cycled manually — in production drive from actual measured/adaptive HLS bitrate).
|
||||
- `view` (active section) — replace with real routing.
|
||||
- `tracks` (built-in + uploaded, merged) — replace with real catalog/uploaded-file data from your backend.
|
||||
- `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.
|
||||
More in [docs/](docs/README.md): [architecture](docs/architecture.md),
|
||||
[authentication](docs/authentication.md), [storage](docs/storage.md),
|
||||
[streaming](docs/streaming.md), [media processing](docs/media-processing.md),
|
||||
[development](docs/development.md).
|
||||
|
||||
+120
-154
@@ -1,192 +1,158 @@
|
||||
# Development deploy
|
||||
# Deploy
|
||||
|
||||
Everything the backend needs for development, in one place. A single
|
||||
`docker-compose.yml` brings up **PostgreSQL, Zitadel, RabbitMQ, SeaweedFS and
|
||||
Jaeger**; the .NET apps run from the IDE (or, optionally, in Docker too).
|
||||
Everything needed to run Musify, in development and in production, from the
|
||||
same set of compose files.
|
||||
|
||||
## 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
|
||||
# From the repo root (Musify/)
|
||||
./deploy/up.ps1
|
||||
./deploy/up.ps1 # infrastructure only, apps from the IDE
|
||||
./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.
|
||||
2. Starts the stack (`docker compose up -d`) and waits for Postgres to be healthy.
|
||||
3. Creates the `webapi-storage` bucket in SeaweedFS (`seaweedfs-init` service).
|
||||
4. Generates the RS256 stream-ticket keys in `D:\weed` if missing.
|
||||
5. Creates the Worker temp directory (`D:\tempsFilesDev`).
|
||||
6. Applies the EF Core migrations to `musify_db`.
|
||||
7. **Provisions Zitadel automatically** (project + OIDC app with JWT token) and
|
||||
wires the ClientId into `Musify.Api` user-secrets and `.env`.
|
||||
1. Creates `deploy/.env` from `.env.example` if it is missing.
|
||||
2. Generates the RS256 stream-ticket keys in `deploy/keys/` and the Worker
|
||||
scratch directory `deploy/.tmp/`.
|
||||
3. Starts the stack and waits for Postgres.
|
||||
4. Applies the EF Core migrations.
|
||||
5. Provisions Zitadel: project, API OIDC app and web player OIDC app, then
|
||||
writes the client ids into `deploy/.env`, `Musify.Api` user-secrets and
|
||||
`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
|
||||
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 |
|
||||
| Service | Port | Used by |
|
||||
|---|---|---|
|
||||
| PostgreSQL | `59000` → 5432 | API + Worker (EF Core) and the Zitadel database |
|
||||
| Zitadel | `8080` | OIDC/OAuth2 (console at `/ui/console`) |
|
||||
| PostgreSQL | `59000` → 5432 | API + Worker (EF Core) and Zitadel |
|
||||
| Zitadel | `8080` | OIDC/OAuth2, console at `/ui/console` |
|
||||
| RabbitMQ | `5672` / `15672` | MassTransit / management UI |
|
||||
| SeaweedFS S3 | `8333` | S3 (presigned PUT/GET) |
|
||||
| SeaweedFS filer | `8888` | serves files → StreamingGateway |
|
||||
| SeaweedFS master | `9333` | admin/UI |
|
||||
| Jaeger | `16686` / `4317` / `4318` | OTLP traces (UI / gRPC / HTTP) |
|
||||
| SeaweedFS | `8333` / `8888` / `9333` | S3 / filer (→ gateway) / master |
|
||||
| Jaeger | `16686` / `4317` / `4318` | traces UI / OTLP gRPC / OTLP HTTP |
|
||||
| pgAdmin (`-Tools`) | `5050` | Postgres UI |
|
||||
| API, gateway, web player (`-Apps`) | `5111` / `8081` / `3000` | |
|
||||
|
||||
Ports are published to the host, so apps run locally connect without changing
|
||||
`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)
|
||||
### Running the apps from the IDE
|
||||
|
||||
```powershell
|
||||
dotnet run --project backend/Musify.Api # :5111
|
||||
dotnet run --project backend/Musify.StreamingGateway # :8081
|
||||
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
|
||||
docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.apps.yml up -d --build
|
||||
`PUBLIC_AUTH_URL` and `S3_PUBLIC_URL` point at `host.docker.internal`, which
|
||||
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
|
||||
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`).
|
||||
It refuses to start until the two things it cannot generate are in place:
|
||||
|
||||
> Note: on Linux the apps look for `appsettings*.json` in lowercase. The `Api`/
|
||||
> `Worker` projects use `AppSettings*.json` (PascalCase), so their Dockerfiles
|
||||
> create lowercase copies at publish time. On Windows it did not matter
|
||||
> (case-insensitive).
|
||||
1. **`deploy/.env.prod`** — copied from `.env.prod.example` on the first run.
|
||||
Replace every `example.com` with your domain and every `CHANGE_ME` with a
|
||||
random value (`openssl rand -hex 24`; the Zitadel masterkey needs exactly 32
|
||||
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
|
||||
./deploy/up.ps1 -SkipMigrations # bring up without touching the DB
|
||||
./deploy/up.ps1 -Down # stop and remove containers (keep data)
|
||||
./deploy/up.ps1 -Destroy # stop and remove containers + volumes (wipe data)
|
||||
Then `up.sh` generates the stream keys, starts the infrastructure and the edge,
|
||||
provisions Zitadel (it needs `curl` and `jq` on the server), applies the
|
||||
migrations and finally builds and starts the applications. It is idempotent —
|
||||
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
|
||||
docker compose -f deploy/docker-compose.yml ps
|
||||
```
|
||||
Only nginx publishes ports (80/443); everything else is reachable only on the
|
||||
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/
|
||||
├─ docker-compose.yml # infrastructure stack (single source of truth)
|
||||
├─ docker-compose.apps.yml # optional overlay: the 3 .NET apps in Docker
|
||||
├─ .env # local config (secrets/ports); not versioned
|
||||
├─ .env.example # config template (versioned)
|
||||
├─ up.ps1 # orchestrator: up / down / destroy
|
||||
├─ README.md # this file
|
||||
├─ compose.yml # all services, environment-independent
|
||||
├─ compose.dev.yml # dev overlay: host ports, pgAdmin
|
||||
├─ compose.prod.yml # prod overlay: nginx edge, secrets
|
||||
├─ .env.example / .env.prod.example
|
||||
├─ up.ps1 # dev orchestrator
|
||||
├─ 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/
|
||||
│ ├─ common.ps1 # shared helpers (Read-DotEnv, Write-Step)
|
||||
│ ├─ stream-keys.ps1 # generates the RS256 stream-ticket key pair
|
||||
│ └─ migrate.ps1 # applies EF Core migrations
|
||||
├─ seaweedfs/
|
||||
│ └─ s3.conf # SeaweedFS S3 identities/credentials
|
||||
│ ├─ common.ps1 / lib.sh # shared helpers
|
||||
│ ├─ stream-keys.ps1 / .sh # RS256 key pair generation
|
||||
│ └─ migrate.ps1 / .sh # EF Core migrations
|
||||
└─ zitadel/
|
||||
├─ provision.ps1 # creates project + OIDC app, wires the ClientId
|
||||
└─ .output/ # PAT + init secrets written by Zitadel (not versioned)
|
||||
|
||||
backend/
|
||||
├─ Musify.Api.Dockerfile # API image
|
||||
├─ Musify.Worker.Dockerfile # Worker image (includes ffmpeg)
|
||||
├─ Musify.StreamingGateway.Dockerfile # Gateway image
|
||||
└─ .dockerignore
|
||||
├─ provision.ps1 / .sh # project + OIDC apps, wires the client ids
|
||||
└─ .output/ # PAT written by Zitadel on init (not versioned)
|
||||
```
|
||||
|
||||
### 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`,
|
||||
`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.
|
||||
## Notes
|
||||
|
||||
### Config
|
||||
|
||||
- **`.env`** — the actual values used by compose (Postgres/RabbitMQ/Zitadel/S3
|
||||
credentials, ports, host paths, `AUTH_CLIENT_ID`). Ignored by git (`*.env`).
|
||||
- **`.env.example`** — the versioned template. Copy it to `.env` (or let `up.ps1`
|
||||
do it) and adjust.
|
||||
|
||||
### Scripts (PowerShell)
|
||||
|
||||
- **`up.ps1`** — the orchestrator. Parses `.env`, starts the stack, waits for
|
||||
Postgres, then calls the sub-scripts in order (keys → migrations → provisioning)
|
||||
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.
|
||||
- **S3 credentials** are rendered into the SeaweedFS identity file from
|
||||
`S3_ACCESS_KEY` / `S3_SECRET_KEY` when the container starts, so they cannot
|
||||
drift from the ones the apps are given.
|
||||
- **Zitadel** runs `start-from-init`, which is idempotent: the same command
|
||||
works on an empty volume and on an existing one.
|
||||
- **`latest` Zitadel images** require the standalone Login UI v2 container,
|
||||
which this stack does not run. The provisioning scripts turn that requirement
|
||||
off so the built-in `/ui/login` is used.
|
||||
- **`AppSettings*.json` are PascalCase** in `Musify.Api` and `Musify.Worker`;
|
||||
on case-sensitive Linux the host looks for `appsettings*.json`, so their
|
||||
Dockerfiles create lowercase copies at publish time.
|
||||
|
||||
+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).
|
||||
- [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.
|
||||
- [../deploy/README.md](../deploy/README.md) — El deploy en detalle: compose, scripts y producción.
|
||||
|
||||
## 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)
|
||||
▼ ▼ ▼
|
||||
@@ -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)
|
||||
|
||||
Login: Cliente ◄──► Zitadel (:8080, OIDC) → WebApi valida el JWT
|
||||
Login: Cliente ◄──► Zitadel (:8080, OIDC) → Musify.Api valida el JWT
|
||||
```
|
||||
|
||||
## Stack
|
||||
|
||||
- **.NET 10**, C#. Minimal APIs (WebApi), Worker Service (Worker), ASP.NET + YARP (StreamingGateway).
|
||||
- **CQRS** con [Mediator] (source-generator) y resultados con **Ardalis.Result**.
|
||||
- **.NET 10**, C#. Minimal APIs (`Musify.Api`), Worker Service (`Musify.Worker`), ASP.NET + YARP (`Musify.StreamingGateway`).
|
||||
- **CQRS** con [Mediator] (source-generator) y resultados con **ErrorOr**.
|
||||
- **EF Core 10** + **PostgreSQL** (metadatos).
|
||||
- **MassTransit 8** + **RabbitMQ** (procesado asíncrono con routing slips/sagas).
|
||||
- **SeaweedFS** (almacenamiento de objetos compatible con S3, vía AWSSDK.S3).
|
||||
- **ffmpeg** (transcode de audio a AAC/`.m4a`), **SixLabors.ImageSharp** (miniaturas).
|
||||
- **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
|
||||
|
||||
- **Clean architecture / capas** en `MusifyBackend`: 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.
|
||||
- **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.
|
||||
- **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 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 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).
|
||||
|
||||
## Componentes
|
||||
|
||||
| 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 |
|
||||
| **MusifyBackend** | Dominio + casos de uso + infraestructura compartida (EF, S3, MassTransit, ffmpeg) | Lógica reutilizada por WebApi y Worker |
|
||||
| **Musify.Api** | API REST (usuarios, pistas, playlists), autoriza, emite presigned URLs y stream-tickets | Punto de entrada fino; no procesa medios ni sirve bytes |
|
||||
| **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 |
|
||||
| **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) | — |
|
||||
@@ -23,15 +23,15 @@
|
||||
## Flujos principales
|
||||
|
||||
### 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).
|
||||
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.
|
||||
|
||||
Ver detalle en [media-processing.md](media-processing.md) y [storage.md](storage.md).
|
||||
|
||||
### 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.
|
||||
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
|
||||
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.
|
||||
|
||||
Ver [authentication.md](authentication.md).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 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.
|
||||
- `JwtBearerOptionsSetup` configura la validación (issuer, audience, claves vía metadata OIDC).
|
||||
|
||||
+48
-50
@@ -1,92 +1,90 @@
|
||||
# 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)
|
||||
|
||||
Todo (PostgreSQL, Zitadel, RabbitMQ, SeaweedFS, Jaeger) vive en un solo stack en
|
||||
[`deploy/`](../deploy/README.md). Bootstrap completo (levanta + bucket + claves + migraciones):
|
||||
## Arranque
|
||||
|
||||
```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
|
||||
docker compose -f deploy\docker-compose.yml up -d
|
||||
```
|
||||
|
||||
Config (usuarios/claves/puertos) en `deploy/.env`. Ver [deploy/README.md](../deploy/README.md)
|
||||
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
|
||||
dotnet run --project backend/Musify.Api # :5111
|
||||
dotnet run --project backend/Musify.StreamingGateway # :8081
|
||||
dotnet run --project backend/Musify.Worker # background
|
||||
npm --prefix web-player run dev # :5173
|
||||
```
|
||||
|
||||
## Mapa de puertos
|
||||
|
||||
| Servicio | Puerto |
|
||||
|---|---|
|
||||
| WebApi | 5111 (`/scalar/v1`, `/openapi/v1.json`) |
|
||||
| StreamingGateway | 8081 (`/health`, `/media/...`) |
|
||||
| Musify.Api | 5111 (`/scalar/v1`, `/openapi/v1.json`) |
|
||||
| Musify.StreamingGateway | 8081 (`/health`, `/media/...`) |
|
||||
| web-player | 5173 (`npm run dev`) o 3000 (contenedor) |
|
||||
| SeaweedFS S3 / filer / master | 8333 / 8888 / 9333 |
|
||||
| Zitadel | 8080 |
|
||||
| PostgreSQL | 59000 (→ 5432 en el contenedor) |
|
||||
| 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
|
||||
|
||||
No hay migración automática al arrancar. `deploy/up.ps1` las aplica en el bootstrap;
|
||||
para hacerlo a mano tras cambios de esquema:
|
||||
No hay migración automática al arrancar; `up.ps1` las aplica en el bootstrap.
|
||||
A mano, tras cambios de esquema:
|
||||
|
||||
```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
|
||||
(`up.ps1` la deja en user-secrets automáticamente).
|
||||
`Musify.Infrastructure` es a la vez el proyecto de migraciones y el de arranque:
|
||||
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)
|
||||
|
||||
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
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out stream_private.pem
|
||||
openssl rsa -in stream_private.pem -pubout -out stream_public.pem
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out deploy/keys/stream_private.pem
|
||||
openssl rsa -in deploy/keys/stream_private.pem -pubout -out deploy/keys/stream_public.pem
|
||||
```
|
||||
|
||||
- WebApi: `StreamTicket:PrivateKeyPath` → `stream_private.pem`.
|
||||
- Gateway: `StreamTicket:PublicKeyPath` → `stream_public.pem`.
|
||||
|
||||
## 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.
|
||||
- API: `StreamTicket:PrivateKeyPath`.
|
||||
- Gateway: `StreamTicket:PublicKeyPath`.
|
||||
|
||||
## 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
|
||||
|
||||
- .NET 10 SDK.
|
||||
- **ffmpeg** en el PATH (transcode de audio del Worker).
|
||||
- .NET 10 SDK y Node 22.
|
||||
- **ffmpeg** y **yt-dlp** en el PATH (transcode y descarga de audio del Worker).
|
||||
- Docker Desktop.
|
||||
- Directorio temporal de trabajo del Worker (`Workers:Routes:TemporaryFilesDirectory`, p. ej. `D:\tempsFilesDev`).
|
||||
|
||||
+18
-11
@@ -1,8 +1,9 @@
|
||||
# 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.
|
||||
|
||||
@@ -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.
|
||||
- **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.
|
||||
- **CORS** totalmente abierto **solo en Development** (para el `player.html` y front local).
|
||||
- Traduce `Ardalis.Result` → HTTP en `ResultHttpExtensions`.
|
||||
- **CORS** totalmente abierto **solo en Development** (para el front local).
|
||||
- Traduce `ErrorOr` → HTTP en `ResultHttpExtensions`.
|
||||
|
||||
**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):
|
||||
|
||||
@@ -31,7 +32,7 @@ Casos de uso (CQRS con Mediator):
|
||||
- `Shared/` → helpers transversales (`StorageKey.Combine`, `ImageSize`).
|
||||
- `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
|
||||
Implementaciones concretas:
|
||||
@@ -40,20 +41,26 @@ Implementaciones concretas:
|
||||
- `MassTransit/` → consumers, activities y routing slips (ver [media-processing.md](media-processing.md)).
|
||||
- `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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
- **`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.)
|
||||
Cliente web (SvelteKit 2 + Svelte 5 + Tailwind 4). La sesión OIDC vive en el
|
||||
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.
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ Expone:
|
||||
- **filer HTTP** (`:8888`) — sirve los ficheros por ruta; lo usa el **StreamingGateway**.
|
||||
- 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)
|
||||
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@
|
||||
|
||||
## 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
|
||||
|
||||
@@ -16,7 +16,7 @@ Se autoriza **el prefijo de la carpeta** de la pista con un *stream-ticket* de c
|
||||
|
||||
## Flujo
|
||||
|
||||
1. `GET /tracks/{id}/stream` (autenticado) → la WebApi autoriza y devuelve:
|
||||
1. `GET /tracks/{id}/stream` (autenticado) → la API autoriza y devuelve:
|
||||
```json
|
||||
{ "manifestUrl": "http://<gateway>/media/Tracks/ProcessedAudios/<folder>/audio.m4a",
|
||||
"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)
|
||||
|
||||
- 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).
|
||||
|
||||
**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)
|
||||
|
||||
@@ -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?
|
||||
|
||||
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**.
|
||||
|
||||
|
||||
+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
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
## Development
|
||||
|
||||
```sh
|
||||
# create a new project
|
||||
npx sv create my-app
|
||||
npm install
|
||||
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
|
||||
# recreate this project
|
||||
npx sv@0.16.2 create --template minimal --types ts --add tailwindcss="plugins:none" --no-download-check --install npm web-player
|
||||
npm run check # svelte-check — must be clean before closing a change
|
||||
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
|
||||
npm run dev
|
||||
|
||||
# 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.
|
||||
Conventions and the design system are documented in [CLAUDE.md](CLAUDE.md) and
|
||||
in the `musify-web` skill.
|
||||
|
||||
Reference in New Issue
Block a user