Commit Graph
90 Commits
Author SHA1 Message Date
Admin 478b90d75c Make album cover pictures mandatory, unify cover handling with playlists
Albums previously had no cover concept at all: no picture upload
flow, no GET /albums/{id}/cover, and AlbumApplicationResponse's
image-key fields were plumbed but never populated. Description,
Pictures and TrackCount already lived on the shared Album base entity
for both local and external albums, so unifying the cover story only
required building the missing local-album picture pipeline.

Backend, cloning PlayList's existing async picture-processing
architecture exactly (MassTransit routing slip + saga), but making
the picture mandatory instead of optional (matching Track's pattern):

- AlbumPictures moves from all-nullable to required fields.
- New AlbumConfiguration (routes/sizes), deliberately without
  PlayList's preset-picture fallback since a picture is now required
  up front.
- CreateAlbum now takes a mandatory PictureIntentId, validates and
  consumes the upload intent, and kicks off the same
  copy-to-final -> resize -> upload -> update-entity routing slip
  PlayLists use, via a new AlbumProcessingState saga that marks the
  album Failed on a picture-processing failure.
- UpdateAlbum takes an optional NewPictureIntentId to replace the
  cover later.
- New GET /albums/{id}/cover and POST /albums/upload-picture
  endpoints, mirroring the playlist ones.
- New EF migration for the AlbumProcessingState saga table.

Frontend:

- AlbumForm gains a cover dropzone, required on create and optional
  on edit (via ImageDropzone's new `required` prop), uploaded through
  the same presigned-upload helper playlists use.
- New /api/albums/{id}/cover proxy route.
- PlaylistArt gains a generic coverUrl prop so AlbumHeader and the
  album list/detail cards render the real uploaded cover instead of
  only the track mosaic fallback.

Delete-time picture cleanup for albums is intentionally left as a
follow-up, matching the existing gap in playlist deletion.
2026-09-10 15:04:36 +02:00
Admin 29a7fed6a5 Generalize YouTube-backed endpoints behind a source parameter
Removes the last YouTube-specific routes (/youtube/*) and merges them
into the general track/album/playlist endpoints, keyed by a `source`
string validated against TrackSource. This mirrors the earlier track
search merge for albums and playlist-track additions:

- GetAlbums now merges local albums with live YouTube Music search
  results, paginated via a youtubeContinuationToken, deduped against
  already-materialized ExternalAlbum rows.
- New GET /albums/external/{source}/{externalId} and
  GET /tracks/external/{source}/{externalId}/stream replace the
  YouTube-only equivalents.
- POST /playlists/{id}/tracks replaces the separate
  .../youtube-tracks endpoint, accepting an optional Source/ExternalId
  pair alongside TrackId.
- AlbumSearchItemResponse/AlbumsSearchResponse follow the same
  discriminated-union + trailing-HasNextPage-field pattern already
  used for track search, so HasNextPage stays a required field in the
  generated OpenAPI schema.
- Frontend routes renamed accordingly (/albums/youtube/[albumId] ->
  /albums/external/[source]/[externalId], etc.) with a single
  /api/thumbnail proxy replacing /api/youtube/thumbnail.

Musify.Api.Endpoints.YouTubeEndpoints and the standalone
SearchYouTubeAlbums query are removed as a result.
2026-09-10 15:03:36 +02:00
Admin ca04a81e81 Polish typography, button states, and login screen
Applies the display typeface (already used for small section labels)
consistently to every major heading — page titles, hero titles on
album/playlist/mix pages, the home hero, the sidebar/mobile wordmark, and
empty-state titles — instead of only the smaller ones, fixing an
inconsistent visual hierarchy. Also fixes a couple of real bugs found along
the way: Input had a focus border color with no border width to show it,
Button/IconButton had no focus-visible ring at all, and SectionHeading's
letter-spacing only applied when it had an actions slot.

Button's secondary/subtle variants drop their harsh border and flat grey
hover for a softer accent-tinted wash. The accent (and the per-track
extracted accent) is desaturated for a calmer, more pastel feel without
touching lightness, so on-accent text contrast is unaffected.

The login screen gets the same treatment plus a subtle ambient glow (reusing
the home page's existing accent-gradient technique) and a fluid,
viewport-scaled headline instead of a few fixed breakpoints, so it fills the
available space on wide screens instead of staying pinned to one size.
2026-09-10 09:56:16 +02:00
Admin 92a06a3c15 Combine track search with live YouTube Music results
GET /tracks now merges the local catalog page with a live YouTube Music
search (deduplicated against tracks already provisioned locally) whenever a
name filter is given, appending the YouTube hits after the local matches and
carrying a continuation token for further pages. Each item is tagged with its
source via the new TrackSearchItemResponse/TracksSearchResponse shapes,
wrapping the existing TrackApplicationResponse or a raw YouTubeSongResult so
neither type had to change.

GET /youtube/search is removed along with its now-unused handler; the
web-player's explore page, its /api/tracks proxy, and the YouTube filler
helper are updated to call the combined endpoint instead of hitting the two
separately.
2026-09-10 09:55:58 +02:00
Admin d867a60126 Changes 2026-08-18 14:38:00 +02:00
Admin f54ebec0eb Changes 2026-07-25 01:46:59 +02:00
Admin 6412a657e7 Changes 2026-07-24 18:54:31 +02:00
Admin 9132e67371 feat(web-player): rebuild Inicio and Buscar to the design handoff
Applies the handoff's layout and image shapes while keeping the current
Musify design system (Inter, colors, tokens) — no font or palette swap.

Inicio now stacks distinct shelves instead of repeated square grids:
recently played rows, a rail of recently listened albums with an "Álbum"
badge, a "Hecho para ti" bento of mixes, a rail of playlists with a
trailing dashed "Ver todas" tile, tall "Canciones nuevas" posters with a
"Nuevo" badge, a rail of circular top-artist avatars, and a recommended
rail. Buscar splits into Álbumes (square cards + badge), Usuarios (a
circular-avatar directory) and a flat Canciones list.

Top artists and the search user directory are filler data until the
backend exposes them. New shared pieces: Rail, CoverBadge, ArtistAvatar,
MixBentoTile. The orphaned TrackTile and MixCard are removed.
2026-07-22 04:40:14 +02:00
Admin 105681e1db fix(web-player): trim the gap under the home hero 2026-07-21 23:18:11 +02:00
Admin e728b333a9 refactor: tighten the home typography and card craft
The playlist cards were showing "No description was pr…" because the API
stored that English sentence whenever a playlist was created without a
description. Descriptions are optional now and the old rows are cleared.

Section headings drop to 18/20px so they stop competing with the content,
and the space moved from inside the sections to between them.

The three media cards repeated the same anchor, art class and label
block, so they share a MediaCard that also carries the hover lift the
track tiles already had.
2026-07-21 23:14:36 +02:00
Admin b4b9e886e8 fix(web-player): tune the card sizes so the grids scale
The recently played rows stretched to 378px on a 1920 screen and the
discover wall broke into eight narrow columns. Cards now stay in a
narrow band: 295-349px for the rows, 203-218px everywhere else.
2026-07-21 19:16:37 +02:00
Admin 0d34a40931 feat: rework the home layout
Recently played keeps its shape. Mixes and playlists now share one row,
half and half on large screens with smaller cards, capped at six each
with a link to the full list. Albums are the ones the user listened to
most recently, which is a new query joining the listening history with
the album tracks. The endless track wall is now "Descubrir".
2026-07-21 19:06:36 +02:00
Admin 7e697e423b fix(web-player): give the track tables room after the duration 2026-07-21 18:55:56 +02:00
Admin 5585f4fa83 refactor(web-player): let TrackTable declare its own columns
Every one of the seven tables repeated the same header markup and hand
written grid templates, always in the same shape: an optional index, the
title, optional desktop-only meta columns, the duration and an optional
action. TrackTable now takes that shape as props and builds both grid
templates and the headers itself.
2026-07-21 18:50:17 +02:00
Admin 04eab6c6d6 refactor: name an artist mix after the artist alone
The "Mezcla:" prefix repeated what the header label already says, and it
pushed the long titles into an extra line on the detail page.
2026-07-21 18:44:56 +02:00
Admin 4646b6c2a4 feat(web-player): label the mix header with its kind 2026-07-21 18:42:10 +02:00
Admin 4d490dfc0f refactor: stop telling mixes apart by source
A mix should read as one list, so the detail page drops the per-source
counters and the YouTube tag on each row, and the generated subtitles no
longer name Musify or YouTube Music.
2026-07-21 18:39:40 +02:00
Admin a48b9840b8 Change to disc-2 2026-07-21 18:33:21 +02:00
Admin 55fc8ab9d4 refactor(web-player): drop the quick mixes shelf
The server-generated mixes cover the same ground and pick better songs,
so the two shuffle-what-is-on-screen cards are redundant.
2026-07-21 18:25:05 +02:00
Admin 74066ea57d refactor: drop dead code left after the worktree move
ChipGroup is orphaned since the explore rewrite dropped the filter chips.
Mix.GeneratedOn was written on every generation and never read, and it
duplicated CreatedAt, which IAuditable already sets at the same moment.
2026-07-21 18:22:50 +02:00
Admin 2f98982d83 feat: bring over the albums, explore and player work from the worktree
Moves the remaining uncommitted work: the home gets quick mixes and a
"Tus álbumes" shelf, saving a YouTube album now goes through
addYoutubeAlbumToPlaylistAction instead of the SaveYouTubeAlbum command,
and UserAlbum drops its unused ExternalId.

The mixes section keeps its current shape (no refresh action, no badge,
hidden when empty) rather than the older version the worktree carried.
2026-07-21 18:18:23 +02:00
Admin b75c97a696 feat(web-player): label each explore result with its kind 2026-07-21 18:13:46 +02:00
Admin 787664f97b feat(web-player): show the cover mosaic on the explore albums 2026-07-21 18:13:46 +02:00
Admin c1a9485d24 perf(web-player): take the cover track ids from the list response 2026-07-21 18:13:46 +02:00
Admin 496a5f4a0b feat: recommend server-generated mixes from Musify and YouTube Music
Mixes are their own concept, not playlists: they live in their own
Mixes/MixItems tables, cannot be edited and never show up among the
user's playlists. Each item is either a Musify track or a YouTube Music
reference, so YouTube songs stream directly instead of being downloaded.

A daily Hangfire job seeds the mixes from the listening history: one
discovery mix, one daily mix and one per top artist.

The per-user YouTube filler query was already in the working tree and
rides along here because it shares files with this change.
2026-07-21 18:13:07 +02:00
Admin 62b0ab37a9 feat(web-player): page the album results and save YouTube albums
The albums section keeps a Ver más button while YouTube Music has another
page, fed by a new /api/youtube/albums route that forwards the continuation
token. The YouTube album page gains a save button that drops the whole album,
tracks included, into your library and takes you to it.
2026-07-20 20:49:56 +02:00
Admin 29c220a1cf feat(web-player): show YouTube Music albums in the explore search
The Albums chip now returns both your own albums and what YouTube Music has
for the term. A YouTube card opens a read-only album page with its cover,
tracklist and play button, and right-clicking a track still offers to add it
to a playlist through the existing menu.
2026-07-20 20:39:21 +02:00
Admin ed7fca5366 feat(web-player): filter the explore results with a chip group
Under the search box there are now three checkbox chips choosing what the page
shows. Songs keep the existing grid and Albums searches the albums by title;
Users is rendered disabled until there is something behind it.
2026-07-20 20:30:33 +02:00
Admin 76812d1eeb feat(web-player): add the albums pages
A grid of your albums with a create dialog, and a detail page with the
tracklist, inline editing and a picker that lists the songs you uploaded so
they can be dropped into the album. Covers reuse the playlist mosaic since an
album has no artwork of its own yet.
2026-07-20 20:21:22 +02:00
Admin 9cd916c098 fix(web-player): follow the API types where they are nullable
The checked-in schema was stale and typed artist, description and externalId
as always present, so the code trusted values the API returns as null. The
regenerated schema tells the truth; the API-shaped types now admit null and it
is normalised on the way into the player, where the state stays non-null.
2026-07-20 20:20:34 +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 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 7c62f8a8e8 Changes 2026-07-20 16:36:18 +02:00
Admin ca24632fac Responsive design 2026-07-20 16:25:44 +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 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 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