Commit Graph
431 Commits
Author SHA1 Message Date
Admin 68ec3a2aae feat(api): expose the album endpoints
Eight routes under /albums covering the listing, the detail, the tracklist and
the owner-only writes, wired to the same validation filter and ErrorOr mapping
the playlists use.
2026-07-20 20:13:40 +02:00
Admin 712f130e1d feat(backend): add the album use cases
Create, update, delete and list albums, plus adding and removing their tracks,
mirroring the playlist handlers. Only tracks the user uploaded can go into an
album, and removing one renumbers the rest so the tracklist stays 1..n.
2026-07-20 20:10:45 +02:00
Admin 8e561f5b2a feat(backend): model albums in the domain
Album is abstract with UserAlbum as its first table-per-type child, the same
split Track already uses, so the YouTube albums can join later without moving
anything. Tracks are linked through AlbumHasTrack rather than a column on the
track itself: an external track is shared between users and the same song
shows up in more than one album.
2026-07-20 20:08:21 +02:00
Admin a59fa3a548 fix(backend): give track titles room to breathe
Titles were capped at 50 characters and the YouTube provisioner silently cut
them there, which mangles anything with a feature or a remix suffix. Albums
are about to make that far more visible, so the column now holds 200.
2026-07-20 20:06:47 +02:00
Admin 8c62faa822 fix: stop the YouTube covers coming out blurry and cropped
The ytimg variants are 16:9, so picking one by width handed a square tile far
fewer pixels than it needed: a 96px cover got the 120x90 file and kept 90px
after the crop. They are chosen by their short side now, the album art is
preferred over the video frame when the search returns both, and the display
sizes account for the tiles being up to 180px on a 2x screen.
2026-07-20 19:05:44 +02:00
Admin 75c08f2a6c fix(backend): keep the saved cover art at full resolution
Search results now carry a small thumbnail, so the artwork downloaded when a
YouTube track is saved would have shrunk with them. The URL is upsized to the
configured artwork resolution right before the download event goes out.
2026-07-20 17:42:11 +02:00
Admin 33cb3c9680 perf(deploy): cache the YouTube artwork at the nginx edge
Thumbnail responses are immutable for a month, so nginx can answer repeats
from disk and never wake the web player or Google for them. The cache lives in
its own volume, collapses concurrent misses with a lock and serves stale
copies while it refreshes; THUMBNAIL_CACHE_SIZE sizes it.
2026-07-20 17:40:28 +02:00
Admin 9e4388332f perf(web-player): cut down the searches the explore page fires
Typing in the search box navigated on every pause, so single letters and
round-trips back to the same term each triggered a full YouTube search and a
new grid of covers. The debounce is longer, a term identical to the current
one no longer navigates, YouTube is only queried past a minimum length, and
the infinite scroll waits until the sentinel is closer before pulling a page.
2026-07-20 17:38:37 +02:00
Admin 470d24c9ca style(web-player): reformat the upload dropzone copy 2026-07-20 17:36:46 +02:00
Admin 2691c40da8 perf(web-player): serve YouTube artwork through a caching proxy
The browser used to pull every cover straight from Google, so a search grid
plus infinite scroll fired dozens of cross-origin image requests and started
coming back as 429. Covers now go through /api/youtube/thumbnail, which
resizes the upstream URL, caches the bytes in memory, collapses duplicate
in-flight fetches, caps the concurrency against Google and backs off when it
answers 429. Responses are immutable for a month and revalidate with an ETag,
and serving them same-origin also lets the accent extractor read the pixels.
2026-07-20 17:36:41 +02:00
Admin dd3150e3d4 perf(backend): cache YouTube searches and shrink the thumbnail URLs
Repeated searches and song lookups now reuse a single-flight memory cache, so
concurrent or debounced queries hit YouTube once instead of on every keystroke.
Thumbnails are resolved at a configurable size instead of the largest variant,
which cuts the image payload the browser pulls from Google.
2026-07-20 17:29:28 +02:00
Admin 7c62f8a8e8 Changes 2026-07-20 16:36:18 +02:00
Admin ca24632fac Responsive design 2026-07-20 16:25:44 +02:00
Admin 589864e48f 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.
2026-07-20 15:14:11 +02:00
Admin cea0dbffa4 chore(web-player): tighten the container image
There was no .dockerignore, so node_modules and build/ were copied into the
build context on every build. Add one, install from the lockfile with npm ci,
prune the dev dependencies out of the runtime layer and run as the node user.
2026-07-20 15:14:04 +02:00
Admin b4385d9028 feat(deploy): provision the web player client in development too
The web player's OIDC app had to be created by hand in the Zitadel console and
its client id copied into web-player/.env. provision.ps1 now creates both apps,
regenerates the client secret when it is no longer known, and wires the values
into deploy/.env, Musify.Api user-secrets and web-player/.env.

up.ps1 follows the new compose files, gains -Apps and -Tools for the two
profiles, generates the keys before starting rather than after, and no longer
swallows a provisioning failure as a warning.
2026-07-20 15:14:00 +02:00
Admin 7682de545d feat(deploy): add a one-command production deploy in shell
Production had no orchestrator at all and its provisioning script required
PowerShell 7 on the server. up.sh now does the whole thing in POSIX shell:
it refuses to start until .env.prod and the origin certificate are in place,
generates the stream keys, brings up the infrastructure and the edge,
provisions Zitadel, applies the migrations and finally builds and starts the
applications. It is idempotent, so the same command deploys a new version.

provision.sh replaces provision.prod.ps1 (curl + jq) and also creates the web
player client, and migrate.prod.sh moves to scripts/migrate.sh reading the env
file with a parser instead of sourcing it as a shell script.
2026-07-20 15:13:54 +02:00
Admin 49130705e3 feat(deploy): template the nginx vhosts with the deployment domain
ikerdemo.cat was hardcoded in six server blocks, so the stack could only be
deployed under that one domain. The vhosts are now a template rendered by the
nginx entrypoint from MUSIFY_ROOT_DOMAIN, with the envsubst filter set so it
does not eat nginx's own variables. The repeated proxy headers move to a
shared snippet, the web player vhost gets the upload body limit it was missing
(audio is posted to its form actions), and the streaming vhost turns off proxy
buffering so range requests stay responsive.
2026-07-20 15:13:45 +02:00
Admin 1e77394a4d refactor(deploy): merge the three compose files into a base plus overlays
The dev and prod stacks each declared the same nine services, which is how the
S3 credentials and the gateway configuration ended up diverging. compose.yml
now holds every service with no published port and no domain in it, and the
overlays only add what is specific: host ports and pgAdmin for development,
the nginx edge and the mounted secrets for production. The applications sit
behind the `apps` profile, so a plain `up` still starts infrastructure only.

Everything environment-specific moves to the env files: the public URLs, the
credentials and the environment name. The SeaweedFS identity file is rendered
inside the container from S3_ACCESS_KEY and S3_SECRET_KEY, so the store and
the applications can no longer be given different keys.
2026-07-20 15:13:39 +02:00
Admin b3eff65000 refactor(backend): clean up the AppSettings files
Drop the dead Keycloak and ConnectionStrings sections from the Worker and move
its hardcoded dev values to AppSettings.Development.json, so the base file is
the empty template the convention describes. Remove the stale Authentication
block from the API dev settings (provisioning writes those to user-secrets) and
the ManifestFileName left over from DASH. Point the stream-ticket key and the
Worker scratch directory at deploy/keys and deploy/.tmp instead of D:\weed and
D:\tempsFilesDev, and make the API listen on localhost instead of a fixed LAN
address.
2026-07-20 15:13:27 +02:00
Admin 9826c1c718 fix(web-player): send presigned uploads to a reachable S3 endpoint
The PUT was always sent to 127.0.0.1, which is nothing inside the container, so
uploads failed whenever the web player did not run on the same host as
SeaweedFS. Take an optional S3_UPLOAD_ENDPOINT instead (the compose stacks
point it at seaweedfs:8333) and fall back to the host in the presigned URL. The
signed Host header is preserved either way, so the signature still matches.
2026-07-20 15:13:21 +02:00
Admin e277ea0bc5 fix(gateway): load the proxy routes and ticket settings in every environment
The YARP routes, the bucket path transform and the StreamTicket section only
existed in appsettings.Development.json, so a Production run had no routes at
all and every /media request fell through to a 404. Move them to
appsettings.json and leave only the environment-specific overrides (key path)
in the Development file; the container gets the filer address, the bucket and
the allowed CORS origins from environment variables.
2026-07-20 15:13:15 +02:00
Admin 2540a3a7da Remove copilot 2026-07-20 14:38:38 +02:00
Admin 08597f6464 Replace 2026-07-20 14:37:21 +02:00
Admin d4a66e4320 refactor(web-player): centralise styles and drop dead code
Styles: adds the scrim and on-art tokens so the black overlays and the
white marks drawn over artwork stop being raw tailwind colours, and
routes every component through the semantic layer instead of reaching
into the --mf-* palette. Renames --mf-accent-60, which was actually an
80% mix, to --mf-accent-soft, and removes the unused --mf-radius and
the --color-neutral-950 override.

Dead code: the root +layout.server.ts already redirects everything
except /login, so the anonymous branches in the home and explore loads
and the needsAuth empty state could never run. Deletes the unused
accentSoft helper, and stops exporting helpers nobody imports.

fmtTime and fmtDate move out of theme/color.ts into $lib/format.ts;
they are not colour concerns.

The accent animation now cancels its pending frame on teardown.
2026-07-20 14:35:45 +02:00
Admin e10c24cad0 refactor(deploy): rename the frontend service to web-player
Matches the folder rename: compose service frontend -> web-player,
container musify-frontend -> musify-web-player, and the nginx upstream
now points at http://web-player:3000.

Deploying this needs --remove-orphans, or the old musify-frontend
container is left behind.
2026-07-20 07:37:31 +02:00
Admin 97daa13ebd refactor: rename frontend/ to web-player/ and format with prettier
Updates the compose build context, the Dockerfile header, the package
name and the docs. The compose service and container keep the name
frontend/musify-frontend, since nginx routes to http://frontend:3000.

The source files also carry the first prettier pass, so the rename and
the reformat land together for them.
2026-07-20 07:35:01 +02:00
Admin 05514a1c6e chore(web-player): add prettier and eslint
Flat ESLint config with typescript-eslint and eslint-plugin-svelte,
Prettier with the svelte and tailwindcss plugins. Adds the format and
lint scripts.

Three rules are off on purpose: no-navigation-without-resolve (the app
is served at the root, there is no base path), prefer-svelte-reactivity
(the Set and URLSearchParams uses are local computations, not reactive
state) and no-unused-expressions in .svelte only, where reading a rune
on its own line is how an $effect registers a dependency.
2026-07-20 07:34:55 +02:00
Admin 987caaa729 fix(web): clear svelte-check warnings and log playback failures
The seeded $state initialisers in PlaylistForm intentionally capture the
prop once; untrack() states that instead of tripping
state_referenced_locally. The player now logs the caught exception
rather than discarding it.
2026-07-20 07:28:17 +02:00
Admin c56eb7e7f3 fix(web): resolve the greeting on the server
It was picked with Math.random() in the component body, so SSR and the
client rendered different text and hydration mismatched. Moving it to
the load keeps the per-visit variety with both sides agreeing, and the
six-level nested ternary becomes a lookup table.
2026-07-20 07:28:12 +02:00
Admin 7ca6d278a4 refactor(web): share track target helpers between home and explore
Both pages reimplemented the same accessors over the target union and a
byte-identical queue-item mapper. Moves the domain type (renamed
ContextMenuTarget -> TrackTarget, since tiles and the queue use it too)
and its helpers to $lib/tracks.ts; TrackContextMenu keeps only its menu
geometry. Home drops from 341 to ~230 lines.

Unifies two divergences the duplication was hiding: home compared the
active tile against track.id while the queue is keyed by
queueIdForTrack, so YouTube-sourced tracks never highlighted; and it
hardcoded explicit to false for local tracks, hiding the badge.

Also cancels in-flight youtube filler results when the effect re-runs,
so a slow response cannot overwrite newer ones.
2026-07-20 07:28:12 +02:00
Admin a40f612700 fix(web): guard authenticated routes instead of asserting the session user
library, playlists and playlists/[id] read locals.user!.sub with no
check, so an anonymous request threw a TypeError and returned 500
instead of redirecting to login. Adds requireUser(), which redirects to
/login?returnTo=... and returns a non-null SessionUser, and drops the
five non-null assertions.

/upload had no load at all: the page rendered for anonymous users and
only failed on submit. It now redirects up front.
2026-07-20 07:28:04 +02:00
Admin ba7ee2c59e chore(web): remove unused adapter-auto dependency
The project builds with adapter-node; adapter-auto was left over from
the SvelteKit template.
2026-07-20 07:27:57 +02:00
Admin 3d20be6e95 refactor(youtube): log service failures and decouple error messages
YouTubeMusicService swallowed the exception into an ErrorOr description
with no trace on the server side. Adds ILogger to the three operations.
The download activity no longer names yt-dlp in its messages, since the
downloader is an implementation detail behind IYouTubeDownloaderService.
2026-07-20 07:27:57 +02:00
Admin 0570852cac feat(worker): install yt-dlp and deno in the worker image
yt-dlp goes through pip in a venv rather than the standalone PyInstaller
binary, which unpacks on every invocation and roughly doubles latency.
deno solves the nsig challenges.
2026-07-20 07:27:51 +02:00
Admin 2507d98c55 feat(deploy): add production stack for the VPS
Compose file for ikerdemo.cat with nginx as TLS terminator, the three
.NET services, Zitadel and the storage stack. Values come from
deploy/.env.prod (untracked). Adds the frontend image, the migration
script and the Zitadel provisioning for production.
2026-07-20 07:27:51 +02:00
Admin 8adf24c716 chore: ignore production env files and mounted secrets
The existing *.env pattern did not match deploy/.env.prod, which holds
database, Zitadel and OIDC credentials. Also ignore deploy/secrets/,
mounted into the api and worker containers.
2026-07-20 07:27:38 +02:00
Admin 2dc544b2c0 Changes 2026-07-18 23:29:57 +02:00
Admin 4fed9bd059 A lot of changes 2026-07-17 18:56:23 +02:00
Admin aac9224c2d A lot of changes 2026-07-17 18:56:19 +02:00
Admin 1bd6f18c5e fix(backend): propagate user renames to artist and harden migration backfill
Code review of the Artist/ownership refactor surfaced three gaps:

- SyncUser updated User.Name on every login but never the linked
  Artist row, so renames never reached track credits - defeating the
  point of centralizing names in Artist.
- YouTubeTrackProvisioner looked up artists by ExternalId only when a
  browseId was known, so it could never reuse the ExternalId-less
  rows the migration backfilled from legacy Artist strings, creating
  duplicate Artist rows for the same person. It now adopts the
  backfilled row (assigning it the browseId) when one matches by
  normalized name.
- AddArtistsAndTrackOwnership deduped legacy YouTube artist strings
  case-sensitively before hashing them into a case-insensitive unique
  index, and never guarded against local tracks with no UserHasTrack
  row - either would abort the migration on real data. Backfill now
  dedupes via DISTINCT ON over the normalized name and drops orphaned
  local tracks before the ownership check constraint is added.
2026-07-17 18:00:00 +02:00
Admin daec1a8d05 refactor(backend): group track pipeline state into owned types
Track mixed catalog fields (Title, Artist credit, Duration) with
picture-processing and audio-transcode pipeline state as ten flat
columns plus two computed [NotMapped] flags, making the entity read
like a media-pipeline worksheet.

Introduce TrackPictures and TrackAudio as EF owned types mapped onto
the same existing columns (explicit column names, no schema change -
verified via `dotnet ef migrations has-pending-model-changes`).
Track now reads as Title/Artist/Duration/Pictures/Audio instead of
fourteen loose properties.
2026-07-17 17:58:56 +02:00
Admin 64e1f3903e refactor(backend): read artist credit from TrackArtists join
Drop the now-redundant Track.Artist string column now that every
track (local and YouTube) links its credited artists through
TrackArtist. TrackApplicationResponse formats the display string by
joining TrackArtists ordered by position, and every read query that
projects a track now includes TrackArtists/Artist so the join isn't
empty at response time.
2026-07-17 17:57:22 +02:00
Admin 4a802e44a3 refactor(backend): unify YouTube track creation via a provisioner
Track get-or-create for YouTube videos was duplicated between the
old GetYouTubeTrackStream query handler (which also wrote to the
database) and AddYouTubeTrackToPlayList, with frontend-supplied
metadata (title/artist/duration/thumbnail) trusted at add-to-playlist
time instead of resolving it server-side.

Extract YouTubeTrackProvisioner as the single get-or-create path,
resolving structured artist refs (id + name) from YouTube Music so
multi-artist songs map to distinct Artist rows. Rename
GetYouTubeTrackStream to ResolveYouTubeTrackStream (it writes, so it
is a command, not a query) and delegate to the provisioner.
AddYouTubeTrackToPlayList slims down to (UserId, PlayListId, VideoId)
since all metadata now comes from the provisioner/YouTube API.
2026-07-17 17:56:37 +02:00
Admin b1618a39c4 feat(backend): add Artist entity and track ownership
Track.Artist was a plain string snapshot with no identity, and
UserHasTrack doubled as both "in my library" and "I own this",
conflating collection membership with administrative ownership.

Introduce Artist/TrackArtist to give artist credits their own
identity (needed so YouTube tracks with multiple credited artists,
and future renames, can be modeled), and Track.OwnerUserId to
represent ownership explicitly (local tracks only, enforced by a
check constraint). CreateTrack now provisions the uploader's Artist
row and links it via TrackArtist; DeleteTrack authorizes against
OwnerUserId instead of any UserHasTrack row.
2026-07-17 17:55:32 +02:00
Admin 3f4f248221 fix(backend): make track media fields and external id nullable
Artist, ExternalId, OriginalPictureName and OriginalAudioName were
non-nullable with '' sentinels, which forced the unique (Source,
ExternalId) index to rely on a filtered "<> ''" clause. Make them
truly nullable and drop the empty-string sentinels, backfilling
existing rows to NULL. Also fixes a formatting regression
(`track != null&&`) introduced alongside the nullability change.
2026-07-17 17:53:57 +02:00
Admin 41dccbcecd fix(frontend): remove context menu borders and add icon to the playlist row
Dropped the border from both menu panels (shadow alone separates them from the page), and gave 'Añadir a una playlist' a leading ListMusic icon so it matches the icon+text layout of 'Añadir a la cola'.
2026-07-16 22:48:48 +02:00
Admin 10d385b90a fix(frontend): stop clipping the add-to-playlist submenu
The redesign added overflow-hidden to the outer menu panel to round its corners, but the playlist submenu is absolutely positioned outside that panel's box (left-full/right-full), so it got clipped invisible. Restored the previous approach: rows get their own rounded-lg inside a padded (not overflow-hidden) panel.
2026-07-16 22:47:14 +02:00
Admin 043a2523ad fix(frontend): restyle the track context menu to match the app's design tokens
Replaced the ad-hoc neutral-900/white-10 Tailwind palette with the same --mf-elevated/--mf-border/--mf-text-2 tokens and rounded-xl/shadow recipe the sidebar's own dropdown already uses, so the menu no longer looks foreign to the rest of the app.
2026-07-16 22:44:51 +02:00
Admin fe71473a3f fix(frontend): make add-to-queue insert the track to play next
It previously appended to the end of the queue; 'Añadir a la cola' is expected to mean the track plays right after the current one, so it now splices in right after the currently playing track's index.
2026-07-16 22:44:51 +02:00