Public Access
Vendor htmx, add Dockerfile, show a placeholder for missing covers
Serve htmx 2.0.4 from the package instead of the jsdelivr CDN. Add a Dockerfile for local testing. Cover images now sit in a styled placeholder box that stays visible when Music Assistant has no cover for an album. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
.venv
|
||||
**/__pycache__
|
||||
*.sqlite3
|
||||
*.sqlite3-*
|
||||
.env
|
||||
.envrc
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.12 /uv /usr/local/bin/uv
|
||||
|
||||
WORKDIR /app
|
||||
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
|
||||
|
||||
# Dependencies first, so code changes don't reinstall them.
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN uv sync --locked --no-dev --no-install-project
|
||||
COPY src ./src
|
||||
RUN uv sync --locked --no-dev
|
||||
|
||||
RUN useradd --system --no-create-home app && mkdir /data && chown app /data
|
||||
USER app
|
||||
|
||||
ENV PATH=/app/.venv/bin:$PATH \
|
||||
DB_PATH=/data/tag_albums.sqlite3
|
||||
VOLUME /data
|
||||
EXPOSE 8087
|
||||
CMD ["tag-albums", "serve", "--port", "8087"]
|
||||
@@ -43,6 +43,21 @@ uv run pytest
|
||||
API_TOKEN=dev HA_URL=https://ha.home.knbg HA_TOKEN=... uv run tag-albums serve --port 8087
|
||||
```
|
||||
|
||||
## Running with Docker
|
||||
|
||||
For local testing. The container has to trust the home CA to reach HA, so mount the CA certificate and point `SSL_CERT_FILE` at it:
|
||||
|
||||
```sh
|
||||
docker build -t tag-albums .
|
||||
docker run --rm -p 8087:8087 -v tag-albums-data:/data \
|
||||
-e API_TOKEN=dev -e HA_URL=https://ha.home.knbg -e HA_TOKEN=... \
|
||||
-v /usr/local/share/ca-certificates/KNBG_Root_CA_306526670621344964268144771797781855543.crt:/ca.crt:ro \
|
||||
-e SSL_CERT_FILE=/ca.crt \
|
||||
tag-albums
|
||||
```
|
||||
|
||||
To import assignments, run `tag-albums import-yaml` inside the container, e.g. `docker run --rm -v tag-albums-data:/data -v $PWD/tag_albums.yaml:/import.yaml:ro tag-albums tag-albums import-yaml /import.yaml`.
|
||||
|
||||
## Deploying in a FreeBSD jail
|
||||
|
||||
```sh
|
||||
|
||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
import httpx
|
||||
from fastapi import Depends, FastAPI, Form, Header, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -40,6 +41,7 @@ def create_app(settings: Settings, store: Store | None = None, library: MusicLib
|
||||
store = store or Store(settings.db_path)
|
||||
library = library or MusicLibrary(settings.ha_url, settings.ha_token)
|
||||
app = FastAPI(title="Tag albums", docs_url=None, redoc_url=None)
|
||||
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
|
||||
|
||||
def require_token(authorization: str = Header(default="")) -> None:
|
||||
scheme, _, token = authorization.partition(" ")
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
|
||||
Zero-Clause BSD
|
||||
=============
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for
|
||||
any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
|
||||
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
|
||||
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -4,7 +4,7 @@
|
||||
<ul class="rows">
|
||||
{% for a in albums %}
|
||||
<li>
|
||||
{% if a.image %}<img class="cover" src="{{ a.image }}" alt="" loading="lazy">{% else %}<span class="cover"></span>{% endif %}
|
||||
<span class="cover">{% if a.image %}<img src="{{ a.image }}" alt="" loading="lazy" onerror="this.remove()">{% endif %}</span>
|
||||
<div class="grow"><strong>{{ a.album }}</strong> · {{ a.artist }}</div>
|
||||
<form class="inline" method="post" action="/tags/{{ tag_id }}/assign">
|
||||
<input type="hidden" name="artist" value="{{ a.artist }}">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Tag albums{% endblock %}</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.4/dist/htmx.min.js"></script>
|
||||
<script src="/static/htmx-2.0.4.min.js"></script>
|
||||
<style>
|
||||
:root { --bg: #fafafa; --fg: #1d1d1f; --muted: #6e6e73; --line: #e2e2e6; --card: #fff; --accent: #2f6fde; --danger: #c0392b; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -22,7 +22,8 @@
|
||||
ul.rows li { display: flex; gap: 12px; align-items: center; padding: 10px 12px; border-top: 1px solid var(--line); }
|
||||
ul.rows li:first-child { border-top: 0; }
|
||||
.grow { flex: 1; min-width: 0; }
|
||||
img.cover { width: 44px; height: 44px; border-radius: 6px; object-fit: cover; background: var(--line); flex: none; }
|
||||
.cover { width: 44px; height: 44px; border-radius: 6px; overflow: hidden; background: var(--line); flex: none; }
|
||||
.cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||
form.inline { display: inline; }
|
||||
button { font: inherit; padding: 5px 12px; border-radius: 7px; border: 1px solid var(--line); background: var(--card); color: var(--fg); cursor: pointer; }
|
||||
button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<ul class="rows">
|
||||
{% for tag in assigned %}
|
||||
<li>
|
||||
{% if tag.image %}<img class="cover" src="{{ tag.image }}" alt="" loading="lazy">{% else %}<span class="cover"></span>{% endif %}
|
||||
<span class="cover">{% if tag.image %}<img src="{{ tag.image }}" alt="" loading="lazy" onerror="this.remove()">{% endif %}</span>
|
||||
<div class="grow">
|
||||
<div><strong>{{ tag.album }}</strong> · {{ tag.artist }}</div>
|
||||
<div class="muted"><code>{{ tag.tag_id }}</code>{% if tag.last_seen %} · last scanned {{ tag.last_seen|local_time }}{% endif %}</div>
|
||||
|
||||
@@ -63,6 +63,12 @@ def test_pages_render(client, store):
|
||||
assert client.get("/tags/ZZ").status_code == 404
|
||||
|
||||
|
||||
def test_htmx_is_served_locally(client):
|
||||
assert 'src="/static/htmx-2.0.4.min.js"' in client.get("/").text
|
||||
r = client.get("/static/htmx-2.0.4.min.js")
|
||||
assert r.status_code == 200 and r.text.startswith("var htmx=")
|
||||
|
||||
|
||||
def test_search_results(client, library):
|
||||
r = client.get("/tags/AA-BB/search", params={"q": " lateralus "})
|
||||
assert library.queries == ["lateralus"]
|
||||
|
||||
Reference in New Issue
Block a user