Files
google-labs-jules[bot] af4c9461e8 Refactor legacy codebase into AniScrap Django platform (#45)
- 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>
2026-01-19 09:52:12 +00:00

332 lines
10 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.
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional, Any, Dict, Callable
import json
from tempfile import NamedTemporaryFile
from os.path import join
import subprocess as sp
import re
import unicodedata
from yt_dlp import YoutubeDL
from .animecix import _video_streams, search_animecix
from .common.utils import get_ydl_opts, get_video_resolution_mpv, extract_video_info
# Try to import other adapters if available for SearchEngine
try:
from .turkanime import TurkAnimeAdapter
except ImportError:
TurkAnimeAdapter = None
def _slugify(text: str) -> str:
"""Basit ve güvenli bir slug üretici: ASCII'ye indirger,
boşlukları '-' yapar, gereksizleri temizler."""
if not text:
return ""
# Unicode -> ASCII transliterasyon
t = unicodedata.normalize("NFKD", str(text))
t = t.encode("ascii", "ignore").decode("ascii")
t = t.lower()
t = re.sub(r"\s+", "-", t)
t = re.sub(r"[^a-z0-9\-]", "-", t)
t = re.sub(r"-+", "-", t).strip("-")
return t[:80]
@dataclass
class AdapterAnime:
slug: str
title: str
def __post_init__(self):
# Eğer slug sayı/ID ise ya da boşsa, başlıktan güvenli bir slug üret.
raw = (self.slug or "").strip()
if not raw or raw.isdigit() or not re.search(r"[a-zA-Z]", raw):
self.slug = _slugify(self.title)
class AdapterVideo:
"""TürkAnime Video arayüzüne minimum uyumlu basit video nesnesi."""
def __init__(
self,
bolum: "AdapterBolum",
url: Optional[str],
label: Optional[str] = None,
player: str = "ANIMECIX",
):
self.bolum = bolum
self._url = url or ""
self.label = label
self.player = player or "ANIMECIX"
self._info: Optional[Dict[str, Any]] = None
self.is_supported = True
self._is_working: Optional[bool] = None
self._resolution: Optional[int] = None
self.ydl_opts = get_ydl_opts()
@property
def url(self) -> str:
return self._url
@property
def info(self) -> Optional[Dict[str, Any]]:
if self._info is None:
info = extract_video_info(self.url, self.ydl_opts)
if not info:
self._info = {}
else:
# info'nun Dict[str, Any] olduğunu garanti edelim
if isinstance(info, dict):
if "direct" in info:
del info["direct"]
if info.get("video_ext") == "html":
self._info = None
else:
self._info = info
else:
self._info = {}
return self._info
@property
def is_working(self) -> bool:
if self._is_working is None:
try:
self._is_working = self.info not in (None, {})
except Exception:
self._is_working = False
return self._is_working
@is_working.setter
def is_working(self, value: bool):
self._is_working = value
def indir(self, callback=None, output=""):
assert self.is_working, "Video çalışmıyor."
seri_slug = self.bolum.anime.slug if getattr(self.bolum, "anime", None) else ""
out_tmpl_dir = join(output, seri_slug, self.bolum.slug)
opts = self.ydl_opts.copy()
if callback:
opts["progress_hooks"] = [callback]
opts["outtmpl"] = {"default": out_tmpl_dir + r".%(ext)s"}
with NamedTemporaryFile("w", delete=False) as tmp:
json.dump(self.info, tmp)
with YoutubeDL(opts) as ydl: # type: ignore
ydl.download_with_info_file(tmp.name)
def get(self, key, default=None):
"""Dictionary-like get method for compatibility."""
if key == "url":
return self.url
elif key == "label":
return self.label
elif key == "player":
return self.player
return default
def oynat(self, dakika_hatirla: bool = False):
"""Videoyu mpv ile oynat."""
import shutil
from .common.utils import BIN_PATH
from os.path import join, exists
# Önce bin/ klasöründeki mpv'yi dene, sonra sistem PATH'ini
mpv_path = join(BIN_PATH, "mpv.exe")
if not exists(mpv_path):
# Sistem PATH'inde mpv ara
mpv_path = shutil.which("mpv")
if not mpv_path:
print(
"MPV bulunamadı! Lütfen mpv'yi yükleyin veya bin/ klasörüne koyun."
)
return None
cmd = [mpv_path, self.url]
# User-agent ekle (HLS için gerekli olabilir)
cmd.extend(
[
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
]
)
# Dakika hatırlama özelliği
if dakika_hatirla:
cmd.append("--save-position-on-quit")
try:
proc = sp.Popen(cmd)
proc.wait() # İşlemin bitmesini bekle
return proc
except OSError as e:
# Binary uyumsuz olabilir, sistem mpv'yi dene
if e.winerror == 216: # WinError 216: Uyumsuz binary
print("bin/mpv.exe uyumsuz, sistem mpv deneniyor...")
sys_mpv = shutil.which("mpv")
if sys_mpv:
cmd[0] = sys_mpv
try:
proc = sp.Popen(cmd)
proc.wait()
return proc
except Exception as e2:
print(f"Sistem MPV hatası: {e2}")
return None
print(f"MPV başlatma hatası: {e}")
return None
except Exception as e:
print(f"MPV başlatma hatası: {e}")
return None
@property
def resolution(self) -> int:
if self._resolution is None:
info = self.info or {}
res = info.get("resolution")
if res:
m = re.findall(r"(\d{3,4})p", str(res))
if m:
self._resolution = int(m[0])
return self._resolution
fmts = info.get("formats") or []
if fmts:
try:
if "height" in (fmts[0] or {}):
self._resolution = (
max(fmts, key=lambda x: x.get("height") or 0).get("height")
or 0
)
else:
t = max(
fmts,
key=lambda x: (x.get("height") or 0, x.get("tbr") or 0),
)
self._resolution = (
t.get("height")
or (720 if (t.get("tbr") or 0) > 1500 else 480)
) or 0
except Exception:
self._resolution = 0
else:
# Label'dan tahmin
m = re.findall(r"(\d{3,4})p", str(self.label or ""))
self._resolution = int(m[0]) if m else 0
# mpv ile son çare çözünürlük tespiti
if not self._resolution:
self._resolution = get_video_resolution_mpv(self.url) or 0
return self._resolution or 0
class AdapterBolum:
def __init__(
self,
url: Optional[str],
title: str,
anime: AdapterAnime,
stream_provider: Optional[Callable[[str], List[Dict[str, str]]]] = None,
player_name: str = "ANIMECIX",
):
self.url = url
self._title = title
self.anime = anime
self._stream_provider = stream_provider
self._player_name = player_name or "ANIMECIX"
# TürkAnime ile uyumlu: animeadı-bolumadı (klasör: anime.slug, dosya adı: animeadı-bolumadı)
self.slug = _slugify(f"{anime.title}-{title}" if anime else title)
@property
def title(self):
return self._title
@property
def fansubs(self):
# AnimeciX tarafında fansub konsepti kullanılmıyor
return []
def best_video(
self,
by_res=True,
by_fansub=None,
default_res=600,
callback=lambda x: None,
early_subset: int = 8,
):
# URL kontrolü
if not self.url:
callback(
{
"current": 1,
"total": 1,
"player": "ANIMECIX",
"status": "URL bulunamadı",
}
)
return None
# Kaynağa uygun stream sağlayıcısını kullan
provider = self._stream_provider or _video_streams
player_label = self._player_name
callback(
{
"current": 0,
"total": 1,
"player": player_label,
"status": "üstbilgi çekiliyor",
}
)
streams = provider(self.url)
if not streams:
callback(
{
"current": 1,
"total": 1,
"player": player_label,
"status": "hiçbiri çalışmıyor",
}
)
return None
def parse_res(label: str) -> int:
m = re.findall(r"(\d{3,4})p", label or "")
return int(m[0]) if m else default_res
picked = (
max(streams, key=lambda s: parse_res(s.get("label") or "0p"))
if by_res
else streams[0]
)
video_url = picked.get("url")
if not video_url:
callback(
{
"current": 1,
"total": 1,
"player": player_label,
"status": "video URL bulunamadı",
}
)
return None
vid = AdapterVideo(self, video_url, picked.get("label"), player=player_label)
if vid.is_working:
callback(
{
"current": 1,
"total": 1,
"player": player_label,
"status": "çalışıyor",
}
)
return vid
callback(
{"current": 1, "total": 1, "player": player_label, "status": "çalışmıyor"}
)
return None
# SearchEngine removed or simplified as it depends on missing modules