Files
Barış Keser 0fb3e54316 Refactor and enhance Jikan client and sources
- Updated JikanCache and JikanClient classes to use Optional types for parameters.
- Improved error handling and dynamic video ID fetching in animecix.py.
- Removed unused Animely adapter and adjusted provider priorities.
- Cleaned up adapter.py by removing the SearchEngine class.
- Enhanced error handling and cookie management in tranime.py.
- Added direct search functionality in tranime.py to improve anime search results.
- Updated title matching logic in title_matching.py to use Optional types.
- Bumped version to 9.4.9.0.
2026-02-26 00:54:11 +03:00

229 lines
7.4 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.
"""AnimeciX kaynağı (minimal Python port)
Bu modül, AnimeciX API uçlarından arama ve bölüm/izleme verilerini çeker.
Mevcut `objects.Anime/Bolum/Video` yapısına dokunmamak için, yalnızca
harici arama/episode/watch listesi sağlar; indirme/oynatma yine yt-dlp/mpv ile.
Cloudflare koruması için cf_bypass modülü entegre edilmiştir.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Dict, Any, Optional, Tuple
import json
import re
from urllib.parse import urlparse, parse_qs, quote, urlsplit, urlunsplit
import urllib.request
# Cloudflare bypass entegrasyonu
try:
from ..common.cf_bypass import CFSession, CFBypassError
HAS_CF_BYPASS = True
except ImportError:
HAS_CF_BYPASS = False
BASE_URL = "https://animecix.tv/"
ALT_URL = "https://mangacix.net/"
HEADERS = {"Accept": "application/json", "User-Agent": "Mozilla/5.0"}
VIDEO_PLAYERS = ["tau-video.xyz", "sibnet"]
# Global CF session (lazy-load)
_cf_session: Optional[CFSession] = None
def _get_cf_session() -> Optional[CFSession]:
"""CF session'ı lazy-load et."""
global _cf_session
if _cf_session is None and HAS_CF_BYPASS:
_cf_session = CFSession(impersonate="chrome110", timeout=15, max_retries=3)
return _cf_session
def _http_get(url: str, timeout: int = 10) -> bytes:
"""HTTP GET isteği - önce CF bypass, sonra fallback urllib."""
# Non-ASCII pathleri ASCII'ye uygun hale getirmek için yüzde-encode et
sp = urlsplit(url)
safe_path = quote(sp.path, safe="/:%@")
safe_url = urlunsplit((sp.scheme, sp.netloc, safe_path, sp.query, sp.fragment))
# Önce CF bypass ile dene
cf_session = _get_cf_session()
if cf_session is not None:
try:
resp = cf_session.get(safe_url, headers=HEADERS)
if resp.status_code == 200:
return resp.content
except (CFBypassError, Exception) as e:
print(f"[AnimeCix] CF bypass başarısız, fallback kullanılıyor: {e}")
# Fallback: Normal urllib
req = urllib.request.Request(safe_url, headers=HEADERS)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.read()
def search_animecix(query: str, timeout: int = 8) -> List[Tuple[str, str]]:
# Boşluk -> '-' ve non-ASCII karakterleri encode et
q = (query or "").strip().replace(" ", "-")
q_enc = quote(q, safe="-")
url = f"{BASE_URL}secure/search/{q_enc}?type=&limit=20"
data = json.loads(_http_get(url, timeout=timeout))
results = []
res = data.get("results") or []
for item in res:
name = item.get("name")
_id = item.get("id")
if name is None or _id is None:
continue
results.append((str(_id), str(name)))
return results
def _seasons_for_title(title_id: int) -> List[int]:
# title_id'yi güvenli şekilde int'e çevir
try:
safe_id = int(title_id)
except (ValueError, TypeError):
safe_id = hash(str(title_id)) % 1000000 if title_id else 0
# Önce title bilgisini al ve video ID'yi dinamik çek
video_id = ""
try:
title_url = f"{BASE_URL}secure/titles/{safe_id}"
title_data = json.loads(_http_get(title_url))
title_obj = title_data.get("title", title_data)
# İlk video'nun ID'sini bul
videos = title_obj.get("videos", [])
if videos:
video_id = str(videos[0].get("id", ""))
# Sezon sayısını direkt al
seasons = title_obj.get("seasons", [])
if seasons:
return list(range(len(seasons)))
except Exception:
pass
# Fallback: related-videos endpoint
if not video_id:
video_id = "637113" # Eski hardcoded değer
url = f"{ALT_URL}secure/related-videos?episode=1&season=1&titleId={safe_id}&videoId={video_id}"
try:
data = json.loads(_http_get(url))
videos = data.get("videos") or []
if not videos:
return []
title = (videos[0] or {}).get("title") or {}
seasons = title.get("seasons") or []
return list(range(len(seasons)))
except Exception:
return []
def _episodes_for_title(title_id: int) -> List[Dict[str, Any]]:
# title_id'yi güvenli şekilde int'e çevir
try:
safe_id = int(title_id)
except (ValueError, TypeError):
safe_id = hash(str(title_id)) % 1000000 if title_id else 0
# Dinamik video ID al
video_id = ""
try:
title_url = f"{BASE_URL}secure/titles/{safe_id}"
title_data = json.loads(_http_get(title_url))
title_obj = title_data.get("title", title_data)
videos = title_obj.get("videos", [])
if videos:
video_id = str(videos[0].get("id", ""))
except Exception:
pass
if not video_id:
video_id = "637113" # Eski hardcoded fallback
episodes: List[Dict[str, Any]] = []
seen = set()
for sidx in _seasons_for_title(safe_id):
url = (
f"{ALT_URL}secure/related-videos?"
f"episode=1&season={sidx+1}&titleId={safe_id}&videoId={video_id}"
)
try:
data = json.loads(_http_get(url))
for v in data.get("videos", []):
name = v.get("name")
ep_url = v.get("url")
if not name or not ep_url:
continue
if name in seen:
continue
episodes.append({"name": name, "url": ep_url, "season_num": v.get("season_num")})
seen.add(name)
except Exception:
continue
return episodes
def _video_streams(embed_path: str) -> List[Dict[str, str]]:
# BASE_URL + embed path'e gidip yönlendirilmiş URL'den player id/vid al
import http.client
import ssl
# Embed path non-ASCII içerebilir; güvenle encode et
full = f"{BASE_URL}{quote(embed_path, safe='/:?=&')}"
# Basit urllib ile final URL
resp = urllib.request.urlopen(urllib.request.Request(full, headers=HEADERS))
final_url = resp.geturl()
p = urlparse(final_url)
parts = p.path.strip("/").split("/")
if len(parts) < 2:
return []
embed_id = parts[1] if parts[0] == "embed" else parts[0]
qs = parse_qs(p.query)
vid = (qs.get("vid") or [None])[0]
if not embed_id or not vid:
return []
api = f"https://{VIDEO_PLAYERS[0]}/api/video/{embed_id}?vid={vid}"
data = json.loads(_http_get(api))
out: List[Dict[str, str]] = []
for u in data.get("urls", []):
label = u.get("label")
url = u.get("url")
if label and url:
out.append({"label": label, "url": url})
return out
@dataclass
class CixEpisode:
title: str
url: str
@dataclass
class CixAnime:
"""AnimeciX başlığı.
Not: Bu sınıf, yalnızca isim ve bölümleri sağlar. Oynatma/indirme için
mevcut Video/Bolum akışı kullanılmaya devam edilir.
"""
id: str # ID artık string olabilir
title: str
@property
def episodes(self) -> List[CixEpisode]:
# ID'yi güvenli şekilde int'e çevir
try:
title_id = int(self.id)
except (ValueError, TypeError):
# String ID ise hash değeri al
title_id = hash(self.id) % 1000000 if isinstance(self.id, str) and self.id else 0
eps = _episodes_for_title(title_id)
out: List[CixEpisode] = []
for i, e in enumerate(eps):
out.append(CixEpisode(title=e.get("name") or f"Bölüm {i+1}", url=e.get("url") or ""))
return out