diff --git a/src/main.py b/src/main.py index 53ea7e5..f516203 100644 --- a/src/main.py +++ b/src/main.py @@ -284,6 +284,66 @@ async def history_clear(): ) +@app.post("/history/retry", response_class=JSONResponse) +async def history_retry(pid: int): + """ + Re-run a past job with the same parameters as a NEW job. YouTube re-downloads + from the stored URL; an upload reuses its source file if still on disk. + """ + if not ENABLE_HISTORY: + raise HTTPException(status_code=404, detail="History is disabled.") + old = DB.get_transcription(pid) + if not old: + return JSONResponse(content={"message": "Job not found."}, status_code=404) + if _is_in_flight(old): + return JSONResponse( + content={"message": "Job is still running."}, status_code=409 + ) + if _count_active_jobs() >= MAX_CONCURRENT_JOBS: + return JSONResponse( + content={"message": "Server busy — too many jobs running. Try again shortly."}, + status_code=429, + ) + + youtube_url = old["youtube_url"] or "" + source_file = None + if not youtube_url: + # Reuse the previous upload's source if it hasn't been cleaned up. + candidates = [ + f for f in OUTPUT_DIR.glob(f"{pid}_*") + if f.is_file() and not f.name.endswith("_logs.txt") + ] + mp3s = [f for f in candidates if f.suffix == ".mp3"] + chosen = mp3s or candidates + if not chosen: + return JSONResponse( + content={"message": "The uploaded file is no longer available — please re-upload it to run again."}, + status_code=410, + ) + source_file = str(chosen[0]) + + new_id = DB.insert_transcription( + youtube_url, old["media_path"], old["language"], old["model"], + old["translation"], old["language_translation"], old["file_export"], + job_status.PROCESSING, str(time.time()), + ) + DB.update_transcription_status(job_status.PROCESSING, "", 10, new_id) + started = await run_in_threadpool( + handle_transcription, new_id, youtube_url or None, None, + old["language"], old["model"], old["translation"], + old["language_translation"], old["file_export"], source_file, + ) + if not started: + row = DB.get_transcription(new_id) + msg = ( + row["status"].removeprefix(f"{job_status.ERROR_PREFIX} ") + if row and row["status"].startswith(job_status.ERROR_PREFIX) + else "Retry failed to start." + ) + return JSONResponse(content={"message": msg, "pid": new_id}, status_code=400) + return JSONResponse(content={"status": "success", "pid": new_id}) + + @app.post("/submit_contact") async def submit_contact( name: str = Form(None), diff --git a/src/utils.py b/src/utils.py index 449ff64..218e808 100644 --- a/src/utils.py +++ b/src/utils.py @@ -107,6 +107,7 @@ def handle_transcription( translation: str, language_translation: str, file_export: str, + source_file: str = None, ) -> bool: """ Handle the transcription process: download YouTube or handle uploaded media, @@ -196,6 +197,15 @@ def handle_transcription( raise output_file = convert_to_mp3(media_file_path) + elif source_file: + # Retry of a previous upload: reuse the source still on disk, copied + # under this job's id so the two jobs own independent files. + src = Path(source_file) + dest = OUTPUT_DIR / f"{job_id}_{src.name}" + if src.resolve() != dest.resolve(): + shutil.copy(src, dest) + output_file = convert_to_mp3(dest) + logger.info(f"Transcription started for: {output_file}") # The download/convert above can take minutes; the user may have diff --git a/static/history.js b/static/history.js index a4a0c1c..2b7bbc5 100644 --- a/static/history.js +++ b/static/history.js @@ -107,6 +107,17 @@ }).catch(() => alert('Could not delete the job.')); }; + window.retryJob = function (id) { + if (!confirm(`Re-run job #${id} with the same settings?`)) return; + fetch(`/history/retry?pid=${id}`, { method: 'POST' }).then(r => { + if (r.ok) { + location.reload(); // the new job shows up (Processing) + } else { + r.json().then(d => alert(d.message || 'Could not retry the job.')).catch(() => alert('Could not retry the job.')); + } + }).catch(() => alert('Could not retry the job.')); + }; + window.clearHistory = function () { if (!confirm('Delete ALL finished jobs and their files? This cannot be undone. (Running jobs are kept.)')) return; fetch('/history/clear', { method: 'POST' }).then(() => location.reload()).catch(() => alert('Could not clear history.')); diff --git a/templates/history.html b/templates/history.html index f101d81..3c819f7 100644 --- a/templates/history.html +++ b/templates/history.html @@ -90,7 +90,10 @@ .act-dl:hover { background: rgba(35, 134, 54, .24); } .act-del { color: #e5484d; background: rgba(229, 72, 77, .12); border-color: rgba(229, 72, 77, .4); } .act-del:hover { background: rgba(229, 72, 77, .22); } + .act-retry { color: #2a6fdb; background: rgba(56, 132, 222, .12); border-color: rgba(56, 132, 222, .4); } + .act-retry:hover { background: rgba(56, 132, 222, .22); } body.dark-theme .act-dl { color: #4ac26b; } + body.dark-theme .act-retry { color: #6aa8ff; } .history-footer { display: flex; align-items: center; justify-content: space-between; margin-top: 1.1rem; gap: 1rem; flex-wrap: wrap; } .pager { display: flex; gap: 1rem; align-items: center; } @@ -194,6 +197,9 @@