Future releases filter settings for feed and RSS
Build & Deploy / build-and-deploy (push) Successful in 7s

This commit is contained in:
2026-07-27 16:42:53 +02:00
parent 9e3bd5e916
commit d4ad3b9d75
9 changed files with 83 additions and 10 deletions
+2
View File
@@ -2,6 +2,8 @@
target/ target/
*.jar *.jar
*.war *.war
mvnw
mvnw.cmd
# IDE # IDE
.idea/ .idea/
+5
View File
@@ -26,6 +26,9 @@ A background job refreshes release groups on a configurable schedule (6 / 12 / 2
### 📰 RSS feed ### 📰 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. 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 ### 🌗 Theme switching
Built-in light/dark theme toggle, persisted in `localStorage`. Built-in light/dark theme toggle, persisted in `localStorage`.
@@ -77,6 +80,8 @@ All settings are in `application.properties`. Key values can be overridden via e
| `discdrop.rss.item-count` | — | `50` | RSS item count | | `discdrop.rss.item-count` | — | `50` | RSS item count |
| `discdrop.sync.default-schedule-hours` | — | `24` | Default sync interval | | `discdrop.sync.default-schedule-hours` | — | `24` | Default sync interval |
| `discdrop.sync.default-primary-types` | — | `album` | Default types for new artists | | `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` The H2 file database lives in `./data/discdrop.mv.db`
@@ -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>