autotako

Service to monitor moombox for completed livestream downloads to upload for distribution
git clone https://code.alwayswait.ing/autotako.git
Log | Files | Refs

commit ac67546779040c0b148ceee0f07c50a49dc43a19
parent e1ee312749718a9e81efcd3c0c14f89167d5ed18
Author: archiveanon <>
Date:   Tue, 21 Jan 2025 07:07:27 +0000

Implement task browsing

Diffstat:
Msrc/autotako/app.py | 21++++++++++++++++++++-
Msrc/autotako/job_render.py | 35+++++++++++++++++++++++++++++++++--
Asrc/autotako/static/style.css | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/autotako/templates/edit.html | 13+++++++++++++
Msrc/autotako/templates/index.html | 43+++++++++++++++++++++++++++++++++++++------
Asrc/autotako/templates/job/upload_error.html | 3+++
Asrc/autotako/templates/job/upload_success.html | 3+++
7 files changed, 176 insertions(+), 9 deletions(-)

diff --git a/src/autotako/app.py b/src/autotako/app.py @@ -5,6 +5,7 @@ import datetime import json import pathlib +import httpx import microdot # type: ignore import microdot.jinja # type: ignore import mistune @@ -42,7 +43,25 @@ def create_app(): @app.get("/") async def index(request): - return await microdot.jinja.Template("index.html").render_async(message="hello") + async with httpx.AsyncClient() as client: + result = await client.get(f"{config.moombox_url}/status") + return ( + await microdot.jinja.Template("index.html").render_async( + moombox_jobs=reversed(result.json()), + moombox_url=config.moombox_url, + ), + { + "Content-Type": "text/html; charset=utf-8", + }, + ) + + @app.get("/static/<path:path>") + async def send_static_file(request, path): + root = pathlib.Path(__file__).parent / "static" + computed_path = root / path + if not (root / path).resolve().is_relative_to(root.resolve()): + return "", 404 + return microdot.send_file(str(computed_path)) @app.post("/submit") async def process_job(request): diff --git a/src/autotako/job_render.py b/src/autotako/job_render.py @@ -3,6 +3,7 @@ import asyncio import datetime import pathlib +import traceback import gofile.api # type: ignore import httpx @@ -16,6 +17,7 @@ from .database import database_ctx app = microdot.Microdot() background_tasks = set() +render_tasks: dict[str, asyncio.Task] = {} upload_tasks: dict[pathlib.Path, asyncio.Task] = {} @@ -136,8 +138,7 @@ async def do_webdav_upload(webdav: WebDavConfig, filepath: pathlib.Path, target: await client.put(dest, content=fh.read()) -@app.get("/<jobid>") -async def show_job(request, jobid): +async def _process_job(jobid): job = await get_moombox_job_by_id(jobid) if not job: return "Couldn't find matching job", 404 @@ -246,4 +247,34 @@ async def show_job(request, jobid): ) background_tasks.add(task) task.add_done_callback(background_tasks.discard) + if not readme_finalized and jobid in render_tasks: + # allow rerun if not finalized + del render_tasks[jobid] return rendered_job + + +def get_process_job_task(jobid: str) -> asyncio.Task: + """ + Creates or retrieves a singleton job rendering task. + This is decoupled from the request callback to allow cancellations and multiple consumers. + """ + if jobid in render_tasks and render_tasks[jobid].done() and render_tasks[jobid].exception(): + del render_tasks[jobid] + if jobid not in render_tasks: + render_tasks[jobid] = asyncio.create_task(_process_job(jobid)) + return render_tasks[jobid] + + +@app.get("/<jobid>") +async def show_job(request, jobid: str): + return await get_process_job_task(jobid) + + +@app.post("/<jobid>") +async def upload_job(request, jobid: str): + try: + await get_process_job_task(jobid) + return await microdot.jinja.Template("job/upload_success.html").render_async() + except torf.TorfError: + traceback.print_exc() + return await microdot.jinja.Template("job/upload_error.html").render_async() diff --git a/src/autotako/static/style.css b/src/autotako/static/style.css @@ -0,0 +1,67 @@ +body { + width: auto; + font-family: var(--sl-font-sans); +} +a { + color: var(--sl-color-primary-700); + text-decoration: none; + &:hover { + color: var(--sl-color-primary-800); + } +} +.job-table { + display: grid; + grid-template-columns: 66% auto fit-content(0); + gap: 0 var(--sl-spacing-medium); +} +.job__item { + display: grid; + grid-template-columns: subgrid; + grid-column: 1/4; + padding: var(--sl-spacing-x-small); + + &:nth-child(odd) { + background-color: var( --sl-color-neutral-50); + } +} +.job__item--disabled { + .job__info { + filter: saturate(0%) brightness(50%); + } +} +.job__info { + display: flex; + flex-direction: column; +} +.job__title { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.job__author { + font-size: var(--sl-font-size-small); + color: var(--sl-color-neutral-600); +} +.job__description { + display: flex; + align-items: center; + input { + flex: 1; + } +} +.job__controls { + display: flex; + align-items: center; +} +.job__autoupload { + color: var(--sl-color-neutral-500); +} +.job__autoupload--active sl-icon-button::part(base) { + color: var(--sl-color-primary-500); +} +.job__manualupload--done sl-icon-button::part(base) { + color: var(--sl-color-success-600); +} +.job__manualupload--fail sl-icon-button::part(base) { + color: var(--sl-color-danger-600); +} diff --git a/src/autotako/templates/edit.html b/src/autotako/templates/edit.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html class="sl-theme-dark"> + <head> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>autotako edit</title> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.16.0/cdn/themes/dark.css" crossorigin="anonymous"/> + <script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.16.0/cdn/shoelace-autoloader.js" crossorigin="anonymous"></script> + <link rel="stylesheet" href="/static/style.css" type="text/css" /> + </head> + <body> + + </body> +</html> diff --git a/src/autotako/templates/index.html b/src/autotako/templates/index.html @@ -1,11 +1,43 @@ <!DOCTYPE html> -<html> +<html class="sl-theme-dark"> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>Hello!</title> - <style type="text/css">.body { width: auto; }</style> + <title>autotako control panel</title> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.16.0/cdn/themes/dark.css" crossorigin="anonymous"/> + <script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.16.0/cdn/shoelace-autoloader.js" crossorigin="anonymous"></script> + <script src="https://unpkg.com/htmx.org@2.0.0" integrity="sha384-wS5l5IKJBvK6sPTKa2WZ1js3d947pvWXbPJ1OmWfEuxLgeHcEbjUUA5i9V5ZkpCw" crossorigin="anonymous"></script> + <link rel="stylesheet" href="static/style.css" type="text/css" /> </head> <body> - {{ message }} + <h2>autotako control panel = w=)b</h2> + <div class="job-table"> +{% for job in moombox_jobs %} +{% set no_outputs = (not job.output_paths and job.status == "Finished") %} + <div class="job__item {{- ' job__item--disabled' if no_outputs }}"> + <div class="job__info"> + <div class="job__title"> + <a href="{{ moombox_url }}job/{{ job.id }}">{{ job.title }}</a> + </div> + <div class="job__author"> + {{ job.author }} + </div> + </div> + <div class="job__description"> + Unarchived Karaoke + <sl-icon-button name="pencil-square"></sl-icon-button> + </div> + <div class="job__controls"> + <div class="job__autoupload"> + <sl-tooltip content="Not scheduled for auto-upload"> + <sl-icon name="stopwatch"></sl-icon> + </sl-tooltip> + </div> + <div class="job__manualupload" hx-target="this"> + <sl-icon-button hx-post="render/{{ job.id }}" hx-swap="outerHTML" name="cloud-upload" {{- ' disabled' if no_outputs }}></sl-icon-button> + </div> + </div> + </div> +{% endfor %} + </div> </body> -</html> -\ No newline at end of file +</html> diff --git a/src/autotako/templates/job/upload_error.html b/src/autotako/templates/job/upload_error.html @@ -0,0 +1,3 @@ +<div class="job__manualupload job__manualupload--fail"> + <sl-icon-button name="x-lg"></sl-icon-button> +</div> diff --git a/src/autotako/templates/job/upload_success.html b/src/autotako/templates/job/upload_success.html @@ -0,0 +1,3 @@ +<div class="job__manualupload job__manualupload--done"> + <sl-icon-button name="check2"></sl-icon-button> +</div>