mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-17 15:43:28 +02:00
remove everything related to requests (#455)
This PR removes the requests feature. The functionality will be replaced either by Seerr or by reimplementing it in a better way.
This commit is contained in:
committed by
GitHub
parent
c2645000e5
commit
a643c9426d
@@ -3,7 +3,6 @@ from uuid import UUID
|
||||
from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint
|
||||
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
|
||||
|
||||
@@ -22,10 +21,6 @@ class Movie(Base):
|
||||
original_language: Mapped[str | None] = mapped_column(default=None)
|
||||
imdb_id: Mapped[str | None] = mapped_column(default=None)
|
||||
|
||||
movie_requests: Mapped[list["MovieRequest"]] = relationship(
|
||||
"MovieRequest", back_populates="movie", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class MovieFile(Base):
|
||||
__tablename__ = "movie_file"
|
||||
@@ -42,31 +37,3 @@ class MovieFile(Base):
|
||||
)
|
||||
|
||||
torrent = relationship("Torrent", back_populates="movie_files", uselist=False)
|
||||
|
||||
|
||||
class MovieRequest(Base):
|
||||
__tablename__ = "movie_request"
|
||||
__table_args__ = (UniqueConstraint("movie_id", "wanted_quality"),)
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True)
|
||||
movie_id: Mapped[UUID] = mapped_column(
|
||||
ForeignKey(column="movie.id", ondelete="CASCADE"),
|
||||
)
|
||||
wanted_quality: Mapped[Quality]
|
||||
min_quality: Mapped[Quality]
|
||||
|
||||
authorized: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
requested_by_id: Mapped[UUID | None] = mapped_column(
|
||||
ForeignKey(column="user.id", ondelete="SET NULL"),
|
||||
)
|
||||
authorized_by_id: Mapped[UUID | None] = mapped_column(
|
||||
ForeignKey(column="user.id", ondelete="SET NULL"),
|
||||
)
|
||||
|
||||
requested_by: Mapped["User|None"] = relationship(
|
||||
foreign_keys=[requested_by_id], uselist=False
|
||||
)
|
||||
authorized_by: Mapped["User|None"] = relationship(
|
||||
foreign_keys=[authorized_by_id], uselist=False
|
||||
)
|
||||
movie = relationship("Movie", back_populates="movie_requests", uselist=False)
|
||||
|
||||
@@ -5,10 +5,10 @@ from sqlalchemy.exc import (
|
||||
IntegrityError,
|
||||
SQLAlchemyError,
|
||||
)
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from media_manager.exceptions import ConflictError, NotFoundError
|
||||
from media_manager.movies.models import Movie, MovieFile, MovieRequest
|
||||
from media_manager.movies.models import Movie, MovieFile
|
||||
from media_manager.movies.schemas import (
|
||||
Movie as MovieSchema,
|
||||
)
|
||||
@@ -17,17 +17,10 @@ from media_manager.movies.schemas import (
|
||||
)
|
||||
from media_manager.movies.schemas import (
|
||||
MovieId,
|
||||
MovieRequestId,
|
||||
)
|
||||
from media_manager.movies.schemas import (
|
||||
MovieRequest as MovieRequestSchema,
|
||||
)
|
||||
from media_manager.movies.schemas import (
|
||||
MovieTorrent as MovieTorrentSchema,
|
||||
)
|
||||
from media_manager.movies.schemas import (
|
||||
RichMovieRequest as RichMovieRequestSchema,
|
||||
)
|
||||
from media_manager.torrent.models import Torrent
|
||||
from media_manager.torrent.schemas import TorrentId
|
||||
|
||||
@@ -173,46 +166,6 @@ class MovieRepository:
|
||||
log.exception(f"Database error while deleting movie {movie_id}")
|
||||
raise
|
||||
|
||||
def add_movie_request(
|
||||
self, movie_request: MovieRequestSchema
|
||||
) -> MovieRequestSchema:
|
||||
"""
|
||||
Adds a Movie to the MovieRequest table, which marks it as requested.
|
||||
|
||||
:param movie_request: The MovieRequest object to add.
|
||||
:return: The added MovieRequest object.
|
||||
:raises IntegrityError: If a similar request already exists or violates constraints.
|
||||
:raises SQLAlchemyError: If a database error occurs.
|
||||
"""
|
||||
log.debug(f"Adding movie request: {movie_request.model_dump_json()}")
|
||||
db_model = MovieRequest(
|
||||
id=movie_request.id,
|
||||
movie_id=movie_request.movie_id,
|
||||
requested_by_id=movie_request.requested_by.id
|
||||
if movie_request.requested_by
|
||||
else None,
|
||||
authorized_by_id=movie_request.authorized_by.id
|
||||
if movie_request.authorized_by
|
||||
else None,
|
||||
wanted_quality=movie_request.wanted_quality,
|
||||
min_quality=movie_request.min_quality,
|
||||
authorized=movie_request.authorized,
|
||||
)
|
||||
try:
|
||||
self.db.add(db_model)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_model)
|
||||
log.info(f"Successfully added movie request with id: {db_model.id}")
|
||||
return MovieRequestSchema.model_validate(db_model)
|
||||
except IntegrityError:
|
||||
self.db.rollback()
|
||||
log.exception("Integrity error while adding movie request")
|
||||
raise
|
||||
except SQLAlchemyError:
|
||||
self.db.rollback()
|
||||
log.exception("Database error while adding movie request")
|
||||
raise
|
||||
|
||||
def set_movie_library(self, movie_id: MovieId, library: str) -> None:
|
||||
"""
|
||||
Sets the library for a movie.
|
||||
@@ -234,49 +187,6 @@ class MovieRepository:
|
||||
log.exception(f"Database error setting library for movie {movie_id}")
|
||||
raise
|
||||
|
||||
def delete_movie_request(self, movie_request_id: MovieRequestId) -> None:
|
||||
"""
|
||||
Removes a MovieRequest by its ID.
|
||||
|
||||
:param movie_request_id: The ID of the movie request to delete.
|
||||
:raises NotFoundError: If the movie request is not found.
|
||||
:raises SQLAlchemyError: If a database error occurs.
|
||||
"""
|
||||
try:
|
||||
stmt = delete(MovieRequest).where(MovieRequest.id == movie_request_id)
|
||||
result = self.db.execute(stmt)
|
||||
if result.rowcount == 0:
|
||||
self.db.rollback()
|
||||
msg = f"movie request with id {movie_request_id} not found."
|
||||
raise NotFoundError(msg)
|
||||
self.db.commit()
|
||||
# Successfully deleted movie request with id: {movie_request_id}
|
||||
except SQLAlchemyError:
|
||||
self.db.rollback()
|
||||
log.exception(
|
||||
f"Database error while deleting movie request {movie_request_id}"
|
||||
)
|
||||
raise
|
||||
|
||||
def get_movie_requests(self) -> list[RichMovieRequestSchema]:
|
||||
"""
|
||||
Retrieve all movie requests.
|
||||
|
||||
:return: A list of RichMovieRequest objects.
|
||||
:raises SQLAlchemyError: If a database error occurs.
|
||||
"""
|
||||
try:
|
||||
stmt = select(MovieRequest).options(
|
||||
joinedload(MovieRequest.requested_by),
|
||||
joinedload(MovieRequest.authorized_by),
|
||||
joinedload(MovieRequest.movie),
|
||||
)
|
||||
results = self.db.execute(stmt).scalars().unique().all()
|
||||
return [RichMovieRequestSchema.model_validate(x) for x in results]
|
||||
except SQLAlchemyError:
|
||||
log.exception("Database error while retrieving movie requests")
|
||||
raise
|
||||
|
||||
def add_movie_file(self, movie_file: MovieFileSchema) -> MovieFileSchema:
|
||||
"""
|
||||
Adds a movie file record to the database.
|
||||
@@ -396,25 +306,6 @@ class MovieRepository:
|
||||
log.exception("Database error retrieving all movies with torrents")
|
||||
raise
|
||||
|
||||
def get_movie_request(self, movie_request_id: MovieRequestId) -> MovieRequestSchema:
|
||||
"""
|
||||
Retrieve a movie request by its ID.
|
||||
|
||||
:param movie_request_id: The ID of the movie request.
|
||||
:return: A MovieRequest object.
|
||||
:raises NotFoundError: If the movie request is not found.
|
||||
:raises SQLAlchemyError: If a database error occurs.
|
||||
"""
|
||||
try:
|
||||
request = self.db.get(MovieRequest, movie_request_id)
|
||||
if not request:
|
||||
msg = f"Movie request with id {movie_request_id} not found."
|
||||
raise NotFoundError(msg)
|
||||
return MovieRequestSchema.model_validate(request)
|
||||
except SQLAlchemyError:
|
||||
log.exception(f"Database error retrieving movie request {movie_request_id}")
|
||||
raise
|
||||
|
||||
def get_movie_by_torrent_id(self, torrent_id: TorrentId) -> MovieSchema:
|
||||
"""
|
||||
Retrieve a movie by a torrent ID.
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from media_manager.auth.schemas import UserRead
|
||||
from media_manager.auth.users import current_active_user, current_superuser
|
||||
from media_manager.config import LibraryItem, MediaManagerConfig
|
||||
from media_manager.exceptions import ConflictError, NotFoundError
|
||||
@@ -13,20 +11,14 @@ from media_manager.indexer.schemas import (
|
||||
)
|
||||
from media_manager.metadataProvider.dependencies import metadata_provider_dep
|
||||
from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult
|
||||
from media_manager.movies import log
|
||||
from media_manager.movies.dependencies import (
|
||||
movie_dep,
|
||||
movie_service_dep,
|
||||
)
|
||||
from media_manager.movies.schemas import (
|
||||
CreateMovieRequest,
|
||||
Movie,
|
||||
MovieRequest,
|
||||
MovieRequestBase,
|
||||
MovieRequestId,
|
||||
PublicMovie,
|
||||
PublicMovieFile,
|
||||
RichMovieRequest,
|
||||
RichMovieTorrent,
|
||||
)
|
||||
from media_manager.schemas import MediaImportSuggestion
|
||||
@@ -188,103 +180,6 @@ def get_available_libraries() -> list[LibraryItem]:
|
||||
return MediaManagerConfig().misc.movie_libraries
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MOVIE REQUESTS
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get(
|
||||
"/requests",
|
||||
dependencies=[Depends(current_active_user)],
|
||||
)
|
||||
def get_all_movie_requests(movie_service: movie_service_dep) -> list[RichMovieRequest]:
|
||||
"""
|
||||
Get all movie requests.
|
||||
"""
|
||||
return movie_service.get_all_movie_requests()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/requests",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def create_movie_request(
|
||||
movie_service: movie_service_dep,
|
||||
movie_request: CreateMovieRequest,
|
||||
user: Annotated[UserRead, Depends(current_active_user)],
|
||||
) -> MovieRequest:
|
||||
"""
|
||||
Create a new movie request.
|
||||
"""
|
||||
log.info(
|
||||
f"User {user.email} is creating a movie request for {movie_request.movie_id}"
|
||||
)
|
||||
movie_request: MovieRequest = MovieRequest.model_validate(movie_request)
|
||||
movie_request.requested_by = user
|
||||
if user.is_superuser:
|
||||
movie_request.authorized = True
|
||||
movie_request.authorized_by = user
|
||||
|
||||
return movie_service.add_movie_request(movie_request=movie_request)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/requests/{movie_request_id}",
|
||||
)
|
||||
def update_movie_request(
|
||||
movie_service: movie_service_dep,
|
||||
movie_request_id: MovieRequestId,
|
||||
update_movie_request: MovieRequestBase,
|
||||
user: Annotated[UserRead, Depends(current_active_user)],
|
||||
) -> MovieRequest:
|
||||
"""
|
||||
Update an existing movie request.
|
||||
"""
|
||||
movie_request = movie_service.get_movie_request_by_id(
|
||||
movie_request_id=movie_request_id
|
||||
)
|
||||
if movie_request.requested_by.id != user.id or user.is_superuser:
|
||||
movie_request.min_quality = update_movie_request.min_quality
|
||||
movie_request.wanted_quality = update_movie_request.wanted_quality
|
||||
|
||||
return movie_service.update_movie_request(movie_request=movie_request)
|
||||
|
||||
|
||||
@router.patch("/requests/{movie_request_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def authorize_request(
|
||||
movie_service: movie_service_dep,
|
||||
movie_request_id: MovieRequestId,
|
||||
user: Annotated[UserRead, Depends(current_superuser)],
|
||||
authorized_status: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Authorize or de-authorize a movie request.
|
||||
"""
|
||||
movie_request = movie_service.get_movie_request_by_id(
|
||||
movie_request_id=movie_request_id
|
||||
)
|
||||
movie_request.authorized = authorized_status
|
||||
if authorized_status:
|
||||
movie_request.authorized_by = user
|
||||
else:
|
||||
movie_request.authorized_by = None
|
||||
movie_service.update_movie_request(movie_request=movie_request)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/requests/{movie_request_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
dependencies=[Depends(current_superuser)],
|
||||
)
|
||||
def delete_movie_request(
|
||||
movie_service: movie_service_dep, movie_request_id: MovieRequestId
|
||||
) -> None:
|
||||
"""
|
||||
Delete a movie request.
|
||||
"""
|
||||
movie_service.delete_movie_request(movie_request_id=movie_request_id)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# MOVIES - SINGLE RESOURCE
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -2,14 +2,12 @@ import typing
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class Movie(BaseModel):
|
||||
@@ -40,38 +38,6 @@ class PublicMovieFile(MovieFile):
|
||||
imported: bool = False
|
||||
|
||||
|
||||
class MovieRequestBase(BaseModel):
|
||||
min_quality: Quality
|
||||
wanted_quality: Quality
|
||||
|
||||
@model_validator(mode="after")
|
||||
def ensure_wanted_quality_is_eq_or_gt_min_quality(self) -> "MovieRequestBase":
|
||||
if self.min_quality.value < self.wanted_quality.value:
|
||||
msg = "wanted_quality must be equal to or lower than minimum_quality."
|
||||
raise ValueError(msg)
|
||||
return self
|
||||
|
||||
|
||||
class CreateMovieRequest(MovieRequestBase):
|
||||
movie_id: MovieId
|
||||
|
||||
|
||||
class MovieRequest(MovieRequestBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: MovieRequestId = Field(default_factory=lambda: MovieRequestId(uuid.uuid4()))
|
||||
|
||||
movie_id: MovieId
|
||||
|
||||
requested_by: UserRead | None = None
|
||||
authorized: bool = False
|
||||
authorized_by: UserRead | None = None
|
||||
|
||||
|
||||
class RichMovieRequest(MovieRequest):
|
||||
movie: Movie
|
||||
|
||||
|
||||
class MovieTorrent(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@ from pathlib import Path
|
||||
from typing import overload
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from media_manager.config import MediaManagerConfig
|
||||
from media_manager.database import SessionLocal, get_session
|
||||
from media_manager.database import get_session
|
||||
from media_manager.exceptions import InvalidConfigError, NotFoundError, RenameError
|
||||
from media_manager.indexer.repository import IndexerRepository
|
||||
from media_manager.indexer.schemas import IndexerQueryResult, IndexerQueryResultId
|
||||
@@ -25,11 +24,8 @@ from media_manager.movies.schemas import (
|
||||
Movie,
|
||||
MovieFile,
|
||||
MovieId,
|
||||
MovieRequest,
|
||||
MovieRequestId,
|
||||
PublicMovie,
|
||||
PublicMovieFile,
|
||||
RichMovieRequest,
|
||||
RichMovieTorrent,
|
||||
)
|
||||
from media_manager.notification.repository import NotificationRepository
|
||||
@@ -38,7 +34,6 @@ from media_manager.schemas import MediaImportSuggestion
|
||||
from media_manager.torrent.repository import TorrentRepository
|
||||
from media_manager.torrent.schemas import (
|
||||
Quality,
|
||||
QualityStrings,
|
||||
Torrent,
|
||||
TorrentStatus,
|
||||
)
|
||||
@@ -89,44 +84,6 @@ class MovieService:
|
||||
metadata_provider.download_movie_poster_image(movie=saved_movie)
|
||||
return saved_movie
|
||||
|
||||
def add_movie_request(self, movie_request: MovieRequest) -> MovieRequest:
|
||||
"""
|
||||
Add a new movie request.
|
||||
|
||||
:param movie_request: The movie request to add.
|
||||
:return: The added movie request.
|
||||
"""
|
||||
return self.movie_repository.add_movie_request(movie_request=movie_request)
|
||||
|
||||
def get_movie_request_by_id(self, movie_request_id: MovieRequestId) -> MovieRequest:
|
||||
"""
|
||||
Get a movie request by its ID.
|
||||
|
||||
:param movie_request_id: The ID of the movie request.
|
||||
:return: The movie request or None if not found.
|
||||
"""
|
||||
return self.movie_repository.get_movie_request(
|
||||
movie_request_id=movie_request_id
|
||||
)
|
||||
|
||||
def update_movie_request(self, movie_request: MovieRequest) -> MovieRequest:
|
||||
"""
|
||||
Update an existing movie request.
|
||||
|
||||
:param movie_request: The movie request to update.
|
||||
:return: The updated movie request.
|
||||
"""
|
||||
self.movie_repository.delete_movie_request(movie_request_id=movie_request.id)
|
||||
return self.movie_repository.add_movie_request(movie_request=movie_request)
|
||||
|
||||
def delete_movie_request(self, movie_request_id: MovieRequestId) -> None:
|
||||
"""
|
||||
Delete a movie request by its ID.
|
||||
|
||||
:param movie_request_id: The ID of the movie request to delete.
|
||||
"""
|
||||
self.movie_repository.delete_movie_request(movie_request_id=movie_request_id)
|
||||
|
||||
def delete_movie(
|
||||
self,
|
||||
movie: Movie,
|
||||
@@ -391,14 +348,6 @@ class MovieService:
|
||||
external_id=external_id, metadata_provider=metadata_provider
|
||||
)
|
||||
|
||||
def get_all_movie_requests(self) -> list[RichMovieRequest]:
|
||||
"""
|
||||
Get all movie requests.
|
||||
|
||||
:return: A list of rich movie requests.
|
||||
"""
|
||||
return self.movie_repository.get_movie_requests()
|
||||
|
||||
def set_movie_library(self, movie: Movie, library: str) -> None:
|
||||
self.movie_repository.set_movie_library(movie_id=movie.id, library=library)
|
||||
|
||||
@@ -471,65 +420,6 @@ class MovieService:
|
||||
self.torrent_service.resume_download(torrent=movie_torrent)
|
||||
return movie_torrent
|
||||
|
||||
def download_approved_movie_request(
|
||||
self, movie_request: MovieRequest, movie: Movie
|
||||
) -> bool:
|
||||
"""
|
||||
Download an approved movie request.
|
||||
|
||||
:param movie_request: The movie request to download.
|
||||
:param movie: The Movie object.
|
||||
:return: True if the download was successful, False otherwise.
|
||||
:raises ValueError: If the movie request is not authorized.
|
||||
"""
|
||||
if not movie_request.authorized:
|
||||
msg = "Movie request is not authorized"
|
||||
raise ValueError(msg)
|
||||
|
||||
log.info(f"Downloading approved movie request {movie_request.id}")
|
||||
|
||||
torrents = self.get_all_available_torrents_for_movie(movie=movie)
|
||||
available_torrents: list[IndexerQueryResult] = []
|
||||
|
||||
for torrent in torrents:
|
||||
if (
|
||||
(torrent.quality.value < movie_request.wanted_quality.value)
|
||||
or (torrent.quality.value > movie_request.min_quality.value)
|
||||
or (torrent.seeders < 3)
|
||||
):
|
||||
log.debug(
|
||||
f"Skipping torrent {torrent.title} with quality {torrent.quality} for movie {movie.id}, because it does not match the requested quality {movie_request.wanted_quality}"
|
||||
)
|
||||
else:
|
||||
available_torrents.append(torrent)
|
||||
log.debug(
|
||||
f"Taking torrent {torrent.title} with quality {torrent.quality} for movie {movie.id} into consideration"
|
||||
)
|
||||
|
||||
if len(available_torrents) == 0:
|
||||
log.warning(
|
||||
f"No torrents found for movie request {movie_request.id} with quality between {QualityStrings[movie_request.min_quality.name]} and {QualityStrings[movie_request.wanted_quality.name]}"
|
||||
)
|
||||
return False
|
||||
|
||||
available_torrents.sort()
|
||||
|
||||
torrent = self.torrent_service.download(indexer_result=available_torrents[0])
|
||||
movie_file = MovieFile(
|
||||
movie_id=movie.id,
|
||||
quality=torrent.quality,
|
||||
torrent_id=torrent.id,
|
||||
file_path_suffix=QualityStrings[torrent.quality.name].value.upper(),
|
||||
)
|
||||
try:
|
||||
self.movie_repository.add_movie_file(movie_file=movie_file)
|
||||
except IntegrityError:
|
||||
log.warning(
|
||||
f"Movie file for movie {movie.name} and torrent {torrent.title} already exists"
|
||||
)
|
||||
self.delete_movie_request(movie_request.id)
|
||||
return True
|
||||
|
||||
def get_movie_root_path(self, movie: Movie) -> Path:
|
||||
misc_config = MediaManagerConfig().misc
|
||||
movie_file_path = (
|
||||
@@ -774,47 +664,6 @@ class MovieService:
|
||||
return importable_movies
|
||||
|
||||
|
||||
def auto_download_all_approved_movie_requests() -> None:
|
||||
"""
|
||||
Auto download all approved movie requests.
|
||||
This is a standalone function as it creates its own DB session.
|
||||
"""
|
||||
db: Session = SessionLocal() if SessionLocal else next(get_session())
|
||||
movie_repository = MovieRepository(db=db)
|
||||
torrent_service = TorrentService(torrent_repository=TorrentRepository(db=db))
|
||||
indexer_service = IndexerService(indexer_repository=IndexerRepository(db=db))
|
||||
notification_service = NotificationService(
|
||||
notification_repository=NotificationRepository(db=db)
|
||||
)
|
||||
movie_service = MovieService(
|
||||
movie_repository=movie_repository,
|
||||
torrent_service=torrent_service,
|
||||
indexer_service=indexer_service,
|
||||
notification_service=notification_service,
|
||||
)
|
||||
|
||||
log.info("Auto downloading all approved movie requests")
|
||||
movie_requests = movie_repository.get_movie_requests()
|
||||
log.info(f"Found {len(movie_requests)} movie requests to process")
|
||||
count = 0
|
||||
|
||||
for movie_request in movie_requests:
|
||||
if movie_request.authorized:
|
||||
movie = movie_repository.get_movie_by_id(movie_id=movie_request.movie_id)
|
||||
if movie_service.download_approved_movie_request(
|
||||
movie_request=movie_request, movie=movie
|
||||
):
|
||||
count += 1
|
||||
else:
|
||||
log.info(
|
||||
f"Could not download movie request {movie_request.id} for movie {movie.name}"
|
||||
)
|
||||
|
||||
log.info(f"Auto downloaded {count} approved movie requests")
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
|
||||
def import_all_movie_torrents() -> None:
|
||||
with next(get_session()) as db:
|
||||
movie_repository = MovieRepository(db=db)
|
||||
|
||||
@@ -5,12 +5,10 @@ from apscheduler.triggers.cron import CronTrigger
|
||||
import media_manager.database
|
||||
from media_manager.config import MediaManagerConfig
|
||||
from media_manager.movies.service import (
|
||||
auto_download_all_approved_movie_requests,
|
||||
import_all_movie_torrents,
|
||||
update_all_movies_metadata,
|
||||
)
|
||||
from media_manager.tv.service import (
|
||||
auto_download_all_approved_season_requests,
|
||||
import_all_show_torrents,
|
||||
update_all_non_ended_shows_metadata,
|
||||
)
|
||||
@@ -23,7 +21,6 @@ def setup_scheduler(config: MediaManagerConfig) -> BackgroundScheduler:
|
||||
jobstores = {"default": SQLAlchemyJobStore(engine=media_manager.database.engine)}
|
||||
scheduler = BackgroundScheduler(jobstores=jobstores)
|
||||
every_15_minutes_trigger = CronTrigger(minute="*/15", hour="*")
|
||||
daily_trigger = CronTrigger(hour=0, minute=0, jitter=60 * 60 * 24 * 2)
|
||||
weekly_trigger = CronTrigger(
|
||||
day_of_week="mon", hour=0, minute=0, jitter=60 * 60 * 24 * 2
|
||||
)
|
||||
@@ -39,18 +36,6 @@ def setup_scheduler(config: MediaManagerConfig) -> BackgroundScheduler:
|
||||
id="import_all_show_torrents",
|
||||
replace_existing=True,
|
||||
)
|
||||
scheduler.add_job(
|
||||
auto_download_all_approved_season_requests,
|
||||
daily_trigger,
|
||||
id="auto_download_all_approved_season_requests",
|
||||
replace_existing=True,
|
||||
)
|
||||
scheduler.add_job(
|
||||
auto_download_all_approved_movie_requests,
|
||||
daily_trigger,
|
||||
id="auto_download_all_approved_movie_requests",
|
||||
replace_existing=True,
|
||||
)
|
||||
scheduler.add_job(
|
||||
update_all_movies_metadata,
|
||||
weekly_trigger,
|
||||
|
||||
@@ -3,7 +3,6 @@ from uuid import UUID
|
||||
from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint
|
||||
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
|
||||
|
||||
@@ -48,10 +47,6 @@ class Season(Base):
|
||||
back_populates="season", cascade="all, delete"
|
||||
)
|
||||
|
||||
season_requests = relationship(
|
||||
"SeasonRequest", back_populates="season", cascade="all, delete"
|
||||
)
|
||||
|
||||
|
||||
class Episode(Base):
|
||||
__tablename__ = "episode"
|
||||
@@ -85,29 +80,3 @@ class EpisodeFile(Base):
|
||||
|
||||
torrent = relationship("Torrent", back_populates="episode_files", uselist=False)
|
||||
episode = relationship("Episode", back_populates="episode_files", uselist=False)
|
||||
|
||||
|
||||
class SeasonRequest(Base):
|
||||
__tablename__ = "season_request"
|
||||
__table_args__ = (UniqueConstraint("season_id", "wanted_quality"),)
|
||||
id: Mapped[UUID] = mapped_column(primary_key=True)
|
||||
season_id: Mapped[UUID] = mapped_column(
|
||||
ForeignKey(column="season.id", ondelete="CASCADE"),
|
||||
)
|
||||
wanted_quality: Mapped[Quality]
|
||||
min_quality: Mapped[Quality]
|
||||
requested_by_id: Mapped[UUID | None] = mapped_column(
|
||||
ForeignKey(column="user.id", ondelete="SET NULL"),
|
||||
)
|
||||
authorized: Mapped[bool] = mapped_column(default=False)
|
||||
authorized_by_id: Mapped[UUID | None] = mapped_column(
|
||||
ForeignKey(column="user.id", ondelete="SET NULL"),
|
||||
)
|
||||
|
||||
requested_by: Mapped["User|None"] = relationship(
|
||||
foreign_keys=[requested_by_id], uselist=False
|
||||
)
|
||||
authorized_by: Mapped["User|None"] = relationship(
|
||||
foreign_keys=[authorized_by_id], uselist=False
|
||||
)
|
||||
season = relationship("Season", back_populates="season_requests", uselist=False)
|
||||
|
||||
@@ -7,7 +7,7 @@ from media_manager.torrent.models import Torrent
|
||||
from media_manager.torrent.schemas import Torrent as TorrentSchema
|
||||
from media_manager.torrent.schemas import TorrentId
|
||||
from media_manager.tv import log
|
||||
from media_manager.tv.models import Episode, EpisodeFile, Season, SeasonRequest, Show
|
||||
from media_manager.tv.models import Episode, EpisodeFile, Season, Show
|
||||
from media_manager.tv.schemas import Episode as EpisodeSchema
|
||||
from media_manager.tv.schemas import EpisodeFile as EpisodeFileSchema
|
||||
from media_manager.tv.schemas import (
|
||||
@@ -15,12 +15,9 @@ from media_manager.tv.schemas import (
|
||||
EpisodeNumber,
|
||||
SeasonId,
|
||||
SeasonNumber,
|
||||
SeasonRequestId,
|
||||
ShowId,
|
||||
)
|
||||
from media_manager.tv.schemas import RichSeasonRequest as RichSeasonRequestSchema
|
||||
from media_manager.tv.schemas import Season as SeasonSchema
|
||||
from media_manager.tv.schemas import SeasonRequest as SeasonRequestSchema
|
||||
from media_manager.tv.schemas import Show as ShowSchema
|
||||
|
||||
|
||||
@@ -256,67 +253,6 @@ class TvRepository:
|
||||
)
|
||||
raise
|
||||
|
||||
def add_season_request(
|
||||
self, season_request: SeasonRequestSchema
|
||||
) -> SeasonRequestSchema:
|
||||
"""
|
||||
Adds a Season to the SeasonRequest table, which marks it as requested.
|
||||
|
||||
:param season_request: The SeasonRequest object to add.
|
||||
:return: The added SeasonRequest object.
|
||||
:raises IntegrityError: If a similar request already exists or violates constraints.
|
||||
:raises SQLAlchemyError: If a database error occurs.
|
||||
"""
|
||||
db_model = SeasonRequest(
|
||||
id=season_request.id,
|
||||
season_id=season_request.season_id,
|
||||
wanted_quality=season_request.wanted_quality,
|
||||
min_quality=season_request.min_quality,
|
||||
requested_by_id=season_request.requested_by.id
|
||||
if season_request.requested_by
|
||||
else None,
|
||||
authorized=season_request.authorized,
|
||||
authorized_by_id=season_request.authorized_by.id
|
||||
if season_request.authorized_by
|
||||
else None,
|
||||
)
|
||||
try:
|
||||
self.db.add(db_model)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_model)
|
||||
return SeasonRequestSchema.model_validate(db_model)
|
||||
except IntegrityError:
|
||||
self.db.rollback()
|
||||
log.exception("Integrity error while adding season request")
|
||||
raise
|
||||
except SQLAlchemyError:
|
||||
self.db.rollback()
|
||||
log.exception("Database error while adding season request")
|
||||
raise
|
||||
|
||||
def delete_season_request(self, season_request_id: SeasonRequestId) -> None:
|
||||
"""
|
||||
Removes a SeasonRequest by its ID.
|
||||
|
||||
:param season_request_id: The ID of the season request to delete.
|
||||
:raises NotFoundError: If the season request is not found.
|
||||
:raises SQLAlchemyError: If a database error occurs.
|
||||
"""
|
||||
try:
|
||||
stmt = delete(SeasonRequest).where(SeasonRequest.id == season_request_id)
|
||||
result = self.db.execute(stmt)
|
||||
if result.rowcount == 0:
|
||||
self.db.rollback()
|
||||
msg = f"SeasonRequest with id {season_request_id} not found."
|
||||
raise NotFoundError(msg)
|
||||
self.db.commit()
|
||||
except SQLAlchemyError:
|
||||
self.db.rollback()
|
||||
log.exception(
|
||||
f"Database error while deleting season request {season_request_id}"
|
||||
)
|
||||
raise
|
||||
|
||||
def get_season_by_number(self, season_number: int, show_id: ShowId) -> SeasonSchema:
|
||||
"""
|
||||
Retrieve a season by its number and show ID.
|
||||
@@ -345,38 +281,6 @@ class TvRepository:
|
||||
)
|
||||
raise
|
||||
|
||||
def get_season_requests(self) -> list[RichSeasonRequestSchema]:
|
||||
"""
|
||||
Retrieve all season requests.
|
||||
|
||||
:return: A list of RichSeasonRequest objects.
|
||||
:raises SQLAlchemyError: If a database error occurs.
|
||||
"""
|
||||
try:
|
||||
stmt = select(SeasonRequest).options(
|
||||
joinedload(SeasonRequest.requested_by),
|
||||
joinedload(SeasonRequest.authorized_by),
|
||||
joinedload(SeasonRequest.season).joinedload(Season.show),
|
||||
)
|
||||
results = self.db.execute(stmt).scalars().unique().all()
|
||||
return [
|
||||
RichSeasonRequestSchema(
|
||||
id=SeasonRequestId(x.id),
|
||||
min_quality=x.min_quality,
|
||||
wanted_quality=x.wanted_quality,
|
||||
season_id=SeasonId(x.season_id),
|
||||
show=x.season.show,
|
||||
season=x.season,
|
||||
requested_by=x.requested_by,
|
||||
authorized_by=x.authorized_by,
|
||||
authorized=x.authorized,
|
||||
)
|
||||
for x in results
|
||||
]
|
||||
except SQLAlchemyError:
|
||||
log.exception("Database error while retrieving season requests")
|
||||
raise
|
||||
|
||||
def add_episode_file(self, episode_file: EpisodeFileSchema) -> EpisodeFileSchema:
|
||||
"""
|
||||
Adds an episode file record to the database.
|
||||
@@ -581,30 +485,6 @@ class TvRepository:
|
||||
)
|
||||
raise
|
||||
|
||||
def get_season_request(
|
||||
self, season_request_id: SeasonRequestId
|
||||
) -> SeasonRequestSchema:
|
||||
"""
|
||||
Retrieve a season request by its ID.
|
||||
|
||||
:param season_request_id: The ID of the season request.
|
||||
:return: A SeasonRequest object.
|
||||
:raises NotFoundError: If the season request is not found.
|
||||
:raises SQLAlchemyError: If a database error occurs.
|
||||
"""
|
||||
try:
|
||||
request = self.db.get(SeasonRequest, season_request_id)
|
||||
if not request:
|
||||
log.warning(f"Season request with id {season_request_id} not found.")
|
||||
msg = f"Season request with id {season_request_id} not found."
|
||||
raise NotFoundError(msg)
|
||||
return SeasonRequestSchema.model_validate(request)
|
||||
except SQLAlchemyError:
|
||||
log.exception(
|
||||
f"Database error retrieving season request {season_request_id}"
|
||||
)
|
||||
raise
|
||||
|
||||
def get_show_by_season_id(self, season_id: SeasonId) -> ShowSchema:
|
||||
"""
|
||||
Retrieve a show by one of its season's ID.
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from media_manager.auth.db import User
|
||||
from media_manager.auth.schemas import UserRead
|
||||
from media_manager.auth.users import current_active_user, current_superuser
|
||||
from media_manager.config import LibraryItem, MediaManagerConfig
|
||||
from media_manager.exceptions import MediaAlreadyExistsError, NotFoundError
|
||||
@@ -17,24 +14,18 @@ from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult
|
||||
from media_manager.schemas import MediaImportSuggestion
|
||||
from media_manager.torrent.schemas import Torrent
|
||||
from media_manager.torrent.utils import get_importable_media_directories
|
||||
from media_manager.tv import log
|
||||
from media_manager.tv.dependencies import (
|
||||
season_dep,
|
||||
show_dep,
|
||||
tv_service_dep,
|
||||
)
|
||||
from media_manager.tv.schemas import (
|
||||
CreateSeasonRequest,
|
||||
PublicEpisodeFile,
|
||||
PublicShow,
|
||||
RichSeasonRequest,
|
||||
RichShowTorrent,
|
||||
Season,
|
||||
SeasonRequest,
|
||||
SeasonRequestId,
|
||||
Show,
|
||||
ShowId,
|
||||
UpdateSeasonRequest,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
@@ -278,110 +269,6 @@ def get_a_shows_torrents(show: show_dep, tv_service: tv_service_dep) -> RichShow
|
||||
return tv_service.get_torrents_for_show(show=show)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SEASONS - REQUESTS
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get(
|
||||
"/seasons/requests",
|
||||
status_code=status.HTTP_200_OK,
|
||||
dependencies=[Depends(current_active_user)],
|
||||
)
|
||||
def get_season_requests(tv_service: tv_service_dep) -> list[RichSeasonRequest]:
|
||||
"""
|
||||
Get all season requests.
|
||||
"""
|
||||
return tv_service.get_all_season_requests()
|
||||
|
||||
|
||||
@router.post("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def request_a_season(
|
||||
user: Annotated[User, Depends(current_active_user)],
|
||||
season_request: CreateSeasonRequest,
|
||||
tv_service: tv_service_dep,
|
||||
) -> None:
|
||||
"""
|
||||
Create a new season request.
|
||||
"""
|
||||
request: SeasonRequest = SeasonRequest.model_validate(season_request)
|
||||
request.requested_by = UserRead.model_validate(user)
|
||||
if user.is_superuser:
|
||||
request.authorized = True
|
||||
request.authorized_by = UserRead.model_validate(user)
|
||||
tv_service.add_season_request(request)
|
||||
return
|
||||
|
||||
|
||||
@router.put("/seasons/requests", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def update_request(
|
||||
tv_service: tv_service_dep,
|
||||
user: Annotated[User, Depends(current_active_user)],
|
||||
season_request: UpdateSeasonRequest,
|
||||
) -> None:
|
||||
"""
|
||||
Update an existing season request.
|
||||
"""
|
||||
updated_season_request: SeasonRequest = SeasonRequest.model_validate(season_request)
|
||||
request = tv_service.get_season_request_by_id(
|
||||
season_request_id=updated_season_request.id
|
||||
)
|
||||
if request.requested_by.id == user.id or user.is_superuser:
|
||||
updated_season_request.requested_by = UserRead.model_validate(user)
|
||||
tv_service.update_season_request(season_request=updated_season_request)
|
||||
return
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/seasons/requests/{season_request_id}", status_code=status.HTTP_204_NO_CONTENT
|
||||
)
|
||||
def authorize_request(
|
||||
tv_service: tv_service_dep,
|
||||
user: Annotated[User, Depends(current_superuser)],
|
||||
season_request_id: SeasonRequestId,
|
||||
authorized_status: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Authorize or de-authorize a season request.
|
||||
"""
|
||||
season_request = tv_service.get_season_request_by_id(
|
||||
season_request_id=season_request_id
|
||||
)
|
||||
if not season_request:
|
||||
raise NotFoundError
|
||||
season_request.authorized_by = UserRead.model_validate(user)
|
||||
season_request.authorized = authorized_status
|
||||
if not authorized_status:
|
||||
season_request.authorized_by = None
|
||||
tv_service.update_season_request(season_request=season_request)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/seasons/requests/{request_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
def delete_season_request(
|
||||
tv_service: tv_service_dep,
|
||||
user: Annotated[User, Depends(current_active_user)],
|
||||
request_id: SeasonRequestId,
|
||||
) -> None:
|
||||
"""
|
||||
Delete a season request.
|
||||
"""
|
||||
request = tv_service.get_season_request_by_id(season_request_id=request_id)
|
||||
if user.is_superuser or request.requested_by.id == user.id:
|
||||
tv_service.delete_season_request(season_request_id=request_id)
|
||||
log.info(f"User {user.id} deleted season request {request_id}.")
|
||||
return
|
||||
log.warning(
|
||||
f"User {user.id} tried to delete season request {request_id} but is not authorized."
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this request",
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SEASONS
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -2,9 +2,8 @@ import typing
|
||||
import uuid
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from media_manager.auth.schemas import UserRead
|
||||
from media_manager.torrent.models import Quality
|
||||
from media_manager.torrent.schemas import TorrentId, TorrentStatus
|
||||
|
||||
@@ -14,7 +13,6 @@ EpisodeId = typing.NewType("EpisodeId", UUID)
|
||||
|
||||
SeasonNumber = typing.NewType("SeasonNumber", int)
|
||||
EpisodeNumber = typing.NewType("EpisodeNumber", int)
|
||||
SeasonRequestId = typing.NewType("SeasonRequestId", UUID)
|
||||
|
||||
|
||||
class Episode(BaseModel):
|
||||
@@ -63,42 +61,6 @@ class Show(BaseModel):
|
||||
seasons: list[Season]
|
||||
|
||||
|
||||
class SeasonRequestBase(BaseModel):
|
||||
min_quality: Quality
|
||||
wanted_quality: Quality
|
||||
|
||||
@model_validator(mode="after")
|
||||
def ensure_wanted_quality_is_eq_or_gt_min_quality(self) -> "SeasonRequestBase":
|
||||
if self.min_quality.value < self.wanted_quality.value:
|
||||
msg = "wanted_quality must be equal to or lower than minimum_quality."
|
||||
raise ValueError(msg)
|
||||
return self
|
||||
|
||||
|
||||
class CreateSeasonRequest(SeasonRequestBase):
|
||||
season_id: SeasonId
|
||||
|
||||
|
||||
class UpdateSeasonRequest(SeasonRequestBase):
|
||||
id: SeasonRequestId
|
||||
|
||||
|
||||
class SeasonRequest(SeasonRequestBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: SeasonRequestId = Field(default_factory=lambda: SeasonRequestId(uuid.uuid4()))
|
||||
|
||||
season_id: SeasonId
|
||||
requested_by: UserRead | None = None
|
||||
authorized: bool = False
|
||||
authorized_by: UserRead | None = None
|
||||
|
||||
|
||||
class RichSeasonRequest(SeasonRequest):
|
||||
show: Show
|
||||
season: Season
|
||||
|
||||
|
||||
class EpisodeFile(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ from media_manager.schemas import MediaImportSuggestion
|
||||
from media_manager.torrent.repository import TorrentRepository
|
||||
from media_manager.torrent.schemas import (
|
||||
Quality,
|
||||
QualityStrings,
|
||||
Torrent,
|
||||
TorrentStatus,
|
||||
)
|
||||
@@ -48,17 +47,13 @@ from media_manager.tv.schemas import (
|
||||
PublicEpisodeFile,
|
||||
PublicSeason,
|
||||
PublicShow,
|
||||
RichSeasonRequest,
|
||||
RichSeasonTorrent,
|
||||
RichShowTorrent,
|
||||
Season,
|
||||
SeasonId,
|
||||
SeasonRequest,
|
||||
SeasonRequestId,
|
||||
Show,
|
||||
ShowId,
|
||||
)
|
||||
from media_manager.tv.schemas import Episode as EpisodeSchema
|
||||
|
||||
|
||||
class TvService:
|
||||
@@ -94,28 +89,6 @@ class TvService:
|
||||
metadata_provider.download_show_poster_image(show=saved_show)
|
||||
return saved_show
|
||||
|
||||
def add_season_request(self, season_request: SeasonRequest) -> SeasonRequest:
|
||||
"""
|
||||
Add a new season request.
|
||||
|
||||
:param season_request: The season request to add.
|
||||
:return: The added season request.
|
||||
"""
|
||||
return self.tv_repository.add_season_request(season_request=season_request)
|
||||
|
||||
def get_season_request_by_id(
|
||||
self, season_request_id: SeasonRequestId
|
||||
) -> SeasonRequest | None:
|
||||
"""
|
||||
Get a season request by its ID.
|
||||
|
||||
:param season_request_id: The ID of the season request.
|
||||
:return: The season request or None if not found.
|
||||
"""
|
||||
return self.tv_repository.get_season_request(
|
||||
season_request_id=season_request_id
|
||||
)
|
||||
|
||||
def get_total_downloaded_episoded_count(self) -> int:
|
||||
"""
|
||||
Get total number of downloaded episodes.
|
||||
@@ -123,27 +96,9 @@ class TvService:
|
||||
|
||||
return self.tv_repository.get_total_downloaded_episodes_count()
|
||||
|
||||
def update_season_request(self, season_request: SeasonRequest) -> SeasonRequest:
|
||||
"""
|
||||
Update an existing season request.
|
||||
|
||||
:param season_request: The season request to update.
|
||||
:return: The updated season request.
|
||||
"""
|
||||
self.tv_repository.delete_season_request(season_request_id=season_request.id)
|
||||
return self.tv_repository.add_season_request(season_request=season_request)
|
||||
|
||||
def set_show_library(self, show: Show, library: str) -> None:
|
||||
self.tv_repository.set_show_library(show_id=show.id, library=library)
|
||||
|
||||
def delete_season_request(self, season_request_id: SeasonRequestId) -> None:
|
||||
"""
|
||||
Delete a season request by its ID.
|
||||
|
||||
:param season_request_id: The ID of the season request to delete.
|
||||
"""
|
||||
self.tv_repository.delete_season_request(season_request_id=season_request_id)
|
||||
|
||||
def delete_show(
|
||||
self,
|
||||
show: Show,
|
||||
@@ -498,14 +453,6 @@ class TvService:
|
||||
"""
|
||||
return self.tv_repository.get_season_by_episode(episode_id=episode_id)
|
||||
|
||||
def get_all_season_requests(self) -> list[RichSeasonRequest]:
|
||||
"""
|
||||
Get all season requests.
|
||||
|
||||
:return: A list of rich season requests.
|
||||
"""
|
||||
return self.tv_repository.get_season_requests()
|
||||
|
||||
def get_torrents_for_show(self, show: Show) -> RichShowTorrent:
|
||||
"""
|
||||
Get torrents for a given show.
|
||||
@@ -632,72 +579,6 @@ class TvService:
|
||||
|
||||
return show_torrent
|
||||
|
||||
def download_approved_season_request(
|
||||
self, season_request: SeasonRequest, show: Show
|
||||
) -> bool:
|
||||
"""
|
||||
Download an approved season request.
|
||||
|
||||
:param season_request: The season request to download.
|
||||
:param show: The Show object.
|
||||
:return: True if the download was successful, False otherwise.
|
||||
:raises ValueError: If the season request is not authorized.
|
||||
"""
|
||||
if not season_request.authorized:
|
||||
msg = f"Season request {season_request.id} is not authorized for download"
|
||||
raise ValueError(msg)
|
||||
|
||||
log.info(f"Downloading approved season request {season_request.id}")
|
||||
|
||||
season = self.get_season(season_id=season_request.season_id)
|
||||
torrents = self.get_all_available_torrents_for_a_season(
|
||||
season_number=season.number, show_id=show.id
|
||||
)
|
||||
available_torrents: list[IndexerQueryResult] = []
|
||||
|
||||
for torrent in torrents:
|
||||
if (
|
||||
(torrent.quality.value < season_request.wanted_quality.value)
|
||||
or (torrent.quality.value > season_request.min_quality.value)
|
||||
or (torrent.seeders < 3)
|
||||
):
|
||||
log.info(
|
||||
f"Skipping torrent {torrent.title} with quality {torrent.quality} for season {season.id}, because it does not match the requested quality {season_request.wanted_quality}"
|
||||
)
|
||||
elif torrent.season != [season.number]:
|
||||
log.info(
|
||||
f"Skipping torrent {torrent.title} with quality {torrent.quality} for season {season.id}, because it contains to many/wrong seasons {torrent.season} (wanted: {season.number})"
|
||||
)
|
||||
else:
|
||||
available_torrents.append(torrent)
|
||||
log.info(
|
||||
f"Taking torrent {torrent.title} with quality {torrent.quality} for season {season.id} into consideration"
|
||||
)
|
||||
|
||||
if len(available_torrents) == 0:
|
||||
log.warning(
|
||||
f"No torrents matching criteria were found (wanted quality: {season_request.wanted_quality}, min_quality: {season_request.min_quality} for season {season.id})"
|
||||
)
|
||||
return False
|
||||
|
||||
available_torrents.sort()
|
||||
|
||||
torrent = self.torrent_service.download(indexer_result=available_torrents[0])
|
||||
season_file = SeasonFile( # noqa: F821
|
||||
season_id=season.id,
|
||||
quality=torrent.quality,
|
||||
torrent_id=torrent.id,
|
||||
file_path_suffix=QualityStrings[torrent.quality.name].value.upper(),
|
||||
)
|
||||
try:
|
||||
self.tv_repository.add_season_file(season_file=season_file)
|
||||
except IntegrityError:
|
||||
log.warning(
|
||||
f"Season file for season {season.id} and quality {torrent.quality} already exists, skipping."
|
||||
)
|
||||
self.delete_season_request(season_request.id)
|
||||
return True
|
||||
|
||||
def get_root_show_directory(self, show: Show) -> Path:
|
||||
misc_config = MediaManagerConfig().misc
|
||||
show_directory_name = f"{remove_special_characters(show.name)} ({show.year}) [{show.metadata_provider}id-{show.external_id}]"
|
||||
@@ -1056,7 +937,7 @@ class TvService:
|
||||
log.debug(
|
||||
f"Adding new episode {fresh_episode_data.number} to season {existing_season.number}"
|
||||
)
|
||||
episode_schema = EpisodeSchema(
|
||||
episode_schema = Episode(
|
||||
id=EpisodeId(fresh_episode_data.id),
|
||||
number=fresh_episode_data.number,
|
||||
external_id=fresh_episode_data.external_id,
|
||||
@@ -1072,7 +953,7 @@ class TvService:
|
||||
f"Adding new season {fresh_season_data.number} to show {db_show.name}"
|
||||
)
|
||||
episodes_for_schema = [
|
||||
EpisodeSchema(
|
||||
Episode(
|
||||
id=EpisodeId(ep_data.id),
|
||||
number=ep_data.number,
|
||||
external_id=ep_data.external_id,
|
||||
@@ -1190,49 +1071,6 @@ class TvService:
|
||||
return import_suggestions
|
||||
|
||||
|
||||
def auto_download_all_approved_season_requests() -> None:
|
||||
"""
|
||||
Auto download all approved season requests.
|
||||
This is a standalone function as it creates its own DB session.
|
||||
"""
|
||||
with next(get_session()) as db:
|
||||
tv_repository = TvRepository(db=db)
|
||||
torrent_service = TorrentService(torrent_repository=TorrentRepository(db=db))
|
||||
indexer_service = IndexerService(indexer_repository=IndexerRepository(db=db))
|
||||
notification_service = NotificationService(
|
||||
notification_repository=NotificationRepository(db=db)
|
||||
)
|
||||
tv_service = TvService(
|
||||
tv_repository=tv_repository,
|
||||
torrent_service=torrent_service,
|
||||
indexer_service=indexer_service,
|
||||
notification_service=notification_service,
|
||||
)
|
||||
|
||||
log.info("Auto downloading all approved season requests")
|
||||
season_requests = tv_repository.get_season_requests()
|
||||
log.info(f"Found {len(season_requests)} season requests to process")
|
||||
count = 0
|
||||
|
||||
for season_request in season_requests:
|
||||
if season_request.authorized:
|
||||
log.info(f"Processing season request {season_request.id} for download")
|
||||
show = tv_repository.get_show_by_season_id(
|
||||
season_id=season_request.season_id
|
||||
)
|
||||
if tv_service.download_approved_season_request(
|
||||
season_request=season_request, show=show
|
||||
):
|
||||
count += 1
|
||||
else:
|
||||
log.warning(
|
||||
f"Failed to download season request {season_request.id} for show {show.name}"
|
||||
)
|
||||
|
||||
log.info(f"Auto downloaded {count} approved season requests")
|
||||
db.commit()
|
||||
|
||||
|
||||
def import_all_show_torrents() -> None:
|
||||
with next(get_session()) as db:
|
||||
tv_repository = TvRepository(db=db)
|
||||
@@ -1310,30 +1148,8 @@ def update_all_non_ended_shows_metadata() -> None:
|
||||
db_show=show, metadata_provider=metadata_provider
|
||||
)
|
||||
|
||||
# Automatically add season requests for new seasons
|
||||
existing_seasons = [x.id for x in show.seasons]
|
||||
new_seasons = [
|
||||
x for x in updated_show.seasons if x.id not in existing_seasons
|
||||
]
|
||||
|
||||
if show.continuous_download:
|
||||
for new_season in new_seasons:
|
||||
log.info(
|
||||
f"Automatically adding season request for new season {new_season.number} of show {updated_show.name}"
|
||||
)
|
||||
tv_service.add_season_request(
|
||||
SeasonRequest(
|
||||
min_quality=Quality.sd,
|
||||
wanted_quality=Quality.uhd,
|
||||
season_id=new_season.id,
|
||||
authorized=True,
|
||||
)
|
||||
)
|
||||
|
||||
if updated_show:
|
||||
log.debug(
|
||||
f"Added new seasons: {len(new_seasons)} to show: {updated_show.name}"
|
||||
)
|
||||
log.debug("Updated show metadata", extra={"show": updated_show.name})
|
||||
else:
|
||||
log.warning(f"Failed to update metadata for show: {show.name}")
|
||||
db.commit()
|
||||
|
||||
Reference in New Issue
Block a user