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:
2026-07-20 15:14:11 +02:00
parent cea0dbffa4
commit 589864e48f
10 changed files with 269 additions and 331 deletions
+36 -64
View File
@@ -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
View File
@@ -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
View File
@@ -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/`.
+9 -9
View File
@@ -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).
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.