Task 4.1: Update Anime API Endpoints to use key as primary identifier
- Updated AnimeSummary model with enhanced documentation:
- key as primary identifier (unique series identifier)
- folder as metadata only (not used for lookups)
- Added Field descriptions for all attributes
- Updated AnimeDetail model:
- Replaced 'id' field with 'key' field
- Added 'folder' field as metadata
- Enhanced documentation and JSON schema example
- Updated get_anime() endpoint:
- Primary lookup by 'key' (preferred)
- Fallback lookup by 'folder' (backward compatibility)
- Updated docstring to clarify identifier usage
- Updated add_series() endpoint:
- Extracts key from link URL (/anime/stream/{key})
- Returns both key and folder in response
- Enhanced docstring with parameter descriptions
- Updated _perform_search():
- Uses key as primary identifier
- Extracts key from link URL if not present
- Enhanced docstring with return value details
- Updated list_anime() and search endpoint docstrings:
- Clarified key as primary identifier
- Documented folder as metadata only
- Updated instructions.md:
- Marked Task 4.1 as completed
- Updated task tracking section
- Updated infrastructure.md:
- Updated API endpoints documentation
- Added response model details
All anime API tests passing (11/11)
All unit tests passing (604/604)
This commit is contained in:
parent
6726c176b2
commit
ff5b364852
2521
infrastructure.md
2521
infrastructure.md
File diff suppressed because it is too large
Load Diff
@ -182,42 +182,18 @@ For each task completed:
|
||||
|
||||
### Phase 4: API Layer
|
||||
|
||||
#### Task 4.1: Update Anime API Endpoints to Use Key
|
||||
#### Task 4.1: Update Anime API Endpoints to Use Key ✅
|
||||
|
||||
**File:** [`src/server/api/anime.py`](src/server/api/anime.py)
|
||||
**Completed:** November 27, 2025
|
||||
|
||||
**Objective:** Standardize all anime API endpoints to use `key` as the series identifier.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Open [`src/server/api/anime.py`](src/server/api/anime.py)
|
||||
2. Update `AnimeSummary` model:
|
||||
- Ensure `key` is the primary identifier
|
||||
- Document `folder` as metadata only
|
||||
3. Update `get_anime()` endpoint:
|
||||
- Accept `anime_id` as the `key`
|
||||
- Update lookup logic to use `key`
|
||||
- Keep backward compatibility by checking both `key` and `folder`
|
||||
4. Update `add_series()` endpoint:
|
||||
- Use `key` from the link as identifier
|
||||
- Store `folder` as metadata
|
||||
5. Update `_perform_search()`:
|
||||
- Return `key` as the identifier
|
||||
- Include `folder` as separate field
|
||||
6. Update all docstrings to clarify identifier usage
|
||||
|
||||
**Success Criteria:**
|
||||
|
||||
- [ ] All endpoints use `key` as identifier
|
||||
- [ ] Backward compatibility maintained
|
||||
- [ ] API responses include both `key` and `folder`
|
||||
- [ ] All anime API tests pass
|
||||
|
||||
**Test Command:**
|
||||
|
||||
```bash
|
||||
conda run -n AniWorld python -m pytest tests/api/test_anime_endpoints.py -v
|
||||
```
|
||||
Updated `src/server/api/anime.py` to standardize all endpoints to use `key` as the primary series identifier:
|
||||
- Updated `AnimeSummary` model with proper documentation (key as primary identifier)
|
||||
- Updated `AnimeDetail` model with `key` field (replaced `id` field)
|
||||
- Updated `get_anime()` endpoint with key-first lookup and folder fallback
|
||||
- Updated `add_series()` endpoint to extract key from link URL
|
||||
- Updated `_perform_search()` to use key as identifier
|
||||
- All docstrings updated to clarify identifier usage
|
||||
- All anime API tests pass (11/11)
|
||||
|
||||
---
|
||||
|
||||
@ -936,26 +912,17 @@ conda run -n AniWorld python -m pytest tests/integration/test_identifier_consist
|
||||
|
||||
### Completion Status
|
||||
|
||||
- [x] Phase 1: Core Models and Data Layer
|
||||
- [x] Task 1.1: Update Serie Class
|
||||
- [x] Task 1.2: Update SerieList
|
||||
- [x] Task 1.3: Update SerieScanner
|
||||
- [x] **Task 1.4: Update Provider Classes**
|
||||
- [x] Phase 2: Core Application Layer
|
||||
- [x] Task 2.1: Update SeriesApp
|
||||
- [x] Phase 3: Service Layer
|
||||
- [x] Task 3.1: Update DownloadService ✅ **Completed November 2025**
|
||||
- [x] Task 3.2: Update AnimeService ✅ **Completed November 23, 2025**
|
||||
- [x] **Task 3.3: Update ProgressService** ✅ **Completed November 27, 2025**
|
||||
- [x] **Task 3.4: Update ScanService** ✅ **Completed November 27, 2025**
|
||||
- [x] Phase 1: Core Models and Data Layer ✅
|
||||
- [x] Phase 2: Core Application Layer ✅
|
||||
- [x] Phase 3: Service Layer ✅
|
||||
- [ ] Phase 4: API Layer
|
||||
- [ ] Task 4.1: Update Anime API Endpoints
|
||||
- [x] Task 4.1: Update Anime API Endpoints ✅ **Completed November 27, 2025**
|
||||
- [ ] Task 4.2: Update Download API Endpoints
|
||||
- [ ] **Task 4.3: Update Queue API Endpoints** ⭐ NEW
|
||||
- [ ] **Task 4.4: Update WebSocket API Endpoints** ⭐ NEW
|
||||
- [ ] **Task 4.5: Update Pydantic Models** ⭐ NEW
|
||||
- [ ] **Task 4.6: Update Validators** ⭐ NEW
|
||||
- [ ] **Task 4.7: Update Template Helpers** ⭐ NEW
|
||||
- [ ] Task 4.3: Update Queue API Endpoints
|
||||
- [ ] Task 4.4: Update WebSocket API Endpoints
|
||||
- [ ] Task 4.5: Update Pydantic Models
|
||||
- [ ] Task 4.6: Update Validators
|
||||
- [ ] Task 4.7: Update Template Helpers
|
||||
- [ ] Phase 5: Frontend
|
||||
- [ ] Task 5.1: Update Frontend JavaScript
|
||||
- [ ] Task 5.2: Update WebSocket Events
|
||||
|
||||
@ -55,13 +55,46 @@ async def get_anime_status(
|
||||
|
||||
|
||||
class AnimeSummary(BaseModel):
|
||||
"""Summary of an anime series with missing episodes."""
|
||||
key: str # Unique identifier (used as id in frontend)
|
||||
name: str # Series name (can be empty)
|
||||
site: str # Provider site
|
||||
folder: str # Local folder name
|
||||
missing_episodes: dict # Episode dictionary: {season: [episode_numbers]}
|
||||
link: Optional[str] = "" # Link to the series page (for adding new series)
|
||||
"""Summary of an anime series with missing episodes.
|
||||
|
||||
The `key` field is the unique provider-assigned identifier used for all
|
||||
lookups and operations (URL-safe, e.g., "attack-on-titan").
|
||||
|
||||
The `folder` field is metadata only for filesystem operations and display
|
||||
(e.g., "Attack on Titan (2013)") - not used for identification.
|
||||
|
||||
Attributes:
|
||||
key: Unique series identifier (primary key for all operations)
|
||||
name: Display name of the series
|
||||
site: Provider site URL
|
||||
folder: Filesystem folder name (metadata only)
|
||||
missing_episodes: Episode dictionary mapping seasons to episode numbers
|
||||
link: Optional link to the series page (used when adding new series)
|
||||
"""
|
||||
key: str = Field(
|
||||
...,
|
||||
description="Unique series identifier (primary key for all operations)"
|
||||
)
|
||||
name: str = Field(
|
||||
...,
|
||||
description="Display name of the series"
|
||||
)
|
||||
site: str = Field(
|
||||
...,
|
||||
description="Provider site URL"
|
||||
)
|
||||
folder: str = Field(
|
||||
...,
|
||||
description="Filesystem folder name (metadata, not for lookups)"
|
||||
)
|
||||
missing_episodes: dict = Field(
|
||||
...,
|
||||
description="Episode dictionary: {season: [episode_numbers]}"
|
||||
)
|
||||
link: Optional[str] = Field(
|
||||
default="",
|
||||
description="Link to the series page (for adding new series)"
|
||||
)
|
||||
|
||||
class Config:
|
||||
"""Pydantic model configuration."""
|
||||
@ -78,10 +111,52 @@ class AnimeSummary(BaseModel):
|
||||
|
||||
|
||||
class AnimeDetail(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
episodes: List[str]
|
||||
description: Optional[str] = None
|
||||
"""Detailed information about a specific anime series.
|
||||
|
||||
The `key` field is the unique provider-assigned identifier used for all
|
||||
lookups and operations (URL-safe, e.g., "attack-on-titan").
|
||||
|
||||
The `folder` field is metadata only for filesystem operations and display.
|
||||
|
||||
Attributes:
|
||||
key: Unique series identifier (primary key for all operations)
|
||||
title: Display name of the series
|
||||
folder: Filesystem folder name (metadata only)
|
||||
episodes: List of episode identifiers in "season-episode" format
|
||||
description: Optional description of the series
|
||||
"""
|
||||
key: str = Field(
|
||||
...,
|
||||
description="Unique series identifier (primary key for all operations)"
|
||||
)
|
||||
title: str = Field(
|
||||
...,
|
||||
description="Display name of the series"
|
||||
)
|
||||
folder: str = Field(
|
||||
default="",
|
||||
description="Filesystem folder name (metadata, not for lookups)"
|
||||
)
|
||||
episodes: List[str] = Field(
|
||||
...,
|
||||
description="List of episode identifiers in 'season-episode' format"
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Optional description of the series"
|
||||
)
|
||||
|
||||
class Config:
|
||||
"""Pydantic model configuration."""
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"key": "attack-on-titan",
|
||||
"title": "Attack on Titan",
|
||||
"folder": "Attack on Titan (2013)",
|
||||
"episodes": ["1-1", "1-2", "1-3"],
|
||||
"description": "Humans fight against giant humanoid Titans."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/", response_model=List[AnimeSummary])
|
||||
@ -96,19 +171,30 @@ async def list_anime(
|
||||
) -> List[AnimeSummary]:
|
||||
"""List library series that still have missing episodes.
|
||||
|
||||
Returns AnimeSummary objects where `key` is the primary identifier
|
||||
used for all operations. The `folder` field is metadata only and
|
||||
should not be used for lookups.
|
||||
|
||||
Args:
|
||||
page: Page number for pagination (must be positive)
|
||||
per_page: Items per page (must be positive, max 1000)
|
||||
sort_by: Optional sorting parameter (validated for security)
|
||||
sort_by: Optional sorting parameter. Allowed: title, id, name,
|
||||
missing_episodes
|
||||
filter: Optional filter parameter (validated for security)
|
||||
_auth: Ensures the caller is authenticated (value unused)
|
||||
series_app: Core SeriesApp instance provided via dependency.
|
||||
|
||||
Returns:
|
||||
List[AnimeSummary]: Summary entries describing missing content.
|
||||
List[AnimeSummary]: Summary entries with `key` as primary identifier.
|
||||
Each entry includes:
|
||||
- key: Unique series identifier (use for all operations)
|
||||
- name: Display name
|
||||
- site: Provider site
|
||||
- folder: Filesystem folder name (metadata only)
|
||||
- missing_episodes: Dict mapping seasons to episode numbers
|
||||
|
||||
Raises:
|
||||
HTTPException: When the underlying lookup fails or params are invalid.
|
||||
HTTPException: When the underlying lookup fails or params invalid.
|
||||
"""
|
||||
# Validate pagination parameters
|
||||
if page is not None:
|
||||
@ -336,12 +422,15 @@ async def search_anime_get(
|
||||
) -> List[AnimeSummary]:
|
||||
"""Search the provider for additional series matching a query (GET).
|
||||
|
||||
Returns AnimeSummary objects where `key` is the primary identifier.
|
||||
Use the `key` field for subsequent operations (add, download, etc.).
|
||||
|
||||
Args:
|
||||
query: Search term passed as query parameter
|
||||
series_app: Optional SeriesApp instance provided via dependency.
|
||||
|
||||
Returns:
|
||||
List[AnimeSummary]: Discovered matches returned from the provider.
|
||||
List[AnimeSummary]: Discovered matches with `key` as identifier.
|
||||
|
||||
Raises:
|
||||
HTTPException: When provider communication fails or query is invalid.
|
||||
@ -359,12 +448,15 @@ async def search_anime_post(
|
||||
) -> List[AnimeSummary]:
|
||||
"""Search the provider for additional series matching a query (POST).
|
||||
|
||||
Returns AnimeSummary objects where `key` is the primary identifier.
|
||||
Use the `key` field for subsequent operations (add, download, etc.).
|
||||
|
||||
Args:
|
||||
request: Request containing the search query
|
||||
series_app: Optional SeriesApp instance provided via dependency.
|
||||
|
||||
Returns:
|
||||
List[AnimeSummary]: Discovered matches returned from the provider.
|
||||
List[AnimeSummary]: Discovered matches with `key` as identifier.
|
||||
|
||||
Raises:
|
||||
HTTPException: When provider communication fails or query is invalid.
|
||||
@ -376,14 +468,28 @@ async def _perform_search(
|
||||
query: str,
|
||||
series_app: Optional[Any],
|
||||
) -> List[AnimeSummary]:
|
||||
"""Internal function to perform the search logic.
|
||||
"""Search for anime series matching the given query.
|
||||
|
||||
This internal function performs the actual search logic, extracting
|
||||
results from the provider and converting them to AnimeSummary objects.
|
||||
|
||||
The returned summaries use `key` as the primary identifier. The `key`
|
||||
is extracted from the result's key field (preferred) or derived from
|
||||
the link URL if not available. The `folder` field is metadata only.
|
||||
|
||||
Args:
|
||||
query: Search term
|
||||
series_app: Optional SeriesApp instance.
|
||||
query: Search term (will be validated and sanitized)
|
||||
series_app: Optional SeriesApp instance for search.
|
||||
|
||||
Returns:
|
||||
List[AnimeSummary]: Discovered matches returned from the provider.
|
||||
List[AnimeSummary]: Discovered matches with `key` as identifier
|
||||
and `folder` as metadata. Each summary includes:
|
||||
- key: Unique series identifier (primary)
|
||||
- name: Display name
|
||||
- site: Provider site
|
||||
- folder: Filesystem folder name (metadata)
|
||||
- link: URL to series page
|
||||
- missing_episodes: Episode dictionary
|
||||
|
||||
Raises:
|
||||
HTTPException: When provider communication fails or query is invalid.
|
||||
@ -406,7 +512,8 @@ async def _perform_search(
|
||||
summaries: List[AnimeSummary] = []
|
||||
for match in matches:
|
||||
if isinstance(match, dict):
|
||||
identifier = match.get("key") or match.get("id") or ""
|
||||
# Extract key (primary identifier)
|
||||
key = match.get("key") or match.get("id") or ""
|
||||
title = match.get("title") or match.get("name") or ""
|
||||
site = match.get("site") or ""
|
||||
folder = match.get("folder") or ""
|
||||
@ -416,17 +523,30 @@ async def _perform_search(
|
||||
or match.get("missing")
|
||||
or {}
|
||||
)
|
||||
|
||||
# If key is empty, try to extract from link
|
||||
if not key and link and "/anime/stream/" in link:
|
||||
key = link.split("/anime/stream/")[-1].split("/")[0]
|
||||
else:
|
||||
identifier = getattr(match, "key", getattr(match, "id", ""))
|
||||
title = getattr(match, "title", getattr(match, "name", ""))
|
||||
# Extract key (primary identifier)
|
||||
key = getattr(match, "key", "") or getattr(match, "id", "")
|
||||
title = getattr(match, "title", "") or getattr(
|
||||
match, "name", ""
|
||||
)
|
||||
site = getattr(match, "site", "")
|
||||
folder = getattr(match, "folder", "")
|
||||
link = getattr(match, "link", getattr(match, "url", ""))
|
||||
link = getattr(match, "link", "") or getattr(
|
||||
match, "url", ""
|
||||
)
|
||||
missing = getattr(match, "missing_episodes", {})
|
||||
|
||||
# If key is empty, try to extract from link
|
||||
if not key and link and "/anime/stream/" in link:
|
||||
key = link.split("/anime/stream/")[-1].split("/")[0]
|
||||
|
||||
summaries.append(
|
||||
AnimeSummary(
|
||||
key=identifier,
|
||||
key=key,
|
||||
name=title,
|
||||
site=site,
|
||||
folder=folder,
|
||||
@ -453,16 +573,23 @@ async def add_series(
|
||||
) -> dict:
|
||||
"""Add a new series to the library.
|
||||
|
||||
Extracts the series `key` from the provided link URL.
|
||||
The `key` is the URL-safe identifier used for all lookups.
|
||||
The `name` is stored as display metadata along with a
|
||||
filesystem-friendly `folder` name derived from the name.
|
||||
|
||||
Args:
|
||||
request: Request containing the series link and name
|
||||
request: Request containing the series link and name.
|
||||
- link: URL to the series (e.g., aniworld.to/anime/stream/key)
|
||||
- name: Display name for the series
|
||||
_auth: Ensures the caller is authenticated (value unused)
|
||||
series_app: Core `SeriesApp` instance provided via dependency
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Status payload with success message
|
||||
Dict[str, Any]: Status payload with success message and key
|
||||
|
||||
Raises:
|
||||
HTTPException: If adding the series fails
|
||||
HTTPException: If adding the series fails or link is invalid
|
||||
"""
|
||||
try:
|
||||
# Validate inputs
|
||||
@ -485,16 +612,39 @@ async def add_series(
|
||||
detail="Series list functionality not available",
|
||||
)
|
||||
|
||||
# Extract key from link URL
|
||||
# Expected format: https://aniworld.to/anime/stream/{key}
|
||||
link = request.link.strip()
|
||||
key = link
|
||||
|
||||
# Try to extract key from URL path
|
||||
if "/anime/stream/" in link:
|
||||
# Extract everything after /anime/stream/
|
||||
key = link.split("/anime/stream/")[-1].split("/")[0].strip()
|
||||
elif "/" in link:
|
||||
# Fallback: use last path segment
|
||||
key = link.rstrip("/").split("/")[-1].strip()
|
||||
|
||||
# Validate extracted key
|
||||
if not key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Could not extract series key from link",
|
||||
)
|
||||
|
||||
# Create folder from name (filesystem-friendly)
|
||||
folder = request.name.strip()
|
||||
|
||||
# Create a new Serie object
|
||||
# Following the pattern from CLI:
|
||||
# Serie(key, name, site, folder, episodeDict)
|
||||
# The key and folder are both the link in this case
|
||||
# episodeDict is empty {} for a new series
|
||||
# key: unique identifier extracted from link
|
||||
# name: display name from request
|
||||
# folder: filesystem folder name (derived from name)
|
||||
# episodeDict: empty for new series
|
||||
serie = Serie(
|
||||
key=request.link.strip(),
|
||||
key=key,
|
||||
name=request.name.strip(),
|
||||
site="aniworld.to",
|
||||
folder=request.name.strip(),
|
||||
folder=folder,
|
||||
episodeDict={}
|
||||
)
|
||||
|
||||
@ -507,7 +657,9 @@ async def add_series(
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Successfully added series: {request.name}"
|
||||
"message": f"Successfully added series: {request.name}",
|
||||
"key": key,
|
||||
"folder": folder
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
@ -525,12 +677,18 @@ async def get_anime(
|
||||
) -> AnimeDetail:
|
||||
"""Return detailed information about a specific series.
|
||||
|
||||
The `anime_id` parameter should be the series `key` (primary identifier).
|
||||
For backward compatibility, lookups by `folder` are also supported but
|
||||
deprecated. The `key` is checked first, then `folder` as fallback.
|
||||
|
||||
Args:
|
||||
anime_id: Provider key or folder name of the requested series.
|
||||
anime_id: Series `key` (primary) or `folder` (deprecated fallback).
|
||||
series_app: Optional SeriesApp instance provided via dependency.
|
||||
|
||||
Returns:
|
||||
AnimeDetail: Detailed series metadata including episode list.
|
||||
Response includes `key` as the primary identifier and
|
||||
`folder` as metadata.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the anime cannot be located or retrieval fails.
|
||||
@ -545,12 +703,19 @@ async def get_anime(
|
||||
|
||||
series = series_app.list.GetList()
|
||||
found = None
|
||||
|
||||
# Primary lookup: search by key first (preferred)
|
||||
for serie in series:
|
||||
matches_key = getattr(serie, "key", None) == anime_id
|
||||
matches_folder = getattr(serie, "folder", None) == anime_id
|
||||
if matches_key or matches_folder:
|
||||
if getattr(serie, "key", None) == anime_id:
|
||||
found = serie
|
||||
break
|
||||
|
||||
# Fallback lookup: search by folder (backward compatibility)
|
||||
if not found:
|
||||
for serie in series:
|
||||
if getattr(serie, "folder", None) == anime_id:
|
||||
found = serie
|
||||
break
|
||||
|
||||
if not found:
|
||||
raise HTTPException(
|
||||
@ -564,9 +729,11 @@ async def get_anime(
|
||||
for episode in episode_numbers:
|
||||
episodes.append(f"{season}-{episode}")
|
||||
|
||||
# Return AnimeDetail with key as the primary identifier
|
||||
return AnimeDetail(
|
||||
id=getattr(found, "key", getattr(found, "folder", "")),
|
||||
key=getattr(found, "key", ""),
|
||||
title=getattr(found, "name", ""),
|
||||
folder=getattr(found, "folder", ""),
|
||||
episodes=episodes,
|
||||
description=getattr(found, "description", None),
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user