Simplify UI and seed default Arr links
All checks were successful
docker / build-and-push (push) Successful in 48s
All checks were successful
docker / build-and-push (push) Successful in 48s
This commit is contained in:
@@ -23,9 +23,23 @@ DB_USER = os.getenv("DB_USER", "jellomator")
|
||||
DB_PASSWORD = os.getenv("DB_PASSWORD", "jellomator")
|
||||
DB_NAME = os.getenv("DB_NAME", "jellomator")
|
||||
|
||||
DEFAULT_LINKS = [
|
||||
{"name": "Jellyfin", "url": "http://localhost:8096", "description": "Media server", "category": "Media"},
|
||||
{"name": "Jellyseerr", "url": "http://localhost:5055", "description": "Request management", "category": "Media"},
|
||||
{"name": "Sonarr", "url": "http://localhost:8989", "description": "TV automation", "category": "Arr"},
|
||||
{"name": "Radarr", "url": "http://localhost:7878", "description": "Movie automation", "category": "Arr"},
|
||||
{"name": "Lidarr", "url": "http://localhost:8686", "description": "Music automation", "category": "Arr"},
|
||||
{"name": "Readarr", "url": "http://localhost:8787", "description": "Book automation", "category": "Arr"},
|
||||
{"name": "Bazarr", "url": "http://localhost:6767", "description": "Subtitle management", "category": "Arr"},
|
||||
{"name": "Prowlarr", "url": "http://localhost:9696", "description": "Indexer management", "category": "Arr"},
|
||||
{"name": "qBittorrent", "url": "http://localhost:8080", "description": "Torrent client", "category": "Download"},
|
||||
{"name": "SABnzbd", "url": "http://localhost:8085", "description": "Usenet client", "category": "Download"},
|
||||
]
|
||||
|
||||
app = FastAPI(title="Jellomator")
|
||||
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
|
||||
@contextmanager
|
||||
def db():
|
||||
conn = pymysql.connect(
|
||||
@@ -44,6 +58,24 @@ def db():
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def seed_default_links(conn):
|
||||
now = datetime.utcnow().isoformat()
|
||||
with conn.cursor() as cur:
|
||||
for item in DEFAULT_LINKS:
|
||||
cur.execute(
|
||||
"""insert into links(name,url,description,category,enabled,created_at,updated_at)
|
||||
values (%s,%s,%s,%s,1,%s,%s)
|
||||
on duplicate key update
|
||||
url=values(url),
|
||||
description=values(description),
|
||||
category=values(category),
|
||||
updated_at=values(updated_at)
|
||||
""",
|
||||
(item["name"], item["url"], item["description"], item["category"], now, now),
|
||||
)
|
||||
|
||||
|
||||
def init_db():
|
||||
with db() as c:
|
||||
with c.cursor() as cur:
|
||||
@@ -79,12 +111,17 @@ def init_db():
|
||||
updated_at varchar(64) not null
|
||||
) engine=InnoDB default charset=utf8mb4
|
||||
""")
|
||||
seed_default_links(c)
|
||||
|
||||
|
||||
init_db()
|
||||
|
||||
|
||||
class SetupIn(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class LinkIn(BaseModel):
|
||||
name: str
|
||||
url: str
|
||||
@@ -93,10 +130,12 @@ class LinkIn(BaseModel):
|
||||
icon_url: Optional[str] = None
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class LoginIn(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
def current_user(request: Request):
|
||||
token = request.cookies.get(SESSION_COOKIE)
|
||||
if not token:
|
||||
@@ -107,6 +146,7 @@ def current_user(request: Request):
|
||||
row = cur.fetchone()
|
||||
return row if row else None
|
||||
|
||||
|
||||
def require_admin(request: Request):
|
||||
user = current_user(request)
|
||||
if not user:
|
||||
@@ -117,11 +157,12 @@ def require_admin(request: Request):
|
||||
def link_name_exists(conn, name: str, *, exclude_id: int | None = None) -> bool:
|
||||
with conn.cursor() as cur:
|
||||
if exclude_id is None:
|
||||
cur.execute("select 1 from links where name=%s limit 1", (name,))
|
||||
cur.execute("select 1 from links where lower(name)=lower(%s) limit 1", (name,))
|
||||
else:
|
||||
cur.execute("select 1 from links where name=%s and id<>%s limit 1", (name, exclude_id))
|
||||
cur.execute("select 1 from links where lower(name)=lower(%s) and id<>%s limit 1", (name, exclude_id))
|
||||
return cur.fetchone() is not None
|
||||
|
||||
|
||||
@app.get("/api/me")
|
||||
def me(request: Request):
|
||||
with db() as c:
|
||||
@@ -130,6 +171,7 @@ def me(request: Request):
|
||||
needs_setup = cur.fetchone()["count"] == 0
|
||||
return {"needs_setup": needs_setup, "current_user": current_user(request)}
|
||||
|
||||
|
||||
@app.post("/api/setup")
|
||||
def setup(inp: SetupIn):
|
||||
with db() as c:
|
||||
@@ -141,6 +183,7 @@ def setup(inp: SetupIn):
|
||||
cur.execute("insert into users(username,password_hash,role) values (%s,%s,%s)", (inp.username, pw, "admin"))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/api/login")
|
||||
def login(inp: LoginIn):
|
||||
with db() as c:
|
||||
@@ -156,6 +199,7 @@ def login(inp: LoginIn):
|
||||
response.set_cookie(SESSION_COOKIE, token, httponly=True, samesite="lax", secure=False, path="/")
|
||||
return response
|
||||
|
||||
|
||||
@app.post("/api/logout")
|
||||
def logout(request: Request):
|
||||
token = request.cookies.get(SESSION_COOKIE)
|
||||
@@ -167,6 +211,7 @@ def logout(request: Request):
|
||||
resp.delete_cookie(SESSION_COOKIE, path="/")
|
||||
return resp
|
||||
|
||||
|
||||
@app.get("/api/links")
|
||||
def links():
|
||||
with db() as c:
|
||||
@@ -180,9 +225,10 @@ def links():
|
||||
icon_url = f"/api/links/{r['id']}/icon"
|
||||
elif r["icon_url"]:
|
||||
icon_url = r["icon_url"]
|
||||
out.append({k: r[k] for k in ["id","name","url","description","category","enabled"]} | {"icon_url": icon_url})
|
||||
out.append({k: r[k] for k in ["id", "name", "url", "description", "category", "enabled"]} | {"icon_url": icon_url})
|
||||
return out
|
||||
|
||||
|
||||
@app.get("/api/links/{link_id}")
|
||||
def get_link(link_id: int):
|
||||
with db() as c:
|
||||
@@ -194,6 +240,7 @@ def get_link(link_id: int):
|
||||
icon_url = f"/api/links/{row['id']}/icon" if row["icon_blob"] else row["icon_url"]
|
||||
return {k: row[k] for k in ["id", "name", "url", "description", "category", "enabled", "icon_url"]}
|
||||
|
||||
|
||||
@app.post("/api/links")
|
||||
def create_link(
|
||||
request: Request,
|
||||
@@ -216,11 +263,14 @@ def create_link(
|
||||
now = datetime.utcnow().isoformat()
|
||||
with db() as c:
|
||||
with c.cursor() as cur:
|
||||
cur.execute("""insert into links(name,url,description,category,icon_blob,icon_mime,icon_url,enabled,created_at,updated_at)
|
||||
cur.execute(
|
||||
"""insert into links(name,url,description,category,icon_blob,icon_mime,icon_url,enabled,created_at,updated_at)
|
||||
values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""",
|
||||
(name, url, description, category, icon_blob, icon_mime, icon_url, int(enabled), now, now))
|
||||
(name, url, description, category, icon_blob, icon_mime, icon_url, int(enabled), now, now),
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/api/links/{link_id}/icon")
|
||||
def link_icon(link_id: int):
|
||||
with db() as c:
|
||||
@@ -231,6 +281,7 @@ def link_icon(link_id: int):
|
||||
raise HTTPException(404, "Not found")
|
||||
return Response(content=row["icon_blob"], media_type=row["icon_mime"] or "image/png")
|
||||
|
||||
|
||||
@app.delete("/api/links/{link_id}")
|
||||
def delete_link(request: Request, link_id: int):
|
||||
require_admin(request)
|
||||
@@ -239,6 +290,7 @@ def delete_link(request: Request, link_id: int):
|
||||
cur.execute("delete from links where id=%s", (link_id,))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.patch("/api/links/{link_id}")
|
||||
def update_link(
|
||||
request: Request,
|
||||
@@ -262,16 +314,22 @@ def update_link(
|
||||
raise HTTPException(409, "Link name already exists")
|
||||
with c.cursor() as cur:
|
||||
if icon_blob:
|
||||
cur.execute("""update links set name=%s,url=%s,description=%s,category=%s,icon_blob=%s,icon_mime=%s,icon_url=%s,enabled=%s,updated_at=%s where id=%s""",
|
||||
(name,url,description,category,icon_blob,icon_mime,icon_url,int(enabled),now,link_id))
|
||||
cur.execute(
|
||||
"""update links set name=%s,url=%s,description=%s,category=%s,icon_blob=%s,icon_mime=%s,icon_url=%s,enabled=%s,updated_at=%s where id=%s""",
|
||||
(name, url, description, category, icon_blob, icon_mime, icon_url, int(enabled), now, link_id),
|
||||
)
|
||||
else:
|
||||
cur.execute("""update links set name=%s,url=%s,description=%s,category=%s,icon_url=%s,enabled=%s,updated_at=%s where id=%s""",
|
||||
(name,url,description,category,icon_url,int(enabled),now,link_id))
|
||||
cur.execute(
|
||||
"""update links set name=%s,url=%s,description=%s,category=%s,icon_url=%s,enabled=%s,updated_at=%s where id=%s""",
|
||||
(name, url, description, category, icon_url, int(enabled), now, link_id),
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
if STATIC_DIR.exists():
|
||||
app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets")
|
||||
|
||||
|
||||
@app.get("/{path:path}")
|
||||
def spa(path: str):
|
||||
if path.startswith("api/"):
|
||||
|
||||
Reference in New Issue
Block a user