KekikStream 2.3.6__py3-none-any.whl → 2.3.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,11 +31,16 @@ class PluginBase(ABC):
31
31
  if proxy:
32
32
  self.cloudscraper.proxies = proxy if isinstance(proxy, dict) else {"http": proxy, "https": proxy}
33
33
 
34
+ # Convert dict proxy to string for httpx if necessary
35
+ httpx_proxy = proxy
36
+ if isinstance(proxy, dict):
37
+ httpx_proxy = proxy.get("https") or proxy.get("http")
38
+
34
39
  # httpx - lightweight and safe for most HTTP requests
35
40
  self.httpx = AsyncClient(
36
41
  timeout = 3,
37
42
  follow_redirects = True,
38
- proxy = proxy
43
+ proxy = httpx_proxy
39
44
  )
40
45
  self.httpx.headers.update(self.cloudscraper.headers)
41
46
  self.httpx.cookies.update(self.cloudscraper.cookies)
@@ -9,6 +9,7 @@ class MolyStream(ExtractorBase):
9
9
 
10
10
  # Birden fazla domain destekle
11
11
  supported_domains = [
12
+ "dbx.molystream.org",
12
13
  "ydx.molystream.org",
13
14
  "yd.sheila.stream",
14
15
  "ydf.popcornvakti.net",
@@ -18,13 +19,54 @@ class MolyStream(ExtractorBase):
18
19
  return any(domain in url for domain in self.supported_domains)
19
20
 
20
21
  async def extract(self, url, referer=None) -> ExtractResult:
21
- if "doctype html" in url:
22
- secici = HTMLHelper(url)
23
- video = secici.select_attr("video#sheplayer source", "src")
22
+ if "doctype html" in url.lower():
23
+ text = url
24
24
  else:
25
- video = url
25
+ # Sheila-style referer fix
26
+ if "/embed/sheila/" in url:
27
+ referer = url.replace("/embed/sheila/", "/embed/")
26
28
 
27
- resp_sec = HTMLHelper(url)
29
+ self.httpx.headers.update({
30
+ "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
31
+ "Referer" : referer or self.main_url
32
+ })
33
+ istek = await self.httpx.get(url, follow_redirects=True)
34
+ text = istek.text
35
+
36
+ # 1. Sheila-style links often have the m3u8 directly as the first http line or in script
37
+ m3u8 = None
38
+ if "#EXTM3U" in text:
39
+ for line in text.splitlines():
40
+ line = line.strip().replace('"', '').replace("'", "")
41
+ if line.startswith("http"):
42
+ m3u8 = line
43
+ break
44
+
45
+ if not m3u8:
46
+ for line in text.splitlines():
47
+ line = line.strip().replace('"', '').replace("'", "")
48
+ if line.startswith("http") and ".m3u8" in line:
49
+ m3u8 = line
50
+ break
51
+
52
+ if not m3u8:
53
+ secici = HTMLHelper(text)
54
+ # 2. Try video tag
55
+ m3u8 = secici.select_attr("video#sheplayer source", "src") or secici.select_attr("video source", "src")
56
+
57
+ if not m3u8:
58
+ # 3. Try regex
59
+ m3u8 = HTMLHelper(text).regex_first(r'["\'](https?://[^"\']+\.m3u8[^"\']*)["\']')
60
+
61
+ if not m3u8:
62
+ # Fallback to any http link in a script if it looks like a video link
63
+ m3u8 = HTMLHelper(text).regex_first(r'["\'](https?://[^"\']+/q/\d+)["\']')
64
+
65
+ if not m3u8:
66
+ m3u8 = url # Final fallback
67
+
68
+ # Subtitles (Sheila style addSrtFile)
69
+ resp_sec = HTMLHelper(text)
28
70
  matches = resp_sec.regex_all(r"addSrtFile\(['\"]([^'\"]+\.srt)['\"]\s*,\s*['\"][a-z]{2}['\"]\s*,\s*['\"]([^'\"]+)['\"]")
29
71
 
30
72
  subtitles = [
@@ -34,8 +76,8 @@ class MolyStream(ExtractorBase):
34
76
 
35
77
  return ExtractResult(
36
78
  name = self.name,
37
- url = video,
38
- referer = video.replace("/sheila", "") if video else None,
39
- user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0",
79
+ url = m3u8,
80
+ referer = url,
81
+ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
40
82
  subtitles = subtitles
41
83
  )
@@ -24,6 +24,8 @@ class VidMoly(ExtractorBase):
24
24
 
25
25
  if ".me" in url:
26
26
  url = url.replace(".me", ".net")
27
+ if ".to" in url:
28
+ url = url.replace(".to", ".net")
27
29
 
28
30
  # VidMoly bazen redirect ediyor, takip et
29
31
  response = await self.httpx.get(url, follow_redirects=True)
@@ -70,6 +72,17 @@ class VidMoly(ExtractorBase):
70
72
  if sub.get("kind") == "captions"
71
73
  ]
72
74
 
75
+ if "#EXTM3U" in response.text:
76
+ for line in response.text.splitlines():
77
+ line = line.strip().replace('"', '').replace("'", "")
78
+ if line.startswith("http"):
79
+ return ExtractResult(
80
+ name = self.name,
81
+ url = line,
82
+ referer = self.main_url,
83
+ subtitles = subtitles
84
+ )
85
+
73
86
  if script_str := resp_sec.regex_first(r"sources:\s*\[(.*?)\],", flags= re.DOTALL):
74
87
  script_content = script_str
75
88
  # Video kaynaklarını ayrıştır
@@ -167,34 +167,44 @@ class DiziBox(PluginBase):
167
167
 
168
168
  crypt_data = iframe_secici.regex_first(r"CryptoJS\.AES\.decrypt\(\"(.*)\",\"", iframe_istek.text)
169
169
  crypt_pass = iframe_secici.regex_first(r"\",\"(.*)\"\);", iframe_istek.text)
170
- decode = CryptoJS.decrypt(crypt_pass, crypt_data)
171
-
172
- if video_match := iframe_secici.regex_first(r"file: '(.*)',", decode):
173
- results.append(video_match)
174
- else:
175
- results.append(decode)
170
+ if crypt_data and crypt_pass:
171
+ decode = CryptoJS.decrypt(crypt_pass, crypt_data)
172
+ if video_match := iframe_secici.regex_first(r"file: '(.*)',", decode):
173
+ results.append(video_match)
174
+ else:
175
+ results.append(decode)
176
176
 
177
177
  elif "/player/moly/moly.php" in iframe_link:
178
178
  iframe_link = iframe_link.replace("moly.php?h=", "moly.php?wmode=opaque&h=")
179
- while True:
179
+ for _ in range(3): # Max 3 attempts
180
180
  await asyncio.sleep(.3)
181
181
  with contextlib.suppress(Exception):
182
182
  moly_istek = await self.httpx.get(iframe_link)
183
183
  moly_secici = HTMLHelper(moly_istek.text)
184
184
 
185
- if atob_data := moly_secici.regex_first(r"unescape\(\"(.*)\"\)", moly_istek.text):
185
+ if atob_data := moly_secici.regex_first(r"unescape\(\"(.*)\"\)"):
186
186
  decoded_atob = urllib.parse.unquote(atob_data)
187
187
  str_atob = base64.b64decode(decoded_atob).decode("utf-8")
188
188
 
189
- iframe_src = HTMLHelper(str_atob).select_attr("div#Player iframe", "src")
190
- if iframe_src:
191
- results.append(iframe_src)
192
-
193
- break
189
+ iframe_src = HTMLHelper(str_atob).select_attr("div#Player iframe", "src")
190
+ if iframe_src:
191
+ # ! Sheila replacement (Kotlin referansı)
192
+ if "/embed/" in iframe_src:
193
+ iframe_src = iframe_src.replace("/embed/", "/embed/sheila/").replace("vidmoly.me", "vidmoly.net")
194
+
195
+ results.append(iframe_src)
196
+ break
197
+ elif embed_matches := moly_secici.regex_all(r'iframe.*?src="(.*?)"'):
198
+ for src in embed_matches:
199
+ if "/embed/" in src:
200
+ src = src.replace("/embed/", "/embed/sheila/").replace("vidmoly.me", "vidmoly.net")
201
+ results.append(src)
202
+ break
194
203
 
195
204
  elif "/player/haydi.php" in iframe_link:
196
- okru_url = base64.b64decode(iframe_link.split("?v=")[-1]).decode("utf-8")
197
- results.append(okru_url)
205
+ with contextlib.suppress(Exception):
206
+ okru_url = base64.b64decode(iframe_link.split("?v=")[-1]).decode("utf-8")
207
+ results.append(okru_url)
198
208
 
199
209
  return results
200
210
 
@@ -127,18 +127,27 @@ class DiziPal(PluginBase):
127
127
 
128
128
  poster = self.fix_url(secici.select_attr("meta[property='og:image']", "content")) if secici.select_attr("meta[property='og:image']", "content") else None
129
129
 
130
- # XPath yerine regex ile HTML'den çıkarma
131
- year = secici.regex_first(r'(?is)Yapım Yılı.*?<div[^>]*>(\d{4})</div>', secici.html)
132
-
133
- description = secici.select_text("div.summary p")
130
+ # Sidebar bilgilerini topla
131
+ info = {}
132
+ for li in secici.select("li"):
133
+ key = secici.select_text("div.key", li)
134
+ val = secici.select_text("div.value", li)
135
+ if key and val:
136
+ info[key.strip(":")] = val.strip()
137
+
138
+ year = info.get("Yapım Yılı")
139
+ rating = info.get("IMDB Puanı")
140
+
141
+ tags_raw = info.get("Türler", "")
142
+ tags = [t.strip() for t in tags_raw.split() if t.strip()] if tags_raw else None
134
143
 
135
- rating = secici.regex_first(r'(?is)IMDB Puanı.*?<div[^>]*>([0-9.]+)</div>', secici.html)
144
+ actors_raw = info.get("Oyuncular")
145
+ actors = [a.strip() for a in actors_raw.split(",") if a.strip()] if actors_raw else None
136
146
 
137
- tags_raw = secici.regex_first(r'(?is)Türler.*?<div[^>]*>([^<]+)</div>', secici.html)
138
- tags = [t.strip() for t in tags_raw.split() if t.strip()] if tags_raw else None
147
+ description = secici.select_text("div.summary p")
139
148
 
140
- duration_raw = secici.regex_first(r'(?is)Ortalama Süre.*?<div[^>]*>(\d+)', secici.html)
141
- duration = int(duration_raw) if duration_raw else None
149
+ duration_raw = info.get("Ortalama Süre")
150
+ duration = int(secici.regex_first(r"(\d+)", duration_raw)) if duration_raw else None
142
151
 
143
152
  if "/dizi/" in url:
144
153
  title = secici.select_text("div.cover h5")
@@ -177,6 +186,7 @@ class DiziPal(PluginBase):
177
186
  year = year,
178
187
  duration = duration,
179
188
  episodes = episodes if episodes else None,
189
+ actors = actors,
180
190
  )
181
191
  else:
182
192
  # Film için title - g-title div'lerinin 2. olanı
@@ -192,6 +202,7 @@ class DiziPal(PluginBase):
192
202
  rating = rating,
193
203
  year = year,
194
204
  duration = duration,
205
+ actors = actors,
195
206
  )
196
207
 
197
208
  async def load_links(self, url: str) -> list[ExtractResult]:
@@ -27,10 +27,10 @@ class DiziWatch(PluginBase):
27
27
  "1" : "Suç",
28
28
  }
29
29
 
30
+ c_key = ""
31
+ c_value = ""
32
+
30
33
  async def _init_session(self):
31
- if getattr(self, "c_key", None) and getattr(self, "c_value", None):
32
- return
33
-
34
34
  # Fetch anime-arsivi to get CSRF tokens
35
35
  resp = await self.httpx.get(f"{self.main_url}/anime-arsivi")
36
36
  sel = HTMLHelper(resp.text)
@@ -137,9 +137,9 @@ class DiziWatch(PluginBase):
137
137
  resp = await self.httpx.get(url)
138
138
  sel = HTMLHelper(resp.text)
139
139
 
140
- title = sel.select_text("h2")
141
- poster = sel.select_attr("img.rounded-md", "src")
142
- description = sel.select_text("div.text-sm")
140
+ title = sel.select_text("h2") or sel.select_text("h1")
141
+ poster = sel.select_attr("img.rounded-md", "src") or sel.select_attr("meta[property='og:image']", "content")
142
+ description = sel.select_text("div.text-sm") or sel.select_text("div.summary")
143
143
 
144
144
  year = sel.regex_first(r"Yap\u0131m Y\u0131l\u0131\s*:\s*(\d+)", resp.text)
145
145
 
@@ -72,7 +72,7 @@ class DiziYou(PluginBase):
72
72
  html_text = istek.text
73
73
 
74
74
  # Title - div.title h1 içinde
75
- title = secici.select_text("div.title h1")
75
+ title = (secici.select_text("div.title h1") or "").strip()
76
76
 
77
77
  # Fallback: Eğer title boşsa URL'den çıkar (telif kısıtlaması olan sayfalar için)
78
78
  if not title:
@@ -81,23 +81,19 @@ class DiziYou(PluginBase):
81
81
  title = slug.replace('-', ' ').title()
82
82
 
83
83
  # Poster
84
- poster_src = secici.select_attr("div.category_image img", "src")
84
+ poster_src = secici.select_attr("div.category_image img", "src") or secici.select_attr("meta[property='og:image']", "content")
85
85
  poster = self.fix_url(poster_src) if poster_src else ""
86
86
 
87
87
  # Year - regex ile çıkarma (xpath yerine)
88
88
  year = secici.regex_first(r"(?is)Yapım Yılı.*?(\d{4})", secici.html)
89
89
 
90
- description = None
91
- # Extract inner HTML via regex and clean
92
- desc_html = secici.regex_first(r'(?s)<div class="diziyou_desc">(.*?)</div>', secici.html)
93
- if desc_html:
94
- # Script taglarını kaldır
95
- desc_html = HTMLHelper(desc_html).regex_replace(r"(?s)<script.*?</script>", "")
96
- # div#icerikcat2 ve sonrasını kaldır (meta bilgileri içeriyor)
97
- desc_html = HTMLHelper(desc_html).regex_replace(r"(?s)<div id=\"icerikcat2\".*", "")
98
- # Kalan HTML'den text çıkar
99
- clean_sel = HTMLHelper(desc_html)
100
- description = clean_sel.select_text()
90
+ description_el = secici.select("div.diziyou_desc") or secici.select("div#icerikcat")
91
+ description = ""
92
+ if description_el:
93
+ # Scriptleri temizle
94
+ for script in secici.select("script", description_el[0]):
95
+ script.decompose()
96
+ description = secici.select_text(None, description_el[0])
101
97
 
102
98
  tags = [secici.select_text(None, a) for a in secici.select("div.genres a") if secici.select_text(None, a)]
103
99
 
@@ -109,9 +105,9 @@ class DiziYou(PluginBase):
109
105
  actors = [actor.strip() for actor in actors_raw.split(",") if actor.strip()] if actors_raw else []
110
106
 
111
107
  episodes = []
112
- # Episodes - daha fazla DOM/URL kalıbını destekle
113
- for link in secici.select("a"):
114
- ep_href = secici.select_attr("a", "href", link)
108
+ # Episodes - div#scrollbar-container a (kısıtlı alan)
109
+ for link in secici.select("div#scrollbar-container a"):
110
+ ep_href = secici.select_attr(None, "href", link)
115
111
  if not ep_href:
116
112
  continue
117
113
 
@@ -179,9 +175,9 @@ class DiziYou(PluginBase):
179
175
  # Player src'den item_id çıkar - önce özel player seçicisini dene
180
176
  player_src = None
181
177
  # Yaygın locatorlar
182
- for sel in ["iframe#diziyouPlayer", "div.player iframe", "iframe[src*='/episodes/']", "iframe"]:
178
+ for sel in ["iframe#diziyouPlayer", "div.player iframe", "iframe[src*='/player/']", "iframe[src*='/episodes/']", "iframe"]:
183
179
  p = secici.select_attr(sel, "src")
184
- if p and "youtube.com" not in p.lower():
180
+ if p and any(x in p.lower() for x in ["/player/", "/episodes/", "diziyou"]):
185
181
  player_src = p
186
182
  break
187
183
 
@@ -137,70 +137,60 @@ class Dizilla(PluginBase):
137
137
  istek = await self.httpx.get(url)
138
138
  secici = HTMLHelper(istek.text)
139
139
 
140
- title = secici.select_text("div.poster.poster h2")
141
- if not title:
140
+ next_data_text = secici.select_text("script#__NEXT_DATA__")
141
+ if not next_data_text:
142
142
  return None
143
143
 
144
- poster = secici.select_attr("div.w-full.page-top.relative img", "src")
145
- poster = self.fix_url(poster) if poster else None
146
-
147
- # Year extraction (Kotlin: [1] index for w-fit min-w-fit)
148
- info_boxes = secici.select("div.w-fit.min-w-fit")
149
- year = None
150
- if len(info_boxes) > 1:
151
- year_text = secici.select_text("span.text-sm.opacity-60", info_boxes[1])
152
- if year_text:
153
- year = year_text.split(" ")[-1] if " " in year_text else year_text
154
-
155
- description = secici.select_text("div.mt-2.text-sm")
156
-
157
- tags_text = secici.select_text("div.poster.poster h3")
158
- tags = [t.strip() for t in tags_text.split(",")] if tags_text else []
159
-
160
- actors = secici.select_all_text("div.global-box h5")
161
-
162
- episodeses = []
163
- # Seasons links iteration
164
- season_links = secici.select("div.flex.items-center.flex-wrap.gap-2.mb-4 a")
165
- for sezon in season_links:
166
- sezon_href = secici.select_attr("a", "href", sezon)
167
- sezon_href = self.fix_url(sezon_href)
168
- sezon_req = await self.httpx.get(sezon_href)
169
-
170
- season_num = None
171
- try:
172
- # URL'den sezon numarasını çek: ...-N-sezon formatı
173
- season_match = secici.regex_first(r"-(\d+)-sezon", sezon_href)
174
- if season_match:
175
- season_num = int(season_match)
176
- except:
177
- pass
178
-
179
- sezon_secici = HTMLHelper(sezon_req.text)
180
- for bolum in sezon_secici.select("div.episodes div.cursor-pointer"):
181
- # Kotlin: bolum.select("a").last()
182
- links = sezon_secici.select("a", bolum)
183
- if not links:
184
- continue
185
-
186
- ep_link = links[-1]
187
- ep_name = sezon_secici.select_text("a", ep_link)
188
- ep_href = sezon_secici.select_attr("a", "href", ep_link)
189
- ep_href = self.fix_url(ep_href)
144
+ next_data = loads(next_data_text)
145
+ secure_data = next_data.get("props", {}).get("pageProps", {}).get("secureData")
146
+ if not secure_data:
147
+ return None
148
+
149
+ decrypted = await self.decrypt_response(secure_data)
150
+ content = decrypted.get("contentItem", {})
151
+ if not content:
152
+ return None
153
+
154
+ title = content.get("original_title") or content.get("used_title")
155
+ description = content.get("description") or content.get("used_description")
156
+ rating = content.get("imdb_point") or content.get("local_vote_avg")
157
+ year = content.get("release_year")
158
+
159
+ # Poster and Backdrop - prefer backdrop if available for SeriesInfo
160
+ poster = self.fix_poster_url(self.fix_url(content.get("back_url") or content.get("poster_url")))
161
+
162
+ # Tags
163
+ tags = []
164
+ categories = decrypted.get("RelatedResults", {}).get("getSerieCategoriesById", {}).get("result", [])
165
+ for cat in categories:
166
+ tags.append(cat.get("name"))
167
+
168
+ # Actors
169
+ actors = []
170
+ casts = decrypted.get("RelatedResults", {}).get("getSerieCastsById", {}).get("result", [])
171
+ for cast in casts:
172
+ actors.append(cast.get("name"))
173
+
174
+ # Episodes
175
+ episodes = []
176
+ seasons_data = decrypted.get("RelatedResults", {}).get("getSerieSeasonAndEpisodes", {}).get("result", [])
177
+ for season_item in seasons_data:
178
+ season_num = season_item.get("season_no")
179
+ for ep_item in season_item.get("episodes", []):
180
+ ep_num = ep_item.get("episode_no")
181
+ ep_slug = ep_item.get("used_slug")
182
+ ep_name = ep_item.get("episode_text") or ""
190
183
 
191
- # Episode number (first link's text usually)
192
- ep_num = None
193
- try:
194
- ep_num_text = sezon_secici.select_text("a", links[0])
195
- ep_num = int(ep_num_text) if ep_num_text else None
196
- except:
197
- pass
198
-
199
- episodeses.append(Episode(
184
+ # Filter out duplicate language entries if any (we just need one link per episode)
185
+ # Usually they share the same slug for the episode page
186
+ if any(e.season == season_num and e.episode == ep_num for e in episodes):
187
+ continue
188
+
189
+ episodes.append(Episode(
200
190
  season = season_num,
201
191
  episode = ep_num,
202
192
  title = ep_name,
203
- url = ep_href
193
+ url = self.fix_url(f"{self.main_url}/{ep_slug}")
204
194
  ))
205
195
 
206
196
  return SeriesInfo(
@@ -209,8 +199,9 @@ class Dizilla(PluginBase):
209
199
  title = title,
210
200
  description = description,
211
201
  tags = tags,
202
+ rating = str(rating) if rating else None,
212
203
  year = str(year) if year else None,
213
- episodes = episodeses,
204
+ episodes = episodes,
214
205
  actors = actors
215
206
  )
216
207
 
@@ -108,18 +108,22 @@ class FilmBip(PluginBase):
108
108
  tags = secici.select_all_text("div.series-profile-type.tv-show-profile-type a")
109
109
 
110
110
  # XPath yerine regex kullanarak yıl, süre vs. çıkarma
111
- year = secici.regex_first(r'(?i)Yapım yılı.*?<p[^>]*>(\d{4})</p>', secici.html)
111
+ year = secici.regex_first(r"(?is)Yap\u0131m y\u0131l\u0131.*?<p[^>]*>(.*?)<\/p>")
112
+ if not year:
113
+ # Fallback: Başlığın sonundaki parantezli yılı yakala
114
+ year = secici.regex_first(r"\((\d{4})\)", title)
112
115
 
113
- duration = secici.regex_first(r'(?i)Süre.*?<p[^>]*>(\d+)', secici.html)
116
+ duration_raw = secici.regex_first(r"(?is)S\u00fcre.*?<p[^>]*>(.*?)<\/p>")
117
+ duration = secici.regex_first(r"(\d+)", duration_raw) if duration_raw else None
114
118
 
115
- rating = secici.regex_first(r'(?i)IMDB Puanı.*?<span[^>]*>([0-9.]+)</span>', secici.html)
119
+ rating = secici.regex_first(r"(?is)IMDB Puan\u0131.*?<span[^>]*>(.*?)<\/span>")
116
120
 
117
121
  actors = [img.attrs.get("alt") for img in secici.select("div.series-profile-cast ul li a img") if img.attrs.get("alt")]
118
122
 
119
123
  return MovieInfo(
120
124
  url = url,
121
125
  poster = self.fix_url(poster) if poster else None,
122
- title = self.clean_title(title) if title else "",
126
+ title = HTMLHelper(title).regex_replace(r"\(\d{4}\)", "").strip() if title else "",
123
127
  description = description,
124
128
  tags = tags,
125
129
  year = year,
@@ -91,7 +91,7 @@ class FilmMakinesi(PluginBase):
91
91
  year = secici.select_text("span.date a") or ""
92
92
 
93
93
  actors = secici.select_all_text("div.cast-name")
94
- tags = secici.select_all_text("div.genre a")
94
+ tags = [a.text(strip=True) for a in secici.select("div.type a") if "/tur/" in (a.attrs.get("href") or "")]
95
95
 
96
96
  duration = None
97
97
  duration_text = secici.select_text("div.time") or None
@@ -93,19 +93,13 @@ class FullHDFilmizlesene(PluginBase):
93
93
 
94
94
  tags = secici.select_all_text("a[rel='category tag']")
95
95
 
96
- # Rating: normalize-space yerine doğrudan class ile ve son kelimeyi al
97
- rating_text = secici.select_text("div.puanx-puan") or None
98
- rating = None
99
- if rating_text:
100
- parts = rating_text.split()
101
- rating = parts[-1] if parts else None
96
+ # Rating: regex ile sayısal değeri yakala
97
+ rating_text = secici.select_text("div.puanx-puan") or ""
98
+ rating = secici.regex_first(r"(\d+\.\d+|\d+)", rating_text)
102
99
 
103
100
  # Year: ilk yıl formatında değer
104
- year_text = secici.select_text("div.dd a.category") or None
105
- year = None
106
- if year_text:
107
- parts = year_text.split()
108
- year = parts[0] if parts else None
101
+ year_text = secici.select_text("div.dd a.category") or ""
102
+ year = secici.regex_first(r"(\d{4})", year_text)
109
103
 
110
104
  # Actors: nth-child yerine tüm li'leri alıp 2. index
111
105
  lis = secici.select("div.film-info ul li")
@@ -113,11 +107,8 @@ class FullHDFilmizlesene(PluginBase):
113
107
  if len(lis) >= 2:
114
108
  actors = secici.select_all_text("a > span", lis[1])
115
109
 
116
- duration = "0"
117
- duration_text = secici.select_text("span.sure") or None
118
- if duration_text:
119
- duration_parts = duration_text.split()
120
- duration = duration_parts[0] if duration_parts else "0"
110
+ # Duration: regex ile yakala (örn: 201 dk)
111
+ duration = secici.regex_first(r"(\d+)\s*(?:dk|dakika)", html_text)
121
112
 
122
113
  return MovieInfo(
123
114
  url = url,
@@ -113,6 +113,10 @@ class JetFilmizle(PluginBase):
113
113
  year = secici.extract_year("div.yap")
114
114
 
115
115
  actors = secici.select_all_text("div[itemprop='actor'] a span")
116
+ if not actors: # Fallback to img alt
117
+ actors = [img.attrs.get("alt") for img in secici.select("div.oyuncular div.oyuncu img") if img.attrs.get("alt")]
118
+
119
+ duration = secici.regex_first(r"(\d+)\s*dk", istek.text)
116
120
 
117
121
  return MovieInfo(
118
122
  url = url,
@@ -122,7 +126,8 @@ class JetFilmizle(PluginBase):
122
126
  tags = tags,
123
127
  rating = rating,
124
128
  year = year,
125
- actors = actors
129
+ actors = actors,
130
+ duration = int(duration) if duration else None
126
131
  )
127
132
 
128
133
  async def load_links(self, url: str) -> list[ExtractResult]:
@@ -91,7 +91,8 @@ class KultFilmler(PluginBase):
91
91
  time_text = secici.select_text("li.time span")
92
92
  duration = secici.regex_first(r"(\d+)", time_text) if time_text else None
93
93
 
94
- rating = secici.select_text("div.imdb-count")
94
+ rating_text = secici.select_text("div.imdb-count")
95
+ rating = secici.regex_first(r"(\d+\.\d+|\d+)", rating_text) if rating_text else None
95
96
 
96
97
  actors = [a.text(strip=True) for a in secici.select("div.actors a") if a.text(strip=True)]
97
98
 
@@ -96,27 +96,49 @@ class RecTV(PluginBase):
96
96
 
97
97
  episodes.append(ep_model)
98
98
 
99
+ # Süreyi dakikaya çevir (Örn: "1h 59min")
100
+ duration_raw = veri.get("duration")
101
+ duration = None
102
+ if duration_raw:
103
+ try:
104
+ h = int(HTMLHelper(duration_raw).regex_first(r"(\d+)h") or 0)
105
+ m = int(HTMLHelper(duration_raw).regex_first(r"(\d+)min") or 0)
106
+ duration = h * 60 + m
107
+ except: pass
108
+
99
109
  return SeriesInfo(
100
110
  url = url,
101
111
  poster = self.fix_url(veri.get("image")),
102
112
  title = veri.get("title"),
103
113
  description = veri.get("description"),
104
114
  tags = [genre.get("title") for genre in veri.get("genres")] if veri.get("genres") else [],
105
- rating = veri.get("imdb") or veri.get("rating"),
106
- year = veri.get("year"),
115
+ rating = str(veri.get("imdb") or veri.get("rating") or ""),
116
+ year = str(veri.get("year") or ""),
107
117
  actors = [],
118
+ duration = duration,
108
119
  episodes = episodes
109
120
  )
110
121
  case _:
122
+ # Süreyi dakikaya çevir
123
+ duration_raw = veri.get("duration")
124
+ duration = None
125
+ if duration_raw:
126
+ try:
127
+ h = int(HTMLHelper(duration_raw).regex_first(r"(\d+)h") or 0)
128
+ m = int(HTMLHelper(duration_raw).regex_first(r"(\d+)min") or 0)
129
+ duration = h * 60 + m
130
+ except: pass
131
+
111
132
  return MovieInfo(
112
133
  url = url,
113
134
  poster = self.fix_url(veri.get("image")),
114
135
  title = veri.get("title"),
115
136
  description = veri.get("description"),
116
137
  tags = [genre.get("title") for genre in veri.get("genres")] if veri.get("genres") else [],
117
- rating = veri.get("imdb") or veri.get("rating"),
118
- year = veri.get("year"),
119
- actors = []
138
+ rating = str(veri.get("imdb") or veri.get("rating") or ""),
139
+ year = str(veri.get("year") or ""),
140
+ actors = [],
141
+ duration = duration
120
142
  )
121
143
 
122
144
  async def load_links(self, url: str) -> list[ExtractResult]: