utility-scripts/download_anime_covers_proxy.py
shu 70814ddc1f import: anime utility scripts from /storage/scripts/
Перенос из vespaserver:/storage/scripts/ перед удалением оригиналов.
Скрипты разовые — клонируются по мере необходимости.
2026-04-29 18:24:58 +03:00

389 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Скачивает обложки аниме через Jikan API (jikan.moe) и встраивает их в аудиофайлы.
Использует SOCKS5 прокси для доступа к API и скачивания изображений.
Название аниме берётся из первых квадратных скобок в названии файла:
TV-2 OP01 - Nobody Knows (Suga Shikao) [xxxHOLiC].mp3
^^^^^^^^
извлекаем это
Jikan API - бесплатный API без аутентификации (парсит MyAnimeList)
https://jikan.moe/
Использование:
python3 download_anime_covers_proxy.py run --dry-run /path/to/music
python3 download_anime_covers_proxy.py run /path/to/music
python3 download_anime_covers_proxy.py search "xxxHOLiC" # Поиск обложки для теста
"""
import os
import sys
import argparse
import re
import time
import requests
from pathlib import Path
from typing import Optional, Tuple, Dict
try:
from mutagen.flac import FLAC
from mutagen.mp3 import MP3
from mutagen.oggvorbis import OggVorbis
from mutagen.oggopus import OggOpus
from mutagen.mp4 import MP4
from mutagen.id3 import APIC, ID3
except ImportError:
print("Установите mutagen: pip install mutagen")
sys.exit(1)
try:
from urllib3.contrib.socks import SOCKSProxyManager
SOCKS_AVAILABLE = True
except ImportError:
SOCKS_AVAILABLE = False
print("Warning: SOCKS proxy not available. Install: pip install requests[socks]")
AUDIO_EXTENSIONS = {".flac", ".mp3", ".ogg", ".opus", ".m4a"}
# ============================================================================
# КОНФИГУРАЦИЯ
# ============================================================================
# Jikan API (бесплатный, без ключа)
JIKAN_BASE_URL = "https://api.jikan.moe/v4"
# Прокси для доступа к API (SOCKS5)
PROXY_URL = "socks5://192.168.31.216:1080"
# Кэш обложек (чтобы не скачивать одну и ту же много раз)
COVER_CACHE: Dict[str, bytes] = {}
# Задержки для API (Jikan требует ~2 секунды между запросами)
JIKAN_DELAY = 2.0
# ============================================================================
# ФУНКЦИИ
# ============================================================================
def extract_anime_name(filepath: Path) -> Optional[str]:
"""
Извлекает название аниме из первых квадратных скобок в названии файла.
"""
stem = filepath.stem
match = re.search(r'\[([^\]]+)\]', stem)
if match:
return match.group(1).strip()
return None
class JikanAPI:
"""Класс для работы с Jikan API (jikan.moe) через прокси"""
def __init__(self, proxy_url: str = None):
self.base_url = JIKAN_BASE_URL
self.session = self._create_session(proxy_url)
self.last_request_time = 0
def _create_session(self, proxy_url: str = None) -> requests.Session:
"""Создаёт сессию с прокси (если указан)"""
session = requests.Session()
# Jikan требует User-Agent
session.headers.update({
'User-Agent': 'Anime-Cover-Downloader/1.0 (shu@vespahomelab.ru)'
})
# Настраиваем прокси через SOCKS5
if proxy_url and SOCKS_AVAILABLE:
try:
proxy_manager = SOCKSProxyManager(proxy_url)
session.mount('http://', proxy_manager)
session.mount('https://', proxy_manager)
print(f" ✓ Прокси настроен: {proxy_url}")
except Exception as e:
print(f" ⚠ Warning: Не удалось настроить прокси: {e}. Будет использовано прямое соединение.")
return session
def _rate_limit(self):
"""Соблюдает лимиты API Jikan (~2 секунды между запросами)"""
elapsed = time.time() - self.last_request_time
if elapsed < JIKAN_DELAY:
time.sleep(JIKAN_DELAY - elapsed)
self.last_request_time = time.time()
def search_anime(self, query: str, limit: int = 5) -> list:
"""Ищет аниме по названию"""
self._rate_limit()
url = f"{self.base_url}/anime"
params = {
'q': query,
'limit': limit
}
try:
response = self.session.get(url, params=params, timeout=30)
response.raise_for_status()
data = response.json()
results = []
for item in data.get('data', []):
title = item.get('title', '')
title_english = item.get('title_english', '')
images = item.get('images', {})
jpg = images.get('jpg', {})
image_url = jpg.get('large', jpg.get('image_url', ''))
if image_url:
results.append({
'title': title,
'title_english': title_english,
'image_url': image_url,
'source': 'Jikan'
})
return results
except Exception as e:
print(f" ✗ Ошибка поиска в Jikan: {e}")
return []
def get_anime_cover(self, anime_name: str) -> Optional[str]:
"""Получает URL обложки для аниме"""
results = self.search_anime(anime_name)
if results:
return results[0]['image_url']
return None
def download_image(url: str, proxy_url: str = None) -> Optional[bytes]:
"""Скачивает изображение по URL через прокси"""
try:
session = requests.Session()
if proxy_url and SOCKS_AVAILABLE:
try:
proxy_manager = SOCKSProxyManager(proxy_url)
session.mount('http://', proxy_manager)
session.mount('https://', proxy_manager)
except:
pass
response = session.get(url, timeout=30)
response.raise_for_status()
return response.content
except Exception as e:
print(f" ✗ Ошибка скачивания изображения: {e}")
return None
def get_cover_image(anime_name: str, jikan_api: JikanAPI, proxy_url: str = None) -> Optional[bytes]:
"""
Получает обложку для аниме через Jikan API.
Использует кэш для повторных запросов.
"""
# Проверяем кэш
cache_key = anime_name.lower()
if cache_key in COVER_CACHE:
print(f" ✓ Найдено в кэше: {anime_name}")
return COVER_CACHE[cache_key]
print(f" Поиск обложки для: {anime_name}")
# Ищем через Jikan
image_url = jikan_api.get_anime_cover(anime_name)
if image_url:
print(f" ✓ Найдено: {image_url}")
image_data = download_image(image_url, proxy_url)
if image_data:
COVER_CACHE[cache_key] = image_data
return image_data
print(f" ✗ Обложка не найдена")
return None
def embed_cover(filepath: Path, image_data: bytes, dry_run: bool = False) -> bool:
"""Встраивает обложку в аудиофайл."""
if dry_run:
return True
ext = filepath.suffix.lower()
try:
if ext == ".mp3":
audio = MP3(str(filepath), ID3=ID3)
if audio.tags is None:
audio.add_tags()
# Удаляем старую обложку если есть
audio.tags.delall('APIC')
# Добавляем новую
audio.tags.add(APIC(
encoding=3,
mime='image/jpeg',
type=3, # Front cover
desc='Cover',
data=image_data
))
audio.save()
elif ext == ".flac":
audio = FLAC(str(filepath))
audio.clear_pictures()
audio.add_picture(image_data)
audio.save()
elif ext == ".ogg":
audio = OggVorbis(str(filepath))
from mutagen.flac import Picture
pic = Picture()
pic.type = 3
pic.mime = 'image/jpeg'
pic.data = image_data
audio.add_picture(pic)
audio.save()
elif ext == ".opus":
audio = OggOpus(str(filepath))
from mutagen.flac import Picture
pic = Picture()
pic.type = 3
pic.mime = 'image/jpeg'
pic.data = image_data
audio.add_picture(pic)
audio.save()
elif ext == ".m4a":
audio = MP4(str(filepath))
audio['covr'] = [image_data]
audio.save()
else:
return False
return True
except Exception as e:
print(f" ✗ Ошибка встраивания обложки: {e}")
return False
def process_directory(root_path: str, dry_run: bool = False):
"""Обработать директорию."""
root = Path(root_path)
if not root.is_dir():
print(f"Директория не найдена: {root_path}")
sys.exit(1)
# Инициализируем Jikan API с прокси
jikan_api = JikanAPI(PROXY_URL)
prefix_str = "[DRY] " if dry_run else ""
processed = 0
skipped = 0
errors = 0
no_cover = 0
audio_files = sorted([
f for f in root.rglob("*")
if f.is_file() and f.suffix.lower() in AUDIO_EXTENSIONS
])
if not audio_files:
print("Аудиофайлы не найдены")
return
print(f"{prefix_str}Найдено файлов: {len(audio_files)}\n")
# Группируем файлы по названию аниме для кэширования обложек
anime_covers: Dict[str, Optional[bytes]] = {}
for idx, filepath in enumerate(audio_files, 1):
anime_name = extract_anime_name(filepath)
if not anime_name:
skipped += 1
continue
print(f"[{idx}/{len(audio_files)}] {filepath.name}")
# Получаем обложку (с кэшированием)
if anime_name not in anime_covers:
cover_data = get_cover_image(anime_name, jikan_api, PROXY_URL)
anime_covers[anime_name] = cover_data
else:
cover_data = anime_covers[anime_name]
print(f" ✓ Используем кэш для: {anime_name}")
if cover_data is None:
no_cover += 1
continue
success = embed_cover(filepath, cover_data, dry_run)
if success:
processed += 1
if dry_run:
print(f" ✓ Будет встроена обложка")
else:
print(f" ✓ Обложка встроена")
else:
errors += 1
print(f"\n{'=' * 60}")
print(f" {'Будет обработано' if dry_run else 'Обработано'}: {processed}")
print(f" Пропущено (нет названия аниме): {skipped}")
print(f" Не найдено обложек: {no_cover}")
print(f" Ошибок: {errors}")
print(f"{'=' * 60}")
def search_cover(anime_name: str):
"""Тестовый поиск обложки для аниме"""
jikan_api = JikanAPI(PROXY_URL)
print(f"Поиск обложки для: {anime_name}\n")
print("Jikan (MyAnimeList):")
results = jikan_api.search_anime(anime_name, limit=5)
for i, result in enumerate(results, 1):
print(f" {i}. {result['title']}")
if result['title_english']:
print(f" English: {result['title_english']}")
print(f" {result['image_url']}")
if not results:
print(" Не найдено")
def main():
parser = argparse.ArgumentParser(description="Скачивание и встраивание обложек аниме (с прокси)")
sub = parser.add_subparsers(dest="command")
p_run = sub.add_parser("run", help="Обработать директорию")
p_run.add_argument("directory", help="Директория с музыкой")
p_run.add_argument("--dry-run", action="store_true", help="Показать что будет сделано без изменений")
p_search = sub.add_parser("search", help="Поиск обложки для теста")
p_search.add_argument("anime_name", help="Название аниме для поиска")
args = parser.parse_args()
if args.command == "search":
search_cover(args.anime_name)
elif args.command == "run":
process_directory(args.directory, args.dry_run)
else:
parser.print_help()
if __name__ == "__main__":
main()