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:
2026-07-02 15:29:16 +02:00
commit f0be591faf
26 changed files with 1646 additions and 0 deletions
@@ -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>