Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ a server behind a portal, offered on native servers too (see
| `HomeView` | `/` | Now Playing. Desktop renders `QueueView variant="full"`; on the mobile shell `/` is only an alias — it replaces itself with `/browse#playing` (queue filled) or `/browse` (empty). The phone's Now Playing is `NowPlayingSheet`, an always-mounted bottom sheet addressed by the `#playing`/`#queue` hash on the current route (see `docs/agents/frontend.md`) |
| `MobileBrowseView` | `/browse` | **Mobile only** — the landing page and the whole navigation surface of the mobile shell (the phone's `AppSidebar`, and where every view's hamburger goes; passes `navRoot` so it shows none itself). One `BrowseShelf` per section — Library (samples the Discovery feed), one per dynamic library (`BrowseAlbumShelf`, newest albums, from the first library on), Playlists, Genres, Radio — each with a heading, a few items in a swipeable strip and a "See all" link. Search and the `UserMenu` account entries (User settings / Admin / About / Change server, desktop app only / Log out — the phone's only logout) sit in the header, the latter behind `⋮`. Redirects to `/library` at desktop width. Replaced the deleted `MobileNavDrawer` |
| `SearchView` | `/search` | Search results |
| `LibraryView` | `/library/:mode?`, `/library/:folderId/:mode?` | Browse the catalog — whole, or one library at a time (`:folderId` is the library's id, Subsonic's `musicFolderId`; not a scan folder). Its views — Discover / Artists / Albums — are real path segments, not a URL hash. The root offers all three (bare `/library` is Discover, then `/library/artists` and `/library/albums`; there is no `/library/discover`), switched from the sidebar's Library block on desktop. Each library is either one sidebar entry in that block or — with `split_views` (`splitViews`, `musicFolderViews` v2) — a sidebar section of its own: a header with its name and an entry per view it offers. The root makes the same choice (the Main library setting, `getMusicFolders`' `catalog` descriptor): its three views as entries, or one "All music" entry. A library offers only the views its admin ticked (`views` + `default_view`, the `musicFolderViews` extension): bare `/library/:folderId` opens its default view, `/library/:folderId/:mode` names another, and a view it does not offer is rewritten back to the bare path. A view switcher — a Discover / Artists / Albums `SelectButton` button group in the header's `#actions`, first before the filters, the same `as-button-group` look as the release-type row; icons only on phones — switches views wherever the sidebar does not: on the phone shell, and on desktop at a single-entry root or inside a single-entry library offering more than one. Choosing a view pushes its path. Discover is a ranked feed of albums + playlists (`getDiscovery` extension, infinite scroll, `DiscoveryFeed` component) — the whole catalog's at the root, the library's own inside one (`musicFolderId`: albums and playlists with a track in the library); there is deliberately no standalone `/discover` route. Albums carries a release-type tab row (All + MusicBrainz primary types: Album/Single/EP/Broadcast/Other) via the `releaseTypeFilter` extension, hidden while the favorites filter is on. It offers only the types the library's albums actually carry (`getReleaseTypes`, `releaseTypeFilter` v2; every type until that answers, and the active `?releaseType=` is always kept) and is dropped entirely when there is none |
| `LibraryView` | `/library/:mode?`, `/library/:folderId/:mode?` | Browse the catalog — whole, or one library at a time (`:folderId` is the library's id, Subsonic's `musicFolderId`; not a scan folder). Its views — Discover / Artists / Groupings (opt-in) / Albums — are real path segments, not a URL hash. The root offers the three base views (bare `/library` is Discover, then `/library/artists` and `/library/albums`; there is no `/library/discover`), and `/library/groupings` once the admin turned Groupings on for it, switched from the sidebar's Library block on desktop. Each library is either one sidebar entry in that block or — with `split_views` (`splitViews`, `musicFolderViews` v2) — a sidebar section of its own: a header with its name and an entry per view it offers. The root makes the same choice (the Main library setting, `getMusicFolders`' `catalog` descriptor): its three views as entries, or one "All music" entry. A library offers only the views its admin ticked (`views` + `default_view`, the `musicFolderViews` extension): bare `/library/:folderId` opens its default view, `/library/:folderId/:mode` names another, and a view it does not offer is rewritten back to the bare path. A view switcher — a Discover / Artists / Albums `SelectButton` button group in the header's `#actions`, first before the filters, the same `as-button-group` look as the release-type row; icons only on phones — switches views wherever the sidebar does not: on the phone shell, and on desktop at a single-entry root or inside a single-entry library offering more than one. Choosing a view pushes its path. Discover is a ranked feed of albums + playlists (`getDiscovery` extension, infinite scroll, `DiscoveryFeed` component) — the whole catalog's at the root, the library's own inside one (`musicFolderId`: albums and playlists with a track in the library); there is deliberately no standalone `/discover` route. Albums carries a release-type tab row (All + MusicBrainz primary types: Album/Single/EP/Broadcast/Other) via the `releaseTypeFilter` extension, hidden while the favorites filter is on. It offers only the types the library's albums actually carry (`getReleaseTypes`, `releaseTypeFilter` v2; every type until that answers, and the active `?releaseType=` is always kept) and is dropped entirely when there is none |
| `AlbumView` | `/album/:id` | Single album detail. The hero eyebrow names its release types (`lib/releaseTypes.ts` `formatReleaseTypes`: primary first, e.g. "Album · Soundtrack"), "Album" when it has none. Hero cover is editable (`updateAlbum` / `albumCoverArt` extension) — the only place a manual album cover is set, since the metadata editor is file-only |
| `ArtistView` | `/artist/:id` | Single artist detail. The discography is grouped into ordered release-type sections (Albums / EPs / Singles / [Broadcast] / Other) via `lib/releaseGrouping.ts`: primary MusicBrainz type per album, untyped→Albums, and non-Album/EP types with fewer than 4 releases folded into Other. `getArtist` also returns the albums the artist only appears on (a track on a soundtrack or compilation — the album's `artists` leaves them out); `splitAppearances` moves those to a trailing "Appears on" section, and Play/Queue skip them (Play is disabled when nothing else is left) |
| `GroupingView` | `/grouping/:id` | A grouping, labelled **Collection** in the UI (the `GROUPING` tag — a franchise, series or box set) and its albums, oldest first, via the `albumGroupings` extension (`getGrouping`). Hero wears the first album's cover; Play/Queue run the albums front to back. Reached from the Groupings view of `LibraryView` (a card grid / list like Artists, no favorites filter — groupings cannot be starred) and from the link under an album's artist in `AlbumView`. Groupings is opt-in per library (a checkbox in the library dialog) and for the root (`MainLibraryPanel`) |
| `PlaylistsView` | `/playlists` | List of playlists |
| `PlaylistDetailView` | `/playlist/:id` | Single playlist detail |
| `GenresView` | `/genres` | Browse by genre |
Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ and `todo list --json` cover the common actions — run `todo --help`.

## Features

- [ ] group BSO collections
- [x] group BSO collections (groupings, see docs/superpowers/specs/2026-11-05-groupings-design.md)
usea new metadata field like collection series, to gropup collections tiggether
in the ui show instead of artist for collections
- [ ] better list view
Expand Down
8 changes: 8 additions & 0 deletions docs/agents/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@ Settled in CLAUDE.md; restated because it decides where every new endpoint goes:
not a foreign key — see [scanning.md](scanning.md)); `Suffix` is the
lowercase extension. `Album`/`Artist`/`Genre` link via join tables
(`AlbumArtist`, `TrackArtist`, `TrackGenre`, `AlbumGenre`).
- `Grouping` — a named container for albums (franchise, series, box set) read
from the `GROUPING` tag: `Name`, `NameNorm` (unique identity). An album belongs
to at most one (`albums.grouping_id`, nullable, not part of album identity);
`tracks.grouping` keeps each file's raw tag so `store.ReconcileGroupings` can
recompute the album's grouping after every scan. Browsed through the
`albumGroupings` `/rest` extension and a `groupings` library view (opt-in per
library, `Library.Views`, and for the root `CatalogSettings.ShowGroupings`;
`model.DefaultLibraryViews()` is what a library created without views gets).
- `WishlistItem` — a MusicBrainz id (`Kind` artist / album / song, unique
with `MBID`) plus a snapshot of the search result it was picked from and a
free-text `Note`. Server-wide, no owner. `MatchID` (+ `MatchAlbumID` for a
Expand Down
2 changes: 1 addition & 1 deletion docs/agents/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ a chosen direction (usually in `TODO.md`). Statuses verified against the code
| setRating | Handler only — **not persisted** | `subsonic/annotation.go`; no rating column yet (TODO.md) |
| Internet radio CRUD | Implemented | `subsonic/radio.go`; writes are admin-gated per spec (`requireAdmin`, error 50; `subsonic.WithAdminChecker`), reads open to all |
| Play queue (cross-device session resume) | Implemented | `subsonic/playqueue.go` — `savePlayQueue`/`getPlayQueue` (spec) plus `savePlayQueueByIndex`/`getPlayQueueByIndex` (`indexBasedQueue` extension). **One stored queue per owner, backed by an INDEX not a track id** (`model.PlayQueue` + `PlayQueueEntry`): a queue may hold the same track twice, so `current` as an id cannot say which copy is playing — the id-based save resolves to the first matching slot. Both variants read/write the same row. A save with no `id` clears the queue (spec's clear call). `GetPlayQueue` drops tracks deleted since the save, shifts `CurrentIndex` onto the survivors, and **zeroes `PositionMs` when the current track itself is gone** — an offset measured in one track is meaningless in another |
| OpenSubsonic extensions | Implemented (17) | `subsonic/extensions.go` — musicFolderViews/Icon, albumList2Index, releaseTypeFilter, internetRadio/playlist/artist/genre/album CoverArt, playlistStar, playlistScrobble, playlistStats, discovery, indexBasedQueue, searchGenres, apiKeyAuthentication, generatedCovers |
| OpenSubsonic extensions | Implemented (18) | `subsonic/extensions.go` — musicFolderViews/Icon, albumGroupings, albumList2Index, releaseTypeFilter, internetRadio/playlist/artist/genre/album CoverArt, playlistStar, playlistScrobble, playlistStats, discovery, indexBasedQueue, searchGenres, apiKeyAuthentication, generatedCovers |
| XML responses (`f=xml`) | Rejected by design (for now) | middleware in `subsonic/subsonic.go` returns an error; TODO.md tracks third-party client compat |
| Transcoding | Not implemented | TODO.md (FFmpeg planned) |
| getArtistInfo/getAlbumInfo, getTopSongs/getSimilarSongs | Not implemented | TODO.md — needs external metadata / play-history analysis |
Expand Down
16 changes: 12 additions & 4 deletions docs/agents/frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -672,12 +672,13 @@ libraries, the same picker, saving on pick through `useUpdateCatalogSettings`
`useMusicFolders`' query (one request, each `select`ing its part); a missing
descriptor means split. Split, the Library block starts with Discover /
Artists / Albums; not split, with one **All music** entry (`/library`,
compass icon, the Library shortcut badge, active on any root view). New libraries default to every view, opening on Discover —
the server's defaults too. `/views…` errors render on the Views row,
compass icon, the Library shortcut badge, active on any root view). New libraries default to Discover / Artists / Albums, opening on Discover —
the server's defaults too (`DEFAULT_LIBRARY_VIEWS`). **Groupings is opt-in:** a fourth checkbox in the views group of the library dialog, unticked for a new library, and a "Show Groupings view" checkbox in `MainLibraryPanel` for the root (`show_groupings` on `PUT /api/v0/libraries/catalog`); the root's `catalog.views` carries it, and the sidebar adds a Groupings entry only when it does. `/views…` errors render on the Views row,
`/default_view` under Opens on.

**`LibraryView`'s views** follow the library: `offeredViews` is every view at
the root and `folder.views` inside a library (the `musicFolderViews`
**`LibraryView`'s views** follow the library: `offeredViews` is the root's
`catalog.views` at the root (the three base views until a descriptor says
otherwise) and `folder.views` inside a library (the `musicFolderViews`
extension); the path's `:mode` shows when offered, else the library's
default (`openingView`). A mode the library does not offer is rewritten to
the bare `/library/:folderId` path (query kept) once the folders are loaded —
Expand All @@ -695,6 +696,13 @@ bare path, every other
view a segment. A library's Discover passes `folderId` to `DiscoveryFeed` /
`useDiscoveryFeed`, so it is that library's own feed.

**Groupings** — shown to users as **Collections** (labels only; the tag, API, routes and code keep "grouping") — (the `albumGroupings` extension; `GroupingGrid` / `GroupingListView` /
`GroupingCard` / `GroupingRow`, fed by `useGroupingTable`; detail in `GroupingView` at
`/grouping/:id`, `useGrouping`). The index reuses the artist index's flattened
letter shape so `VirtualCardGrid` and `AlphabetRail` work unchanged; the favorites
heart is hidden on this view (`supportsFavorites` in `LibraryView`) because groupings
cannot be starred. `AlbumView` links to its grouping under the artist.

**`LibraryFilterBuilder`** (`components/admin/LibraryFilterBuilder.vue`,
inside `LibraryDialog`) is the one place filters are edited. `LibraryDialog`
always sends `filters` on both create and update — never omits the key: on
Expand Down
13 changes: 13 additions & 0 deletions docs/agents/scanning.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,19 @@ album identity; `reconcileTrack` sets it from any track carrying the tag (one th
lacks it never blanks it), so an incremental scan only fills it for files it
re-reads — a full scan stamps the whole catalog.

**Groupings.** `reconcileTrack` copies the file's `GROUPING` tag (first value,
trimmed; `tags.Metadata.Grouping`) onto `tracks.grouping`. It does NOT set the
album's grouping itself: after `Cleanup` (full/incremental scan) and after the
prune in `RescanPaths`, `store.ReconcileGroupings` derives every album's
`grouping_id` from its tracks' stored values — the most frequent normalized name
wins, ties go to the alphabetically first, an album with no tagged track is
un-grouped — and deletes groupings left with no album. Working from stored
values (not the scan's changes) is what keeps it correct after an incremental
scan that skipped unchanged files. TagLib maps the `GROUPING` key per format
(`GRP1` in ID3v2, `©grp` in MP4, `GROUPING` in Vorbis comments); a `TIT1` in an
MP3 reads as `WORK`, not grouping, so such files need retagging (the metadata
editor's Grouping field writes it).

## Targeted re-index (`internal/scanner/rescan.go`, `app/tasks/reindex.go`)

`Scanner.RescanPaths(ctx, scanFolder, absPaths)` re-indexes an explicit list
Expand Down
24 changes: 23 additions & 1 deletion docs/agents/subsonic-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ directly from a handler. Errors use Subsonic numeric codes via `writeError`.

Subsonic IDs are strings; aether encodes the entity type into a prefix
(`subsonic.go`): `ar-` artist, `al-` album, `tr-` track, `pl-` playlist,
`rs-` radio station, `ge-` genre — `encode*ID(uint)` / `decodeID(string)`.
`rs-` radio station, `ge-` genre, `gr-` grouping — `encode*ID(uint)` / `decodeID(string)`.
Endpoints that accept a generic `id` (e.g. `star`, `getCoverArt`) dispatch on
the decoded type. **Any new entity exposed over `/rest` needs its own prefix
here**, and the frontend types (`webui/src/types/subsonic.ts`) consume these
Expand Down Expand Up @@ -174,6 +174,28 @@ favorites, not the playing user's.
"Appears on" section (`ArtistView`, `splitAppearances`) rests on it:
`TestGetArtistAlbumsListEveryAlbumArtist`.

## Groupings (`getGroupings` / `getGrouping`, the `albumGroupings` extension)

A grouping is a named container for albums read from the files' `GROUPING` tag —
a franchise, a series, a box set (the Lord of the Rings scores, Doctor Who
seasons). Handlers in `subsonic/groupings.go`; model, store and scan rules in
[architecture.md](architecture.md) and [scanning.md](scanning.md).

- **`getGroupings?musicFolderId=`** answers `groupings.index[].grouping[]` in the
letter-index shape of `getArtists` (`id`, `name`, `coverArt`, `albumCount`,
`songCount`, `duration`, `year` = earliest album year). **`getGrouping?id=`**
answers one with `album[]` oldest first (undated last, then by name). Both honour
`musicFolderId`: a grouping seen through a library counts and lists only the
albums with a track in it. A bad or non-`gr-` id is error 0, a missing one 70.
- **`getAlbum` carries `grouping: {id, name}`** when the album has one. Lists do
not (the SPA only needs it on the album page).
- **`getCoverArt` accepts `gr-`**: a grouping has no art of its own and wears its
first album's cover (`Store.GroupingCoverAlbum`, `groupingCoverMeta`).
- **`musicFolderViews`**: `views` may include `groupings`. A library offers it only
when its admin ticked it; the root's descriptor adds it only when
`CatalogSettings.ShowGroupings` is on (`rootViews`).
- Advertised as `albumGroupings` v1. Tests: `groupings_test.go`.

## Album lists (`getAlbumList2` / `getAlbumList2Index`, the `releaseTypeFilter` extension)

**`releaseTypeFilter` (v1).** `getAlbumList2` and `getAlbumList2Index` accept an
Expand Down
Loading
Loading