Remove Shikimori API, use AnimeThemes only, switch to WebM format

- Remove ShikimoriService, use AnimeThemes API for search
- Replace shikimori_id with animethemes_slug as primary identifier
- Remove FFmpeg MP3 conversion, download WebM directly
- Add .webm support in storage and upload endpoints
- Update frontend to use animethemes_slug

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-12 11:22:46 +03:00
parent 333de65fbd
commit cc11f0b773
12 changed files with 138 additions and 263 deletions

View File

@@ -7,6 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from ..db_models import Anime, AnimeTheme, ThemeType
from ..schemas import AnimeSearchResult
logger = logging.getLogger(__name__)
@@ -31,41 +32,100 @@ class AnimeThemesService:
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."""
async def search(self, query: str, limit: int = 20) -> List[AnimeSearchResult]:
"""Search anime by query using AnimeThemes API."""
try:
response = await self.client.get(
"/anime",
params={
"q": query,
"include": "images",
"page[size]": limit,
},
)
response.raise_for_status()
data = response.json()
# Try different title variations
search_terms = [
anime.title_english,
anime.title_russian,
anime.title_japanese,
]
results = []
for anime in data.get("anime", []):
# Get poster URL from images
poster_url = None
images = anime.get("images", [])
if images:
# Prefer large_cover or first available
for img in images:
if img.get("facet") == "Large Cover":
poster_url = img.get("link")
break
if not poster_url and images:
poster_url = images[0].get("link")
for term in search_terms:
if not term:
continue
results.append(AnimeSearchResult(
animethemes_slug=anime.get("slug"),
title_english=anime.get("name"),
year=anime.get("year"),
poster_url=poster_url,
))
try:
response = await self.client.get(
"/anime",
params={
"q": term,
"include": "animethemes.animethemeentries.videos.audio",
},
)
return results
except Exception as e:
logger.error(f"Failed to search AnimeThemes: {e}")
return []
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
async def get_or_create_anime(self, db: AsyncSession, slug: str) -> Optional[Anime]:
"""Get anime from DB or fetch from AnimeThemes and create."""
return None
# Check if exists (with themes eagerly loaded)
query = (
select(Anime)
.where(Anime.animethemes_slug == slug)
.options(selectinload(Anime.themes))
)
result = await db.execute(query)
anime = result.scalar_one_or_none()
if anime:
return anime
# Fetch from AnimeThemes
try:
response = await self.client.get(
f"/anime/{slug}",
params={"include": "images"},
)
response.raise_for_status()
data = response.json()
except Exception as e:
logger.error(f"Failed to fetch anime from AnimeThemes: {e}")
return None
anime_data = data.get("anime", {})
if not anime_data:
return None
# Get poster URL
poster_url = None
images = anime_data.get("images", [])
if images:
for img in images:
if img.get("facet") == "Large Cover":
poster_url = img.get("link")
break
if not poster_url and images:
poster_url = images[0].get("link")
anime = Anime(
animethemes_slug=slug,
title_english=anime_data.get("name"),
year=anime_data.get("year"),
poster_url=poster_url,
)
db.add(anime)
await db.commit()
await db.refresh(anime)
return anime
async def fetch_themes(self, db: AsyncSession, anime: Anime) -> List[AnimeTheme]:
"""Fetch themes from AnimeThemes API and sync to DB."""
@@ -79,17 +139,8 @@ class AnimeThemesService:
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}")
logger.warning(f"No AnimeThemes slug for anime {anime.id}")
return current_themes
# Fetch themes from AnimeThemes API