Feature/subscription table #2
No files matched your search
+15
-1
@@ -9,4 +9,18 @@ CREATE TABLE `seadex_state` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_user_anilist` (`user_id`,`anilist_id`),
|
||||
CONSTRAINT `fk_seadex_state_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE
|
||||
)
|
||||
)
|
||||
|
||||
CREATE TABLE `seadex_state_subscriptions` (
|
||||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) NOT NULL,
|
||||
`seadex_state_id` int(10) unsigned NOT NULL,
|
||||
`anime_name` varchar(255) NOT NULL,
|
||||
`active` tinyint(1) NOT NULL DEFAULT '1',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_user_state` (`user_id`,`seadex_state_id`),
|
||||
KEY `idx_subscriptions_state` (`seadex_state_id`),
|
||||
CONSTRAINT `fk_subscriptions_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE,
|
||||
CONSTRAINT `fk_subscriptions_state` FOREIGN KEY (`seadex_state_id`) REFERENCES `seadex_state` (`id`) ON DELETE CASCADE
|
||||
);
|
||||
+52
-47
@@ -5,7 +5,6 @@ from datetime import UTC, datetime
|
||||
|
||||
import uvicorn
|
||||
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
|
||||
from pydantic import BaseModel, Field, computed_field
|
||||
from seadex import EntryNotFoundError, SeaDexEntry
|
||||
from service_auth import UserId
|
||||
|
||||
@@ -19,30 +18,13 @@ from db_module import (
|
||||
)
|
||||
from metrics_server import REQUEST_COUNTER, metrics_api
|
||||
from secret_manager import cleanup_secret_files
|
||||
from send_notifications import NotificationClient
|
||||
from send_notifications import NotificationClient, NotificationUnavailableError, NotificationError
|
||||
from simple_logger_handler import LOG_LEVEL, setup_logger
|
||||
from models import AnimeCreate,AnimeEntry,AnimeList
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
class AnimeCreate(BaseModel):
|
||||
anilist_id: int = Field(gt=0)
|
||||
|
||||
class AnimeEntry(BaseModel):
|
||||
name: str
|
||||
last_updated: str | None = None
|
||||
|
||||
class Counts(BaseModel):
|
||||
active: int
|
||||
inactive: int
|
||||
|
||||
class AnimeList(BaseModel):
|
||||
active: list[AnimeEntry] = Field(default_factory=list)
|
||||
inactive: list[AnimeEntry] = Field(default_factory=list)
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def counts(self) -> Counts:
|
||||
return Counts(active=len(self.active),inactive=len(self.inactive))
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
@@ -113,52 +95,75 @@ def check_seadex(request:Request, db=Depends(get_db)):
|
||||
|
||||
with db.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT id, anilist_id, anime_name, last_updated, user_id "
|
||||
"SELECT id, anilist_id, anime_name, last_updated "
|
||||
"FROM seadex_state WHERE active = 1"
|
||||
)
|
||||
anime = cursor.fetchall()
|
||||
|
||||
cache: dict[int, str | None] = {}
|
||||
logger.debug(f"[FastAPI] Found {len(anime)} active anime to check.")
|
||||
if not anime[0]:
|
||||
logger.info("[FastAPI] No active Seadex entries")
|
||||
return results
|
||||
|
||||
with SeaDexEntry() as seadex:
|
||||
for row_id, anilist_id, anime_name, last_updated, user_id in anime:
|
||||
for seadex_state_id, anilist_id, anime_name, last_updated in anime:
|
||||
results["checked"] += 1
|
||||
try:
|
||||
if anilist_id not in cache:
|
||||
cache[anilist_id] = check_for_update(seadex, anilist_id)
|
||||
current = cache[anilist_id]
|
||||
|
||||
current = check_for_update(seadex, anilist_id)
|
||||
if current is None:
|
||||
logger.debug(f"[FastAPI] No entry yet for {anime_name} on SeaDex")
|
||||
logger.debug(f"[FastAPI] No entry yet for Anime: {anime_name} on SeaDex")
|
||||
continue
|
||||
if current == last_updated:
|
||||
continue
|
||||
|
||||
if current != last_updated:
|
||||
logger.info(
|
||||
f"[FastAPI] {anime_name} updated, Anilist ID: {anilist_id}, User ID: {user_id}"
|
||||
)
|
||||
request.app.state.notifier.send(
|
||||
title=anime_name,
|
||||
body="SeaDex entry updated",
|
||||
link=f"https://seadex.moe/{anilist_id}/",
|
||||
user_id=user_id,
|
||||
logger.info(f"[FastAPI] Anime: {anime_name} updated, Anilist ID: {anilist_id}")
|
||||
with db.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT user_id, anime_name as custom_name "
|
||||
"FROM seadex_state_subscriptions "
|
||||
"WHERE seadex_state_id = %s AND active = 1",
|
||||
(seadex_state_id,),
|
||||
)
|
||||
subscribers = cursor.fetchall()
|
||||
|
||||
with db.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"UPDATE seadex_state SET last_updated = %s WHERE id = %s AND user_id = %s",
|
||||
(current, row_id, user_id),
|
||||
sent = 0
|
||||
failed = 0
|
||||
for user_id, custom_name in subscribers:
|
||||
try:
|
||||
request.app.state.notifier.send(
|
||||
title=custom_name or anime_name,
|
||||
body="SeaDex entry updated",
|
||||
link=f"https://seadex.moe/{anilist_id}/",
|
||||
user_id=user_id,
|
||||
)
|
||||
db.commit()
|
||||
sent += 1
|
||||
except NotificationUnavailableError:
|
||||
failed = len(subscribers) - sent
|
||||
logger.error (f"[FastAPI] Notifications backend unavailable, skipping "
|
||||
f"{failed} remaining user(s) for Anime: {anime_name}")
|
||||
break
|
||||
except NotificationError as e:
|
||||
failed += 1
|
||||
logger.error(f"[FastAPI] Failed to send notification to user_id={user_id} for Anime: {anime_name} "
|
||||
f"status={getattr(e, 'status_code',None)}")
|
||||
if subscribers and sent == 0:
|
||||
results["errors"] += 1
|
||||
logger.error(f"[FastAPI] All notifications failed for Anime: {anime_name}, will retry later")
|
||||
continue
|
||||
|
||||
logger.debug(
|
||||
f"[FastAPI] Notified and updated db for id={row_id}, "
|
||||
f"Anilist ID={anilist_id}, User ID: {user_id}"
|
||||
with db.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"UPDATE seadex_state SET last_updated = %s WHERE id = %s",
|
||||
(current, seadex_state_id),
|
||||
)
|
||||
results["new"] += 1
|
||||
db.commit()
|
||||
logger.debug(f"[FastAPI] Notification sent and database updated for Anime: {anime_name}")
|
||||
results["new"] += 1
|
||||
|
||||
except Exception:
|
||||
db.rollback()
|
||||
results["errors"] += 1
|
||||
logger.exception(f"[FastAPI] Failed for id={row_id} Anilist={anilist_id}, User ID: {user_id}",)
|
||||
logger.exception(f"[FastAPI] Failed for id: {seadex_state_id} Anilist: {anilist_id}",)
|
||||
|
||||
if results["errors"]:
|
||||
logger.warning(f"[FastAPI] SeaDex check finished with errors: {results}")
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from pydantic import BaseModel, Field, computed_field
|
||||
|
||||
class AnimeCreate(BaseModel):
|
||||
anilist_id: int = Field(gt=0)
|
||||
|
||||
class AnimeEntry(BaseModel):
|
||||
name: str
|
||||
last_updated: str | None = None
|
||||
|
||||
class Counts(BaseModel):
|
||||
active: int
|
||||
inactive: int
|
||||
|
||||
class AnimeList(BaseModel):
|
||||
active: list[AnimeEntry] = Field(default_factory=list)
|
||||
inactive: list[AnimeEntry] = Field(default_factory=list)
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def counts(self) -> Counts:
|
||||
return Counts(active=len(self.active),inactive=len(self.inactive))
|
||||
Reference in new issue
Block a user