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
|
### 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.
|
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)
|
||||||
**Steps:**
|
- Updated `AnimeDetail` model with `key` field (replaced `id` field)
|
||||||
|
- Updated `get_anime()` endpoint with key-first lookup and folder fallback
|
||||||
1. Open [`src/server/api/anime.py`](src/server/api/anime.py)
|
- Updated `add_series()` endpoint to extract key from link URL
|
||||||
2. Update `AnimeSummary` model:
|
- Updated `_perform_search()` to use key as identifier
|
||||||
- Ensure `key` is the primary identifier
|
- All docstrings updated to clarify identifier usage
|
||||||
- Document `folder` as metadata only
|
- All anime API tests pass (11/11)
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -936,26 +912,17 @@ conda run -n AniWorld python -m pytest tests/integration/test_identifier_consist
|
|||||||
|
|
||||||
### Completion Status
|
### Completion Status
|
||||||
|
|
||||||
- [x] Phase 1: Core Models and Data Layer
|
- [x] Phase 1: Core Models and Data Layer ✅
|
||||||
- [x] Task 1.1: Update Serie Class
|
- [x] Phase 2: Core Application Layer ✅
|
||||||
- [x] Task 1.2: Update SerieList
|
- [x] Phase 3: Service Layer ✅
|
||||||
- [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**
|
|
||||||
- [ ] Phase 4: API 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.2: Update Download API Endpoints
|
||||||
- [ ] **Task 4.3: Update Queue API Endpoints** ⭐ NEW
|
- [ ] Task 4.3: Update Queue API Endpoints
|
||||||
- [ ] **Task 4.4: Update WebSocket API Endpoints** ⭐ NEW
|
- [ ] Task 4.4: Update WebSocket API Endpoints
|
||||||
- [ ] **Task 4.5: Update Pydantic Models** ⭐ NEW
|
- [ ] Task 4.5: Update Pydantic Models
|
||||||
- [ ] **Task 4.6: Update Validators** ⭐ NEW
|
- [ ] Task 4.6: Update Validators
|
||||||
- [ ] **Task 4.7: Update Template Helpers** ⭐ NEW
|
- [ ] Task 4.7: Update Template Helpers
|
||||||
- [ ] Phase 5: Frontend
|
- [ ] Phase 5: Frontend
|
||||||
- [ ] Task 5.1: Update Frontend JavaScript
|
- [ ] Task 5.1: Update Frontend JavaScript
|
||||||
- [ ] Task 5.2: Update WebSocket Events
|
- [ ] Task 5.2: Update WebSocket Events
|
||||||
|
|||||||
@ -55,13 +55,46 @@ async def get_anime_status(
|
|||||||
|
|
||||||
|
|
||||||
class AnimeSummary(BaseModel):
|
class AnimeSummary(BaseModel):
|
||||||
"""Summary of an anime series with missing episodes."""
|
"""Summary of an anime series with missing episodes.
|
||||||
key: str # Unique identifier (used as id in frontend)
|
|
||||||
name: str # Series name (can be empty)
|
The `key` field is the unique provider-assigned identifier used for all
|
||||||
site: str # Provider site
|
lookups and operations (URL-safe, e.g., "attack-on-titan").
|
||||||
folder: str # Local folder name
|
|
||||||
missing_episodes: dict # Episode dictionary: {season: [episode_numbers]}
|
The `folder` field is metadata only for filesystem operations and display
|
||||||
link: Optional[str] = "" # Link to the series page (for adding new series)
|
(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:
|
class Config:
|
||||||
"""Pydantic model configuration."""
|
"""Pydantic model configuration."""
|
||||||
@ -78,10 +111,52 @@ class AnimeSummary(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class AnimeDetail(BaseModel):
|
class AnimeDetail(BaseModel):
|
||||||
id: str
|
"""Detailed information about a specific anime series.
|
||||||
title: str
|
|
||||||
episodes: List[str]
|
The `key` field is the unique provider-assigned identifier used for all
|
||||||
description: Optional[str] = None
|
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])
|
@router.get("/", response_model=List[AnimeSummary])
|
||||||
@ -96,19 +171,30 @@ async def list_anime(
|
|||||||
) -> List[AnimeSummary]:
|
) -> List[AnimeSummary]:
|
||||||
"""List library series that still have missing episodes.
|
"""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:
|
Args:
|
||||||
page: Page number for pagination (must be positive)
|
page: Page number for pagination (must be positive)
|
||||||
per_page: Items per page (must be positive, max 1000)
|
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)
|
filter: Optional filter parameter (validated for security)
|
||||||
_auth: Ensures the caller is authenticated (value unused)
|
_auth: Ensures the caller is authenticated (value unused)
|
||||||
series_app: Core SeriesApp instance provided via dependency.
|
series_app: Core SeriesApp instance provided via dependency.
|
||||||
|
|
||||||
Returns:
|
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:
|
Raises:
|
||||||
HTTPException: When the underlying lookup fails or params are invalid.
|
HTTPException: When the underlying lookup fails or params invalid.
|
||||||
"""
|
"""
|
||||||
# Validate pagination parameters
|
# Validate pagination parameters
|
||||||
if page is not None:
|
if page is not None:
|
||||||
@ -336,12 +422,15 @@ async def search_anime_get(
|
|||||||
) -> List[AnimeSummary]:
|
) -> List[AnimeSummary]:
|
||||||
"""Search the provider for additional series matching a query (GET).
|
"""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:
|
Args:
|
||||||
query: Search term passed as query parameter
|
query: Search term passed as query parameter
|
||||||
series_app: Optional SeriesApp instance provided via dependency.
|
series_app: Optional SeriesApp instance provided via dependency.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[AnimeSummary]: Discovered matches returned from the provider.
|
List[AnimeSummary]: Discovered matches with `key` as identifier.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: When provider communication fails or query is invalid.
|
HTTPException: When provider communication fails or query is invalid.
|
||||||
@ -359,12 +448,15 @@ async def search_anime_post(
|
|||||||
) -> List[AnimeSummary]:
|
) -> List[AnimeSummary]:
|
||||||
"""Search the provider for additional series matching a query (POST).
|
"""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:
|
Args:
|
||||||
request: Request containing the search query
|
request: Request containing the search query
|
||||||
series_app: Optional SeriesApp instance provided via dependency.
|
series_app: Optional SeriesApp instance provided via dependency.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[AnimeSummary]: Discovered matches returned from the provider.
|
List[AnimeSummary]: Discovered matches with `key` as identifier.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: When provider communication fails or query is invalid.
|
HTTPException: When provider communication fails or query is invalid.
|
||||||
@ -376,14 +468,28 @@ async def _perform_search(
|
|||||||
query: str,
|
query: str,
|
||||||
series_app: Optional[Any],
|
series_app: Optional[Any],
|
||||||
) -> List[AnimeSummary]:
|
) -> 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:
|
Args:
|
||||||
query: Search term
|
query: Search term (will be validated and sanitized)
|
||||||
series_app: Optional SeriesApp instance.
|
series_app: Optional SeriesApp instance for search.
|
||||||
|
|
||||||
Returns:
|
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:
|
Raises:
|
||||||
HTTPException: When provider communication fails or query is invalid.
|
HTTPException: When provider communication fails or query is invalid.
|
||||||
@ -406,7 +512,8 @@ async def _perform_search(
|
|||||||
summaries: List[AnimeSummary] = []
|
summaries: List[AnimeSummary] = []
|
||||||
for match in matches:
|
for match in matches:
|
||||||
if isinstance(match, dict):
|
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 ""
|
title = match.get("title") or match.get("name") or ""
|
||||||
site = match.get("site") or ""
|
site = match.get("site") or ""
|
||||||
folder = match.get("folder") or ""
|
folder = match.get("folder") or ""
|
||||||
@ -416,17 +523,30 @@ async def _perform_search(
|
|||||||
or match.get("missing")
|
or match.get("missing")
|
||||||
or {}
|
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:
|
else:
|
||||||
identifier = getattr(match, "key", getattr(match, "id", ""))
|
# Extract key (primary identifier)
|
||||||
title = getattr(match, "title", getattr(match, "name", ""))
|
key = getattr(match, "key", "") or getattr(match, "id", "")
|
||||||
|
title = getattr(match, "title", "") or getattr(
|
||||||
|
match, "name", ""
|
||||||
|
)
|
||||||
site = getattr(match, "site", "")
|
site = getattr(match, "site", "")
|
||||||
folder = getattr(match, "folder", "")
|
folder = getattr(match, "folder", "")
|
||||||
link = getattr(match, "link", getattr(match, "url", ""))
|
link = getattr(match, "link", "") or getattr(
|
||||||
|
match, "url", ""
|
||||||
|
)
|
||||||
missing = getattr(match, "missing_episodes", {})
|
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(
|
summaries.append(
|
||||||
AnimeSummary(
|
AnimeSummary(
|
||||||
key=identifier,
|
key=key,
|
||||||
name=title,
|
name=title,
|
||||||
site=site,
|
site=site,
|
||||||
folder=folder,
|
folder=folder,
|
||||||
@ -453,16 +573,23 @@ async def add_series(
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
"""Add a new series to the library.
|
"""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:
|
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)
|
_auth: Ensures the caller is authenticated (value unused)
|
||||||
series_app: Core `SeriesApp` instance provided via dependency
|
series_app: Core `SeriesApp` instance provided via dependency
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: Status payload with success message
|
Dict[str, Any]: Status payload with success message and key
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: If adding the series fails
|
HTTPException: If adding the series fails or link is invalid
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Validate inputs
|
# Validate inputs
|
||||||
@ -485,16 +612,39 @@ async def add_series(
|
|||||||
detail="Series list functionality not available",
|
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
|
# Create a new Serie object
|
||||||
# Following the pattern from CLI:
|
# key: unique identifier extracted from link
|
||||||
# Serie(key, name, site, folder, episodeDict)
|
# name: display name from request
|
||||||
# The key and folder are both the link in this case
|
# folder: filesystem folder name (derived from name)
|
||||||
# episodeDict is empty {} for a new series
|
# episodeDict: empty for new series
|
||||||
serie = Serie(
|
serie = Serie(
|
||||||
key=request.link.strip(),
|
key=key,
|
||||||
name=request.name.strip(),
|
name=request.name.strip(),
|
||||||
site="aniworld.to",
|
site="aniworld.to",
|
||||||
folder=request.name.strip(),
|
folder=folder,
|
||||||
episodeDict={}
|
episodeDict={}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -507,7 +657,9 @@ async def add_series(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": f"Successfully added series: {request.name}"
|
"message": f"Successfully added series: {request.name}",
|
||||||
|
"key": key,
|
||||||
|
"folder": folder
|
||||||
}
|
}
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
@ -525,12 +677,18 @@ async def get_anime(
|
|||||||
) -> AnimeDetail:
|
) -> AnimeDetail:
|
||||||
"""Return detailed information about a specific series.
|
"""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:
|
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.
|
series_app: Optional SeriesApp instance provided via dependency.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
AnimeDetail: Detailed series metadata including episode list.
|
AnimeDetail: Detailed series metadata including episode list.
|
||||||
|
Response includes `key` as the primary identifier and
|
||||||
|
`folder` as metadata.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: If the anime cannot be located or retrieval fails.
|
HTTPException: If the anime cannot be located or retrieval fails.
|
||||||
@ -545,12 +703,19 @@ async def get_anime(
|
|||||||
|
|
||||||
series = series_app.list.GetList()
|
series = series_app.list.GetList()
|
||||||
found = None
|
found = None
|
||||||
|
|
||||||
|
# Primary lookup: search by key first (preferred)
|
||||||
for serie in series:
|
for serie in series:
|
||||||
matches_key = getattr(serie, "key", None) == anime_id
|
if getattr(serie, "key", None) == anime_id:
|
||||||
matches_folder = getattr(serie, "folder", None) == anime_id
|
|
||||||
if matches_key or matches_folder:
|
|
||||||
found = serie
|
found = serie
|
||||||
break
|
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:
|
if not found:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -564,9 +729,11 @@ async def get_anime(
|
|||||||
for episode in episode_numbers:
|
for episode in episode_numbers:
|
||||||
episodes.append(f"{season}-{episode}")
|
episodes.append(f"{season}-{episode}")
|
||||||
|
|
||||||
|
# Return AnimeDetail with key as the primary identifier
|
||||||
return AnimeDetail(
|
return AnimeDetail(
|
||||||
id=getattr(found, "key", getattr(found, "folder", "")),
|
key=getattr(found, "key", ""),
|
||||||
title=getattr(found, "name", ""),
|
title=getattr(found, "name", ""),
|
||||||
|
folder=getattr(found, "folder", ""),
|
||||||
episodes=episodes,
|
episodes=episodes,
|
||||||
description=getattr(found, "description", None),
|
description=getattr(found, "description", None),
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user