unit3dprep 1.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unit3dprep/__init__.py +0 -0
- unit3dprep/cli.py +256 -0
- unit3dprep/core.py +556 -0
- unit3dprep/i18n.py +151 -0
- unit3dprep/media.py +278 -0
- unit3dprep/upload.py +146 -0
- unit3dprep/web/__init__.py +0 -0
- unit3dprep/web/_env.py +40 -0
- unit3dprep/web/api/__init__.py +1 -0
- unit3dprep/web/api/auth.py +36 -0
- unit3dprep/web/api/fs.py +64 -0
- unit3dprep/web/api/library.py +503 -0
- unit3dprep/web/api/logs.py +42 -0
- unit3dprep/web/api/queue.py +54 -0
- unit3dprep/web/api/quickupload.py +153 -0
- unit3dprep/web/api/search.py +40 -0
- unit3dprep/web/api/settings.py +77 -0
- unit3dprep/web/api/tmdb.py +77 -0
- unit3dprep/web/api/trackers.py +30 -0
- unit3dprep/web/api/uploaded.py +26 -0
- unit3dprep/web/api/version.py +754 -0
- unit3dprep/web/api/webup.py +59 -0
- unit3dprep/web/api/wizard.py +512 -0
- unit3dprep/web/app.py +201 -0
- unit3dprep/web/auth.py +41 -0
- unit3dprep/web/clients.py +167 -0
- unit3dprep/web/config.py +932 -0
- unit3dprep/web/db.py +184 -0
- unit3dprep/web/dist/assets/JetBrainsMono-Italic-VariableFont_wght-CZO9PUqx.ttf +0 -0
- unit3dprep/web/dist/assets/JetBrainsMono-VariableFont_wght-BrlcHZ7m.ttf +0 -0
- unit3dprep/web/dist/assets/SpaceGrotesk-VariableFont_wght-DIScfSlK.ttf +0 -0
- unit3dprep/web/dist/assets/index-BizNr_oP.js +255 -0
- unit3dprep/web/dist/assets/index-DChRHChM.css +1 -0
- unit3dprep/web/dist/index.html +14 -0
- unit3dprep/web/duplicate_check.py +92 -0
- unit3dprep/web/lang_cache.py +118 -0
- unit3dprep/web/logbuf.py +164 -0
- unit3dprep/web/tmdb_cache.py +116 -0
- unit3dprep/web/trackers.py +177 -0
- unit3dprep/web/webup_client.py +190 -0
- unit3dprep/web/webup_job_fix.py +231 -0
- unit3dprep/web/webup_logclass.py +107 -0
- unit3dprep/web/webup_orchestrator.py +796 -0
- unit3dprep/web/webup_ws.py +141 -0
- unit3dprep-1.0.4.dist-info/METADATA +819 -0
- unit3dprep-1.0.4.dist-info/RECORD +50 -0
- unit3dprep-1.0.4.dist-info/WHEEL +5 -0
- unit3dprep-1.0.4.dist-info/entry_points.txt +3 -0
- unit3dprep-1.0.4.dist-info/licenses/LICENSE +674 -0
- unit3dprep-1.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Quick upload (UploadModal flow) — wraps Unit3DWebUp without the full wizard.
|
|
2
|
+
|
|
3
|
+
Takes a path + mode (u|f|scan), drives the webup HTTP API, streams logs via SSE.
|
|
4
|
+
No audio check, no hardlink: for power users who already staged files in
|
|
5
|
+
their seeding path. DB record is created on start; exit code updated on done.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import secrets
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, AsyncGenerator
|
|
15
|
+
|
|
16
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
17
|
+
from fastapi.responses import JSONResponse
|
|
18
|
+
from pydantic import BaseModel
|
|
19
|
+
from sse_starlette.sse import EventSourceResponse
|
|
20
|
+
|
|
21
|
+
from ...i18n import get_request_lang, t
|
|
22
|
+
from ..db import update_exit_code
|
|
23
|
+
from ..logbuf import emit as log_emit
|
|
24
|
+
from ..webup_orchestrator import stream_webup, stream_webup_batch
|
|
25
|
+
|
|
26
|
+
router = APIRouter(prefix="/api", tags=["quickupload"])
|
|
27
|
+
|
|
28
|
+
_jobs: dict[str, dict[str, Any]] = {}
|
|
29
|
+
_created: dict[str, float] = {}
|
|
30
|
+
_TTL = 3600
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _cleanup():
|
|
34
|
+
now = time.time()
|
|
35
|
+
for j in [j for j, ct in _created.items() if now - ct > _TTL]:
|
|
36
|
+
_jobs.pop(j, None)
|
|
37
|
+
_created.pop(j, None)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class QuickBody(BaseModel):
|
|
41
|
+
path: str
|
|
42
|
+
mode: str = "u" # u|f|scan
|
|
43
|
+
tracker: str = "ITT"
|
|
44
|
+
tmdb_id: str = ""
|
|
45
|
+
skip_tmdb: bool = False
|
|
46
|
+
skip_youtube: bool = False
|
|
47
|
+
anon: bool = False
|
|
48
|
+
webp: bool = False
|
|
49
|
+
screenshots: bool = True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@router.post("/upload/quick")
|
|
53
|
+
async def create(request: Request, body: QuickBody):
|
|
54
|
+
lang = get_request_lang(request)
|
|
55
|
+
p = Path(body.path).resolve()
|
|
56
|
+
if not p.exists():
|
|
57
|
+
raise HTTPException(404, t("err.path_not_found", lang))
|
|
58
|
+
if body.mode not in {"u", "f", "scan"}:
|
|
59
|
+
raise HTTPException(400, t("err.invalid_mode", lang))
|
|
60
|
+
_cleanup()
|
|
61
|
+
job_id = secrets.token_urlsafe(16)
|
|
62
|
+
_jobs[job_id] = {
|
|
63
|
+
"path": str(p),
|
|
64
|
+
"mode": body.mode,
|
|
65
|
+
"tmdb_id": body.tmdb_id,
|
|
66
|
+
}
|
|
67
|
+
_created[job_id] = time.time()
|
|
68
|
+
return JSONResponse({"job": job_id})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@router.get("/upload/{job}/stream")
|
|
72
|
+
async def stream(request: Request, job: str):
|
|
73
|
+
state = _jobs.get(job)
|
|
74
|
+
if state is None:
|
|
75
|
+
raise HTTPException(404, t("err.job_not_found", get_request_lang(request)))
|
|
76
|
+
path: str = state["path"]
|
|
77
|
+
mode: str = state["mode"]
|
|
78
|
+
tmdb_id: str = state.get("tmdb_id", "")
|
|
79
|
+
app = request.app
|
|
80
|
+
|
|
81
|
+
async def gen() -> AsyncGenerator[dict, None]:
|
|
82
|
+
if mode == "scan":
|
|
83
|
+
async for ev in stream_webup_batch(
|
|
84
|
+
client=app.state.webup,
|
|
85
|
+
ws=app.state.webup_ws,
|
|
86
|
+
scan_lock=app.state.webup_scan_lock,
|
|
87
|
+
folder=path,
|
|
88
|
+
):
|
|
89
|
+
et = ev["type"]
|
|
90
|
+
if et == "log":
|
|
91
|
+
ev_kind = ev.get("kind", "info")
|
|
92
|
+
event_slug = ev.get("event")
|
|
93
|
+
log_emit(ev_kind, ev["data"], "webup", source="webup", event=event_slug)
|
|
94
|
+
yield {"event": "log", "data": ev["data"]}
|
|
95
|
+
elif et == "job_done":
|
|
96
|
+
job_path = ev.get("path") or ""
|
|
97
|
+
if job_path:
|
|
98
|
+
await update_exit_code(job_path, ev.get("exit_code", -1))
|
|
99
|
+
yield {"event": "job_done", "data": json.dumps(ev)}
|
|
100
|
+
elif et == "error":
|
|
101
|
+
log_emit("error", ev["data"], "webup", source="webup")
|
|
102
|
+
yield {"event": "error", "data": ev["data"]}
|
|
103
|
+
elif et == "done":
|
|
104
|
+
code = ev.get("exit_code", -1)
|
|
105
|
+
state["exit_code"] = code
|
|
106
|
+
log_emit(
|
|
107
|
+
"ok" if code == 0 else "error",
|
|
108
|
+
f"webup batch exit {code} (ok={ev.get('ok',0)} fail={ev.get('fail',0)})",
|
|
109
|
+
"quickupload",
|
|
110
|
+
)
|
|
111
|
+
yield {"event": "done", "data": json.dumps({
|
|
112
|
+
"exit_code": code,
|
|
113
|
+
"ok": ev.get("ok", 0),
|
|
114
|
+
"fail": ev.get("fail", 0),
|
|
115
|
+
})}
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
kind = "series" if mode == "f" else "movie"
|
|
119
|
+
async for ev in stream_webup(
|
|
120
|
+
client=app.state.webup,
|
|
121
|
+
ws=app.state.webup_ws,
|
|
122
|
+
scan_lock=app.state.webup_scan_lock,
|
|
123
|
+
seeding_path=path,
|
|
124
|
+
kind=kind,
|
|
125
|
+
tmdb_id=tmdb_id,
|
|
126
|
+
):
|
|
127
|
+
et = ev["type"]
|
|
128
|
+
if et == "log":
|
|
129
|
+
ev_kind = ev.get("kind", "info")
|
|
130
|
+
event_slug = ev.get("event")
|
|
131
|
+
log_emit(ev_kind, ev["data"], "webup", source="webup", event=event_slug)
|
|
132
|
+
yield {"event": "log", "data": ev["data"]}
|
|
133
|
+
elif et == "progress":
|
|
134
|
+
yield {"event": "progress", "data": json.dumps({
|
|
135
|
+
"phase": ev.get("phase"),
|
|
136
|
+
"label": ev.get("label"),
|
|
137
|
+
"pct": ev.get("pct", 0),
|
|
138
|
+
"sub_pct": ev.get("sub_pct", 0),
|
|
139
|
+
})}
|
|
140
|
+
elif et == "error":
|
|
141
|
+
log_emit("error", ev["data"], "webup", source="webup")
|
|
142
|
+
yield {"event": "error", "data": ev["data"]}
|
|
143
|
+
elif et == "done":
|
|
144
|
+
code = ev.get("exit_code", -1)
|
|
145
|
+
state["exit_code"] = code
|
|
146
|
+
await update_exit_code(state["path"], code)
|
|
147
|
+
log_emit(
|
|
148
|
+
"ok" if code == 0 else "error",
|
|
149
|
+
f"webup exit {code}", "quickupload",
|
|
150
|
+
)
|
|
151
|
+
yield {"event": "done", "data": json.dumps({"exit_code": code})}
|
|
152
|
+
|
|
153
|
+
return EventSourceResponse(gen())
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Tracker search + reseed triggers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
5
|
+
from fastapi.responses import JSONResponse
|
|
6
|
+
|
|
7
|
+
from ...i18n import get_request_lang, t
|
|
8
|
+
from .. import config, trackers
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/api", tags=["search"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _build():
|
|
14
|
+
return trackers.build_trackers(config.load())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.get("/search")
|
|
18
|
+
async def search(request: Request, q: str, tracker: str = "ITT"):
|
|
19
|
+
if not q.strip():
|
|
20
|
+
return JSONResponse({"results": [], "tracker": tracker})
|
|
21
|
+
trk_map = _build()
|
|
22
|
+
trk = trk_map.get(tracker.upper())
|
|
23
|
+
if trk is None:
|
|
24
|
+
raise HTTPException(404, t("err.tracker_unknown", get_request_lang(request), tracker=tracker))
|
|
25
|
+
try:
|
|
26
|
+
results = await trk.search(q)
|
|
27
|
+
except NotImplementedError as e:
|
|
28
|
+
raise HTTPException(501, str(e))
|
|
29
|
+
except Exception as e:
|
|
30
|
+
raise HTTPException(502, str(e))
|
|
31
|
+
return JSONResponse({
|
|
32
|
+
"tracker": tracker.upper(),
|
|
33
|
+
"results": [r.to_dict() for r in results],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.post("/reseed/{tracker}/{torrent_id}")
|
|
38
|
+
async def reseed(request: Request, tracker: str, torrent_id: int):
|
|
39
|
+
# Placeholder: trigger a unit3dup reseed run for a known torrent id.
|
|
40
|
+
raise HTTPException(501, t("err.reseed_not_implemented", get_request_lang(request)))
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Shared .env read/write + runtime U3DP_* env view + filesystem checks."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
|
|
10
|
+
from .. import config
|
|
11
|
+
from ...media import media_root, seedings_root
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/api", tags=["settings"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.get("/settings")
|
|
17
|
+
async def get_settings():
|
|
18
|
+
cfg = config.load()
|
|
19
|
+
env_path = config.config_path()
|
|
20
|
+
return JSONResponse({
|
|
21
|
+
"config": config.mask_secrets(cfg),
|
|
22
|
+
"env": config.env_runtime(),
|
|
23
|
+
"config_path": str(env_path),
|
|
24
|
+
"env_path": str(env_path),
|
|
25
|
+
"webup_envpath_dir": str(config.webup_envpath_dir()),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@router.put("/settings")
|
|
30
|
+
async def put_settings(incoming: dict):
|
|
31
|
+
existing = config.load()
|
|
32
|
+
merged = {**existing, **config.merge_secrets(existing, incoming)}
|
|
33
|
+
config.save(merged)
|
|
34
|
+
return JSONResponse({"ok": True, "config": config.mask_secrets(merged)})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _closest_existing(p: Path) -> Path | None:
|
|
38
|
+
cur: Path | None = p
|
|
39
|
+
while cur is not None:
|
|
40
|
+
if cur.exists():
|
|
41
|
+
return cur
|
|
42
|
+
parent = cur.parent
|
|
43
|
+
if parent == cur:
|
|
44
|
+
return None
|
|
45
|
+
cur = parent
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.get("/settings/fs-check")
|
|
50
|
+
async def settings_fs_check():
|
|
51
|
+
"""Return whether MEDIA_ROOT and SEEDINGS_DIR are on the same filesystem.
|
|
52
|
+
|
|
53
|
+
Walks up missing paths so a freshly-configured seedings dir still reports
|
|
54
|
+
the parent filesystem it would live on.
|
|
55
|
+
"""
|
|
56
|
+
media = media_root()
|
|
57
|
+
seed = seedings_root()
|
|
58
|
+
media_real = _closest_existing(media)
|
|
59
|
+
seed_real = _closest_existing(seed)
|
|
60
|
+
try:
|
|
61
|
+
media_dev = os.stat(media_real).st_dev if media_real else None
|
|
62
|
+
except OSError:
|
|
63
|
+
media_dev = None
|
|
64
|
+
try:
|
|
65
|
+
seed_dev = os.stat(seed_real).st_dev if seed_real else None
|
|
66
|
+
except OSError:
|
|
67
|
+
seed_dev = None
|
|
68
|
+
same = (media_dev is not None) and (media_dev == seed_dev)
|
|
69
|
+
return JSONResponse({
|
|
70
|
+
"media_root": str(media),
|
|
71
|
+
"seedings_dir": str(seed),
|
|
72
|
+
"media_exists": media.exists(),
|
|
73
|
+
"seedings_exists": seed.exists(),
|
|
74
|
+
"media_dev": media_dev,
|
|
75
|
+
"seedings_dev": seed_dev,
|
|
76
|
+
"same_fs": same,
|
|
77
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""TMDB search + manual cache set/clear."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from ...core import tmdb_fetch_bilingual, tmdb_poster_url, tmdb_search, tmdb_year
|
|
12
|
+
from ...i18n import get_request_lang, t
|
|
13
|
+
from ..tmdb_cache import delete_cache, set_cache
|
|
14
|
+
|
|
15
|
+
TMDB_API_KEY = os.environ.get("TMDB_API_KEY", "")
|
|
16
|
+
|
|
17
|
+
router = APIRouter(prefix="/api", tags=["tmdb"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.get("/tmdb/search")
|
|
21
|
+
async def search(request: Request, q: str, year: str = "", kind: str = "movie"):
|
|
22
|
+
lang = get_request_lang(request)
|
|
23
|
+
if not TMDB_API_KEY:
|
|
24
|
+
raise HTTPException(400, t("err.tmdb_api_key_missing", lang))
|
|
25
|
+
loop = asyncio.get_event_loop()
|
|
26
|
+
try:
|
|
27
|
+
results = await loop.run_in_executor(
|
|
28
|
+
None, tmdb_search, kind, q, year, TMDB_API_KEY
|
|
29
|
+
)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
raise HTTPException(502, str(e))
|
|
32
|
+
return JSONResponse({"results": results})
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SetBody(BaseModel):
|
|
36
|
+
source_path: str
|
|
37
|
+
tmdb_id: str
|
|
38
|
+
tmdb_kind: str = "movie"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@router.post("/tmdb/set")
|
|
42
|
+
async def set_manual(request: Request, body: SetBody):
|
|
43
|
+
if not TMDB_API_KEY:
|
|
44
|
+
raise HTTPException(400, t("err.tmdb_api_key_missing", get_request_lang(request)))
|
|
45
|
+
loop = asyncio.get_event_loop()
|
|
46
|
+
try:
|
|
47
|
+
data = await loop.run_in_executor(
|
|
48
|
+
None, tmdb_fetch_bilingual, body.tmdb_kind, body.tmdb_id, TMDB_API_KEY
|
|
49
|
+
)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
raise HTTPException(502, str(e))
|
|
52
|
+
title = data.get("title") or ""
|
|
53
|
+
year = tmdb_year(data, body.tmdb_kind)
|
|
54
|
+
poster = tmdb_poster_url(data)
|
|
55
|
+
await set_cache(
|
|
56
|
+
body.source_path,
|
|
57
|
+
tmdb_id=body.tmdb_id,
|
|
58
|
+
tmdb_kind=body.tmdb_kind,
|
|
59
|
+
title=title,
|
|
60
|
+
title_en=data.get("title_en", ""),
|
|
61
|
+
original_title=data.get("original_title", ""),
|
|
62
|
+
year=year,
|
|
63
|
+
poster=poster,
|
|
64
|
+
overview=(data.get("overview") or "")[:300],
|
|
65
|
+
overview_en=(data.get("overview_en") or "")[:300],
|
|
66
|
+
)
|
|
67
|
+
return JSONResponse({"ok": True, "title": title, "year": year, "poster": poster})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ClearBody(BaseModel):
|
|
71
|
+
source_path: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.post("/tmdb/clear")
|
|
75
|
+
async def clear(body: ClearBody):
|
|
76
|
+
await delete_cache(body.source_path)
|
|
77
|
+
return JSONResponse({"ok": True})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Tracker status (for sidebar online/offline indicators)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
8
|
+
|
|
9
|
+
from .. import config
|
|
10
|
+
from ..trackers import build_trackers
|
|
11
|
+
|
|
12
|
+
router = APIRouter(prefix="/api", tags=["trackers"])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.get("/trackers/status")
|
|
16
|
+
async def status():
|
|
17
|
+
trk_map = build_trackers(config.load())
|
|
18
|
+
names = list(trk_map.keys())
|
|
19
|
+
statuses = await asyncio.gather(
|
|
20
|
+
*(trk_map[n].status() for n in names), return_exceptions=True
|
|
21
|
+
)
|
|
22
|
+
out = []
|
|
23
|
+
for name, s in zip(names, statuses):
|
|
24
|
+
trk = trk_map[name]
|
|
25
|
+
out.append({
|
|
26
|
+
"name": name,
|
|
27
|
+
"online": bool(s) if not isinstance(s, Exception) else False,
|
|
28
|
+
"configured": bool(getattr(trk, "configured", False)),
|
|
29
|
+
})
|
|
30
|
+
return JSONResponse({"trackers": out})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Upload history JSON endpoints."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
5
|
+
from fastapi.responses import JSONResponse
|
|
6
|
+
|
|
7
|
+
from ...i18n import get_request_lang, t
|
|
8
|
+
from ..db import delete_record, list_uploads
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/api", tags=["uploaded"])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/uploaded")
|
|
14
|
+
async def list_all():
|
|
15
|
+
records = await list_uploads()
|
|
16
|
+
return JSONResponse({"records": records})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.delete("/uploaded/{record_id}")
|
|
20
|
+
async def delete_by_id(request: Request, record_id: int):
|
|
21
|
+
records = await list_uploads()
|
|
22
|
+
target = next((r for r in records if r.get("id") == record_id), None)
|
|
23
|
+
if target is None:
|
|
24
|
+
raise HTTPException(404, t("err.record_not_found", get_request_lang(request)))
|
|
25
|
+
await delete_record(target["seeding_path"])
|
|
26
|
+
return JSONResponse({"ok": True})
|