Commit Graph
403 Commits
Author SHA1 Message Date
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
Admin 953fb46f7b feat(frontend): add queue option and enlarge first-level context menu text
The right-click menu now has a plain 'Añadir a la cola' item (appends the track to player.tracks via the new player.addToQueue) alongside the existing playlist submenu, and its first-level text is text-base to match the rest of the menu.
2026-07-16 19:19:08 +02:00
Admin 6233123b02 fix(frontend): enlarge playlist submenu text, thumbnail and padding 2026-07-16 17:40:34 +02:00
Admin 911467f0ef fix(frontend): close the diagonal hover gap in the playlist submenu
The submenu was positioned with a margin gap after the trigger row, so moving the mouse toward it crossed a dead zone with no hovered element and the CSS :hover state dropped, hiding the submenu before the cursor reached it. The gap is now padding on the submenu's own wrapper, which keeps it part of the hoverable box and bridges the trigger to the submenu seamlessly.
2026-07-16 17:38:43 +02:00
Admin 90be0c6538 fix(frontend): extend add-to-playlist context menu to local Musify tracks
Right-clicking a local track in the Musify section now opens the same nested add-to-playlist menu as YouTube results. Also switched the playlist thumbnail corners to rounded-lg.
2026-07-16 17:36:36 +02:00
Admin 4ba20d741e feat(frontend): add server action to attach local tracks to a playlist from explore 2026-07-16 17:36:36 +02:00
Admin ef7ebff9f6 fix(frontend): nested add-to-playlist submenu with playlist thumbnails
The right-click menu now shows a single 'Añadir a una playlist' item; hovering it reveals a submenu listing every playlist with a small cover thumbnail next to its name, instead of dumping the whole playlist list into the first-level menu.
2026-07-16 17:33:38 +02:00
Admin f891002528 fix(frontend): explore search polish
- Rename the local results heading from 'Mi música' to 'Musify'.
- Replace the per-card add-to-playlist icon on YouTube results with a right-click context menu listing all playlists.
- Stop the search box from clearing what the user is typing when a debounced navigation resolves after they've kept typing, by decoupling the input's value from the server-driven query prop.
2026-07-16 17:29:46 +02:00
Admin 6cbcc18aab fix(frontend): wire hardware media-key next/previous track buttons
The player never registered Media Session action handlers, so the OS/keyboard next-track and previous-track keys had nothing to call — only play/pause worked via the browser's built-in audio-element heuristic. Now previoustrack/nexttrack are wired to the player, and metadata/playback state are kept in sync for the lock-screen/OS media widget.
2026-07-16 17:20:49 +02:00
Admin bbc5651ca5 chore(frontend): show 27 local tracks per page in explore 2026-07-16 17:20:48 +02:00
Admin 7e8fed9bd7 fix(frontend): stop duplicating YouTube tracks in recently played after download
The queue/history identity for a song originating on YouTube now always uses its videoId, instead of switching to the local track id once it finishes downloading. Previously that switch made the same song produce two separate entries when merging session and server listening history.
2026-07-16 17:00:35 +02:00
Admin cfa5bdc6ff feat(frontend): hide empty local/YouTube sections when searching in explore 2026-07-16 17:00:27 +02:00
Admin 1050372634 feat(frontend): search local and YouTube Music together without filter tabs 2026-07-16 06:23:21 +02:00
Admin 5f10be4791 feat: show explicit badge on YouTube Music results 2026-07-16 06:15:09 +02:00
Admin f90d76fad9 feat(frontend): unified explore search across local and YouTube Music
Explore now has Todo/Mi musica/YouTube Music filters (searching both by default), YouTube results share the local grid card style, and cards show the artist instead of the upload date.
2026-07-16 06:02:57 +02:00
Admin 4069b36202 feat(frontend): show YouTube tracks in recently played with their thumbnails 2026-07-16 01:15:50 +02:00
Admin c5b0ee3e48 feat(backend): record listening history for YouTube playback
Playing a YouTube song now creates a lightweight track row (without queueing a download) and writes listening history; saving to a playlist queues the download via the new DownloadRequested flag.
2026-07-16 01:15:36 +02:00
Admin 37e0a4a4bf fix(frontend): show real cover art in the player for YouTube tracks 2026-07-16 01:06:20 +02:00
Admin 3baac742d0 feat(frontend): YouTube Music search, playback and playlist saving 2026-07-16 01:06:20 +02:00
Admin 73826b9d08 feat(backend): auto-download YouTube tracks saved to playlists 2026-07-16 01:06:04 +02:00
Admin 11ed8b40ff feat(backend): YouTube Music search and playback resolution endpoints 2026-07-16 01:05:42 +02:00
Admin 2cecd118ed feat(backend): add source, external id and artist to tracks 2026-07-16 01:05:28 +02:00
Admin cc1c1c204f style(frontend): full-width track rows with duration and listens on library page
Rows now span the full page width and show track duration plus
listens count, matching the columns already used elsewhere in the
app. Also trims the trailing bottom padding to match other pages.
2026-07-15 16:18:57 +02:00
Admin af805a172f style(backend): formatting pass on GetListeningHistory 2026-07-15 16:15:37 +02:00
Admin 1438d9bf1a fix(backend): stop loading a user's entire listening history into memory
GetListeningHistory used AsEnumerable() before DistinctBy/Take, which
forced EF Core to materialize every history row for the user just to
keep the last 15 distinct tracks. Now groups by track and takes the
15 most recently played track ids in SQL, then fetches only those.
2026-07-15 16:12:26 +02:00
Admin 135cea6c76 chore(deploy): add pgAdmin to the dev stack
Runs alongside Postgres in the unified compose file at
http://localhost:5050, config via PGADMIN_* env vars.
2026-07-15 16:10:18 +02:00
Admin 02e093d254 style(frontend): even out page-bottom spacing and header sizing
Home and explore had an oversized trailing bottom padding compared to
the rest of the app; brought both down to match. Also matches "Mis
listas" grid/limit on home to "Novedades", and sizes the library page
header the same as the playlists page header.
2026-07-15 16:10:11 +02:00
Admin d1cba7b846 feat(frontend): redesign playlist pages with cover image support
Create and edit playlist modals now upload/preview a cover image
(reusing the presigned-upload flow already used for tracks) and share
identical layout and coloring. Adds a cover proxy route and wires
PlaylistArt to render a playlist's real cover before falling back to
the track mosaic/gradient.

Also reworks the playlist detail page: full-width borderless "Añadir
de tu biblioteca" table with listens count and click-to-play, drops
the empty-playlist placeholder message, and restyles the playlists
grid to match the rest of the app.
2026-07-15 16:10:04 +02:00
Admin e98798894e fix(backend): compute track listens count via SQL instead of an unloaded navigation
TrackApplicationResponse.FromEntity counted track.ListeningHistories,
but none of the queries that built a response ever Included it, so
the collection was always empty and listensCount was always 0. Each
query now projects the count as a correlated subquery before mapping
to the response.
2026-07-15 16:09:54 +02:00