Compare commits
4
Commits
508bf77b6d
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ad3b9d75 | ||
|
|
9e3bd5e916 | ||
|
|
b17a794d4f | ||
|
|
846e27b95f |
@@ -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/
|
target/
|
||||||
*.jar
|
*.jar
|
||||||
*.war
|
*.war
|
||||||
|
mvnw
|
||||||
|
mvnw.cmd
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
@@ -21,5 +23,8 @@ data/
|
|||||||
*.mv.db
|
*.mv.db
|
||||||
*.trace.db
|
*.trace.db
|
||||||
|
|
||||||
|
# Environment (personal config)
|
||||||
|
.env
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.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.
|
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
|
```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
|
./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 |
|
All settings are in `application.properties`. Key values can be overridden via environment variables:
|
||||||
|---|---|---|
|
|
||||||
| `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. |
|
|
||||||
|
|
||||||
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
|
## 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
|
||||||
|
|||||||
@@ -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 |
@@ -6,6 +6,7 @@ import io.discdrop.persistence.FollowedArtist;
|
|||||||
import io.discdrop.persistence.ReleaseGroupEntity;
|
import io.discdrop.persistence.ReleaseGroupEntity;
|
||||||
import io.discdrop.service.ArtistService;
|
import io.discdrop.service.ArtistService;
|
||||||
import io.discdrop.service.FeedService;
|
import io.discdrop.service.FeedService;
|
||||||
|
import io.discdrop.service.SettingsService;
|
||||||
import io.discdrop.service.SyncService;
|
import io.discdrop.service.SyncService;
|
||||||
import io.quarkus.qute.Location;
|
import io.quarkus.qute.Location;
|
||||||
import io.quarkus.qute.Template;
|
import io.quarkus.qute.Template;
|
||||||
@@ -34,6 +35,9 @@ public class ArtistResource {
|
|||||||
@Inject
|
@Inject
|
||||||
FeedService feedService;
|
FeedService feedService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SettingsService settingsService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@Location("fragments/feed-list.html")
|
@Location("fragments/feed-list.html")
|
||||||
Template fragments_feed_list;
|
Template fragments_feed_list;
|
||||||
@@ -113,7 +117,8 @@ public class ArtistResource {
|
|||||||
if (offset < 0) {
|
if (offset < 0) {
|
||||||
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();
|
int nextOffset = offset + rows.size();
|
||||||
boolean hasMore = rows.size() == feedService.pageSize();
|
boolean hasMore = rows.size() == feedService.pageSize();
|
||||||
return fragments_feed_list.data("rows", rows)
|
return fragments_feed_list.data("rows", rows)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package io.discdrop.resource;
|
|||||||
import io.discdrop.persistence.FollowedArtist;
|
import io.discdrop.persistence.FollowedArtist;
|
||||||
import io.discdrop.persistence.ReleaseGroupEntity;
|
import io.discdrop.persistence.ReleaseGroupEntity;
|
||||||
import io.discdrop.service.FeedService;
|
import io.discdrop.service.FeedService;
|
||||||
|
import io.discdrop.service.SettingsService;
|
||||||
import io.discdrop.service.SyncService;
|
import io.discdrop.service.SyncService;
|
||||||
import io.quarkus.qute.Location;
|
import io.quarkus.qute.Location;
|
||||||
import io.quarkus.qute.Template;
|
import io.quarkus.qute.Template;
|
||||||
@@ -24,6 +25,9 @@ public class PageResource {
|
|||||||
@Inject
|
@Inject
|
||||||
SyncService syncService;
|
SyncService syncService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SettingsService settingsService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
Template index;
|
Template index;
|
||||||
|
|
||||||
@@ -38,7 +42,8 @@ public class PageResource {
|
|||||||
if (offset < 0) {
|
if (offset < 0) {
|
||||||
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();
|
int nextOffset = offset + rows.size();
|
||||||
boolean hasMore = rows.size() == feedService.pageSize();
|
boolean hasMore = rows.size() == feedService.pageSize();
|
||||||
return fragments_feed_list.data("rows", rows)
|
return fragments_feed_list.data("rows", rows)
|
||||||
@@ -52,7 +57,8 @@ public class PageResource {
|
|||||||
@GET
|
@GET
|
||||||
@Transactional
|
@Transactional
|
||||||
public TemplateInstance index() {
|
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();
|
boolean hasMore = rows.size() == feedService.pageSize();
|
||||||
return index.data("rows", rows)
|
return index.data("rows", rows)
|
||||||
.data("nextOffset", rows.size())
|
.data("nextOffset", rows.size())
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.discdrop.resource;
|
|||||||
|
|
||||||
import io.discdrop.persistence.ReleaseGroupEntity;
|
import io.discdrop.persistence.ReleaseGroupEntity;
|
||||||
import io.discdrop.service.FeedService;
|
import io.discdrop.service.FeedService;
|
||||||
|
import io.discdrop.service.SettingsService;
|
||||||
import io.quarkus.qute.Location;
|
import io.quarkus.qute.Location;
|
||||||
import io.quarkus.qute.Template;
|
import io.quarkus.qute.Template;
|
||||||
import io.quarkus.qute.TemplateInstance;
|
import io.quarkus.qute.TemplateInstance;
|
||||||
@@ -27,6 +28,9 @@ public class RssResource {
|
|||||||
@Inject
|
@Inject
|
||||||
FeedService feedService;
|
FeedService feedService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SettingsService settingsService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@Location("rss.xml")
|
@Location("rss.xml")
|
||||||
Template rss;
|
Template rss;
|
||||||
@@ -40,7 +44,8 @@ public class RssResource {
|
|||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_XML)
|
@Produces(MediaType.APPLICATION_XML)
|
||||||
public TemplateInstance feed() {
|
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());
|
List<RssItem> items = new ArrayList<>(entities.size());
|
||||||
for (ReleaseGroupEntity e : entities) {
|
for (ReleaseGroupEntity e : entities) {
|
||||||
String pubDate = null;
|
String pubDate = null;
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ public class SettingsResource {
|
|||||||
|
|
||||||
@POST
|
@POST
|
||||||
public TemplateInstance save(@FormParam("defaultPrimaryTypes") Set<String> defaultPrimaryTypes,
|
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) {
|
if (defaultPrimaryTypes == null) {
|
||||||
defaultPrimaryTypes = Set.of();
|
defaultPrimaryTypes = Set.of();
|
||||||
}
|
}
|
||||||
@@ -47,6 +49,8 @@ public class SettingsResource {
|
|||||||
if (previous != syncScheduleHours) {
|
if (previous != syncScheduleHours) {
|
||||||
syncService.reschedule();
|
syncService.reschedule();
|
||||||
}
|
}
|
||||||
|
settingsService.setHideFutureFeed(hideFutureFeed);
|
||||||
|
settingsService.setHideFutureRss(hideFutureRss);
|
||||||
return buildPanel("saved");
|
return buildPanel("saved");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +58,8 @@ public class SettingsResource {
|
|||||||
return fragments_settings_panel
|
return fragments_settings_panel
|
||||||
.data("defaultPrimaryTypes", settingsService.getDefaultPrimaryTypes())
|
.data("defaultPrimaryTypes", settingsService.getDefaultPrimaryTypes())
|
||||||
.data("syncScheduleHours", settingsService.getSyncScheduleHours())
|
.data("syncScheduleHours", settingsService.getSyncScheduleHours())
|
||||||
|
.data("hideFutureFeed", settingsService.isHideFutureFeed())
|
||||||
|
.data("hideFutureRss", settingsService.isHideFutureRss())
|
||||||
.data("allPrimaryTypes", SettingsService.ALL_PRIMARY_TYPES)
|
.data("allPrimaryTypes", SettingsService.ALL_PRIMARY_TYPES)
|
||||||
.data("scheduleOptions", SCHEDULE_OPTIONS)
|
.data("scheduleOptions", SCHEDULE_OPTIONS)
|
||||||
.data("flash", flash);
|
.data("flash", flash);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import jakarta.enterprise.context.ApplicationScoped;
|
|||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
@@ -17,21 +18,27 @@ public class FeedService {
|
|||||||
private static final String ORDER_HQL =
|
private static final String ORDER_HQL =
|
||||||
"ORDER BY firstReleaseDate DESC NULLS LAST, discoveredAt DESC";
|
"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);
|
return ReleaseGroupEntity.find(ORDER_HQL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<ReleaseGroupEntity> feedPage(int offset) {
|
public List<ReleaseGroupEntity> feedPage(int offset, boolean hideFuture) {
|
||||||
if (offset < 0) {
|
if (offset < 0) {
|
||||||
offset = 0;
|
offset = 0;
|
||||||
}
|
}
|
||||||
return orderedQuery().range(offset, offset + pageSize - 1).list();
|
return orderedQuery(hideFuture).range(offset, offset + pageSize - 1).list();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public List<ReleaseGroupEntity> latest(int count) {
|
public List<ReleaseGroupEntity> latest(int count, boolean hideFuture) {
|
||||||
return orderedQuery().range(0, Math.max(0, count - 1)).list();
|
return orderedQuery(hideFuture).range(0, Math.max(0, count - 1)).list();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int pageSize() {
|
public int pageSize() {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ public class SettingsService {
|
|||||||
|
|
||||||
public static final String KEY_DEFAULT_PRIMARY_TYPES = "defaultPrimaryTypes";
|
public static final String KEY_DEFAULT_PRIMARY_TYPES = "defaultPrimaryTypes";
|
||||||
public static final String KEY_SYNC_SCHEDULE_HOURS = "syncScheduleHours";
|
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");
|
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));
|
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) {
|
private Set<String> parseTypes(String raw) {
|
||||||
if (raw == null || raw.isBlank()) {
|
if (raw == null || raw.isBlank()) {
|
||||||
return new LinkedHashSet<>();
|
return new LinkedHashSet<>();
|
||||||
|
|||||||
@@ -34,6 +34,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<button class="btn btn-primary btn-sm w-full">Save</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user