release: 2901262036
This commit is contained in:
@@ -145,6 +145,7 @@ def init_db() -> None:
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
auth_provider TEXT NOT NULL DEFAULT 'local',
|
||||
jellyseerr_user_id INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
last_login_at TEXT,
|
||||
is_blocked INTEGER NOT NULL DEFAULT 0,
|
||||
@@ -173,6 +174,7 @@ def init_db() -> None:
|
||||
year INTEGER,
|
||||
requested_by TEXT,
|
||||
requested_by_norm TEXT,
|
||||
requested_by_id INTEGER,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
payload_json TEXT NOT NULL
|
||||
@@ -258,6 +260,23 @@ def init_db() -> None:
|
||||
conn.execute("ALTER TABLE users ADD COLUMN last_jellyfin_auth_at TEXT")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
try:
|
||||
conn.execute("ALTER TABLE users ADD COLUMN jellyseerr_user_id INTEGER")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
try:
|
||||
conn.execute("ALTER TABLE requests_cache ADD COLUMN requested_by_id INTEGER")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
try:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_requests_cache_requested_by_id
|
||||
ON requests_cache (requested_by_id)
|
||||
"""
|
||||
)
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
_backfill_auth_providers()
|
||||
ensure_admin_user()
|
||||
|
||||
@@ -361,31 +380,41 @@ def ensure_admin_user() -> None:
|
||||
create_user(settings.admin_username, settings.admin_password, role="admin")
|
||||
|
||||
|
||||
def create_user(username: str, password: str, role: str = "user", auth_provider: str = "local") -> None:
|
||||
def create_user(
|
||||
username: str,
|
||||
password: str,
|
||||
role: str = "user",
|
||||
auth_provider: str = "local",
|
||||
jellyseerr_user_id: Optional[int] = None,
|
||||
) -> None:
|
||||
created_at = datetime.now(timezone.utc).isoformat()
|
||||
password_hash = hash_password(password)
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO users (username, password_hash, role, auth_provider, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO users (username, password_hash, role, auth_provider, jellyseerr_user_id, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(username, password_hash, role, auth_provider, created_at),
|
||||
(username, password_hash, role, auth_provider, jellyseerr_user_id, created_at),
|
||||
)
|
||||
|
||||
|
||||
def create_user_if_missing(
|
||||
username: str, password: str, role: str = "user", auth_provider: str = "local"
|
||||
username: str,
|
||||
password: str,
|
||||
role: str = "user",
|
||||
auth_provider: str = "local",
|
||||
jellyseerr_user_id: Optional[int] = None,
|
||||
) -> bool:
|
||||
created_at = datetime.now(timezone.utc).isoformat()
|
||||
password_hash = hash_password(password)
|
||||
with _connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
INSERT OR IGNORE INTO users (username, password_hash, role, auth_provider, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT OR IGNORE INTO users (username, password_hash, role, auth_provider, jellyseerr_user_id, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(username, password_hash, role, auth_provider, created_at),
|
||||
(username, password_hash, role, auth_provider, jellyseerr_user_id, created_at),
|
||||
)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
@@ -394,10 +423,10 @@ def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT id, username, password_hash, role, auth_provider, created_at, last_login_at,
|
||||
is_blocked, jellyfin_password_hash, last_jellyfin_auth_at
|
||||
SELECT id, username, password_hash, role, auth_provider, jellyseerr_user_id,
|
||||
created_at, last_login_at, is_blocked, jellyfin_password_hash, last_jellyfin_auth_at
|
||||
FROM users
|
||||
WHERE username = ?
|
||||
WHERE username = ? COLLATE NOCASE
|
||||
""",
|
||||
(username,),
|
||||
).fetchone()
|
||||
@@ -409,19 +438,47 @@ def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
|
||||
"password_hash": row[2],
|
||||
"role": row[3],
|
||||
"auth_provider": row[4],
|
||||
"created_at": row[5],
|
||||
"last_login_at": row[6],
|
||||
"is_blocked": bool(row[7]),
|
||||
"jellyfin_password_hash": row[8],
|
||||
"last_jellyfin_auth_at": row[9],
|
||||
"jellyseerr_user_id": row[5],
|
||||
"created_at": row[6],
|
||||
"last_login_at": row[7],
|
||||
"is_blocked": bool(row[8]),
|
||||
"jellyfin_password_hash": row[9],
|
||||
"last_jellyfin_auth_at": row[10],
|
||||
}
|
||||
|
||||
|
||||
def get_user_by_id(user_id: int) -> Optional[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT id, username, password_hash, role, auth_provider, jellyseerr_user_id,
|
||||
created_at, last_login_at, is_blocked, jellyfin_password_hash, last_jellyfin_auth_at
|
||||
FROM users
|
||||
WHERE id = ?
|
||||
""",
|
||||
(user_id,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
return {
|
||||
"id": row[0],
|
||||
"username": row[1],
|
||||
"password_hash": row[2],
|
||||
"role": row[3],
|
||||
"auth_provider": row[4],
|
||||
"jellyseerr_user_id": row[5],
|
||||
"created_at": row[6],
|
||||
"last_login_at": row[7],
|
||||
"is_blocked": bool(row[8]),
|
||||
"jellyfin_password_hash": row[9],
|
||||
"last_jellyfin_auth_at": row[10],
|
||||
}
|
||||
|
||||
def get_all_users() -> list[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT id, username, role, auth_provider, created_at, last_login_at, is_blocked
|
||||
SELECT id, username, role, auth_provider, jellyseerr_user_id, created_at, last_login_at, is_blocked
|
||||
FROM users
|
||||
ORDER BY username COLLATE NOCASE
|
||||
"""
|
||||
@@ -434,14 +491,35 @@ def get_all_users() -> list[Dict[str, Any]]:
|
||||
"username": row[1],
|
||||
"role": row[2],
|
||||
"auth_provider": row[3],
|
||||
"created_at": row[4],
|
||||
"last_login_at": row[5],
|
||||
"is_blocked": bool(row[6]),
|
||||
"jellyseerr_user_id": row[4],
|
||||
"created_at": row[5],
|
||||
"last_login_at": row[6],
|
||||
"is_blocked": bool(row[7]),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def delete_non_admin_users() -> int:
|
||||
with _connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
DELETE FROM users WHERE role != 'admin'
|
||||
"""
|
||||
)
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
def set_user_jellyseerr_id(username: str, jellyseerr_user_id: Optional[int]) -> None:
|
||||
with _connect() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE users SET jellyseerr_user_id = ? WHERE username = ?
|
||||
""",
|
||||
(jellyseerr_user_id, username),
|
||||
)
|
||||
|
||||
|
||||
def set_last_login(username: str) -> None:
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
with _connect() as conn:
|
||||
@@ -614,8 +692,8 @@ def get_user_activity_summary(username: str) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def get_user_request_stats(username_norm: str) -> Dict[str, Any]:
|
||||
if not username_norm:
|
||||
def get_user_request_stats(username_norm: str, requested_by_id: Optional[int] = None) -> Dict[str, Any]:
|
||||
if requested_by_id is None:
|
||||
return {
|
||||
"total": 0,
|
||||
"ready": 0,
|
||||
@@ -632,26 +710,26 @@ def get_user_request_stats(username_norm: str) -> Dict[str, Any]:
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM requests_cache
|
||||
WHERE requested_by_norm = ?
|
||||
WHERE requested_by_id = ?
|
||||
""",
|
||||
(username_norm,),
|
||||
(requested_by_id,),
|
||||
).fetchone()
|
||||
status_rows = conn.execute(
|
||||
"""
|
||||
SELECT status, COUNT(*)
|
||||
FROM requests_cache
|
||||
WHERE requested_by_norm = ?
|
||||
WHERE requested_by_id = ?
|
||||
GROUP BY status
|
||||
""",
|
||||
(username_norm,),
|
||||
(requested_by_id,),
|
||||
).fetchall()
|
||||
last_row = conn.execute(
|
||||
"""
|
||||
SELECT MAX(created_at)
|
||||
FROM requests_cache
|
||||
WHERE requested_by_norm = ?
|
||||
WHERE requested_by_id = ?
|
||||
""",
|
||||
(username_norm,),
|
||||
(requested_by_id,),
|
||||
).fetchone()
|
||||
counts = {int(row[0]): int(row[1]) for row in status_rows if row[0] is not None}
|
||||
pending = counts.get(1, 0)
|
||||
@@ -706,6 +784,7 @@ def upsert_request_cache(
|
||||
year: Optional[int],
|
||||
requested_by: Optional[str],
|
||||
requested_by_norm: Optional[str],
|
||||
requested_by_id: Optional[int],
|
||||
created_at: Optional[str],
|
||||
updated_at: Optional[str],
|
||||
payload_json: str,
|
||||
@@ -749,11 +828,12 @@ def upsert_request_cache(
|
||||
year,
|
||||
requested_by,
|
||||
requested_by_norm,
|
||||
requested_by_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
payload_json
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(request_id) DO UPDATE SET
|
||||
media_id = excluded.media_id,
|
||||
media_type = excluded.media_type,
|
||||
@@ -762,6 +842,7 @@ def upsert_request_cache(
|
||||
year = excluded.year,
|
||||
requested_by = excluded.requested_by,
|
||||
requested_by_norm = excluded.requested_by_norm,
|
||||
requested_by_id = excluded.requested_by_id,
|
||||
created_at = excluded.created_at,
|
||||
updated_at = excluded.updated_at,
|
||||
payload_json = excluded.payload_json
|
||||
@@ -775,6 +856,7 @@ def upsert_request_cache(
|
||||
normalized_year,
|
||||
requested_by,
|
||||
requested_by_norm,
|
||||
requested_by_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
payload_json,
|
||||
@@ -844,15 +926,20 @@ def get_cached_requests(
|
||||
limit: int,
|
||||
offset: int,
|
||||
requested_by_norm: Optional[str] = None,
|
||||
requested_by_id: Optional[int] = None,
|
||||
since_iso: Optional[str] = None,
|
||||
) -> list[Dict[str, Any]]:
|
||||
query = """
|
||||
SELECT request_id, media_id, media_type, status, title, year, requested_by, created_at, payload_json
|
||||
SELECT request_id, media_id, media_type, status, title, year, requested_by,
|
||||
requested_by_norm, requested_by_id, created_at, payload_json
|
||||
FROM requests_cache
|
||||
"""
|
||||
params: list[Any] = []
|
||||
conditions = []
|
||||
if requested_by_norm:
|
||||
if requested_by_id is not None:
|
||||
conditions.append("requested_by_id = ?")
|
||||
params.append(requested_by_id)
|
||||
elif requested_by_norm:
|
||||
conditions.append("requested_by_norm = ?")
|
||||
params.append(requested_by_norm)
|
||||
if since_iso:
|
||||
@@ -865,17 +952,19 @@ def get_cached_requests(
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(query, tuple(params)).fetchall()
|
||||
logger.debug(
|
||||
"requests_cache list: count=%s requested_by_norm=%s since_iso=%s",
|
||||
"requests_cache list: count=%s requested_by_norm=%s requested_by_id=%s since_iso=%s",
|
||||
len(rows),
|
||||
requested_by_norm,
|
||||
requested_by_id,
|
||||
since_iso,
|
||||
)
|
||||
results: list[Dict[str, Any]] = []
|
||||
for row in rows:
|
||||
title = row[4]
|
||||
year = row[5]
|
||||
if (not title or not year) and row[8]:
|
||||
derived_title, derived_year = _extract_title_year_from_payload(row[8])
|
||||
payload_json = row[10]
|
||||
if (not title or not year) and payload_json:
|
||||
derived_title, derived_year = _extract_title_year_from_payload(payload_json)
|
||||
if not title:
|
||||
title = derived_title
|
||||
if not year:
|
||||
@@ -889,7 +978,9 @@ def get_cached_requests(
|
||||
"title": title,
|
||||
"year": year,
|
||||
"requested_by": row[6],
|
||||
"created_at": row[7],
|
||||
"requested_by_norm": row[7],
|
||||
"requested_by_id": row[8],
|
||||
"created_at": row[9],
|
||||
}
|
||||
)
|
||||
return results
|
||||
@@ -900,7 +991,8 @@ def get_request_cache_overview(limit: int = 50) -> list[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT request_id, media_id, media_type, status, title, year, requested_by, created_at, updated_at, payload_json
|
||||
SELECT request_id, media_id, media_type, status, title, year, requested_by,
|
||||
requested_by_norm, requested_by_id, created_at, updated_at, payload_json
|
||||
FROM requests_cache
|
||||
ORDER BY updated_at DESC, request_id DESC
|
||||
LIMIT ?
|
||||
@@ -910,8 +1002,8 @@ def get_request_cache_overview(limit: int = 50) -> list[Dict[str, Any]]:
|
||||
results: list[Dict[str, Any]] = []
|
||||
for row in rows:
|
||||
title = row[4]
|
||||
if not title and row[9]:
|
||||
derived_title, _ = _extract_title_year_from_payload(row[9])
|
||||
if not title and row[11]:
|
||||
derived_title, _ = _extract_title_year_from_payload(row[11])
|
||||
title = derived_title or row[4]
|
||||
results.append(
|
||||
{
|
||||
@@ -922,8 +1014,10 @@ def get_request_cache_overview(limit: int = 50) -> list[Dict[str, Any]]:
|
||||
"title": title,
|
||||
"year": row[5],
|
||||
"requested_by": row[6],
|
||||
"created_at": row[7],
|
||||
"updated_at": row[8],
|
||||
"requested_by_norm": row[7],
|
||||
"requested_by_id": row[8],
|
||||
"created_at": row[9],
|
||||
"updated_at": row[10],
|
||||
}
|
||||
)
|
||||
return results
|
||||
@@ -1202,7 +1296,8 @@ def get_cached_requests_since(since_iso: str) -> list[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT request_id, media_id, media_type, status, title, year, requested_by, requested_by_norm, created_at
|
||||
SELECT request_id, media_id, media_type, status, title, year, requested_by,
|
||||
requested_by_norm, requested_by_id, created_at
|
||||
FROM requests_cache
|
||||
WHERE created_at >= ?
|
||||
ORDER BY created_at DESC, request_id DESC
|
||||
@@ -1221,14 +1316,17 @@ def get_cached_requests_since(since_iso: str) -> list[Dict[str, Any]]:
|
||||
"year": row[5],
|
||||
"requested_by": row[6],
|
||||
"requested_by_norm": row[7],
|
||||
"created_at": row[8],
|
||||
"requested_by_id": row[8],
|
||||
"created_at": row[9],
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def get_cached_request_by_media_id(
|
||||
media_id: int, requested_by_norm: Optional[str] = None
|
||||
media_id: int,
|
||||
requested_by_norm: Optional[str] = None,
|
||||
requested_by_id: Optional[int] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
query = """
|
||||
SELECT request_id, status
|
||||
@@ -1236,7 +1334,10 @@ def get_cached_request_by_media_id(
|
||||
WHERE media_id = ?
|
||||
"""
|
||||
params: list[Any] = [media_id]
|
||||
if requested_by_norm:
|
||||
if requested_by_id is not None:
|
||||
query += " AND requested_by_id = ?"
|
||||
params.append(requested_by_id)
|
||||
elif requested_by_norm:
|
||||
query += " AND requested_by_norm = ?"
|
||||
params.append(requested_by_norm)
|
||||
query += " ORDER BY created_at DESC, request_id DESC LIMIT 1"
|
||||
|
||||
Reference in New Issue
Block a user