Compare commits

...

6 Commits

Author SHA1 Message Date
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
8 changed files with 59 additions and 3 deletions

View File

@@ -1 +1 @@
v1.4.4 v1.4.7

View File

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

View File

@@ -1,6 +1,7 @@
"""Authentication API endpoints for Aniworld.""" """Authentication API endpoints for Aniworld."""
from typing import Optional from typing import Optional
import structlog
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from fastapi import status as http_status from fastapi import status as http_status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer 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.auth_service import AuthError, LockedOutError, auth_service
from src.server.services.config_service import get_config_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 # NOTE: import dependencies (optional_auth, security) lazily inside handlers
# to avoid importing heavyweight modules (e.g. sqlalchemy) at import time. # to avoid importing heavyweight modules (e.g. sqlalchemy) at import time.

View File

@@ -32,6 +32,7 @@ class SetupRedirectMiddleware(BaseHTTPMiddleware):
# Paths that should always be accessible, even without setup # Paths that should always be accessible, even without setup
EXEMPT_PATHS = { EXEMPT_PATHS = {
"/setup", # Setup page itself "/setup", # Setup page itself
"/setup/unresolved", # Unresolved folders page (after setup)
"/loading", # Loading page (initialization progress) "/loading", # Loading page (initialization progress)
"/login", # Login page (needs to be accessible after setup) "/login", # Login page (needs to be accessible after setup)
"/queue", # Queue page (for initial load) "/queue", # Queue page (for initial load)

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} db_folders: set[str] = {series.folder for series in all_series if series.folder}
for folder_name in db_folders: 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" key_file = folder_path / "key"
if not key_file.exists(): if not key_file.exists():

View File

@@ -378,6 +378,18 @@ class SetupService:
) )
continue 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 # Check filesystem properties
props = cls._get_series_properties(folder) props = cls._get_series_properties(folder)

View File

@@ -481,8 +481,10 @@
async function checkUnresolvedAndProceed() { async function checkUnresolvedAndProceed() {
try { try {
const token = localStorage.getItem('auth_token'); const token = localStorage.getItem('auth_token');
console.log('Checking unresolved folders, token exists:', !!token);
if (!token) { if (!token) {
// No token, go to login // No token, go to login
console.log('No auth token found, showing completion');
document.getElementById('completionMessage').style.display = 'block'; document.getElementById('completionMessage').style.display = 'block';
return; return;
} }
@@ -490,20 +492,30 @@
const res = await fetch('/api/setup/unresolved', { const res = await fetch('/api/setup/unresolved', {
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` }
}); });
console.log('Unresolved API response status:', res.status);
if (res.ok) { if (res.ok) {
const unresolved = await res.json(); const unresolved = await res.json();
console.log('Unresolved folders:', unresolved);
if (unresolved && unresolved.length > 0) { if (unresolved && unresolved.length > 0) {
// Has unresolved folders - redirect to unresolved page // Has unresolved folders - redirect to unresolved page
console.log('Redirecting to /setup/unresolved');
window.location.href = '/setup/unresolved'; window.location.href = '/setup/unresolved';
return; return;
} }
} else if (res.status === 401) {
// Token invalid, clear it
localStorage.removeItem('auth_token');
console.log('Token invalid, showing completion');
document.getElementById('completionMessage').style.display = 'block';
return;
} }
} catch (e) { } catch (e) {
console.error('Error checking unresolved folders:', e); console.error('Error checking unresolved folders:', e);
} }
// No unresolved folders or error - show completion message // No unresolved folders or error - show completion message
console.log('No unresolved folders or error, showing completion');
document.getElementById('completionMessage').style.display = 'block'; document.getElementById('completionMessage').style.display = 'block';
} }

View File

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