feat: Enhanced anime add flow with sanitized folders and targeted scan
- Add sanitize_folder_name utility for filesystem-safe folder names - Add sanitized_folder property to Serie entity - Update SerieList.add() to use sanitized display names for folders - Add scan_single_series() method for targeted episode scanning - Enhance add_series endpoint: DB save -> folder create -> targeted scan - Update response to include missing_episodes and total_missing - Add comprehensive unit tests for new functionality - Update API tests with proper mock support
This commit is contained in:
@@ -42,11 +42,17 @@ class FakeSeriesApp:
|
||||
def __init__(self):
|
||||
"""Initialize fake series app."""
|
||||
self.list = self # Changed from self.List to self.list
|
||||
self.scanner = FakeScanner() # Add fake scanner
|
||||
self.directory = "/tmp/fake_anime"
|
||||
self.keyDict = {} # Add keyDict for direct access
|
||||
self._items = [
|
||||
# Using realistic key values (URL-safe, lowercase, hyphenated)
|
||||
FakeSerie("test-show-key", "Test Show", "Test Show (2023)", {1: [1, 2]}),
|
||||
FakeSerie("complete-show-key", "Complete Show", "Complete Show (2022)", {}),
|
||||
]
|
||||
# Populate keyDict
|
||||
for item in self._items:
|
||||
self.keyDict[item.key] = item
|
||||
|
||||
def GetMissingEpisode(self):
|
||||
"""Return series with missing episodes."""
|
||||
@@ -60,11 +66,21 @@ class FakeSeriesApp:
|
||||
"""Trigger rescan with callback."""
|
||||
callback()
|
||||
|
||||
def add(self, serie):
|
||||
"""Add a serie to the list."""
|
||||
def add(self, serie, use_sanitized_folder=True):
|
||||
"""Add a serie to the list.
|
||||
|
||||
Args:
|
||||
serie: The Serie instance to add
|
||||
use_sanitized_folder: Whether to use sanitized folder name
|
||||
|
||||
Returns:
|
||||
str: The folder path (fake path for testing)
|
||||
"""
|
||||
# Check if already exists
|
||||
if not any(s.key == serie.key for s in self._items):
|
||||
self._items.append(serie)
|
||||
self.keyDict[serie.key] = serie
|
||||
return f"/tmp/fake_anime/{serie.folder}"
|
||||
|
||||
async def search(self, query):
|
||||
"""Search for series (async)."""
|
||||
@@ -85,6 +101,14 @@ class FakeSeriesApp:
|
||||
pass
|
||||
|
||||
|
||||
class FakeScanner:
|
||||
"""Mock SerieScanner for testing."""
|
||||
|
||||
def scan_single_series(self, key, folder):
|
||||
"""Mock scan that returns some fake missing episodes."""
|
||||
return {1: [1, 2, 3], 2: [1, 2]}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_auth_state():
|
||||
"""Reset auth service state before each test."""
|
||||
@@ -273,3 +297,122 @@ async def test_add_series_endpoint_empty_link(authenticated_client):
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert "link" in data["detail"].lower()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_series_extracts_key_from_full_url(authenticated_client):
|
||||
"""Test that add_series extracts key from full URL."""
|
||||
response = await authenticated_client.post(
|
||||
"/api/anime/add",
|
||||
json={
|
||||
"link": "https://aniworld.to/anime/stream/attack-on-titan",
|
||||
"name": "Attack on Titan"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["key"] == "attack-on-titan"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_series_sanitizes_folder_name(authenticated_client):
|
||||
"""Test that add_series creates sanitized folder name."""
|
||||
response = await authenticated_client.post(
|
||||
"/api/anime/add",
|
||||
json={
|
||||
"link": "https://aniworld.to/anime/stream/rezero",
|
||||
"name": "Re:Zero - Starting Life in Another World?"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Folder should not contain invalid characters
|
||||
folder = data["folder"]
|
||||
assert ":" not in folder
|
||||
assert "?" not in folder
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_series_returns_missing_episodes(authenticated_client):
|
||||
"""Test that add_series returns missing episodes info."""
|
||||
response = await authenticated_client.post(
|
||||
"/api/anime/add",
|
||||
json={
|
||||
"link": "https://aniworld.to/anime/stream/test-anime",
|
||||
"name": "Test Anime"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Response should contain missing episodes fields
|
||||
assert "missing_episodes" in data
|
||||
assert "total_missing" in data
|
||||
assert isinstance(data["missing_episodes"], dict)
|
||||
assert isinstance(data["total_missing"], int)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_series_response_structure(authenticated_client):
|
||||
"""Test the full response structure of add_series."""
|
||||
response = await authenticated_client.post(
|
||||
"/api/anime/add",
|
||||
json={
|
||||
"link": "https://aniworld.to/anime/stream/new-anime",
|
||||
"name": "New Anime Series"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Verify all expected fields are present
|
||||
assert "status" in data
|
||||
assert "message" in data
|
||||
assert "key" in data
|
||||
assert "folder" in data
|
||||
assert "missing_episodes" in data
|
||||
assert "total_missing" in data
|
||||
|
||||
# Status should be success or exists
|
||||
assert data["status"] in ("success", "exists")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_series_special_characters_in_name(authenticated_client):
|
||||
"""Test adding series with various special characters in name."""
|
||||
test_cases = [
|
||||
("86: Eighty-Six", "86-eighty-six"),
|
||||
("Fate/Stay Night", "fate-stay-night"),
|
||||
("What If...?", "what-if"),
|
||||
("Steins;Gate", "steins-gate"),
|
||||
]
|
||||
|
||||
for name, key in test_cases:
|
||||
response = await authenticated_client.post(
|
||||
"/api/anime/add",
|
||||
json={
|
||||
"link": f"https://aniworld.to/anime/stream/{key}",
|
||||
"name": name
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Get just the folder name (last part of path)
|
||||
folder_path = data["folder"]
|
||||
# Handle both full paths and just folder names
|
||||
if "/" in folder_path:
|
||||
folder_name = folder_path.rstrip("/").split("/")[-1]
|
||||
else:
|
||||
folder_name = folder_path
|
||||
|
||||
# Folder name should not contain invalid filesystem characters
|
||||
invalid_chars = [':', '\\', '?', '*', '<', '>', '|', '"']
|
||||
for char in invalid_chars:
|
||||
assert char not in folder_name, f"Found '{char}' in folder name for {name}"
|
||||
|
||||
Reference in New Issue
Block a user