mirror of
https://github.com/RoyalnetworkTR/turkanime-client.git
synced 2026-09-25 01:00:00 +03:00
Pre-compile regular expressions used in the `Anime.fetch_info` loop to
avoid redundant compilation and cache lookups during anime data parsing.
This results in a ~41% performance improvement in the regex-intensive
portions of the metadata extraction logic.
Optimized patterns:
- _TAG_RE: <.*?>
- _LEAD_SPACE_RE: ^ {1,3}
- _SCORE_RE: ^(.*?)
Co-authored-by: barkeser2002 <45911502+barkeser2002@users.noreply.github.com>
515 lines
20 KiB
Python
515 lines
20 KiB
Python
""" Örnek:
|
||
>>> ani = Anime("non-non-biyori")
|
||
>>> bol3 = ani.bolumler[2]
|
||
>>> vid1 = bol3.videos[0]
|
||
>>> vid1.oynat()
|
||
"""
|
||
from os import remove
|
||
from os.path import join
|
||
from tempfile import NamedTemporaryFile
|
||
import subprocess as sp
|
||
import re
|
||
import json
|
||
from yt_dlp import YoutubeDL
|
||
try:
|
||
from yt_dlp.networking.impersonate import ImpersonateTarget
|
||
except ImportError:
|
||
ImpersonateTarget = None
|
||
from .common.utils import get_ydl_opts, get_video_resolution_mpv, extract_video_info
|
||
|
||
from .bypass import get_real_url, unmask_real_url, fetch, get_alucard_m3u8
|
||
from .common.utils import get_platform, get_arch
|
||
|
||
_TAG_RE = re.compile("<.*?>")
|
||
_LEAD_SPACE_RE = re.compile("^ {1,3}")
|
||
_SCORE_RE = re.compile("^(.*?) ")
|
||
|
||
# Çalıştığı bilinen playerlar ve öncelikleri
|
||
SUPPORTED = [
|
||
"YADISK",
|
||
"MAIL",
|
||
"ALUCARD(BETA)",
|
||
"PIXELDRAIN",
|
||
"AMATERASU(BETA)",
|
||
"HDVID",
|
||
"ODNOKLASSNIKI",
|
||
"GDRIVE",
|
||
"MP4UPLOAD",
|
||
"DAILYMOTION",
|
||
"SIBNET",
|
||
"VK",
|
||
"VIDMOLY",
|
||
"YOURUPLOAD",
|
||
"SENDVID",
|
||
"MYVI",
|
||
"UQLOAD",
|
||
]
|
||
|
||
class LogHandler:
|
||
""" TODO: ytdlp log handler prototipi """
|
||
@staticmethod
|
||
def error(msg):
|
||
pass # msg is unused but required by yt-dlp interface
|
||
@staticmethod
|
||
def warning(msg):
|
||
pass # msg is unused but required by yt-dlp interface
|
||
@staticmethod
|
||
def debug(msg):
|
||
pass # msg is unused but required by yt-dlp interface
|
||
|
||
|
||
class Anime:
|
||
"""
|
||
Bir anime serisi hakkında her verinin bulunduğu obje.
|
||
Obje'yi yaratmak için animenin kodu veya URL'si yeterlidir.
|
||
|
||
Öznitelikler:
|
||
- slug: Animenin kodu: "non-non-biyori".
|
||
- title: Animenin okunaklı ismi, örneğin: Non Non Biyori
|
||
- anime_id: Animenin türkanime id'si. Bölümleri ve anime resmini getirirken gerekli.
|
||
- bolumler_data: Anime bölümlerinin [(slug:str,isim:str),] formatında listesi.
|
||
- bolumler: Anime bölümlerinin [bolum1:Bolum,bolum2:Bolum,] formatında obje listesi.
|
||
- info: Anime sayfasından parse'lanan özet,kategori,stüdyo gibi meta bilgileri içeren dict.
|
||
- parse_fansubs: Bolum objesi yaratılırken fansubları da parse'lamasını belirt.
|
||
"""
|
||
def __init__(self,slug,parse_fansubs=True):
|
||
self.slug = slug
|
||
self.title = None
|
||
self.anime_id = 0
|
||
self.info = {
|
||
"Kategori":None,
|
||
"Japonca":None,
|
||
"Anime Türü":[],
|
||
"Bölüm Sayısı":0,
|
||
"Başlama Tarihi":None,
|
||
"Bitiş Tarihi":None,
|
||
"Stüdyo":None,
|
||
"Puanı":0.0,
|
||
"Özet":None,
|
||
"Resim":None
|
||
}
|
||
self.fetch_info()
|
||
self._bolumler_data = None
|
||
self._bolumler = []
|
||
self.parse_fansubs = parse_fansubs
|
||
|
||
def fetch_info(self):
|
||
"""Anime detay sayfasını ayrıştır."""
|
||
src = fetch(f'/anime/{self.slug}')
|
||
twitmeta = re.findall(r'twitter.image" content="(.*?serilerb/(.*?)\.jpg)"',src)[0]
|
||
self.info["Resim"], self.anime_id = twitmeta
|
||
if not self.title:
|
||
self.title = re.findall(r'<title>(.*?)<\/title>',src).pop()
|
||
|
||
# Anime sayfasındaki bilgi tablosunu parse'la
|
||
info_table=re.findall(r'<div id="animedetay">(<table.*?</table>)',src)[0]
|
||
raw_m = re.findall(r"<tr>.*?<b>(.*?)<\/b>.*?width.*?>(.*?)<\/td>.*?<\/tr>",info_table)
|
||
for key,val in raw_m:
|
||
if not key in self.info:
|
||
continue
|
||
val = _TAG_RE.sub("",val)
|
||
val = _LEAD_SPACE_RE.sub("",val)
|
||
if key == "Puanı":
|
||
val = float(_SCORE_RE.findall(val).pop())
|
||
elif key == "Anime Türü":
|
||
val = val.split(" ")
|
||
self.info[key] = val
|
||
self.info["Özet"] = re.findall('"ozet">(.*?)</p>',info_table)[0]
|
||
|
||
def get_bolum_listesi(self):
|
||
""" Anime bölümlerinin [(slug,isim),] formatında listesi. """
|
||
anime_id = self.anime_id
|
||
src = fetch(f'/ajax/bolumler&animeId={anime_id}')
|
||
return re.findall(r'\/video\/(.*?)\\?".*?title=.*?"(.*?)\\?"',src)
|
||
|
||
@staticmethod
|
||
def get_anime_listesi():
|
||
""" Anime serilerinin [(slug,isim),] formatında listesi. """
|
||
src = fetch("/ajax/tamliste")
|
||
return re.findall(r'\/anime\/(.*?)".*?animeAdi">(.*?)<',src)
|
||
|
||
@property
|
||
def bolumler(self):
|
||
""" Anime bölümlerinin [Bolum,] formatında listesi. """
|
||
if not self._bolumler:
|
||
for slug,title in self.get_bolum_listesi():
|
||
self._bolumler.append(
|
||
Bolum(
|
||
slug=slug,
|
||
title=title,
|
||
anime=self,
|
||
parse_fansubs=self.parse_fansubs))
|
||
return self._bolumler
|
||
|
||
|
||
|
||
class Bolum:
|
||
"""
|
||
Bir anime bölum'ünü temsil eden obje.
|
||
Obje'yi yaratmak için bölümün kodu veya URL'si yeterlidir.
|
||
|
||
Öznitelikler:
|
||
- slug: Bölümün kodu: "naruto-54-bolum" veya URLsi: "https://turkani.co/video/naruto-54-bolum".
|
||
- title: Bölümün okunaklı ismi. (opsiyonel)
|
||
- anime: Bölümün ait olduğu anime'nin objesi, eğer tanımlanmadıysa erişildiğinde yaratılır.
|
||
- parse_fansubs: Fansubları da parse'la. Fazladan fansub sayısı kadar istek gönderir.
|
||
"""
|
||
def __init__(self,slug,anime=None,title=None,parse_fansubs=True):
|
||
if "http" == slug[:4]:
|
||
slug = slug.split("/")[-1]
|
||
self.slug = slug
|
||
self.parse_fansubs = parse_fansubs
|
||
self._title = title
|
||
self._html = None
|
||
self._videos = []
|
||
self._anime = anime
|
||
self._fansubs = []
|
||
|
||
@property
|
||
def html(self):
|
||
if self._html is None:
|
||
self._html = fetch(f"/video/{self.slug}")
|
||
return self._html
|
||
|
||
@property
|
||
def title(self):
|
||
if self._title is None:
|
||
self._title = re.findall(r'<title>(.*?)<\/title>',self.html)[0]
|
||
return self._title
|
||
|
||
@property
|
||
def videos(self):
|
||
if not self._videos:
|
||
self.get_videos()
|
||
return self._videos
|
||
|
||
@property
|
||
def anime(self):
|
||
""" Bu bölümün ait olduğu anime serisi objesini yarat. """
|
||
if self._anime is None:
|
||
...
|
||
return self._anime
|
||
|
||
@property
|
||
def fansubs(self):
|
||
""" Bölüm sayfasından fansub listesini ayrıştır. """
|
||
if not self._fansubs:
|
||
self._fansubs = re.findall(r"</span> ([^<>/]*?)</a></button>",self.html)
|
||
if not self._fansubs:
|
||
self._fansubs = re.findall(r"</span> ([^\\<>]*)</button>.*?iframe",self.html)
|
||
return self._fansubs
|
||
|
||
def get_videos(self):
|
||
self._videos = []
|
||
# Yalnızca tek bir fansub varsa
|
||
if not re.search(".*birden fazla grup",self.html):
|
||
fansub = re.findall(r"</span> ([^\\<>]*)</button>.*?iframe",self.html)[0]
|
||
vids = re.findall(
|
||
r"/embed/#/url/(.*?)\?status=0\".*?</span> ([^ ]*?) ?</button>",
|
||
self.html
|
||
)
|
||
vids += re.findall(
|
||
r"(ajax\/videosec&b=[A-Za-z0-9]+&v=.*?)'.*?<\/span> ?(.*?)<\/button",
|
||
self.html
|
||
)
|
||
for vpath,player in vids:
|
||
self._videos.append(Video(self,vpath,player,fansub))
|
||
# Fansublar da parse'lanacaksa
|
||
elif self.parse_fansubs:
|
||
fansubs = re.findall(r"(ajax\/videosec&.*?)'.*?<\/span> ?(.*?)<\/a>",self.html)
|
||
for path,fansub in fansubs:
|
||
src = fetch(path)
|
||
vids = re.findall(
|
||
r"/embed/#/url/(.*?)\?status=0\".*?</span> ([^ ]*?) ?</button>",
|
||
src
|
||
)
|
||
vids += re.findall(
|
||
r"(ajax\/videosec&b=[A-Za-z0-9]+&v=.*?)'.*?<\/span> ?(.*?)<\/button",
|
||
src
|
||
)
|
||
for vpath,player in vids:
|
||
self._videos.append(Video(self,vpath,player,fansub))
|
||
# Fansubları parselamaksızın tüm videoları getir
|
||
else:
|
||
allpath = re.findall(
|
||
r"(ajax\/videosec&b=[A-Za-z0-9]+.*?)&[fv]=.*?'.*?<\/span>",
|
||
self.html
|
||
)[0]
|
||
src = fetch(allpath)
|
||
vids = re.findall(r"/embed/#/url/(.*?)\?status=0\".*?</span> ([^ ]*?) ?</button>", src)
|
||
vids += re.findall(
|
||
r"(ajax\/videosec&b=[A-Za-z0-9]+&v=.*?)'.*?<\/span> ?(.*?)<\/button",
|
||
src
|
||
)
|
||
for vpath,player in vids:
|
||
self._videos.append(Video(self,vpath,player))
|
||
return self._videos
|
||
|
||
def best_video(self, by_res=True, by_fansub=None, default_res=600, callback=lambda x: None, early_subset: int = 8):
|
||
"""
|
||
Parametrelerde belirtilmiş en ideal videoyu tespit edip döndürür.
|
||
|
||
- by_res: 1080p'ye ulaşmaya çalış
|
||
- by_fansub: belirtilen fansub'u öncelikli tut.
|
||
- default_res: Çözünürlüğü bilinmeyen videoların varsayılan çözünürlüğü.
|
||
- callback: function(hook_dict)
|
||
"""
|
||
# Yalnızca desteklenenleri filtrele.
|
||
vids = list(filter(lambda x: x.is_supported, self.videos))
|
||
|
||
# Callback fonksiyonu belirtilmişse paslanacak hook_dict
|
||
hook_dict = {
|
||
"current": None,
|
||
"total": len(vids),
|
||
"player": None,
|
||
"status": None,
|
||
"object": self
|
||
}
|
||
|
||
# Kaliteli player'a göre sırala
|
||
vids = sorted(vids, key = lambda x: SUPPORTED.index(x.player))
|
||
# Seçilmiş fansub'un videolarını öncelikli tut
|
||
if by_fansub:
|
||
vids = sorted(vids, key = lambda x: x.fansub != by_fansub)
|
||
|
||
# 1080p seçim hızlandırma: çözünürlükleri paralel önbellekle
|
||
# İlk etapta oyuncu önceliğine göre ilk N adayın çözünürlüklerini önceden hesapla
|
||
if by_res and vids:
|
||
try:
|
||
import concurrent.futures as _cf
|
||
n = max(1, int(early_subset or 1))
|
||
subset = vids[:n]
|
||
with _cf.ThreadPoolExecutor(max_workers=min(n, len(subset))) as _ex:
|
||
list(_ex.map(lambda v: getattr(v, 'resolution'), subset))
|
||
# Eğer 1080+ bulunan varsa doğrudan onu al
|
||
cands = [
|
||
v for v in subset
|
||
if (v.resolution or default_res) >= 1080 and v.is_working
|
||
]
|
||
if cands:
|
||
pick = sorted(cands, key=lambda v: SUPPORTED.index(v.player))[0]
|
||
callback({
|
||
"current": len(vids),
|
||
"total": len(vids),
|
||
"player": pick.player,
|
||
"status": "çalışıyor"
|
||
})
|
||
return pick
|
||
except Exception:
|
||
pass
|
||
|
||
for i,vid in enumerate(vids.copy(),start=1):
|
||
hook_dict = {**hook_dict, "current": i, "player": vid.player}
|
||
callback({**hook_dict, "status": "üstbilgi çekiliyor"})
|
||
if not vid.is_working:
|
||
callback({**hook_dict, "status": "çalışmıyor"})
|
||
vids.remove(vid)
|
||
continue
|
||
# Çözünürlük önemli değilse ya da max çözünürlük bulunduysa videoyu döndür.
|
||
if not by_res or (vid.resolution or default_res) >= 1080:
|
||
callback({**hook_dict, "current":len(vids), "status": "çalışıyor"})
|
||
return vid
|
||
if vids == []:
|
||
callback({**hook_dict, "player": None, "status": "hiçbiri çalışmıyor"})
|
||
return None
|
||
# 1080+ bulunamadıysa, en yüksek çözünürlüğü seç.
|
||
vid = max(vids, key = lambda x:x.resolution or default_res)
|
||
callback({**hook_dict, "player": vid.player , "status": "çalışıyor"})
|
||
return vid
|
||
|
||
|
||
class Video:
|
||
"""
|
||
Bir bölüm adına yüklenmiş herhangi bir videoyu temsil eden obje.
|
||
|
||
Öznitelikler:
|
||
- path: Video'nun türkanime'deki hash'li dizin yolu.
|
||
- bolum: Video'nun ait olduğu bölüm objesi.
|
||
- fansub: eğer belirtilmişse, videoyu yükleyen fansub'un ismi.
|
||
- url: Videonun decrypted gerçek url'si, örn: https://youtube.com/watch?v=XXXXXXXX
|
||
"""
|
||
def __init__(self,bolum,path,player=None,fansub=None,log_handler=LogHandler):
|
||
self.path = path
|
||
self.player = player
|
||
self.fansub = fansub
|
||
self.bolum = bolum
|
||
self._resolution = None
|
||
self._info = None
|
||
self._url = None
|
||
self._is_working = None
|
||
self.is_supported = self.player in SUPPORTED
|
||
|
||
self.ydl_opts = get_ydl_opts(log_handler)
|
||
if self.player == "ALUCARD(BETA)":
|
||
self.ydl_opts['impersonate'] = ImpersonateTarget("chrome")
|
||
|
||
@property
|
||
def url(self):
|
||
if self._url is None:
|
||
if "/" in self.path:
|
||
src = fetch(self.path)
|
||
cipher = re.findall(r"\/embed\/#\/url\/(.*?)\?status",src)[0]
|
||
else: # Zaten seçili video ise
|
||
cipher = self.path
|
||
plaintext = get_real_url(cipher)
|
||
# "\\/\\/fembed.com\\/v\\/0d1e8ilg" --> "https://fembed.com/v/0d1e8ilg"
|
||
self._url = "https:"+json.loads(plaintext)
|
||
self._url = self._url.replace("uqload.io","uqload.com") # .com mirror'unu kullan
|
||
if "turkanime" in self._url: # Alucard, Amaterasu, Bankai, HDVID
|
||
self._url = unmask_real_url(self._url)
|
||
self.is_working = False if "turkanime" in self._url else True
|
||
return self._url
|
||
|
||
@property
|
||
def info(self):
|
||
if self._info is None:
|
||
assert self.is_supported, "Bu player desteklenmiyor."
|
||
info = extract_video_info(self.url, self.ydl_opts)
|
||
if not info:
|
||
self._info = {}
|
||
return self._info
|
||
# nedense mpv direct=True ise oynatmıyor
|
||
if isinstance(info, dict) and "direct" in info:
|
||
del info["direct"]
|
||
# false-pozitifleri önlemek için.
|
||
if isinstance(info, dict) and info.get("video_ext") == "html":
|
||
info = None
|
||
self._info = info
|
||
return self._info
|
||
|
||
@property
|
||
def resolution(self):
|
||
""" Video çözünürlüğünü ara, bulunamadıysa None. """
|
||
if self._resolution is None:
|
||
info = self.info
|
||
if not isinstance(info, dict):
|
||
self._resolution = 0
|
||
return self._resolution
|
||
formats = info.get("formats")
|
||
res = info.get("resolution")
|
||
if res and isinstance(res, str) and re.search(r'\d{2,4}', res):
|
||
res = int(re.findall(r'\d{2,4}', res)[-1])
|
||
elif isinstance(formats, list) and formats:
|
||
first_format = formats[0]
|
||
if isinstance(first_format, dict) and "height" in first_format:
|
||
best_format = max(
|
||
formats,
|
||
key=lambda x: (x.get("height") or 0) if isinstance(x, dict) else 0
|
||
)
|
||
res = best_format.get("height") if isinstance(best_format, dict) else None
|
||
elif isinstance(first_format, dict) and "format_id" in first_format:
|
||
fid = first_format.get("format_id")
|
||
if isinstance(fid, str):
|
||
res = {"sd": 480, "hd": 720, "fhd": 1080, "hq": 2160}.get(fid)
|
||
else:
|
||
# Ek fallback: vcodec isimlerinden veya tbr'den yaklaşık çözünürlük tahmini
|
||
try:
|
||
t = max(
|
||
formats,
|
||
key=lambda x: (
|
||
(x.get("height") or 0) if isinstance(x, dict) else 0,
|
||
(x.get("tbr") or 0) if isinstance(x, dict) else 0
|
||
)
|
||
)
|
||
if isinstance(t, dict):
|
||
res = t.get("height") or (720 if (t.get("tbr") or 0) > 1500 else 480)
|
||
else:
|
||
res = None
|
||
except Exception:
|
||
res = None
|
||
self._resolution = res or 0
|
||
# Son çare: mpv ile çözünürlük oku
|
||
if not self._resolution:
|
||
self._resolution = get_video_resolution_mpv(self.url) or 0
|
||
return self._resolution
|
||
|
||
@property
|
||
def is_working(self):
|
||
""" Video çalışıyor mu? """
|
||
assert self.is_supported, "Bu player desteklenmiyor."
|
||
if self._is_working is None:
|
||
try:
|
||
if "turkanime" in self.url:
|
||
raise LookupError
|
||
self._is_working = self.info not in (None, {})
|
||
except:
|
||
self._is_working = False
|
||
return self._is_working
|
||
|
||
@is_working.setter
|
||
def is_working(self,value):
|
||
self._is_working = value
|
||
|
||
def indir(self, callback=None, output=""):
|
||
""" info.json'u kullanarak videoyu indir """
|
||
assert self.is_working, "Video çalışmıyor."
|
||
seri_slug = self.bolum.anime.slug if self.bolum.anime else ""
|
||
output = join(output, seri_slug, self.bolum.slug)
|
||
opts = self.ydl_opts.copy()
|
||
if callback:
|
||
opts['progress_hooks'] = [callback]
|
||
opts['outtmpl'] = {'default': output + r'.%(ext)s'}
|
||
with NamedTemporaryFile("w",delete=False) as tmp:
|
||
json.dump(self.info, tmp)
|
||
with YoutubeDL(opts) as ydl:
|
||
ydl.download_with_info_file(tmp.name)
|
||
remove(tmp.name)
|
||
|
||
def oynat(self, dakika_hatirla=False ,izlerken_kaydet=False, mpv_opts=[]):
|
||
""" Oynatmak için yt-dlp + mpv kullanıyoruz. """
|
||
assert self.is_working, "Video çalışmıyor."
|
||
|
||
# Platform ve mimari bilgisini al
|
||
platform_info = get_platform()
|
||
arch = get_arch()
|
||
|
||
# ARM mimarileri için Android MPV kullan
|
||
if arch in ["armv7l", "arm64"] or "aarch64" in platform_info:
|
||
# Android MPV komutu
|
||
cmd = [
|
||
"nohup", "am", "start", "--user", "0",
|
||
"-a", "android.intent.action.VIEW",
|
||
"-d", self.url,
|
||
"-n", "is.xyz.mpv/.MPVActivity"
|
||
]
|
||
# Android için stdout/stderr'ı /dev/null'a yönlendir
|
||
return sp.run(cmd, stdout=sp.DEVNULL, stderr=sp.DEVNULL)
|
||
|
||
# Standart masaüstü MPV komutu
|
||
with NamedTemporaryFile("w",delete=False) as tmp:
|
||
json.dump(self.info, tmp)
|
||
cmd = [
|
||
"mpv",
|
||
"--no-input-terminal",
|
||
"--msg-level=all=error",
|
||
"--script-opts=ytdl_hook-ytdl_path=yt-dlp,ytdl_hook-try_ytdl_first=yes",
|
||
"--ytdl-raw-options=load-info-json=" + tmp.name,
|
||
"ytdl://" + self.bolum.slug # Kaldığın yerden devam etmenin çalışması için.
|
||
]
|
||
|
||
if self.player == "ALUCARD(BETA)":
|
||
cmd += [
|
||
"--demuxer-lavf-o=protocol_whitelist=[file,tcp,tls,https],"
|
||
"http_keep_alive=0,http_persistent=0"
|
||
]
|
||
cmd += ["--cache=yes", get_alucard_m3u8(self.url) ]
|
||
del cmd[4]
|
||
|
||
if dakika_hatirla:
|
||
mpv_opts.append("--save-position-on-quit")
|
||
if izlerken_kaydet:
|
||
mpv_opts.append("--stream-record")
|
||
for opt in mpv_opts:
|
||
cmd.insert(1,opt)
|
||
return sp.run(cmd, text=True, stdout=sp.PIPE, stderr=sp.PIPE)
|
||
|
||
def get(self, key, default=None):
|
||
"""Dictionary-like get method for compatibility."""
|
||
if key == 'url':
|
||
return self.url
|
||
elif key == 'label':
|
||
return getattr(self, 'fansub', None) or self.player
|
||
elif key == 'player':
|
||
return self.player
|
||
return default
|