Initial import: Music_Server, MusicFree, catalog-sync

This commit is contained in:
2026-05-23 16:51:14 +08:00
commit 069af30dba
847 changed files with 179878 additions and 0 deletions
@@ -0,0 +1,273 @@
{% extends "ops/base.html" %}
{% block content %}
{% set done_statuses = ("completed", "completed_with_errors", "failed", "canceled") %}
{% macro render_task_tree_node(row) -%}
{% set row_status = row.status or "" %}
{% set toggle_command = "resume" if row_status in ("paused", "pause_requested") else "pause" if row_status in ("queued", "running") else "" %}
{% set can_cancel = row_status in ("queued", "running", "paused", "pause_requested") %}
<section class="task-tree-node task-tree-node-task" data-task-node="{{ row.id }}">
<div class="task-tree-row">
<button type="button" class="tree-toggle" data-task-toggle="{{ row.id }}" aria-expanded="false" aria-label="Expand task {{ row.id }}">+</button>
<div class="task-tree-main">
<div class="task-tree-title-line">
<strong data-task-name>{{ row.display_name }}</strong>
<span class="muted task-tree-meta-inline" data-task-meta-inline>#{{ row.id }} / {{ row.job_type }} / {{ row.scope_summary }} / {{ row.queue_label or row.lane_type or "-" }} / workers {{ row.active_worker_count }}</span>
<span class="status-tag status-{{ row.status }}" data-task-status>{{ row.status }}</span>
</div>
</div>
<div class="task-tree-progress" data-task-progress>
<div class="progress-meta">
<span>{{ row.primary_progress_text or "-" }}</span>
<strong>{{ row.primary_progress_percent or 0 }}%</strong>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {{ row.primary_progress_percent or 0 }}%;"></div>
</div>
</div>
<div class="task-tree-actions">
<div class="button-grid">
{% if toggle_command %}
<button
type="button"
data-task-command-toggle="{{ row.id }}"
data-task-command-type="{{ toggle_command }}"
>
{% if toggle_command == "resume" %}&gt;{% else %}||{% endif %}
</button>
{% else %}
<span class="muted">-</span>
{% endif %}
{% if can_cancel %}
<button type="button" class="secondary" data-task-command-cancel="{{ row.id }}">x</button>
{% endif %}
</div>
</div>
</div>
<div class="task-tree-children" data-task-children="{{ row.id }}" hidden>
<p class="muted">Expand to load playlists...</p>
</div>
</section>
{%- endmacro %}
<h1>Task Center</h1>
<div class="card">
<div data-live-status>Live snapshot: waiting...</div>
</div>
<div class="grid">
<div class="card">
<h2>Summary</h2>
<table>
<tr><th>Total Jobs</th><td data-summary-field="total_jobs">{{ summary.total_jobs }}</td></tr>
<tr><th>Queued</th><td data-summary-field="queued_jobs">{{ summary.queued_jobs }}</td></tr>
<tr><th>Queued Download Jobs</th><td data-summary-field="queued_download_jobs">{{ summary.queued_download_jobs }}</td></tr>
<tr><th>Running</th><td data-summary-field="running_jobs">{{ summary.running_jobs }}</td></tr>
<tr><th>Paused</th><td data-summary-field="paused_jobs">{{ summary.paused_jobs }}</td></tr>
<tr><th>Failed / Errors</th><td data-summary-field="failed_jobs">{{ summary.failed_jobs }}</td></tr>
<tr><th>Downloaded Songs</th><td data-download-field="downloaded_songs">{{ download_stats.downloaded_songs }}</td></tr>
<tr><th>Running Songs</th><td data-download-field="running_song_items">{{ download_stats.running_song_items }}</td></tr>
</table>
</div>
<div class="card">
<h2>Quick Actions</h2>
<div class="button-grid">
<form action="/api/jobs" method="post" data-json-form data-success="reload">
<input type="hidden" name="job_type" value="catalog_sync" />
<input type="hidden" name="requested_by" value="ops-console" />
<input type="hidden" name="sources" value="{{ default_sources }}" />
<input type="hidden" name="download_sources" value="{{ default_download_sources }}" />
<button type="submit">Full Pipeline</button>
</form>
<form action="/api/jobs" method="post" data-json-form data-success="reload">
<input type="hidden" name="job_type" value="collect_only" />
<input type="hidden" name="requested_by" value="ops-console" />
<input type="hidden" name="sources" value="{{ default_sources }}" />
<button type="submit">Collect</button>
</form>
<form action="/api/jobs" method="post" data-json-form data-success="reload">
<input type="hidden" name="job_type" value="sync_only" />
<input type="hidden" name="requested_by" value="ops-console" />
<input type="hidden" name="sources" value="{{ default_sources }}" />
<button type="submit">Sync</button>
</form>
<form action="/api/jobs" method="post" data-json-form data-success="reload">
<input type="hidden" name="job_type" value="download_only" />
<input type="hidden" name="requested_by" value="ops-console" />
<input type="hidden" name="download_sources" value="{{ default_download_sources }}" />
<button type="submit">Download</button>
</form>
<form action="/api/jobs" method="post" data-json-form data-success="reload">
<input type="hidden" name="job_type" value="upload_only" />
<input type="hidden" name="requested_by" value="ops-console" />
<input type="hidden" name="download_sources" value="{{ default_download_sources }}" />
<button type="submit">Upload</button>
</form>
</div>
</div>
<div class="card">
<h2>Create Job</h2>
<form action="/api/jobs" method="post" data-json-form data-success="reload">
<label>
Job Type
<select name="job_type">
{% for value, label in job_type_options %}
<option value="{{ value }}">{{ label }}</option>
{% endfor %}
</select>
</label>
<label>
Requested By
<input type="text" name="requested_by" value="ops-console" />
</label>
<label>
Collect Sources
<input type="text" name="sources" value="{{ default_sources }}" />
</label>
<label>
Download Sources
<input type="text" name="download_sources" value="{{ default_download_sources }}" />
</label>
<button type="submit">Create Job</button>
</form>
</div>
<div class="card">
<h2>Playlist Coverage</h2>
<table>
<thead>
<tr>
<th>Platform</th>
<th>Pool Kind</th>
<th>Pool Name</th>
<th>Playlists</th>
</tr>
</thead>
<tbody data-playlist-sources-body>
{% for row in playlist_sources %}
<tr>
<td>{{ row.platform }}</td>
<td>{{ row.pool_kind }}</td>
<td>{{ row.pool_name }}</td>
<td>{{ row.playlist_count }}</td>
</tr>
{% else %}
<tr><td colspan="4">No playlist sources collected yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div
class="card"
data-maintenance-panel="local-duplicates"
data-scan-api="{{ maintenance_local_duplicates_scan_api }}"
data-dedupe-api="{{ maintenance_local_duplicates_dedupe_api }}"
>
<h2>Maintenance</h2>
<div class="button-grid">
<button type="button" data-maintenance-action="scan">Scan Duplicate Local Copies</button>
<button type="button" class="secondary" data-maintenance-action="dedupe">Run Local Dedupe</button>
</div>
<p class="muted" data-maintenance-status>No local duplicate scan has been run yet.</p>
<div data-maintenance-result>
<p class="muted">Scan first to inspect duplicate local file copies before dedupe.</p>
</div>
</div>
</div>
<div class="card">
<div class="task-tree-panel-head">
<h2>Task Center</h2>
<span class="muted" data-task-center-transfer>Down {{ transfer_stats.download_speed_text }} | Up {{ transfer_stats.upload_speed_text }}</span>
</div>
<div class="task-tree-columns">
<section class="task-tree-panel">
<div class="task-tree-panel-head">
<h3>Doing</h3>
<span class="muted">Task -> Playlist -> Song</span>
</div>
<div class="task-tree" data-task-tree-root="doing">
{% for row in doing_task_rows %}
{{ render_task_tree_node(row) }}
{% else %}
<p class="muted" data-task-tree-empty>No active tasks.</p>
{% endfor %}
</div>
</section>
<section class="task-tree-panel">
<div class="task-tree-panel-head">
<h3>Recent Done</h3>
<span class="muted">Task -> Playlist</span>
</div>
<div class="task-tree" data-task-tree-root="done">
{% for row in done_task_rows %}
{{ render_task_tree_node(row) }}
{% else %}
<p class="muted" data-task-tree-empty>No recently finished tasks.</p>
{% endfor %}
</div>
</section>
</div>
</div>
<div class="grid">
<div class="card">
<h2>Active Workers</h2>
<table>
<thead>
<tr>
<th>Worker</th>
<th>Status</th>
<th>Stage</th>
<th>Current Item</th>
<th>Progress</th>
</tr>
</thead>
<tbody data-workers-body>
{% for worker in workers %}
<tr>
<td>{{ worker.worker_name }}</td>
<td>{{ worker.status }}</td>
<td>{{ worker.stage_type or "-" }}</td>
<td>{{ worker.display_text or "-" }}</td>
<td>{{ worker.last_progress_text or "-" }}</td>
</tr>
{% else %}
<tr><td colspan="5">No active workers.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
<h2>Running Items</h2>
<table>
<thead>
<tr>
<th>Job</th>
<th>Worker</th>
<th>Stage</th>
<th>Item</th>
<th>Started</th>
</tr>
</thead>
<tbody data-running-items-body>
{% for item in running_items %}
<tr>
<td><a href="/jobs/{{ item.job_run_id }}">{{ item.job_run_id }}</a></td>
<td>{{ item.worker_name or "-" }}</td>
<td>{{ item.stage_type }}</td>
<td>{{ item.display_name }}</td>
<td>{{ item.started_at or "-" }}</td>
</tr>
{% else %}
<tr><td colspan="5">No running items.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}