mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-17 15:43:28 +02:00
add notificaton module
This commit is contained in:
@@ -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),
|
||||
|
||||
0
media_manager/notification/__init__.py
Normal file
0
media_manager/notification/__init__.py
Normal file
32
media_manager/notification/dependencies.py
Normal file
32
media_manager/notification/dependencies.py
Normal 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)
|
||||
]
|
||||
|
||||
19
media_manager/notification/models.py
Normal file
19
media_manager/notification/models.py
Normal 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)
|
||||
|
||||
75
media_manager/notification/repository.py
Normal file
75
media_manager/notification/repository.py
Normal 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
|
||||
109
media_manager/notification/router.py
Normal file
109
media_manager/notification/router.py
Normal 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)
|
||||
23
media_manager/notification/schemas.py
Normal file
23
media_manager/notification/schemas.py
Normal 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")
|
||||
70
media_manager/notification/service.py
Normal file
70
media_manager/notification/service.py
Normal 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user