Phase 1: Project scaffold
- Quarkus Maven project with all dependencies - SQLite DatabaseService with DAO methods - MusicBrainz REST client with DTOs - ArtistService, ReleaseGroupService, CacheService, FeedService - HTMX web controllers (ReleasesPage, ArtistsPage, ArtistController, FeedController, SettingsController) - RSS feed builder using Rome library - Qute templates with daisyUI theme switching - Base layout with navbar, search, theme picker, settings modal
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# Server
|
||||
quarkus.http.port=8080
|
||||
|
||||
# MusicBrainz API
|
||||
quarkus.rest-client."com.discdrop.client.MusicBrainzClient".url=https://musicbrainz.org/ws/2
|
||||
quarkus.rest-client."com.discdrop.client.MusicBrainzClient".default-headers=User-Agent: DiscDrop/1.0 (david.alvarez.81@gmail.com)
|
||||
|
||||
# Qute templating
|
||||
quarkus.qute.suffixes=html
|
||||
quarkus.qute.content-types.html=text/html
|
||||
|
||||
# Jackson
|
||||
quarkus.jackson.fail-on-unknown-properties=false
|
||||
|
||||
# DiscDrop settings
|
||||
discdrop.db.path=${DISC_DROP_DB_PATH:data/discdrop.db}
|
||||
discdrop.cache.ttl-hours=${DISC_DROP_CACHE_TTL:8}
|
||||
discdrop.base-url=${DISC_DROP_BASE_URL:http://localhost:8080}
|
||||
@@ -0,0 +1,9 @@
|
||||
{#include layouts/base}
|
||||
{#page-content}
|
||||
<div class="container mx-auto p-4">
|
||||
<h2 class="text-2xl font-bold mb-4">🎤 Artists</h2>
|
||||
<div id="artist-list" hx-get="/api/artists/list" hx-trigger="load">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
</div>
|
||||
{/page-content}
|
||||
@@ -0,0 +1,80 @@
|
||||
{#if artists.isEmpty()}
|
||||
<div class="card bg-base-100 shadow-xl p-8 text-center">
|
||||
<p class="text-lg text-base-content/60">No artists tracked yet</p>
|
||||
<p class="text-sm text-base-content/40 mt-2">Search for artists using the search bar above and click "+ Add"</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artist</th>
|
||||
<th>Since</th>
|
||||
<th>Release Types</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#for artist in artists}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="font-medium">{artist.name}</div>
|
||||
{#if artist.disambiguation}
|
||||
<div class="text-xs text-base-content/60">{artist.disambiguation}</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-sm text-base-content/60">{artist.createdAt}</td>
|
||||
<td>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<label class="label cursor-pointer gap-1">
|
||||
<input type="checkbox" class="checkbox checkbox-xs checkbox-primary"
|
||||
{#if artist.albumMonitored}checked{/if}
|
||||
hx-patch="/api/artists/{artist.mbid}/monitor?type=album"
|
||||
hx-target="#artist-list" hx-swap="innerHTML">
|
||||
<span class="label-text text-xs">Album</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-1">
|
||||
<input type="checkbox" class="checkbox checkbox-xs"
|
||||
{#if artist.singleMonitored}checked{/if}
|
||||
hx-patch="/api/artists/{artist.mbid}/monitor?type=single"
|
||||
hx-target="#artist-list" hx-swap="innerHTML">
|
||||
<span class="label-text text-xs">Single</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-1">
|
||||
<input type="checkbox" class="checkbox checkbox-xs"
|
||||
{#if artist.epMonitored}checked{/if}
|
||||
hx-patch="/api/artists/{artist.mbid}/monitor?type=ep"
|
||||
hx-target="#artist-list" hx-swap="innerHTML">
|
||||
<span class="label-text text-xs">EP</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-1">
|
||||
<input type="checkbox" class="checkbox checkbox-xs"
|
||||
{#if artist.broadcastMonitored}checked{/if}
|
||||
hx-patch="/api/artists/{artist.mbid}/monitor?type=broadcast"
|
||||
hx-target="#artist-list" hx-swap="innerHTML">
|
||||
<span class="label-text text-xs">Broadcast</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-1">
|
||||
<input type="checkbox" class="checkbox checkbox-xs"
|
||||
{#if artist.otherMonitored}checked{/if}
|
||||
hx-patch="/api/artists/{artist.mbid}/monitor?type=other"
|
||||
hx-target="#artist-list" hx-swap="innerHTML">
|
||||
<span class="label-text text-xs">Other</span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-error btn-xs"
|
||||
hx-delete="/api/artists/{artist.mbid}"
|
||||
hx-target="#artist-list"
|
||||
hx-swap="innerHTML"
|
||||
onclick="return confirm('Remove {artist.name}?')">
|
||||
Remove
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/for}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,37 @@
|
||||
{#if entries.isEmpty()}
|
||||
<div class="card bg-base-100 shadow-xl p-8 text-center">
|
||||
<p class="text-lg text-base-content/60">No releases found</p>
|
||||
<p class="text-sm text-base-content/40 mt-2">Add artists to your collection to see their releases here</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Artist</th>
|
||||
<th>Release</th>
|
||||
<th>Type</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#for entry in entries}
|
||||
<tr>
|
||||
<td class="font-medium">{entry.artistName}</td>
|
||||
<td>
|
||||
<a href="https://musicbrainz.org/release-group/{entry.mbid}"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="link link-hover">
|
||||
{entry.title}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-outline badge-sm">{entry.primaryType}</span>
|
||||
</td>
|
||||
<td class="text-sm text-base-content/60">{entry.firstReleaseDate}</td>
|
||||
</tr>
|
||||
{/for}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,31 @@
|
||||
<div class="card bg-base-100 shadow-xl mt-1 max-h-64 overflow-y-auto">
|
||||
{#if artists.isEmpty()}
|
||||
<div class="p-3 text-sm text-base-content/60">No artists found</div>
|
||||
{:else}
|
||||
{#for artist in artists}
|
||||
<div class="flex items-center justify-between p-3 hover:bg-base-200 border-b border-base-200 last:border-b-0">
|
||||
<div class="flex-1 min-w-0">
|
||||
<span class="font-medium">{artist.name}</span>
|
||||
{#if artist.disambiguation}
|
||||
<span class="text-sm text-base-content/60">— {artist.disambiguation}</span>
|
||||
{/if}
|
||||
<br>
|
||||
<span class="text-xs text-base-content/40">{artist.area}</span>
|
||||
</div>
|
||||
<div class="ml-2 flex-shrink-0">
|
||||
{#if artist.alreadyTracked}
|
||||
<span class="badge badge-success badge-sm">✓ Added</span>
|
||||
{:else}
|
||||
<button class="btn btn-primary btn-xs"
|
||||
hx-post="/api/artists/{artist.mbid}/track"
|
||||
hx-target="#artist-list"
|
||||
hx-swap="innerHTML"
|
||||
onclick="this.closest('#search-results').innerHTML=''; document.querySelector('[name=q]').value=''">
|
||||
+ Add
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/for}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,70 @@
|
||||
<h3 class="font-bold text-lg mb-4">⚙️ Settings</h3>
|
||||
<form hx-patch="/api/settings"
|
||||
hx-target="#settings-modal-content"
|
||||
hx-swap="innerHTML">
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Refresh releases every</span>
|
||||
</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="radio" name="cache_ttl_hours" value="6" class="radio radio-primary radio-sm"
|
||||
{#if cacheTtl == '6'}checked{/if}>
|
||||
<span class="label-text">6 hours</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="radio" name="cache_ttl_hours" value="8" class="radio radio-primary radio-sm"
|
||||
{#if cacheTtl == '8'}checked{/if}>
|
||||
<span class="label-text">8 hours</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="radio" name="cache_ttl_hours" value="12" class="radio radio-primary radio-sm"
|
||||
{#if cacheTtl == '12'}checked{/if}>
|
||||
<span class="label-text">12 hours</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="radio" name="cache_ttl_hours" value="24" class="radio radio-primary radio-sm"
|
||||
{#if cacheTtl == '24'}checked{/if}>
|
||||
<span class="label-text">24 hours</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-4">
|
||||
<label class="label">
|
||||
<span class="label-text">Default Release Types (when adding artists)</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="checkbox" name="default_types" value="Album" class="checkbox checkbox-sm"
|
||||
{#if defaultTypes.contains('Album')}checked{/if}>
|
||||
<span class="label-text">Album</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="checkbox" name="default_types" value="Single" class="checkbox checkbox-sm"
|
||||
{#if defaultTypes.contains('Single')}checked{/if}>
|
||||
<span class="label-text">Single</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="checkbox" name="default_types" value="EP" class="checkbox checkbox-sm"
|
||||
{#if defaultTypes.contains('EP')}checked{/if}>
|
||||
<span class="label-text">EP</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="checkbox" name="default_types" value="Broadcast" class="checkbox checkbox-sm"
|
||||
{#if defaultTypes.contains('Broadcast')}checked{/if}>
|
||||
<span class="label-text">Broadcast</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<input type="checkbox" name="default_types" value="Other" class="checkbox checkbox-sm"
|
||||
{#if defaultTypes.contains('Other')}checked{/if}>
|
||||
<span class="label-text">Other</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-action">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
<button type="button" class="btn" onclick="settingsModal.close()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DiscDrop</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.23/dist/full.min.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script>
|
||||
<style>
|
||||
.search-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-base-200">
|
||||
<div class="navbar bg-base-100 shadow-lg px-4">
|
||||
<div class="flex-1 gap-2">
|
||||
<a href="/" class="text-xl font-bold">🎵 DiscDrop</a>
|
||||
<div class="tabs tabs-box ml-4 hidden sm:flex">
|
||||
<a href="/" class="tab {#if activeTab == 'releases'}tab-active{/if}">Releases</a>
|
||||
<a href="/artists" class="tab {#if activeTab == 'artists'}tab-active{/if}">Artists</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-none gap-2 relative">
|
||||
<div class="form-control">
|
||||
<input type="text" placeholder="Search artists..."
|
||||
class="input input-bordered input-sm w-48 md:w-64"
|
||||
name="q"
|
||||
hx-get="/api/artists/search"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#search-results"
|
||||
hx-swap="innerHTML"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
<div id="search-results" class="search-dropdown"></div>
|
||||
|
||||
<select class="select select-bordered select-sm w-full max-w-[8rem]" data-choose-theme>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="synthwave">Synthwave</option>
|
||||
<option value="retro">Retro</option>
|
||||
<option value="dracula">Dracula</option>
|
||||
<option value="night">Night</option>
|
||||
<option value="dim">Dim</option>
|
||||
<option value="nord">Nord</option>
|
||||
</select>
|
||||
|
||||
<button class="btn btn-ghost btn-sm"
|
||||
onclick="settingsModal.showModal()">⚙️</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="container mx-auto p-4">
|
||||
{#insert page-content}{/page-content}
|
||||
</main>
|
||||
|
||||
<dialog id="settingsModal" class="modal">
|
||||
<div class="modal-box" id="settings-modal-content"
|
||||
hx-get="/api/settings/form"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML">
|
||||
<span class="loading loading-spinner"></span>
|
||||
</div>
|
||||
</dialog>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
{#include layouts/base}
|
||||
{#page-content}
|
||||
<div class="container mx-auto p-4">
|
||||
<h2 class="text-2xl font-bold mb-4">📡 Releases</h2>
|
||||
<div id="feed-entries" hx-get="/api/artists/feed-table" hx-trigger="load">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
<div class="mt-4 flex gap-4">
|
||||
<a href="/feed/rss.xml" class="btn btn-primary">📥 RSS Feed</a>
|
||||
<a href="/feed/opml" class="btn btn-ghost">📋 OPML</a>
|
||||
</div>
|
||||
</div>
|
||||
{/page-content}
|
||||
Reference in New Issue
Block a user