Compare commits
18
Commits
e5c41ae8ab
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ad3b9d75 | ||
|
|
9e3bd5e916 | ||
|
|
b17a794d4f | ||
|
|
846e27b95f | ||
|
|
508bf77b6d | ||
|
|
bc0f516bf3 | ||
|
|
7f107420a8 | ||
|
|
2434d7301d | ||
|
|
406d987e94 | ||
|
|
8f6e891bc6 | ||
|
|
9019ead439 | ||
|
|
827e87a8a8 | ||
|
|
4ef83cd701 | ||
|
|
03e5b2810c | ||
|
|
11aba60109 | ||
|
|
4236f7b7f7 | ||
|
|
d7d68b9ab0 | ||
|
|
5861da89b3 |
@@ -0,0 +1,2 @@
|
||||
# Required: MusicBrainz User-Agent (must be a meaningful contact string)
|
||||
MBZ_USER_AGENT=DiscDrop/1.0 ([email protected])
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Extract tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: droideparanoico/discdrop
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -2,6 +2,8 @@
|
||||
target/
|
||||
*.jar
|
||||
*.war
|
||||
mvnw
|
||||
mvnw.cmd
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
@@ -21,5 +23,8 @@ data/
|
||||
*.mv.db
|
||||
*.trace.db
|
||||
|
||||
# Environment (personal config)
|
||||
.env
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 droideparanoico
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,29 +1,95 @@
|
||||
# DiscDrop
|
||||
# DiscDrop 💿
|
||||
|
||||
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.
|
||||
Drop the needle on every new release from the artists you follow. DiscDrop tracks MusicBrainz **release groups** and presents them as a combined web feed plus RSS.
|
||||
|
||||
Built with Java 21 + Quarkus, htmx, and daisyUI.
|
||||
|
||||
## Run (dev)
|
||||
### Feed view
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
### 🔍 Search & follow
|
||||
Search MusicBrainz by artist name with autocomplete — see disambiguation and area at a glance. Follow any artist with one click; the feed updates immediately via htmx without a full page reload.
|
||||
|
||||
### 📡 Combined feed
|
||||
Every release group from every followed artist, sorted by first-release date. Cover art is fetched lazily from the Cover Art Archive — no extra API calls during sync. Load more pagination keeps the page snappy.
|
||||
|
||||
Each row shows cover art thumbnail, artist name, release title, type badges (album / single / EP / …), and release date with an external link to MusicBrainz.
|
||||
|
||||
### 🎚️ Per-artist type toggles
|
||||
Control which release types appear in your feed per artist — toggle `album`, `single`, `ep`, `broadcast`, and `other` independently. Changes trigger a re-sync and refresh the feed in-place.
|
||||
|
||||
### ⏱️ Scheduled sync
|
||||
A background job refreshes release groups on a configurable schedule (6 / 12 / 24 hours). Follow a new artist and it syncs immediately, so the feed populates without waiting.
|
||||
|
||||
### 📰 RSS feed
|
||||
A global RSS 2.0 feed with Media RSS cover art — subscribe from your reader of choice. Discoverable via `<link rel="alternate">` from the app root.
|
||||
|
||||
### 📅 Future release filter
|
||||
Hide unreleased releases from the feed, the RSS feed, or both — independently configurable from the settings panel. Releases without a date are always shown.
|
||||
|
||||
### 🌗 Theme switching
|
||||
Built-in light/dark theme toggle, persisted in `localStorage`.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### With Docker Compose (easiest)
|
||||
|
||||
```bash
|
||||
# 1. Configure your MusicBrainz User-Agent
|
||||
cp .env.example .env
|
||||
# Edit .env with your contact email
|
||||
|
||||
# 2. Start
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Open http://localhost:8080 — RSS at http://localhost:8080/rss
|
||||
|
||||
### With Docker
|
||||
|
||||
```bash
|
||||
docker run -p 8080:8080 \
|
||||
-v $(pwd)/data:/app/data \
|
||||
-e MBZ_USER_AGENT="DiscDrop/1.0 ([email protected])" \
|
||||
droideparanoico/discdrop
|
||||
```
|
||||
|
||||
> **`MBZ_USER_AGENT` is required.** MusicBrainz blocks clients without a meaningful `User-Agent`. Set it to your contact email.
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Terminal — Quarkus dev server
|
||||
./mvnw quarkus:dev
|
||||
```
|
||||
|
||||
App at http://localhost:8080 — RSS at http://localhost:8080/rss
|
||||
Open http://localhost:8080
|
||||
|
||||
## Configuration (`application.properties`)
|
||||
## Configuration
|
||||
|
||||
| Property | Default | Notes |
|
||||
|---|---|---|
|
||||
| `discdrop.mbz.user-agent` | `DiscDrop/1.0 ([email protected])` | **Required.** MusicBrainz needs a meaningful contact string. Override per environment. |
|
||||
| `quarkus.rest-client."musicbrainz".url` | `https://musicbrainz.org/ws/2` | MBZ API base. |
|
||||
| `discdrop.mbz.rate-limit-ms` | `1000` | Enforced gap between MBZ calls (≤1 req/s). |
|
||||
| `discdrop.feed.page-size` | `25` | Feed page size / load-more batch. |
|
||||
| `discdrop.rss.item-count` | `50` | RSS items. |
|
||||
All settings are in `application.properties`. Key values can be overridden via environment variables:
|
||||
|
||||
The H2 file database lives in `./data/discdrop.mv.db` (gitignored).
|
||||
| Property | Env var | Default | Notes |
|
||||
|---|---|---|---|
|
||||
| `discdrop.mbz.user-agent` | `MBZ_USER_AGENT` | *(required, no default)* | Contact string for MusicBrainz |
|
||||
| `quarkus.rest-client."musicbrainz".url` | — | `https://musicbrainz.org/ws/2` | MBZ API base |
|
||||
| `discdrop.mbz.rate-limit-ms` | — | `1000` | Gap between MBZ calls (≤1 req/s) |
|
||||
| `discdrop.feed.page-size` | — | `25` | Feed page / load-more batch |
|
||||
| `discdrop.rss.item-count` | — | `50` | RSS item count |
|
||||
| `discdrop.sync.default-schedule-hours` | — | `24` | Default sync interval |
|
||||
| `discdrop.sync.default-primary-types` | — | `album` | Default types for new artists |
|
||||
| `hideFutureFeed` | — | `false` | Hide unreleased from web feed |
|
||||
| `hideFutureRss` | — | `false` | Hide unreleased from RSS feed |
|
||||
|
||||
The H2 file database lives in `./data/discdrop.mv.db`
|
||||
|
||||
## Architecture
|
||||
|
||||
See [discdrop-plan.md](discdrop-plan.md) for the full design.
|
||||
- **Backend**: Quarkus (Java 21, JAX-RS, Hibernate ORM with Panache) — H2 file database, zero external services
|
||||
- **Frontend**: Server-rendered Qute templates + htmx + daisyUI (Tailwind) — no SPA framework
|
||||
- **MusicBrainz**: REST Client (MicroProfile) with rate-limited access, externalized `User-Agent`
|
||||
- **Sync**: Quarkus Scheduler — configurable cadence, immediate sync on follow
|
||||
- **RSS**: RSS 2.0 with Media RSS cover art, Qute XML template
|
||||
- **Cover art**: Deterministic URLs from Cover Art Archive — fetched lazily by the browser, no extra sync overhead
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
# 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 ([email protected])` |
|
||||
| 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<String, String> update(MultivaluedMap<String, String> incoming,
|
||||
MultivaluedMap<String, String> 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 `<img src="https://coverartarchive.org/release-group/{mbid}/front" onerror="this.src='/img/no-cover.svg'">` — 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 `<img>`) 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 `<html>` (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 — `<img src="https://coverartarchive.org/release-group/{mbid}/front" onerror="…no-cover placeholder…">`, 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 (`<link rel="icon" ...>` and header `<img>`).
|
||||
|
||||
---
|
||||
|
||||
## 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 `<item>`: title = `{artist} – {title}`; link = `https://musicbrainz.org/release-group/{mbid}`; pubDate = `firstReleaseDate`; description = type(s) + date; `<guid>` = 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:content url="https://coverartarchive.org/release-group/{mbid}/front"
|
||||
type="image/jpeg" medium="image" />
|
||||
```
|
||||
Media RSS is preferred over a plain `<enclosure>` because `<enclosure>` requires a `length` (byte size) attribute we don't know without a HEAD probe, whereas `<media:content>` has no such requirement and is widely supported by modern RSS readers. Add `xmlns:media` to the `<rss>` root element.
|
||||
- Channel metadata: "DiscDrop – New Releases", link to the app root.
|
||||
- Discoverable via `<link rel="alternate" type="application/rss+xml" ...>` 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 ([email protected])
|
||||
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 `<media:content>` cover art per item; `<link rel="alternate">` 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 `<img>`, with a JS `onerror` placeholder for release groups without art. Keeps sync fast and the data model lean.
|
||||
- **Media RSS `<media:content>` over `<enclosure>`** for RSS images: avoids the mandatory `length` (byte-size) attribute that `<enclosure>` 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).
|
||||
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
discdrop:
|
||||
image: droideparanoico/discdrop
|
||||
container_name: discdrop
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
# Configure here your data directory if you want persistence.
|
||||
- ./data:/app/data
|
||||
env_file:
|
||||
- .env
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 277 KiB |
@@ -1,295 +0,0 @@
|
||||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
@@ -1,189 +0,0 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
@@ -6,6 +6,7 @@ import io.discdrop.persistence.FollowedArtist;
|
||||
import io.discdrop.persistence.ReleaseGroupEntity;
|
||||
import io.discdrop.service.ArtistService;
|
||||
import io.discdrop.service.FeedService;
|
||||
import io.discdrop.service.SettingsService;
|
||||
import io.discdrop.service.SyncService;
|
||||
import io.quarkus.qute.Location;
|
||||
import io.quarkus.qute.Template;
|
||||
@@ -34,6 +35,9 @@ public class ArtistResource {
|
||||
@Inject
|
||||
FeedService feedService;
|
||||
|
||||
@Inject
|
||||
SettingsService settingsService;
|
||||
|
||||
@Inject
|
||||
@Location("fragments/feed-list.html")
|
||||
Template fragments_feed_list;
|
||||
@@ -113,7 +117,8 @@ public class ArtistResource {
|
||||
if (offset < 0) {
|
||||
offset = 0;
|
||||
}
|
||||
List<ReleaseGroupEntity> rows = feedService.feedPage(offset);
|
||||
boolean hideFuture = settingsService.isHideFutureFeed();
|
||||
List<ReleaseGroupEntity> rows = feedService.feedPage(offset, hideFuture);
|
||||
int nextOffset = offset + rows.size();
|
||||
boolean hasMore = rows.size() == feedService.pageSize();
|
||||
return fragments_feed_list.data("rows", rows)
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.discdrop.resource;
|
||||
import io.discdrop.persistence.FollowedArtist;
|
||||
import io.discdrop.persistence.ReleaseGroupEntity;
|
||||
import io.discdrop.service.FeedService;
|
||||
import io.discdrop.service.SettingsService;
|
||||
import io.discdrop.service.SyncService;
|
||||
import io.quarkus.qute.Location;
|
||||
import io.quarkus.qute.Template;
|
||||
@@ -24,6 +25,9 @@ public class PageResource {
|
||||
@Inject
|
||||
SyncService syncService;
|
||||
|
||||
@Inject
|
||||
SettingsService settingsService;
|
||||
|
||||
@Inject
|
||||
Template index;
|
||||
|
||||
@@ -38,7 +42,8 @@ public class PageResource {
|
||||
if (offset < 0) {
|
||||
offset = 0;
|
||||
}
|
||||
List<ReleaseGroupEntity> rows = feedService.feedPage(offset);
|
||||
boolean hideFuture = settingsService.isHideFutureFeed();
|
||||
List<ReleaseGroupEntity> rows = feedService.feedPage(offset, hideFuture);
|
||||
int nextOffset = offset + rows.size();
|
||||
boolean hasMore = rows.size() == feedService.pageSize();
|
||||
return fragments_feed_list.data("rows", rows)
|
||||
@@ -52,7 +57,8 @@ public class PageResource {
|
||||
@GET
|
||||
@Transactional
|
||||
public TemplateInstance index() {
|
||||
List<ReleaseGroupEntity> rows = feedService.feedPage(0);
|
||||
boolean hideFuture = settingsService.isHideFutureFeed();
|
||||
List<ReleaseGroupEntity> rows = feedService.feedPage(0, hideFuture);
|
||||
boolean hasMore = rows.size() == feedService.pageSize();
|
||||
return index.data("rows", rows)
|
||||
.data("nextOffset", rows.size())
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.discdrop.resource;
|
||||
|
||||
import io.discdrop.persistence.ReleaseGroupEntity;
|
||||
import io.discdrop.service.FeedService;
|
||||
import io.discdrop.service.SettingsService;
|
||||
import io.quarkus.qute.Location;
|
||||
import io.quarkus.qute.Template;
|
||||
import io.quarkus.qute.TemplateInstance;
|
||||
@@ -27,6 +28,9 @@ public class RssResource {
|
||||
@Inject
|
||||
FeedService feedService;
|
||||
|
||||
@Inject
|
||||
SettingsService settingsService;
|
||||
|
||||
@Inject
|
||||
@Location("rss.xml")
|
||||
Template rss;
|
||||
@@ -40,7 +44,8 @@ public class RssResource {
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_XML)
|
||||
public TemplateInstance feed() {
|
||||
List<ReleaseGroupEntity> entities = feedService.latest(itemCount);
|
||||
boolean hideFuture = settingsService.isHideFutureRss();
|
||||
List<ReleaseGroupEntity> entities = feedService.latest(itemCount, hideFuture);
|
||||
List<RssItem> items = new ArrayList<>(entities.size());
|
||||
for (ReleaseGroupEntity e : entities) {
|
||||
String pubDate = null;
|
||||
|
||||
@@ -37,7 +37,9 @@ public class SettingsResource {
|
||||
|
||||
@POST
|
||||
public TemplateInstance save(@FormParam("defaultPrimaryTypes") Set<String> defaultPrimaryTypes,
|
||||
@FormParam("syncScheduleHours") int syncScheduleHours) {
|
||||
@FormParam("syncScheduleHours") int syncScheduleHours,
|
||||
@FormParam("hideFutureFeed") boolean hideFutureFeed,
|
||||
@FormParam("hideFutureRss") boolean hideFutureRss) {
|
||||
if (defaultPrimaryTypes == null) {
|
||||
defaultPrimaryTypes = Set.of();
|
||||
}
|
||||
@@ -47,6 +49,8 @@ public class SettingsResource {
|
||||
if (previous != syncScheduleHours) {
|
||||
syncService.reschedule();
|
||||
}
|
||||
settingsService.setHideFutureFeed(hideFutureFeed);
|
||||
settingsService.setHideFutureRss(hideFutureRss);
|
||||
return buildPanel("saved");
|
||||
}
|
||||
|
||||
@@ -54,6 +58,8 @@ public class SettingsResource {
|
||||
return fragments_settings_panel
|
||||
.data("defaultPrimaryTypes", settingsService.getDefaultPrimaryTypes())
|
||||
.data("syncScheduleHours", settingsService.getSyncScheduleHours())
|
||||
.data("hideFutureFeed", settingsService.isHideFutureFeed())
|
||||
.data("hideFutureRss", settingsService.isHideFutureRss())
|
||||
.data("allPrimaryTypes", SettingsService.ALL_PRIMARY_TYPES)
|
||||
.data("scheduleOptions", SCHEDULE_OPTIONS)
|
||||
.data("flash", flash);
|
||||
|
||||
@@ -6,6 +6,7 @@ import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
@@ -17,21 +18,27 @@ public class FeedService {
|
||||
private static final String ORDER_HQL =
|
||||
"ORDER BY firstReleaseDate DESC NULLS LAST, discoveredAt DESC";
|
||||
|
||||
private PanacheQuery<ReleaseGroupEntity> orderedQuery() {
|
||||
private static final String WHERE_ORDER_HQL =
|
||||
"WHERE firstReleaseDate IS NULL OR firstReleaseDate <= ?1 " + ORDER_HQL;
|
||||
|
||||
private PanacheQuery<ReleaseGroupEntity> orderedQuery(boolean hideFuture) {
|
||||
if (hideFuture) {
|
||||
return ReleaseGroupEntity.find(WHERE_ORDER_HQL, LocalDate.now());
|
||||
}
|
||||
return ReleaseGroupEntity.find(ORDER_HQL);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<ReleaseGroupEntity> feedPage(int offset) {
|
||||
public List<ReleaseGroupEntity> feedPage(int offset, boolean hideFuture) {
|
||||
if (offset < 0) {
|
||||
offset = 0;
|
||||
}
|
||||
return orderedQuery().range(offset, offset + pageSize - 1).list();
|
||||
return orderedQuery(hideFuture).range(offset, offset + pageSize - 1).list();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<ReleaseGroupEntity> latest(int count) {
|
||||
return orderedQuery().range(0, Math.max(0, count - 1)).list();
|
||||
public List<ReleaseGroupEntity> latest(int count, boolean hideFuture) {
|
||||
return orderedQuery(hideFuture).range(0, Math.max(0, count - 1)).list();
|
||||
}
|
||||
|
||||
public int pageSize() {
|
||||
|
||||
@@ -15,6 +15,8 @@ public class SettingsService {
|
||||
|
||||
public static final String KEY_DEFAULT_PRIMARY_TYPES = "defaultPrimaryTypes";
|
||||
public static final String KEY_SYNC_SCHEDULE_HOURS = "syncScheduleHours";
|
||||
public static final String KEY_HIDE_FUTURE_FEED = "hideFutureFeed";
|
||||
public static final String KEY_HIDE_FUTURE_RSS = "hideFutureRss";
|
||||
|
||||
public static final List<String> ALL_PRIMARY_TYPES = List.of("album", "single", "ep", "broadcast", "other");
|
||||
|
||||
@@ -42,6 +44,24 @@ public class SettingsService {
|
||||
AppSetting.set(KEY_SYNC_SCHEDULE_HOURS, String.valueOf(hours));
|
||||
}
|
||||
|
||||
public boolean isHideFutureFeed() {
|
||||
return Boolean.parseBoolean(AppSetting.get(KEY_HIDE_FUTURE_FEED, "false"));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setHideFutureFeed(boolean hide) {
|
||||
AppSetting.set(KEY_HIDE_FUTURE_FEED, String.valueOf(hide));
|
||||
}
|
||||
|
||||
public boolean isHideFutureRss() {
|
||||
return Boolean.parseBoolean(AppSetting.get(KEY_HIDE_FUTURE_RSS, "false"));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void setHideFutureRss(boolean hide) {
|
||||
AppSetting.set(KEY_HIDE_FUTURE_RSS, String.valueOf(hide));
|
||||
}
|
||||
|
||||
private Set<String> parseTypes(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return new LinkedHashSet<>();
|
||||
|
||||
@@ -34,6 +34,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider my-1"></div>
|
||||
<div class="text-sm font-semibold mb-1">Future releases</div>
|
||||
<div class="space-y-2">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" name="hideFutureFeed" value="true"
|
||||
class="checkbox checkbox-sm"
|
||||
{#if hideFutureFeed}checked{/if}/>
|
||||
<span class="text-xs">Hide unreleased in feed</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" name="hideFutureRss" value="true"
|
||||
class="checkbox checkbox-sm"
|
||||
{#if hideFutureRss}checked{/if}/>
|
||||
<span class="text-xs">Hide unreleased in RSS</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-sm w-full">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user