Process 1 build 0703261729
This commit is contained in:
@@ -20,6 +20,7 @@ SEERR_MEDIA_FAILURE_PERSISTENT_THRESHOLD = 3
|
||||
SQLITE_BUSY_TIMEOUT_MS = 5_000
|
||||
SQLITE_CACHE_SIZE_KIB = 32_768
|
||||
SQLITE_MMAP_SIZE_BYTES = 256 * 1024 * 1024
|
||||
_DB_UNSET = object()
|
||||
|
||||
|
||||
def _db_path() -> str:
|
||||
@@ -349,6 +350,43 @@ def init_db() -> None:
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS portal_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
kind TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
media_type TEXT,
|
||||
year INTEGER,
|
||||
external_ref TEXT,
|
||||
source_system TEXT,
|
||||
source_request_id INTEGER,
|
||||
status TEXT NOT NULL,
|
||||
priority TEXT NOT NULL,
|
||||
created_by_username TEXT NOT NULL,
|
||||
created_by_id INTEGER,
|
||||
assignee_username TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
last_activity_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS portal_comments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
item_id INTEGER NOT NULL,
|
||||
author_username TEXT NOT NULL,
|
||||
author_role TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
is_internal INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(item_id) REFERENCES portal_items(id) ON DELETE CASCADE
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_requests_cache_created_at
|
||||
@@ -409,6 +447,30 @@ def init_db() -> None:
|
||||
ON password_reset_tokens (expires_at)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_portal_items_kind_status
|
||||
ON portal_items (kind, status, updated_at DESC, id DESC)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_portal_items_creator
|
||||
ON portal_items (created_by_username, updated_at DESC, id DESC)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_portal_items_status
|
||||
ON portal_items (status, updated_at DESC, id DESC)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_portal_comments_item_created
|
||||
ON portal_comments (item_id, created_at DESC, id DESC)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS user_activity (
|
||||
@@ -2879,6 +2941,417 @@ def clear_seerr_media_failure(media_type: Optional[str], tmdb_id: Optional[int])
|
||||
)
|
||||
|
||||
|
||||
def _portal_item_from_row(row: tuple[Any, ...]) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": row[0],
|
||||
"kind": row[1],
|
||||
"title": row[2],
|
||||
"description": row[3],
|
||||
"media_type": row[4],
|
||||
"year": row[5],
|
||||
"external_ref": row[6],
|
||||
"source_system": row[7],
|
||||
"source_request_id": row[8],
|
||||
"status": row[9],
|
||||
"priority": row[10],
|
||||
"created_by_username": row[11],
|
||||
"created_by_id": row[12],
|
||||
"assignee_username": row[13],
|
||||
"created_at": row[14],
|
||||
"updated_at": row[15],
|
||||
"last_activity_at": row[16],
|
||||
}
|
||||
|
||||
|
||||
def _portal_comment_from_row(row: tuple[Any, ...]) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": row[0],
|
||||
"item_id": row[1],
|
||||
"author_username": row[2],
|
||||
"author_role": row[3],
|
||||
"message": row[4],
|
||||
"is_internal": bool(row[5]),
|
||||
"created_at": row[6],
|
||||
}
|
||||
|
||||
|
||||
def create_portal_item(
|
||||
*,
|
||||
kind: str,
|
||||
title: str,
|
||||
description: str,
|
||||
created_by_username: str,
|
||||
created_by_id: Optional[int],
|
||||
media_type: Optional[str] = None,
|
||||
year: Optional[int] = None,
|
||||
external_ref: Optional[str] = None,
|
||||
source_system: Optional[str] = None,
|
||||
source_request_id: Optional[int] = None,
|
||||
status: str = "new",
|
||||
priority: str = "normal",
|
||||
assignee_username: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
with _connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
INSERT INTO portal_items (
|
||||
kind,
|
||||
title,
|
||||
description,
|
||||
media_type,
|
||||
year,
|
||||
external_ref,
|
||||
source_system,
|
||||
source_request_id,
|
||||
status,
|
||||
priority,
|
||||
created_by_username,
|
||||
created_by_id,
|
||||
assignee_username,
|
||||
created_at,
|
||||
updated_at,
|
||||
last_activity_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
kind,
|
||||
title,
|
||||
description,
|
||||
media_type,
|
||||
year,
|
||||
external_ref,
|
||||
source_system,
|
||||
source_request_id,
|
||||
status,
|
||||
priority,
|
||||
created_by_username,
|
||||
created_by_id,
|
||||
assignee_username,
|
||||
now,
|
||||
now,
|
||||
now,
|
||||
),
|
||||
)
|
||||
item_id = cursor.lastrowid
|
||||
created = get_portal_item(item_id)
|
||||
if not created:
|
||||
raise RuntimeError("Portal item could not be loaded after insert.")
|
||||
logger.info(
|
||||
"portal item created id=%s kind=%s status=%s priority=%s created_by=%s",
|
||||
created["id"],
|
||||
created["kind"],
|
||||
created["status"],
|
||||
created["priority"],
|
||||
created["created_by_username"],
|
||||
)
|
||||
return created
|
||||
|
||||
|
||||
def get_portal_item(item_id: int) -> Optional[Dict[str, Any]]:
|
||||
with _connect() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT
|
||||
id,
|
||||
kind,
|
||||
title,
|
||||
description,
|
||||
media_type,
|
||||
year,
|
||||
external_ref,
|
||||
source_system,
|
||||
source_request_id,
|
||||
status,
|
||||
priority,
|
||||
created_by_username,
|
||||
created_by_id,
|
||||
assignee_username,
|
||||
created_at,
|
||||
updated_at,
|
||||
last_activity_at
|
||||
FROM portal_items
|
||||
WHERE id = ?
|
||||
""",
|
||||
(item_id,),
|
||||
).fetchone()
|
||||
return _portal_item_from_row(row) if row else None
|
||||
|
||||
|
||||
def list_portal_items(
|
||||
*,
|
||||
kind: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
mine_username: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[Dict[str, Any]]:
|
||||
clauses: list[str] = []
|
||||
params: list[Any] = []
|
||||
if isinstance(kind, str) and kind.strip():
|
||||
clauses.append("kind = ?")
|
||||
params.append(kind.strip().lower())
|
||||
if isinstance(status, str) and status.strip():
|
||||
clauses.append("status = ?")
|
||||
params.append(status.strip().lower())
|
||||
if isinstance(mine_username, str) and mine_username.strip():
|
||||
clauses.append("created_by_username = ?")
|
||||
params.append(mine_username.strip())
|
||||
if isinstance(search, str) and search.strip():
|
||||
token = f"%{search.strip().lower()}%"
|
||||
clauses.append("(LOWER(title) LIKE ? OR LOWER(description) LIKE ? OR CAST(id AS TEXT) = ?)")
|
||||
params.extend([token, token, search.strip()])
|
||||
where_sql = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
safe_limit = max(1, min(int(limit), 500))
|
||||
safe_offset = max(0, int(offset))
|
||||
params.extend([safe_limit, safe_offset])
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT
|
||||
id,
|
||||
kind,
|
||||
title,
|
||||
description,
|
||||
media_type,
|
||||
year,
|
||||
external_ref,
|
||||
source_system,
|
||||
source_request_id,
|
||||
status,
|
||||
priority,
|
||||
created_by_username,
|
||||
created_by_id,
|
||||
assignee_username,
|
||||
created_at,
|
||||
updated_at,
|
||||
last_activity_at
|
||||
FROM portal_items
|
||||
{where_sql}
|
||||
ORDER BY last_activity_at DESC, id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""",
|
||||
tuple(params),
|
||||
).fetchall()
|
||||
return [_portal_item_from_row(row) for row in rows]
|
||||
|
||||
|
||||
def count_portal_items(
|
||||
*,
|
||||
kind: Optional[str] = None,
|
||||
status: Optional[str] = None,
|
||||
mine_username: Optional[str] = None,
|
||||
search: Optional[str] = None,
|
||||
) -> int:
|
||||
clauses: list[str] = []
|
||||
params: list[Any] = []
|
||||
if isinstance(kind, str) and kind.strip():
|
||||
clauses.append("kind = ?")
|
||||
params.append(kind.strip().lower())
|
||||
if isinstance(status, str) and status.strip():
|
||||
clauses.append("status = ?")
|
||||
params.append(status.strip().lower())
|
||||
if isinstance(mine_username, str) and mine_username.strip():
|
||||
clauses.append("created_by_username = ?")
|
||||
params.append(mine_username.strip())
|
||||
if isinstance(search, str) and search.strip():
|
||||
token = f"%{search.strip().lower()}%"
|
||||
clauses.append("(LOWER(title) LIKE ? OR LOWER(description) LIKE ? OR CAST(id AS TEXT) = ?)")
|
||||
params.extend([token, token, search.strip()])
|
||||
where_sql = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
with _connect() as conn:
|
||||
row = conn.execute(
|
||||
f"SELECT COUNT(*) FROM portal_items {where_sql}",
|
||||
tuple(params),
|
||||
).fetchone()
|
||||
return int(row[0] or 0) if row else 0
|
||||
|
||||
|
||||
def update_portal_item(
|
||||
item_id: int,
|
||||
*,
|
||||
title: Any = _DB_UNSET,
|
||||
description: Any = _DB_UNSET,
|
||||
status: Any = _DB_UNSET,
|
||||
priority: Any = _DB_UNSET,
|
||||
assignee_username: Any = _DB_UNSET,
|
||||
media_type: Any = _DB_UNSET,
|
||||
year: Any = _DB_UNSET,
|
||||
external_ref: Any = _DB_UNSET,
|
||||
source_system: Any = _DB_UNSET,
|
||||
source_request_id: Any = _DB_UNSET,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
updates: list[str] = []
|
||||
params: list[Any] = []
|
||||
if title is not _DB_UNSET:
|
||||
updates.append("title = ?")
|
||||
params.append(title)
|
||||
if description is not _DB_UNSET:
|
||||
updates.append("description = ?")
|
||||
params.append(description)
|
||||
if status is not _DB_UNSET:
|
||||
updates.append("status = ?")
|
||||
params.append(status)
|
||||
if priority is not _DB_UNSET:
|
||||
updates.append("priority = ?")
|
||||
params.append(priority)
|
||||
if assignee_username is not _DB_UNSET:
|
||||
updates.append("assignee_username = ?")
|
||||
params.append(assignee_username)
|
||||
if media_type is not _DB_UNSET:
|
||||
updates.append("media_type = ?")
|
||||
params.append(media_type)
|
||||
if year is not _DB_UNSET:
|
||||
updates.append("year = ?")
|
||||
params.append(year)
|
||||
if external_ref is not _DB_UNSET:
|
||||
updates.append("external_ref = ?")
|
||||
params.append(external_ref)
|
||||
if source_system is not _DB_UNSET:
|
||||
updates.append("source_system = ?")
|
||||
params.append(source_system)
|
||||
if source_request_id is not _DB_UNSET:
|
||||
updates.append("source_request_id = ?")
|
||||
params.append(source_request_id)
|
||||
if not updates:
|
||||
return get_portal_item(item_id)
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
updates.append("updated_at = ?")
|
||||
updates.append("last_activity_at = ?")
|
||||
params.extend([now, now, item_id])
|
||||
with _connect() as conn:
|
||||
changed = conn.execute(
|
||||
f"""
|
||||
UPDATE portal_items
|
||||
SET {', '.join(updates)}
|
||||
WHERE id = ?
|
||||
""",
|
||||
tuple(params),
|
||||
).rowcount
|
||||
if not changed:
|
||||
return None
|
||||
updated = get_portal_item(item_id)
|
||||
if updated:
|
||||
logger.info(
|
||||
"portal item updated id=%s status=%s priority=%s assignee=%s",
|
||||
updated["id"],
|
||||
updated["status"],
|
||||
updated["priority"],
|
||||
updated["assignee_username"],
|
||||
)
|
||||
return updated
|
||||
|
||||
|
||||
def add_portal_comment(
|
||||
item_id: int,
|
||||
*,
|
||||
author_username: str,
|
||||
author_role: str,
|
||||
message: str,
|
||||
is_internal: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
with _connect() as conn:
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
INSERT INTO portal_comments (
|
||||
item_id,
|
||||
author_username,
|
||||
author_role,
|
||||
message,
|
||||
is_internal,
|
||||
created_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
item_id,
|
||||
author_username,
|
||||
author_role,
|
||||
message,
|
||||
1 if is_internal else 0,
|
||||
now,
|
||||
),
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE portal_items
|
||||
SET last_activity_at = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(now, now, item_id),
|
||||
)
|
||||
comment_id = cursor.lastrowid
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT id, item_id, author_username, author_role, message, is_internal, created_at
|
||||
FROM portal_comments
|
||||
WHERE id = ?
|
||||
""",
|
||||
(comment_id,),
|
||||
).fetchone()
|
||||
if not row:
|
||||
raise RuntimeError("Portal comment could not be loaded after insert.")
|
||||
comment = _portal_comment_from_row(row)
|
||||
logger.info(
|
||||
"portal comment created id=%s item_id=%s author=%s internal=%s",
|
||||
comment["id"],
|
||||
comment["item_id"],
|
||||
comment["author_username"],
|
||||
comment["is_internal"],
|
||||
)
|
||||
return comment
|
||||
|
||||
|
||||
def list_portal_comments(item_id: int, *, include_internal: bool = True, limit: int = 200) -> list[Dict[str, Any]]:
|
||||
clauses = ["item_id = ?"]
|
||||
params: list[Any] = [item_id]
|
||||
if not include_internal:
|
||||
clauses.append("is_internal = 0")
|
||||
safe_limit = max(1, min(int(limit), 500))
|
||||
params.append(safe_limit)
|
||||
with _connect() as conn:
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT id, item_id, author_username, author_role, message, is_internal, created_at
|
||||
FROM portal_comments
|
||||
WHERE {' AND '.join(clauses)}
|
||||
ORDER BY created_at ASC, id ASC
|
||||
LIMIT ?
|
||||
""",
|
||||
tuple(params),
|
||||
).fetchall()
|
||||
return [_portal_comment_from_row(row) for row in rows]
|
||||
|
||||
|
||||
def get_portal_overview() -> Dict[str, Any]:
|
||||
with _connect() as conn:
|
||||
kind_rows = conn.execute(
|
||||
"""
|
||||
SELECT kind, COUNT(*)
|
||||
FROM portal_items
|
||||
GROUP BY kind
|
||||
"""
|
||||
).fetchall()
|
||||
status_rows = conn.execute(
|
||||
"""
|
||||
SELECT status, COUNT(*)
|
||||
FROM portal_items
|
||||
GROUP BY status
|
||||
"""
|
||||
).fetchall()
|
||||
total_items_row = conn.execute("SELECT COUNT(*) FROM portal_items").fetchone()
|
||||
total_comments_row = conn.execute("SELECT COUNT(*) FROM portal_comments").fetchone()
|
||||
return {
|
||||
"total_items": int(total_items_row[0] or 0) if total_items_row else 0,
|
||||
"total_comments": int(total_comments_row[0] or 0) if total_comments_row else 0,
|
||||
"by_kind": {str(row[0]): int(row[1] or 0) for row in kind_rows},
|
||||
"by_status": {str(row[0]): int(row[1] or 0) for row in status_rows},
|
||||
}
|
||||
|
||||
|
||||
def run_integrity_check() -> str:
|
||||
with _connect() as conn:
|
||||
row = conn.execute("PRAGMA integrity_check").fetchone()
|
||||
@@ -2922,6 +3395,8 @@ def get_database_diagnostics() -> Dict[str, Any]:
|
||||
"snapshots": int(conn.execute("SELECT COUNT(*) FROM snapshots").fetchone()[0] or 0),
|
||||
"seerr_media_failures": int(conn.execute("SELECT COUNT(*) FROM seerr_media_failures").fetchone()[0] or 0),
|
||||
"password_reset_tokens": int(conn.execute("SELECT COUNT(*) FROM password_reset_tokens").fetchone()[0] or 0),
|
||||
"portal_items": int(conn.execute("SELECT COUNT(*) FROM portal_items").fetchone()[0] or 0),
|
||||
"portal_comments": int(conn.execute("SELECT COUNT(*) FROM portal_comments").fetchone()[0] or 0),
|
||||
}
|
||||
row_count_ms = round((perf_counter() - row_count_started) * 1000, 1)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user