@@ -0,0 +1,20 @@
|
||||
# Server
|
||||
quarkus.http.port=8080
|
||||
|
||||
# MusicBrainz API
|
||||
quarkus.rest-client."com.discdrop.client.MusicBrainzClient".url=https://musicbrainz.org/ws/2
|
||||
|
||||
# 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}
|
||||
|
||||
# User-Agent sent to MusicBrainz API (env: DISC_DROP_USER_AGENT)
|
||||
discdrop.user-agent=${DISC_DROP_USER_AGENT:DiscDrop/0.1 (https://https://github.com/droideparanoico/discdrop)}
|
||||
@@ -0,0 +1,9 @@
|
||||
{#include layouts/base}
|
||||
{#page-content}
|
||||
<div class="w-full">
|
||||
<h2 class="text-2xl font-bold mb-5">🎤 Tracked Artists</h2>
|
||||
<div id="artist-list" hx-get="/api/artists/list" hx-trigger="load">
|
||||
<span class="loading loading-spinner loading-lg block mx-auto my-12"></span>
|
||||
</div>
|
||||
</div>
|
||||
{/page-content}
|
||||
@@ -0,0 +1,90 @@
|
||||
<div id="artist-list" hx-swap-oob="true">
|
||||
{#if artists.isEmpty()}
|
||||
<div class="bg-base-200" style="border:1px solid var(--fallback-b3,oklch(var(--b3)/1));border-radius:0.5rem;padding:2.5rem;text-align:center">
|
||||
<svg class="icon-lg text-base-content/20" style="margin:0 auto 0.75rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
<p class="text-lg font-medium 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</p>
|
||||
</div>
|
||||
{#else}
|
||||
<div class="overflow-x-auto rounded-box border border-base-300">
|
||||
<table class="table table-pin-rows">
|
||||
<thead>
|
||||
<tr style="background:var(--fallback-b2,oklch(var(--b2)/1))">
|
||||
<th>Artist</th>
|
||||
<th>Since</th>
|
||||
<th>Release Types</th>
|
||||
<th style="width:6rem">Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#for artist in artists}
|
||||
<tr style="transition:background 0.15s" onmouseover="this.style.background='var(--fallback-b2,oklch(var(--b2)/1))'" onmouseout="this.style.background=''">
|
||||
<td>
|
||||
<a href="https://musicbrainz.org/artist/{artist.mbid}" target="_blank" rel="noopener noreferrer" style="font-weight:500;text-decoration:none;color:inherit">{artist.name}</a>
|
||||
{#if artist.disambiguation}
|
||||
<div class="text-xs text-base-content/50">{artist.disambiguation}</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="text-sm text-base-content/50">{artist.createdAt}</td>
|
||||
<td>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.375rem">
|
||||
<label class="label cursor-pointer gap-1 p-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" style="color:var(--p)">Album</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-1 p-1">
|
||||
<input type="checkbox" class="checkbox checkbox-xs checkbox-secondary"
|
||||
{#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" style="color:var(--s)">Single</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-1 p-1">
|
||||
<input type="checkbox" class="checkbox checkbox-xs checkbox-accent"
|
||||
{#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" style="color:var(--a)">EP</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-1 p-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 text-base-content/60">Broadcast</span>
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-1 p-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 text-base-content/60">Other</span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-ghost btn-sm btn-square" style="color:var(--er)"
|
||||
hx-delete="/api/artists/{artist.mbid}"
|
||||
hx-target="#artist-list"
|
||||
hx-swap="innerHTML"
|
||||
onclick="return confirm('Remove {artist.name}?')">
|
||||
<svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/for}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
{#if entries.isEmpty()}
|
||||
<div class="card bg-base-200 shadow-xl text-center" style="padding:3rem">
|
||||
<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}
|
||||
{#for entry in entries}
|
||||
<a href="https://musicbrainz.org/release-group/{entry.mbid}" target="_blank" rel="noopener noreferrer"
|
||||
class="card card-side bg-base-200 border border-base-300 hover:bg-base-300 card-shadow overflow-hidden">
|
||||
<figure class="w-64">
|
||||
<img src="https://coverartarchive.org/release-group/{entry.mbid}/front-250"
|
||||
alt="{entry.title}" class="object-cover w-full h-full"
|
||||
loading="lazy"
|
||||
onerror="this.parentElement.innerHTML='<div class=\'flex items-center justify-center h-full text-base-content/20\'>No Cover</div>'">
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<h3 style="font-size:1.5rem;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:var(--fallback-bc,oklch(var(--bc)/0.6))">{entry.artistName}</h3>
|
||||
<h2 style="font-size:2rem;font-weight:700">{entry.title}</h2>
|
||||
<div style="margin-top:0.75rem">
|
||||
{#if entry.firstReleaseDate}
|
||||
<div style="font-size:1.5rem;color:var(--fallback-bc,oklch(var(--bc)/0.4))">{entry.firstReleaseDate}</div>
|
||||
{/if}
|
||||
<div style="margin-top:0.4rem">
|
||||
{#if entry.primaryType == 'Album'}
|
||||
<span class="badge badge-primary" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">Album</span>
|
||||
{#else if entry.primaryType == 'Single'}
|
||||
<span class="badge badge-secondary" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">Single</span>
|
||||
{#else if entry.primaryType == 'EP'}
|
||||
<span class="badge badge-accent" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">EP</span>
|
||||
{#else}
|
||||
<span class="badge badge-ghost" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">{entry.primaryType}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/for}
|
||||
{#if hasMore}
|
||||
<div style="display:flex;justify-content:center;padding:1rem 0">
|
||||
<button class="btn btn-primary btn-sm"
|
||||
hx-get="/api/artists/feed-table?page={nextPage}"
|
||||
hx-target="closest div"
|
||||
hx-swap="outerHTML">Load more</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -0,0 +1,40 @@
|
||||
<div class="card bg-base-300 dropdown-shadow border border-base-300 mt-1" style="max-height:18rem;overflow-y:auto">
|
||||
{#if artists.isEmpty()}
|
||||
<div style="padding:1rem;text-align:center;font-size:0.875rem;color:var(--fallback-bc,oklch(var(--bc)/0.5))">No artists found</div>
|
||||
{#else}
|
||||
{#for artist in artists}
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:1rem;border-bottom:1px solid var(--fallback-b3,oklch(var(--b3)/1));transition:background 0.1s"
|
||||
onmouseover="this.style.background='var(--fallback-b2,oklch(var(--b2)/1))'" onmouseout="this.style.background=''">
|
||||
<div style="flex:1;min-width:0;margin-right:0.5rem">
|
||||
<div style="display:flex;align-items:center;gap:0.5rem">
|
||||
<span style="font-weight:600;font-size:0.9rem">{artist.name}</span>
|
||||
{#if artist.type}
|
||||
<span class="badge badge-ghost badge-xs">{artist.type}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if artist.disambiguation}
|
||||
<div style="font-size:0.75rem;color:var(--fallback-bc,oklch(var(--bc)/0.5));overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{artist.disambiguation}</div>
|
||||
{/if}
|
||||
{#if artist.area}
|
||||
<div style="font-size:0.75rem;color:var(--fallback-bc,oklch(var(--bc)/0.4))">{artist.area}</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
{#if artist.alreadyTracked}
|
||||
<button class="btn btn-warning btn-sm"
|
||||
hx-delete="/api/artists/{artist.mbid}/from-search"
|
||||
hx-include="#search-input"
|
||||
hx-target="#search-results"
|
||||
hx-swap="innerHTML">✓ Added</button>
|
||||
{#else}
|
||||
<button class="btn btn-primary btn-sm"
|
||||
hx-post="/api/artists/{artist.mbid}/track"
|
||||
hx-target="#search-results"
|
||||
hx-swap="innerHTML"
|
||||
onclick="document.getElementById('search-input').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,89 @@
|
||||
<!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/dist/full.min.css" rel="stylesheet">
|
||||
<script src="https://unpkg.com/htmx.org@2/dist/htmx.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
#search-wrapper { position: relative; width: 100%; }
|
||||
#search-results { position: absolute; top: 100%; left: 0; right: 0; z-index: 50; }
|
||||
main { max-width: 1100px; margin: 0 auto; padding: 1.5rem 1rem; }
|
||||
.icon-sm { width: 16px; height: 16px; flex-shrink: 0; }
|
||||
.icon-xs { width: 12px; height: 12px; flex-shrink: 0; }
|
||||
.icon-lg { width: 48px; height: 48px; flex-shrink: 0; }
|
||||
.card-shadow { box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
|
||||
.card-shadow:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.25); }
|
||||
.dropdown-shadow { box-shadow: 0 8px 24px rgba(0,0,0,0.3); }
|
||||
</style>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var saved = localStorage.getItem('daisyui-theme');
|
||||
if (saved) document.documentElement.setAttribute('data-theme', saved);
|
||||
document.addEventListener('click', function (e) {
|
||||
var search = document.getElementById('search-wrapper');
|
||||
var results = document.getElementById('search-results');
|
||||
if (results && search && !search.contains(e.target)) {
|
||||
results.innerHTML = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
function setTheme(sel) {
|
||||
var theme = sel.value;
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('daisyui-theme', theme);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="min-h-screen bg-base-100">
|
||||
<div class="navbar bg-base-300 shadow-lg px-4 min-h-0 h-14">
|
||||
<div class="navbar-start"></div>
|
||||
|
||||
<div class="navbar-center gap-4">
|
||||
<a href="/" class="btn btn-ghost btn-sm no-underline">DiscDrop</a>
|
||||
<a href="/artists" class="btn btn-ghost btn-sm {#if activeTab == 'artists'}btn-active{/if}">Artists</a>
|
||||
<div id="search-wrapper" style="width:24rem">
|
||||
<form hx-get="/api/artists/search"
|
||||
hx-target="#search-results"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="keyup changed delay:300ms from:#search-input">
|
||||
<input id="search-input" type="text" placeholder="Search artist to follow"
|
||||
class="input input-bordered input-sm" name="q" autocomplete="off" style="width:100%">
|
||||
</form>
|
||||
<div id="search-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end gap-2">
|
||||
<select class="select select-bordered select-sm" style="width:7rem" onchange="setTheme(this)">
|
||||
<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>
|
||||
{#insert page-content}{/insert}
|
||||
</main>
|
||||
|
||||
<dialog id="settingsModal" class="modal">
|
||||
<div class="modal-box max-w-md" 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,26 @@
|
||||
{#include layouts/base}
|
||||
{#page-content}
|
||||
<div class="w-full">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;flex-wrap:wrap;gap:0.75rem">
|
||||
<h2 style="font-size:1.5rem;font-weight:700">📡 Releases</h2>
|
||||
<div style="display:flex;gap:0.5rem">
|
||||
<a href="/feed/rss.xml" class="btn btn-primary btn-sm gap-2">
|
||||
<svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 11a9 9 0 019 9"/><path d="M4 4a16 16 0 0116 16"/>
|
||||
<circle cx="5" cy="19" r="1"/>
|
||||
</svg>
|
||||
RSS Feed
|
||||
</a>
|
||||
<a href="/feed/opml" class="btn btn-ghost btn-sm gap-2">
|
||||
<svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
OPML
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="feed-entries" hx-get="/api/artists/feed-table" hx-trigger="load" style="display:flex;flex-direction:column;gap:2.5rem">
|
||||
<div style="display:flex;justify-content:center;padding:4rem 0">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/page-content}
|
||||
Reference in New Issue
Block a user