feat: add isolated utility export modes
This commit is contained in:
@@ -18,7 +18,7 @@ HTML_FORM = """
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>YouTube Playlist Submit</title>
|
||||
<title>YouTube Media Submit</title>
|
||||
|
||||
<!-- Make it behave on mobile -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
@@ -96,6 +96,33 @@ HTML_FORM = """
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.download-options {
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.download-options label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: #6b7280;
|
||||
@@ -139,6 +166,26 @@ HTML_FORM = """
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.export-status {
|
||||
margin-top: 1rem;
|
||||
padding-top: 0.9rem;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.25);
|
||||
}
|
||||
|
||||
.export-job {
|
||||
margin-top: 0.65rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
border-radius: 0.55rem;
|
||||
background: rgba(30, 41, 59, 0.7);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.export-path,
|
||||
.export-error {
|
||||
overflow-wrap: anywhere;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page-wrapper {
|
||||
padding: 1rem;
|
||||
@@ -167,9 +214,9 @@ HTML_FORM = """
|
||||
<body>
|
||||
<div class="page-wrapper">
|
||||
<div class="card">
|
||||
<h2>Submit YouTube Playlist</h2>
|
||||
<h2>Submit YouTube Media</h2>
|
||||
<p class="subtitle">
|
||||
Paste a playlist link below. It’ll be queued for download and assigned to the next free slot.
|
||||
Paste a YouTube video or playlist link and choose a download type.
|
||||
</p>
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
@@ -212,12 +259,40 @@ HTML_FORM = """
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit">Submit Playlist</button>
|
||||
<fieldset class="form-group">
|
||||
<legend>Download as:</legend>
|
||||
<div class="download-options">
|
||||
<label><input type="radio" name="download_mode" value="player_slot" checked> Player Slot</label>
|
||||
<label><input type="radio" name="download_mode" value="mp4"> MP4 Export</label>
|
||||
<label><input type="radio" name="download_mode" value="m4a"> M4A Audio</label>
|
||||
<label><input type="radio" name="download_mode" value="mp3"> MP3 Audio</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<p class="helper">
|
||||
Playlists are kept for roughly 48 hours after they’re downloaded.
|
||||
Utility exports are kept until an operator removes them.
|
||||
</p>
|
||||
|
||||
{% if export_jobs %}
|
||||
<section class="export-status">
|
||||
<strong>Recent utility exports</strong>
|
||||
{% for job in export_jobs %}
|
||||
<div class="export-job">
|
||||
<div>{{ download_mode_labels[job.mode] }} — {{ export_status_labels[job.status] }} (job {{ job.job_id[:8] }})</div>
|
||||
{% for path in job.output_paths %}
|
||||
<div class="export-path">{{ path }}</div>
|
||||
{% endfor %}
|
||||
{% if job.error %}
|
||||
<div class="export-error">{{ job.error }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -238,6 +313,19 @@ MAX_URL_LENGTH = 2048
|
||||
MAX_SUBMITTER_LENGTH = 200
|
||||
MAX_NOTE_LENGTH = 1000
|
||||
MAX_REQUEST_CONTENT_LENGTH = 32 * 1024
|
||||
DOWNLOAD_MODE_LABELS = {
|
||||
"player_slot": "Player Slot",
|
||||
"mp4": "MP4 Export",
|
||||
"m4a": "M4A Audio",
|
||||
"mp3": "MP3 Audio",
|
||||
}
|
||||
EXPORT_STATUS_LABELS = {
|
||||
"queued": "queued",
|
||||
"running": "downloading",
|
||||
"completed": "completed",
|
||||
"failed": "failed",
|
||||
"interrupted": "interrupted",
|
||||
}
|
||||
|
||||
|
||||
def _valid_media_url(value: str) -> bool:
|
||||
@@ -258,6 +346,20 @@ def _valid_media_url(value: str) -> bool:
|
||||
|
||||
def _submission_message(result: dict[str, object]) -> str:
|
||||
status = result.get("status")
|
||||
if result.get("kind") == "export":
|
||||
mode = str(result.get("mode") or "")
|
||||
label = DOWNLOAD_MODE_LABELS.get(mode, "Utility export")
|
||||
if status == "queue_full":
|
||||
return "The utility export queue is full; please try again later."
|
||||
if status == "shutting_down":
|
||||
return "The Controller is shutting down; please try again shortly."
|
||||
if status == "worker_error":
|
||||
return f"The {label} job was reserved, but its worker could not start."
|
||||
if status != "queued":
|
||||
return f"The Controller could not queue that {label} job; please try again."
|
||||
job_id = str(result.get("job_id") or "")[:8]
|
||||
identity = f" (job {job_id})" if job_id else ""
|
||||
return f"{label} queued{identity}. Refresh this page to see its result."
|
||||
if status == "queue_full":
|
||||
seconds = max(0, int(result.get("oldest_expires_in_seconds") or 0))
|
||||
hours = max(1, (seconds + 3599) // 3600)
|
||||
@@ -308,7 +410,19 @@ def create_app(
|
||||
|
||||
@web_app.get("/")
|
||||
def index():
|
||||
return render_template_string(HTML_FORM)
|
||||
try:
|
||||
export_jobs = service.list_export_jobs(
|
||||
limit=min(100, config.downloads.max_queued_exports + 10)
|
||||
)
|
||||
except Exception as exc:
|
||||
LOG.warning("could not load utility export status: %s", exc)
|
||||
export_jobs = []
|
||||
return render_template_string(
|
||||
HTML_FORM,
|
||||
export_jobs=export_jobs,
|
||||
download_mode_labels=DOWNLOAD_MODE_LABELS,
|
||||
export_status_labels=EXPORT_STATUS_LABELS,
|
||||
)
|
||||
|
||||
@web_app.errorhandler(413)
|
||||
def _submission_too_large(_error):
|
||||
@@ -320,6 +434,8 @@ def create_app(
|
||||
url = request.form.get("url", "").strip()
|
||||
submitter = request.form.get("submitter", "").strip() or None
|
||||
note = request.form.get("note", "").strip() or None
|
||||
raw_mode = request.form.get("download_mode", "player_slot")
|
||||
download_mode = raw_mode.strip() if isinstance(raw_mode, str) else ""
|
||||
|
||||
if not _valid_media_url(url):
|
||||
flash(
|
||||
@@ -333,9 +449,20 @@ def create_app(
|
||||
if note is not None and len(note) > MAX_NOTE_LENGTH:
|
||||
flash(f"The note must be {MAX_NOTE_LENGTH} characters or fewer.")
|
||||
return redirect(url_for("index"))
|
||||
if download_mode not in DOWNLOAD_MODE_LABELS:
|
||||
flash("Invalid download type. Choose one of the available options.")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
try:
|
||||
result = service.enqueue_download(url, submitter, note)
|
||||
if download_mode == "player_slot":
|
||||
result = service.enqueue_download(url, submitter, note)
|
||||
else:
|
||||
result = service.enqueue_export(
|
||||
url,
|
||||
download_mode,
|
||||
submitter,
|
||||
note,
|
||||
)
|
||||
except ControllerUnavailableError as exc:
|
||||
LOG.warning("playlist submission was rejected: %s", exc)
|
||||
flash("The Controller is unavailable; please try again shortly.")
|
||||
|
||||
Reference in New Issue
Block a user