Files
google-labs-jules[bot]andbarkeser2002 706ea67072 perf: pre-compile regex patterns in Anime.fetch_info
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>
2026-04-19 21:09:22 +00:00

515 lines
20 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.
""" Ö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