Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4ad3b9d75 | ||
|
|
9e3bd5e916 | ||
|
|
b17a794d4f | ||
|
|
846e27b95f | ||
|
|
508bf77b6d | ||
|
|
bc0f516bf3 | ||
|
|
7f107420a8 | ||
|
|
2434d7301d | ||
|
|
406d987e94 | ||
|
|
8f6e891bc6 | ||
|
|
9019ead439 | ||
|
|
827e87a8a8 | ||
|
|
4ef83cd701 | ||
|
|
03e5b2810c | ||
|
|
11aba60109 | ||
|
|
4236f7b7f7 | ||
|
|
d7d68b9ab0 | ||
|
|
5861da89b3 |
@@ -0,0 +1,7 @@
|
|||||||
|
data/
|
||||||
|
target/
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
.DS_Store
|
||||||
@@ -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,29 @@
|
|||||||
|
# This workflow builds DiscDrop whenever code is pushed to main.
|
||||||
|
# The runner has Docker socket access so it can manage containers on the host.
|
||||||
|
# See: https://docs.gitea.com/usage/actions
|
||||||
|
|
||||||
|
name: Build & Deploy
|
||||||
|
run-name: Build ${{ gitea.sha.substring(0, 7) }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: discdrop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
docker build -t ${{ env.IMAGE_NAME }}:${{ gitea.sha }} .
|
||||||
|
docker tag ${{ env.IMAGE_NAME }}:${{ gitea.sha }} ${{ env.IMAGE_NAME }}:latest
|
||||||
|
|
||||||
|
- name: Clean up old images
|
||||||
|
run: docker image prune -f --filter "until=24h"
|
||||||
@@ -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,16 @@
|
|||||||
|
# DiscDrop — Quarkus multi-stage build
|
||||||
|
|
||||||
|
# ── Stage 1: Build ──────────────────────────────────────
|
||||||
|
FROM maven:3.9-eclipse-temurin-21 AS build
|
||||||
|
WORKDIR /build
|
||||||
|
COPY pom.xml ./
|
||||||
|
COPY src ./src
|
||||||
|
RUN mvn package -DskipTests
|
||||||
|
|
||||||
|
# ── Stage 2: Runtime ────────────────────────────────────
|
||||||
|
FROM eclipse-temurin:21-jre-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /build/target/quarkus-app/ /app/
|
||||||
|
VOLUME /data
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["java", "-XX:MinHeapFreeRatio=10", "-XX:MaxHeapFreeRatio=20", "-jar", "quarkus-run.jar"]
|
||||||
@@ -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` 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
|
||||||
|
After Width: | Height: | Size: 277 KiB |
@@ -1,295 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
# or more contributor license agreements. See the NOTICE file
|
|
||||||
# distributed with this work for additional information
|
|
||||||
# regarding copyright ownership. The ASF licenses this file
|
|
||||||
# to you under the Apache License, Version 2.0 (the
|
|
||||||
# "License"); you may not use this file except in compliance
|
|
||||||
# with the License. You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing,
|
|
||||||
# software distributed under the License is distributed on an
|
|
||||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
# KIND, either express or implied. See the License for the
|
|
||||||
# specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
|
||||||
#
|
|
||||||
# Optional ENV vars
|
|
||||||
# -----------------
|
|
||||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
|
||||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
|
||||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
|
||||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
set -euf
|
|
||||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
|
||||||
|
|
||||||
# OS specific support.
|
|
||||||
native_path() { printf %s\\n "$1"; }
|
|
||||||
case "$(uname)" in
|
|
||||||
CYGWIN* | MINGW*)
|
|
||||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
|
||||||
native_path() { cygpath --path --windows "$1"; }
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# set JAVACMD and JAVACCMD
|
|
||||||
set_java_home() {
|
|
||||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
|
||||||
if [ -n "${JAVA_HOME-}" ]; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
|
||||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
|
||||||
else
|
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
|
||||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
|
||||||
|
|
||||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
|
||||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
|
||||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD="$(
|
|
||||||
'set' +e
|
|
||||||
'unset' -f command 2>/dev/null
|
|
||||||
'command' -v java
|
|
||||||
)" || :
|
|
||||||
JAVACCMD="$(
|
|
||||||
'set' +e
|
|
||||||
'unset' -f command 2>/dev/null
|
|
||||||
'command' -v javac
|
|
||||||
)" || :
|
|
||||||
|
|
||||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
|
||||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# hash string like Java String::hashCode
|
|
||||||
hash_string() {
|
|
||||||
str="${1:-}" h=0
|
|
||||||
while [ -n "$str" ]; do
|
|
||||||
char="${str%"${str#?}"}"
|
|
||||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
|
||||||
str="${str#?}"
|
|
||||||
done
|
|
||||||
printf %x\\n $h
|
|
||||||
}
|
|
||||||
|
|
||||||
verbose() { :; }
|
|
||||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf %s\\n "$1" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
trim() {
|
|
||||||
# MWRAPPER-139:
|
|
||||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
|
||||||
# Needed for removing poorly interpreted newline sequences when running in more
|
|
||||||
# exotic environments such as mingw bash on Windows.
|
|
||||||
printf "%s" "${1}" | tr -d '[:space:]'
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptDir="$(dirname "$0")"
|
|
||||||
scriptName="$(basename "$0")"
|
|
||||||
|
|
||||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
|
||||||
while IFS="=" read -r key value; do
|
|
||||||
case "${key-}" in
|
|
||||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
|
||||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
|
||||||
esac
|
|
||||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
|
||||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
|
||||||
|
|
||||||
case "${distributionUrl##*/}" in
|
|
||||||
maven-mvnd-*bin.*)
|
|
||||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
|
||||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
|
||||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
|
||||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
|
||||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
|
||||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
|
||||||
*)
|
|
||||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
|
||||||
distributionPlatform=linux-amd64
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
|
||||||
;;
|
|
||||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
|
||||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
|
||||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
|
||||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
|
||||||
distributionUrlName="${distributionUrl##*/}"
|
|
||||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
|
||||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
|
||||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
|
||||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
|
||||||
|
|
||||||
exec_maven() {
|
|
||||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
|
||||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ -d "$MAVEN_HOME" ]; then
|
|
||||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
|
||||||
exec_maven "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "${distributionUrl-}" in
|
|
||||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
|
||||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# prepare tmp dir
|
|
||||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
|
||||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
|
||||||
trap clean HUP INT TERM EXIT
|
|
||||||
else
|
|
||||||
die "cannot create temp dir"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
|
||||||
|
|
||||||
# Download and Install Apache Maven
|
|
||||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
|
||||||
verbose "Downloading from: $distributionUrl"
|
|
||||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
|
||||||
|
|
||||||
# select .zip or .tar.gz
|
|
||||||
if ! command -v unzip >/dev/null; then
|
|
||||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
|
||||||
distributionUrlName="${distributionUrl##*/}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# verbose opt
|
|
||||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
|
||||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
|
||||||
|
|
||||||
# normalize http auth
|
|
||||||
case "${MVNW_PASSWORD:+has-password}" in
|
|
||||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
|
||||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
|
||||||
verbose "Found wget ... using wget"
|
|
||||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
|
||||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
|
||||||
verbose "Found curl ... using curl"
|
|
||||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
|
||||||
elif set_java_home; then
|
|
||||||
verbose "Falling back to use Java to download"
|
|
||||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
|
||||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
|
||||||
cat >"$javaSource" <<-END
|
|
||||||
public class Downloader extends java.net.Authenticator
|
|
||||||
{
|
|
||||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
|
||||||
{
|
|
||||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
|
||||||
}
|
|
||||||
public static void main( String[] args ) throws Exception
|
|
||||||
{
|
|
||||||
setDefault( new Downloader() );
|
|
||||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END
|
|
||||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
|
||||||
verbose " - Compiling Downloader.java ..."
|
|
||||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
|
||||||
verbose " - Running Downloader.java ..."
|
|
||||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
|
||||||
if [ -n "${distributionSha256Sum-}" ]; then
|
|
||||||
distributionSha256Result=false
|
|
||||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
|
||||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
|
||||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
|
||||||
exit 1
|
|
||||||
elif command -v sha256sum >/dev/null; then
|
|
||||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
|
||||||
distributionSha256Result=true
|
|
||||||
fi
|
|
||||||
elif command -v shasum >/dev/null; then
|
|
||||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
|
||||||
distributionSha256Result=true
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
|
||||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [ $distributionSha256Result = false ]; then
|
|
||||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
|
||||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# unzip and move
|
|
||||||
if command -v unzip >/dev/null; then
|
|
||||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
|
||||||
else
|
|
||||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
|
||||||
actualDistributionDir=""
|
|
||||||
|
|
||||||
# First try the expected directory name (for regular distributions)
|
|
||||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
|
||||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
|
||||||
actualDistributionDir="$distributionUrlNameMain"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
|
||||||
if [ -z "$actualDistributionDir" ]; then
|
|
||||||
# enable globbing to iterate over items
|
|
||||||
set +f
|
|
||||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
|
||||||
if [ -d "$dir" ]; then
|
|
||||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
|
||||||
actualDistributionDir="$(basename "$dir")"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
set -f
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$actualDistributionDir" ]; then
|
|
||||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
|
||||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
|
||||||
die "Could not find Maven distribution directory in extracted archive"
|
|
||||||
fi
|
|
||||||
|
|
||||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
|
||||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
|
||||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
|
||||||
|
|
||||||
clean || :
|
|
||||||
exec_maven "$@"
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
<# : batch portion
|
|
||||||
@REM ----------------------------------------------------------------------------
|
|
||||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
@REM or more contributor license agreements. See the NOTICE file
|
|
||||||
@REM distributed with this work for additional information
|
|
||||||
@REM regarding copyright ownership. The ASF licenses this file
|
|
||||||
@REM to you under the Apache License, Version 2.0 (the
|
|
||||||
@REM "License"); you may not use this file except in compliance
|
|
||||||
@REM with the License. You may obtain a copy of the License at
|
|
||||||
@REM
|
|
||||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@REM
|
|
||||||
@REM Unless required by applicable law or agreed to in writing,
|
|
||||||
@REM software distributed under the License is distributed on an
|
|
||||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
@REM KIND, either express or implied. See the License for the
|
|
||||||
@REM specific language governing permissions and limitations
|
|
||||||
@REM under the License.
|
|
||||||
@REM ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@REM ----------------------------------------------------------------------------
|
|
||||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
|
||||||
@REM
|
|
||||||
@REM Optional ENV vars
|
|
||||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
|
||||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
|
||||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
|
||||||
@REM ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
|
||||||
@SET __MVNW_CMD__=
|
|
||||||
@SET __MVNW_ERROR__=
|
|
||||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
|
||||||
@SET PSModulePath=
|
|
||||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
|
||||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
|
||||||
)
|
|
||||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
|
||||||
@SET __MVNW_PSMODULEP_SAVE=
|
|
||||||
@SET __MVNW_ARG0_NAME__=
|
|
||||||
@SET MVNW_USERNAME=
|
|
||||||
@SET MVNW_PASSWORD=
|
|
||||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
|
||||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
|
||||||
@GOTO :EOF
|
|
||||||
: end batch / begin powershell #>
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
if ($env:MVNW_VERBOSE -eq "true") {
|
|
||||||
$VerbosePreference = "Continue"
|
|
||||||
}
|
|
||||||
|
|
||||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
|
||||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
|
||||||
if (!$distributionUrl) {
|
|
||||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
|
||||||
}
|
|
||||||
|
|
||||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
|
||||||
"maven-mvnd-*" {
|
|
||||||
$USE_MVND = $true
|
|
||||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
|
||||||
$MVN_CMD = "mvnd.cmd"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default {
|
|
||||||
$USE_MVND = $false
|
|
||||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
|
||||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
|
||||||
if ($env:MVNW_REPOURL) {
|
|
||||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
|
||||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
|
||||||
}
|
|
||||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
|
||||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
|
||||||
|
|
||||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
|
||||||
if ($env:MAVEN_USER_HOME) {
|
|
||||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
|
||||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$MAVEN_WRAPPER_DISTS = $null
|
|
||||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
|
||||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
|
||||||
} else {
|
|
||||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
|
||||||
}
|
|
||||||
|
|
||||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
|
||||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
|
||||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
|
||||||
|
|
||||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
|
||||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
|
||||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
|
||||||
exit $?
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
|
||||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
|
||||||
}
|
|
||||||
|
|
||||||
# prepare tmp dir
|
|
||||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
|
||||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
|
||||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
|
||||||
trap {
|
|
||||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
|
||||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
|
||||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
|
||||||
|
|
||||||
# Download and Install Apache Maven
|
|
||||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
|
||||||
Write-Verbose "Downloading from: $distributionUrl"
|
|
||||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
|
||||||
|
|
||||||
$webclient = New-Object System.Net.WebClient
|
|
||||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
|
||||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
|
||||||
}
|
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
||||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
|
||||||
|
|
||||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
|
||||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
|
||||||
if ($distributionSha256Sum) {
|
|
||||||
if ($USE_MVND) {
|
|
||||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
|
||||||
}
|
|
||||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
|
||||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
|
||||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# unzip and move
|
|
||||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
|
||||||
|
|
||||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
|
||||||
$actualDistributionDir = ""
|
|
||||||
|
|
||||||
# First try the expected directory name (for regular distributions)
|
|
||||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
|
||||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
|
||||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
|
||||||
$actualDistributionDir = $distributionUrlNameMain
|
|
||||||
}
|
|
||||||
|
|
||||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
|
||||||
if (!$actualDistributionDir) {
|
|
||||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
|
||||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
|
||||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
|
||||||
$actualDistributionDir = $_.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$actualDistributionDir) {
|
|
||||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
|
||||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
|
||||||
try {
|
|
||||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
|
||||||
} catch {
|
|
||||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
|
||||||
Write-Error "fail to move MAVEN_HOME"
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
|
||||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
|
||||||
@@ -29,8 +29,10 @@ public class AppSetting extends PanacheEntity {
|
|||||||
if (s == null) {
|
if (s == null) {
|
||||||
s = new AppSetting();
|
s = new AppSetting();
|
||||||
s.key = key;
|
s.key = key;
|
||||||
|
s.value = value;
|
||||||
s.persist();
|
s.persist();
|
||||||
|
} else {
|
||||||
|
s.value = value;
|
||||||
}
|
}
|
||||||
s.value = value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import java.time.Instant;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.DateTimeParseException;
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@ApplicationScoped
|
@ApplicationScoped
|
||||||
public class ReleaseGroupRepository {
|
public class ReleaseGroupRepository {
|
||||||
@@ -29,6 +32,25 @@ public class ReleaseGroupRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void replaceForArtist(String mbid, List<ReleaseGroupBrowseResult.ReleaseGroupDto> newDtos) {
|
||||||
|
FollowedArtist artist = FollowedArtist.findByMbid(mbid);
|
||||||
|
if (artist == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> newMbids = new HashSet<>();
|
||||||
|
for (ReleaseGroupBrowseResult.ReleaseGroupDto rg : newDtos) {
|
||||||
|
newMbids.add(rg.id);
|
||||||
|
upsertOne(artist, rg);
|
||||||
|
}
|
||||||
|
List<ReleaseGroupEntity> existing = ReleaseGroupEntity.list("artist", artist);
|
||||||
|
for (ReleaseGroupEntity rg : existing) {
|
||||||
|
if (!newMbids.contains(rg.mbid)) {
|
||||||
|
rg.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void markSynced(String mbid) {
|
public void markSynced(String mbid) {
|
||||||
FollowedArtist artist = FollowedArtist.findByMbid(mbid);
|
FollowedArtist artist = FollowedArtist.findByMbid(mbid);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -17,8 +18,10 @@ import jakarta.ws.rs.GET;
|
|||||||
import jakarta.ws.rs.POST;
|
import jakarta.ws.rs.POST;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.PathParam;
|
import jakarta.ws.rs.PathParam;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
@Path("/artists")
|
@Path("/artists")
|
||||||
public class ArtistResource {
|
public class ArtistResource {
|
||||||
@@ -32,10 +35,17 @@ 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;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@Location("fragments/feed-row.html")
|
||||||
|
Template fragments_feed_row;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@Location("fragments/artist-row.html")
|
@Location("fragments/artist-row.html")
|
||||||
Template fragments_artist_row;
|
Template fragments_artist_row;
|
||||||
@@ -61,16 +71,16 @@ public class ArtistResource {
|
|||||||
dto.area = new ArtistSearchResult.ArtistDto.Area();
|
dto.area = new ArtistSearchResult.ArtistDto.Area();
|
||||||
dto.area.name = area;
|
dto.area.name = area;
|
||||||
}
|
}
|
||||||
FollowedArtist artist = artistService.follow(dto);
|
artistService.follow(dto);
|
||||||
syncService.syncArtist(artist.mbid);
|
CompletableFuture.runAsync(() -> syncService.syncArtist(mbid));
|
||||||
return feedFragment(0);
|
return feedFragment(0, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("/{mbid}")
|
@Path("/{mbid}")
|
||||||
public TemplateInstance unfollow(@PathParam("mbid") String mbid) {
|
public Response unfollow(@PathParam("mbid") String mbid) {
|
||||||
artistService.unfollow(mbid);
|
artistService.unfollow(mbid);
|
||||||
return feedFragment(0);
|
return Response.ok("").build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -85,7 +95,8 @@ public class ArtistResource {
|
|||||||
FollowedArtist artist = artistService.findArtist(mbid);
|
FollowedArtist artist = artistService.findArtist(mbid);
|
||||||
List<ArtistTypeSetting> settings = artist != null ? artistService.settingsFor(artist) : List.of();
|
List<ArtistTypeSetting> settings = artist != null ? artistService.settingsFor(artist) : List.of();
|
||||||
return fragments_artist_row.data("artist", artist)
|
return fragments_artist_row.data("artist", artist)
|
||||||
.data("settings", settings);
|
.data("settings", settings)
|
||||||
|
.data("syncing", syncService.isSyncing(mbid));
|
||||||
}
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@@ -93,24 +104,28 @@ public class ArtistResource {
|
|||||||
public TemplateInstance toggleType(@PathParam("mbid") String mbid,
|
public TemplateInstance toggleType(@PathParam("mbid") String mbid,
|
||||||
@FormParam("primaryType") String primaryType,
|
@FormParam("primaryType") String primaryType,
|
||||||
@FormParam("enabled") boolean enabled) {
|
@FormParam("enabled") boolean enabled) {
|
||||||
|
artistService.setTypeEnabled(mbid, primaryType, enabled);
|
||||||
|
CompletableFuture.runAsync(() -> syncService.resyncArtist(mbid));
|
||||||
FollowedArtist artist = artistService.findArtist(mbid);
|
FollowedArtist artist = artistService.findArtist(mbid);
|
||||||
if (artist != null) {
|
List<ArtistTypeSetting> settings = artist != null ? artistService.settingsFor(artist) : List.of();
|
||||||
artistService.setTypeEnabled(artist, primaryType, enabled);
|
return fragments_artist_row.data("artist", artist)
|
||||||
syncService.resyncArtist(mbid);
|
.data("settings", settings)
|
||||||
}
|
.data("syncing", true);
|
||||||
return feedFragment(0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TemplateInstance feedFragment(int offset) {
|
private TemplateInstance feedFragment(int offset, boolean autoRefresh, boolean syncing) {
|
||||||
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)
|
||||||
.data("nextOffset", nextOffset)
|
.data("nextOffset", nextOffset)
|
||||||
.data("hasMore", hasMore)
|
.data("hasMore", hasMore)
|
||||||
.data("pageSize", feedService.pageSize());
|
.data("pageSize", feedService.pageSize())
|
||||||
|
.data("autoRefresh", autoRefresh)
|
||||||
|
.data("syncing", syncing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ 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.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;
|
||||||
@@ -20,6 +22,12 @@ public class PageResource {
|
|||||||
@Inject
|
@Inject
|
||||||
FeedService feedService;
|
FeedService feedService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SyncService syncService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SettingsService settingsService;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
Template index;
|
Template index;
|
||||||
|
|
||||||
@@ -34,24 +42,30 @@ 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)
|
||||||
.data("nextOffset", nextOffset)
|
.data("nextOffset", nextOffset)
|
||||||
.data("hasMore", hasMore)
|
.data("hasMore", hasMore)
|
||||||
.data("pageSize", feedService.pageSize());
|
.data("pageSize", feedService.pageSize())
|
||||||
|
.data("autoRefresh", false)
|
||||||
|
.data("syncing", syncService.isAnySyncing());
|
||||||
}
|
}
|
||||||
|
|
||||||
@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())
|
||||||
.data("hasMore", hasMore)
|
.data("hasMore", hasMore)
|
||||||
.data("pageSize", feedService.pageSize())
|
.data("pageSize", feedService.pageSize())
|
||||||
|
.data("autoRefresh", false)
|
||||||
|
.data("syncing", syncService.isAnySyncing())
|
||||||
.data("followedCount", FollowedArtist.count())
|
.data("followedCount", FollowedArtist.count())
|
||||||
.data("feedCount", ReleaseGroupEntity.count());
|
.data("feedCount", ReleaseGroupEntity.count());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -63,7 +63,11 @@ public class ArtistService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void setTypeEnabled(FollowedArtist artist, String primaryType, boolean enabled) {
|
public void setTypeEnabled(String mbid, String primaryType, boolean enabled) {
|
||||||
|
FollowedArtist artist = FollowedArtist.findByMbid(mbid);
|
||||||
|
if (artist == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (ArtistTypeSetting s : ArtistTypeSetting.findByArtist(artist)) {
|
for (ArtistTypeSetting s : ArtistTypeSetting.findByArtist(artist)) {
|
||||||
if (s.primaryType.equalsIgnoreCase(primaryType)) {
|
if (s.primaryType.equalsIgnoreCase(primaryType)) {
|
||||||
s.enabled = enabled;
|
s.enabled = enabled;
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.discdrop.service;
|
|||||||
|
|
||||||
import io.discdrop.persistence.AppSetting;
|
import io.discdrop.persistence.AppSetting;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
@@ -14,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");
|
||||||
|
|
||||||
@@ -22,6 +25,7 @@ public class SettingsService {
|
|||||||
return parseTypes(raw);
|
return parseTypes(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
public void setDefaultPrimaryTypes(Set<String> types) {
|
public void setDefaultPrimaryTypes(Set<String> types) {
|
||||||
AppSetting.set(KEY_DEFAULT_PRIMARY_TYPES, String.join(",", types));
|
AppSetting.set(KEY_DEFAULT_PRIMARY_TYPES, String.join(",", types));
|
||||||
}
|
}
|
||||||
@@ -35,10 +39,29 @@ public class SettingsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
public void setSyncScheduleHours(int hours) {
|
public void setSyncScheduleHours(int hours) {
|
||||||
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<>();
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import jakarta.enterprise.context.ApplicationScoped;
|
|||||||
import jakarta.enterprise.event.Observes;
|
import jakarta.enterprise.event.Observes;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
@@ -31,6 +34,8 @@ public class SyncService {
|
|||||||
@Inject
|
@Inject
|
||||||
SettingsService settingsService;
|
SettingsService settingsService;
|
||||||
|
|
||||||
|
private final Set<String> syncing = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
private final ScheduledExecutorService executor =
|
private final ScheduledExecutorService executor =
|
||||||
Executors.newSingleThreadScheduledExecutor(r -> {
|
Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
Thread t = new Thread(r, "discdrop-sync");
|
Thread t = new Thread(r, "discdrop-sync");
|
||||||
@@ -73,27 +78,61 @@ public class SyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void syncArtist(String mbid) {
|
public boolean isSyncing(String mbid) {
|
||||||
Set<String> enabledTypes = artistService.enabledTypes(mbid);
|
return syncing.contains(mbid);
|
||||||
String typeFilter = enabledTypes.isEmpty() ? null : String.join("|", enabledTypes);
|
}
|
||||||
|
|
||||||
int limit = 100;
|
public boolean isAnySyncing() {
|
||||||
int offset = 0;
|
return !syncing.isEmpty();
|
||||||
boolean more = true;
|
}
|
||||||
while (more) {
|
|
||||||
ReleaseGroupBrowseResult page = mbzService.browseReleaseGroups(mbid, typeFilter, limit, offset);
|
public void syncArtist(String mbid) {
|
||||||
if (page == null || page.releaseGroups == null || page.releaseGroups.isEmpty()) {
|
syncing.add(mbid);
|
||||||
break;
|
try {
|
||||||
|
Set<String> enabledTypes = artistService.enabledTypes(mbid);
|
||||||
|
String typeFilter = enabledTypes.isEmpty() ? null : String.join("|", enabledTypes);
|
||||||
|
|
||||||
|
int limit = 100;
|
||||||
|
int offset = 0;
|
||||||
|
boolean more = true;
|
||||||
|
while (more) {
|
||||||
|
ReleaseGroupBrowseResult page = mbzService.browseReleaseGroups(mbid, typeFilter, limit, offset);
|
||||||
|
if (page == null || page.releaseGroups == null || page.releaseGroups.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
repo.upsertPage(mbid, page.releaseGroups);
|
||||||
|
offset += page.releaseGroups.size();
|
||||||
|
more = page.releaseGroups.size() == limit && offset < page.count;
|
||||||
}
|
}
|
||||||
repo.upsertPage(mbid, page.releaseGroups);
|
repo.markSynced(mbid);
|
||||||
offset += page.releaseGroups.size();
|
} finally {
|
||||||
more = page.releaseGroups.size() == limit && offset < page.count;
|
syncing.remove(mbid);
|
||||||
}
|
}
|
||||||
repo.markSynced(mbid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resyncArtist(String mbid) {
|
public void resyncArtist(String mbid) {
|
||||||
repo.deleteByArtistMbid(mbid);
|
syncing.add(mbid);
|
||||||
syncArtist(mbid);
|
try {
|
||||||
|
Set<String> enabledTypes = artistService.enabledTypes(mbid);
|
||||||
|
String typeFilter = enabledTypes.isEmpty() ? null : String.join("|", enabledTypes);
|
||||||
|
|
||||||
|
List<ReleaseGroupBrowseResult.ReleaseGroupDto> all = new ArrayList<>();
|
||||||
|
int limit = 100;
|
||||||
|
int offset = 0;
|
||||||
|
boolean more = true;
|
||||||
|
while (more) {
|
||||||
|
ReleaseGroupBrowseResult page = mbzService.browseReleaseGroups(mbid, typeFilter, limit, offset);
|
||||||
|
if (page == null || page.releaseGroups == null || page.releaseGroups.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
all.addAll(page.releaseGroups);
|
||||||
|
offset += page.releaseGroups.size();
|
||||||
|
more = page.releaseGroups.size() == limit && offset < page.count;
|
||||||
|
}
|
||||||
|
repo.replaceForArtist(mbid, all);
|
||||||
|
repo.markSynced(mbid);
|
||||||
|
} finally {
|
||||||
|
syncing.remove(mbid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/* DiscDrop custom styles */
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
|
||||||
|
/* Hide empty search dropdown */
|
||||||
|
#search-dropdown:empty { display: none; }
|
||||||
|
|
||||||
|
/* Remove native search input artifacts */
|
||||||
|
input[type="text"]::-webkit-search-cancel-button,
|
||||||
|
input[type="search"]::-webkit-search-cancel-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cover art in feed */
|
||||||
|
.cover-art {
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 469 B After Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 676 B After Width: | Height: | Size: 676 B |
|
Before Width: | Height: | Size: 418 B After Width: | Height: | Size: 418 B |
@@ -11,7 +11,7 @@ quarkus.hibernate-orm.database.generation=update
|
|||||||
# MusicBrainz REST client (configKey="musicbrainz")
|
# MusicBrainz REST client (configKey="musicbrainz")
|
||||||
quarkus.rest-client."musicbrainz".url=https://musicbrainz.org/ws/2
|
quarkus.rest-client."musicbrainz".url=https://musicbrainz.org/ws/2
|
||||||
# User-Agent (MBZ requires a meaningful contact string) - override per environment, never hardcode in code
|
# User-Agent (MBZ requires a meaningful contact string) - override per environment, never hardcode in code
|
||||||
discdrop.mbz.user-agent=DiscDrop/1.0 ([email protected])
|
discdrop.mbz.user-agent=${MBZ_USER_AGENT}
|
||||||
discdrop.mbz.rate-limit-ms=1000
|
discdrop.mbz.rate-limit-ms=1000
|
||||||
discdrop.feed.page-size=25
|
discdrop.feed.page-size=25
|
||||||
discdrop.rss.item-count=50
|
discdrop.rss.item-count=50
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
/* DiscDrop custom styles */
|
|
||||||
html { scroll-behavior: smooth; }
|
|
||||||
#search-dropdown:empty { display: none; }
|
|
||||||
@@ -4,22 +4,25 @@
|
|||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>DiscDrop – Artists</title>
|
<title>DiscDrop – Artists</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg"/>
|
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg"/>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css"/>
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css"/>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://unpkg.com/[email protected]"></script>
|
<script src="https://unpkg.com/[email protected]"></script>
|
||||||
<link rel="stylesheet" href="/static/css/app.css"/>
|
<link rel="stylesheet" href="/css/app.css"/>
|
||||||
<script src="/static/js/app.js" defer></script>
|
<script src="/js/app.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="sticky top-0 z-30 bg-base-100/90 backdrop-blur border-b border-base-300">
|
<header class="sticky top-0 z-30 bg-base-100/90 backdrop-blur border-b border-base-300">
|
||||||
<div class="navbar flex flex-wrap justify-center gap-4 px-4 py-3 max-w-5xl mx-auto">
|
<div class="navbar flex flex-wrap justify-center gap-4 px-4 py-3 max-w-5xl mx-auto">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<img src="/static/img/logo.svg" alt="DiscDrop logo" class="h-9 w-9"/>
|
<img src="/img/logo.svg" alt="DiscDrop logo" class="h-9 w-9"/>
|
||||||
<span class="text-xl font-bold">DiscDrop</span>
|
<span class="text-xl font-bold">DiscDrop</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<a href="/" class="btn btn-ghost btn-sm">Feed</a>
|
<a href="/" class="btn btn-ghost btn-sm">Feed</a>
|
||||||
|
<a href="/rss" class="btn btn-ghost btn-circle" aria-label="RSS feed" title="RSS feed">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M6.18 15.64a2.18 2.18 0 012.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 012.18-2.18M4 4.44A15.56 15.56 0 0119.56 20h-2.83A12.73 12.73 0 004 7.27V4.44m0 5.66a9.9 9.9 0 019.9 9.9h-2.83A7.07 7.07 0 004 12.93V10.1z"/></svg>
|
||||||
|
</a>
|
||||||
<button id="theme-switcher" class="btn btn-ghost btn-circle" aria-label="Toggle theme" onclick="DiscDrop.toggleTheme()">
|
<button id="theme-switcher" class="btn btn-ghost btn-circle" aria-label="Toggle theme" onclick="DiscDrop.toggleTheme()">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -36,7 +39,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{#else}
|
{#else}
|
||||||
{#for artist in artists}
|
{#for artist in artists}
|
||||||
<div hx-get="/artists/{artist.mbid}/row" hx-trigger="revealed" hx-swap="innerHTML">
|
<div id="artist-{artist.mbid}"
|
||||||
|
hx-get="/artists/{artist.mbid}/row"
|
||||||
|
hx-trigger="revealed"
|
||||||
|
hx-target="this"
|
||||||
|
hx-swap="innerHTML">
|
||||||
<div class="loading loading-spinner loading-sm"></div>
|
<div class="loading loading-spinner loading-sm"></div>
|
||||||
</div>
|
</div>
|
||||||
{/for}
|
{/for}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="btn btn-ghost btn-xs text-error"
|
<button class="btn btn-ghost btn-xs text-error"
|
||||||
hx-delete="/artists/{artist.mbid}"
|
hx-delete="/artists/{artist.mbid}"
|
||||||
hx-target="closest div.space-y-3"
|
hx-target="#artist-{artist.mbid}"
|
||||||
hx-swap="outerHTML"
|
hx-swap="delete"
|
||||||
hx-on::after-request="DiscDrop.refreshFeed()">
|
hx-on::after-request="DiscDrop.refreshFeed()">
|
||||||
Unfollow
|
Unfollow
|
||||||
</button>
|
</button>
|
||||||
@@ -20,17 +20,30 @@
|
|||||||
{#for s in settings}
|
{#for s in settings}
|
||||||
<label class="label cursor-pointer gap-1 badge badge-outline py-2">
|
<label class="label cursor-pointer gap-1 badge badge-outline py-2">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
|
name="enabled"
|
||||||
class="checkbox checkbox-xs"
|
class="checkbox checkbox-xs"
|
||||||
value="true"
|
value="true"
|
||||||
{#if s.enabled}checked{/if}
|
{#if s.enabled}checked{/if}
|
||||||
hx-post="/artists/{artist.mbid}/types"
|
hx-post="/artists/{artist.mbid}/types"
|
||||||
hx-vals='{"primaryType":"{s.primaryType}"}'
|
hx-vals='{"primaryType":"{s.primaryType}"}'
|
||||||
hx-include="this"
|
hx-include="this"
|
||||||
hx-target="#feed-list"
|
hx-target="#artist-{artist.mbid}"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-on::after-request="DiscDrop.refreshFeed()"/>
|
hx-on::after-request="DiscDrop.refreshFeed()"/>
|
||||||
<span class="text-xs">{s.primaryType}</span>
|
<span class="text-xs">{s.primaryType}</span>
|
||||||
</label>
|
</label>
|
||||||
{/for}
|
{/for}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if syncing?? && syncing}
|
||||||
|
<div class="flex items-center gap-2 text-sm text-base-content/60 pt-1">
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
<span>Syncing releases from MusicBrainz…</span>
|
||||||
|
</div>
|
||||||
|
<div hx-get="/artists/{artist.mbid}/row"
|
||||||
|
hx-target="#artist-{artist.mbid}"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-trigger="load delay:3s"
|
||||||
|
class="hidden"></div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
{#if syncing?? && syncing}
|
||||||
|
<div class="flex items-center justify-center gap-2 py-2 text-base-content/60">
|
||||||
|
<span class="loading loading-spinner loading-sm text-primary"></span>
|
||||||
|
<span class="text-sm">Fetching releases from MusicBrainz…</span>
|
||||||
|
</div>
|
||||||
|
<div hx-get="/feed?offset=0"
|
||||||
|
hx-target="#feed-list"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-trigger="load delay:3s"
|
||||||
|
class="hidden"></div>
|
||||||
|
{/if}
|
||||||
{#for row in rows}
|
{#for row in rows}
|
||||||
{#include fragments/feed-row.html row=row /}
|
{#include fragments/feed-row.html row=row /}
|
||||||
{/for}
|
{/for}
|
||||||
@@ -10,7 +21,7 @@
|
|||||||
Load more
|
Load more
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !hasMore && rows.empty}
|
{#if !hasMore && rows.empty && !(syncing?? && syncing)}
|
||||||
<div class="text-center text-base-content/60 py-12">
|
<div class="text-center text-base-content/60 py-12">
|
||||||
<p class="text-lg">No releases yet.</p>
|
<p class="text-lg">No releases yet.</p>
|
||||||
<p class="text-sm">Search for an artist above and click <strong>Follow</strong> to start tracking their release groups.</p>
|
<p class="text-sm">Search for an artist above and click <strong>Follow</strong> to start tracking their release groups.</p>
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
<div class="flex gap-3 items-center bg-base-100 rounded-box border border-base-300 p-3 shadow-sm">
|
<div class="flex gap-3 items-center bg-base-100 rounded-box border border-base-300 p-2.5 shadow-sm">
|
||||||
<img src="https://coverartarchive.org/release-group/{row.mbid}/front"
|
<img src="https://coverartarchive.org/release-group/{row.mbid}/front"
|
||||||
alt="cover"
|
alt="cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="w-16 h-16 rounded object-cover bg-base-200"
|
class="cover-art w-40 h-40 object-cover shrink-0"
|
||||||
onerror="this.onerror=null;this.src='/static/img/no-cover.svg'"/>
|
onerror="this.onerror=null;this.src='/img/no-cover.svg'"/>
|
||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0 flex flex-col justify-center gap-1">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="font-semibold text-sm text-base-content/70 truncate">{row.artist.name}</div>
|
||||||
<span class="font-semibold truncate">{row.artist.name}</span>
|
<a href="{row.mbzUrl}" target="_blank" rel="noopener"
|
||||||
{#if row.primaryType??}<span class="badge badge-primary badge-sm">{row.primaryType}</span>{/if}
|
class="link link-hover text-base font-bold leading-tight block truncate">
|
||||||
{#if row.secondaryTypes??}<span class="badge badge-ghost badge-sm">{row.secondaryTypes}</span>{/if}
|
|
||||||
</div>
|
|
||||||
<a href="{row.mbzUrl}" target="_blank" rel="noopener" class="link link-hover text-base-content/90 block truncate">
|
|
||||||
{row.title}
|
{row.title}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<div class="flex flex-wrap items-center gap-2 mt-0.5">
|
||||||
|
{#if row.primaryType??}<span class="badge badge-primary badge-sm">{row.primaryType}</span>{/if}
|
||||||
<div class="text-right text-sm text-base-content/70 shrink-0">
|
{#if row.secondaryTypes??}<span class="badge badge-ghost badge-sm">{row.secondaryTypes}</span>{/if}
|
||||||
{#if row.firstReleaseDateRaw??}{row.firstReleaseDateRaw}{#else}—{/if}
|
<span class="text-sm text-base-content/60">
|
||||||
|
{#if row.firstReleaseDateRaw??}{row.firstReleaseDateRaw}{#else}—{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -4,38 +4,37 @@
|
|||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>DiscDrop</title>
|
<title>DiscDrop</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg"/>
|
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg"/>
|
||||||
<link rel="alternate" type="application/rss+xml" title="DiscDrop – New Releases" href="/rss"/>
|
<link rel="alternate" type="application/rss+xml" title="DiscDrop – New Releases" href="/rss"/>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css"/>
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css"/>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://unpkg.com/[email protected]"></script>
|
<script src="https://unpkg.com/[email protected]"></script>
|
||||||
<link rel="stylesheet" href="/static/css/app.css"/>
|
<link rel="stylesheet" href="/css/app.css"/>
|
||||||
<script src="/static/js/app.js" defer></script>
|
<script src="/js/app.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="sticky top-0 z-30 bg-base-100/90 backdrop-blur border-b border-base-300">
|
<header class="sticky top-0 z-30 bg-base-100/90 backdrop-blur border-b border-base-300">
|
||||||
<div class="navbar flex flex-wrap justify-center gap-4 px-4 py-3 max-w-5xl mx-auto">
|
<div class="navbar flex flex-wrap justify-center gap-4 px-4 py-3 max-w-5xl mx-auto">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<img src="/static/img/logo.svg" alt="DiscDrop logo" class="h-9 w-9"/>
|
<img src="/img/logo.svg" alt="DiscDrop logo" class="h-9 w-9"/>
|
||||||
<span class="text-xl font-bold">DiscDrop</span>
|
<span class="text-xl font-bold">DiscDrop</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="search-wrap" class="relative flex-1 min-w-[220px] max-w-xl">
|
<div id="search-wrap" class="relative flex-1 min-w-[220px] max-w-xl">
|
||||||
<form id="search-form" class="join w-full" autocomplete="off">
|
<input
|
||||||
<input
|
id="search-input"
|
||||||
id="search-input"
|
type="text"
|
||||||
type="search"
|
name="q"
|
||||||
name="q"
|
placeholder="Search MusicBrainz artists…"
|
||||||
placeholder="Search MusicBrainz artists…"
|
class="input input-bordered w-full"
|
||||||
class="input input-bordered join-item w-full"
|
autocomplete="off"
|
||||||
hx-get="/search/artists"
|
hx-get="/search/artists"
|
||||||
hx-trigger="input changed delay:300ms"
|
hx-trigger="input changed delay:300ms"
|
||||||
hx-target="#search-dropdown"
|
hx-target="#search-dropdown"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-include="this"/>
|
hx-include="this"/>
|
||||||
</form>
|
|
||||||
<div id="search-dropdown"
|
<div id="search-dropdown"
|
||||||
class="absolute left-0 right-0 z-40 mt-1 bg-base-100 rounded-box shadow-lg border border-base-300 max-h-96 overflow-y-auto"></div>
|
class="absolute left-0 right-0 top-full z-40 mt-1 bg-base-100 rounded-box shadow-lg border border-base-300 max-h-96 overflow-y-auto"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -57,6 +56,10 @@
|
|||||||
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<a href="/rss" class="btn btn-ghost btn-circle" aria-label="RSS feed" title="RSS feed">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M6.18 15.64a2.18 2.18 0 012.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 012.18-2.18M4 4.44A15.56 15.56 0 0119.56 20h-2.83A12.73 12.73 0 004 7.27V4.44m0 5.66a9.9 9.9 0 019.9 9.9h-2.83A7.07 7.07 0 004 12.93V10.1z"/></svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
<a href="/artists" class="btn btn-ghost btn-sm" aria-label="Artists">Artists</a>
|
<a href="/artists" class="btn btn-ghost btn-sm" aria-label="Artists">Artists</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||