feat(SerieScanner): add folder ignore patterns for non-anime content
- Add NFO_FOLDER_IGNORE_PATTERNS setting to skip TV shows like The Last of Us, Loki, Chernobyl, Star Trek Discovery - Update SerieScanner.__find_mp4_files() to skip ignored folders - Update SerieList.load_series() to skip ignored folders - Add should_ignore_folder() method for pattern matching - Add folder_ignore_patterns property for pattern parsing - Add comprehensive tests for ignore pattern functionality - Update NFO_GUIDE.md with ignore patterns documentation - Update CONFIGURATION.md with new setting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -114,6 +115,40 @@ class Settings(BaseSettings):
|
|||||||
validation_alias="NFO_PREFER_FSK_RATING",
|
validation_alias="NFO_PREFER_FSK_RATING",
|
||||||
description="Prefer German FSK rating over MPAA rating in NFO files"
|
description="Prefer German FSK rating over MPAA rating in NFO files"
|
||||||
)
|
)
|
||||||
|
nfo_folder_ignore_patterns: str = Field(
|
||||||
|
default="The Last of Us|Loki|Chernobyl|Star Trek Discovery|Marvel|Matrix|Fast & Furious|Jurassic|James Bond|Mission: Impossible|Bourne|Hunger Games|Die Hard|John Wick|Pacific Rim|Guardians of the Galaxy|Avengers|Batman|Superman|Wonder Woman|Spider-Man|X-Men|Fantastic Four|Terminator|Predator|Rambo|Rocky|Expendables|Tomb Raider|Jumanji|Jurassic Park|Pirates of the Caribbean|Harry Potter|Lord of the Rings|Hobbit|Game of Thrones|Westworld|Stranger Things|Breaking Bad|Better Call Saul|Sherlock|Downton Abbey|The Crown|Bridgerton|Sex Education|Normal People|Emily in Paris|The Witcher|Servant|Lucifer|Dark|Shadow and Bone|Grimm|Fairytale",
|
||||||
|
validation_alias="NFO_FOLDER_IGNORE_PATTERNS",
|
||||||
|
description="Regex patterns for folder names to skip during scan (pipe-separated)"
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def folder_ignore_patterns(self) -> list[str]:
|
||||||
|
"""Parse ignore patterns from comma-separated string into list.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of regex patterns to skip during folder scanning.
|
||||||
|
"""
|
||||||
|
if not self.nfo_folder_ignore_patterns:
|
||||||
|
return []
|
||||||
|
return [
|
||||||
|
pattern.strip()
|
||||||
|
for pattern in self.nfo_folder_ignore_patterns.split("|")
|
||||||
|
if pattern.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
def should_ignore_folder(self, folder_name: str) -> bool:
|
||||||
|
"""Check if folder should be ignored based on ignore patterns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder_name: Name of folder to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if folder matches any ignore pattern, False otherwise.
|
||||||
|
"""
|
||||||
|
for pattern in self.folder_ignore_patterns:
|
||||||
|
if re.search(pattern, folder_name, re.IGNORECASE):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def allowed_origins(self) -> list[str]:
|
def allowed_origins(self) -> list[str]:
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from typing import Callable, Iterable, Iterator, Optional
|
|||||||
|
|
||||||
from events import Events
|
from events import Events
|
||||||
|
|
||||||
|
from src.config.settings import settings
|
||||||
from src.core.entities.series import Serie
|
from src.core.entities.series import Serie
|
||||||
from src.core.exceptions.Exceptions import MatchNotFoundError, NoKeyFoundException
|
from src.core.exceptions.Exceptions import MatchNotFoundError, NoKeyFoundException
|
||||||
from src.core.providers.base_provider import Loader
|
from src.core.providers.base_provider import Loader
|
||||||
@@ -597,6 +598,9 @@ class SerieScanner:
|
|||||||
for anime_name in os.listdir(self.directory):
|
for anime_name in os.listdir(self.directory):
|
||||||
anime_path = os.path.join(self.directory, anime_name)
|
anime_path = os.path.join(self.directory, anime_name)
|
||||||
if os.path.isdir(anime_path):
|
if os.path.isdir(anime_path):
|
||||||
|
if settings.should_ignore_folder(anime_name):
|
||||||
|
logger.debug("Skipping ignored folder: %s", anime_name)
|
||||||
|
continue
|
||||||
mp4_files: list[str] = []
|
mp4_files: list[str] = []
|
||||||
has_files = False
|
has_files = False
|
||||||
for root, _, files in os.walk(anime_path):
|
for root, _, files in os.walk(anime_path):
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import warnings
|
|||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
from typing import Dict, Iterable, List, Optional
|
from typing import Dict, Iterable, List, Optional
|
||||||
|
|
||||||
|
from src.config.settings import settings
|
||||||
from src.core.entities.series import Serie
|
from src.core.entities.series import Serie
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -214,6 +215,9 @@ class SerieList:
|
|||||||
}
|
}
|
||||||
|
|
||||||
for anime_folder in entries:
|
for anime_folder in entries:
|
||||||
|
if settings.should_ignore_folder(anime_folder):
|
||||||
|
logger.debug("Skipping ignored folder: %s", anime_folder)
|
||||||
|
continue
|
||||||
anime_path = os.path.join(self.directory, anime_folder, "data")
|
anime_path = os.path.join(self.directory, anime_folder, "data")
|
||||||
if os.path.isfile(anime_path):
|
if os.path.isfile(anime_path):
|
||||||
logger.debug("Found data file for folder %s", anime_folder)
|
logger.debug("Found data file for folder %s", anime_folder)
|
||||||
|
|||||||
222
tests/unit/test_folder_ignore_patterns.py
Normal file
222
tests/unit/test_folder_ignore_patterns.py
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
"""Tests for folder ignore patterns feature."""
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import warnings
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.config.settings import Settings
|
||||||
|
|
||||||
|
|
||||||
|
class TestShouldIgnoreFolder:
|
||||||
|
"""Test should_ignore_folder method."""
|
||||||
|
|
||||||
|
def test_ignore_pattern_matches_exact(self):
|
||||||
|
"""Test exact folder name match."""
|
||||||
|
settings = Settings()
|
||||||
|
assert settings.should_ignore_folder("The Last of Us") is True
|
||||||
|
|
||||||
|
def test_ignore_pattern_matches_case_insensitive(self):
|
||||||
|
"""Test case-insensitive matching."""
|
||||||
|
settings = Settings()
|
||||||
|
assert settings.should_ignore_folder("the last of us") is True
|
||||||
|
assert settings.should_ignore_folder("THE LAST OF US") is True
|
||||||
|
|
||||||
|
def test_ignore_pattern_partial_match(self):
|
||||||
|
"""Test partial folder name match."""
|
||||||
|
settings = Settings()
|
||||||
|
assert settings.should_ignore_folder("Loki Season 2") is True
|
||||||
|
assert settings.should_ignore_folder("Chernobyl Complete") is True
|
||||||
|
|
||||||
|
def test_non_matching_folder_returns_false(self):
|
||||||
|
"""Test non-matching folder passes through."""
|
||||||
|
settings = Settings()
|
||||||
|
assert settings.should_ignore_folder("Attack on Titan") is False
|
||||||
|
assert settings.should_ignore_folder("Naruto") is False
|
||||||
|
|
||||||
|
def test_empty_folder_returns_false(self):
|
||||||
|
"""Test empty folder name."""
|
||||||
|
settings = Settings()
|
||||||
|
assert settings.should_ignore_folder("") is False
|
||||||
|
|
||||||
|
def test_custom_patterns_via_env_var(self, monkeypatch):
|
||||||
|
"""Test custom ignore patterns via environment variable."""
|
||||||
|
monkeypatch.setenv("NFO_FOLDER_IGNORE_PATTERNS", "MyShow|AnotherShow")
|
||||||
|
settings = Settings()
|
||||||
|
assert settings.should_ignore_folder("MyShow") is True
|
||||||
|
assert settings.should_ignore_folder("AnotherShow") is True
|
||||||
|
assert settings.should_ignore_folder("OtherShow") is False
|
||||||
|
|
||||||
|
def test_custom_patterns_case_insensitive_via_env_var(self, monkeypatch):
|
||||||
|
"""Test custom patterns respect case-insensitivity via env var."""
|
||||||
|
monkeypatch.setenv("NFO_FOLDER_IGNORE_PATTERNS", "myshow")
|
||||||
|
settings = Settings()
|
||||||
|
assert settings.should_ignore_folder("MyShow") is True
|
||||||
|
assert settings.should_ignore_folder("MYSHOW") is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestFolderIgnorePatternsProperty:
|
||||||
|
"""Test folder_ignore_patterns property."""
|
||||||
|
|
||||||
|
def test_default_patterns_parsed(self):
|
||||||
|
"""Test default patterns are parsed correctly."""
|
||||||
|
settings = Settings()
|
||||||
|
patterns = settings.folder_ignore_patterns
|
||||||
|
assert len(patterns) > 0
|
||||||
|
assert "The Last of Us" in patterns
|
||||||
|
assert "Loki" in patterns
|
||||||
|
|
||||||
|
def test_empty_string_via_env_var_returns_empty_list(self, monkeypatch):
|
||||||
|
"""Test empty patterns string via env var."""
|
||||||
|
monkeypatch.setenv("NFO_FOLDER_IGNORE_PATTERNS", "")
|
||||||
|
settings = Settings()
|
||||||
|
patterns = settings.folder_ignore_patterns
|
||||||
|
assert patterns == []
|
||||||
|
|
||||||
|
def test_single_pattern_via_env_var(self, monkeypatch):
|
||||||
|
"""Test single pattern via env var."""
|
||||||
|
monkeypatch.setenv("NFO_FOLDER_IGNORE_PATTERNS", "TestShow")
|
||||||
|
settings = Settings()
|
||||||
|
patterns = settings.folder_ignore_patterns
|
||||||
|
# Single pattern in pipe-separated string
|
||||||
|
assert "TestShow" in patterns
|
||||||
|
|
||||||
|
def test_pipe_separated_patterns_via_env_var(self, monkeypatch):
|
||||||
|
"""Test pipe-separated patterns via env var."""
|
||||||
|
monkeypatch.setenv("NFO_FOLDER_IGNORE_PATTERNS", "Show1|Show2|Show3")
|
||||||
|
settings = Settings()
|
||||||
|
patterns = settings.folder_ignore_patterns
|
||||||
|
assert len(patterns) == 3
|
||||||
|
assert "Show1" in patterns
|
||||||
|
assert "Show2" in patterns
|
||||||
|
assert "Show3" in patterns
|
||||||
|
|
||||||
|
def test_pattern_with_spaces_trimmed_via_env_var(self, monkeypatch):
|
||||||
|
"""Test patterns with spaces are trimmed."""
|
||||||
|
monkeypatch.setenv("NFO_FOLDER_IGNORE_PATTERNS", "Show1 | Show2 | Show3 ")
|
||||||
|
settings = Settings()
|
||||||
|
patterns = settings.folder_ignore_patterns
|
||||||
|
# All patterns should be trimmed of whitespace
|
||||||
|
for p in patterns:
|
||||||
|
assert p == p.strip()
|
||||||
|
|
||||||
|
|
||||||
|
class TestSerieScannerIgnorePatterns:
|
||||||
|
"""Test SerieScanner respects ignore patterns."""
|
||||||
|
|
||||||
|
def test_scanner_skips_ignored_folders(self, tmp_path):
|
||||||
|
"""Test scanner skips folders matching ignore patterns."""
|
||||||
|
from src.core.SerieScanner import SerieScanner
|
||||||
|
from src.core.providers.aniworld_provider import AniworldLoader
|
||||||
|
|
||||||
|
# Create test folders
|
||||||
|
ignored_folder = tmp_path / "The Last of Us"
|
||||||
|
ignored_folder.mkdir()
|
||||||
|
(ignored_folder / "S01E01.mp4").touch()
|
||||||
|
|
||||||
|
normal_folder = tmp_path / "Attack on Titan"
|
||||||
|
normal_folder.mkdir()
|
||||||
|
(normal_folder / "S01E01.mp4").touch()
|
||||||
|
|
||||||
|
loader = AniworldLoader()
|
||||||
|
scanner = SerieScanner(str(tmp_path), loader)
|
||||||
|
|
||||||
|
# Get MP4 files - should only find Attack on Titan
|
||||||
|
mp4_files = list(scanner._SerieScanner__find_mp4_files())
|
||||||
|
folder_names = [name for name, _ in mp4_files]
|
||||||
|
|
||||||
|
assert "Attack on Titan" in folder_names
|
||||||
|
assert "The Last of Us" not in folder_names
|
||||||
|
|
||||||
|
def test_scanner_normal_folders_not_ignored(self, tmp_path):
|
||||||
|
"""Test normal folders are not skipped."""
|
||||||
|
from src.core.SerieScanner import SerieScanner
|
||||||
|
from src.core.providers.aniworld_provider import AniworldLoader
|
||||||
|
|
||||||
|
folder1 = tmp_path / "Attack on Titan"
|
||||||
|
folder1.mkdir()
|
||||||
|
(folder1 / "S01E01.mp4").touch()
|
||||||
|
|
||||||
|
folder2 = tmp_path / "Naruto"
|
||||||
|
folder2.mkdir()
|
||||||
|
(folder2 / "S01E01.mp4").touch()
|
||||||
|
|
||||||
|
loader = AniworldLoader()
|
||||||
|
scanner = SerieScanner(str(tmp_path), loader)
|
||||||
|
|
||||||
|
mp4_files = list(scanner._SerieScanner__find_mp4_files())
|
||||||
|
folder_names = [name for name, _ in mp4_files]
|
||||||
|
|
||||||
|
assert "Attack on Titan" in folder_names
|
||||||
|
assert "Naruto" in folder_names
|
||||||
|
|
||||||
|
def test_scanner_respects_default_ignore_patterns(self, tmp_path):
|
||||||
|
"""Test scanner respects default ignore patterns."""
|
||||||
|
from src.core.SerieScanner import SerieScanner
|
||||||
|
from src.core.providers.aniworld_provider import AniworldLoader
|
||||||
|
|
||||||
|
# Create folder matching default ignore pattern (Chernobyl)
|
||||||
|
ignored_folder = tmp_path / "Chernobyl Complete Series"
|
||||||
|
ignored_folder.mkdir()
|
||||||
|
(ignored_folder / "S01E01.mp4").touch()
|
||||||
|
|
||||||
|
normal_folder = tmp_path / "Normal Anime"
|
||||||
|
normal_folder.mkdir()
|
||||||
|
(normal_folder / "S01E01.mp4").touch()
|
||||||
|
|
||||||
|
loader = AniworldLoader()
|
||||||
|
scanner = SerieScanner(str(tmp_path), loader)
|
||||||
|
mp4_files = list(scanner._SerieScanner__find_mp4_files())
|
||||||
|
folder_names = [name for name, _ in mp4_files]
|
||||||
|
|
||||||
|
assert "Normal Anime" in folder_names
|
||||||
|
assert "Chernobyl Complete Series" not in folder_names
|
||||||
|
|
||||||
|
|
||||||
|
class TestSerieListIgnorePatterns:
|
||||||
|
"""Test SerieList respects ignore patterns."""
|
||||||
|
|
||||||
|
def test_load_series_skips_ignored_folders(self, tmp_path):
|
||||||
|
"""Test load_series skips folders matching ignore patterns."""
|
||||||
|
from src.core.entities.SerieList import SerieList
|
||||||
|
from src.core.entities.series import Serie
|
||||||
|
|
||||||
|
# Create ignored folder with data file
|
||||||
|
ignored_folder = tmp_path / "The Last of Us"
|
||||||
|
ignored_folder.mkdir()
|
||||||
|
ignored_data = ignored_folder / "data"
|
||||||
|
|
||||||
|
ignored_serie = Serie(
|
||||||
|
key="the-last-of-us",
|
||||||
|
name="The Last of Us",
|
||||||
|
site="https://aniworld.to/anime/stream/the-last-of-us",
|
||||||
|
folder="The Last of Us",
|
||||||
|
episodeDict={1: [1, 2, 3]}
|
||||||
|
)
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore", DeprecationWarning)
|
||||||
|
ignored_serie.save_to_file(str(ignored_data))
|
||||||
|
|
||||||
|
# Create normal folder with data file
|
||||||
|
normal_folder = tmp_path / "Attack on Titan"
|
||||||
|
normal_folder.mkdir()
|
||||||
|
normal_data = normal_folder / "data"
|
||||||
|
|
||||||
|
normal_serie = Serie(
|
||||||
|
key="attack-on-titan",
|
||||||
|
name="Attack on Titan",
|
||||||
|
site="https://aniworld.to/anime/stream/attack-on-titan",
|
||||||
|
folder="Attack on Titan",
|
||||||
|
episodeDict={1: [1, 2]}
|
||||||
|
)
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore", DeprecationWarning)
|
||||||
|
normal_serie.save_to_file(str(normal_data))
|
||||||
|
|
||||||
|
# Load series
|
||||||
|
serie_list = SerieList(str(tmp_path))
|
||||||
|
|
||||||
|
# Verify ignored folder was skipped
|
||||||
|
assert serie_list.contains("attack-on-titan") is True
|
||||||
|
assert serie_list.contains("the-last-of-us") is False
|
||||||
Reference in New Issue
Block a user