feat: Add retry functionality for jobs and update UI to support job retries

This commit is contained in:
JB
2025-10-09 11:46:38 -07:00
parent 9828730061
commit 205c435e72
4 changed files with 85 additions and 0 deletions
+11
View File
@@ -2225,6 +2225,7 @@ def job_detail(job_id: str) -> str:
"job_detail.html", "job_detail.html",
job=job, job=job,
options=_template_options(), options=_template_options(),
JobStatus=JobStatus,
) )
@@ -2260,6 +2261,16 @@ def delete_job(job_id: str) -> ResponseReturnValue:
return redirect(url_for("web.index")) return redirect(url_for("web.index"))
@web_bp.post("/jobs/<job_id>/retry")
def retry_job(job_id: str) -> ResponseReturnValue:
new_job = _service().retry(job_id)
if request.headers.get("HX-Request"):
return _render_jobs_panel()
if new_job:
return redirect(url_for("web.job_detail", job_id=new_job.id))
return redirect(url_for("web.job_detail", job_id=job_id))
@web_bp.post("/jobs/clear-finished") @web_bp.post("/jobs/clear-finished")
def clear_finished_jobs() -> ResponseReturnValue: def clear_finished_jobs() -> ResponseReturnValue:
_service().clear_finished() _service().clear_finished()
+65
View File
@@ -413,6 +413,71 @@ class ConversionService:
self._persist_state() self._persist_state()
return True return True
def retry(self, job_id: str) -> Optional[Job]:
with self._lock:
job = self._jobs.get(job_id)
if job is None:
return None
if job.status not in {JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED}:
job.add_log(
"Retry requested while job still active; ignoring.",
level="warning",
)
return None
stored_path = job.stored_path
if not isinstance(stored_path, Path):
stored_path = Path(str(stored_path))
if not stored_path.exists():
job.add_log(
f"Retry requested but source file is missing: {stored_path}",
level="error",
)
return None
new_job = self.enqueue(
original_filename=job.original_filename,
stored_path=stored_path,
language=job.language,
voice=job.voice,
speed=job.speed,
use_gpu=job.use_gpu,
subtitle_mode=job.subtitle_mode,
output_format=job.output_format,
save_mode=job.save_mode,
output_folder=job.output_folder,
replace_single_newlines=job.replace_single_newlines,
subtitle_format=job.subtitle_format,
total_characters=job.total_characters,
chapters=job.chapters,
save_chapters_separately=job.save_chapters_separately,
merge_chapters_at_end=job.merge_chapters_at_end,
separate_chapters_format=job.separate_chapters_format,
silence_between_chapters=job.silence_between_chapters,
save_as_project=job.save_as_project,
voice_profile=job.voice_profile,
max_subtitle_words=job.max_subtitle_words,
metadata_tags=job.metadata_tags,
cover_image_path=job.cover_image_path,
cover_image_mime=job.cover_image_mime,
chapter_intro_delay=job.chapter_intro_delay,
chunk_level=job.chunk_level,
chunks=job.chunks,
speakers=job.speakers,
speaker_mode=job.speaker_mode,
generate_epub3=job.generate_epub3,
speaker_analysis=job.speaker_analysis,
speaker_analysis_threshold=job.speaker_analysis_threshold,
analysis_requested=job.analysis_requested,
)
new_job.speaker_voice_languages = list(job.speaker_voice_languages)
new_job.applied_speaker_config = job.applied_speaker_config
new_job.add_log(f"Retry created from job {job.id}", level="info")
job.add_log(f"Retry scheduled as job {new_job.id}", level="info")
return new_job
def delete(self, job_id: str) -> bool: def delete(self, job_id: str) -> bool:
with self._lock: with self._lock:
job = self._jobs.get(job_id) job = self._jobs.get(job_id)
+6
View File
@@ -42,9 +42,15 @@
{% if job.result.audio_path %} {% if job.result.audio_path %}
<p><a class="button" href="{{ url_for('web.download_job', job_id=job.id) }}">Download audio</a></p> <p><a class="button" href="{{ url_for('web.download_job', job_id=job.id) }}">Download audio</a></p>
{% endif %} {% endif %}
{% if job.status in [JobStatus.PENDING, JobStatus.RUNNING, JobStatus.PAUSED] %}
<form action="{{ url_for('web.cancel_job', job_id=job.id) }}" method="post"> <form action="{{ url_for('web.cancel_job', job_id=job.id) }}" method="post">
<button type="submit" class="button button--ghost">Cancel job</button> <button type="submit" class="button button--ghost">Cancel job</button>
</form> </form>
{% elif job.status in [JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED] %}
<form action="{{ url_for('web.retry_job', job_id=job.id) }}" method="post">
<button type="submit" class="button">Retry job</button>
</form>
{% endif %}
</article> </article>
</div> </div>
</section> </section>
+3
View File
@@ -69,6 +69,9 @@
</div> </div>
<div class="job-card__footer"> <div class="job-card__footer">
<a class="button button--ghost" href="{{ url_for('web.job_detail', job_id=job.id) }}">Inspect</a> <a class="button button--ghost" href="{{ url_for('web.job_detail', job_id=job.id) }}">Inspect</a>
{% if job.status in [JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED] %}
<button type="button" class="button button--ghost" hx-post="{{ url_for('web.retry_job', job_id=job.id) }}" hx-target="#jobs-panel" hx-swap="innerHTML">Retry</button>
{% endif %}
{% if job.status == JobStatus.COMPLETED and job.result.audio_path %} {% if job.status == JobStatus.COMPLETED and job.result.audio_path %}
<a class="button button--ghost" href="{{ url_for('web.download_job', job_id=job.id) }}">Download</a> <a class="button button--ghost" href="{{ url_for('web.download_job', job_id=job.id) }}">Download</a>
{% endif %} {% endif %}