feat(videos): add manual Convert button per MOV file on Pi
- New POST /api/local-video/convert route triggers queue_convert(force=True) - queue_convert() now returns True/False/None and accepts force param to bypass the auto-convert toggle (manual always works) - Convert button shown for non-MP4, non-converting files in video list - Clicking spins the button and refreshes list after 1s to show badge Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -176,6 +176,17 @@ def local_video_delete():
|
|||||||
}
|
}
|
||||||
return jsonify(summary), status_codes.get(summary["status"], 409)
|
return jsonify(summary), status_codes.get(summary["status"], 409)
|
||||||
|
|
||||||
|
@app.route('/api/local-video/convert', methods=['POST'])
|
||||||
|
def local_video_convert():
|
||||||
|
body = request.get_json(silent=True) or {}
|
||||||
|
path = body.get("path", "")
|
||||||
|
if not path:
|
||||||
|
return jsonify({"status": "invalid_path", "message": "No path provided."}), 400
|
||||||
|
queued = app_video.queue_convert(path, force=True)
|
||||||
|
if queued is False:
|
||||||
|
return jsonify({"status": "busy", "message": "Already converting."}), 409
|
||||||
|
return jsonify({"status": "started"})
|
||||||
|
|
||||||
@app.route('/api/upload/status')
|
@app.route('/api/upload/status')
|
||||||
def upload_status():
|
def upload_status():
|
||||||
return jsonify(app_upload.get_status())
|
return jsonify(app_upload.get_status())
|
||||||
|
|||||||
@@ -25,20 +25,21 @@ def is_convert_enabled():
|
|||||||
return app_sql.get_setting(CONVERT_SETTING, "0") == "1"
|
return app_sql.get_setting(CONVERT_SETTING, "0") == "1"
|
||||||
|
|
||||||
|
|
||||||
def queue_convert(relative_path):
|
def queue_convert(relative_path, force=False):
|
||||||
"""Spawn a background greenlet to re-encode relative_path to web-ready MP4.
|
"""Spawn a background thread to re-encode relative_path to web-ready MP4.
|
||||||
No-op if conversion is disabled, already running, or file is already MP4."""
|
Returns True if queued, False if already converting, None if skipped (disabled/MP4).
|
||||||
if not is_convert_enabled():
|
Set force=True to bypass the is_convert_enabled() check (manual trigger)."""
|
||||||
return
|
if not force and not is_convert_enabled():
|
||||||
|
return None
|
||||||
if relative_path.lower().endswith(".mp4"):
|
if relative_path.lower().endswith(".mp4"):
|
||||||
return
|
return None
|
||||||
with _converting_lock:
|
with _converting_lock:
|
||||||
if relative_path in _converting:
|
if relative_path in _converting:
|
||||||
return
|
return False
|
||||||
_converting.add(relative_path)
|
_converting.add(relative_path)
|
||||||
gevent.sleep(0) # yield before spawning thread
|
|
||||||
t = threading.Thread(target=_convert_bg, args=(relative_path,), daemon=True)
|
t = threading.Thread(target=_convert_bg, args=(relative_path,), daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _convert_bg(relative_path):
|
def _convert_bg(relative_path):
|
||||||
|
|||||||
@@ -172,6 +172,16 @@
|
|||||||
playButton.innerHTML = '<i class="fas fa-play"></i><span class="btn-label"> Play</span>';
|
playButton.innerHTML = '<i class="fas fa-play"></i><span class="btn-label"> Play</span>';
|
||||||
actionsDiv.appendChild(playButton);
|
actionsDiv.appendChild(playButton);
|
||||||
|
|
||||||
|
// Convert button — only for non-MP4 files not already converting
|
||||||
|
if (!file.converting && !file.path.toLowerCase().endsWith(".mp4")) {
|
||||||
|
const convertButton = document.createElement("button");
|
||||||
|
convertButton.type = "button";
|
||||||
|
convertButton.className = "btn btn-sm btn-outline-secondary btn-icon-mobile";
|
||||||
|
convertButton.dataset.convertPath = file.path;
|
||||||
|
convertButton.innerHTML = '<i class="fas fa-exchange-alt"></i><span class="btn-label"> Convert</span>';
|
||||||
|
actionsDiv.appendChild(convertButton);
|
||||||
|
}
|
||||||
|
|
||||||
const deleteButton = document.createElement("button");
|
const deleteButton = document.createElement("button");
|
||||||
deleteButton.type = "button";
|
deleteButton.type = "button";
|
||||||
deleteButton.className = "btn btn-sm btn-outline-danger btn-icon-mobile";
|
deleteButton.className = "btn btn-sm btn-outline-danger btn-icon-mobile";
|
||||||
@@ -238,6 +248,22 @@
|
|||||||
deleteVideo(deleteButton.dataset.deleteLocalPath).finally(function () {
|
deleteVideo(deleteButton.dataset.deleteLocalPath).finally(function () {
|
||||||
deleteButton.disabled = false;
|
deleteButton.disabled = false;
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const convertButton = event.target.closest("[data-convert-path]");
|
||||||
|
if (convertButton) {
|
||||||
|
convertButton.disabled = true;
|
||||||
|
convertButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||||
|
fetch("/api/local-video/convert", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ path: convertButton.dataset.convertPath }),
|
||||||
|
}).then(function () {
|
||||||
|
setTimeout(refreshFiles, 1000);
|
||||||
|
}).catch(function (err) {
|
||||||
|
summary.textContent = `Convert failed: ${err.message}`;
|
||||||
|
convertButton.disabled = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user