Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""create episode file table and add episode column to indexerqueryresult

Revision ID: 3a8fbd71e2c2
Revises: 9f3c1b2a4d8e
Create Date: 2026-01-08 13:43:00

"""

from typing import Sequence, Union

from alembic import op
from sqlalchemy.dialects import postgresql
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "3a8fbd71e2c2"
down_revision: Union[str, None] = "9f3c1b2a4d8e"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None



def upgrade() -> None:
quality_enum = postgresql.ENUM("uhd", "fullhd", "hd", "sd", "unknown", name="quality",
create_type=False,
)
# Create episode file table
op.create_table(
"episode_file",
sa.Column("episode_id", sa.UUID(), nullable=False),
sa.Column("torrent_id", sa.UUID(), nullable=True),
sa.Column("file_path_suffix", sa.String(), nullable=False),
sa.Column("quality", quality_enum, nullable=False),
sa.ForeignKeyConstraint(["episode_id"], ["episode.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["torrent_id"], ["torrent.id"], ondelete="SET NULL"),
sa.PrimaryKeyConstraint("episode_id", "file_path_suffix"),
)
# Add episode column to indexerqueryresult
op.add_column(
"indexer_query_result", sa.Column("episode", postgresql.ARRAY(sa.Integer()), nullable=True),
)

def downgrade() -> None:
op.drop_table("episode_file")
op.drop_column("indexer_query_result", "episode")
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""add overview column to episode table

Revision ID: 9f3c1b2a4d8e
Revises: 2c61f662ca9e
Create Date: 2025-12-29 21:45:00

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "9f3c1b2a4d8e"
down_revision: Union[str, None] = "2c61f662ca9e"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# Add overview to episode table
op.add_column(
"episode",
sa.Column("overview", sa.Text(), nullable=True),
)

def downgrade() -> None:
op.drop_column("episode", "overview")

1 change: 1 addition & 0 deletions media_manager/indexer/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class IndexerQueryResult(Base):
flags = mapped_column(ARRAY(String))
quality: Mapped[Quality]
season = mapped_column(ARRAY(Integer))
episode = mapped_column(ARRAY(Integer))
size = mapped_column(BigInteger)
usenet: Mapped[bool]
age: Mapped[int]
Expand Down
59 changes: 52 additions & 7 deletions media_manager/indexer/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,59 @@ def quality(self) -> Quality:
@computed_field
@property
def season(self) -> list[int]:
pattern = r"\bS(\d+)\b"
matches = re.findall(pattern, self.title, re.IGNORECASE)
if matches.__len__() == 2:
result = list(range(int(matches[0]), int(matches[1]) + 1))
elif matches.__len__() == 1:
result = [int(matches[0])]
title = self.title.lower()
result: list[int] = []

# 1) S01E01 / S1E2
m = re.search(r"s(\d{1,2})e\d{1,3}", title)
if m:
result = [int(m.group(1))]
return result

# 2) Range S01-S03 / S1-S3
m = re.search(r"s(\d{1,2})\s*[-–]\s*s?(\d{1,2})", title)
if m:
start, end = int(m.group(1)), int(m.group(2))
if start <= end:
result = list(range(start, end + 1))
return result

# 3) Pack S01 / S1
m = re.search(r"\bs(\d{1,2})\b", title)
if m:
result = [int(m.group(1))]
return result

# 4) Season 01 / Season 1
m = re.search(r"\bseason\s*(\d{1,2})\b", title)
if m:
result = [int(m.group(1))]
return result

return result

@computed_field(return_type=list[int])
@property
def episode(self) -> list[int]:
title = self.title.lower()
result: list[int] = []

pattern = r"s\d{1,2}e(\d{1,3})(?:\s*-\s*(?:s?\d{1,2}e)?(\d{1,3}))?"
match = re.search(pattern, title)

if not match:
return result

start = int(match.group(1))
end = match.group(2)

if end:
end = int(end)
if end >= start:
result = list(range(start, end + 1))
else:
result = []
result = [start]

return result

def __gt__(self, other: "IndexerQueryResult") -> bool:
Expand Down
1 change: 1 addition & 0 deletions media_manager/torrent/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ class Torrent(Base):
usenet: Mapped[bool]

season_files = relationship("SeasonFile", back_populates="torrent")
episode_files = relationship("EpisodeFile", back_populates="torrent")
movie_files = relationship("MovieFile", back_populates="torrent")
29 changes: 17 additions & 12 deletions media_manager/torrent/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@
MovieFile as MovieFileSchema,
)
from media_manager.torrent.models import Torrent
from media_manager.torrent.schemas import Torrent as TorrentSchema
from media_manager.torrent.schemas import TorrentId
from media_manager.tv.models import Season, SeasonFile, Show
from media_manager.tv.schemas import SeasonFile as SeasonFileSchema
from media_manager.tv.schemas import Show as ShowSchema

from media_manager.torrent.schemas import TorrentId, Torrent as TorrentSchema
from media_manager.tv.models import SeasonFile, Show, Season, EpisodeFile, Episode
from media_manager.tv.schemas import SeasonFile as SeasonFileSchema, Show as ShowSchema, EpisodeFile as EpisodeFileSchema

class TorrentRepository:
def __init__(self, db: DbSessionDependency) -> None:
Expand All @@ -28,12 +25,20 @@ def get_seasons_files_of_torrent(
result = self.db.execute(stmt).scalars().all()
return [SeasonFileSchema.model_validate(season_file) for season_file in result]

def get_episode_files_of_torrent(
self, torrent_id: TorrentId
) -> list[EpisodeFileSchema]:
stmt = select(EpisodeFile).where(EpisodeFile.torrent_id == torrent_id)
result = self.db.execute(stmt).scalars().all()
return [EpisodeFileSchema.model_validate(episode_file) for episode_file in result]

def get_show_of_torrent(self, torrent_id: TorrentId) -> ShowSchema | None:
stmt = (
select(Show)
.join(SeasonFile.season)
.join(Season.show)
.where(SeasonFile.torrent_id == torrent_id)
.join(Show.seasons)
.join(Season.episodes)
.join(Episode.episode_files)
.where(EpisodeFile.torrent_id == torrent_id)
)
result = self.db.execute(stmt).unique().scalar_one_or_none()
if result is None:
Expand Down Expand Up @@ -69,10 +74,10 @@ def delete_torrent(
)
self.db.execute(movie_files_stmt)

season_files_stmt = delete(SeasonFile).where(
SeasonFile.torrent_id == torrent_id
episode_files_stmt = delete(EpisodeFile).where(
EpisodeFile.torrent_id == torrent_id
)
self.db.execute(season_files_stmt)
self.db.execute(episode_files_stmt)

self.db.delete(self.db.get(Torrent, torrent_id))

Expand Down
13 changes: 12 additions & 1 deletion media_manager/torrent/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from media_manager.torrent.manager import DownloadManager
from media_manager.torrent.repository import TorrentRepository
from media_manager.torrent.schemas import Torrent, TorrentId
from media_manager.tv.schemas import SeasonFile, Show
from media_manager.tv.schemas import SeasonFile, Show, EpisodeFile
from media_manager.movies.schemas import Movie

log = logging.getLogger(__name__)

Expand All @@ -29,6 +30,16 @@ def get_season_files_of_torrent(self, torrent: Torrent) -> list[SeasonFile]:
torrent_id=torrent.id
)

def get_episode_files_of_torrent(self, torrent: Torrent) -> list[EpisodeFile]:
"""
Returns all episode files of a torrent
:param torrent: the torrent to get the episode files of
:return: list of episode files
"""
return self.torrent_repository.get_episode_files_of_torrent(
torrent_id=torrent.id
)

def get_show_of_torrent(self, torrent: Torrent) -> Show | None:
"""
Returns the show of a torrent
Expand Down
19 changes: 19 additions & 0 deletions media_manager/tv/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,12 @@ class Episode(Base):
number: Mapped[int]
external_id: Mapped[int]
title: Mapped[str]
overview: Mapped[str]

season: Mapped["Season"] = relationship(back_populates="episodes")
episode_files = relationship(
"EpisodeFile", back_populates="episode", cascade="all, delete"
)


class SeasonFile(Base):
Expand All @@ -86,6 +90,21 @@ class SeasonFile(Base):
season = relationship("Season", back_populates="season_files", uselist=False)


class EpisodeFile(Base):
__tablename__ = "episode_file"
__table_args__ = (PrimaryKeyConstraint("episode_id", "file_path_suffix"),)
episode_id: Mapped[UUID] = mapped_column(
ForeignKey(column="episode.id", ondelete="CASCADE"),
)
torrent_id: Mapped[UUID | None] = mapped_column(
ForeignKey(column="torrent.id", ondelete="SET NULL"),
)
file_path_suffix: Mapped[str]
quality: Mapped[Quality]

torrent = relationship("Torrent", back_populates="episode_files", uselist=False)
episode = relationship("Episode", back_populates="episode_files", uselist=False)

class SeasonRequest(Base):
__tablename__ = "season_request"
__table_args__ = (UniqueConstraint("season_id", "wanted_quality"),)
Expand Down
Loading