2a19f84575
genre_review_service.run() and dedup_review_service._run_scan() both unconditionally created a new run row even when the underlying job never actually executed (skipped_lock, or any other non-success status). For genres this was directly user-visible: the review page always shows the most recent run, so a skipped click created an empty run that displaced the real previous preview, making it look like every pending change had vanished. Both now return None (persisting nothing) when the job didn't succeed, and both routers surface a "didn't run, something else was using the pipeline" notice instead of silently redirecting. Cleaned up the one phantom empty genre_runs row already sitting in production, restoring the real 20-change preview. Also added a "Run now" button to each row on the Playlists list page (previously only on a playlist's own detail page), for the common case of adding new tracks and wanting to sync immediately.
66 lines
2.8 KiB
HTML
66 lines
2.8 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Playlists — alembic{% endblock %}
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<div>
|
|
<h1>Playlists</h1>
|
|
<p class="subtitle muted">Each active playlist syncs on its own schedule, or run it manually from its page.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-wrap scroll">
|
|
<table>
|
|
<thead>
|
|
<tr><th>Name</th><th>Active</th><th>Sync time</th><th>No M3U</th><th></th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for p in playlists %}
|
|
<tr>
|
|
<td><a href="/playlists/{{ p.id }}">{{ p.name }}</a></td>
|
|
<td>{% if p.active %}<span class="badge badge-success">active</span>{% else %}<span class="badge badge-muted">paused</span>{% endif %}</td>
|
|
<td class="muted mono">{{ sync_times.get(p.id) or "unscheduled" }}</td>
|
|
<td class="muted">{{ "yes" if p.no_m3u else "no" }}</td>
|
|
<td class="actions-cell">
|
|
<form method="post" action="/settings/jobs/playlist:{{ p.name }}/run" class="inline">
|
|
<button type="submit" class="btn btn-sm" title="Sync this playlist now instead of waiting for its scheduled time">Run now</button>
|
|
</form>
|
|
<form method="post" action="/playlists/{{ p.id }}/delete" class="inline"
|
|
onsubmit="return confirm('Remove {{ p.name }}? This deletes its rendered config but not any downloaded music.')">
|
|
<button type="submit" class="btn btn-sm btn-danger">Remove</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr class="empty-row"><td colspan="5">
|
|
<div class="empty-state">
|
|
<div class="sparkle-inline"><span class="sparkle" style="position:relative;top:0;left:0;"></span></div>
|
|
<p class="muted" style="margin:0;">No playlists yet — add your first one below.</p>
|
|
</div>
|
|
</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h2>Add a playlist</h2>
|
|
<div class="panel" style="max-width:560px;">
|
|
<form method="post" action="/playlists">
|
|
<div class="field">
|
|
<label for="name">Name (used as the beets grouping tag)</label>
|
|
<input type="text" id="name" name="name" required pattern="[a-z0-9_-]+" placeholder="late-night-drive">
|
|
</div>
|
|
<div class="field">
|
|
<label for="spotify_url">Spotify playlist URL</label>
|
|
<input type="text" id="spotify_url" name="spotify_url" required placeholder="https://open.spotify.com/playlist/...">
|
|
</div>
|
|
<div class="field">
|
|
<label for="sync_time">Sync time (daily)</label>
|
|
<input type="time" id="sync_time" name="sync_time">
|
|
<span class="hint">Leave blank for unscheduled/manual-only.</span>
|
|
</div>
|
|
<label class="checkbox-field"><input type="checkbox" name="no_m3u" value="true"> Skip M3U generation (e.g. "liked songs")</label>
|
|
<button type="submit" class="btn btn-primary">Add playlist</button>
|
|
</form>
|
|
</div>
|
|
{% endblock %}
|