Skip to content
Open
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
37 changes: 37 additions & 0 deletions alembic/versions/add_air_date_to_season_and_movie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Add air_date to Season and Movie tables

Revision ID: f1a2b3c4d5e6
Revises: eb0bd3cc1852
Create Date: 2025-11-02 21:49:00.000000

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = "f1a2b3c4d5e6"
down_revision: Union[str, None] = "eb0bd3cc1852"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
op.add_column(
"season",
sa.Column("air_date", sa.Date(), nullable=True),
)
op.add_column(
"movie",
sa.Column("air_date", sa.Date(), nullable=True),
)


def downgrade() -> None:
"""Downgrade schema."""
op.drop_column("season", "air_date")
op.drop_column("movie", "air_date")
5 changes: 5 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ torrent_directory = "/data/torrents" # this is where MediaManager will search fo
# you probaly don't need to change this
development = false

# Air date check settings - prevent downloading content that hasn't aired/been released yet
# This helps avoid fake releases on public indexers
prevent_unaired_tv_downloads = false
prevent_unaired_movie_downloads = false

# Custom Media Libraries
# These paths should match your volume mounts in docker-compose.yaml
# Example: if you mount "./movies:/media/movies" then use path = "/media/movies/subdirectory"
Expand Down
4 changes: 4 additions & 0 deletions media_manager/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class BasicConfig(BaseSettings):
tv_libraries: list[LibraryItem] = []
movie_libraries: list[LibraryItem] = []

# Air date check settings
prevent_unaired_tv_downloads: bool = False
prevent_unaired_movie_downloads: bool = False


class AllEncompassingConfig(BaseSettings):
model_config = SettingsConfigDict(
Expand Down
19 changes: 19 additions & 0 deletions media_manager/metadataProvider/tmdb.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from datetime import datetime

import requests

Expand Down Expand Up @@ -177,13 +178,22 @@ def get_show_metadata(self, id: int = None) -> Show:
)
)

# Parse air_date
air_date = None
if season_metadata.get("air_date"):
try:
air_date = datetime.strptime(season_metadata["air_date"], "%Y-%m-%d").date()
except (ValueError, TypeError):
log.warning(f"Could not parse air_date for season {season_metadata['season_number']}: {season_metadata.get('air_date')}")

season_list.append(
Season(
external_id=int(season_metadata["id"]),
name=season_metadata["name"],
overview=season_metadata["overview"],
number=SeasonNumber(season_metadata["season_number"]),
episodes=episode_list,
air_date=air_date,
)
)

Expand Down Expand Up @@ -262,11 +272,20 @@ def get_movie_metadata(self, id: int = None) -> Movie:
movie_metadata["release_date"]
)

# Parse air_date (release_date for movies)
air_date = None
if movie_metadata.get("release_date"):
try:
air_date = datetime.strptime(movie_metadata["release_date"], "%Y-%m-%d").date()
except (ValueError, TypeError):
log.warning(f"Could not parse release_date for movie {id}: {movie_metadata.get('release_date')}")

movie = Movie(
external_id=id,
name=movie_metadata["title"],
overview=movie_metadata["overview"],
year=year,
air_date=air_date,
metadata_provider=self.name,
)

Expand Down
28 changes: 24 additions & 4 deletions media_manager/metadataProvider/tvdb.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import requests
import logging
from datetime import datetime


import media_manager.metadataProvider.utils
Expand Down Expand Up @@ -89,13 +90,23 @@ def get_show_metadata(self, id: int = None) -> Show:
)
for episode in s["episodes"]
]

# Parse air_date if available
air_date = None
if s.get("air_date"):
try:
air_date = datetime.strptime(s["air_date"], "%Y-%m-%d").date()
except (ValueError, TypeError):
log.warning(f"Could not parse air_date for season {s['number']}: {s.get('air_date')}")

seasons.append(
Season(
number=SeasonNumber(s["number"]),
name="TVDB doesn't provide Season Names",
overview="TVDB doesn't provide Season Overviews",
external_id=int(s["id"]),
episodes=episodes,
air_date=air_date,
)
)
try:
Expand Down Expand Up @@ -259,17 +270,26 @@ def get_movie_metadata(self, id: int = None) -> Movie:
:return: returns a Movie object
:rtype: Movie
"""
movie = self.__get_movie(id)
movie_data = self.__get_movie(id)
try:
year = movie["year"]
year = movie_data["year"]
except KeyError:
year = None

# Parse air_date if available
air_date = None
if movie_data.get("release_date"):
try:
air_date = datetime.strptime(movie_data["release_date"], "%Y-%m-%d").date()
except (ValueError, TypeError):
log.warning(f"Could not parse release_date for movie {id}: {movie_data.get('release_date')}")

movie = Movie(
name=movie["name"],
name=movie_data["name"],
overview="TVDB does not provide overviews",
year=year,
external_id=movie["id"],
air_date=air_date,
external_id=movie_data["id"],
metadata_provider=self.name,
)

Expand Down
2 changes: 2 additions & 0 deletions media_manager/movies/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from uuid import UUID
from datetime import date

from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
Expand All @@ -18,6 +19,7 @@ class Movie(Base):
name: Mapped[str]
overview: Mapped[str]
year: Mapped[int | None]
air_date: Mapped[date | None]
library: Mapped[str] = mapped_column(default="")
movie_requests: Mapped[list["MovieRequest"]] = relationship(
"MovieRequest", back_populates="movie", cascade="all, delete-orphan"
Expand Down
2 changes: 2 additions & 0 deletions media_manager/movies/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import typing
import uuid
from uuid import UUID
from datetime import date

from pydantic import BaseModel, Field, ConfigDict, model_validator

Expand All @@ -19,6 +20,7 @@ class Movie(BaseModel):
name: str
overview: str
year: int | None
air_date: date | None = None

external_id: int
metadata_provider: str
Expand Down
12 changes: 12 additions & 0 deletions media_manager/movies/service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
from pathlib import Path
from datetime import date

from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
Expand Down Expand Up @@ -421,6 +422,17 @@ def download_approved_movie_request(

log.info(f"Downloading approved movie request {movie_request.id}")

# Check if movie has aired (if air date check is enabled)
config = AllEncompassingConfig()
if config.misc.prevent_unaired_movie_downloads:
if movie.air_date is not None:
today = date.today()
if movie.air_date > today:
log.info(
f"Skipping movie {movie.name} because it hasn't been released yet (release date: {movie.air_date})"
)
return False

torrents = self.get_all_available_torrents_for_a_movie(movie_id=movie.id)
available_torrents: list[IndexerQueryResult] = []

Expand Down
2 changes: 2 additions & 0 deletions media_manager/tv/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from uuid import UUID
from datetime import date

from sqlalchemy import ForeignKey, PrimaryKeyConstraint, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
Expand Down Expand Up @@ -39,6 +40,7 @@ class Season(Base):
external_id: Mapped[int]
name: Mapped[str]
overview: Mapped[str]
air_date: Mapped[date | None]

show: Mapped["Show"] = relationship(back_populates="seasons")
episodes: Mapped[list["Episode"]] = relationship(
Expand Down
3 changes: 3 additions & 0 deletions media_manager/tv/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import typing
import uuid
from uuid import UUID
from datetime import date

from pydantic import BaseModel, Field, ConfigDict, model_validator

Expand Down Expand Up @@ -36,6 +37,7 @@ class Season(BaseModel):
overview: str

external_id: int
air_date: date | None = None

episodes: list[Episode]

Expand Down Expand Up @@ -145,6 +147,7 @@ class PublicSeason(BaseModel):
overview: str

external_id: int
air_date: date | None = None

episodes: list[Episode]

Expand Down
13 changes: 13 additions & 0 deletions media_manager/tv/service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from datetime import date

from sqlalchemy.exc import IntegrityError

Expand Down Expand Up @@ -467,6 +468,18 @@ def download_approved_season_request(
log.info(f"Downloading approved season request {season_request.id}")

season = self.get_season(season_id=season_request.season_id)

# Check if season has aired (if air date check is enabled)
config = AllEncompassingConfig()
if config.misc.prevent_unaired_tv_downloads:
if season.air_date is not None:
today = date.today()
if season.air_date > today:
log.info(
f"Skipping season {season.number} of show {show.name} because it hasn't aired yet (air date: {season.air_date})"
)
return False

torrents = self.get_all_available_torrents_for_a_season(
season_number=season.number, show_id=show.id
)
Expand Down