Compare commits

..

8 Commits

Author SHA1 Message Date
8bb8c6aa64 chore: bump version 2026-06-06 21:53:57 +02:00
109d3c8ac9 fix: streamline initialization flow after setup
- Remove nfo_scan and media_scan from loading page steps (no longer shown in UI)
- Remove perform_nfo_scan_if_needed calls from fastapi_app and auth.py
- Always redirect to /setup/unresolved after initialization completes
  instead of conditionally checking for unresolved folders
- Fix middleware to allow access to /loading page - let it handle
  its own redirect flow via WebSocket events

This ensures users always reach the unresolved folders page after
initial setup to manually configure any unmatched anime series.
2026-06-06 21:33:41 +02:00
6a934db8ac chore: bump version 2026-06-06 20:38:21 +02:00
ac7302b1dd fix: add /setup/unresolved to exempt paths and improve error handling
- Add /setup/unresolved to EXEMPT_PATHS to allow access after initial setup
- Handle 401 Unauthorized response in loading page (clear invalid token)
- Add console.log statements for debugging setup flow issues
2026-06-06 20:37:11 +02:00
ac5ee3bb27 chore: bump version 2026-06-06 20:08:05 +02:00
a9084202e3 fixed missing import 2026-06-06 20:07:45 +02:00
be9f2a4c0c chore: bump version 2026-06-06 19:40:21 +02:00
53fe09351f fix: prevent duplicate series when same anime key exists in different folder
- Add check for existing series by key in SetupService.run to skip duplicates
- Fix Path construction in initialization_service.py cleanup function
- Update unit tests to mock get_by_key and get_series_app
2026-06-06 19:39:32 +02:00
9 changed files with 57 additions and 61 deletions

View File

@@ -1 +1 @@
v1.4.4
v1.4.8

View File

@@ -1,6 +1,6 @@
{
"name": "aniworld-web",
"version": "1.4.4",
"version": "1.4.8",
"description": "Aniworld Anime Download Manager - Web Frontend",
"type": "module",
"scripts": {

View File

@@ -1,6 +1,7 @@
"""Authentication API endpoints for Aniworld."""
from typing import Optional
import structlog
from fastapi import APIRouter, Depends, HTTPException
from fastapi import status as http_status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
@@ -16,6 +17,8 @@ from src.server.models.config import AppConfig
from src.server.services.auth_service import AuthError, LockedOutError, auth_service
from src.server.services.config_service import get_config_service
logger = structlog.get_logger(__name__)
# NOTE: import dependencies (optional_auth, security) lazily inside handlers
# to avoid importing heavyweight modules (e.g. sqlalchemy) at import time.
@@ -144,10 +147,7 @@ async def setup_auth(req: SetupRequest):
# Trigger initialization in background task
import asyncio
from src.server.services.initialization_service import (
perform_initial_setup,
perform_nfo_scan_if_needed,
)
from src.server.services.initialization_service import perform_initial_setup
from src.server.services.progress_service import get_progress_service
progress_service = get_progress_service()
@@ -158,9 +158,6 @@ async def setup_auth(req: SetupRequest):
# Perform the initial series sync and mark as completed
await perform_initial_setup(progress_service)
# Perform NFO scan if configured
await perform_nfo_scan_if_needed(progress_service)
# Start scheduler if anime_directory is now set
try:
from src.server.services.scheduler.scheduler_service import (

View File

@@ -344,7 +344,6 @@ async def lifespan(_application: FastAPI):
from src.server.services.initialization_service import (
perform_initial_setup,
perform_media_scan_if_needed,
perform_nfo_scan_if_needed,
)
try:
@@ -373,9 +372,6 @@ async def lifespan(_application: FastAPI):
"exist yet): %s", e
)
# Run NFO scan only on first run (if configured)
await perform_nfo_scan_if_needed()
# Initialize download service
try:
from src.server.utils.dependencies import get_download_service

View File

@@ -32,6 +32,7 @@ class SetupRedirectMiddleware(BaseHTTPMiddleware):
# Paths that should always be accessible, even without setup
EXEMPT_PATHS = {
"/setup", # Setup page itself
"/setup/unresolved", # Unresolved folders page (after setup)
"/loading", # Loading page (initialization progress)
"/login", # Login page (needs to be accessible after setup)
"/queue", # Queue page (for initial load)
@@ -126,21 +127,10 @@ class SetupRedirectMiddleware(BaseHTTPMiddleware):
# Otherwise redirect to login
return RedirectResponse(url="/login", status_code=302)
elif path == "/loading":
# Check if initialization is complete
try:
from src.server.database.connection import get_db_session
from src.server.database.system_settings_service import (
SystemSettingsService,
)
async with get_db_session() as db:
is_complete = await SystemSettingsService.is_initial_scan_completed(db)
if is_complete:
# Initialization complete, redirect to login
return RedirectResponse(url="/login", status_code=302)
except Exception:
# If we can't check, allow access to loading page
pass
# Always allow access to loading page - it handles its own
# redirect flow via WebSocket events (initialization_complete
# event triggers redirect to /setup/unresolved)
pass
# Skip setup check for exempt paths
if self._is_path_exempt(path):

View File

@@ -165,7 +165,7 @@ async def _cleanup_legacy_key_files() -> int:
db_folders: set[str] = {series.folder for series in all_series if series.folder}
for folder_name in db_folders:
folder_path = settings.anime_directory / folder_name
folder_path = Path(settings.anime_directory) / folder_name
key_file = folder_path / "key"
if not key_file.exists():

View File

@@ -378,6 +378,18 @@ class SetupService:
)
continue
# Also check if a series with this key already exists (different folder, same anime)
existing_by_key = await AnimeSeriesService.get_by_key(db, resolved_key)
if existing_by_key:
logger.debug(
"Series with key already exists, skipping",
folder=folder_name,
key=resolved_key,
existing_folder=existing_by_key.folder
)
skipped_existing += 1
continue
# Check filesystem properties
props = cls._get_series_properties(folder)

View File

@@ -281,15 +281,11 @@
let isComplete = false;
const stepOrder = [
'series_sync',
'nfo_scan',
'media_scan'
'series_sync'
];
const stepTitles = {
'series_sync': 'Syncing Series Database',
'nfo_scan': 'Processing NFO Metadata',
'media_scan': 'Scanning Media Files'
'series_sync': 'Syncing Series Database'
};
function connectWebSocket() {
@@ -479,32 +475,9 @@
}
async function checkUnresolvedAndProceed() {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
// No token, go to login
document.getElementById('completionMessage').style.display = 'block';
return;
}
const res = await fetch('/api/setup/unresolved', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
const unresolved = await res.json();
if (unresolved && unresolved.length > 0) {
// Has unresolved folders - redirect to unresolved page
window.location.href = '/setup/unresolved';
return;
}
}
} catch (e) {
console.error('Error checking unresolved folders:', e);
}
// No unresolved folders or error - show completion message
document.getElementById('completionMessage').style.display = 'block';
// Always redirect to /setup/unresolved after initialization
// so users can manually enter unresolved animes
window.location.href = '/setup/unresolved';
}
function showError(message) {

View File

@@ -167,10 +167,16 @@ class TestSetupServiceRun:
mock_get_db = MagicMock()
mock_get_db.__aenter__.return_value = mock_db
mock_get_db.__aexit__.return_value = None
mock_series_app = AsyncMock()
mock_series_app.search.return_value = []
with patch(
'src.server.services.setup_service.settings'
) as mock_settings, \
patch(
'src.server.services.setup_service.get_series_app',
return_value=mock_series_app
), \
patch(
'src.server.services.setup_service.get_db_session',
return_value=mock_get_db
@@ -179,6 +185,10 @@ class TestSetupServiceRun:
'src.server.services.setup_service.AnimeSeriesService.get_by_folder',
new_callable=AsyncMock, return_value=None
), \
patch(
'src.server.services.setup_service.AnimeSeriesService.get_by_key',
new_callable=AsyncMock, return_value=None
), \
patch(
'src.server.services.setup_service.UnresolvedFolderService.get_by_folder_name',
new_callable=AsyncMock, return_value=None
@@ -258,10 +268,16 @@ class TestSetupServiceRun:
mock_get_db = MagicMock()
mock_get_db.__aenter__.return_value = mock_db
mock_get_db.__aexit__.return_value = None
mock_series_app = AsyncMock()
mock_series_app.search.return_value = []
with patch(
'src.server.services.setup_service.settings'
) as mock_settings, \
patch(
'src.server.services.setup_service.get_series_app',
return_value=mock_series_app
), \
patch(
'src.server.services.setup_service.get_db_session',
return_value=mock_get_db
@@ -270,6 +286,10 @@ class TestSetupServiceRun:
'src.server.services.setup_service.AnimeSeriesService.get_by_folder',
new_callable=AsyncMock, return_value=None
), \
patch(
'src.server.services.setup_service.AnimeSeriesService.get_by_key',
new_callable=AsyncMock, return_value=None
), \
patch(
'src.server.services.setup_service.UnresolvedFolderService.get_by_folder_name',
new_callable=AsyncMock, return_value=None
@@ -323,6 +343,10 @@ class TestSetupServiceRun:
'src.server.services.setup_service.AnimeSeriesService.get_by_folder',
new_callable=AsyncMock, return_value=None
), \
patch(
'src.server.services.setup_service.AnimeSeriesService.get_by_key',
new_callable=AsyncMock, return_value=None
), \
patch(
'src.server.services.setup_service.UnresolvedFolderService.get_by_folder_name',
new_callable=AsyncMock, return_value=None
@@ -401,6 +425,10 @@ class TestSetupServiceRun:
'src.server.services.setup_service.AnimeSeriesService.get_by_folder',
new_callable=AsyncMock, return_value=None
), \
patch(
'src.server.services.setup_service.AnimeSeriesService.get_by_key',
new_callable=AsyncMock, return_value=None
), \
patch(
'src.server.services.setup_service.UnresolvedFolderService.get_by_folder_name',
new_callable=AsyncMock, return_value=None