Fix M3U write permission failure; add recently-downloaded section to dashboard
Playlist M3U regen now writes to a temp file and renames it into place, so a stray wrong-owner leftover file (root:root, from pre-migration host-cron runs) can't block nightly writes the way it did last night. Dashboard now lists tracks added to the beets library in the last 24h with playlist and format, sourced from a new beets_service.recently_added().
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import time
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import select
|
||||
@@ -10,6 +12,8 @@ from app.services import beets_service, credential_service, playlist_service
|
||||
router = APIRouter(tags=["dashboard"])
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
_RECENT_TRACKS_WINDOW_SECONDS = 24 * 60 * 60
|
||||
|
||||
|
||||
def _human_bytes(n: int) -> str:
|
||||
size = float(n)
|
||||
@@ -27,6 +31,7 @@ async def dashboard(request: Request, user: dict = Depends(require_auth), db=Dep
|
||||
recent_runs = list(
|
||||
db.execute(select(JobRun).order_by(JobRun.started_at.desc()).limit(10)).scalars()
|
||||
)
|
||||
recent_tracks = beets_service.recently_added(time.time() - _RECENT_TRACKS_WINDOW_SECONDS)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"dashboard.html",
|
||||
@@ -37,6 +42,7 @@ async def dashboard(request: Request, user: dict = Depends(require_auth), db=Dep
|
||||
"playlist_count": len(playlists),
|
||||
"active_playlist_count": len([p for p in playlists if p.active]),
|
||||
"recent_runs": recent_runs,
|
||||
"recent_tracks": recent_tracks,
|
||||
"auth_states": credential_service.auth_states(db),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -185,6 +185,25 @@ def distinct_groupings(conn: sqlite3.Connection | None = None) -> list[str]:
|
||||
conn.close()
|
||||
|
||||
|
||||
def recently_added(since: float, limit: int = 200) -> list[dict]:
|
||||
"""Tracks whose beets `added` timestamp falls after `since` (epoch
|
||||
seconds) -- drives the dashboard's "downloaded recently" list. Capped
|
||||
at `limit`: a normal nightly batch is tens of tracks, this is just a
|
||||
guard against one huge backfill import flooding the dashboard."""
|
||||
if not db_exists():
|
||||
return []
|
||||
cols = ", ".join(_ITEM_COLUMNS)
|
||||
conn = _connect()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
f"SELECT {cols} FROM items WHERE added >= ? ORDER BY added DESC LIMIT ?",
|
||||
(since, limit),
|
||||
)
|
||||
return [_row_to_dict(row) for row in cur.fetchall()]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def distinct_formats() -> list[str]:
|
||||
"""For the library page's format filter dropdown."""
|
||||
if not db_exists():
|
||||
|
||||
@@ -49,6 +49,25 @@
|
||||
</ul>
|
||||
<p><a href="/settings/credentials">Manage credentials →</a></p>
|
||||
|
||||
<h2>Downloaded in the last 24 hours</h2>
|
||||
<div class="table-wrap scroll">
|
||||
<table>
|
||||
<thead><tr><th>Artist</th><th>Title</th><th>Playlist</th><th>Format</th></tr></thead>
|
||||
<tbody>
|
||||
{% for t in recent_tracks %}
|
||||
<tr>
|
||||
<td>{{ t.artist }}</td>
|
||||
<td>{{ t.title }}</td>
|
||||
<td class="muted">{{ t.grouping }}</td>
|
||||
<td><span class="badge badge-muted">{{ t.format }}</span></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr class="empty-row"><td colspan="4" class="muted">Nothing downloaded in the last 24 hours.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>Recent job runs</h2>
|
||||
<div class="table-wrap scroll">
|
||||
<table>
|
||||
|
||||
@@ -173,10 +173,18 @@ else
|
||||
fi
|
||||
|
||||
if [[ "$TRACK_COUNT" -gt 0 ]]; then
|
||||
# Write to a temp file in the same dir, then rename over the target.
|
||||
# rename(2) only needs write access on the directory, not on the file
|
||||
# being replaced -- so this also survives a stray wrong-owner leftover
|
||||
# M3U (e.g. one written by a previous non-containerized run) without
|
||||
# needing a host-side chown, and it's atomic for anything (Navidrome)
|
||||
# reading the file mid-write.
|
||||
M3U_TMP="${M3U_OUT}.tmp.$$"
|
||||
{
|
||||
echo "#EXTM3U"
|
||||
echo "$BEET_OUTPUT"
|
||||
} > "$M3U_OUT"
|
||||
} > "$M3U_TMP"
|
||||
mv "$M3U_TMP" "$M3U_OUT"
|
||||
log "M3U updated with $TRACK_COUNT tracks"
|
||||
else
|
||||
log "WARNING: beet list returned no tracks for grouping:$PLAYLIST_NAME — not updating M3U"
|
||||
|
||||
Reference in New Issue
Block a user