From 8aa03af9a47fa9ea5c4032d26626a8da2ad59c8d Mon Sep 17 00:00:00 2001 From: droideparanoico Date: Thu, 16 Jul 2026 09:17:27 +0200 Subject: [PATCH] Dockerfile --- .dockerignore | 7 + Dockerfile | 17 ++ README.md | 2 +- discdrop-plan.md | 456 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 481 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 discdrop-plan.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..91dbfd0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +data/ +target/ +.git/ +.gitignore +README.md +*.md +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..491b561 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# DiscDrop — Quarkus multi-stage build + +# ── Stage 1: Build ────────────────────────────────────── +FROM maven:3.9-eclipse-temurin-21 AS build +WORKDIR /build +COPY pom.xml ./ +COPY src ./src +RUN mvn package -DskipTests + +# ── Stage 2: Runtime ──────────────────────────────────── +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app +COPY --from=build /build/target/quarkus-app/ /app/ +VOLUME /data +EXPOSE 8080 +USER discdrop +ENTRYPOINT ["java", "-XX:MinHeapFreeRatio=10", "-XX:MaxHeapFreeRatio=20", "-jar", "quarkus-run.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 14a9b8f..fb71434 100644 --- a/README.md +++ b/README.md @@ -26,4 +26,4 @@ The H2 file database lives in `./data/discdrop.mv.db` (gitignored). ## Architecture -See `../discdrop-plan.md` for the full design. +See [discdrop-plan.md](discdrop-plan.md) for the full design. diff --git a/discdrop-plan.md b/discdrop-plan.md new file mode 100644 index 0000000..64c6988 --- /dev/null +++ b/discdrop-plan.md @@ -0,0 +1,456 @@ +# DiscDrop — Implementation Plan + +A self-hosted, single-user app to follow MusicBrainz artists and track their **release groups** (not individual releases, to avoid duplicate vinyl/CD editions). Shows a combined web feed plus an RSS feed of new release groups, ordered by first-release-date. + +> Tagline: _Drop the needle on every new release from the artists you follow._ + +--- + +## 1. Overview & Goals + +| Aspect | Decision | +|---|---| +| Language / framework | Java 17 + Quarkus 3.x | +| Frontend | Server-rendered Qute templates + **htmx** + **daisyUI** (Tailwind) | +| Persistence | **Embedded H2 (file-based)** via Quarkus Hibernate ORM + Panache | +| MBZ access | **Quarkus REST Client (MicroProfile) + JSON** (no external Java library) | +| RSS | Single global RSS 2.0 feed of all followed artists | +| Users | Single-user, local/self-hosted (no auth layer in v1) | +| MBZ identity | `User-Agent: DiscDrop/1.0 (david.alvarez.81@gmail.com)` | +| Build / run | Maven (`./mvnw quarkus:dev`) | + +### Functional requirements covered +1. Modern, adaptive UI with daisyUI theme switching. +2. Logo + favicon reflecting the app's purpose (vinyl "drop"). +3. One combined feed of all followed artists. +4. Artist search autocomplete showing **disambiguation** + **area name**. +5. Centered, responsive header: logo, app name, search box, settings, theme switcher. +6. Feed rows: artist name, release title, release type, release date, MBZ link; ordered by first-release-date; "load more" pagination. +7. Follow artists from search results. +8. Unfollow already-tracked artists (search shows "unfollow" when tracked). +9. Follow/unfollow updates the feed in place (htmx) and clears the search box. +10. Click-outside closes the autocomplete dropdown. +11. Track **release groups**, not releases. +12. Separate **Artists page**: unfollow + per-artist primary-type monitoring toggles. +13. **Settings**: default primary type for new artists (default `album`) + sync schedule (6/12/24 h); closes on click-outside. + +--- + +## 2. Architecture + +``` +┌──────────────── Browser (htmx + daisyUI) ────────────────┐ +│ Header: logo · DiscDrop · search · settings · theme │ +│ Feed (Qute fragments swapped by htmx) │ +│ /artists page · /rss feed │ +└───────────────────────────┬──────────────────────────────┘ + │ HTTP (HTML fragments / RSS / JSON) +┌─────────────── Quarkus App (DiscDrop) ───────────────────┐ +│ Resources (Qute controllers) │ +│ ├─ PageResource GET / (index + feed fragment) │ +│ ├─ SearchResource GET /search/artists?q= (fragment) │ +│ ├─ ArtistResource POST/DELETE follow · GET /artists │ +│ ├─ SettingsResource GET/POST /settings (fragment) │ +│ └─ RssResource GET /rss │ +│ Services │ +│ ├─ FeedService builds ordered feed page │ +│ ├─ ArtistService follow/unfollow, settings │ +│ ├─ SyncService refresh release groups │ +│ └─ SettingsService app-wide settings │ +│ MusicBrainzClient (REST Client, JSON, rate-limited) │ +│ Panache repositories (FollowedArtist, ReleaseGroup, …) │ +└───────────────┬───────────────────────┬──────────────────┘ + H2 file DB MusicBrainz API + (discdrop.mv.db) https://musicbrainz.org/ws/2/ + (1 req/sec, User-Agent) +``` + +**Request flow examples** +- Search typing → `GET /search/artists?q=…` → `MusicBrainzClient.searchArtists()` → Qute fragment (dropdown rows). +- Follow click → `POST /artists/follow` → persist `FollowedArtist` (+ default type settings) → enqueue immediate sync for that artist → return **updated feed fragment** + empty search fragment. +- Scheduled sync → for each followed artist, page through `release-group?artist=…&type=…` → upsert `ReleaseGroup` rows. + +--- + +## 3. Data Model + +Panache entities backed by H2. Keep it small; no premature normalization. + +### `FollowedArtist` +| field | type | notes | +|---|---|---| +| id | Long (auto) | PK | +| mbid | String (UUID) | unique; MBZ artist MBID | +| name | String | display name | +| sortName | String | | +| disambiguation | String | e.g. "90s US grunge band" | +| areaName | String | from `area.name` | +| type | String | Person / Group / … | +| country | String | | +| followedAt | Instant | | +| lastSyncedAt | Instant | nullable | + +### `ArtistTypeSetting` +Per-artist toggle for each **primary type** (requirement 12). +| field | type | notes | +|---|---|---| +| id | Long | PK | +| artist | FollowedArtist | FK | +| primaryType | String | `album` / `single` / `ep` / `broadcast` / `other` | +| enabled | boolean | | + +Primary types come from the MBZ spec: `album, single, ep, broadcast, other`. +On follow, seed one row per primary type with `enabled` derived from the **default primary types** setting (`album` enabled by default). + +### `ReleaseGroup` +Cached release groups (one row per MBZ release group per artist). +| field | type | notes | +|---|---|---| +| id | Long | PK | +| mbid | String (UUID) | unique | +| artist | FollowedArtist | FK | +| title | String | | +| firstReleaseDate | LocalDate | sortable; may be null/partial | +| firstReleaseDateRaw | String | preserve "2024-03" / "2024" granularity | +| primaryType | String | album / single / ep / … | +| secondaryTypes | String | comma-joined (live, compilation, …) | +| mbzUrl | String | `https://musicbrainz.org/release-group/{mbid}` | +| discoveredAt | Instant | when first seen by sync | + +Unique on `mbid`. Index on `(firstReleaseDate DESC)` for feed ordering. + +> **Cover art** needs no stored field — the front-cover URL is deterministic from the MBID: `https://coverartarchive.org/release-group/{mbid}/front` (Cover Art Archive, an MBZ-affiliated service). See §4.5. + +### `AppSetting` +Key/value for global settings (requirement 13). +| key | example value | notes | +|---|---|---| +| `defaultPrimaryTypes` | `album` | comma-list applied to new artists | +| `syncScheduleHours` | `6` | one of `6`, `12`, `24` | +| `theme` | `dark` | optional server-side mirror of client theme | + +--- + +## 4. MusicBrainz Integration + +### 4.1 Endpoints used (all `fmt=json`) + +| Purpose | Method + URL | +|---|---| +| Artist search (autocomplete) | `GET /ws/2/artist?query={q}&limit=10&fmt=json` | +| Browse release groups by artist | `GET /ws/2/release-group?artist={mbid}&type={types}&limit=100&offset={n}&inc=artist-credits&fmt=json&release-group-status=website-default` | +| (Optional) Artist lookup on follow | `GET /ws/2/artist/{mbid}?inc=aliases&fmt=json` | + +Notes: +- `type` accepts primary types joined by `|` (e.g. `album|ep`); maps directly to each artist's enabled `ArtistTypeSetting` rows. +- `release-group-status=website-default` excludes groups that contain only promotional/bootleg/pseudo-release editions — keeps the feed clean. Default on; can later expose `all`. +- Release-group JSON fields we use: `id`, `title`, `first-release-date`, `primary-type`, `secondary-types[]`, `artist-credit[]` (for display name fallback). +- Artist search JSON fields we use: `id`, `name`, `disambiguation`, `area.name`, `type`, `country`, `score`. + +### 4.2 REST Client definition (sketch) + +The `User-Agent` is **not** hardcoded in the interface — it is supplied at runtime by a `ClientHeadersFactory` that reads `discdrop.mbz.user-agent` from `application.properties` (see §10). This keeps MBZ contact info out of source so it can be overridden per environment (dev vs. prod, different maintainer email) without recompiling. + +```java +@ApplicationScoped +@RegisterRestClient(configKey = "musicbrainz") +@RegisterClientHeaders(MusicBrainzHeadersFactory.class) +public interface MusicBrainzClient { + @GET @Path("/artist") + ArtistSearchResult searchArtists(@QueryParam("query") String q, + @QueryParam("limit") int limit, + @QueryParam("fmt") String fmt); + + @GET @Path("/release-group") + ReleaseGroupBrowseResult browseReleaseGroups(@QueryParam("artist") String mbid, + @QueryParam("type") String types, + @QueryParam("limit") int limit, + @QueryParam("offset") int offset, + @QueryParam("inc") String inc, + @QueryParam("fmt") String fmt, + @QueryParam("release-group-status") String status); +} +``` + +```java +@ApplicationScoped +public class MusicBrainzHeadersFactory implements ClientHeadersFactory { + @ConfigProperty(name = "discdrop.mbz.user-agent") + String userAgent; + + @Override + public MultivaluedMap update(MultivaluedMap incoming, + MultivaluedMap outgoing) { + outgoing.putSingle("User-Agent", userAgent); + return outgoing; + } +} +``` + +Base URI and other REST Client settings are externalized via `quarkus.rest-client."musicbrainz".*` keys in `application.properties` (see §10). + +### 4.3 Rate limiting (mandatory: ≤ 1 req/sec) + +A single shared gate that **all** MBZ calls pass through: +- `MusicBrainzService` wraps the client with a synchronized `acquire()` that enforces ≥1000 ms between outgoing requests (track `lastCallMillis`). +- The background sync loop naturally spaces calls (one artist/page at a time). +- Live search also goes through `acquire()`; combined with 300 ms client-side debounce, this keeps us compliant even with fast typing. + +### 4.4 DTOs (Jackson) +`ArtistSearchResult { count, artists[] }` → `ArtistDto { id, name, disambiguation, area { name }, type, country, score }`. +`ReleaseGroupBrowseResult { "release-group-count", "release-group-offset", "release-groups[] }` → `ReleaseGroupDto { id, title, first-release-date, primary-type, secondary-types[], artist-credit[] }`. + +### 4.5 Cover Art Archive +Cover art comes from the **Cover Art Archive** (`https://coverartarchive.org`), the MBZ-affiliated image service hosted by the Internet Archive. No API key required. +- **Front cover by release-group MBID**: `https://coverartarchive.org/release-group/{mbid}/front` — responds with `307` redirect to the archived image (JPEG), or `404` if no art exists. +- This URL is **deterministic from the MBID**, so: + - No extra HTTP calls during sync. + - No `coverArtUrl` field on `ReleaseGroup` needed. + - Templates render `` — the browser follows the redirect, and a small JS `onerror` swaps to a local placeholder when a release group has no art (common for bootlegs/obscure releases). +- **Optional optimization (later)**: during sync, issue a `HEAD` per release group and store a `hasCoverArt` boolean to avoid any broken-image flashes. Defer this unless the `onerror` flicker is bothersome; it adds N calls per sync and is unnecessary for v1. +- Rate limits on `coverartarchive.org` are more lenient than the MBZ web service, and since we hit it client-side (browser ``) or not at all, it never contends with our 1 req/sec MBZ gate. + +--- + +## 5. Sync / Scheduler + +- `SyncService` with a `@Scheduled` method. The cadence is read from `AppSetting.syncScheduleHours` and reconfigured at runtime (Quarkus supports programmatic scheduler via `Scheduler` / `ScheduledExecutor`, or simply restart after settings change for v1). +- Per run, iterate `FollowedArtist` rows; for each, build the `type=` filter from its enabled `ArtistTypeSetting` rows, page through release groups (offset increments by received count, up to `limit=100`), and upsert into `ReleaseGroup`. +- After an artist is processed, update `lastSyncedAt`. +- **Immediate sync on follow**: when a user follows an artist, trigger a one-off sync for that artist (async, fire-and-forget) so the feed populates without waiting for the next schedule. +- Honor rate limit between every MBZ call. + +--- + +## 6. UI / UX + +### 6.1 Layout & theming +- Single base template (`templates/index.html`) with a sticky **header** and the **feed** below. +- daisyUI via CDN for rapid dev (`daisyui` full CSS + Tailwind Play CDN); switch to a built/minified local Tailwind CSS for production. +- Theme switching: toggle `data-theme` on `` (e.g. `light`/`dark` or a daisyUI pair like `nord`/`dim`). Persist in `localStorage` for instant restore; optionally mirror to `AppSetting.theme`. Small vanilla JS handler on the theme switcher button. + +### 6.2 Header (centered, responsive, consistent gap) +A flex container, `justify-center`, `gap-4`, wrapping gracefully on narrow screens: +`[logo] [DiscDrop] [search box] [settings btn] [theme switcher]` + +- **Logo**: small SVG (see §7). +- **App name**: "DiscDrop" wordmark. +- **Search box**: text input with autocomplete dropdown anchored below it. +- **Settings button**: opens a dropdown panel (§6.6). +- **Theme switcher**: icon button cycling/toggling themes. + +### 6.3 Search + autocomplete (requirements 4, 7, 8, 9, 10) +- Input wired with htmx: `hx-get="/search/artists?q=..."`, `hx-trigger="input changed delay:300ms, keyup changed"`, `hx-target="#search-dropdown"`, `hx-swap="innerHTML"`. +- Dropdown rows render: **name** · **disambiguation** (muted) · **area name** (badge). Each row carries a **Follow**/**Unfollow** button depending on whether the MBID is already in `FollowedArtist`. +- Follow/Unfollow buttons: `hx-post="/artists/follow"` / `hx-delete="/artists/{mbid}"` with: + - `hx-target="#feed-list"` and `hx-swap="innerHTML"` (refresh feed), **and** + - `hx-on::after-request="this.closest('form').reset(); document.querySelector('#search-dropdown').innerHTML='';"` (clear input + close dropdown) — satisfying requirement 9. +- **Click-outside to close** (requirement 10): a tiny JS helper listens for `document` clicks; if the target is outside `#search-wrap`, it empties `#search-dropdown`. (Same helper pattern reused for settings.) + +### 6.4 Feed (requirements 3, 6, 11) +- Server renders the first page of release groups ordered by `firstReleaseDate DESC`. +- Each row: **cover art thumbnail** (see §4.5 — ``, sized ~64–96px, lazy-loaded), **artist name** (link to artist page section), **release title**, **release type** badge(s) (primary + secondary), **release date** (human-friendly, with raw granularity preserved), and an **external link** to `https://musicbrainz.org/release-group/{mbid}`. +- **Load more**: a button `hx-get="/feed?offset={N+page}" hx-target="this" hx-swap="outerHTML"` that appends the next page and re-emits itself with the new offset, until exhausted (then no button). + +### 6.5 Artists page (requirement 12) +- Route `/artists` listing every `FollowedArtist` as a card/row with: + - name + disambiguation + area, + - **Unfollow** button (`hx-delete="/artists/{mbid}"`, target the row, swap `outerHTML` → removes row), + - **primary-type toggles**: a set of daisyUI checkboxes/switches for `album, single, ep, broadcast, other`; each change `hx-post="/artists/{mbid}/types"` (persist + re-sync that artist) with `hx-swap="none"` and an out-of-band feed refresh. + +### 6.6 Settings (requirement 13) +- Settings button opens a dropdown panel (anchored, same click-outside helper). +- Controls: + - **Default primary types** for newly followed artists (checkbox set; default `album` checked) → `AppSetting.defaultPrimaryTypes`. + - **Sync schedule**: radio/select among `6h / 12h / 24h` → `AppSetting.syncScheduleHours`. +- Save: `hx-post="/settings"` with `hx-swap="innerHTML"` on the panel (re-render + confirm). Click-outside closes (does not save unsaved changes in v1, or autosave on change — pick autosave-on-change to keep it simple). + +--- + +## 7. Branding: Logo & Favicon + +Concept: **a vinyl record dropping into a slot** — reinforces "DiscDrop". + +**Logo SVG (inline, scales in header)** +- Outer black circle (vinyl) with 2–3 thin concentric groove rings. +- Colored center label (accent theme color) with a tiny spindle hole. +- A subtle downward chevron / motion line at the bottom-right indicating "drop". +- Wordmark "DiscDrop" next to it in the header. + +**Favicon** +- Simplified 32×32 / SVG: just the vinyl circle + center label + a small downward arrow. Provide `favicon.svg` (modern browsers) plus a generated `favicon.ico`/`192.png` for broader support. + +Assets live in `src/main/resources/static/img/` and are referenced from the base template (`` and header ``). + +--- + +## 8. htmx Interaction Flows (summary) + +| Action | Request | Targets / effects | +|---|---|---| +| Type in search | `GET /search/artists?q=` | swap `#search-dropdown` | +| Follow | `POST /artists/follow` | swap `#feed-list` + clear search + close dropdown | +| Unfollow (from search) | `DELETE /artists/{mbid}` | swap `#feed-list` + clear search + close dropdown | +| Unfollow (artists page) | `DELETE /artists/{mbid}` | remove row (`outerHTML`) + OOB feed refresh | +| Toggle artist type | `POST /artists/{mbid}/types` | OOB `#feed-list` refresh | +| Load more | `GET /feed?offset=` | append rows, replace button | +| Save settings | `POST /settings` | re-render panel | +| Theme switch | (client JS) | toggle `data-theme`, save `localStorage` | + +Out-of-band swaps (`hx-swap-oob`) keep the feed consistent when follow/unfollow/type-toggle happens from anywhere but the feed itself. + +--- + +## 9. RSS Feed + +- `GET /rss` returns **RSS 2.0** (`application/rss+xml`; Qute template generating XML, or a small builder). +- Single global feed: latest N (e.g. 50) release groups across all followed artists, ordered by `firstReleaseDate DESC`. +- Each ``: title = `{artist} – {title}`; link = `https://musicbrainz.org/release-group/{mbid}`; pubDate = `firstReleaseDate`; description = type(s) + date; `` = the MBZ MBID (stable). +- **Cover art**: each item embeds the front cover via the [Media RSS](https://www.rssboard.org/media-rss) namespace (`xmlns:media="http://search.yahoo.com/mrss/"`): + ```xml + + ``` + Media RSS is preferred over a plain `` because `` requires a `length` (byte size) attribute we don't know without a HEAD probe, whereas `` has no such requirement and is widely supported by modern RSS readers. Add `xmlns:media` to the `` root element. +- Channel metadata: "DiscDrop – New Releases", link to the app root. +- Discoverable via `` in the base template. + +--- + +## 10. Project Structure + +``` +discdrop/ +├── pom.xml +├── README.md +├── src/main/java/io/discdrop/ +│ ├── DiscDropApp.java +│ ├── resource/ +│ │ ├── PageResource.java # GET / (index + feed fragment), GET /feed +│ │ ├── SearchResource.java # GET /search/artists +│ │ ├── ArtistResource.java # follow/unfollow, GET /artists, type toggles +│ │ ├── SettingsResource.java # GET/POST /settings +│ │ └── RssResource.java # GET /rss +│ ├── service/ +│ │ ├── FeedService.java +│ │ ├── ArtistService.java +│ │ ├── SyncService.java +│ │ └── SettingsService.java +│ ├── mbz/ +│ │ ├── MusicBrainzClient.java # REST Client interface (no hardcoded User-Agent) +│ │ ├── MusicBrainzHeadersFactory.java # injects discdrop.mbz.user-agent from config +│ │ ├── MusicBrainzService.java # rate-limited wrapper +│ │ └── dto/{ArtistSearchResult,ArtistDto,ReleaseGroupBrowseResult,ReleaseGroupDto}.java +│ └── persistence/ +│ ├── FollowedArtist.java +│ ├── ArtistTypeSetting.java +│ ├── ReleaseGroupEntity.java +│ └── AppSetting.java +├── src/main/resources/ +│ ├── application.properties +│ ├── templates/ +│ │ ├── index.html # base: header + feed +│ │ ├── artists.html +│ │ ├── rss.xml +│ │ └── fragments/ +│ │ ├── feed-list.html +│ │ ├── feed-row.html +│ │ ├── search-dropdown.html +│ │ ├── settings-panel.html +│ │ └── artist-row.html +│ └── static/ +│ ├── img/{logo.svg, favicon.svg, favicon.ico, no-cover.svg} +│ ├── css/ (daisyUI/Tailwind, dev via CDN or built) +│ └── js/app.js (theme switch + click-outside helper) +``` + +### Key `application.properties` +```properties +quarkus.application.name=DiscDrop +quarkus.http.port=8080 + +# H2 file DB +quarkus.datasource.db-kind=h2 +quarkus.datasource.jdbc.url=jdbc:h2:file:./data/discdrop;DB_CLOSE_DELAY=-1 +quarkus.datasource.username=sa +quarkus.datasource.password= +quarkus.hibernate-orm.database.generation=update + +# MusicBrainz REST client +quarkus.rest-client."musicbrainz".url=https://musicbrainz.org/ws/2 +# User-Agent (MBZ requires a meaningful contact string) — override per environment, never hardcode in code +discdrop.mbz.user-agent=DiscDrop/1.0 (david.alvarez.81@gmail.com) +discdrop.mbz.rate-limit-ms=1000 +discdrop.feed.page-size=25 +discdrop.rss.item-count=50 +``` +On startup, validate that `discdrop.mbz.user-agent` is non-empty (fail fast with a clear `StartupException` rather than emitting anonymous requests MBZ may block). + +### Maven dependencies (essentials) +- `quarkus-resteasy-reactive-qute`, `quarkus-rest-client-jackson`, `quarkus-hibernate-orm`, `quarkus-jdbc-h2`, `quarkus-panache`, `quarkus-scheduler`, `quarkus-resteasy-reactive-jackson`. + +--- + +## 11. Implementation Phases + +### Phase 0 — Scaffold (0.5 day) +- `quarkus create app` (Java 17, Maven, REST + Qute + Hibernate + H2 + Scheduler + REST Client). +- Base template with daisyUI CDN, htmx CDN, logo/favicon placeholders, header layout. +- Verify `./mvnw quarkus:dev` boots and renders the header. + +### Phase 1 — MBZ client + search (1 day) +- `MusicBrainzClient` + DTOs + rate-limited `MusicBrainzService`. +- `/search/artists` returning Qute dropdown fragment (name, disambiguation, area). +- Click-outside close helper. + +### Phase 2 — Persistence + follow/unfollow (1 day) +- Panache entities + repositories; `SettingsService` defaults. +- Follow/unfollow endpoints; seed `ArtistTypeSetting` from default primary types. +- Immediate one-off sync on follow; feed fragment endpoint. +- Wire follow buttons in search dropdown; clear search + refresh feed via htmx. + +### Phase 3 — Sync + feed (1 day) +- `SyncService` scheduler (configurable 6/12/24 h); paged release-group browse + upsert. +- Feed ordering by `firstReleaseDate DESC`, row fragment with **cover art thumbnail** (Cover Art Archive URL + `onerror` placeholder), **load more** pagination. +- MBZ external links on each row. + +### Phase 4 — Artists page + type toggles (0.5 day) +- `/artists` page with unfollow + per-artist primary-type switches; OOB feed refresh on change. + +### Phase 5 — Settings panel (0.5 day) +- Settings dropdown: default primary types + sync schedule; autosave-on-change; click-outside close. + +### Phase 6 — RSS + branding polish (0.5 day) +- `/rss` RSS 2.0 with `` cover art per item; `` discovery. +- Finalize logo/favicon SVG + `no-cover.svg` placeholder; theme switcher; responsive pass. + +### Phase 7 — Hardening (0.5 day) +- Error states (MBZ down / rate-limited / empty feed) with friendly UI. +- README with run + config instructions. +- Optional: replace daisyUI/Tailwind CDN with a built local CSS for production. + +**Estimated total: ~5–6 days.** + +--- + +## 12. Out of Scope (v1) / Future +- Multi-user / auth (currently single-user local). +- Cover-art `HEAD` probing during sync (to pre-flag missing art and avoid broken-image flashes); the `onerror` placeholder handles this client-side for v1. +- Secondary-type per-artist filters (only primary types are toggled per requirement 12; secondary types are shown as badges). +- Notifications (email/webhook) beyond RSS. +- Migration to PostgreSQL (entity model is portable; only datasource config changes). +- Replacing the daisyUI/Tailwind CDN with a production Tailwind build. + +--- + +## 13. Key Design Decisions & Rationale +- **Release groups, not releases** (req 11): browse `release-group` with `inc=artist-credits`; one row per group, no duplicate vinyl/CD. +- **`release-group-status=website-default`**: filters promo/bootleg/pseudo-release-only groups → cleaner feed. +- **Server-side rate limiting shared by search + sync**: guarantees the 1 req/sec MBZ rule regardless of UI activity. +- **Quarkus REST Client + JSON** over the dead/GPL `musicbrainzws2-java` library: lighter, idiomatic, no licensing constraints. +- **Externalized `User-Agent` via `ClientHeadersFactory`**: the MBZ contact string lives in `application.properties` (`discdrop.mbz.user-agent`), not in code — so it can be overridden per environment and updated without a recompile. App fails fast at startup if the property is missing, to avoid sending anonymous requests MBZ may block. +- **H2 file-based**: zero-ops for a personal app; portable to Postgres later by changing datasource props. +- **htmx + Qute fragments**: the feed, search dropdown, and settings all swap HTML fragments — no SPA needed; matches requirements 9 and 10 (in-place updates, dropdown close). +- **Cover art via deterministic Cover Art Archive URLs**: `https://coverartarchive.org/release-group/{mbid}/front` needs no per-item API call or storage — the URL is derived from the MBID, fetched lazily by the browser ``, with a JS `onerror` placeholder for release groups without art. Keeps sync fast and the data model lean. +- **Media RSS `` over ``** for RSS images: avoids the mandatory `length` (byte-size) attribute that `` requires and that we'd need a HEAD probe to obtain. +- **Autosave settings on change**: avoids a "save" button and keeps the settings panel simple (closes on click-outside per req 13).