add download service
This commit is contained in:
187
backend/app/openings_downloader/services/animethemes.py
Normal file
187
backend/app/openings_downloader/services/animethemes.py
Normal file
@@ -0,0 +1,187 @@
|
||||
import httpx
|
||||
import logging
|
||||
import re
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from ..db_models import Anime, AnimeTheme, ThemeType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Shared HTTP client for AnimeThemes API
|
||||
_animethemes_client: Optional[httpx.AsyncClient] = None
|
||||
|
||||
|
||||
def _get_animethemes_client() -> httpx.AsyncClient:
|
||||
"""Get or create shared HTTP client for AnimeThemes."""
|
||||
global _animethemes_client
|
||||
if _animethemes_client is None or _animethemes_client.is_closed:
|
||||
_animethemes_client = httpx.AsyncClient(
|
||||
base_url="https://api.animethemes.moe",
|
||||
timeout=30.0,
|
||||
)
|
||||
return _animethemes_client
|
||||
|
||||
|
||||
class AnimeThemesService:
|
||||
"""Service for AnimeThemes API (api.animethemes.moe)."""
|
||||
|
||||
def __init__(self):
|
||||
self.client = _get_animethemes_client()
|
||||
|
||||
async def _find_anime_slug(self, anime: Anime) -> Optional[str]:
|
||||
"""Find AnimeThemes slug by searching anime title."""
|
||||
|
||||
# Try different title variations
|
||||
search_terms = [
|
||||
anime.title_english,
|
||||
anime.title_russian,
|
||||
anime.title_japanese,
|
||||
]
|
||||
|
||||
for term in search_terms:
|
||||
if not term:
|
||||
continue
|
||||
|
||||
try:
|
||||
response = await self.client.get(
|
||||
"/anime",
|
||||
params={
|
||||
"q": term,
|
||||
"include": "animethemes.animethemeentries.videos.audio",
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
animes = data.get("anime", [])
|
||||
if animes:
|
||||
slug = animes[0].get("slug")
|
||||
logger.info(f"Found AnimeThemes slug '{slug}' for '{term}'")
|
||||
return slug
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to search AnimeThemes for '{term}': {e}")
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
async def fetch_themes(self, db: AsyncSession, anime: Anime) -> List[AnimeTheme]:
|
||||
"""Fetch themes from AnimeThemes API and sync to DB."""
|
||||
|
||||
# Always reload anime with themes to avoid lazy loading issues
|
||||
result = await db.execute(
|
||||
select(Anime)
|
||||
.where(Anime.id == anime.id)
|
||||
.options(selectinload(Anime.themes))
|
||||
)
|
||||
anime = result.scalar_one()
|
||||
current_themes = anime.themes or []
|
||||
|
||||
# Find slug if not cached
|
||||
if not anime.animethemes_slug:
|
||||
logger.info(f"Searching AnimeThemes slug for: {anime.title_english or anime.title_russian}")
|
||||
slug = await self._find_anime_slug(anime)
|
||||
logger.info(f"Found slug: {slug}")
|
||||
if slug:
|
||||
anime.animethemes_slug = slug
|
||||
await db.commit()
|
||||
|
||||
if not anime.animethemes_slug:
|
||||
logger.warning(f"No AnimeThemes slug found for anime {anime.id}: {anime.title_english or anime.title_russian}")
|
||||
return current_themes
|
||||
|
||||
# Fetch themes from AnimeThemes API
|
||||
try:
|
||||
response = await self.client.get(
|
||||
f"/anime/{anime.animethemes_slug}",
|
||||
params={
|
||||
"include": "animethemes.animethemeentries.videos.audio,animethemes.song.artists",
|
||||
},
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.warning(f"AnimeThemes API returned {response.status_code} for {anime.animethemes_slug}")
|
||||
return current_themes
|
||||
|
||||
data = response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch themes from AnimeThemes for {anime.animethemes_slug}: {e}")
|
||||
return current_themes
|
||||
|
||||
anime_data = data.get("anime", {})
|
||||
themes_data = anime_data.get("animethemes", [])
|
||||
logger.info(f"AnimeThemes API returned {len(themes_data)} themes for {anime.animethemes_slug}")
|
||||
|
||||
# Build dict of existing themes
|
||||
existing_themes = {
|
||||
(t.theme_type, t.sequence): t
|
||||
for t in current_themes
|
||||
}
|
||||
|
||||
for theme_data in themes_data:
|
||||
# Parse theme type and sequence: "OP1", "ED1", etc.
|
||||
slug = theme_data.get("slug", "") # e.g., "OP1", "ED1"
|
||||
match = re.match(r"(OP|ED)(\d*)", slug)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
theme_type = ThemeType.OP if match.group(1) == "OP" else ThemeType.ED
|
||||
sequence = int(match.group(2)) if match.group(2) else 1
|
||||
|
||||
# Get video URL (prioritize audio link, then video link)
|
||||
video_url = None
|
||||
entries = theme_data.get("animethemeentries", [])
|
||||
if entries:
|
||||
videos = entries[0].get("videos", [])
|
||||
if videos:
|
||||
# Try to get audio link first
|
||||
audio = videos[0].get("audio")
|
||||
if audio:
|
||||
video_url = audio.get("link")
|
||||
# Fallback to video link
|
||||
if not video_url:
|
||||
video_url = videos[0].get("link")
|
||||
|
||||
# Get song info
|
||||
song_data = theme_data.get("song", {})
|
||||
song_title = song_data.get("title")
|
||||
artist = None
|
||||
artists = song_data.get("artists", [])
|
||||
if artists:
|
||||
artist = artists[0].get("name")
|
||||
|
||||
key = (theme_type, sequence)
|
||||
if key in existing_themes:
|
||||
# Update existing theme
|
||||
theme = existing_themes[key]
|
||||
theme.song_title = song_title
|
||||
theme.artist = artist
|
||||
if video_url:
|
||||
theme.animethemes_video_url = video_url
|
||||
else:
|
||||
# Create new theme
|
||||
theme = AnimeTheme(
|
||||
anime_id=anime.id,
|
||||
theme_type=theme_type,
|
||||
sequence=sequence,
|
||||
song_title=song_title,
|
||||
artist=artist,
|
||||
animethemes_video_url=video_url,
|
||||
)
|
||||
db.add(theme)
|
||||
if anime.themes is None:
|
||||
anime.themes = []
|
||||
anime.themes.append(theme)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Reload anime with themes to get fresh data
|
||||
result = await db.execute(
|
||||
select(Anime)
|
||||
.where(Anime.id == anime.id)
|
||||
.options(selectinload(Anime.themes))
|
||||
)
|
||||
refreshed_anime = result.scalar_one()
|
||||
return refreshed_anime.themes
|
||||
Reference in New Issue
Block a user