Remove preset services and move to MariaDB
All checks were successful
docker / build-and-push (push) Successful in 54s

This commit is contained in:
Space-Banane
2026-05-20 20:50:59 +02:00
parent e62b9ee019
commit 556cdc36b6
6 changed files with 128 additions and 122 deletions

View File

@@ -1,51 +1,84 @@
from __future__ import annotations
import secrets
import sqlite3
import os
from datetime import datetime
from contextlib import contextmanager
from pathlib import Path
from typing import Optional
import bcrypt
import pymysql
from fastapi import FastAPI, File, Form, HTTPException, Request, Response, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
DB_PATH = Path("/app/data/jellomator.sqlite")
STATIC_DIR = Path("frontend/dist")
SESSION_COOKIE = "jellomator_session"
DB_HOST = os.getenv("DB_HOST", "mariadb")
DB_PORT = int(os.getenv("DB_PORT", "3306"))
DB_USER = os.getenv("DB_USER", "jellomator")
DB_PASSWORD = os.getenv("DB_PASSWORD", "jellomator")
DB_NAME = os.getenv("DB_NAME", "jellomator")
app = FastAPI(title="Jellomator")
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
PRESET_LINKS = [
{"name": "Sonarr", "url": "http://sonarr:8989", "description": "TV library automation", "category": "Arr*", "enabled": True},
{"name": "Radarr", "url": "http://radarr:7878", "description": "Movie library automation", "category": "Arr*", "enabled": True},
{"name": "Lidarr", "url": "http://lidarr:8686", "description": "Music library automation", "category": "Arr*", "enabled": True},
{"name": "Readarr", "url": "http://readarr:8787", "description": "Book library automation", "category": "Arr*", "enabled": True},
{"name": "Prowlarr", "url": "http://prowlarr:9696", "description": "Indexer management", "category": "Arr*", "enabled": True},
{"name": "Bazarr", "url": "http://bazarr:6767", "description": "Subtitle management", "category": "Arr*", "enabled": True},
{"name": "qBittorrent", "url": "http://qbittorrent:8080", "description": "Torrent client", "category": "Downloads", "enabled": True},
{"name": "Jellyfin", "url": "http://jellyfin:8096", "description": "Media server", "category": "Media", "enabled": True},
{"name": "Jellyseerr", "url": "http://jellyseerr:5055", "description": "Request management", "category": "Requests", "enabled": True},
{"name": "Overseerr", "url": "http://overseerr:5055", "description": "Request management", "category": "Requests", "enabled": True},
]
@contextmanager
def db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
conn = pymysql.connect(
host=DB_HOST,
port=DB_PORT,
user=DB_USER,
password=DB_PASSWORD,
cursorclass=pymysql.cursors.DictCursor,
autocommit=True,
)
try:
with conn.cursor() as cur:
cur.execute(f"create database if not exists `{DB_NAME}` default charset=utf8mb4")
conn.select_db(DB_NAME)
yield conn
finally:
conn.close()
def init_db():
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
with db() as c:
c.executescript("""
create table if not exists users(id integer primary key, username text unique, password_hash blob, role text not null);
create table if not exists sessions(token text primary key, user_id integer not null, created_at text not null);
create table if not exists links(id integer primary key, name text not null, url text not null, description text, category text, icon_blob blob, icon_mime text, icon_url text, enabled integer not null default 1, created_at text not null, updated_at text not null);
""")
with c.cursor() as cur:
cur.execute("""
create table if not exists users(
id bigint auto_increment primary key,
username varchar(255) not null unique,
password_hash varbinary(255) not null,
role varchar(32) not null
) engine=InnoDB default charset=utf8mb4
""")
cur.execute("""
create table if not exists sessions(
token varchar(255) primary key,
user_id bigint not null,
created_at varchar(64) not null,
index (user_id),
constraint sessions_user_fk foreign key (user_id) references users(id) on delete cascade
) engine=InnoDB default charset=utf8mb4
""")
cur.execute("""
create table if not exists links(
id bigint auto_increment primary key,
name varchar(255) not null,
url text not null,
description text,
category varchar(255),
icon_blob longblob,
icon_mime varchar(255),
icon_url text,
enabled tinyint(1) not null default 1,
created_at varchar(64) not null,
updated_at varchar(64) not null
) engine=InnoDB default charset=utf8mb4
""")
init_db()
class SetupIn(BaseModel):
@@ -69,8 +102,10 @@ def current_user(request: Request):
if not token:
return None
with db() as c:
row = c.execute("select u.username,u.role from sessions s join users u on u.id=s.user_id where s.token=?", (token,)).fetchone()
return dict(row) if row else None
with c.cursor() as cur:
cur.execute("select u.username,u.role from sessions s join users u on u.id=s.user_id where s.token=%s", (token,))
row = cur.fetchone()
return row if row else None
def require_admin(request: Request):
user = current_user(request)
@@ -78,39 +113,36 @@ def require_admin(request: Request):
raise HTTPException(401, "Unauthorized")
return user
def seed_preset_links(conn: sqlite3.Connection):
now = datetime.utcnow().isoformat()
for preset in PRESET_LINKS:
conn.execute(
"""insert into links(name,url,description,category,enabled,created_at,updated_at)
values (?,?,?,?,?,?,?)""",
(preset["name"], preset["url"], preset["description"], preset["category"], int(preset["enabled"]), now, now),
)
@app.get("/api/me")
def me(request: Request):
with db() as c:
needs_setup = c.execute("select count(*) from users").fetchone()[0] == 0
with c.cursor() as cur:
cur.execute("select count(*) as count from users")
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:
if c.execute("select count(*) from users").fetchone()[0] > 0:
raise HTTPException(400, "Setup already complete")
pw = bcrypt.hashpw(inp.password.encode(), bcrypt.gensalt())
c.execute("insert into users(username,password_hash,role) values (?,?,?)", (inp.username, pw, "admin"))
seed_preset_links(c)
with c.cursor() as cur:
cur.execute("select count(*) as count from users")
if cur.fetchone()["count"] > 0:
raise HTTPException(400, "Setup already complete")
pw = bcrypt.hashpw(inp.password.encode(), bcrypt.gensalt())
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:
row = c.execute("select id,password_hash from users where username=?", (inp.username,)).fetchone()
with c.cursor() as cur:
cur.execute("select id,password_hash from users where username=%s", (inp.username,))
row = cur.fetchone()
if not row or not bcrypt.checkpw(inp.password.encode(), row["password_hash"]):
raise HTTPException(401, "Invalid credentials")
token = secrets.token_urlsafe(32)
c.execute("insert into sessions(token,user_id,created_at) values (?,?,?)", (token, row["id"], datetime.utcnow().isoformat()))
with c.cursor() as cur:
cur.execute("insert into sessions(token,user_id,created_at) values (%s,%s,%s)", (token, row["id"], datetime.utcnow().isoformat()))
response = JSONResponse({"ok": True})
response.set_cookie(SESSION_COOKIE, token, httponly=True, samesite="lax", secure=False, path="/")
return response
@@ -119,7 +151,9 @@ def login(inp: LoginIn):
def logout(request: Request):
token = request.cookies.get(SESSION_COOKIE)
with db() as c:
if token: c.execute("delete from sessions where token=?", (token,))
if token:
with c.cursor() as cur:
cur.execute("delete from sessions where token=%s", (token,))
resp = JSONResponse({"ok": True})
resp.delete_cookie(SESSION_COOKIE, path="/")
return resp
@@ -127,7 +161,9 @@ def logout(request: Request):
@app.get("/api/links")
def links():
with db() as c:
rows = c.execute("select * from links order by enabled desc, category, name").fetchall()
with c.cursor() as cur:
cur.execute("select * from links order by enabled desc, category, name")
rows = cur.fetchall()
out = []
for r in rows:
icon_url = None
@@ -141,7 +177,9 @@ def links():
@app.get("/api/links/{link_id}")
def get_link(link_id: int):
with db() as c:
row = c.execute("select * from links where id=?", (link_id,)).fetchone()
with c.cursor() as cur:
cur.execute("select * from links where id=%s", (link_id,))
row = cur.fetchone()
if not row:
raise HTTPException(404, "Not found")
icon_url = f"/api/links/{row['id']}/icon" if row["icon_blob"] else row["icon_url"]
@@ -165,15 +203,18 @@ def create_link(
icon_mime = icon.content_type
now = datetime.utcnow().isoformat()
with db() as c:
c.execute("""insert into links(name,url,description,category,icon_blob,icon_mime,icon_url,enabled,created_at,updated_at)
values (?,?,?,?,?,?,?,?,?,?)""",
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)
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))
return {"ok": True}
@app.get("/api/links/{link_id}/icon")
def link_icon(link_id: int):
with db() as c:
row = c.execute("select icon_blob,icon_mime from links where id=?", (link_id,)).fetchone()
with c.cursor() as cur:
cur.execute("select icon_blob,icon_mime from links where id=%s", (link_id,))
row = cur.fetchone()
if not row or not row["icon_blob"]:
raise HTTPException(404, "Not found")
return Response(content=row["icon_blob"], media_type=row["icon_mime"] or "image/png")
@@ -182,7 +223,8 @@ def link_icon(link_id: int):
def delete_link(request: Request, link_id: int):
require_admin(request)
with db() as c:
c.execute("delete from links where id=?", (link_id,))
with c.cursor() as cur:
cur.execute("delete from links where id=%s", (link_id,))
return {"ok": True}
@app.patch("/api/links/{link_id}")
@@ -204,12 +246,13 @@ def update_link(
icon_mime = icon.content_type
now = datetime.utcnow().isoformat()
with db() as c:
if icon_blob:
c.execute("""update links set name=?,url=?,description=?,category=?,icon_blob=?,icon_mime=?,icon_url=?,enabled=?,updated_at=? where id=?""",
(name,url,description,category,icon_blob,icon_mime,icon_url,int(enabled),now,link_id))
else:
c.execute("""update links set name=?,url=?,description=?,category=?,icon_url=?,enabled=?,updated_at=? where id=?""",
(name,url,description,category,icon_url,int(enabled),now,link_id))
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))
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))
return {"ok": True}
if STATIC_DIR.exists():

View File

@@ -4,3 +4,4 @@ bcrypt==4.2.1
jinja2==3.1.5
pydantic==2.10.6
python-multipart==0.0.20
pymysql==1.1.1