feat(setup): track unresolved folders for manual key resolution
When SetupService cannot auto-resolve a provider key for an anime folder,
the folder is now tracked in the new 'unresolved_folders' table instead of
being silently skipped. Users can then resolve these via the new API:
- GET /api/setup/unresolved - list unresolved folders with search suggestions
- POST /api/setup/unresolved/{folder}/resolve - provide key to resolve folder
The SetupService.run() now:
- Tracks unresolved folders instead of skipping them
- Re-creates AnimeSeries for previously unresolved folders that are now resolved
- Includes unresolved count in logs
New files:
- src/server/api/setup_endpoints.py - API endpoints for unresolved management
- tests/unit/test_unresolved_folder_service.py - service and model tests
Modified:
- src/server/database/models.py - add UnresolvedFolder model
- src/server/database/service.py - add UnresolvedFolderService
- src/server/services/setup_service.py - track unresolved folders
- src/server/fastapi_app.py - include setup router
This commit is contained in:
@@ -626,6 +626,96 @@ class UserSession(Base, TimestampMixin):
|
||||
self.is_active = False
|
||||
|
||||
|
||||
class UnresolvedFolder(Base, TimestampMixin):
|
||||
"""SQLAlchemy model for folders that couldn't be resolved during setup.
|
||||
|
||||
Tracks anime folders whose provider key couldn't be auto-resolved
|
||||
during the initial setup scan. Users can provide the correct key
|
||||
via the API to complete the series registration.
|
||||
|
||||
Attributes:
|
||||
id: Primary key
|
||||
folder_name: Original filesystem folder name
|
||||
title: Extracted title from folder name
|
||||
year: Extracted release year (optional)
|
||||
provider_key: User-provided provider key to resolve this folder
|
||||
search_attempts: Number of auto-search attempts made
|
||||
last_search_result: Cached search results (JSON string) for UI suggestions
|
||||
resolved_at: Timestamp when provider_key was provided
|
||||
created_at: Creation timestamp (from TimestampMixin)
|
||||
updated_at: Last update timestamp (from TimestampMixin)
|
||||
"""
|
||||
__tablename__ = "unresolved_folders"
|
||||
|
||||
# Primary key
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True
|
||||
)
|
||||
|
||||
# Folder metadata
|
||||
folder_name: Mapped[str] = mapped_column(
|
||||
String(1000), unique=True, nullable=False, index=True,
|
||||
doc="Original filesystem folder name"
|
||||
)
|
||||
title: Mapped[str] = mapped_column(
|
||||
String(500), nullable=False,
|
||||
doc="Extracted title from folder name"
|
||||
)
|
||||
year: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True,
|
||||
doc="Extracted release year"
|
||||
)
|
||||
|
||||
# Resolution data
|
||||
provider_key: Mapped[Optional[str]] = mapped_column(
|
||||
String(255), nullable=True,
|
||||
doc="User-provided provider key to resolve this folder"
|
||||
)
|
||||
search_attempts: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=0, server_default="0",
|
||||
doc="Number of auto-search attempts made"
|
||||
)
|
||||
last_search_result: Mapped[Optional[str]] = mapped_column(
|
||||
Text, nullable=True,
|
||||
doc="Cached search results (JSON) for UI display"
|
||||
)
|
||||
resolved_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True,
|
||||
doc="Timestamp when this folder was resolved"
|
||||
)
|
||||
|
||||
@validates('folder_name')
|
||||
def validate_folder_name(self, key: str, value: str) -> str:
|
||||
"""Validate folder name is not empty."""
|
||||
if not value or not value.strip():
|
||||
raise ValueError("Folder name cannot be empty")
|
||||
if len(value) > 1000:
|
||||
raise ValueError("Folder name must be 1000 characters or less")
|
||||
return value.strip()
|
||||
|
||||
@validates('title')
|
||||
def validate_title(self, key: str, value: str) -> str:
|
||||
"""Validate title is not empty."""
|
||||
if not value or not value.strip():
|
||||
raise ValueError("Title cannot be empty")
|
||||
if len(value) > 500:
|
||||
raise ValueError("Title must be 500 characters or less")
|
||||
return value.strip()
|
||||
|
||||
@property
|
||||
def is_resolved(self) -> bool:
|
||||
"""Check if this folder has been resolved with a provider key."""
|
||||
return self.provider_key is not None and self.resolved_at is not None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<UnresolvedFolder(id={self.id}, "
|
||||
f"folder_name='{self.folder_name}', "
|
||||
f"title='{self.title}', "
|
||||
f"resolved={self.is_resolved})>"
|
||||
)
|
||||
|
||||
|
||||
class SystemSettings(Base, TimestampMixin):
|
||||
"""SQLAlchemy model for system-wide settings and state.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user