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
7 changes: 7 additions & 0 deletions src/program/db/db_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,13 @@ def run_thread_with_db_item(
)
program.em.remove_id_from_queues(input_item.id)

if item.is_excluded:
logger.trace(
f"Item {item.log_string} is excluded, deleting from DB."
)

session.delete(item)

if not cancellation_event.is_set():
# Update parent item based on type
if input_item.type == "episode":
Expand Down
8 changes: 8 additions & 0 deletions src/program/managers/event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ def submit_job(self, service, program, event=None):
program (Program): The program containing the service.
item (Event, optional): The event item to process. Defaults to None.
"""

if event and event.content_item and event.content_item.is_excluded:
logger.debug(
f"Event {event.log_message if event else 'N/A'} is excluded from {service.__name__}, skipping submission."
)

return

log_message = f"Submitting service {service.__name__} to be executed"
# Content services dont provide an event.
if event:
Expand Down
24 changes: 20 additions & 4 deletions src/program/media/item.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""MediaItem class"""

from datetime import datetime
from functools import cached_property
from typing import Any, List, Optional, TYPE_CHECKING

import sqlalchemy
Expand Down Expand Up @@ -321,6 +322,21 @@ def schedule(

return False

@cached_property
def is_excluded(self) -> bool:
"""Check if the item is excluded based on its IDs."""

from program.utils.exclusions import exclusions

is_excluded = exclusions.is_excluded(self)

if is_excluded:
logger.trace(
f"Item {self.log_string} is being excluded as the ID was found in exclusions."
)

return is_excluded

@property
def is_released(self) -> bool:
"""Check if an item has been released."""
Expand Down Expand Up @@ -743,7 +759,7 @@ def __init__(self, item):
super().__init__(item)

def __repr__(self):
return f"Movie:{self.log_string}:{self.state.name}"
return f"Movie [tmdb: {self.tmdb_id} | imdb: {self.imdb_id}]: {self.log_string} - {self.state.name}"

def __hash__(self):
return super().__hash__()
Expand Down Expand Up @@ -823,7 +839,7 @@ def store_state(self, given_state: States = None) -> tuple[States, States]:
return super().store_state(given_state)

def __repr__(self):
return f"Show:{self.log_string}:{self.state.name}"
return f"Show [tvdb: {self.tvdb_id}]: #{self.log_string} - {self.state.name}"

def __hash__(self):
return super().__hash__()
Expand Down Expand Up @@ -970,7 +986,7 @@ def __getattribute__(self, name):
return value

def __repr__(self):
return f"Season:{self.number}:{self.state.name}"
return f"Season [tvdb: {self.tvdb_id}]: {self.log_string} #{self.number} - {self.state.name}"

def __hash__(self):
return super().__hash__()
Expand Down Expand Up @@ -1037,7 +1053,7 @@ def __init__(self, item):
super().__init__(item)

def __repr__(self):
return f"Episode:{self.number}:{self.state.name}"
return f"Episode [tvdb: {self.tvdb_id}]: {self.log_string} #{self.number} - {self.state.name}"

def __hash__(self):
return super().__hash__()
Expand Down
1 change: 1 addition & 0 deletions src/program/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def initialize_services(self):

# Instantiate services fresh on each settings change; settings_manager observers handle reinit
_downloader = Downloader()

self.services = {
IndexerService: IndexerService(),
Scraping: Scraping(),
Expand Down
4 changes: 1 addition & 3 deletions src/program/services/filesystem/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
Common utilities used by FilesystemService (VFS-only).
"""

from typing import List

from program.media.item import Episode, MediaItem, Movie, Season, Show
from program.media.state import States


def get_items_to_update(item: MediaItem) -> List[MediaItem]:
def get_items_to_update(item: MediaItem) -> list[MediaItem]:
"""Return leaf items to process (movies/episodes), expanding shows/seasons.
Only include episodes that have reached Downloaded state for parent inputs.
"""
Expand Down
7 changes: 7 additions & 0 deletions src/program/services/filesystem/filesystem_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(self, downloader: Downloader):

def _initialize_rivenvfs(self, downloader: Downloader):
"""Initialize or synchronize RivenVFS"""

try:
from .vfs import RivenVFS

Expand Down Expand Up @@ -76,6 +77,12 @@ def run(self, item: MediaItem) -> Generator[MediaItem, None, None]:

# Process each episode/movie
for episode_or_movie in items_to_process:
if item.is_excluded:
logger.debug(
f"Item {episode_or_movie.log_string} is excluded from filesystem processing, skipping."
)
continue # Item is excluded, skip processing

success = self.riven_vfs.add(episode_or_movie)

if not success:
Expand Down
25 changes: 24 additions & 1 deletion src/program/services/filesystem/vfs/rivenvfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,14 @@ def add(self, item: "MediaItem") -> bool:
Returns:
True if successfully added, False otherwise
"""

if item.is_excluded:
logger.info(
f"Excluding {item.log_string} from VFS add based on exclusion rules"
)

return False

from program.media.media_entry import MediaEntry

# Only process if this item has a filesystem entry
Expand Down Expand Up @@ -744,6 +752,7 @@ def _sync_full(self) -> None:

item_ids = []
rematched_count = 0
excluded_item_ids = set()

with db_module.Session() as session:
entries = (
Expand All @@ -759,6 +768,10 @@ def _sync_full(self) -> None:
)
continue

if item.is_excluded:
excluded_item_ids.add(item.id)
continue

# Re-match library profiles based on current settings
new_profiles = matcher.get_matching_profiles(item)
old_profiles = entry.library_profiles or []
Expand All @@ -773,10 +786,14 @@ def _sync_full(self) -> None:
item_ids.append(item.id)

session.commit()
logger.debug(f"Re-matched {rematched_count} entries with updated profiles")

logger.debug(
f"Re-matched {rematched_count} entries with updated profiles; excluded {len(excluded_item_ids)} items due to excluded_items settings."
)

# Step 2: Clear VFS tree and rebuild from scratch
logger.debug("Clearing VFS tree for rebuild")

with self._tree_lock:
# Create new root node
self._root = VFSRoot()
Expand Down Expand Up @@ -857,6 +874,12 @@ def _sync_individual(self, item: "MediaItem") -> None:
Args:
item: MediaItem to re-sync
"""

if item.is_excluded:
logger.debug(f"Item {item.id} is excluded, skipping individual sync")

return

from sqlalchemy.orm import object_session
from program.db.db import db as db_module

Expand Down
3 changes: 3 additions & 0 deletions src/program/services/indexers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ def run(
logger.error("Item is None")
return

if in_item.is_excluded:
return

item_type = in_item.type or "mediaitem"

if item_type == "movie" or (in_item.tmdb_id and not in_item.tvdb_id):
Expand Down
2 changes: 1 addition & 1 deletion src/program/services/indexers/tvdb_indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from program.apis.trakt_api import TraktAPI
from program.media.item import Episode, MediaItem, Season, Show
from program.services.indexers.base import BaseIndexer
from program.settings.manager import settings_manager


class TVDBIndexer(BaseIndexer):
Expand All @@ -21,6 +20,7 @@ class TVDBIndexer(BaseIndexer):

def __init__(self):
super().__init__()

self.key = "tvdbindexer"
self.api = di[TVDBApi]
self.trakt_api = di[TraktAPI]
Expand Down
1 change: 1 addition & 0 deletions src/program/services/scrapers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def __init__(self):
service for service in self.services if service.initialized
]
self.initialized = self.validate()

if not self.initialized:
return

Expand Down
1 change: 0 additions & 1 deletion src/program/services/scrapers/jackett.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Jackett scraper module"""

import concurrent.futures
from types import SimpleNamespace
from typing import Dict, List, Optional

from loguru import logger
Expand Down
13 changes: 12 additions & 1 deletion src/program/services/scrapers/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,20 @@


def _parse_results(
item: MediaItem, results: Dict[str, str], log_msg: bool = True
item: MediaItem,
results: Dict[str, str],
log_msg: bool = True,
) -> Dict[str, Stream]:
"""Parse the results from the scrapers into Torrent objects."""

torrents: Set[Torrent] = set()
processed_infohashes: Set[str] = set()
correct_title: str = item.get_top_title()

aliases: Dict[str, list[str]] = (
item.get_aliases() if scraping_settings.enable_aliases else {}
)

# we should remove keys from aliases if we are excluding the language
aliases = {
k: v for k, v in aliases.items() if k not in ranking_settings.languages.exclude
Expand All @@ -45,6 +49,13 @@ def _parse_results(
if infohash in processed_infohashes:
continue

if infohash in settings_manager.settings.filesystem.excluded_items.infohashes:
logger.trace(
f"Skipping torrent for {item.log_string} due to excluded infohash: {infohash}"
)

continue

try:
torrent: Torrent = rtn.rank(
raw_title=raw_title,
Expand Down
8 changes: 7 additions & 1 deletion src/program/settings/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,18 @@ def validate_library_path(cls, v):
return v


class ExcludedItems(BaseModel):
shows: set[str] = Field(default_factory=set)
movies: set[str] = Field(default_factory=set)
infohashes: set[str] = Field(default_factory=set)


class FilesystemModel(Observable):
mount_path: Path = Field(
default=Path("/path/to/riven/mount"),
description="Path where Riven will mount the virtual filesystem",
)

excluded_items: ExcludedItems = Field(default_factory=ExcludedItems)
library_profiles: dict[str, LibraryProfile] = Field(
default_factory=lambda: {
"anime": LibraryProfile(
Expand Down
2 changes: 1 addition & 1 deletion src/program/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class ProcessedEvent:
class Event:
emitted_by: Service
item_id: Optional[str] = None
content_item: Optional[MediaItem] = None
content_item: "MediaItem | None" = None
run_at: datetime = datetime.now()
item_state: Optional[str] = None # Cached state for priority sorting

Expand Down
42 changes: 42 additions & 0 deletions src/program/utils/exclusions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing import TYPE_CHECKING

from program.media.item import Movie, Show
from program.settings.manager import settings_manager

if TYPE_CHECKING:
from program.media.item import MediaItem


class Exclusions:
excluded_shows: set[str]
excluded_movies: set[str]

def __init__(self):
excluded_items = settings_manager.settings.filesystem.excluded_items

self.excluded_movies = excluded_items.movies
self.excluded_shows = excluded_items.shows

def is_excluded(self, item: "MediaItem") -> bool:
is_excluded_movie = self._is_excluded_movie(item)
is_excluded_show = self._is_excluded_show(item._get_top_parent())

return is_excluded_movie or is_excluded_show

def _is_excluded_show(self, item: Show) -> bool:
if item.tvdb_id is None:
return False

return str(item.tvdb_id) in self.excluded_shows

def _is_excluded_movie(self, item: Movie) -> bool:
if item.tmdb_id is None and item.imdb_id is None:
return False

return (
str(item.tmdb_id) in self.excluded_movies
or str(item.imdb_id) in self.excluded_movies
)


exclusions = Exclusions()