add notificaton module

This commit is contained in:
maxDorninger
2025-07-01 14:15:13 +02:00
parent 3088c65b09
commit 89b4fbb056
14 changed files with 344 additions and 0 deletions

View File

@@ -63,6 +63,7 @@ from media_manager.movies.service import ( # noqa: E402
import_all_movie_torrents,
update_all_movies_metadata,
)
from media_manager.notification.router import router as notification_router # noqa: E402
import uvicorn # noqa: E402
from fastapi.staticfiles import StaticFiles # noqa: E402
from media_manager.auth.users import openid_client # noqa: E402
@@ -226,6 +227,7 @@ if openid_client is not None:
app.include_router(tv_router.router, prefix="/tv", tags=["tv"])
app.include_router(torrent_router.router, prefix="/torrent", tags=["torrent"])
app.include_router(movies_router.router, prefix="/movies", tags=["movie"])
app.include_router(notification_router, prefix="/notification", tags=["notification"])
app.mount(
"/static/image",
StaticFiles(directory=basic_config.image_directory),

View File

View File

@@ -0,0 +1,32 @@
from typing import Annotated
from fastapi import Depends
from media_manager.database import DbSessionDependency
from media_manager.notification.repository import NotificationRepository
from media_manager.notification.service import NotificationService
def get_notification_repository(
db_session: DbSessionDependency,
) -> NotificationRepository:
return NotificationRepository(db_session)
notification_repository_dep = Annotated[
NotificationRepository, Depends(get_notification_repository)
]
def get_notification_service(
notification_repository: NotificationRepository = Depends(
notification_repository_dep
),
) -> NotificationService:
return NotificationService(notification_repository)
notification_service_dep = Annotated[
NotificationService, Depends(get_notification_service)
]

View File

@@ -0,0 +1,19 @@
from datetime import datetime
from uuid import UUID
from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint, DateTime
from sqlalchemy.orm import Mapped, mapped_column, relationship
from media_manager.auth.db import User
from media_manager.database import Base
from media_manager.torrent.models import Quality
class Notification(Base):
__tablename__ = "notification"
id: Mapped[UUID] = mapped_column(primary_key=True)
message: Mapped[str]
read: Mapped[bool]
timestamp = mapped_column(type=DateTime)

View File

@@ -0,0 +1,75 @@
from sqlalchemy import select, delete, update
from sqlalchemy.exc import (
IntegrityError,
SQLAlchemyError,
)
from sqlalchemy.orm import Session, joinedload
import logging
from media_manager.exceptions import NotFoundError, MediaAlreadyExists
from media_manager.notification.models import Notification
from media_manager.notification.schemas import NotificationId, Notification as NotificationSchema
log = logging.getLogger(__name__)
class NotificationRepository:
def __init__(self, db: Session):
self.db = db
def get_notification(self, id: NotificationId) -> NotificationSchema:
result= self.db.get(Notification, id)
if not result:
raise NotFoundError
return NotificationSchema.model_validate(result)
def get_unread_notifications(self) -> list[NotificationSchema]:
try:
stmt = select(Notification).where(Notification.read == False).order_by(Notification.timestamp.desc())
results = self.db.execute(stmt).scalars().all()
log.info(f"Successfully retrieved {len(results)} unread notifications.")
return [NotificationSchema.model_validate(notification) for notification in results]
except SQLAlchemyError as e:
log.error(f"Database error while retrieving unread notifications: {e}")
raise
def get_all_notifications(self) -> list[NotificationSchema]:
try:
stmt = select(Notification).order_by(Notification.timestamp.desc())
results = self.db.execute(stmt).scalars().all()
log.info(f"Successfully retrieved {len(results)} notifications.")
return [NotificationSchema.model_validate(notification) for notification in results]
except SQLAlchemyError as e:
log.error(f"Database error while retrieving notifications: {e}")
raise
def save_notification(self, notification: NotificationSchema):
try:
self.db.add(notification)
self.db.commit()
except IntegrityError as e:
log.error(f"Could not save notification, Error: {e}")
raise MediaAlreadyExists(f"Notification with id {notification.id} already exists.")
return
def mark_notification_as_read(self, id: NotificationId) -> None:
stmt = update(Notification).where(Notification.id == id).values(read=True)
self.db.execute(stmt)
return
def mark_notification_as_unread(self, id: NotificationId) -> None:
stmt = update(Notification).where(Notification.id == id).values(read=False)
self.db.execute(stmt)
return
def delete_notification(self, id: NotificationId) -> None:
stmt = delete(Notification).where(Notification.id == id)
result = self.db.execute(stmt)
if result.rowcount == 0:
log.warning(f"Notification with id {id} not found for deletion.")
raise NotFoundError(f"Notification with id {id} not found.")
self.db.commit()
log.info(f"Successfully deleted notification with id: {id}")
return

View File

@@ -0,0 +1,109 @@
from fastapi import APIRouter, Depends, status
from media_manager.auth.users import current_active_user
from media_manager.notification.schemas import Notification, NotificationId
from media_manager.notification.dependencies import notification_service_dep
router = APIRouter()
# --------------------------------
# GET NOTIFICATIONS
# --------------------------------
@router.get(
"",
dependencies=[Depends(current_active_user)],
response_model=list[Notification],
)
def get_all_notifications(notification_service: notification_service_dep):
"""
Get all notifications.
"""
return notification_service.get_all_notifications()
@router.get(
"/unread",
dependencies=[Depends(current_active_user)],
response_model=list[Notification],
)
def get_unread_notifications(notification_service: notification_service_dep):
"""
Get all unread notifications.
"""
return notification_service.get_unread_notifications()
@router.get(
"/{notification_id}",
dependencies=[Depends(current_active_user)],
response_model=Notification,
responses={
status.HTTP_404_NOT_FOUND: {"description": "Notification not found"},
},
)
def get_notification(
notification_id: NotificationId, notification_service: notification_service_dep
):
"""
Get a specific notification by ID.
"""
return notification_service.get_notification(id=notification_id)
# --------------------------------
# MANAGE NOTIFICATIONS
# --------------------------------
@router.patch(
"/{notification_id}/read",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(current_active_user)],
responses={
status.HTTP_404_NOT_FOUND: {"description": "Notification not found"},
},
)
def mark_notification_as_read(
notification_id: NotificationId, notification_service: notification_service_dep
):
"""
Mark a notification as read.
"""
notification_service.mark_notification_as_read(id=notification_id)
@router.patch(
"/{notification_id}/unread",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(current_active_user)],
responses={
status.HTTP_404_NOT_FOUND: {"description": "Notification not found"},
},
)
def mark_notification_as_unread(
notification_id: NotificationId, notification_service: notification_service_dep
):
"""
Mark a notification as unread.
"""
notification_service.mark_notification_as_unread(id=notification_id)
@router.delete(
"/{notification_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(current_active_user)],
responses={
status.HTTP_404_NOT_FOUND: {"description": "Notification not found"},
},
)
def delete_notification(
notification_id: NotificationId, notification_service: notification_service_dep
):
"""
Delete a notification.
"""
notification_service.delete_notification(id=notification_id)

View File

@@ -0,0 +1,23 @@
import typing
import uuid
from datetime import datetime
from uuid import UUID
from pydantic import BaseModel, Field, ConfigDict, model_validator
from media_manager.auth.schemas import UserRead
from media_manager.torrent.models import Quality
from media_manager.torrent.schemas import TorrentId, TorrentStatus
MovieId = typing.NewType("MovieId", UUID)
MovieRequestId = typing.NewType("MovieRequestId", UUID)
NotificationId = typing.NewType("NotificationId", UUID)
class Notification(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: NotificationId = Field(default_factory=uuid.uuid4, description="Unique identifier for the notification")
read: bool = Field(False, description="Whether the notification has been read")
message: str = Field(description="The content of the notification")
timestamp: datetime = Field(default_factory=datetime.now, description="The timestamp of the notification")

View File

@@ -0,0 +1,70 @@
import re
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from media_manager.exceptions import InvalidConfigError
from media_manager.indexer.repository import IndexerRepository
from media_manager.database import SessionLocal
from media_manager.indexer.schemas import IndexerQueryResult
from media_manager.indexer.schemas import IndexerQueryResultId
from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult
from media_manager.notification.repository import NotificationRepository
from media_manager.notification.schemas import NotificationId, Notification
from media_manager.torrent.schemas import Torrent, TorrentStatus
from media_manager.torrent.service import TorrentService
from media_manager.movies import log
from media_manager.movies.schemas import (
Movie,
MovieId,
MovieRequest,
MovieFile,
RichMovieTorrent,
PublicMovie,
PublicMovieFile,
MovieRequestId,
RichMovieRequest,
)
from media_manager.torrent.schemas import QualityStrings
from media_manager.movies.repository import MovieRepository
from media_manager.exceptions import NotFoundError
import pprint
from media_manager.config import BasicConfig
from media_manager.torrent.repository import TorrentRepository
from media_manager.torrent.utils import import_file, import_torrent
from media_manager.indexer.service import IndexerService
from media_manager.metadataProvider.abstractMetaDataProvider import (
AbstractMetadataProvider,
)
from media_manager.metadataProvider.tmdb import TmdbMetadataProvider
from media_manager.metadataProvider.tvdb import TvdbMetadataProvider
class NotificationService:
def __init__(
self,
notification_repository: NotificationRepository,
):
self.notification_repository = notification_repository
def get_notification(self, id: NotificationId) -> Notification:
return self.notification_repository.get_notification(id=id)
def get_unread_notifications(self) -> list[Notification]:
return self.notification_repository.get_unread_notifications()
def get_all_notifications(self) -> list[Notification]:
return self.notification_repository.get_all_notifications()
def save_notification(self, notification: Notification) -> None:
return self.notification_repository.save_notification(notification)
def mark_notification_as_read(self, id: NotificationId) -> None:
return self.notification_repository.mark_notification_as_read(id=id)
def mark_notification_as_unread(self, id: NotificationId) -> None:
return self.notification_repository.mark_notification_as_unread(id=id)
def delete_notification(self, id: NotificationId) -> None:
return self.notification_repository.delete_notification(id=id)

View File

@@ -0,0 +1,14 @@
import abc
class AbstractNotificationServiceProvider(abc.ABC):
@abc.abstractmethod
def send_notification(self, message: str) -> bool:
"""
Sends a notification with the given message.
:param message: The message to send in the notification.
:return: True if the notification was sent successfully, False otherwise.
"""
pass