mirror of
https://github.com/maxdorninger/MediaManager.git
synced 2026-04-17 15:13:24 +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
@@ -30,14 +30,13 @@ from media_manager.auth.db import OAuthAccount, User # noqa: E402
|
|||||||
from media_manager.config import MediaManagerConfig # noqa: E402
|
from media_manager.config import MediaManagerConfig # noqa: E402
|
||||||
from media_manager.database import Base # noqa: E402
|
from media_manager.database import Base # noqa: E402
|
||||||
from media_manager.indexer.models import IndexerQueryResult # noqa: E402
|
from media_manager.indexer.models import IndexerQueryResult # noqa: E402
|
||||||
from media_manager.movies.models import Movie, MovieFile, MovieRequest # noqa: E402
|
from media_manager.movies.models import Movie, MovieFile # noqa: E402
|
||||||
from media_manager.notification.models import Notification # noqa: E402
|
from media_manager.notification.models import Notification # noqa: E402
|
||||||
from media_manager.torrent.models import Torrent # noqa: E402
|
from media_manager.torrent.models import Torrent # noqa: E402
|
||||||
from media_manager.tv.models import ( # noqa: E402
|
from media_manager.tv.models import ( # noqa: E402
|
||||||
Episode,
|
Episode,
|
||||||
EpisodeFile,
|
EpisodeFile,
|
||||||
Season,
|
Season,
|
||||||
SeasonRequest,
|
|
||||||
Show,
|
Show,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,11 +50,9 @@ __all__ = [
|
|||||||
"IndexerQueryResult",
|
"IndexerQueryResult",
|
||||||
"Movie",
|
"Movie",
|
||||||
"MovieFile",
|
"MovieFile",
|
||||||
"MovieRequest",
|
|
||||||
"Notification",
|
"Notification",
|
||||||
"OAuthAccount",
|
"OAuthAccount",
|
||||||
"Season",
|
"Season",
|
||||||
"SeasonRequest",
|
|
||||||
"Show",
|
"Show",
|
||||||
"Torrent",
|
"Torrent",
|
||||||
"User",
|
"User",
|
||||||
|
|||||||
65
alembic/versions/e60ae827ed98_remove_requests.py
Normal file
65
alembic/versions/e60ae827ed98_remove_requests.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"""remove requests
|
||||||
|
|
||||||
|
Revision ID: e60ae827ed98
|
||||||
|
Revises: a6f714d3c8b9
|
||||||
|
Create Date: 2026-02-22 18:07:12.866130
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'e60ae827ed98'
|
||||||
|
down_revision: Union[str, None] = 'a6f714d3c8b9'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('movie_request')
|
||||||
|
op.drop_table('season_request')
|
||||||
|
op.alter_column('episode', 'overview',
|
||||||
|
existing_type=sa.TEXT(),
|
||||||
|
type_=sa.String(),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.create_table('season_request',
|
||||||
|
sa.Column('id', sa.UUID(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('season_id', sa.UUID(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('wanted_quality', postgresql.ENUM('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('min_quality', postgresql.ENUM('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('requested_by_id', sa.UUID(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column('authorized', sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('authorized_by_id', sa.UUID(), autoincrement=False, nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['authorized_by_id'], ['user.id'], name=op.f('season_request_authorized_by_id_fkey'), ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['requested_by_id'], ['user.id'], name=op.f('season_request_requested_by_id_fkey'), ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['season_id'], ['season.id'], name=op.f('season_request_season_id_fkey'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('season_request_pkey')),
|
||||||
|
sa.UniqueConstraint('season_id', 'wanted_quality', name=op.f('season_request_season_id_wanted_quality_key'), postgresql_include=[], postgresql_nulls_not_distinct=False)
|
||||||
|
)
|
||||||
|
op.create_table('movie_request',
|
||||||
|
sa.Column('id', sa.UUID(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('movie_id', sa.UUID(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('wanted_quality', postgresql.ENUM('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('min_quality', postgresql.ENUM('uhd', 'fullhd', 'hd', 'sd', 'unknown', name='quality'), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('authorized', sa.BOOLEAN(), autoincrement=False, nullable=False),
|
||||||
|
sa.Column('requested_by_id', sa.UUID(), autoincrement=False, nullable=True),
|
||||||
|
sa.Column('authorized_by_id', sa.UUID(), autoincrement=False, nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['authorized_by_id'], ['user.id'], name=op.f('movie_request_authorized_by_id_fkey'), ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['movie_id'], ['movie.id'], name=op.f('movie_request_movie_id_fkey'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['requested_by_id'], ['user.id'], name=op.f('movie_request_requested_by_id_fkey'), ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('movie_request_pkey')),
|
||||||
|
sa.UniqueConstraint('movie_id', 'wanted_quality', name=op.f('movie_request_movie_id_wanted_quality_key'), postgresql_include=[], postgresql_nulls_not_distinct=False)
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -3,7 +3,6 @@ from uuid import UUID
|
|||||||
from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint
|
from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from media_manager.auth.db import User
|
|
||||||
from media_manager.database import Base
|
from media_manager.database import Base
|
||||||
from media_manager.torrent.models import Quality
|
from media_manager.torrent.models import Quality
|
||||||
|
|
||||||
@@ -22,10 +21,6 @@ class Movie(Base):
|
|||||||
original_language: Mapped[str | None] = mapped_column(default=None)
|
original_language: Mapped[str | None] = mapped_column(default=None)
|
||||||
imdb_id: 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):
|
class MovieFile(Base):
|
||||||
__tablename__ = "movie_file"
|
__tablename__ = "movie_file"
|
||||||
@@ -42,31 +37,3 @@ class MovieFile(Base):
|
|||||||
)
|
)
|
||||||
|
|
||||||
torrent = relationship("Torrent", back_populates="movie_files", uselist=False)
|
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,
|
IntegrityError,
|
||||||
SQLAlchemyError,
|
SQLAlchemyError,
|
||||||
)
|
)
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from media_manager.exceptions import ConflictError, NotFoundError
|
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 (
|
from media_manager.movies.schemas import (
|
||||||
Movie as MovieSchema,
|
Movie as MovieSchema,
|
||||||
)
|
)
|
||||||
@@ -17,17 +17,10 @@ from media_manager.movies.schemas import (
|
|||||||
)
|
)
|
||||||
from media_manager.movies.schemas import (
|
from media_manager.movies.schemas import (
|
||||||
MovieId,
|
MovieId,
|
||||||
MovieRequestId,
|
|
||||||
)
|
|
||||||
from media_manager.movies.schemas import (
|
|
||||||
MovieRequest as MovieRequestSchema,
|
|
||||||
)
|
)
|
||||||
from media_manager.movies.schemas import (
|
from media_manager.movies.schemas import (
|
||||||
MovieTorrent as MovieTorrentSchema,
|
MovieTorrent as MovieTorrentSchema,
|
||||||
)
|
)
|
||||||
from media_manager.movies.schemas import (
|
|
||||||
RichMovieRequest as RichMovieRequestSchema,
|
|
||||||
)
|
|
||||||
from media_manager.torrent.models import Torrent
|
from media_manager.torrent.models import Torrent
|
||||||
from media_manager.torrent.schemas import TorrentId
|
from media_manager.torrent.schemas import TorrentId
|
||||||
|
|
||||||
@@ -173,46 +166,6 @@ class MovieRepository:
|
|||||||
log.exception(f"Database error while deleting movie {movie_id}")
|
log.exception(f"Database error while deleting movie {movie_id}")
|
||||||
raise
|
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:
|
def set_movie_library(self, movie_id: MovieId, library: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sets the library for a movie.
|
Sets the library for a movie.
|
||||||
@@ -234,49 +187,6 @@ class MovieRepository:
|
|||||||
log.exception(f"Database error setting library for movie {movie_id}")
|
log.exception(f"Database error setting library for movie {movie_id}")
|
||||||
raise
|
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:
|
def add_movie_file(self, movie_file: MovieFileSchema) -> MovieFileSchema:
|
||||||
"""
|
"""
|
||||||
Adds a movie file record to the database.
|
Adds a movie file record to the database.
|
||||||
@@ -396,25 +306,6 @@ class MovieRepository:
|
|||||||
log.exception("Database error retrieving all movies with torrents")
|
log.exception("Database error retrieving all movies with torrents")
|
||||||
raise
|
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:
|
def get_movie_by_torrent_id(self, torrent_id: TorrentId) -> MovieSchema:
|
||||||
"""
|
"""
|
||||||
Retrieve a movie by a torrent ID.
|
Retrieve a movie by a torrent ID.
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
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.auth.users import current_active_user, current_superuser
|
||||||
from media_manager.config import LibraryItem, MediaManagerConfig
|
from media_manager.config import LibraryItem, MediaManagerConfig
|
||||||
from media_manager.exceptions import ConflictError, NotFoundError
|
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.dependencies import metadata_provider_dep
|
||||||
from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult
|
from media_manager.metadataProvider.schemas import MetaDataProviderSearchResult
|
||||||
from media_manager.movies import log
|
|
||||||
from media_manager.movies.dependencies import (
|
from media_manager.movies.dependencies import (
|
||||||
movie_dep,
|
movie_dep,
|
||||||
movie_service_dep,
|
movie_service_dep,
|
||||||
)
|
)
|
||||||
from media_manager.movies.schemas import (
|
from media_manager.movies.schemas import (
|
||||||
CreateMovieRequest,
|
|
||||||
Movie,
|
Movie,
|
||||||
MovieRequest,
|
|
||||||
MovieRequestBase,
|
|
||||||
MovieRequestId,
|
|
||||||
PublicMovie,
|
PublicMovie,
|
||||||
PublicMovieFile,
|
PublicMovieFile,
|
||||||
RichMovieRequest,
|
|
||||||
RichMovieTorrent,
|
RichMovieTorrent,
|
||||||
)
|
)
|
||||||
from media_manager.schemas import MediaImportSuggestion
|
from media_manager.schemas import MediaImportSuggestion
|
||||||
@@ -188,103 +180,6 @@ def get_available_libraries() -> list[LibraryItem]:
|
|||||||
return MediaManagerConfig().misc.movie_libraries
|
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
|
# MOVIES - SINGLE RESOURCE
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import typing
|
|||||||
import uuid
|
import uuid
|
||||||
from uuid 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.models import Quality
|
||||||
from media_manager.torrent.schemas import TorrentId, TorrentStatus
|
from media_manager.torrent.schemas import TorrentId, TorrentStatus
|
||||||
|
|
||||||
MovieId = typing.NewType("MovieId", UUID)
|
MovieId = typing.NewType("MovieId", UUID)
|
||||||
MovieRequestId = typing.NewType("MovieRequestId", UUID)
|
|
||||||
|
|
||||||
|
|
||||||
class Movie(BaseModel):
|
class Movie(BaseModel):
|
||||||
@@ -40,38 +38,6 @@ class PublicMovieFile(MovieFile):
|
|||||||
imported: bool = False
|
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):
|
class MovieTorrent(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ from pathlib import Path
|
|||||||
from typing import overload
|
from typing import overload
|
||||||
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from media_manager.config import MediaManagerConfig
|
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.exceptions import InvalidConfigError, NotFoundError, RenameError
|
||||||
from media_manager.indexer.repository import IndexerRepository
|
from media_manager.indexer.repository import IndexerRepository
|
||||||
from media_manager.indexer.schemas import IndexerQueryResult, IndexerQueryResultId
|
from media_manager.indexer.schemas import IndexerQueryResult, IndexerQueryResultId
|
||||||
@@ -25,11 +24,8 @@ from media_manager.movies.schemas import (
|
|||||||
Movie,
|
Movie,
|
||||||
MovieFile,
|
MovieFile,
|
||||||
MovieId,
|
MovieId,
|
||||||
MovieRequest,
|
|
||||||
MovieRequestId,
|
|
||||||
PublicMovie,
|
PublicMovie,
|
||||||
PublicMovieFile,
|
PublicMovieFile,
|
||||||
RichMovieRequest,
|
|
||||||
RichMovieTorrent,
|
RichMovieTorrent,
|
||||||
)
|
)
|
||||||
from media_manager.notification.repository import NotificationRepository
|
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.repository import TorrentRepository
|
||||||
from media_manager.torrent.schemas import (
|
from media_manager.torrent.schemas import (
|
||||||
Quality,
|
Quality,
|
||||||
QualityStrings,
|
|
||||||
Torrent,
|
Torrent,
|
||||||
TorrentStatus,
|
TorrentStatus,
|
||||||
)
|
)
|
||||||
@@ -89,44 +84,6 @@ class MovieService:
|
|||||||
metadata_provider.download_movie_poster_image(movie=saved_movie)
|
metadata_provider.download_movie_poster_image(movie=saved_movie)
|
||||||
return 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(
|
def delete_movie(
|
||||||
self,
|
self,
|
||||||
movie: Movie,
|
movie: Movie,
|
||||||
@@ -391,14 +348,6 @@ class MovieService:
|
|||||||
external_id=external_id, metadata_provider=metadata_provider
|
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:
|
def set_movie_library(self, movie: Movie, library: str) -> None:
|
||||||
self.movie_repository.set_movie_library(movie_id=movie.id, library=library)
|
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)
|
self.torrent_service.resume_download(torrent=movie_torrent)
|
||||||
return 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:
|
def get_movie_root_path(self, movie: Movie) -> Path:
|
||||||
misc_config = MediaManagerConfig().misc
|
misc_config = MediaManagerConfig().misc
|
||||||
movie_file_path = (
|
movie_file_path = (
|
||||||
@@ -774,47 +664,6 @@ class MovieService:
|
|||||||
return importable_movies
|
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:
|
def import_all_movie_torrents() -> None:
|
||||||
with next(get_session()) as db:
|
with next(get_session()) as db:
|
||||||
movie_repository = MovieRepository(db=db)
|
movie_repository = MovieRepository(db=db)
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ from apscheduler.triggers.cron import CronTrigger
|
|||||||
import media_manager.database
|
import media_manager.database
|
||||||
from media_manager.config import MediaManagerConfig
|
from media_manager.config import MediaManagerConfig
|
||||||
from media_manager.movies.service import (
|
from media_manager.movies.service import (
|
||||||
auto_download_all_approved_movie_requests,
|
|
||||||
import_all_movie_torrents,
|
import_all_movie_torrents,
|
||||||
update_all_movies_metadata,
|
update_all_movies_metadata,
|
||||||
)
|
)
|
||||||
from media_manager.tv.service import (
|
from media_manager.tv.service import (
|
||||||
auto_download_all_approved_season_requests,
|
|
||||||
import_all_show_torrents,
|
import_all_show_torrents,
|
||||||
update_all_non_ended_shows_metadata,
|
update_all_non_ended_shows_metadata,
|
||||||
)
|
)
|
||||||
@@ -23,7 +21,6 @@ def setup_scheduler(config: MediaManagerConfig) -> BackgroundScheduler:
|
|||||||
jobstores = {"default": SQLAlchemyJobStore(engine=media_manager.database.engine)}
|
jobstores = {"default": SQLAlchemyJobStore(engine=media_manager.database.engine)}
|
||||||
scheduler = BackgroundScheduler(jobstores=jobstores)
|
scheduler = BackgroundScheduler(jobstores=jobstores)
|
||||||
every_15_minutes_trigger = CronTrigger(minute="*/15", hour="*")
|
every_15_minutes_trigger = CronTrigger(minute="*/15", hour="*")
|
||||||
daily_trigger = CronTrigger(hour=0, minute=0, jitter=60 * 60 * 24 * 2)
|
|
||||||
weekly_trigger = CronTrigger(
|
weekly_trigger = CronTrigger(
|
||||||
day_of_week="mon", hour=0, minute=0, jitter=60 * 60 * 24 * 2
|
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",
|
id="import_all_show_torrents",
|
||||||
replace_existing=True,
|
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(
|
scheduler.add_job(
|
||||||
update_all_movies_metadata,
|
update_all_movies_metadata,
|
||||||
weekly_trigger,
|
weekly_trigger,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from uuid import UUID
|
|||||||
from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint
|
from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from media_manager.auth.db import User
|
|
||||||
from media_manager.database import Base
|
from media_manager.database import Base
|
||||||
from media_manager.torrent.models import Quality
|
from media_manager.torrent.models import Quality
|
||||||
|
|
||||||
@@ -48,10 +47,6 @@ class Season(Base):
|
|||||||
back_populates="season", cascade="all, delete"
|
back_populates="season", cascade="all, delete"
|
||||||
)
|
)
|
||||||
|
|
||||||
season_requests = relationship(
|
|
||||||
"SeasonRequest", back_populates="season", cascade="all, delete"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Episode(Base):
|
class Episode(Base):
|
||||||
__tablename__ = "episode"
|
__tablename__ = "episode"
|
||||||
@@ -85,29 +80,3 @@ class EpisodeFile(Base):
|
|||||||
|
|
||||||
torrent = relationship("Torrent", back_populates="episode_files", uselist=False)
|
torrent = relationship("Torrent", back_populates="episode_files", uselist=False)
|
||||||
episode = relationship("Episode", 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 Torrent as TorrentSchema
|
||||||
from media_manager.torrent.schemas import TorrentId
|
from media_manager.torrent.schemas import TorrentId
|
||||||
from media_manager.tv import log
|
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 Episode as EpisodeSchema
|
||||||
from media_manager.tv.schemas import EpisodeFile as EpisodeFileSchema
|
from media_manager.tv.schemas import EpisodeFile as EpisodeFileSchema
|
||||||
from media_manager.tv.schemas import (
|
from media_manager.tv.schemas import (
|
||||||
@@ -15,12 +15,9 @@ from media_manager.tv.schemas import (
|
|||||||
EpisodeNumber,
|
EpisodeNumber,
|
||||||
SeasonId,
|
SeasonId,
|
||||||
SeasonNumber,
|
SeasonNumber,
|
||||||
SeasonRequestId,
|
|
||||||
ShowId,
|
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 Season as SeasonSchema
|
||||||
from media_manager.tv.schemas import SeasonRequest as SeasonRequestSchema
|
|
||||||
from media_manager.tv.schemas import Show as ShowSchema
|
from media_manager.tv.schemas import Show as ShowSchema
|
||||||
|
|
||||||
|
|
||||||
@@ -256,67 +253,6 @@ class TvRepository:
|
|||||||
)
|
)
|
||||||
raise
|
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:
|
def get_season_by_number(self, season_number: int, show_id: ShowId) -> SeasonSchema:
|
||||||
"""
|
"""
|
||||||
Retrieve a season by its number and show ID.
|
Retrieve a season by its number and show ID.
|
||||||
@@ -345,38 +281,6 @@ class TvRepository:
|
|||||||
)
|
)
|
||||||
raise
|
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:
|
def add_episode_file(self, episode_file: EpisodeFileSchema) -> EpisodeFileSchema:
|
||||||
"""
|
"""
|
||||||
Adds an episode file record to the database.
|
Adds an episode file record to the database.
|
||||||
@@ -581,30 +485,6 @@ class TvRepository:
|
|||||||
)
|
)
|
||||||
raise
|
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:
|
def get_show_by_season_id(self, season_id: SeasonId) -> ShowSchema:
|
||||||
"""
|
"""
|
||||||
Retrieve a show by one of its season's ID.
|
Retrieve a show by one of its season's ID.
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
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.auth.users import current_active_user, current_superuser
|
||||||
from media_manager.config import LibraryItem, MediaManagerConfig
|
from media_manager.config import LibraryItem, MediaManagerConfig
|
||||||
from media_manager.exceptions import MediaAlreadyExistsError, NotFoundError
|
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.schemas import MediaImportSuggestion
|
||||||
from media_manager.torrent.schemas import Torrent
|
from media_manager.torrent.schemas import Torrent
|
||||||
from media_manager.torrent.utils import get_importable_media_directories
|
from media_manager.torrent.utils import get_importable_media_directories
|
||||||
from media_manager.tv import log
|
|
||||||
from media_manager.tv.dependencies import (
|
from media_manager.tv.dependencies import (
|
||||||
season_dep,
|
season_dep,
|
||||||
show_dep,
|
show_dep,
|
||||||
tv_service_dep,
|
tv_service_dep,
|
||||||
)
|
)
|
||||||
from media_manager.tv.schemas import (
|
from media_manager.tv.schemas import (
|
||||||
CreateSeasonRequest,
|
|
||||||
PublicEpisodeFile,
|
PublicEpisodeFile,
|
||||||
PublicShow,
|
PublicShow,
|
||||||
RichSeasonRequest,
|
|
||||||
RichShowTorrent,
|
RichShowTorrent,
|
||||||
Season,
|
Season,
|
||||||
SeasonRequest,
|
|
||||||
SeasonRequestId,
|
|
||||||
Show,
|
Show,
|
||||||
ShowId,
|
ShowId,
|
||||||
UpdateSeasonRequest,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
router = APIRouter()
|
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)
|
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
|
# SEASONS
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import typing
|
|||||||
import uuid
|
import uuid
|
||||||
from uuid 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.models import Quality
|
||||||
from media_manager.torrent.schemas import TorrentId, TorrentStatus
|
from media_manager.torrent.schemas import TorrentId, TorrentStatus
|
||||||
|
|
||||||
@@ -14,7 +13,6 @@ EpisodeId = typing.NewType("EpisodeId", UUID)
|
|||||||
|
|
||||||
SeasonNumber = typing.NewType("SeasonNumber", int)
|
SeasonNumber = typing.NewType("SeasonNumber", int)
|
||||||
EpisodeNumber = typing.NewType("EpisodeNumber", int)
|
EpisodeNumber = typing.NewType("EpisodeNumber", int)
|
||||||
SeasonRequestId = typing.NewType("SeasonRequestId", UUID)
|
|
||||||
|
|
||||||
|
|
||||||
class Episode(BaseModel):
|
class Episode(BaseModel):
|
||||||
@@ -63,42 +61,6 @@ class Show(BaseModel):
|
|||||||
seasons: list[Season]
|
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):
|
class EpisodeFile(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
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.repository import TorrentRepository
|
||||||
from media_manager.torrent.schemas import (
|
from media_manager.torrent.schemas import (
|
||||||
Quality,
|
Quality,
|
||||||
QualityStrings,
|
|
||||||
Torrent,
|
Torrent,
|
||||||
TorrentStatus,
|
TorrentStatus,
|
||||||
)
|
)
|
||||||
@@ -48,17 +47,13 @@ from media_manager.tv.schemas import (
|
|||||||
PublicEpisodeFile,
|
PublicEpisodeFile,
|
||||||
PublicSeason,
|
PublicSeason,
|
||||||
PublicShow,
|
PublicShow,
|
||||||
RichSeasonRequest,
|
|
||||||
RichSeasonTorrent,
|
RichSeasonTorrent,
|
||||||
RichShowTorrent,
|
RichShowTorrent,
|
||||||
Season,
|
Season,
|
||||||
SeasonId,
|
SeasonId,
|
||||||
SeasonRequest,
|
|
||||||
SeasonRequestId,
|
|
||||||
Show,
|
Show,
|
||||||
ShowId,
|
ShowId,
|
||||||
)
|
)
|
||||||
from media_manager.tv.schemas import Episode as EpisodeSchema
|
|
||||||
|
|
||||||
|
|
||||||
class TvService:
|
class TvService:
|
||||||
@@ -94,28 +89,6 @@ class TvService:
|
|||||||
metadata_provider.download_show_poster_image(show=saved_show)
|
metadata_provider.download_show_poster_image(show=saved_show)
|
||||||
return 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:
|
def get_total_downloaded_episoded_count(self) -> int:
|
||||||
"""
|
"""
|
||||||
Get total number of downloaded episodes.
|
Get total number of downloaded episodes.
|
||||||
@@ -123,27 +96,9 @@ class TvService:
|
|||||||
|
|
||||||
return self.tv_repository.get_total_downloaded_episodes_count()
|
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:
|
def set_show_library(self, show: Show, library: str) -> None:
|
||||||
self.tv_repository.set_show_library(show_id=show.id, library=library)
|
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(
|
def delete_show(
|
||||||
self,
|
self,
|
||||||
show: Show,
|
show: Show,
|
||||||
@@ -498,14 +453,6 @@ class TvService:
|
|||||||
"""
|
"""
|
||||||
return self.tv_repository.get_season_by_episode(episode_id=episode_id)
|
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:
|
def get_torrents_for_show(self, show: Show) -> RichShowTorrent:
|
||||||
"""
|
"""
|
||||||
Get torrents for a given show.
|
Get torrents for a given show.
|
||||||
@@ -632,72 +579,6 @@ class TvService:
|
|||||||
|
|
||||||
return show_torrent
|
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:
|
def get_root_show_directory(self, show: Show) -> Path:
|
||||||
misc_config = MediaManagerConfig().misc
|
misc_config = MediaManagerConfig().misc
|
||||||
show_directory_name = f"{remove_special_characters(show.name)} ({show.year}) [{show.metadata_provider}id-{show.external_id}]"
|
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(
|
log.debug(
|
||||||
f"Adding new episode {fresh_episode_data.number} to season {existing_season.number}"
|
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),
|
id=EpisodeId(fresh_episode_data.id),
|
||||||
number=fresh_episode_data.number,
|
number=fresh_episode_data.number,
|
||||||
external_id=fresh_episode_data.external_id,
|
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}"
|
f"Adding new season {fresh_season_data.number} to show {db_show.name}"
|
||||||
)
|
)
|
||||||
episodes_for_schema = [
|
episodes_for_schema = [
|
||||||
EpisodeSchema(
|
Episode(
|
||||||
id=EpisodeId(ep_data.id),
|
id=EpisodeId(ep_data.id),
|
||||||
number=ep_data.number,
|
number=ep_data.number,
|
||||||
external_id=ep_data.external_id,
|
external_id=ep_data.external_id,
|
||||||
@@ -1190,49 +1071,6 @@ class TvService:
|
|||||||
return import_suggestions
|
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:
|
def import_all_show_torrents() -> None:
|
||||||
with next(get_session()) as db:
|
with next(get_session()) as db:
|
||||||
tv_repository = TvRepository(db=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
|
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:
|
if updated_show:
|
||||||
log.debug(
|
log.debug("Updated show metadata", extra={"show": updated_show.name})
|
||||||
f"Added new seasons: {len(new_seasons)} to show: {updated_show.name}"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
log.warning(f"Failed to update metadata for show: {show.name}")
|
log.warning(f"Failed to update metadata for show: {show.name}")
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
1094
web/package-lock.json
generated
1094
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
518
web/src/lib/api/api.d.ts
vendored
518
web/src/lib/api/api.d.ts
vendored
@@ -530,74 +530,6 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
'/api/v1/tv/seasons/requests': {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Get Season Requests
|
|
||||||
* @description Get all season requests.
|
|
||||||
*/
|
|
||||||
get: operations['get_season_requests_api_v1_tv_seasons_requests_get'];
|
|
||||||
/**
|
|
||||||
* Update Request
|
|
||||||
* @description Update an existing season request.
|
|
||||||
*/
|
|
||||||
put: operations['update_request_api_v1_tv_seasons_requests_put'];
|
|
||||||
/**
|
|
||||||
* Request A Season
|
|
||||||
* @description Create a new season request.
|
|
||||||
*/
|
|
||||||
post: operations['request_a_season_api_v1_tv_seasons_requests_post'];
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
'/api/v1/tv/seasons/requests/{season_request_id}': {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put?: never;
|
|
||||||
post?: never;
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
/**
|
|
||||||
* Authorize Request
|
|
||||||
* @description Authorize or de-authorize a season request.
|
|
||||||
*/
|
|
||||||
patch: operations['authorize_request_api_v1_tv_seasons_requests__season_request_id__patch'];
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
'/api/v1/tv/seasons/requests/{request_id}': {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put?: never;
|
|
||||||
post?: never;
|
|
||||||
/**
|
|
||||||
* Delete Season Request
|
|
||||||
* @description Delete a season request.
|
|
||||||
*/
|
|
||||||
delete: operations['delete_season_request_api_v1_tv_seasons_requests__request_id__delete'];
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
'/api/v1/tv/seasons/{season_id}': {
|
'/api/v1/tv/seasons/{season_id}': {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -896,58 +828,6 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
'/api/v1/movies/requests': {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Get All Movie Requests
|
|
||||||
* @description Get all movie requests.
|
|
||||||
*/
|
|
||||||
get: operations['get_all_movie_requests_api_v1_movies_requests_get'];
|
|
||||||
put?: never;
|
|
||||||
/**
|
|
||||||
* Create Movie Request
|
|
||||||
* @description Create a new movie request.
|
|
||||||
*/
|
|
||||||
post: operations['create_movie_request_api_v1_movies_requests_post'];
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
'/api/v1/movies/requests/{movie_request_id}': {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
/**
|
|
||||||
* Update Movie Request
|
|
||||||
* @description Update an existing movie request.
|
|
||||||
*/
|
|
||||||
put: operations['update_movie_request_api_v1_movies_requests__movie_request_id__put'];
|
|
||||||
post?: never;
|
|
||||||
/**
|
|
||||||
* Delete Movie Request
|
|
||||||
* @description Delete a movie request.
|
|
||||||
*/
|
|
||||||
delete: operations['delete_movie_request_api_v1_movies_requests__movie_request_id__delete'];
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
/**
|
|
||||||
* Authorize Request
|
|
||||||
* @description Authorize or de-authorize a movie request.
|
|
||||||
*/
|
|
||||||
patch: operations['authorize_request_api_v1_movies_requests__movie_request_id__patch'];
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
'/api/v1/movies/{movie_id}': {
|
'/api/v1/movies/{movie_id}': {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -1283,26 +1163,6 @@ export interface components {
|
|||||||
/** Token */
|
/** Token */
|
||||||
token: string;
|
token: string;
|
||||||
};
|
};
|
||||||
/** CreateMovieRequest */
|
|
||||||
CreateMovieRequest: {
|
|
||||||
min_quality: components['schemas']['Quality'];
|
|
||||||
wanted_quality: components['schemas']['Quality'];
|
|
||||||
/**
|
|
||||||
* Movie Id
|
|
||||||
* Format: uuid
|
|
||||||
*/
|
|
||||||
movie_id: string;
|
|
||||||
};
|
|
||||||
/** CreateSeasonRequest */
|
|
||||||
CreateSeasonRequest: {
|
|
||||||
min_quality: components['schemas']['Quality'];
|
|
||||||
wanted_quality: components['schemas']['Quality'];
|
|
||||||
/**
|
|
||||||
* Season Id
|
|
||||||
* Format: uuid
|
|
||||||
*/
|
|
||||||
season_id: string;
|
|
||||||
};
|
|
||||||
/** Episode */
|
/** Episode */
|
||||||
Episode: {
|
Episode: {
|
||||||
/**
|
/**
|
||||||
@@ -1432,33 +1292,6 @@ export interface components {
|
|||||||
/** Imdb Id */
|
/** Imdb Id */
|
||||||
imdb_id?: string | null;
|
imdb_id?: string | null;
|
||||||
};
|
};
|
||||||
/** MovieRequest */
|
|
||||||
MovieRequest: {
|
|
||||||
min_quality: components['schemas']['Quality'];
|
|
||||||
wanted_quality: components['schemas']['Quality'];
|
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
* Format: uuid
|
|
||||||
*/
|
|
||||||
id?: string;
|
|
||||||
/**
|
|
||||||
* Movie Id
|
|
||||||
* Format: uuid
|
|
||||||
*/
|
|
||||||
movie_id: string;
|
|
||||||
requested_by?: components['schemas']['UserRead'] | null;
|
|
||||||
/**
|
|
||||||
* Authorized
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
authorized: boolean;
|
|
||||||
authorized_by?: components['schemas']['UserRead'] | null;
|
|
||||||
};
|
|
||||||
/** MovieRequestBase */
|
|
||||||
MovieRequestBase: {
|
|
||||||
min_quality: components['schemas']['Quality'];
|
|
||||||
wanted_quality: components['schemas']['Quality'];
|
|
||||||
};
|
|
||||||
/** MovieTorrent */
|
/** MovieTorrent */
|
||||||
MovieTorrent: {
|
MovieTorrent: {
|
||||||
/**
|
/**
|
||||||
@@ -1662,29 +1495,6 @@ export interface components {
|
|||||||
* @enum {integer}
|
* @enum {integer}
|
||||||
*/
|
*/
|
||||||
Quality: 1 | 2 | 3 | 4 | 5;
|
Quality: 1 | 2 | 3 | 4 | 5;
|
||||||
/** RichMovieRequest */
|
|
||||||
RichMovieRequest: {
|
|
||||||
min_quality: components['schemas']['Quality'];
|
|
||||||
wanted_quality: components['schemas']['Quality'];
|
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
* Format: uuid
|
|
||||||
*/
|
|
||||||
id?: string;
|
|
||||||
/**
|
|
||||||
* Movie Id
|
|
||||||
* Format: uuid
|
|
||||||
*/
|
|
||||||
movie_id: string;
|
|
||||||
requested_by?: components['schemas']['UserRead'] | null;
|
|
||||||
/**
|
|
||||||
* Authorized
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
authorized: boolean;
|
|
||||||
authorized_by?: components['schemas']['UserRead'] | null;
|
|
||||||
movie: components['schemas']['Movie'];
|
|
||||||
};
|
|
||||||
/** RichMovieTorrent */
|
/** RichMovieTorrent */
|
||||||
RichMovieTorrent: {
|
RichMovieTorrent: {
|
||||||
/**
|
/**
|
||||||
@@ -1701,30 +1511,6 @@ export interface components {
|
|||||||
/** Torrents */
|
/** Torrents */
|
||||||
torrents: components['schemas']['MovieTorrent'][];
|
torrents: components['schemas']['MovieTorrent'][];
|
||||||
};
|
};
|
||||||
/** RichSeasonRequest */
|
|
||||||
RichSeasonRequest: {
|
|
||||||
min_quality: components['schemas']['Quality'];
|
|
||||||
wanted_quality: components['schemas']['Quality'];
|
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
* Format: uuid
|
|
||||||
*/
|
|
||||||
id?: string;
|
|
||||||
/**
|
|
||||||
* Season Id
|
|
||||||
* Format: uuid
|
|
||||||
*/
|
|
||||||
season_id: string;
|
|
||||||
requested_by?: components['schemas']['UserRead'] | null;
|
|
||||||
/**
|
|
||||||
* Authorized
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
authorized: boolean;
|
|
||||||
authorized_by?: components['schemas']['UserRead'] | null;
|
|
||||||
show: components['schemas']['Show'];
|
|
||||||
season: components['schemas']['Season'];
|
|
||||||
};
|
|
||||||
/** RichSeasonTorrent */
|
/** RichSeasonTorrent */
|
||||||
RichSeasonTorrent: {
|
RichSeasonTorrent: {
|
||||||
/**
|
/**
|
||||||
@@ -1846,16 +1632,6 @@ export interface components {
|
|||||||
* @enum {integer}
|
* @enum {integer}
|
||||||
*/
|
*/
|
||||||
TorrentStatus: 1 | 2 | 3 | 4;
|
TorrentStatus: 1 | 2 | 3 | 4;
|
||||||
/** UpdateSeasonRequest */
|
|
||||||
UpdateSeasonRequest: {
|
|
||||||
min_quality: components['schemas']['Quality'];
|
|
||||||
wanted_quality: components['schemas']['Quality'];
|
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
* Format: uuid
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
};
|
|
||||||
/** UserCreate */
|
/** UserCreate */
|
||||||
UserCreate: {
|
UserCreate: {
|
||||||
/**
|
/**
|
||||||
@@ -1930,6 +1706,10 @@ export interface components {
|
|||||||
msg: string;
|
msg: string;
|
||||||
/** Error Type */
|
/** Error Type */
|
||||||
type: string;
|
type: string;
|
||||||
|
/** Input */
|
||||||
|
input?: unknown;
|
||||||
|
/** Context */
|
||||||
|
ctx?: Record<string, never>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: never;
|
responses: never;
|
||||||
@@ -3085,148 +2865,6 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
get_season_requests_api_v1_tv_seasons_requests_get: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
/** @description Successful Response */
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['RichSeasonRequest'][];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
update_request_api_v1_tv_seasons_requests_put: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody: {
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['UpdateSeasonRequest'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
|
||||||
/** @description Successful Response */
|
|
||||||
204: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content?: never;
|
|
||||||
};
|
|
||||||
/** @description Validation Error */
|
|
||||||
422: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['HTTPValidationError'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
request_a_season_api_v1_tv_seasons_requests_post: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody: {
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['CreateSeasonRequest'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
|
||||||
/** @description Successful Response */
|
|
||||||
204: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content?: never;
|
|
||||||
};
|
|
||||||
/** @description Validation Error */
|
|
||||||
422: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['HTTPValidationError'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
authorize_request_api_v1_tv_seasons_requests__season_request_id__patch: {
|
|
||||||
parameters: {
|
|
||||||
query?: {
|
|
||||||
authorized_status?: boolean;
|
|
||||||
};
|
|
||||||
header?: never;
|
|
||||||
path: {
|
|
||||||
season_request_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
/** @description Successful Response */
|
|
||||||
204: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content?: never;
|
|
||||||
};
|
|
||||||
/** @description Validation Error */
|
|
||||||
422: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['HTTPValidationError'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
delete_season_request_api_v1_tv_seasons_requests__request_id__delete: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path: {
|
|
||||||
request_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
/** @description Successful Response */
|
|
||||||
204: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content?: never;
|
|
||||||
};
|
|
||||||
/** @description Validation Error */
|
|
||||||
422: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['HTTPValidationError'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
get_season_api_v1_tv_seasons__season_id__get: {
|
get_season_api_v1_tv_seasons__season_id__get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -3741,154 +3379,6 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
get_all_movie_requests_api_v1_movies_requests_get: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
/** @description Successful Response */
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['RichMovieRequest'][];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
create_movie_request_api_v1_movies_requests_post: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody: {
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['CreateMovieRequest'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
|
||||||
/** @description Successful Response */
|
|
||||||
201: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['MovieRequest'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/** @description Validation Error */
|
|
||||||
422: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['HTTPValidationError'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
update_movie_request_api_v1_movies_requests__movie_request_id__put: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path: {
|
|
||||||
movie_request_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody: {
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['MovieRequestBase'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
responses: {
|
|
||||||
/** @description Successful Response */
|
|
||||||
200: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['MovieRequest'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/** @description Validation Error */
|
|
||||||
422: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['HTTPValidationError'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
delete_movie_request_api_v1_movies_requests__movie_request_id__delete: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path: {
|
|
||||||
movie_request_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
/** @description Successful Response */
|
|
||||||
204: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content?: never;
|
|
||||||
};
|
|
||||||
/** @description Validation Error */
|
|
||||||
422: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['HTTPValidationError'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
authorize_request_api_v1_movies_requests__movie_request_id__patch: {
|
|
||||||
parameters: {
|
|
||||||
query?: {
|
|
||||||
authorized_status?: boolean;
|
|
||||||
};
|
|
||||||
header?: never;
|
|
||||||
path: {
|
|
||||||
movie_request_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
/** @description Successful Response */
|
|
||||||
204: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content?: never;
|
|
||||||
};
|
|
||||||
/** @description Validation Error */
|
|
||||||
422: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
'application/json': components['schemas']['HTTPValidationError'];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
get_movie_by_id_api_v1_movies__movie_id__get: {
|
get_movie_by_id_api_v1_movies__movie_id__get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@@ -34,10 +34,6 @@
|
|||||||
{
|
{
|
||||||
title: 'Torrents',
|
title: 'Torrents',
|
||||||
url: resolve('/dashboard/tv/torrents', {})
|
url: resolve('/dashboard/tv/torrents', {})
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Requests',
|
|
||||||
url: resolve('/dashboard/tv/requests', {})
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -54,10 +50,6 @@
|
|||||||
{
|
{
|
||||||
title: 'Torrents',
|
title: 'Torrents',
|
||||||
url: resolve('/dashboard/movies/torrents', {})
|
url: resolve('/dashboard/movies/torrents', {})
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Requests',
|
|
||||||
url: resolve('/dashboard/movies/requests', {})
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Button, buttonVariants } from '$lib/components/ui/button';
|
|
||||||
import * as Dialog from '$lib/components/ui/dialog';
|
|
||||||
import { Label } from '$lib/components/ui/label';
|
|
||||||
import * as Select from '$lib/components/ui/select';
|
|
||||||
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
|
||||||
import { getFullyQualifiedMediaName, getTorrentQualityString } from '$lib/utils.js';
|
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
import client from '$lib/api';
|
|
||||||
import type { components } from '$lib/api/api';
|
|
||||||
import { invalidateAll } from '$app/navigation';
|
|
||||||
|
|
||||||
let { movie }: { movie: components['schemas']['PublicMovie'] } = $props();
|
|
||||||
let dialogOpen = $state(false);
|
|
||||||
let minQuality = $state<string | undefined>(undefined);
|
|
||||||
let wantedQuality = $state<string | undefined>(undefined);
|
|
||||||
let isSubmittingRequest = $state(false);
|
|
||||||
let submitRequestError = $state<string | null>(null);
|
|
||||||
|
|
||||||
const qualityValues: components['schemas']['Quality'][] = [1, 2, 3, 4];
|
|
||||||
let qualityOptions = $derived(
|
|
||||||
qualityValues.map((q) => ({ value: q.toString(), label: getTorrentQualityString(q) }))
|
|
||||||
);
|
|
||||||
let isFormInvalid = $derived(
|
|
||||||
!minQuality || !wantedQuality || parseInt(wantedQuality) > parseInt(minQuality)
|
|
||||||
);
|
|
||||||
|
|
||||||
async function handleRequestMovie() {
|
|
||||||
isSubmittingRequest = true;
|
|
||||||
submitRequestError = null;
|
|
||||||
const { response } = await client.POST('/api/v1/movies/requests', {
|
|
||||||
body: {
|
|
||||||
movie_id: movie.id!,
|
|
||||||
min_quality: parseInt(minQuality!) as components['schemas']['Quality'],
|
|
||||||
wanted_quality: parseInt(wantedQuality!) as components['schemas']['Quality']
|
|
||||||
}
|
|
||||||
});
|
|
||||||
isSubmittingRequest = false;
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
dialogOpen = false;
|
|
||||||
minQuality = undefined;
|
|
||||||
wantedQuality = undefined;
|
|
||||||
toast.success('Movie request submitted successfully!');
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to submit request');
|
|
||||||
}
|
|
||||||
await invalidateAll();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Dialog.Root bind:open={dialogOpen}>
|
|
||||||
<Dialog.Trigger
|
|
||||||
class={buttonVariants({ variant: 'default' })}
|
|
||||||
onclick={() => {
|
|
||||||
dialogOpen = true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Request Movie
|
|
||||||
</Dialog.Trigger>
|
|
||||||
<Dialog.Content class="max-h-[90vh] w-fit min-w-[clamp(300px,50vw,600px)] overflow-y-auto">
|
|
||||||
<Dialog.Header>
|
|
||||||
<Dialog.Title>Request {getFullyQualifiedMediaName(movie)}</Dialog.Title>
|
|
||||||
<Dialog.Description>Select desired qualities to submit a request.</Dialog.Description>
|
|
||||||
</Dialog.Header>
|
|
||||||
<div class="grid gap-4 py-4">
|
|
||||||
<!-- Min Quality Select -->
|
|
||||||
<div class="grid grid-cols-[1fr_3fr] items-center gap-4 md:grid-cols-[100px_1fr]">
|
|
||||||
<Label class="text-right" for="min-quality">Min Quality</Label>
|
|
||||||
<Select.Root bind:value={minQuality} type="single">
|
|
||||||
<Select.Trigger class="w-full" id="min-quality">
|
|
||||||
{minQuality ? getTorrentQualityString(parseInt(minQuality)) : 'Select Minimum Quality'}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{#each qualityOptions as option (option.value)}
|
|
||||||
<Select.Item value={option.value}>{option.label}</Select.Item>
|
|
||||||
{/each}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Wanted Quality Select -->
|
|
||||||
<div class="grid grid-cols-[1fr_3fr] items-center gap-4 md:grid-cols-[100px_1fr]">
|
|
||||||
<Label class="text-right" for="wanted-quality">Wanted Quality</Label>
|
|
||||||
<Select.Root bind:value={wantedQuality} type="single">
|
|
||||||
<Select.Trigger class="w-full" id="wanted-quality">
|
|
||||||
{wantedQuality
|
|
||||||
? getTorrentQualityString(parseInt(wantedQuality))
|
|
||||||
: 'Select Wanted Quality'}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{#each qualityOptions as option (option.value)}
|
|
||||||
<Select.Item value={option.value}>{option.label}</Select.Item>
|
|
||||||
{/each}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if submitRequestError}
|
|
||||||
<p class="col-span-full text-center text-sm text-red-500">{submitRequestError}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<Dialog.Footer>
|
|
||||||
<Button disabled={isSubmittingRequest} onclick={() => (dialogOpen = false)} variant="outline"
|
|
||||||
>Cancel
|
|
||||||
</Button>
|
|
||||||
<Button disabled={isFormInvalid || isSubmittingRequest} onclick={handleRequestMovie}>
|
|
||||||
{#if isSubmittingRequest}
|
|
||||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Submitting...
|
|
||||||
{:else}
|
|
||||||
Submit Request
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</Dialog.Footer>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { Button, buttonVariants } from '$lib/components/ui/button';
|
|
||||||
import * as Dialog from '$lib/components/ui/dialog';
|
|
||||||
import { Label } from '$lib/components/ui/label';
|
|
||||||
import * as Select from '$lib/components/ui/select';
|
|
||||||
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
|
||||||
import { getFullyQualifiedMediaName, getTorrentQualityString } from '$lib/utils.js';
|
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
import client from '$lib/api';
|
|
||||||
import type { components } from '$lib/api/api';
|
|
||||||
|
|
||||||
let { show }: { show: components['schemas']['PublicShow'] } = $props();
|
|
||||||
|
|
||||||
let dialogOpen = $state(false);
|
|
||||||
let selectedSeasonsIds = $state<string[]>([]);
|
|
||||||
let minQuality = $state<string | undefined>(undefined);
|
|
||||||
let wantedQuality = $state<string | undefined>(undefined);
|
|
||||||
let isSubmittingRequest = $state(false);
|
|
||||||
let submitRequestError = $state<string | null>(null);
|
|
||||||
|
|
||||||
const qualityValues: components['schemas']['Quality'][] = [1, 2, 3, 4];
|
|
||||||
let qualityOptions = $derived(
|
|
||||||
qualityValues.map((q) => ({ value: q.toString(), label: getTorrentQualityString(q) }))
|
|
||||||
);
|
|
||||||
let isFormInvalid = $derived(
|
|
||||||
!selectedSeasonsIds ||
|
|
||||||
selectedSeasonsIds.length === 0 ||
|
|
||||||
!minQuality ||
|
|
||||||
!wantedQuality ||
|
|
||||||
parseInt(wantedQuality) > parseInt(minQuality)
|
|
||||||
);
|
|
||||||
|
|
||||||
async function handleRequestSeason() {
|
|
||||||
isSubmittingRequest = true;
|
|
||||||
submitRequestError = null;
|
|
||||||
|
|
||||||
for (const id of selectedSeasonsIds) {
|
|
||||||
const { response, error } = await client.POST('/api/v1/tv/seasons/requests', {
|
|
||||||
body: {
|
|
||||||
season_id: id,
|
|
||||||
min_quality: parseInt(minQuality!) as components['schemas']['Quality'],
|
|
||||||
wanted_quality: parseInt(wantedQuality!) as components['schemas']['Quality']
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
toast.error('Failed to submit request: ' + error);
|
|
||||||
submitRequestError = `Failed to submit request for season ID ${id}: ${error}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!submitRequestError) {
|
|
||||||
dialogOpen = false;
|
|
||||||
// Reset form fields
|
|
||||||
selectedSeasonsIds = [];
|
|
||||||
minQuality = undefined;
|
|
||||||
wantedQuality = undefined;
|
|
||||||
toast.success('Season request(s) submitted successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
isSubmittingRequest = false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Dialog.Root bind:open={dialogOpen}>
|
|
||||||
<Dialog.Trigger
|
|
||||||
class={buttonVariants({ variant: 'default' })}
|
|
||||||
onclick={() => {
|
|
||||||
dialogOpen = true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Request Season
|
|
||||||
</Dialog.Trigger>
|
|
||||||
<Dialog.Content class="max-h-[90vh] w-fit min-w-[clamp(300px,50vw,600px)] overflow-y-auto">
|
|
||||||
<Dialog.Header>
|
|
||||||
<Dialog.Title>Request a Season for {getFullyQualifiedMediaName(show)}</Dialog.Title>
|
|
||||||
<Dialog.Description>
|
|
||||||
Select a season and desired qualities to submit a request.
|
|
||||||
</Dialog.Description>
|
|
||||||
</Dialog.Header>
|
|
||||||
<div class="grid gap-4 py-4">
|
|
||||||
<!-- Season Select -->
|
|
||||||
<div class="grid grid-cols-[1fr_3fr] items-center gap-4 md:grid-cols-[100px_1fr]">
|
|
||||||
<Label class="text-right" for="season">Season</Label>
|
|
||||||
<Select.Root bind:value={selectedSeasonsIds} type="multiple">
|
|
||||||
<Select.Trigger class="w-full" id="season">
|
|
||||||
{#each selectedSeasonsIds as seasonId (seasonId)}
|
|
||||||
{#if show.seasons.find((season) => season.id === seasonId)}
|
|
||||||
Season {show.seasons.find((season) => season.id === seasonId)?.number},
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
Select one or more seasons
|
|
||||||
{/each}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{#each show.seasons as season (season.id)}
|
|
||||||
<Select.Item value={season.id || ''}>
|
|
||||||
Season {season.number}{season.name ? `: ${season.name}` : ''}
|
|
||||||
</Select.Item>
|
|
||||||
{/each}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Min Quality Select -->
|
|
||||||
<div class="grid grid-cols-[1fr_3fr] items-center gap-4 md:grid-cols-[100px_1fr]">
|
|
||||||
<Label class="text-right" for="min-quality">Min Quality</Label>
|
|
||||||
<Select.Root bind:value={minQuality} type="single">
|
|
||||||
<Select.Trigger class="w-full" id="min-quality">
|
|
||||||
{minQuality ? getTorrentQualityString(parseInt(minQuality)) : 'Select Minimum Quality'}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{#each qualityOptions as option (option.value)}
|
|
||||||
<Select.Item value={option.value}>{option.label}</Select.Item>
|
|
||||||
{/each}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Wanted Quality Select -->
|
|
||||||
<div class="grid grid-cols-[1fr_3fr] items-center gap-4 md:grid-cols-[100px_1fr]">
|
|
||||||
<Label class="text-right" for="wanted-quality">Wanted Quality</Label>
|
|
||||||
<Select.Root bind:value={wantedQuality} type="single">
|
|
||||||
<Select.Trigger class="w-full" id="wanted-quality">
|
|
||||||
{wantedQuality
|
|
||||||
? getTorrentQualityString(parseInt(wantedQuality))
|
|
||||||
: 'Select Wanted Quality'}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
{#each qualityOptions as option (option.value)}
|
|
||||||
<Select.Item value={option.value}>{option.label}</Select.Item>
|
|
||||||
{/each}
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if submitRequestError}
|
|
||||||
<p class="col-span-full text-center text-sm text-red-500">{submitRequestError}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<Dialog.Footer>
|
|
||||||
<Button disabled={isSubmittingRequest} onclick={() => (dialogOpen = false)} variant="outline"
|
|
||||||
>Cancel
|
|
||||||
</Button>
|
|
||||||
<Button disabled={isFormInvalid || isSubmittingRequest} onclick={handleRequestSeason}>
|
|
||||||
{#if isSubmittingRequest}
|
|
||||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Submitting...
|
|
||||||
{:else}
|
|
||||||
Submit Request
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</Dialog.Footer>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { getFullyQualifiedMediaName, getTorrentQualityString } from '$lib/utils.js';
|
|
||||||
import CheckmarkX from '$lib/components/checkmark-x.svelte';
|
|
||||||
import type { components } from '$lib/api/api';
|
|
||||||
|
|
||||||
import * as Table from '$lib/components/ui/table';
|
|
||||||
import { getContext } from 'svelte';
|
|
||||||
import { Button } from '$lib/components/ui/button';
|
|
||||||
import { toast } from 'svelte-sonner';
|
|
||||||
import { goto, invalidateAll } from '$app/navigation';
|
|
||||||
import { resolve } from '$app/paths';
|
|
||||||
import client from '$lib/api';
|
|
||||||
|
|
||||||
let {
|
|
||||||
requests,
|
|
||||||
filter = () => true,
|
|
||||||
isShow = true
|
|
||||||
}: {
|
|
||||||
requests: (
|
|
||||||
| components['schemas']['RichSeasonRequest']
|
|
||||||
| components['schemas']['RichMovieRequest']
|
|
||||||
)[];
|
|
||||||
filter?: (
|
|
||||||
request:
|
|
||||||
| components['schemas']['RichSeasonRequest']
|
|
||||||
| components['schemas']['RichMovieRequest']
|
|
||||||
) => boolean;
|
|
||||||
isShow: boolean;
|
|
||||||
} = $props();
|
|
||||||
const user: () => components['schemas']['UserRead'] = getContext('user');
|
|
||||||
async function approveRequest(requestId: string, currentAuthorizedStatus: boolean) {
|
|
||||||
let response;
|
|
||||||
if (isShow) {
|
|
||||||
const data = await client.PATCH('/api/v1/tv/seasons/requests/{season_request_id}', {
|
|
||||||
params: {
|
|
||||||
path: {
|
|
||||||
season_request_id: requestId
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
authorized_status: !currentAuthorizedStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
response = data.response;
|
|
||||||
} else {
|
|
||||||
const data = await client.PATCH('/api/v1/movies/requests/{movie_request_id}', {
|
|
||||||
params: {
|
|
||||||
path: {
|
|
||||||
movie_request_id: requestId
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
authorized_status: !currentAuthorizedStatus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
response = data.response;
|
|
||||||
}
|
|
||||||
if (response.ok) {
|
|
||||||
toast.success(
|
|
||||||
`Request ${!currentAuthorizedStatus ? 'approved' : 'unapproved'} successfully.`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error(`Failed to update request status ${response.statusText}`, errorText);
|
|
||||||
toast.error(`Failed to update request status: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
await invalidateAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteRequest(requestId: string) {
|
|
||||||
if (
|
|
||||||
!window.confirm(
|
|
||||||
'Are you sure you want to delete this season request? This action cannot be undone.'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let response;
|
|
||||||
if (isShow) {
|
|
||||||
const data = await client.DELETE('/api/v1/tv/seasons/requests/{request_id}', {
|
|
||||||
params: {
|
|
||||||
path: {
|
|
||||||
request_id: requestId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
response = data.response;
|
|
||||||
} else {
|
|
||||||
const data = await client.DELETE('/api/v1/movies/requests/{movie_request_id}', {
|
|
||||||
params: {
|
|
||||||
path: {
|
|
||||||
movie_request_id: requestId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
response = data.response;
|
|
||||||
}
|
|
||||||
if (response.ok) {
|
|
||||||
toast.success('Request deleted successfully');
|
|
||||||
} else {
|
|
||||||
console.error(`Failed to delete request ${response.statusText}`, await response.text());
|
|
||||||
toast.error('Failed to delete request');
|
|
||||||
}
|
|
||||||
await invalidateAll();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Table.Root>
|
|
||||||
<Table.Caption>A list of all requests.</Table.Caption>
|
|
||||||
<Table.Header>
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Head>{isShow ? 'Show' : 'Movie'}</Table.Head>
|
|
||||||
{#if isShow}
|
|
||||||
<Table.Head>Season</Table.Head>
|
|
||||||
{/if}
|
|
||||||
<Table.Head>Minimum Quality</Table.Head>
|
|
||||||
<Table.Head>Wanted Quality</Table.Head>
|
|
||||||
<Table.Head>Requested by</Table.Head>
|
|
||||||
<Table.Head>Approved</Table.Head>
|
|
||||||
<Table.Head>Approved by</Table.Head>
|
|
||||||
<Table.Head>Actions</Table.Head>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body>
|
|
||||||
{#each requests as request (request.id)}
|
|
||||||
{#if filter(request)}
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell>
|
|
||||||
{#if isShow}
|
|
||||||
<a
|
|
||||||
href={resolve('/dashboard/tv/[showId]', {
|
|
||||||
showId: (request as components['schemas']['RichSeasonRequest']).show.id!
|
|
||||||
})}
|
|
||||||
class="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
{getFullyQualifiedMediaName(
|
|
||||||
(request as components['schemas']['RichSeasonRequest']).show
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
{:else}
|
|
||||||
<a
|
|
||||||
href={resolve('/dashboard/movies/[movieId]', {
|
|
||||||
movieId: (request as components['schemas']['RichMovieRequest']).movie.id!
|
|
||||||
})}
|
|
||||||
class="text-primary hover:underline"
|
|
||||||
>
|
|
||||||
{getFullyQualifiedMediaName(
|
|
||||||
(request as components['schemas']['RichMovieRequest']).movie
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
</Table.Cell>
|
|
||||||
{#if isShow}
|
|
||||||
<Table.Cell>
|
|
||||||
{(request as components['schemas']['RichSeasonRequest']).season.number}
|
|
||||||
</Table.Cell>
|
|
||||||
{/if}
|
|
||||||
<Table.Cell>
|
|
||||||
{getTorrentQualityString(request.min_quality)}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
{getTorrentQualityString(request.wanted_quality)}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
{request.requested_by?.email ?? 'N/A'}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
<CheckmarkX state={request.authorized} />
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
{request.authorized_by?.email ?? 'N/A'}
|
|
||||||
</Table.Cell>
|
|
||||||
<!-- TODO: ADD DIALOGUE TO MODIFY REQUEST -->
|
|
||||||
<Table.Cell class="flex max-w-[150px] flex-col gap-1">
|
|
||||||
{#if user().is_superuser}
|
|
||||||
<Button
|
|
||||||
class=""
|
|
||||||
size="sm"
|
|
||||||
onclick={() => approveRequest(request.id!, request.authorized)}
|
|
||||||
>
|
|
||||||
{request.authorized ? 'Unapprove' : 'Approve'}
|
|
||||||
</Button>
|
|
||||||
{#if isShow}
|
|
||||||
<Button
|
|
||||||
class=""
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onclick={() =>
|
|
||||||
goto(
|
|
||||||
resolve('/dashboard/tv/[showId]', {
|
|
||||||
showId: (request as components['schemas']['RichSeasonRequest']).show.id!
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Download manually
|
|
||||||
</Button>
|
|
||||||
{:else}
|
|
||||||
<Button
|
|
||||||
class=""
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onclick={() =>
|
|
||||||
goto(
|
|
||||||
resolve('/dashboard/movies/[movieId]', {
|
|
||||||
movieId: (request as components['schemas']['RichMovieRequest']).movie.id!
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Download manually
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{#if user().is_superuser || user().id === request.requested_by?.id}
|
|
||||||
<Button variant="destructive" size="sm" onclick={() => deleteRequest(request.id!)}
|
|
||||||
>Delete
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell colspan={8} class="text-center">There are currently no requests.</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
{/each}
|
|
||||||
</Table.Body>
|
|
||||||
</Table.Root>
|
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
import TorrentTable from '$lib/components/torrents/torrent-table.svelte';
|
import TorrentTable from '$lib/components/torrents/torrent-table.svelte';
|
||||||
import MediaPicture from '$lib/components/media-picture.svelte';
|
import MediaPicture from '$lib/components/media-picture.svelte';
|
||||||
import DownloadMovieDialog from '$lib/components/download-dialogs/download-movie-dialog.svelte';
|
import DownloadMovieDialog from '$lib/components/download-dialogs/download-movie-dialog.svelte';
|
||||||
import RequestMovieDialog from '$lib/components/requests/request-movie-dialog.svelte';
|
|
||||||
import LibraryCombobox from '$lib/components/library-combobox.svelte';
|
import LibraryCombobox from '$lib/components/library-combobox.svelte';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import * as Card from '$lib/components/ui/card/index.js';
|
import * as Card from '$lib/components/ui/card/index.js';
|
||||||
@@ -108,7 +107,6 @@
|
|||||||
{#if user().is_superuser}
|
{#if user().is_superuser}
|
||||||
<DownloadMovieDialog {movie} />
|
<DownloadMovieDialog {movie} />
|
||||||
{/if}
|
{/if}
|
||||||
<RequestMovieDialog {movie} />
|
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { page } from '$app/state';
|
|
||||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
|
||||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
|
||||||
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
|
|
||||||
import RequestsTable from '$lib/components/requests/requests-table.svelte';
|
|
||||||
import { resolve } from '$app/paths';
|
|
||||||
|
|
||||||
let requests = $derived(page.data.requestsData);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>Movie Requests - MediaManager</title>
|
|
||||||
<meta content="View and manage movie download requests in MediaManager" name="description" />
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<header class="flex h-16 shrink-0 items-center gap-2">
|
|
||||||
<div class="flex items-center gap-2 px-4">
|
|
||||||
<Sidebar.Trigger class="-ml-1" />
|
|
||||||
<Separator class="mr-2 h-4" orientation="vertical" />
|
|
||||||
<Breadcrumb.Root>
|
|
||||||
<Breadcrumb.List>
|
|
||||||
<Breadcrumb.Item class="hidden md:block">
|
|
||||||
<Breadcrumb.Link href={resolve('/dashboard', {})}>MediaManager</Breadcrumb.Link>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Separator class="hidden md:block" />
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Link href={resolve('/dashboard', {})}>Home</Breadcrumb.Link>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Separator class="hidden md:block" />
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Link href={resolve('/dashboard/movies', {})}>Movies</Breadcrumb.Link>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Separator class="hidden md:block" />
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Page>Movie Requests</Breadcrumb.Page>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
</Breadcrumb.List>
|
|
||||||
</Breadcrumb.Root>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]">
|
|
||||||
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
|
|
||||||
Movie Requests
|
|
||||||
</h1>
|
|
||||||
<RequestsTable {requests} isShow={false} />
|
|
||||||
</main>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { PageLoad } from './$types';
|
|
||||||
import client from '$lib/api';
|
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch }) => {
|
|
||||||
const { data } = await client.GET('/api/v1/movies/requests', { fetch: fetch });
|
|
||||||
|
|
||||||
return {
|
|
||||||
requestsData: data
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
import CheckmarkX from '$lib/components/checkmark-x.svelte';
|
import CheckmarkX from '$lib/components/checkmark-x.svelte';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import TorrentTable from '$lib/components/torrents/torrent-table.svelte';
|
import TorrentTable from '$lib/components/torrents/torrent-table.svelte';
|
||||||
import RequestSeasonDialog from '$lib/components/requests/request-season-dialog.svelte';
|
|
||||||
import MediaPicture from '$lib/components/media-picture.svelte';
|
import MediaPicture from '$lib/components/media-picture.svelte';
|
||||||
import { Switch } from '$lib/components/ui/switch/index.js';
|
import { Switch } from '$lib/components/ui/switch/index.js';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
@@ -241,7 +240,6 @@
|
|||||||
<DownloadCustomDialog {show} />
|
<DownloadCustomDialog {show} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<RequestSeasonDialog {show} />
|
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { page } from '$app/state';
|
|
||||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
|
||||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
|
||||||
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
|
|
||||||
import RequestsTable from '$lib/components/requests/requests-table.svelte';
|
|
||||||
import { resolve } from '$app/paths';
|
|
||||||
import type { components } from '$lib/api/api';
|
|
||||||
|
|
||||||
let requests: components['schemas']['RichSeasonRequest'][] = $derived(page.data.requestsData);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>TV Show Requests - MediaManager</title>
|
|
||||||
<meta content="View and manage TV show download requests in MediaManager" name="description" />
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<header class="flex h-16 shrink-0 items-center gap-2">
|
|
||||||
<div class="flex items-center gap-2 px-4">
|
|
||||||
<Sidebar.Trigger class="-ml-1" />
|
|
||||||
<Separator class="mr-2 h-4" orientation="vertical" />
|
|
||||||
<Breadcrumb.Root>
|
|
||||||
<Breadcrumb.List>
|
|
||||||
<Breadcrumb.Item class="hidden md:block">
|
|
||||||
<Breadcrumb.Link href={resolve('/dashboard', {})}>MediaManager</Breadcrumb.Link>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Separator class="hidden md:block" />
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Link href={resolve('/dashboard', {})}>Home</Breadcrumb.Link>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Separator class="hidden md:block" />
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Link href={resolve('/dashboard/tv', {})}>Shows</Breadcrumb.Link>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Separator class="hidden md:block" />
|
|
||||||
<Breadcrumb.Item>
|
|
||||||
<Breadcrumb.Page>Season Requests</Breadcrumb.Page>
|
|
||||||
</Breadcrumb.Item>
|
|
||||||
</Breadcrumb.List>
|
|
||||||
</Breadcrumb.Root>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="mx-auto flex w-full flex-1 flex-col gap-4 p-4 md:max-w-[80em]">
|
|
||||||
<h1 class="scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
|
|
||||||
Season Requests
|
|
||||||
</h1>
|
|
||||||
<RequestsTable {requests} isShow={true} />
|
|
||||||
</main>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { PageLoad } from './$types';
|
|
||||||
import client from '$lib/api';
|
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch }) => {
|
|
||||||
const { data } = await client.GET('/api/v1/tv/seasons/requests', { fetch: fetch });
|
|
||||||
return {
|
|
||||||
requestsData: data
|
|
||||||
};
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user