mirror of
https://github.com/barkeser2002/offline-db.git
synced 2026-09-25 05:20:06 +03:00
- Initialized 'aniscrap_core' project. - Implemented 'users', 'content', 'billing', 'scraper_module', 'core' apps. - Added models for Anime, Episode, VideoFile (HLS/Encrypted), User, Wallet, etc. - Implemented Celery tasks for Video Encoding (H.265/HLS) and Revenue Distribution. - Integrated Shopier mock payment flow. - Configured Unfold Admin Dashboard. - Refactored Jikan scraper client. - Added 'init_aniscrap' management command. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
203 lines
6.3 KiB
Python
203 lines
6.3 KiB
Python
"""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
|
||
|
||
url = f"{ALT_URL}secure/related-videos?episode=1&season=1&titleId={safe_id}&videoId=637113"
|
||
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)))
|
||
|
||
|
||
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
|
||
|
||
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=637113"
|
||
)
|
||
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)
|
||
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
|
||
|
||
|
||
# Alias for compatibility with video_service
|
||
get_episode_streams = _video_streams
|