diff --git a/IPTVPlayer/hosts/hostalooytv.py b/IPTVPlayer/hosts/hostalooytv.py new file mode 100644 index 00000000..af814969 --- /dev/null +++ b/IPTVPlayer/hosts/hostalooytv.py @@ -0,0 +1,872 @@ +# -*- coding: utf-8 -*- +# Last modified: 14/5/2026 +# AlooyTV Host (Created By Dr HYTHAM MAHMOUD) + + +from Components.config import ConfigSelection, ConfigText, config, getConfigListEntry +from Plugins.Extensions.IPTVPlayer.components.ihost import CBaseHostClass, CHostBase +from Plugins.Extensions.IPTVPlayer.components.iptvplayerinit import TranslateTXT as _ +from Plugins.Extensions.IPTVPlayer.tools.iptvtools import printDBG, printExc, MergeDicts +from Plugins.Extensions.IPTVPlayer.tools.iptvtypes import strwithmeta +import re + +try: + import json +except Exception: + json = None + +try: + import base64 +except Exception: + base64 = None + +try: + from urllib.parse import quote_plus as urllib_quote_plus +except ImportError: + from urllib import quote_plus as urllib_quote_plus + +try: + from html import unescape as html_unescape +except Exception: + try: + from HTMLParser import HTMLParser + + html_unescape = HTMLParser().unescape + except Exception: + + def html_unescape(txt): + return txt + + +config.plugins.iptvplayer.cimalina_proxy = ConfigSelection(default="None", choices=[("None", _("None")), ("proxy_1", _("Alternative proxy server (1)")), ("proxy_2", _("Alternative proxy server (2)"))]) +config.plugins.iptvplayer.cimalina_alt_domain = ConfigText(default="", fixed_size=False) + + +def GetConfigList(): + optionList = [] + optionList.append(getConfigListEntry(_("Use proxy server:"), config.plugins.iptvplayer.cimalina_proxy)) + if config.plugins.iptvplayer.cimalina_proxy.value == "None": + optionList.append(getConfigListEntry(_("Alternative domain:"), config.plugins.iptvplayer.cimalina_alt_domain)) + return optionList + + +def gettytul(): + return "AlooyTV" + + +class AlooyTV(CBaseHostClass): + + # مسارات تدل على أن الرابط فيديو حتى لو امتداده صورة + VIDEO_PATH_HINTS = [ + "/file/wp-", + "/file/wp-alooytv", + "/uploads/20", + "/stream/", + "/hls/", + "/video/", + "/media/", + "/cdn/", + "/content/", + "/episode/", + ] + + # مسارات تدل على أن الرابط صورة thumbnail حقيقية + THUMB_PATH_HINTS = [ + "/uploads/video_thumb/", + "/thumbs/", + "/thumbnail/", + "/thumbnails/", + "/poster/", + "/posters/", + "/cover/", + "/covers/", + "/img/", + "/images/", + "/icons/", + ] + + def __init__(self): + CBaseHostClass.__init__(self, {"history": "alooytv", "cookie": "alooytv.cookie"}) + + self.MAIN_URL = None + self.DEFAULT_ICON_URL = "https://ci.alooytv12.xyz/uploads/system_logo/logo.png" + + self.HEADER = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "Accept-Language": "ar,en-US;q=0.9,en;q=0.8", "DNT": "1"} + + # headers مطابقة لما يرسله Chrome عند تشغيل فيديو + self.VIDEO_HEADER = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + "Accept": "*/*", + "Accept-Language": "ar,en-US;q=0.9,en;q=0.8", + "Accept-Encoding": "identity;q=1, *;q=0", + "Range": "bytes=0-", + "Sec-Fetch-Dest": "video", + "Sec-Fetch-Mode": "no-cors", + "Sec-Fetch-Site": "cross-site", + "Sec-Fetch-Storage-Access": "active", + "Sec-Ch-Ua": '"Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147"', + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": '"Windows"', + "Connection": "keep-alive", + } + + self.AJAX_HEADER = dict(self.HEADER) + self.AJAX_HEADER.update({"X-Requested-With": "XMLHttpRequest", "Referer": ""}) + + self.defaultParams = {"header": self.HEADER, "use_cookie": True, "load_cookie": True, "save_cookie": True, "cookiefile": self.COOKIE_FILE, "return_data": True} + + self.cacheLinks = {} + + def getProxy(self): + proxy = config.plugins.iptvplayer.cimalina_proxy.value + if proxy != "None": + if proxy == "proxy_1": + try: + proxy = config.plugins.iptvplayer.alternativeproxy1.value + except Exception: + proxy = None + else: + try: + proxy = config.plugins.iptvplayer.alternativeproxy2.value + except Exception: + proxy = None + else: + proxy = None + return proxy + + def getPage(self, baseUrl, addParams=None, post_data=None): + if addParams is None: + addParams = dict(self.defaultParams) + else: + addParams = dict(addParams) + + for k, v in self.defaultParams.items(): + if k not in addParams: + addParams[k] = v + + proxy = self.getProxy() + if proxy and "http_proxy" not in addParams: + addParams = MergeDicts(addParams, {"http_proxy": proxy}) + + return self.cm.getPage(baseUrl, addParams, post_data) + + def selectDomain(self): + domains = [ + "https://ci.alooytv12.xyz/", + "https://www.ci.alooytv12.xyz/", + "https://alooytv12.xyz/", + ] + + altDomain = config.plugins.iptvplayer.cimalina_alt_domain.value.strip() + if self.cm.isValidUrl(altDomain): + if not altDomain.endswith("/"): + altDomain += "/" + domains.insert(0, altDomain) + + for domain in domains: + sts, data = self.getPage(domain) + if not sts: + continue + low = data.lower() + if "tv-series" in low or "autocompleteajax" in low or "alooytv" in low: + try: + self.setMainUrl(self.cm.meta["url"]) + except Exception: + self.setMainUrl(domain) + self.MAIN_URL = self.getMainUrl() + return + + self.setMainUrl(domains[0]) + self.MAIN_URL = self.getMainUrl() + + def getFullIconUrl(self, url): + url = (url or "").strip() + url = CBaseHostClass.getFullIconUrl(self, url) + if not url: + return "" + proxy = self.getProxy() + if proxy: + url = strwithmeta(url, {"iptv_http_proxy": proxy}) + return url + + def _normUrl(self, url): + url = (url or "").strip() + if not url: + return "" + url = html_unescape(url) + url = url.replace("\\/", "/").replace("\\\\/", "/").replace("&", "&") + url = url.strip().strip("\\").strip('"').strip("'") + if url.startswith("//"): + url = "https:" + url + return self.getFullUrl(url) + + def _cleanTitle(self, txt): + return self.cleanHtmlStr(html_unescape(txt or "")).strip() + + def _slugToTitle(self, url): + url = (url or "").split("?", 1)[0].split("#", 1)[0] + slug = url.rstrip("/").split("/")[-1] + if slug.endswith(".html"): + slug = slug[:-5] + if slug.startswith("watch"): + slug = slug[5:] + slug = slug.replace("-", " ").replace("_", " ") + slug = re.sub(r"\s+", " ", slug).strip() + return self._cleanTitle(slug) + + def _appendUniqueLink(self, retTab, name, url, need_resolve=1): + url = self._normUrl(url) + if not url or not self.cm.isValidUrl(url): + return False + for item in retTab: + if str(item.get("url", "")) == str(url): + return False + retTab.append({"name": name, "url": url, "need_resolve": need_resolve}) + return True + + def _extractIframeSrc(self, html): + html = html_unescape(html or "") + patterns = [r']+src=["\']([^"\']+?)["\']', r"]+src=([^\s>]+)", r'src=["\']([^"\']+?)["\']', r"src=([^\s>]+)"] + for pattern in patterns: + val = self.cm.ph.getSearchGroups(html, pattern)[0] + if val: + return self._normUrl(val) + return "" + + def _decodeBase64JsonValue(self, value): + if not value or not base64 or not json: + return {} + try: + value = html_unescape(value).strip().strip('"').strip("'") + missing = len(value) % 4 + if missing: + value += "=" * (4 - missing) + txt = base64.b64decode(value) + try: + txt = txt.decode("utf-8") + except Exception: + txt = txt.decode("utf-8", "ignore") + return json.loads(txt) + except Exception: + printExc() + return {} + + def _decodeBase64Text(self, value): + if not value or not base64: + return "" + try: + value = html_unescape(value).strip().strip('"').strip("'") + missing = len(value) % 4 + if missing: + value += "=" * (4 - missing) + txt = base64.b64decode(value) + try: + return txt.decode("utf-8") + except Exception: + return txt.decode("utf-8", "ignore") + except Exception: + return "" + + def _isVideoUrl(self, url): + """ + يتعرف على روابط الفيديو بما فيها الملفات المموهة بامتداد صورة. + مثال: /file/wp-alooytv/uploads/2023/Echo/s01/01.jpg => فيديو حقيقي + """ + rawUrl = self._normUrl(url) + if not rawUrl: + return False + low = rawUrl.lower().split("#")[0].split("?")[0] + + # امتدادات فيديو مباشرة + if ".m3u8" in low or ".mp4" in low or ".ts" in low or ".mkv" in low or ".avi" in low: + return True + + # ملفات بامتداد صورة لكن في مسار فيديو معروف (تمويه) + imgExts = (".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp") + if any(low.endswith(ext) for ext in imgExts): + for hint in self.VIDEO_PATH_HINTS: + if hint in low: + return True + return False + + return False + + def _isImageUrl(self, url): + """ + يتحقق إذا كان الرابط صورة thumbnail حقيقية وليس فيديو مموه. + يفحص المسار أولاً قبل الاعتماد على الامتداد وحده. + """ + rawUrl = (url or "").lower().split("?")[0].split("#")[0] + + # مسارات thumbnail معروفة => صورة دائماً + for hint in self.THUMB_PATH_HINTS: + if hint in rawUrl: + return True + + # مسارات فيديو معروفة => ليست صورة حتى لو امتدادها jpg + for hint in self.VIDEO_PATH_HINTS: + if hint in rawUrl: + return False + + # فحص الامتداد الاعتيادي للحالات الأخرى + for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"]: + if rawUrl.endswith(ext): + return True + + return False + + def _findNextPage(self, data, page, currentUrl=""): + nextPageUrl = self.cm.ph.getSearchGroups(data, r']+href=["\']([^"\']+?)["\'][^>]*rel=["\']next["\']')[0] + if nextPageUrl: + nextPageUrl = self.getFullUrl(nextPageUrl) + if nextPageUrl and nextPageUrl != currentUrl: + return nextPageUrl + + pagination = self.cm.ph.getDataBeetwenMarkers(data, '
", False)[1] + if not pagination: + pagination = self.cm.ph.getDataBeetwenMarkers(data, '
    ", False)[1] + if not pagination: + return "" + + patterns = [r'href=["\']([^"\']+?)["\'][^>]*data-ci-pagination-page=["\']%d["\']' % (page + 1), r'href=["\']([^"\']+?)["\'][^>]*>\s*(?:Next|التالي|›|»|»)\s*<', r'href=["\']([^"\']+?)["\'][^>]*>\s*%d\s*<' % (page + 1), r'href=["\']([^"\']*?[?&]page=%d[^"\']*)["\']' % (page + 1), r'href=["\']([^"\']*?/page/%d/?[^"\']*)["\']' % (page + 1), r'href=["\']([^"\']*?%d\.html[^"\']*)["\']' % ((page + 1) * 50)] + + for pattern in patterns: + nextPageUrl = self.cm.ph.getSearchGroups(pagination, pattern)[0] + if nextPageUrl: + nextPageUrl = self.getFullUrl(nextPageUrl) + if nextPageUrl and nextPageUrl != currentUrl: + return nextPageUrl + + return "" + + def _extractIconFromSegment(self, segment): + icon = self.cm.ph.getSearchGroups(segment, r']+src=["\']([^"\']+?)["\']')[0] + if not icon: + icon = self.cm.ph.getSearchGroups(segment, r'data-src=["\']([^"\']+?)["\']')[0] + if not icon: + icon = self.cm.ph.getSearchGroups(segment, r'data-original=["\']([^"\']+?)["\']')[0] + # تجاهل أيقونات play + if icon and (".svg" in icon.lower() or "play" in icon.lower()): + icon = "" + return self.getFullIconUrl(icon) + + def _collectCategoryEntries(self, data): + entries = [] + seen = set() + + block = self.cm.ph.getDataBeetwenMarkers(data, '
    ', '
    ]+class=["\'][^"\']*latest-movie-img-container[^"\']*["\']', block) + + for part in parts[1:]: + segment = part[:5000] + + itemUrl = self.cm.ph.getSearchGroups(segment, r']+href=["\']([^"\']+?\.html(?:\?[^"\']*)?)["\'][^>]*class=["\'][^"\']*ico-play[^"\']*["\']')[0] + if not itemUrl: + itemUrl = self.cm.ph.getSearchGroups(segment, r']+href=["\']([^"\']+?\.html(?:\?[^"\']*)?)["\']')[0] + + itemUrl = self._normUrl(itemUrl) + if not itemUrl or itemUrl in seen: + continue + + title = self.cm.ph.getSearchGroups(segment, r'class=["\']movie-title["\'][^>]*>.*?]*>.*?]*>(.*?)')[0] + if not title: + title = self.cm.ph.getSearchGroups(segment, r"]*>\s*]*>(.*?)")[0] + if not title: + title = self.cm.ph.getSearchGroups(segment, r']+alt=["\']([^"\']+?)["\']')[0] + title = self._cleanTitle(title) + if not title: + title = self._slugToTitle(itemUrl) + + icon = self._extractIconFromSegment(segment) + label = self.cleanHtmlStr(self.cm.ph.getSearchGroups(segment, r']+class=["\']label label-primary["\'][^>]*>\s*([^<]+?)\s*<')[0]) + + seen.add(itemUrl) + entries.append({"url": itemUrl, "title": title, "icon": icon, "label": label}) + + return entries + + def _extractEpisodesBlock(self, data): + start = data.find('class="season"') + if start < 0: + start = data.find("class='season'") + if start < 0: + return "" + + endMarkers = ["You May Like", "similler-movie", 'class="similler-movie"', "class='similler-movie'", "", ""] + endPos = len(data) + for marker in endMarkers: + pos = data.find(marker, start) + if pos > start and pos < endPos: + endPos = pos + return data[start:endPos] + + def _collectEpisodeEntries(self, data, currentUrl): + block = self._extractEpisodesBlock(data) + if not block: + return [] + + entries = [] + seen = set() + + patterns = [ + r']+href=["\']([^"\']+?)["\'][^>]*class=["\']([^"\']*btn-inline[^"\']*)["\'][^>]*>(.*?)', + r']+href=["\']([^"\']*watch[^"\']+?\?key=[^"\']+)["\'][^>]*>(.*?)', + r']+href=["\']([^"\']*watch[^"\']+?&key=[^"\']+)["\'][^>]*>(.*?)', + ] + + for pattern in patterns: + for match in re.finditer(pattern, block, re.I | re.S): + if len(match.groups()) == 3: + itemUrl = self._normUrl(match.group(1)) + cssClass = match.group(2) + title = self._cleanTitle(match.group(3)) + if "btn-inline" not in cssClass: + continue + else: + itemUrl = self._normUrl(match.group(1)) + title = self._cleanTitle(match.group(2)) + + if not itemUrl or itemUrl in seen: + continue + if "watch" not in itemUrl: + continue + if "?key=" not in itemUrl and "&key=" not in itemUrl: + continue + + seen.add(itemUrl) + entries.append({"url": itemUrl, "title": title, "icon": "", "label": _("Episode")}) + + if not entries: + for itemUrl in re.findall(r'href=["\']([^"\']*watch[^"\']+?(?:\?|&)key=[^"\']+)["\']', block, re.I): + itemUrl = self._normUrl(itemUrl) + if not itemUrl or itemUrl in seen: + continue + seen.add(itemUrl) + entries.append({"url": itemUrl, "title": self._slugToTitle(itemUrl), "icon": "", "label": _("Episode")}) + + return entries + + def _getDirectUrlWithMeta(self, url, referer=""): + url = self._normUrl(url) + if not url: + return "" + if not referer: + referer = self.getMainUrl() + + mainDomain = self.getMainUrl() + if not mainDomain.endswith("/"): + mainDomain += "/" + + videoMeta = dict(self.VIDEO_HEADER) + videoMeta["Referer"] = mainDomain + videoMeta["Origin"] = mainDomain.rstrip("/") + + proxy = self.getProxy() + if proxy: + videoMeta["iptv_http_proxy"] = proxy + + return strwithmeta(url, videoMeta) + + def _extractDownloadVideoUrl(self, data): + items = re.findall(r'href=["\']([^"\']*downloadvideo\.php\?[^"\']+)["\']', data, re.I) + for item in items: + full = self._normUrl(item) + encoded = self.cm.ph.getSearchGroups(full, r"[?&]videourl=([^&]+)")[0] + if not encoded: + continue + decoded = self._decodeBase64Text(encoded) + decoded = self._normUrl(decoded) + if decoded and self._isVideoUrl(decoded) and not self._isImageUrl(decoded): + return decoded + return "" + + def listMainMenu(self, cItem): + tab = [ + {"category": "list_items", "title": _("أحدث الإضافات"), "url": self.getFullUrl("/tv-series.html"), "icon": self.DEFAULT_ICON_URL}, + {"category": "sections", "title": _("الأقسام"), "icon": self.DEFAULT_ICON_URL}, + {"category": "ramadan", "title": _("رمضان"), "icon": self.DEFAULT_ICON_URL}, + {"category": "list_items", "title": _("الرئيسية"), "url": self.getMainUrl(), "icon": self.DEFAULT_ICON_URL}, + {"category": "search", "title": _("Search"), "search_item": True, "icon": self.DEFAULT_ICON_URL}, + {"category": "search_history", "title": _("Search history"), "icon": self.DEFAULT_ICON_URL}, + ] + self.listsTab(tab, cItem) + + def listSections(self, cItem): + tab = [ + {"category": "list_items", "title": _("عربي"), "url": self.getFullUrl("/genre/arabic.html")}, + {"category": "list_items", "title": _("خليجي"), "url": self.getFullUrl("/genre/kleeji.html")}, + {"category": "list_items", "title": _("تركي"), "url": self.getFullUrl("/genre/turki.html")}, + {"category": "list_items", "title": _("فارسي"), "url": self.getFullUrl("/genre/farisi.html")}, + {"category": "list_items", "title": _("أنمي"), "url": self.getFullUrl("/genre/anmi.html")}, + {"category": "list_items", "title": _("أفلام أجنبية"), "url": self.getFullUrl("/genre/foreign-movies.html")}, + {"category": "list_items", "title": _("أفلام كورية"), "url": self.getFullUrl("/genre/Korean-movies.html")}, + {"category": "list_items", "title": _("مسلسلات أجنبية"), "url": self.getFullUrl("/genre/Foreign-series.html")}, + {"category": "list_items", "title": _("مسلسلات كورية"), "url": self.getFullUrl("/genre/Korean-series.html")}, + {"category": "list_items", "title": _("مسلسلات آسيوية"), "url": self.getFullUrl("/genre/asia-series.html")}, + ] + self.listsTab(tab, cItem) + + def listRamadanYears(self, cItem): + tab = [ + {"category": "ramadan_year", "title": _("رمضان 2026"), "year": "2026"}, + {"category": "ramadan_year", "title": _("رمضان 2025"), "year": "2025"}, + {"category": "ramadan_year", "title": _("رمضان 2024"), "year": "2024"}, + {"category": "ramadan_year", "title": _("رمضان 2023"), "year": "2023"}, + ] + self.listsTab(tab, cItem) + + def listRamadanYear(self, cItem): + year = cItem.get("year", "") + if year == "2023": + tab = [ + {"category": "list_items", "title": _("عربي"), "url": self.getFullUrl("/genre/ramadan-arabi.html")}, + {"category": "list_items", "title": _("خليجي"), "url": self.getFullUrl("/genre/ramadan-kleeji.html")}, + ] + else: + tab = [ + {"category": "list_items", "title": _("عربي"), "url": self.getFullUrl("/genre/ramadan-arabi-%s.html" % year)}, + {"category": "list_items", "title": _("خليجي"), "url": self.getFullUrl("/genre/ramadan-kleeji-%s.html" % year)}, + ] + self.listsTab(tab, cItem) + + def listItems(self, cItem): + page = cItem.get("page", 1) + url = self._normUrl(cItem.get("url", "")) + if not url: + return + + sts, data = self.getPage(url) + if not sts: + return + + try: + self.setMainUrl(self.cm.meta.get("url", self.getMainUrl())) + except Exception: + pass + + entries = self._collectCategoryEntries(data) + + if not entries: + seen = set() + for href in re.findall(r']+href=["\']([^"\']+?\.html(?:\?[^"\']*)?)["\']', data, re.I): + itemUrl = self._normUrl(href) + if not itemUrl or itemUrl in seen: + continue + if any(x in itemUrl for x in ["/genre/", "/search", "tv-series.html", "javascript"]): + continue + if "/watch/" not in itemUrl and not re.search(r"/watch[^/]", itemUrl): + continue + seen.add(itemUrl) + entries.append({"url": itemUrl, "title": self._slugToTitle(itemUrl), "icon": self.DEFAULT_ICON_URL, "label": ""}) + + for entry in entries: + params = dict(cItem) + params.update({"good_for_fav": True, "title": entry["title"], "url": entry["url"], "prev_url": entry["url"], "icon": entry["icon"] or self.DEFAULT_ICON_URL, "desc": entry.get("label", ""), "category": "explore_item"}) + self.addDir(params) + + nextPageUrl = self._findNextPage(data, page, url) + if nextPageUrl and nextPageUrl != url: + params = dict(cItem) + params.update({"title": _("Next page"), "url": nextPageUrl, "page": page + 1, "category": "list_items"}) + self.addDir(params) + + def exploreItems(self, cItem): + url = cItem.get("url", "") + sts, data = self.getPage(url) + if not sts: + return + + episodeEntries = self._collectEpisodeEntries(data, url) + uniqueEpisodes = [] + seen = set() + + for item in episodeEntries: + if item["url"] in seen: + continue + seen.add(item["url"]) + uniqueEpisodes.append(item) + + if len(uniqueEpisodes) > 1: + for entry in uniqueEpisodes: + epTitle = entry["title"] + if not epTitle: + epTitle = cItem.get("title", "") + elif cItem.get("title", "") and cItem.get("title", "") not in epTitle: + epTitle = "%s - %s" % (cItem.get("title", ""), epTitle) + params = dict(cItem) + params.update({"good_for_fav": True, "title": epTitle, "url": entry["url"], "prev_url": entry["url"], "icon": cItem.get("icon", self.DEFAULT_ICON_URL), "desc": _("Episode"), "urlSeparateRequest": 1}) + self.addVideo(params) + return + + if len(uniqueEpisodes) == 1: + entry = uniqueEpisodes[0] + epTitle = entry["title"] or cItem.get("title", "") + params = dict(cItem) + params.update({"good_for_fav": True, "title": epTitle, "url": entry["url"], "prev_url": entry["url"], "icon": cItem.get("icon", self.DEFAULT_ICON_URL), "desc": _("Episode"), "urlSeparateRequest": 1}) + self.addVideo(params) + return + + params = dict(cItem) + params.update({"good_for_fav": True, "prev_url": url, "url": url, "icon": cItem.get("icon", self.DEFAULT_ICON_URL), "urlSeparateRequest": 1}) + self.addVideo(params) + + def listSearchResult(self, cItem, searchPattern, searchType): + if searchType == "movies": + pattern = "فيلم " + searchPattern + elif searchType == "series": + pattern = "مسلسل " + searchPattern + else: + pattern = searchPattern + + url = self.getFullUrl("/search?q=%s" % urllib_quote_plus(pattern)) + params = dict(cItem) + params.update({"name": "category", "category": "list_items", "url": url, "good_for_fav": False}) + self.listItems(params) + + def _extractWatchForm(self, data): + patterns = [r'(]+id=["\']formWatch["\'][\s\S]+?)', r'(]+class=["\'][^"\']*formWatch[^"\']*["\'][\s\S]+?)', r'(]+method=["\']post["\'][\s\S]+?)'] + for pattern in patterns: + try: + m = re.search(pattern, data, re.I) + if m: + return m.group(1) + except Exception: + pass + return "" + + def _getInputValue(self, formData, name): + patterns = [r'name=["\']%s["\'][^>]*value=["\']([^"\']+?)["\']' % re.escape(name), r'value=["\']([^"\']+?)["\'][^>]*name=["\']%s["\']' % re.escape(name)] + for pattern in patterns: + val = self.cm.ph.getSearchGroups(formData, pattern)[0] + if val: + return html_unescape(val).strip() + return "" + + def _appendLinksFromEncodedMap(self, retTab, encodedData, cItemTitle): + dataMap = self._decodeBase64JsonValue(encodedData) + if not isinstance(dataMap, dict): + return + idx = 0 + for key in dataMap: + idx += 1 + link = self._normUrl(dataMap[key]) + if not link or not self._isVideoUrl(link) or self._isImageUrl(link): + continue + label = self.cleanHtmlStr(key) or ("Server %d" % idx) + hostName = self.up.getHostName(link, True) + self._appendUniqueLink(retTab, "%s [%s] - %s" % (cItemTitle, label, hostName), link, 1) + + def _parseServersPage(self, pageData, cItem): + retTab = [] + cItemTitle = cItem.get("title", "") + + serversSection = self.cm.ph.getDataBeetwenMarkers(pageData, "serversList", "
", False)[1] + if not serversSection: + serversSection = self.cm.ph.getDataBeetwenMarkers(pageData, "list_servers", "", False)[1] + + if serversSection: + items = self.cm.ph.getAllItemsBeetwenMarkers(serversSection, "") + for item in items: + serverName = self.cleanHtmlStr(item) or "Server" + serverData = self.cm.ph.getSearchGroups(item, r'data-server=["\']([^"\']+?)["\']')[0] + if not serverData: + serverData = self.cm.ph.getSearchGroups(item, r"data-server=([^ >]+)")[0] + if not serverData: + serverData = self.cm.ph.getSearchGroups(item, r'data-embed=["\']([^"\']+?)["\']')[0] + + iframeUrl = self._extractIframeSrc(serverData) + if not iframeUrl: + rawUrl = self.cm.ph.getSearchGroups(serverData, r'(https?://[^\s"\']+|//[^\s"\']+)')[0] + iframeUrl = self._normUrl(rawUrl) + + if iframeUrl: + hostName = self.up.getHostName(iframeUrl, True) + self._appendUniqueLink(retTab, "%s [%s] - %s" % (cItemTitle, serverName, hostName), iframeUrl, 1) + + if not retTab: + for iframeUrl in re.findall(r']+src=["\']([^"\']+?)["\']', pageData, re.I): + iframeUrl = self._normUrl(iframeUrl) + if not iframeUrl: + continue + hostName = self.up.getHostName(iframeUrl, True) + self._appendUniqueLink(retTab, "%s [Default] - %s" % (cItemTitle, hostName), iframeUrl, 1) + + return retTab + + def getLinksForVideo(self, cItem): + url = cItem.get("prev_url", cItem.get("url", "")) + cacheKey = str(url) + if cacheKey in self.cacheLinks: + return self.cacheLinks[cacheKey] + + retTab = [] + if not url: + return retTab + + mainDomain = self.getMainUrl() + if not mainDomain.endswith("/"): + mainDomain += "/" + + ajaxParams = dict(self.defaultParams) + ajaxParams["header"] = dict(self.AJAX_HEADER) + ajaxParams["header"]["Referer"] = mainDomain + + sts, data = self.getPage(url, ajaxParams) + if not sts: + sts, data = self.getPage(url) + if not sts: + return retTab + + # أنماط استخراج روابط الفيديو - تشمل الملفات المموهة بامتداد صورة + directPatterns = [ + r'["\'](https?://[^"\']+?\.mp4(?:\?[^"\']*)?)["\']', + r'["\'](https?://[^"\']+?\.m3u8(?:\?[^"\']*)?)["\']', + r'["\'](https?://[^"\']+?\.ts(?:\?[^"\']*)?)["\']', + # ملفات jpg/jpeg في مسارات فيديو معروفة (تمويه) + r'["\'](https?://[^"\']*(?:/file/wp-|/uploads/20)[^"\']+?\.jpe?g(?:\?[^"\']*)?)["\']', + r'["\'](https?://[^"\']*(?:/file/wp-|/uploads/20)[^"\']+?\.png(?:\?[^"\']*)?)["\']', + r'file["\']?\s*:\s*["\']([^"\']+?)["\']', + r']+src=["\']([^"\']+?)["\']', + r'contentUrl["\']?\s*:\s*["\']([^"\']+?)["\']', + ] + + def add_direct(candidate): + candidate = self._normUrl(candidate) + if not candidate: + return + if not self._isVideoUrl(candidate): + return + if self._isImageUrl(candidate): + return + hostName = self.up.getHostName(candidate, True) or "direct" + finalUrl = self._getDirectUrlWithMeta(candidate, mainDomain) + for t in retTab: + if str(t.get("url", "")) == str(finalUrl): + return + retTab.append({"name": "%s - %s" % (cItem.get("title", ""), hostName), "url": finalUrl, "need_resolve": 0}) + + for pattern in directPatterns: + for itemUrl in re.findall(pattern, data, re.I): + add_direct(itemUrl) + + if not retTab: + dlUrl = self._extractDownloadVideoUrl(data) + if dlUrl: + add_direct(dlUrl) + + if not retTab: + formData = self._extractWatchForm(data) + if formData: + actionUrl = self.getFullUrl(self.cm.ph.getSearchGroups(formData, r'action=["\']([^"\']+?)["\']')[0]) + servers = self._getInputValue(formData, "servers") + downloads = self._getInputValue(formData, "downloads") + + if actionUrl: + sts2, postData = self.getPage(actionUrl, ajaxParams, {"servers": servers, "downloads": downloads, "submit": ""}) + if sts2 and postData: + for pattern in directPatterns: + for itemUrl in re.findall(pattern, postData, re.I): + add_direct(itemUrl) + + if not retTab: + dlUrl = self._extractDownloadVideoUrl(postData) + if dlUrl: + add_direct(dlUrl) + + if not retTab: + retTab.extend(self._parseServersPage(postData, cItem)) + + if not retTab: + self._appendLinksFromEncodedMap(retTab, servers, cItem.get("title", "")) + self._appendLinksFromEncodedMap(retTab, downloads, cItem.get("title", "")) + + if not retTab: + retTab.extend(self._parseServersPage(data, cItem)) + + self.cacheLinks[cacheKey] = retTab + return retTab + + def getVideoLinks(self, videoUrl): + if not self.cm.isValidUrl(videoUrl): + return [] + if ".mp4" in videoUrl or ".m3u8" in videoUrl or self._isVideoUrl(videoUrl): + return [{"name": "direct", "url": videoUrl}] + return self.up.getVideoLinkExt(videoUrl) + + def getArticleContent(self, cItem): + otherInfo = {} + url = cItem.get("prev_url", cItem.get("url", "")) + sts, data = self.getPage(url) + if not sts: + return [] + + desc = self.cleanHtmlStr(self.cm.ph.getDataBeetwenNodes(data, ("", "StoryMovie"), (""), False)[1]) + if not desc: + desc = cItem.get("desc", "") + + year = self.cleanHtmlStr(self.cm.ph.getSearchGroups(data, r"([12][0-9]{3})")[0]) + if year: + otherInfo["year"] = year + + genre = self.cleanHtmlStr(self.cm.ph.getSearchGroups(data, r"genre[^>]*>\s*([^<]+?)\s*<")[0]) + if genre: + otherInfo["genre"] = genre + + duration = self.cleanHtmlStr(self.cm.ph.getSearchGroups(data, r"مده العرض[^<]*]+>\s*([^<]+?)\s*<")[0]) + if duration: + otherInfo["duration"] = duration + + return [{"title": cItem.get("title", ""), "text": desc, "images": [{"title": "", "url": cItem.get("icon", self.DEFAULT_ICON_URL)}], "other_info": otherInfo}] + + def handleService(self, index, refresh=0, searchPattern="", searchType=""): + CBaseHostClass.handleService(self, index, refresh, searchPattern, searchType) + + if self.MAIN_URL is None: + self.selectDomain() + + name = self.currItem.get("name", "") + category = self.currItem.get("category", "") + self.currList = [] + + try: + if not name and not category: + self.listMainMenu({"name": "category"}) + elif category == "sections": + self.listSections(self.currItem) + elif category == "ramadan": + self.listRamadanYears(self.currItem) + elif category == "ramadan_year": + self.listRamadanYear(self.currItem) + elif category == "list_items": + self.listItems(self.currItem) + elif category == "explore_item": + self.exploreItems(self.currItem) + elif category in ("search", "search_next_page"): + params = dict(self.currItem) + params.update({"search_item": False, "name": "category"}) + self.listSearchResult(params, searchPattern, searchType) + elif category == "search_history": + self.listsHistory({"name": "history", "category": "search"}, "desc", _("Type: ")) + except Exception: + printExc() + + CBaseHostClass.endHandleService(self, index, refresh) + + +class IPTVHost(CHostBase): + + def __init__(self): + CHostBase.__init__(self, AlooyTV(), True) + + def getSearchTypes(self): + return [("All", "all"), ("Movies", "movies"), ("Tv Series", "series")] + + def withArticleContent(self, cItem): + return cItem.get("urlSeparateRequest", 0) == 1 or "prev_url" in cItem or cItem.get("category", "") == "explore_item" diff --git a/IPTVPlayer/hosts/hostarabseed.py b/IPTVPlayer/hosts/hostarabseed.py index b2fae77e..0ef11f28 100644 --- a/IPTVPlayer/hosts/hostarabseed.py +++ b/IPTVPlayer/hosts/hostarabseed.py @@ -1,34 +1,18 @@ # -*- coding: utf-8 -*- # Last modified: 13/10/2025 - popking (odem2014) -# Last updated: 18/04/2025 - M.Elsafty (angel_heart) +# Last updated: 20/05/2025 - M.Elsafty (angel_heart) ################################################### # LOCAL import ################################################### # localization library from Plugins.Extensions.IPTVPlayer.components.iptvplayerinit import TranslateTXT as _ - -# host main class from Plugins.Extensions.IPTVPlayer.components.ihost import CHostBase, CBaseHostClass - -# tools - write on log, write exception infos and merge dicts from Plugins.Extensions.IPTVPlayer.tools.iptvtools import printDBG, printExc, MergeDicts, E2ColoR - -# add metadata to url from Plugins.Extensions.IPTVPlayer.tools.iptvtypes import strwithmeta - -# library for json (instead of standard json.loads and json.dumps) from Plugins.Extensions.IPTVPlayer.libs.e2ijson import loads as json_loads, dumps as json_dumps - -# read informations in m3u8 from Plugins.Extensions.IPTVPlayer.libs.urlparserhelper import getDirectM3U8Playlist - -################################################### from Plugins.Extensions.IPTVPlayer.p2p3.UrlParse import urljoin from Plugins.Extensions.IPTVPlayer.p2p3.UrlLib import urllib_quote_plus - -################################################### -# FOREIGN import -################################################### import re import base64 import json @@ -38,29 +22,22 @@ except ImportError: from urllib import urlencode, urlopen, unquote, quote - ################################################### - def GetConfigList(): return [] def gettytul(): - return "https://asd.pics/" # main url of host + return "https://m.asd.ink/" # M.elsafty.20260516 class ArabSeed(CBaseHostClass): def __init__(self): - # init global variables for this class CBaseHostClass.__init__(self, {"history": "arabseed", "cookie": "arabseed.cookie"}) # names for history and cookie files in cache - # vars default values self.urlencode = urlencode - # various urls self.MAIN_URL = gettytul() - self.SEARCH_URL = "https://asd.pics/search" - # url for default icon + self.SEARCH_URL = "https://m.asd.ink/search" self.DEFAULT_ICON_URL = "https://raw.githubusercontent.com/oe-mirrors/e2iplayer/gh-pages/Thumbnails/arabseed.png" - # default header and http params self.HEADER = self.cm.getDefaultHeader(browser="chrome") self.defaultParams = {"header": self.HEADER, "use_cookie": True, "load_cookie": True, "save_cookie": True, "cookiefile": self.COOKIE_FILE} @@ -72,72 +49,8 @@ def getPage(self, base_url, add_params=None, post_data=None): add_params["cloudflare_params"] = {"cookie_file": self.COOKIE_FILE, "User-Agent": self.HEADER.get("User-Agent")} return self.cm.getPageCFProtection(base_url, add_params, post_data) - def getLinksForVideo(self, cItem): - printDBG("ArabSeed.getLinksForVideo [%s]" % cItem) - linksTab = [] - url = cItem.get("url", "") - # =============================== - # IMDb Resolver - # =============================== - if "imdb.com/video/" in url: - printDBG("Detected IMDb trailer") - return self.getIMDBTrailer(url) - referer = cItem.get("url", self.MAIN_URL) - headers = {"Referer": referer, "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"} - sts, data = self.getPage(cItem["url"], self.defaultParams) - if not sts: - return [] - # Prevent redirect/park traps (like yfdpco2.com) - redirect_domains = ["yfdpco2.com", "ww38.m.seeeed.xyz", "ww38.m.reviewrate.net"] - # --- Step 1: find embedded iframes --- - iframes = self.cm.ph.getAllItemsBeetwenMarkers(data, "") - for iframe in iframes: - url = self.cm.ph.getSearchGroups(iframe, 'src="([^"]+)"')[0] - if not url: - continue - # Skip redirect trap URLs - if any(dom in url for dom in redirect_domains): - continue - # Auto-wrap only for known domains (fix for seeeed.xyz embeds) - if "m.seeeed.xyz" in url and not url.startswith("https://m.reviewrate.net/asd.php?url="): - import base64 - - b64url = base64.b64encode(url.encode()).decode().replace("+", "-").replace("/", "_").replace("=", "") - wrapped = "https://m.reviewrate.net/asd.php?url=" + b64url - url = wrapped - printDBG("Auto-wrapped seeeed.xyz URL -> %s" % url) - linksTab.append({"name": self.up.getHostName(url).capitalize(), "url": strwithmeta(url, headers), "need_resolve": 1}) - # --- Step 2: try tags (direct .mp4) --- - video_links = self.cm.ph.getAllItemsBeetwenMarkers(data, "") - for link in video_links: - url = self.cm.ph.getSearchGroups(link, 'src="([^"]+)"')[0] - if not url: - continue - if any(dom in url for dom in redirect_domains): - continue - linksTab.append({"name": self.up.getHostName(url).capitalize(), "url": strwithmeta(url, headers), "need_resolve": 0}) - # --- Step 3: fallback to getVideoLinks --- - if not linksTab: - printDBG("ArabSeed.getLinksForVideo: no direct links found, fallback to parser") - resolved = self.getVideoLinks(cItem["url"]) - for entry in resolved: - if isinstance(entry, dict): - entry["url"] = strwithmeta(entry.get("url"), headers) - linksTab.append(entry) - else: - linksTab.append({"name": self.up.getHostName(cItem["url"]), "url": strwithmeta(entry, headers), "need_resolve": 0}) - printDBG("ArabSeed.getLinksForVideo -> Final linksTab: %s" % str(linksTab)) - return linksTab - - def getVideoLinks(self, url): - printDBG("ArabSeed.getVideoLinks [%s]" % url) - if self.cm.isValidUrl(url): - return self.up.getVideoLinkExt(url) - def listMainMenu(self, cItem): - # items of main menu printDBG("ArabSeed.listMainMenu") - # Define main categories statically like FilmPalast does self.MAIN_CAT_TAB = [ {"category": "movies_folder", "title": "الافلام"}, {"category": "series_folder", "title": "المسلسلات"}, @@ -146,63 +59,61 @@ def listMainMenu(self, cItem): {"category": "series_packs_folder", "title": "مواسم مسلسلات - برامج - أنمي"}, {"category": "other_folder", "title": "اخري"}, ] + self.searchItems() - # Define subcategories for each folder self.MOVIES_CAT_TAB = [ - {"category": "list_items", "title": "افلام عربي", "url": self.getFullUrl("/category/arabic-movies-10/")}, - {"category": "list_items", "title": "افلام اجنبي", "url": self.getFullUrl("/category/foreign-movies-10/")}, - {"category": "list_items", "title": "افلام Netfilx", "url": self.getFullUrl("/category/netfilx/افلام-netfilx/")}, - {"category": "list_items", "title": "افلام هندى", "url": self.getFullUrl("/category/indian-movies/")}, + {"category": "list_items", "title": "افلام عربي", "url": self.getFullUrl("/category/arabic-movies-14/")}, + {"category": "list_items", "title": "افلام اجنبي", "url": self.getFullUrl("/category/foreign-movies-14/")}, + {"category": "list_items", "title": "افلام Netfilx", "url": self.getFullUrl("/category/netflix/netflix-movies/")}, + {"category": "list_items", "title": "افلام هندى", "url": self.getFullUrl("/category/indian-movies-2/")}, {"category": "list_items", "title": "افلام تركية", "url": self.getFullUrl("/category/turkish-movies/")}, - {"category": "list_items", "title": "افلام اسيوية", "url": self.getFullUrl("/category/asian-movies/")}, + {"category": "list_items", "title": "افلام اسيوية", "url": self.getFullUrl("/category/asian-movies-2/")}, {"category": "list_items", "title": "افلام كلاسيكيه", "url": self.getFullUrl("/category/افلام-كلاسيكيه/")}, - {"category": "list_items", "title": "افلام مدبلجة", "url": self.getFullUrl("/category/افلام-مدبلجة-1/")}, + {"category": "list_items", "title": "افلام مدبلجة", "url": self.getFullUrl("/category/dubbed-movies/")}, ] self.SERIES_CAT_TAB = [ - {"category": "series", "title": "مسلسلات عربية", "url": self.getFullUrl("/category/arabic-series-8/")}, + {"category": "series", "title": "مسلسلات عربية", "url": self.getFullUrl("/category/arabic-series-14/")}, {"category": "series", "title": "مسلسلات مصرية", "url": self.getFullUrl("/category/مسلسلات-مصريه/")}, - {"category": "series", "title": "مسلسلات اجنبية", "url": self.getFullUrl("/category/foreign-series-3/")}, - {"category": "series", "title": "مسلسلات Netfilx", "url": self.getFullUrl("/category/netfilx/مسلسلات-netfilx-1/")}, + {"category": "series", "title": "مسلسلات اجنبية", "url": self.getFullUrl("/category/foreign-series-7/")}, + {"category": "series", "title": "مسلسلات Netfilx", "url": self.getFullUrl("/category/netflix/netflix-series/")}, {"category": "series", "title": "مسلسلات تركية", "url": self.getFullUrl("/category/turkish-series-2/")}, {"category": "series", "title": "مسلسلات هندية", "url": self.getFullUrl("/category/مسلسلات-هندية/")}, {"category": "series", "title": "مسلسلات كورية", "url": self.getFullUrl("/category/مسلسلات-كوريه/")}, - {"category": "series", "title": "مسلسلات مدبلجة", "url": self.getFullUrl("/category/مسلسلات-مدبلجة/")}, + {"category": "series", "title": "مسلسلات مدبلجة", "url": self.getFullUrl("/category/dubbed-series/")}, {"category": "series", "title": "مسلسلات كرتون", "url": self.getFullUrl("/category/cartoon-series/")}, ] self.SERIES_PACKS_CAT_TAB = [ - {"category": "series_packs", "title": "مواسم مسلسلات عربية", "url": self.getFullUrl("/category/arabic-series-8/packs/")}, + {"category": "series_packs", "title": "مواسم مسلسلات عربية", "url": self.getFullUrl("/category/arabic-series-14/packs/")}, {"category": "series_packs", "title": "مواسم مسلسلات مصرية", "url": self.getFullUrl("/category/مسلسلات-مصريه/packs/")}, - {"category": "series_packs", "title": "مواسم مسلسلات اجنبية", "url": self.getFullUrl("/category/foreign-series-3/packs/")}, + {"category": "series_packs", "title": "مواسم مسلسلات اجنبية", "url": self.getFullUrl("/category/foreign-series-7/packs/")}, {"category": "series_packs", "title": "مواسم مسلسلات تركية", "url": self.getFullUrl("/category/turkish-series-2/packs/")}, {"category": "series_packs", "title": "مواسم مسلسلات هندية", "url": self.getFullUrl("/category/مسلسلات-هندية/packs/")}, {"category": "series_packs", "title": "مواسم مسلسلات كورية", "url": self.getFullUrl("/category/مسلسلات-كوريه/packs/")}, - {"category": "series_packs", "title": "مواسم مسلسلات مدبلجة", "url": self.getFullUrl("/category/مسلسلات-مدبلجة/packs/")}, - {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2026", "url": self.getFullUrl("/category/مسلسلات-رمضان/ramadan-series-2026/packs/")}, - {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2025", "url": self.getFullUrl("/category/مسلسلات-رمضان/ramadan-series-2025/packs/")}, - {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2024", "url": self.getFullUrl("/category/مسلسلات-رمضان/ramadan-series-2024/packs/")}, - {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2023", "url": self.getFullUrl("/category/مسلسلات-رمضان/ramadan-series-2023/packs/")}, - {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2022", "url": self.getFullUrl("/category/مسلسلات-رمضان/مسلسلات-رمضان-2022/packs/")}, - {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2021", "url": self.getFullUrl("/category/مسلسلات-رمضان/مسلسلات-رمضان-2021/packs/")}, - {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2020", "url": self.getFullUrl("/category/مسلسلات-رمضان/مسلسلات-رمضان-2020-hd/packs/")}, - {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2019", "url": self.getFullUrl("/category/مسلسلات-رمضان/مسلسلات-رمضان-2019/packs/")}, + {"category": "series_packs", "title": "مواسم مسلسلات مدبلجة", "url": self.getFullUrl("/category/dubbed-series/packs/")}, + {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2026", "url": self.getFullUrl("/category/مسلسلات-رمضان/ramadan-series-2026-1/packs/")}, + {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2025", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/ramadan-series-2025/packs/")}, + {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2024", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/ramadan-series-2024/packs/")}, + {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2023", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/ramadan-series-2023/packs/")}, + {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2022", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/مسلسلات-رمضان-2022/packs/")}, + {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2021", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/مسلسلات-رمضان-2021/packs/")}, + {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2020", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/مسلسلات-رمضان-2020-hd/packs/")}, + {"category": "series_packs", "title": "مواسم مسلسلات رمضان 2019", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/مسلسلات-رمضان-2019/packs/")}, {"category": "series_packs", "title": "مواسم برامج تليفزيونية", "url": self.getFullUrl("/category/برامج-تلفزيونية/packs/")}, {"category": "series_packs", "title": "مواسم مسلسلات كرتون", "url": self.getFullUrl("/category/cartoon-series/packs/")}, ] self.RAMADAN_CAT_TAB = [ - {"category": "series", "title": "مسلسلات رمضان 2026", "url": self.getFullUrl("/category/مسلسلات-رمضان/ramadan-series-2026/")}, - {"category": "series", "title": "مسلسلات رمضان 2025", "url": self.getFullUrl("/category/مسلسلات-رمضان/ramadan-series-2025/")}, - {"category": "series", "title": "مسلسلات رمضان 2024", "url": self.getFullUrl("/category/مسلسلات-رمضان/ramadan-series-2024/")}, - {"category": "series", "title": "مسلسلات رمضان 2023", "url": self.getFullUrl("/category/مسلسلات-رمضان/ramadan-series-2023/")}, - {"category": "series", "title": "مسلسلات رمضان 2022", "url": self.getFullUrl("/category/مسلسلات-رمضان/مسلسلات-رمضان-2022/")}, - {"category": "series", "title": "مسلسلات رمضان 2021", "url": self.getFullUrl("/category/مسلسلات-رمضان/مسلسلات-رمضان-2021/")}, - {"category": "series", "title": "مسلسلات رمضان 2020", "url": self.getFullUrl("/category/مسلسلات-رمضان/مسلسلات-رمضان-2020-hd/")}, - {"category": "series", "title": "مسلسلات رمضان 2019", "url": self.getFullUrl("/category/مسلسلات-رمضان/مسلسلات-رمضان-2019/")}, + {"category": "series", "title": "مسلسلات رمضان 2026", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/ramadan-series-2026/")}, + {"category": "series", "title": "مسلسلات رمضان 2025", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/ramadan-series-2025/")}, + {"category": "series", "title": "مسلسلات رمضان 2024", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/ramadan-series-2024/")}, + {"category": "series", "title": "مسلسلات رمضان 2023", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/ramadan-series-2023/")}, + {"category": "series", "title": "مسلسلات رمضان 2022", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/مسلسلات-رمضان-2022/")}, + {"category": "series", "title": "مسلسلات رمضان 2021", "url": self.getFullUrl("/category/مسل1لات-رمضان-1/مسلسلات-رمضان-2022/")}, + {"category": "series", "title": "مسلسلات رمضان 2020", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/مسلسلات-رمضان-2020-hd/")}, + {"category": "series", "title": "مسلسلات رمضان 2019", "url": self.getFullUrl("/category/مسلسلات-رمضان-1/مسلسلات-رمضان-2019/")}, ] self.ANIME_CAT_TAB = [ - {"category": "list_items", "title": "افلام انيميشن", "url": self.getFullUrl("/category/افلام-انيميشن/")}, + {"category": "list_items", "title": "افلام انيميشن", "url": self.getFullUrl("/category/animation-movies/")}, {"category": "series", "title": "مسلسلات كرتون", "url": self.getFullUrl("/category/cartoon-series/")}, ] self.OTHER_CAT_TAB = [{"category": "list_items", "title": "اغاني عربي", "url": self.getFullUrl("/category/اغاني-عربي/")}, {"category": "list_items", "title": "مصارعه", "url": self.getFullUrl("/category/wwe-shows-1/")}, {"category": "list_items", "title": "برامج تلفزيونية", "url": self.getFullUrl("/category/برامج-تلفزيونية/")}, {"category": "list_items", "title": "مسرحيات عربيه", "url": self.getFullUrl("/category/مسرحيات-عربي/")}] - # Display main categories self.listsTab(self.MAIN_CAT_TAB, cItem) def listMoviesFolder(self, cItem): @@ -229,6 +140,77 @@ def listOtherFolder(self, cItem): printDBG("ArabSeed.listOtherFolder") self.listsTab(self.OTHER_CAT_TAB, cItem) + def getLinksForVideo(self, cItem): + printDBG("ArabSeed.getLinksForVideo %s" % cItem) + url = cItem.get("url", "") + title = cItem.get("title", "") + need_resolve = cItem.get("need_resolve", 1) + common_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "DNT": "1"} + # ===== YouTube / IMDB Direct ===== + if "youtube.com" in url or "youtu.be" in url: + headers = {"User-Agent": common_headers["User-Agent"], "Referer": "https://www.youtube.com/"} + return [{"name": "YouTube", "url": strwithmeta(url, headers), "need_resolve": 1}] + if "imdb.com/video/" in url: + imdb_links = self.getIMDBTrailer(url) + if imdb_links: + return imdb_links + return [] + # ArabSeed Direct Server + if "reviewrate.net" in url and "/embed-" in url: + printDBG("ArabSeed: Parsing reviewrate embed page") + headers = dict(common_headers) + headers["Referer"] = "https://m.asd.ink/" + headers["Origin"] = "https://m.asd.ink" + sts, data = self.cm.getPage(url, {"header": headers}) + if sts and data: + patterns = [r"""https?://[^"' ]+\.m3u8[^"' ]*""", r"""https?://[^"' ]+\.mp4[^"' ]*""", r"""file\s*:\s*["']([^"']+)["']""", r"""source\s+src=["']([^"']+)["']"""] + found = [] + for pattern in patterns: + matches = re.findall(pattern, data, re.I) + for video_url in matches: + if not video_url: + continue + if not video_url.startswith("http"): + continue + if video_url in found: + continue + found.append(video_url) + printDBG("ArabSeed: Found video URL >>> %s" % video_url) + video_headers = {"User-Agent": common_headers["User-Agent"], "Referer": "https://m.asd.ink/", "Origin": "https://m.asd.ink"} + return [{"name": "سيرفر عرب سيد", "url": strwithmeta(video_url, video_headers), "need_resolve": 0}] + printDBG("ArabSeed: No direct link found, returning embed") + return [{"name": "سيرفر عرب سيد", "url": strwithmeta(url, headers), "need_resolve": 1}] + # External Servers + if need_resolve == 1: + printDBG("ArabSeed: Resolving external server: %s" % url) + domain_match = re.search(r"https?://([^/]+)", url) + stream_domain = domain_match.group(1) if domain_match else "" + dynamic_headers = {"User-Agent": common_headers["User-Agent"], "Referer": "https://%s/" % stream_domain if stream_domain else url, "Origin": "https://%s" % stream_domain if stream_domain else ""} + try: + linksTab = self.up.getVideoLinkExt(url) + if linksTab and isinstance(linksTab, list): + fixed_links = [] + for link in linksTab: + if not isinstance(link, dict): + link = {"url": link, "name": title} + fixed_link = dict(link) + video_url = fixed_link.get("url", "") + if video_url: + fixed_link["url"] = strwithmeta(video_url, headers) + fixed_links.append(fixed_link) + printDBG("ArabSeed: External server resolved OK") + return fixed_links + except Exception as e: + printDBG("ArabSeed.getVideoLinkExt error: %s" % str(e)) + return [{"name": title, "url": strwithmeta(url, dynamic_headers), "need_resolve": 1}] + # Direct + return [{"name": title, "url": strwithmeta(url, common_headers), "need_resolve": 0}] + + def getVideoLinks(self, url): + printDBG("ArabSeed.getVideoLinks [%s]" % url) + if self.cm.isValidUrl(url): + return self.up.getVideoLinkExt(url) + def listItems(self, cItem): printDBG("ArabSeed.listItems [%s]" % cItem) sts, data = self.getPage(cItem["url"]) @@ -237,31 +219,21 @@ def listItems(self, cItem): data_items = re.findall(r']*class="[^"]*box__xs__2[^"]*"[^>]*>(.*?)', data, re.S) printDBG("Items found: %s" % len(data_items)) for m in data_items: - # Extract basic info - title = self.cm.ph.getSearchGroups(m, r'title=[\'"]([^\'"]+)[\'"]')[0] - pureurl = self.cm.ph.getSearchGroups(m, r'href=[\'"]([^\'"]+)[\'"]')[0] - pureicon = self.cm.ph.getSearchGroups(m, r'data-src=[\'"]([^\'"]+)[\'"]')[0] - # Fix URLs safely - if pureurl: + title = self.cm.ph.getSearchGroups(m, r'title=[\'"]([^\'"]+)[\'"]')[0] or "" + pureurl = self.cm.ph.getSearchGroups(m, r'href=[\'"]([^\'"]+)[\'"]')[0] or "" + pureicon = self.cm.ph.getSearchGroups(m, r'data-src=[\'"]([^\'"]+)[\'"]')[0] or "" + url = "" + if pureurl and "/" in pureurl: baseurl, filenameurl = pureurl.rsplit("/", 1) - fixedfilenameurl = urllib_quote_plus(filenameurl) - url = baseurl + "/" + fixedfilenameurl + "watch/" - else: - url = "" - if pureicon: + url = baseurl + "/" + urllib_quote_plus(filenameurl) + "watch/" + icon = "" + if pureicon and "/" in pureicon: baseicon, filenameicon = pureicon.rsplit("/", 1) - fixedfilenameicon = urllib_quote_plus(filenameicon) - icon = baseicon + "/" + fixedfilenameicon - else: - icon = "" - ################################################### - # Extract genre, quality, and story - ################################################### + icon = baseicon + "/" + urllib_quote_plus(filenameicon) genre = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
', "
", False)[1]).strip() quality = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
', "
", False)[1]).strip() Ratings = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
', "
", False)[1]).strip() story = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, "

", "

", False)[1]).strip() - # # Build combined description line1_parts = [] line2_parts = [] if genre: @@ -274,7 +246,7 @@ def listItems(self, cItem): q_color = "orange" elif re.search(r"CAM|TS|HDCAM", quality, re.I): q_color = "red" - line1_parts.append(f"{E2ColoR('yellow')}Quality:{E2ColoR('white')} " f"{E2ColoR(q_color)}{quality}{E2ColoR('white')}") + line1_parts.append(f"{E2ColoR('yellow')}Quality:{E2ColoR('white')} {E2ColoR(q_color)}{quality}{E2ColoR('white')}") if Ratings: rate_match = re.search(r"(\d+(\.\d+)?)", Ratings) rate_color = "white" @@ -286,22 +258,16 @@ def listItems(self, cItem): rate_color = "orange" else: rate_color = "red" - line1_parts.append(f"{E2ColoR('yellow')}Ratings:{E2ColoR('white')} " f"{E2ColoR(rate_color)}{Ratings}{E2ColoR('white')}") + line1_parts.append(f"{E2ColoR('yellow')}Ratings:{E2ColoR('white')} {E2ColoR(rate_color)}{Ratings}{E2ColoR('white')}") if story: line2_parts.append(f"{E2ColoR('yellow')}Story:{E2ColoR('white')} {story}") desc = " | ".join(line1_parts) if line2_parts: desc += "\n" + " ".join(line2_parts) - ################################################### - # Colorize title (movie name + year) - ################################################### - colored_title = self.colorizeTitle(title) - ################################################### - # Final item - ################################################### + clean_title = self.clean_title_prefix(title, sub_mode=0, url=cItem.get("url", "")) + colored_title = self.colorizeTitle(clean_title) params = {"category": "explore_item", "title": colored_title, "icon": icon, "url": url, "desc": desc} self.addDir(params) - # === PAGINATION HANDLING === pagination = self.cm.ph.getDataBeetwenMarkers(data, '
', "
", False)[1] next_page = self.cm.ph.getSearchGroups(pagination, r']+class="next page-numbers"[^>]+href="([^"]+)"')[0] if next_page: @@ -339,9 +305,6 @@ def listSeriesItems(self, cItem): icon = baseicon + "/" + fixedfilenameicon else: icon = "" - ################################################### - # Extract genre, quality, and story - ################################################### genre = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
', "
", False)[1]).strip() quality = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
', "
", False)[1]).strip() Ratings = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
', "
", False)[1]).strip() @@ -378,17 +341,11 @@ def listSeriesItems(self, cItem): desc = line1 if line2: desc += "\n" + line2 - ################################################### - # Colorize title (movie name + year) - ################################################### - colored_title = self.colorizeTitle(title) - ################################################### - # Final item - ################################################### + clean_title = self.clean_title_prefix(title, sub_mode=1, url=cItem.get("url", "")) + colored_title = self.colorizeTitle(clean_title) params = {"category": "explore_item", "title": colored_title, "icon": icon, "url": url, "desc": desc} printDBG(str(params)) self.addDir(params) - # === PAGINATION HANDLING === pagination = self.cm.ph.getDataBeetwenMarkers(data, '
', "
", False)[1] next_page = self.cm.ph.getSearchGroups(pagination, r']+class="next page-numbers"[^>]+href="([^"]+)"')[0] if next_page: @@ -406,71 +363,113 @@ def listSeriesItems(self, cItem): def exploreItems(self, cItem): printDBG("ArabSeed.exploreItems >>> %s" % cItem) + import time + url = cItem.get("url") sts, data = self.cm.getPage(url) if not sts or not data: return - videos = [] - processed_urls = [] - csrf_token = self.cm.ph.getSearchGroups(data, r"['\s]csrf__token['\s:]+['\"]([^'\"]+)")[0] - post_id = self.cm.ph.getSearchGroups(data, r"psot_id['\s:]+['\"](\d+)")[0] or self.cm.ph.getSearchGroups(data, r"post_id['\s:]+['\"](\d+)")[0] - if csrf_token and post_id: - qualities = ["1080", "720", "480"] - for qu in qualities: - for server_id in range(0, 6): - ajax_url = "https://asd.pics/get__watch__server/" - post_data = {"post_id": post_id, "quality": qu, "server": str(server_id), "csrf_token": csrf_token} - ajax_headers = {"X-Requested-With": "XMLHttpRequest", "Referer": url, "User-Agent": "Mozilla/5.0", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"} - sts_ajax, json_data = self.cm.getPage(ajax_url, {"header": ajax_headers}, post_data) - if not sts_ajax: - continue - try: - res = json.loads(json_data) - if res.get("type") == "success" and res.get("server"): - video_url = res["server"] - if "url=" in video_url or "id=" in video_url: - b64_match = re.search(r"(?:url=|id=)([A-Za-z0-9+/=]+)", video_url) - if b64_match: - b64_str = b64_match.group(1) - b64_str += "=" * ((4 - len(b64_str) % 4) % 4) - video_url = base64.b64decode(b64_str).decode("utf-8") - if video_url not in processed_urls: - domain = re.search(r"https?://([^/]+)", video_url) - domain_name = domain.group(1) if domain else "" - is_arabseed = server_id == 0 or "reviewrate" in domain_name - s_name = "سيرفر عرب سيد" if is_arabseed else ("سيرفر %s (%s)" % (server_id, domain_name)) - videos.append({"quality": qu, "server": s_name, "url": video_url, "is_arabseed": is_arabseed, "need_resolve": 0 if is_arabseed else 1}) - processed_urls.append(video_url) - except: - continue - - def sort_key(v): - q_val = int(v["quality"]) - priority = 0 if v["is_arabseed"] else 1 - quality_order = -q_val - return (priority, quality_order) - - videos.sort(key=sort_key) - for v in videos: - clean_title = "[%sP] - %s" % (v["quality"], v["server"]) - title = clean_title - if hasattr(self, "colorizeServer"): - title = self.colorizeServer(v["server"], v["quality"]) - self.addVideo({"title": title, "url": v["url"], "type": "video", "need_resolve": v["need_resolve"]}) - printDBG("ArabSeed.exploreItems <<< done") + post_id = self.cm.ph.getSearchGroups(data, r'post_id["\']?\s*[:=]\s*["\']?(\d+)')[0] or self.cm.ph.getSearchGroups(data, r'psot_id["\']?\s*[:=]\s*["\']?(\d+)')[0] + csrf_token = self.cm.ph.getSearchGroups(data, r'csrf__token["\']?\s*[:=]\s*["\']([^"\']+)')[0] + if not post_id or not csrf_token: + printDBG("Missing post_id or csrf_token") + return + printDBG("post_id: %s, csrf_token: %s" % (post_id, csrf_token)) + available_qualities = ["1080", "720", "480"] + qual_block = self.cm.ph.getDataBeetwenMarkers(data, "quality__options", "
", False)[1] + if qual_block: + quals = re.findall(r'data-qu="(\d+)"', qual_block) + if quals: + available_qualities = sorted(list(set(quals)), reverse=True) + ajax_url = self.MAIN_URL.rstrip("/") + "/get__watch__server/" + referer = url + results = [] + + def fetch_server_link(quality, srv_idx): + try: + post_data = {"post_id": post_id, "quality": quality, "server": str(srv_idx), "csrf_token": csrf_token} + headers = {"X-Requested-With": "XMLHttpRequest", "Referer": referer, "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "User-Agent": self.HEADER.get("User-Agent", "Mozilla/5.0")} + params = dict(self.defaultParams) + params["header"] = headers + params["timeout"] = 3 + params["connect_timeout"] = 1 + sts, response = self.cm.getPage(ajax_url, params, post_data) + if sts and response and response.strip(): + res = json_loads(response.strip()) + if res.get("type") == "success" and res.get("server"): + video_url = res["server"] + enc_match = re.search(r"(?:url=|id=)([A-Za-z0-9+/=]+)", video_url) + if enc_match: + try: + encoded = enc_match.group(1) + padding = 4 - len(encoded) % 4 + if padding != 4: + encoded += "=" * padding + video_url = base64.b64decode(encoded).decode("utf-8") + except: + pass + if video_url and video_url.startswith("http"): + domain = re.search(r"https?://([^/]+)", video_url) + domain = domain.group(1) if domain else "unknown" + if srv_idx == 0: + server_name = "سيرفر عرب سيد" + else: + server_name = f"سيرفر {srv_idx} - {domain.capitalize()}" + server_colored = f"{E2ColoR('cyan')}{server_name}{E2ColoR('white')}" + quality_colored = self.colorizeQuality(quality) + label = f"{server_colored} [{quality_colored}]" + sort_value = 10000 + int(quality) * 10 if srv_idx == 0 else int(quality) * 10 + (5 - srv_idx) + return {"name": label, "url": video_url, "need_resolve": 1, "sort": sort_value, "header": {"Referer": "https://" + domain + "/", "User-Agent": self.HEADER.get("User-Agent", "Mozilla/5.0"), "Origin": "https://" + domain}} + except Exception as e: + printDBG("fetch_server_link error: %s" % str(e)) + return None + + for quality in available_qualities: + result = fetch_server_link(quality, 0) + if result: + results.append(result) + printDBG("Added: %s" % result["name"]) + for srv_idx in range(1, 6): + for quality in available_qualities: + result = fetch_server_link(quality, srv_idx) + if result: + results.append(result) + printDBG("Added: %s" % result["name"]) + time.sleep(0.05) + results.sort(key=lambda x: x.get("sort", 0), reverse=True) + for item in results: + item.pop("sort", None) + original_title = cItem.get("title", "عنوان غير متاح") + plain_title = re.sub(r"\\c00[0-9A-F]{6}", "", original_title) + clean_title = self.clean_title_prefix(plain_title, sub_mode=-1, url=cItem.get("url", "")) + colored_clean_title = self.colorizeTitle(clean_title) + final_title = f"{colored_clean_title} {E2ColoR('white')}| {item['name']}" + self.addVideo({"title": final_title, "url": item["url"], "type": "video", "need_resolve": item["need_resolve"], "header": item.get("header", {})}) + printDBG("ArabSeed.exploreItems <<< done - Found %d links" % len(results)) + + def safe_b64decode_urlsafe(self, data): + """Base64 decode with automatic padding fix and URL-safe characters.""" + if not data: + return None + data = data.replace("-", "+").replace("_", "/") + padding = 4 - len(data) % 4 + if padding != 4: + data += "=" * padding + try: + return base64.b64decode(data).decode("utf-8", errors="ignore") + except: + return None def exploreSeriesItems(self, cItem): printDBG("ArabSeed.exploreSeriesItems >>> %s" % cItem) url = cItem.get("url") if not url: return - # --- Load the episode page --- sts, data = self.getPage(url) if not sts or not data: printDBG("[ArabSeed] Failed to load episode page: %s" % url) return - # --- Extract token and post_id --- def extract_first(patterns, data_src): for p in patterns: try: @@ -486,11 +485,9 @@ def extract_first(patterns, data_src): if not token or not post_id: printDBG("[ArabSeed] Missing required POST params (csrf_token or post_id/psot_id)") return - # --- Constants --- - post_url = "https://asd.pics/get__watch__server/" + post_url = "https://m.asd.ink/get__watch__server/" servers = [0, 1, 2, 3, 4] qualities = [480, 720, 1080] - # --- Loop through servers and qualities --- for server in servers: for quality in qualities: payload = {"post_id": post_id, "quality": str(quality), "server": str(server), "csrf_token": token} @@ -508,13 +505,11 @@ def extract_first(patterns, data_src): link = result.get("server", "") if not link: continue - # --- Normalize server name --- server_name = self.cm.ph.getSearchGroups(link, r"https?://([^/]+)/")[0] if server_name in ["m.reviewrate.net", "m.reviewtech.me"]: server_name = "ArabSeed" if not server_name: server_name = "server%d" % server - # --- Add video entry --- colored_server = self.colorizeServer(server_name, quality) colored_title = self.colorizeTitle(cItem.get("title", "Episode")) params_video = MergeDicts( @@ -529,11 +524,6 @@ def extract_first(patterns, data_src): ) self.addVideo(params_video) - def safe_b64decode(self, data): - """Base64 decode with automatic padding fix.""" - data += "=" * (-len(data) % 4) - return base64.b64decode(data).decode("utf-8") - def listSearchResult(self, cItem, searchPattern, searchType): printDBG("ArabSeed.listSearchResult cItem[%s], searchPattern[%s] searchType[%s]" % (cItem, searchPattern, searchType)) page = cItem.get("page", 1) @@ -553,11 +543,9 @@ def listSearchItems(self, cItem): data_items = self.cm.ph.getAllItemsBeetwenMarkers(tmp, '
  • ") printDBG("data_items.listSearchItems [%s]" % data_items) for m in data_items: - # Extract basic info title = self.cm.ph.getSearchGroups(m, r'title=[\'"]([^\'"]+)[\'"]')[0] pureurl = self.cm.ph.getSearchGroups(m, r'href=[\'"]([^\'"]+)[\'"]')[0] pureicon = self.cm.ph.getSearchGroups(m, r'data-src=[\'"]([^\'"]+)[\'"]')[0] - # Fix URLs safely if pureurl: baseurl, filenameurl = pureurl.rsplit("/", 1) fixedfilenameurl = urllib_quote_plus(filenameurl) @@ -570,14 +558,10 @@ def listSearchItems(self, cItem): icon = baseicon + "/" + fixedfilenameicon else: icon = "" - ################################################### - # Extract genre, quality, and story - ################################################### genre = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
    ', "
    ", False)[1]).strip() quality = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
    ', "
    ", False)[1]).strip() section = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '", False)[1]).strip() story = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, "

    ", "

    ", False)[1]).strip() - # Build combined description line1_parts = [] line2_parts = [] if genre: @@ -598,13 +582,8 @@ def listSearchItems(self, cItem): desc = " | ".join(line1_parts) if line2_parts: desc += "\n" + " ".join(line2_parts) - ################################################### - # Colorize title (movie name + year) - ################################################### - colored_title = self.colorizeTitle(title) - ################################################### - # Final item - ################################################### + clean_title = self.clean_title_prefix(title, sub_mode=-1, url=cItem.get("url", "")) + colored_title = self.colorizeTitle(clean_title) params = {"category": "explore_item", "title": colored_title, "icon": icon, "url": url, "desc": desc} printDBG(str(params)) self.addDir(params) @@ -643,119 +622,77 @@ def setInitListFromFavouriteItem(self, fav_data): printExc() return cItem - ################################################### - # SERIES PACKS FLOW - # series_packs_folder → listSeriesPacks() - # series_seasons_list → listSeasons() - # series_episodes_list → listEpisodes() - # explore_episodes → exploreSeriesItems() - ################################################### def listSeriesPacks(self, cItem): printDBG("ArabSeed.listSeriesPacks >>> %s" % cItem) url = cItem.get("url", "").strip() if not url: return - # Step 1: Load the page directly (GET request) sts, data = self.getPage(url) if not sts or not data: printDBG("[ArabSeed] Failed to load packs page: %s" % url) return - # Step 2: Extract csrf__token from the page (for pagination) token = self.cm.ph.getSearchGroups(data, r"csrf__token['\"]:\s*[\"']([^\"']+)")[0] - printDBG("listSeriesPacks token >>> %s" % token) if not token: - printDBG("[ArabSeed] No csrf__token found!") - # Step 3: Parse the page content directly - start_marker = '
    ' - end_marker = '
    ', "
    ", False)[1] + if not ajax_area: + ajax_area = data + items = re.findall(r'(
  • ([^<]+)')[0].strip() + link_tag = self.cm.ph.getDataBeetwenMarkers(item, '", False)[1] + href = self.cm.ph.getSearchGroups(link_tag, r'^([^"]+)')[0].strip() if link_tag else "" + title = self.cm.ph.getSearchGroups(item, r'title="([^"]+)"')[0].strip() + if not title: + title = self.cm.ph.getSearchGroups(item, r'
    ]*>([^<]+)
    ')[0].strip() icon = self.cm.ph.getSearchGroups(item, r'data-src="([^"]+)"')[0].strip() - href = quote(href, safe=":/?&=%") - icon = quote(icon, safe=":/?&=%") + if not icon: + icon = self.cm.ph.getSearchGroups(item, r'src="([^"]+)"')[0].strip() if not href or not title: continue - colored_title = self.colorizeTitle(title) - # ===== DESCRIPTION (FIXED & ROBUST) ===== - year = country = genre = quality = story = "" + href = quote(href, safe=":/?&=%") + icon = quote(icon, safe=":/?&=%") if icon else self.DEFAULT_ICON_URL + bottom_ul = self.cm.ph.getDataBeetwenMarkers(item, '
      ', "
    ", False)[1] + bottom_items = self.cm.ph.getAllItemsBeetwenMarkers(bottom_ul, "
  • ", "
  • ") if bottom_ul else [] + quality = self.cleanHtmlStr(bottom_items[0]) if len(bottom_items) > 0 else "" + genre = self.cleanHtmlStr(bottom_items[1]) if len(bottom_items) > 1 else "" + dots_info = self.cm.ph.getDataBeetwenMarkers(item, '
      ', "
    ", False)[1] + dot_spans = self.cm.ph.getAllItemsBeetwenMarkers(dots_info, "", "") if dots_info else [] + year = self.cleanHtmlStr(dot_spans[0]) if len(dot_spans) > 0 else "" + country = self.cleanHtmlStr(dot_spans[1]) if len(dot_spans) > 1 else "" + story = self.cm.ph.getSearchGroups(item, r'

    ([^<]+)

    ')[0].strip() desc_parts = [] - # ---------- STORY ---------- - story = self.cm.ph.getDataBeetwenMarkers(item, '

    ', "

    ", False)[1].strip() - # ---------- HOVER BOX ---------- - hover_box = self.cm.ph.getDataBeetwenMarkers(item, '
    ", False)[1] - # ---------- DOTS INFO ---------- - dots_info = self.cm.ph.getDataBeetwenMarkers(hover_box, '
      ', "
    ", False)[1] - if dots_info: - spans = self.cm.ph.getAllItemsBeetwenMarkers(dots_info, "", "") - if len(spans) > 0: - year = self.cleanHtmlStr(spans[0]) - if len(spans) > 1: - country = self.cleanHtmlStr(spans[1]) - # ---------- QUALITY / GENRE ---------- - bottom_ul = self.cm.ph.getDataBeetwenMarkers(hover_box, '
      ', "
    ", False)[1] - if bottom_ul: - li_items = self.cm.ph.getAllItemsBeetwenMarkers(bottom_ul, "
  • ", "
  • ") - if len(li_items) > 0: - quality = self.cleanHtmlStr(li_items[0]) - if len(li_items) > 1: - genre = self.cleanHtmlStr(li_items[1]) - # ---------- QUALITY COLOR ---------- - q_color = "white" - if quality: - if re.search(r"4K|2160|1080|BluRay|FHD", quality, re.I): - q_color = "green" - elif re.search(r"720|WEB|HDRip|HD", quality, re.I): - q_color = "orange" - elif re.search(r"CAM|TS|HDCAM|SD", quality, re.I): - q_color = "red" - # ---------- BUILD DESC ---------- if quality: - desc_parts.append(f"{E2ColoR('yellow')}Quality : {E2ColoR(q_color)}{quality}{E2ColoR('white')}") + q_color = "green" if re.search(r"4K|1080|BluRay|FHD", quality, re.I) else "orange" if re.search(r"720|WEB|HDRip", quality, re.I) else "white" + desc_parts.append(f"{E2ColoR('yellow')}Quality:{E2ColoR('white')} {E2ColoR(q_color)}{quality}{E2ColoR('white')}") if genre: - desc_parts.append(f"{E2ColoR('yellow')}Genre : {E2ColoR('white')}{genre}") + desc_parts.append(f"{E2ColoR('yellow')}Genre:{E2ColoR('white')} {genre}") if year: - desc_parts.append(f"{E2ColoR('yellow')}Year : {E2ColoR('white')}{year}") + desc_parts.append(f"{E2ColoR('yellow')}Year:{E2ColoR('white')} {year}") if country: - desc_parts.append(f"{E2ColoR('yellow')}Country : {E2ColoR('white')}{country}") + desc_parts.append(f"{E2ColoR('yellow')}Country:{E2ColoR('white')} {country}") desc = " | ".join(desc_parts) if story: - desc += f"\n{E2ColoR('yellow')}Story : {E2ColoR('white')}{story}" - printDBG("ICON URL >>> %s" % icon) + desc += f"\n{E2ColoR('yellow')}Story:{E2ColoR('white')} {story[:200]}{'...' if len(story) > 200 else ''}" + clean_title = self.clean_title_prefix(title, sub_mode=2, url=cItem.get("url", "")) + colored_title = self.colorizeTitle(clean_title) params = dict(cItem) params.update({"category": "series_seasons_list", "title": colored_title, "url": href, "icon": icon, "desc": desc, "csrf_token": token}) - printDBG(f"Description for '{colored_title}': {desc}") self.addDir(params) - # Step 5: Pagination - pagination_match = re.search(r"
      (.*?)
    ", data, re.DOTALL) - if pagination_match: - pagination_html = pagination_match.group(1) - printDBG("pagination_html >>> %s" % pagination_html) - next_page_match = re.search(r']+class="next page-numbers"[^>]+href="([^"]+)"', pagination_html) - if next_page_match: - next_page = next_page_match.group(1).strip() - next_page = next_page.replace(" ", "") - if next_page.startswith("http://") or next_page.startswith("https://"): - parsed = re.search(r"https?://[^/]+(/.*)", next_page) - if parsed: - next_page = parsed.group(1) - else: - next_page = "/" + next_page.split("/", 3)[-1] if len(next_page.split("/")) > 3 else next_page - next_page = next_page.replace("/packs/packs/", "/packs/") - if not next_page.startswith("/"): - next_page = "/" + next_page - next_page_url = self.MAIN_URL.rstrip("/") + next_page - printDBG("Built next_page_url >>> %s" % next_page_url) - params = dict(cItem) - params.update({"title": _("Next Page") + " ▶", "url": next_page_url, "category": "series_packs"}) - self.addDir(params) + printDBG(f"✓ Added: {title[:50]}...") + next_match = re.search(r']+class="next page-numbers"[^>]+href="([^"]+)"', data) + if next_match: + next_page = next_match.group(1).strip() + if "arabseed.show" in next_page: + next_page = next_page.replace("arabseed.show", "asd.ink") + if next_page.startswith("//"): + next_page = "https:" + next_page + elif next_page.startswith("/"): + next_page = self.MAIN_URL.rstrip("/") + next_page + params = dict(cItem) + params.update({"title": _("Next Page ▶"), "url": next_page, "category": "series_packs"}) + self.addDir(params) + printDBG("Next page: %s" % next_page) printDBG("ArabSeed.listSeriesPacks <<< done") def listSeasons(self, cItem): @@ -765,28 +702,22 @@ def listSeasons(self, cItem): if not url or not csrf_token: printDBG("[ArabSeed] Missing params in listSeasons") return - # Step 1: Load series page sts, data = self.getPage(url) if not sts or not data: printDBG("[ArabSeed] Failed to load series page") return - # --- Trailer --- trailer_url = self.cm.ph.getSearchGroups(data, r'data-iframe="([^"]+)"')[0] if trailer_url: - # YouTube if "youtube.com/embed/" in trailer_url: trailer_url = trailer_url.replace("youtube.com/embed/", "youtube.com/watch?v=") - # IMDb elif "imdb.com/video/" in trailer_url: pass params = dict(cItem) params.update({"title": f"{E2ColoR('lime')}TRAILER{E2ColoR('white')}", "url": trailer_url, "type": "video", "need_resolve": 1}) self.addVideo(params) printDBG("[ArabSeed] trailer_url = %s" % trailer_url) - # Step 2: Try to get seasons list seasons_block = self.cm.ph.getDataBeetwenMarkers(data, 'id="seasons__list"', "
    ", False)[1] if seasons_block: - # Seasons exist season_items = self.cm.ph.getAllItemsBeetwenMarkers(seasons_block, "") for s in season_items: season_id = self.cm.ph.getSearchGroups(s, r'data-term="([^"]+)"')[0] @@ -798,7 +729,6 @@ def listSeasons(self, cItem): self.addDir(params) printDBG("ArabSeed.listSeasons <<< done with seasons") else: - # No seasons → try direct episodes printDBG("[ArabSeed] No seasons found → trying direct episodes") episodes_block = self.cm.ph.getDataBeetwenMarkers(data, '
      ", False)[1] if episodes_block: @@ -815,9 +745,6 @@ def listSeasons(self, cItem): self.addDir(params) printDBG("ArabSeed.listSeasons <<< done with direct episodes") return - # ================================================== - # STEP EXTRA: single promo / one episode only - # ================================================== promo_url = self.cm.ph.getSearchGroups( data, r'
      > name[%s], category[%s] " % (name, category)) self.currList = [] - # MAIN MENU if name is None: self.listMainMenu({"name": "category"}) elif category == "list_items": self.listItems(self.currItem) elif category == "series": self.listSeriesItems(self.currItem) - # FOLDERS elif category == "movies_folder": self.listMoviesFolder(self.currItem) elif category == "series_folder": @@ -1017,12 +997,10 @@ def handleService(self, index, refresh=0, searchPattern="", searchType=""): self.listOtherFolder(self.currItem) elif category == "explore_item": self.exploreItems(self.currItem) - # SEARCH elif category in ["search", "search_next_page"]: cItem = dict(self.currItem) cItem.update({"search_item": False, "name": "category"}) self.listSearchResult(cItem, searchPattern, searchType) - # HISTORY SEARCH elif category == "search_history": self.listsHistory({"name": "history", "category": "search"}, "desc", _("Type: ")) else: diff --git a/IPTVPlayer/hosts/hostaradrama.py b/IPTVPlayer/hosts/hostaradrama.py new file mode 100644 index 00000000..54dccd44 --- /dev/null +++ b/IPTVPlayer/hosts/hostaradrama.py @@ -0,0 +1,655 @@ +# -*- coding: utf-8 -*- +# Last modified: 3/1/2026 +# Aradrama Host (Created By Dr HYTHAM MAHMOUD) +import re +from Components.config import ConfigSelection, ConfigText, config, getConfigListEntry + +# from Plugins.Extensions.IPTVPlayer.compat import urllib_quote_plus +from Plugins.Extensions.IPTVPlayer.p2p3.UrlLib import urllib_quote_plus +from Plugins.Extensions.IPTVPlayer.components.ihost import CBaseHostClass, CHostBase +from Plugins.Extensions.IPTVPlayer.components.iptvplayerinit import TranslateTXT as _ +from Plugins.Extensions.IPTVPlayer.tools.iptvtools import MergeDicts, printDBG, printExc +from Plugins.Extensions.IPTVPlayer.tools.iptvtypes import strwithmeta + +try: + from Plugins.Extensions.IPTVPlayer.tools.iptvtools import ParseColor +except Exception: + + def ParseColor(color, text): + return text + + +TRAILER_LABEL = "Trailer" +ALT_TITLE_REGEX = r'alt=[\'"]([^"^\']+?)[\'"]' +# -------------------- config -------------------- +config.plugins.iptvplayer.aradramtv_proxy = ConfigSelection(default="None", choices=[("None", _("None")), ("proxy_1", _("Alternative proxy server (1)")), ("proxy_2", _("Alternative proxy server (2)"))]) +config.plugins.iptvplayer.aradramtv_alt_domain = ConfigText(default="", fixed_size=False) + + +def GetConfigList(): + tab = [] + tab.append(getConfigListEntry(_("Use proxy server:"), config.plugins.iptvplayer.aradramtv_proxy)) + if config.plugins.iptvplayer.aradramtv_proxy.value == "None": + tab.append(getConfigListEntry(_("Alternative domain:"), config.plugins.iptvplayer.aradramtv_alt_domain)) + return tab + + +def gettytul(): + return "ARADrama" + + +class ARADrama(CBaseHostClass): + def __init__(self): + CBaseHostClass.__init__(self, {"history": "aradramtv", "cookie": "aradramtv.cookie"}) + self.MAIN_URL = None + self.DEFAULT_ICON_URL = "https://aradramatv.cc/wp-content/uploads/logo-v3.1.png" + self.HEADER = self.cm.getDefaultHeader("chrome") + self.HEADER.update( + { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "ar,en-US;q=0.9,en;q=0.8", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + } + ) + self.AJAX_HEADER = dict(self.HEADER) + self.AJAX_HEADER.update({"X-Requested-With": "XMLHttpRequest"}) + self.defaultParams = {"header": self.HEADER, "with_metadata": True, "use_cookie": True, "load_cookie": True, "save_cookie": True, "cookiefile": self.COOKIE_FILE} + + # -------------------- net -------------------- + def getProxy(self): + proxy = config.plugins.iptvplayer.aradramtv_proxy.value + if proxy != "None": + if proxy == "proxy_1": + return config.plugins.iptvplayer.alternative_proxy1.value + return config.plugins.iptvplayer.alternative_proxy2.value + return None + + def _withProxy(self, params): + proxy = self.getProxy() + if proxy: + params = MergeDicts(params, {"http_proxy": proxy}) + return params + + def _ensureReferer(self, params, url): + try: + hdr = params.get("header", {}) + if "Referer" not in hdr: + hdr = dict(hdr) + hdr["Referer"] = self.getMainUrl() or self.getFullUrl("/") + params["header"] = hdr + except Exception: + pass + return params + + def getPage(self, baseUrl, addParams=None, post_data=None): + params = dict(self.defaultParams) + if addParams: + try: + params.update(addParams) + except Exception: + pass + params = self._withProxy(params) + params = self._ensureReferer(params, baseUrl) + try: + if hasattr(self.cm, "getPageCFFProtection"): + return self.cm.getPageCFProtection(baseUrl, params, post_data) + except Exception: + pass + try: + if hasattr(self.cm, "getPageCFProtection"): + return self.cm.getPageCFProtection(baseUrl, params, post_data) + except Exception: + pass + return self.cm.getPage(baseUrl, params, post_data) + + def selectDomain(self): + domains = [ + "https://aradramatv.cc/", + "https://aradramatv.co/", + "https://aradramtv.com/", + "https://aradramatv.com/", + ] + alt = config.plugins.iptvplayer.aradramtv_alt_domain.value.strip() + if self.cm.isValidUrl(alt): + if not alt.endswith("/"): + alt += "/" + domains.insert(0, alt) + for d in domains: + sts, data = self.getPage(d) + if sts and data and ("Aradrama" in data or "wp-content" in data or "page-content" in data): + try: + self.setMainUrl(self.cm.meta.get("url", d)) + except Exception: + self.setMainUrl(d) + self.MAIN_URL = self.getMainUrl() + return + if self.MAIN_URL is None: + self.setMainUrl(domains[0]) + self.MAIN_URL = self.getMainUrl() + + # -------------------- text helpers -------------------- + def _cleanTitle(self, s): + if not s: + return "" + s = s.strip() + if (">" in s) and (s.startswith("class=") or s.startswith("id=") or s.startswith("style=") or s.startswith("data-")): + s = s.split(">", 1)[-1] + s = self.cleanHtmlStr(s).strip() + s = re.sub(r"\s+", " ", s).strip() + return s + + def _normalizeTitle(self, s): + s = self._cleanTitle(s) + if not s: + return "" + s = s.replace("<", "").replace(">", "").replace("‹", "").replace("›", "").replace("«", "").replace("»", "") + s = re.sub(r"[\u200e\u200f\u202a-\u202e\u2066-\u2069]", "", s) + s = re.sub(r"\b[0-9٠-٩]{5,}\b", "", s) # remove long numeric ids + s = re.sub(r"\s*-\s*", " - ", s) + s = re.sub(r"\s+", " ", s).strip() + s = re.sub(r"^\s*-\s*", "", s).strip() + s = re.sub(r"\s*-\s*$", "", s).strip() + return s + + # ---------- RTL fix (Isolates + manual wrap) ---------- + def _stripBidi(self, s): + if not s: + return "" + return re.sub(r"[\u200e\u200f\u202a-\u202e\u2066-\u2069]", "", s) + + def _wrapTextBlock(self, text, width=74): + if not text: + return "" + out = [] + for ln in text.splitlines(): + ln = ln.strip() + if not ln: + out.append("") + continue + words = ln.split() + cur = "" + for w in words: + if not cur: + cur = w + elif len(cur) + 1 + len(w) <= width: + cur += " " + w + else: + out.append(cur) + cur = w + if cur: + out.append(cur) + return "\n".join(out) + + def _stripLeadingDashesPerLine(self, text): + """ + ✅ إزالة أي شرطة/داش في بداية كل سطر بعد اللفّ. + يشمل: - – — ـ + """ + if not text: + return "" + fixed = [] + for ln in text.splitlines(): + # remove leading dashes/em-dashes/tatweel and spaces + ln = re.sub(r"^\s*([\-–—ـ]+)\s*", "", ln) + fixed.append(ln) + return "\n".join(fixed) + + def _isolateLTRRuns(self, s): + if not s: + return "" + LRI = "\u2066" + RLI = "\u2067" + PDI = "\u2069" + + def repl(m): + t = m.group(0) + return LRI + t + PDI + + s = re.sub(r"[A-Za-z0-9][A-Za-z0-9\-\._:/ ]*", repl, s) + s = RLI + s + PDI + return s + + def _forceRTLText(self, text): + if not text: + return "" + text = self._stripBidi(text) + lines = [] + for ln in text.splitlines(): + ln = ln.strip() + if not ln: + lines.append("") + continue + ln = "\u200f" + self._isolateLTRRuns(ln) + lines.append(ln) + return "\n".join(lines) + + # -------------------- extract helpers -------------------- + def _extractTrailerUrl(self, data): + block = "" + for marker in ("tab-trailer-1", "tab-trailer", "trailer"): + tmp = self.cm.ph.getDataBeetwenMarkers(data, marker, "" + break + if not block: + return "" + src = self.cm.ph.getSearchGroups(block, r']+src=[\'"]([^\'"]+)[\'"]')[0].strip() + if not src: + return "" + if not self.cm.isValidUrl(src): + src = self.getFullUrl(src) + low = src.lower() + if any(low.endswith(ext) for ext in (".jpg", ".jpeg", ".png", ".webp", ".gif")): + return "" + return src + + def _hasServersOnPage(self, data): + if "data-url=" in data: + return True + tmp = self.cm.ph.getDataBeetwenMarkers(data, "Servrs", "]+property=[\'"]og:image[\'"][^>]+content=[\'"]([^\'"]+)[\'"]')[0] + if not img: + img = self.cm.ph.getSearchGroups(data, r']+name=[\'"]twitter:image[\'"][^>]+content=[\'"]([^\'"]+)[\'"]')[0] + if img and (not self.cm.isValidUrl(img)): + img = self.getFullUrl(img) + return img + + def _getStory(self, data): + descBlock = self.cm.ph.getDataBeetwenMarkers(data, "b_block s-desc", "]+name=[\'"]description[\'"][^>]+content=[\'"]([^\'"]+)[\'"]')[0] + metaDesc = self._cleanTitle(metaDesc) + if metaDesc: + if ("البرنامج" in metaDesc and "+" in metaDesc) or ("جميع الحلقات" in metaDesc): + return "" + return metaDesc + p = self.cm.ph.getDataBeetwenMarkers(data, "", False)[1] + return self._cleanTitle(p) + + def getFullIconUrl(self, url): + iconUrl = CBaseHostClass.getFullIconUrl(self, (url or "").strip()) + if not iconUrl: + return "" + proxy = self.getProxy() + if proxy: + iconUrl = strwithmeta(iconUrl, {"iptv_http_proxy": proxy}) + return iconUrl + + # -------------------- INFO parse + format -------------------- + def _getDescHtml(self, data): + for a, b in [ + ("b_block s-desc", "', "
      ", False)[1] + nextUrl = "" + if pagination: + items = self.cm.ph.getAllItemsBeetwenMarkers(pagination, "", withMarkers=True) + for a in items: + href = self.cm.ph.getSearchGroups(a, r'href=[\'"]([^\'"]+?)[\'"]')[0] + num = self.cleanHtmlStr(a) + if (num.isdigit() and int(num) == page + 1) or ("»" in num and page == 1): + nextUrl = self.getFullUrl(href) + break + main = self.cm.ph.getDataBeetwenMarkers(data, '
      ', "", withMarkers=True) + for item in posts: + img = self.cm.ph.getSearchGroups(item, r']+src=[\'"]([^\'"]+?)[\'"]')[0] + icon = self.getFullIconUrl(img) + href = self.cm.ph.getSearchGroups(item, r']+class=[\'"]first_A[\'"][^>]+href=[\'"]([^\'"]+?)[\'"]')[0] + if not href: + href = self.cm.ph.getSearchGroups(item, r']+href=[\'"]([^\'"]+?)[\'"]')[0] + url = self.getFullUrl(href) + title = self.cm.ph.getSearchGroups(item, r"]*>\s*]*>(.*?)\s*")[0] + if not title: + title = self.cm.ph.getDataBeetwenMarkers(item, "", False)[1] + title = self._normalizeTitle(title) + if not title: + title = self._normalizeTitle(self.cm.ph.getSearchGroups(item, ALT_TITLE_REGEX)[0]) + if not url or not title: + continue + params = dict(cItem) + params.update({"category": nextCategory, "good_for_fav": True, "EPG": True, "title": title, "url": url, "icon": icon or self.DEFAULT_ICON_URL, "desc": ""}) + self.addDir(params) + if nextUrl: + params = dict(cItem) + params.update({"title": _("Next page"), "url": nextUrl, pageKey: page + 1}) + self.addDir(params) + + # -------------------- explore -------------------- + def exploreItems(self, cItem): + printDBG("ARADrama.exploreItems cItem[%s]" % cItem) + sts, data = self.getPage(cItem["url"]) + if not sts or not data: + return + cItem["prev_url"] = cItem["url"] + trailerUrl = self._extractTrailerUrl(data) + if self.cm.isValidUrl(trailerUrl): + params = dict(cItem) + params.update({"good_for_fav": False, "title": "[%s]" % (ParseColor("#6082b6", TRAILER_LABEL)), "url": trailerUrl, "desc": ""}) + self.addVideo(params) + story = self._getStory(data) + baseTitle = self._normalizeTitle(cItem.get("title", "")) + if self._hasServersOnPage(data): + params = dict(cItem) + params.update({"good_for_fav": True, "EPG": True, "title": baseTitle, "url": cItem["url"], "icon": cItem.get("icon", self.DEFAULT_ICON_URL), "desc": story}) + self.addVideo(params) + else: + playIframe = self._getPlayableIframe(data) + if self.cm.isValidUrl(playIframe): + params = dict(cItem) + params.update({"good_for_fav": True, "EPG": True, "title": "%s - %s" % (baseTitle, _("مشاهدة")), "url": playIframe, "icon": cItem.get("icon", self.DEFAULT_ICON_URL), "desc": story}) + self.addVideo(params) + tmp = self.cm.ph.getDataBeetwenMarkers(data, "vc_btn3-inline", "
      ", False)[1] + href = self.cm.ph.getSearchGroups(tmp, r'href=[\'"]([^\'"]+?)[\'"]')[0] + if not href: + return + if "?" in href: + url = self.getFullUrl("?" + href.split("?", 1)[1]) + else: + url = self.getFullUrl(href) + sts, data2 = self.getPage(url) + if not sts or not data2: + return + main = self.cm.ph.getDataBeetwenMarkers(data2, '
      ', "", withMarkers=True) + for item in posts: + epUrl = self.getFullUrl(self.cm.ph.getSearchGroups(item, r']+href=[\'"]([^\'"]+?)[\'"]')[0]) + epTitle = self.cm.ph.getSearchGroups(item, r"]*>\s*]*>(.*?)\s*")[0] + if not epTitle: + epTitle = self.cm.ph.getDataBeetwenMarkers(item, "", False)[1] + epTitle = self._normalizeTitle(epTitle) + if not epUrl or not epTitle: + continue + params = dict(cItem) + params.update({"good_for_fav": True, "EPG": True, "title": epTitle, "url": epUrl, "icon": cItem.get("icon", self.DEFAULT_ICON_URL), "desc": story}) + self.addVideo(params) + + # -------------------- search -------------------- + def listSearchResult(self, cItem, searchPattern, searchType): + printDBG("ARADrama.listSearchResult cItem[%s], searchPattern[%s] searchType[%s]" % (cItem, searchPattern, searchType)) + url = self.getFullUrl("/?s=%s" % urllib_quote_plus(searchPattern)) + params = {"name": "category", "good_for_fav": False, "url": url} + self.listItems(params, "explore_item") + + # -------------------- links -------------------- + def getLinksForVideo(self, cItem): + printDBG("ARADrama.getLinksForVideo [%s]" % cItem) + if TRAILER_LABEL in cItem.get("title", ""): + return self.up.getVideoLinkExt(cItem["url"]) + sts, data = self.getPage(cItem["url"]) + if not sts or not data: + if self.cm.isValidUrl(cItem.get("url", "")): + return self.up.getVideoLinkExt(cItem["url"]) + return [] + meta = {} + try: + meta = getattr(data, "meta", {}) or {} + except Exception: + meta = {} + referer = meta.get("url", cItem["url"]) + found = [] + tmp = self.cm.ph.getDataBeetwenMarkers(data, "Servrs", "
      ", False)[1] + servers = self.cm.ph.getAllItemsBeetwenMarkers(tmp, "", withMarkers=True) + for s in servers: + u = self.cm.ph.getSearchGroups(s, r'data-url=[\\\'"]([^"^\\\']+?)[\\\'"]')[0] + if u: + found.append(u) + if not found: + found = re.findall(r'data-url=[\'"]([^\'"]+)[\'"]', data, flags=re.I) + if not found: + iframe = self._getPlayableIframe(data) + if self.cm.isValidUrl(iframe): + return self.up.getVideoLinkExt(iframe) + linksTab = [] + baseTitle = self._normalizeTitle(cItem.get("title", "")) + for u in found: + host = self.up.getHostName(u, True) + label = "%s - %s" % (host, baseTitle) + linksTab.append({"name": label, "url": strwithmeta(u, {"Referer": referer}), "need_resolve": 1}) + return linksTab + + def getVideoLinks(self, videoUrl): + printDBG("ARADrama.getVideoLinks [%s]" % videoUrl) + if self.cm.isValidUrl(videoUrl): + return self.up.getVideoLinkExt(videoUrl) + return [] + + # -------------------- info (RTL fixed) -------------------- + def getArticleContent(self, cItem): + printDBG("ARADrama.getArticleContent [%s]" % cItem) + url = cItem.get("url", "") + if "prev_url" in cItem: + url = cItem["prev_url"] + sts, data = self.getPage(url) + if not sts or not data: + return [] + poster = self._extractPoster(data) or cItem.get("icon", "") + descHtml = self._getDescHtml(data) + info = self._parsePairsArabic(descHtml) + storyFromBlock, cast = self._extractStoryAndCast(descHtml) + story = storyFromBlock or self._getStory(data) or cItem.get("desc", "") + story = self._cleanTitle(story) + title = self._normalizeTitle(cItem.get("title", "") or info.get("اسم المسلسل", "")) + if not title: + title = self._normalizeTitle(self.cm.ph.getSearchGroups(data, r"([^<]+)")[0]) + text = self._buildPrettyInfoText(info, story, cast) + # ✅ لفّ يدوي ثم إزالة الداش من بداية السطر ثم RTL isolates + title = self._forceRTLText(title).replace("\n", " ") + text = self._wrapTextBlock(text, width=74) + text = self._stripLeadingDashesPerLine(text) + text = self._forceRTLText(text) + return [{"title": title, "text": text, "images": [{"title": "", "url": poster}], "other_info": {}}] + + # -------------------- service -------------------- + def handleService(self, index, refresh=0, searchPattern="", searchType=""): + printDBG("handleService start") + try: + CBaseHostClass.handleService(self, index, refresh, searchPattern, searchType) + except TypeError: + try: + CBaseHostClass.handleService(self, index, refresh, searchPattern) + except TypeError: + CBaseHostClass.handleService(self, index, refresh) + if self.MAIN_URL is None: + self.selectDomain() + name = self.currItem.get("name", None) + category = self.currItem.get("category", "") + printDBG("handleService: name[%s], category[%s]" % (name, category)) + self.currList = [] + try: + if not name and not category: + self.listMainMenu({"name": "category", "type": "category"}) + elif category in ("series", "movies"): + self.listCatItems(self.currItem, "listItems") + elif category in ("listItems", "tvshow"): + self.listItems(self.currItem, "explore_item") + elif category == "explore_item": + self.exploreItems(self.currItem) + elif category in ("search", "search_next_page"): + cItem = dict(self.currItem) + cItem.update({"search_item": False, "name": "category"}) + self.listSearchResult(cItem, searchPattern, searchType) + elif category == "search_history": + self.listsHistory({"name": "history", "category": "search"}, "desc", _("Type: ")) + else: + printExc() + except Exception: + printExc() + CBaseHostClass.endHandleService(self, index, refresh) + + +class IPTVHost(CHostBase): + def __init__(self): + CHostBase.__init__(self, ARADrama(), True, []) + + def withArticleContent(self, cItem): + if "EPG" in cItem or "prev_url" in cItem or cItem.get("category", "") == "explore_item": + return True + return False diff --git a/IPTVPlayer/hosts/hostasi1tv.py b/IPTVPlayer/hosts/hostasi1tv.py index e8b67403..4072d644 100644 --- a/IPTVPlayer/hosts/hostasi1tv.py +++ b/IPTVPlayer/hosts/hostasi1tv.py @@ -11,19 +11,20 @@ from Plugins.Extensions.IPTVPlayer.libs.jsunpack import get_packed_data from Plugins.Extensions.IPTVPlayer.p2p3.UrlParse import urljoin from Plugins.Extensions.IPTVPlayer.p2p3.UrlLib import urllib_quote_plus, urllib_quote + ################################################### # FOREIGN import ################################################### import re import time import json + ################################################### Y = E2ColoR("yellow") W = E2ColoR("white") LB = E2ColoR("lightblue") G = E2ColoR("green") R = E2ColoR("red") -################################################### def GetConfigList(): diff --git a/IPTVPlayer/hosts/hostbotolat.py b/IPTVPlayer/hosts/hostbotolat.py new file mode 100644 index 00000000..ba2780da --- /dev/null +++ b/IPTVPlayer/hosts/hostbotolat.py @@ -0,0 +1,1042 @@ +# -*- coding: utf-8 -*- +# Last Modified: 22.01.2026 +################################################### +# LOCAL import +################################################### +from Plugins.Extensions.IPTVPlayer.components.iptvplayerinit import TranslateTXT as _ +from Plugins.Extensions.IPTVPlayer.components.ihost import CHostBase, CBaseHostClass +from Plugins.Extensions.IPTVPlayer.tools.iptvtools import printDBG, printExc, E2ColoR, CSearchHistoryHelper +from Plugins.Extensions.IPTVPlayer.tools.iptvtypes import strwithmeta +from Components.config import config, ConfigText + +################################################### +# FOREIGN import +################################################### +import re +import json +import os +import codecs +import requests + +from urllib.parse import urlparse + +################################################### +Y = E2ColoR("yellow") +W = E2ColoR("white") +DD = E2ColoR("dodgerblue") +CY = E2ColoR("cyan") +G = E2ColoR("green") +R = E2ColoR("red") + + +################################################### +def GetConfigList(): + return [] + + +def gettytul(): + return "https://www.btolat.com/" + + +class BtolatCom(CBaseHostClass): + def __init__(self): + CBaseHostClass.__init__(self, {"history": "btolat.com", "cookie": "btolat.com.cookie"}) + config.plugins.iptvplayer.btolat_user = ConfigText(default="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", fixed_size=False) + self.USER_AGENT = config.plugins.iptvplayer.btolat_user.value + self.MAIN_URL = "https://www.btolat.com/" + self.DEFAULT_ICON_URL = "https://i.ibb.co/RkbWVvBZ/botolat.png" + self.HTTP_HEADER = {"User-Agent": self.USER_AGENT, "DNT": "1", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "Accept-Language": "ar,en-US;q=0.7,en;q=0.3", "Referer": self.getMainUrl(), "Origin": self.getMainUrl()} + self.history = CSearchHistoryHelper("btolat") + self.defaultParams = {"header": self.HTTP_HEADER, "with_metadata": True, "use_cookie": True, "load_cookie": True, "save_cookie": True, "cookiefile": self.COOKIE_FILE} + self.cacheLeagues = {} + self.cacheVideos = {} + + def getPage(self, baseUrl, addParams={}, post_data=None): + if addParams == {}: + addParams = dict(self.defaultParams) + baseUrl = self.cm.iriToUri(baseUrl) + sts, data = self.cm.getPage(baseUrl, addParams, post_data) + return sts, data + + def getFullIconUrl(self, url): + url = self.getFullUrl(url) + if url == "": + return "" + cookieHeader = self.cm.getCookieHeader(self.COOKIE_FILE) + return strwithmeta(url, {"Cookie": cookieHeader, "User-Agent": self.USER_AGENT}) + + def setMainUrl(self, url): + if self.cm.isValidUrl(url): + self.MAIN_URL = self.cm.getBaseUrl(url) + + def listMainMenu(self, cItem): + printDBG("BtolatCom.listMainMenu") + MAIN_CAT_TAB = [ + {"category": "list_videos", "title": _("أحدث الفيديوهات"), "url": self.getFullUrl("/video")}, + {"category": "list_leagues", "title": _("البطولات والدوريات"), "url": self.getFullUrl("/leagues")}, + {"category": "list_top_scorers", "title": _("الهدافين"), "url": self.getFullUrl("/leagues")}, + {"category": "list_professionals", "title": _("المحترفين"), "url": self.getFullUrl("/professionals/video")}, + {"category": "list_players", "title": _("قائمة اللاعبين وأهدافهم"), "url": self.getFullUrl("/leagues")}, + {"category": "search", "title": _("Search"), "search_item": True}, + {"category": "search_history", "title": _("Search history")}, + {"category": "delete_history", "title": _("Delete search history")}, + ] + self.listsTab(MAIN_CAT_TAB, cItem) + + def listLeagues(self, cItem): + printDBG("BtolatCom.listLeagues") + sts, data = self.getPage(cItem["url"]) + if not sts: + return + # ===== Get all "mostLeagues" blocks ===== + blocks = re.findall(r'(]*>.*?)', data, re.DOTALL | re.IGNORECASE) + for b in blocks: + title = self.cm.ph.getSearchGroups(b, r"]*>(.*?)")[0] + if not title: + continue + title_clean = self.cleanHtmlStr(title) + # ===== Section marker ===== + if "أهم البطولات" in title_clean: + self.addMarker({"title": Y + "========== * أهم البطولات * ==========" + W}) + elif "كل البطولات" in title_clean: + self.addMarker({"title": CY + "========== * كل البطولات * ==========" + W}) + else: + continue + # ===== Extract leagues under each marker ===== + leagues = re.findall(r']+class="[^"]*leagueBox[^"]*"[^>]*>.*?', b, re.DOTALL | re.IGNORECASE) + printDBG("BtolatCom.listLeagues [%s] leagues found: %d" % (title_clean, len(leagues))) + for item in leagues: + url = self.cm.ph.getSearchGroups(item, r'href="([^"]+)"')[0] + icon = self.cm.ph.getSearchGroups(item, r']+src="([^"]+)"')[0] + title = self.cm.ph.getSearchGroups(item, r"

      (.*?)

      ")[0] + if not url or not title: + continue + full_url = self.getFullUrl(url.replace("/league/", "/league/videos/")) + params = dict(cItem) + params.update({"good_for_fav": True, "category": "list_league_videos", "title": self.cleanHtmlStr(title), "url": full_url, "icon": self.getFullIconUrl(icon), "team_url": self.getFullUrl(url)}) + self.addDir(params) + + def listLeagueVideos(self, cItem): + printDBG("BtolatCom.listLeagueVideos [%s]" % cItem) + page = cItem.get("page", 1) + url = cItem["url"] + if page > 1: + if "?" in url: + url += "&p=" + str(page) + else: + url += "?p=" + str(page) + sts, data = self.getPage(url) + if not sts: + return + # ----- 1) Add "All videos" folder for the league ----- + params_all_videos = dict(cItem) + params_all_videos.update({"category": "list_all_league_videos", "title": Y + "جميع الفيديوهات" + W, "url": url}) # all league videos + self.addDir(params_all_videos) + # ----- 2) Add a marker under "All videos" folder ----- + params_marker = dict(cItem) + params_marker.update({"category": "none", "title": CY + "========== الأندية والفرق ==========" + W}) + self.addMarker(params_marker) + # ----- 3) Add teams folders under the marker ----- + teams_block = re.search(r'
      (.*?)
      \s*', data, re.DOTALL) + if teams_block: + teams = re.findall(r'.*?]*>.*?

      (.*?)

      ', teams_block.group(1), re.DOTALL) + for team_url, team_icon, team_name in teams: + params = dict(cItem) + params.update({"good_for_fav": True, "category": "list_league_team_videos", "title": self.cleanHtmlStr(team_name), "url": self.getFullUrl(team_url), "team_url": self.getFullUrl(team_url), "icon": self.getFullIconUrl(team_icon)}) # team videos handler + self.addDir(params) + + def listLeagueTeamVideos(self, cItem): + printDBG("BtolatCom.listLeagueTeamVideos [%s]" % cItem) + page = cItem.get("page", 1) + url = cItem["url"] + # Convert team URL to its videos page + videos_url = url.replace("/team/", "/team/videos/") if "/team/" in url else url + if page > 1: + videos_url += "&p=" + str(page) if "?" in videos_url else "?p=" + str(page) + sts, data = self.getPage(videos_url) + if not sts: + return + # ----- 1) Search for team videos ----- + blocks = re.findall(r'
      .*?
      ', data, re.DOTALL) + if blocks: + for b in blocks: + # Video URL + video_url = self.cm.ph.getSearchGroups(b, r']+href=[\'"](/video/[^\'"]+)[\'"]') + if not video_url: + continue + video_url = video_url[0] + # Video title + title = self.cm.ph.getSearchGroups(b, r"]*>(.*?)") + if not title: + continue + title = self.cleanHtmlStr(title[0]) + # Video icon + icon = self.cm.ph.getSearchGroups(b, r'data-original=[\'"]([^\'"]+)[\'"]') + if not icon: + icon = self.cm.ph.getSearchGroups(b, r'src=[\'"]([^\'"]+)[\'"]') + icon = icon[0] if icon else "" + # League / category name + cat = self.cm.ph.getSearchGroups(b, r']*>(.*?)
      ') + cat_name = self.cleanHtmlStr(cat[0]) if cat else "" + # ---------- Extract image URL first ---------- + icon = self.cm.ph.getSearchGroups(b, r'data-original=[\'"]([^\'"]+)[\'"]') + if not icon: + icon = self.cm.ph.getSearchGroups(b, r'src=[\'"]([^\'"]+)[\'"]') + icon = icon[0] if icon else "" + # ---------- Extract date from image URL ---------- + video_date = "" + if icon: + date_m = re.search(r"/(\d{4})/(\d{1,2})/(\d{1,2})/video/", icon) + if date_m: + video_date = "%s-%s-%s" % (date_m.group(1), date_m.group(2), date_m.group(3)) + # Video description (category + date) + if cat_name and video_date: + desc = cat_name + "\n" + CY + video_date + W + elif video_date: + desc = CY + video_date + W + else: + desc = cat_name or title + params = dict(cItem) + params.update({"good_for_fav": True, "title": title, "url": self.getFullUrl(video_url), "icon": self.getFullIconUrl(icon), "desc": Y + desc + W}) + self.addVideo(params) + else: + # ----- 2) If no videos found, add a marker ----- + params_marker = dict(cItem) + params_marker.update({"category": "none", "title": CY + "لا يوجد بيانات حاليا" + W}) + self.addMarker(params_marker) + # ===== pagination ===== + if 'class="next-page"' in data: + params = dict(cItem) + params.update({"title": Y + _("Next Page المزيد من الفيديوهات") + " ▶▶▶" + W, "page": page + 1, "url": cItem["url"], "category": "list_league_team_videos"}) + self.addDir(params) + + def listAllLeagueVideos(self, cItem): + printDBG("BtolatCom.listAllLeagueVideos [%s]" % cItem) + page = cItem.get("page", 1) + url = cItem["url"] + if page > 1: + url += "&p=" + str(page) if "?" in url else "?p=" + str(page) + sts, data = self.getPage(url) + if not sts: + return + # ===== Extract all videos (same as listLeagueVideos) ===== + blocks = re.findall(r'
      .*?
      ', data, re.DOTALL) + printDBG("BtolatCom.listAllLeagueVideos found [%d] items" % len(blocks)) + for b in blocks: + # Video URL + video_url = self.cm.ph.getSearchGroups(b, r']+href=[\'"](/video/[^\'"]+)[\'"]') + # Video title + title = self.cm.ph.getSearchGroups(b, r"]*>(.*?)") + # Video icon + icon = self.cm.ph.getSearchGroups(b, r'data-original=[\'"]([^\'"]+)[\'"]') + if not icon: + icon = self.cm.ph.getSearchGroups(b, r'src=[\'"]([^\'"]+)[\'"]') + if not video_url or not title: + printDBG("BtolatCom.listAllLeagueVideos skip item (no data)") + continue + video_url = video_url[0] + title = self.cleanHtmlStr(title[0]) + icon = icon[0] if icon else "" + # League / category name + cat = self.cm.ph.getSearchGroups(b, r']*>(.*?)') + cat_name = self.cleanHtmlStr(cat[0]) if cat else "" + # Extract date from image URL + date_m = re.search(r"https://img\.btolat\.com/(\d+)/(\d+)/(\d+)/video/", b) + video_date = "%s-%s-%s" % (date_m.group(1), date_m.group(2), date_m.group(3)) + # Video description: category + date + if cat_name and video_date: + desc = cat_name + "\n" + CY + video_date + W + elif video_date: + desc = CY + video_date + W + else: + desc = cat_name or title + params = dict(cItem) + params.update({"good_for_fav": True, "title": title, "url": self.getFullUrl(video_url), "icon": self.getFullIconUrl(icon), "desc": Y + desc + W}) + self.addVideo(params) + # ===== pagination ===== + if 'class="next-page"' in data: + params = dict(cItem) + params.update({"title": Y + _("Next Page المزيد من الفيديوهات") + " ▶▶▶" + W, "page": page + 1, "url": url, "category": "list_all_league_videos"}) + self.addDir(params) + + def listTopScorersMenu(self, cItem): + printDBG("BtolatCom.listTopScorersMenu [%s]" % cItem) + sts, data = self.getPage(cItem["url"]) + if not sts: + return + # Extract leagues / competitions + leagues = re.findall(r']+class="[^"]*leagueBox[^"]*"[^>]*>.*?', data, re.DOTALL | re.IGNORECASE) + for item in leagues: + url = self.cm.ph.getSearchGroups(item, r'href="([^"]+)"')[0] + icon = self.cm.ph.getSearchGroups(item, r']+src="([^"]+)"')[0] + title = self.cm.ph.getSearchGroups(item, r"

      (.*?)

      ")[0] + if not url or not title: + continue + params = dict(cItem) + params.update({"good_for_fav": True, "category": "list_league_top_scorers", "title": self.cleanHtmlStr(title), "url": self.getFullUrl(url.replace("/league/", "/league/topscores/")), "icon": self.getFullIconUrl(icon), "league_id": self.cm.ph.getSearchGroups(url, r"/league/(\d+)")[0]}) # Scorers page URL # Extract league ID + self.addDir(params) + + def listLeagueTopScorers(self, cItem): + printDBG("BtolatCom.listLeagueTopScorers [%s]" % cItem) + page = cItem.get("page", 1) + last_pos = cItem.get("last_position", 0) + league_id = cItem.get("league_id", "") + if page == 1: + url = cItem["url"] + sts, data = self.getPage(url) + if not sts: + return + # ====== No top scorers table ====== + if "لا يوجد جدول هدافين" in data: + self.addMarker({"title": Y + "لا يوجد هدافين لهذه البطولة" + W, "icon": cItem.get("icon", "")}) + return + else: + # url = f'https://www.btolat.com/league/TopScoresLoadMore/{league_id}/aa' + url = "https://www.btolat.com/league/TopScoresLoadMore/%s/aa" % league_id + post_data = {"Id": league_id, "lastPosition": last_pos} + sts, data = self.getPage(url, post_data=post_data) + if not sts: + return + try: + data_json = json.loads(data) + except Exception: + return + if not data_json.get("success", True): + printDBG("No more top scorers (success = false)") + return + data = data_json.get("html", "") + # ===== Extract players + images ===== + players = re.findall( + r'.*?' # position / rank + r'.*?' # player profile link + r']+src="([^"]+)".*?' # player image + r"([^<]+).*?" # player name + r".*?" + r'.*?' # team link + r']+src="([^"]+)".*?' # team logo + r"([^<]+).*?" # team name + r".*?" + r"(\d+)", # goals count + data, + re.DOTALL | re.IGNORECASE, + ) + if not players: + if page == 1: + self.addMarker({"title": Y + "لا يوجد هدافين لهذه البطولة" + W, "icon": cItem.get("icon", "")}) + return + for pos, player_url, player_img, player_name, team_url, team_img, team_name, goals in players: + player_url_full = self.getFullUrl(player_url) + img_url = self.getFullIconUrl(player_img.strip()) + title = "%s- P| %s - %s - (%s %sهدف%s)" % (pos, player_name.strip(), team_name.strip(), goals, Y, W) + params = {"good_for_fav": True, "category": "list_player_videos", "title": title, "url": player_url_full, "icon": img_url, "page": 1} + self.addDir(params) + # ===== Load more button ===== + last_position = int(players[-1][0]) + params = dict(cItem) + params.update({"title": Y + _("Next Page المزيد من الهدافين") + " ▶▶▶" + W, "page": page + 1, "last_position": last_position, "league_id": league_id, "url": cItem["url"], "category": "list_league_top_scorers"}) + self.addDir(params) + + def listProfessionals(self, cItem): + printDBG("BtolatCom.listProfessionals") + url = cItem["url"] + sts, data = self.getPage(url) + if not sts: + return + # Add professionals videos link + params = dict(cItem) + params.update( + { + "good_for_fav": True, + "category": "list_player_videos", + "title": Y + "فيديوهات كل المحترفين" + W, + "url": self.getFullUrl("/professionals/video"), + } + ) + self.addDir(params) + params = dict(cItem) + params.update( + { + "category": "none", + "title": CY + "========== المحترفين المصريين ==========" + W, + } + ) + self.addMarker(params) + # Extract players + player_links = re.findall(r']+href="(/player/\d+/[^"]+)"[^>]*class="importantTeam"[^>]*>.*?' r']+src="([^"]+)"[^>]*>.*?' r"

      (.*?)

      ", data, re.DOTALL | re.IGNORECASE) + for player_url, player_icon, player_name in player_links: + player_url_full = self.getFullUrl(player_url) + sts, player_data = self.getPage(player_url_full) + if not sts: + desc = "" + else: + info_block = re.search(r'
      (.*?)
      ', player_data, re.DOTALL) + if info_block: + info_html = info_block.group(1) + job_title = re.search(r']*>(.*?)', info_html) + team = re.search(r']*itemprop="memberOf"[^>]*>.*?]*>(.*?)', info_html, re.DOTALL) + birth_place = re.search(r']*itemprop="address"[^>]*>.*?(.*?)', info_html, re.DOTALL) + nationality = re.search(r'الجنسيه\s*:\s*(.*?)', info_html, re.DOTALL) + birth_date = re.search(r'', info_html, re.DOTALL) + age = re.search(r"السن\s*:\s*(.*?)", info_html, re.DOTALL) + height = re.search(r'(.*?)', info_html, re.DOTALL) + weight = re.search(r'(.*?)', info_html, re.DOTALL) + # Build description with yellow color + desc = " {}المركز:{} {} | {}الفريق:{} {} | {}محل الميلاد:{} {} | {}الجنسية:{} {} | {}تاريخ الميلاد:{} {} | {}العمر:{} {} | {}الطول:{} {} | {}الوزن:{} {}".format(Y, W, job_title.group(1).strip() if job_title else "", Y, W, team.group(1).strip() if team else "", Y, W, birth_place.group(1).strip() if birth_place else "", Y, W, nationality.group(1).strip() if nationality else "", Y, W, birth_date.group(1).strip() if birth_date else "", Y, W, age.group(1).strip() if age else "", Y, W, height.group(1).strip() if height else "", Y, W, weight.group(1).strip() if weight else "") + else: + desc = "" + params = dict(cItem) + params.update({"good_for_fav": True, "category": "list_player_videos", "title": self.cleanHtmlStr(player_name), "url": player_url_full, "icon": self.getFullIconUrl(player_icon), "desc": desc}) + self.addDir(params) + + def listPlayerVideos(self, cItem): + printDBG("BtolatCom.listPlayerVideos") + page = cItem.get("page", 1) + base_url = cItem["url"] + # ====================================================== + # ============ Professionals section ================== + # ====================================================== + if "/professionals/video" in base_url: + if page == 1: + url = base_url + sts, data = self.getPage(url) + if not sts: + return + blocks = re.findall(r'(]*>.*?)', data, re.DOTALL | re.IGNORECASE) + last_row_id = re.search(r'data-val="(\d+)"', blocks[-1]).group(1) if blocks else "" + last_row_date = re.search(r'data-date="([^"]+)"', blocks[-1]).group(1) if blocks else "" + else: + url = "https://www.btolat.com/api/video/LoadMore/0" + post_data = {"lastRowId": cItem.get("last_row_id", ""), "lasRowDate": cItem.get("last_row_date", "")} + sts, data = self.getPage(url, post_data=post_data) + if not sts: + return + data = json.loads(data) + blocks = re.findall(r'(]*>.*?)', data["html"], re.DOTALL | re.IGNORECASE) + last_row_id = re.search(r'data-val="(\d+)"', blocks[-1]).group(1) if blocks else "" + last_row_date = re.search(r'data-date="([^"]+)"', blocks[-1]).group(1) if blocks else "" + for block in blocks: + vid = re.search(r'href=[\'"](/video/\d+)[\'"]', block) + img = re.search(r'data-src="([^"]+)"', block) + title = re.search(r"

      (.*?)

      ", block, re.DOTALL) + cat = re.search(r'(.*?)', block, re.DOTALL) + date_m = re.search(r'data-date="([^"]+)"', block) + if not vid or not title: + continue + video_href = vid.group(1) + img_url = img.group(1) if img else "" + name = self.cleanHtmlStr(title.group(1)) + cat_name = self.cleanHtmlStr(cat.group(1)) if cat else "" + video_date = "" + if date_m: + raw_date = date_m.group(1).strip() # Raw date (example: 12/13/2025) + try: + m, d, y = raw_date.split("/") + video_date = "%s-%s-%s" % (d.zfill(2), m.zfill(2), y) + except: + video_date = raw_date + # Description = league + date + if cat_name and video_date: + desc = cat_name + "\n" + CY + video_date + W + elif video_date: + desc = CY + video_date + W + else: + desc = cat_name or name + params = {"good_for_fav": True, "category": "video", "title": name, "url": self.getFullUrl(video_href), "icon": self.getFullIconUrl(img_url), "desc": Y + desc + W} + self.addVideo(params) + # ===== Manual load more button ===== + if len(blocks) > 0: + params = dict(cItem) + params.update({"title": Y + _("Next Page المزيد من الفيديوهات") + " ▶▶▶" + W, "page": page + 1, "last_row_id": last_row_id, "last_row_date": last_row_date, "url": base_url, "category": "list_player_videos"}) + self.addDir(params) + # ====================================================== + # ================== Other players ==================== + # ====================================================== + else: + if page == 1: + url = base_url + sts, data = self.getPage(url) + if not sts: + return + # ===== Check if videos tab exists ===== + videos_tab = self.cm.ph.getSearchGroups(data, r'href="(/player/videos/[^"]+)"')[0] + if not videos_tab: + params = {"title": Y + "لا يوجد فيديوهات متاحة لهذا اللاعب حاليا" + W, "type": "marker", "icon": cItem.get("icon", "")} + self.addMarker(params) + return + # ===== Videos exist → auto redirect ===== + base_url = self.getFullUrl(videos_tab) + url = base_url + else: + # Convert link to /player/videos/... + if "/player/videos/" not in base_url: + base_url = base_url.replace("/player/", "/player/videos/") + url = base_url + "?p=" + str(page) + sts, data = self.getPage(url) + if not sts: + return + printDBG("========== PLAYER VIDEOS PAGE ==========") + printDBG("URL: %s" % url) + printDBG("========== END PAGE ==========") + blocks = re.findall(r'(]*>.*?)', data, re.DOTALL | re.IGNORECASE) + printDBG("BtolatCom.listPlayerVideos VIDEO blocks found: %d" % len(blocks)) + for block in blocks: + vid = re.search(r'href=[\'"](/video/\d+)[\'"]', block) + img = re.search(r'data-original="([^"]+)"', block) + title = re.search(r"

      (.*?)

      ", block, re.DOTALL) + cat = re.search(r']*>(.*?)', block, re.DOTALL) + if not vid or not title: + continue + video_href = vid.group(1) + img_url = img.group(1) if img else "" + name = self.cleanHtmlStr(title.group(1)) + name = self.cleanHtmlStr(title.group(1)) + cat_name = self.cleanHtmlStr(cat.group(1)) if cat else "" + # ===== Extract date from image URL ===== + video_date = "" + if img: + m = re.search(r"/(\d{4})/(\d{1,2})/(\d{1,2})/video/", img.group(1)) + if m: + video_date = "%s-%s-%s" % (m.group(1), m.group(2), m.group(3)) + # ===== Two-line description ===== + if cat_name and video_date: + desc = cat_name + "\n" + CY + video_date + W + elif video_date: + desc = CY + video_date + W + else: + desc = cat_name or name + params = {"good_for_fav": True, "category": "video", "title": name, "url": self.getFullUrl(video_href), "icon": self.getFullIconUrl(img_url), "desc": Y + desc + W} + self.addVideo(params) + # ===== Manual load more button ===== + if len(blocks) > 0 and "التالى" in data: + params = dict(cItem) + params.update({"title": Y + "Next Page المزيد من الفيديوهات" + W, "page": page + 1, "url": base_url, "category": "list_player_videos"}) + self.addDir(params) + + def listVideos(self, cItem): + printDBG("BtolatCom.listVideos [%s]" % cItem) + page = cItem.get("page", 1) + url = cItem["url"] + if page == 1: + sts, data = self.getPage(url) + if not sts: + return + blocks = re.findall(r'(]*>.*?)', data, re.DOTALL | re.IGNORECASE) + else: + url = "https://www.btolat.com/api/video/LoadMore/0" + post_data = {"lastRowId": cItem.get("last_row_id", ""), "lasRowDate": cItem.get("last_row_date", "")} + sts, data = self.getPage(url, post_data=post_data) + if not sts: + return + data = json.loads(data) + blocks = re.findall(r'(]*>.*?)', data["html"], re.DOTALL | re.IGNORECASE) + for block in blocks: + vid = re.search(r'href=[\'"](/video/\d+)[\'"]', block) + img = re.search(r'data-src="([^"]+)"', block) + title = re.search(r"

      (.*?)

      ", block, re.DOTALL) + cat = re.search(r'(.*?)', block, re.DOTALL) + date_m = re.search(r'data-date="([^"]+)"', block) + if not vid or not title: + continue + video_href = vid.group(1) + img_url = img.group(1) if img else "" + name = self.cleanHtmlStr(title.group(1)) + cat_name = self.cleanHtmlStr(cat.group(1)) if cat else "" + video_date = date_m.group(1).replace("/", "-") if date_m else "" # Convert 12/17/2025 → 12-17-2025 + # Description = league + date (two lines) + if cat_name and video_date: + desc = cat_name + "\n" + CY + video_date + W + elif video_date: + desc = CY + video_date + W + else: + desc = cat_name or name + params = {"good_for_fav": True, "category": "video", "title": name, "url": self.getFullUrl(video_href), "icon": self.getFullIconUrl(img_url), "desc": Y + desc + W} + self.addVideo(params) + # ===== Manual load more button ===== + if len(blocks) > 0: + params = dict(cItem) + params.update({"title": Y + "Next Page المزيد من الفيديوهات" + W, "page": page + 1, "last_row_id": blocks[-1].split('data-val="')[1].split('"')[0], "last_row_date": blocks[-1].split('data-date="')[1].split('"')[0], "url": cItem.get("url", "/video"), "category": "list_videos"}) + self.addDir(params) + + def listPlayersMenu(self, cItem): + """ + قائمة الدوريات (مدخل مسار: الدوريات → الفرق → اللاعبين → فيديوهات اللاعب) + """ + printDBG("BtolatCom.listPlayersMenu [%s]" % cItem) + cItem = dict(cItem) + cItem["url"] = "https://www.btolat.com/leagues" + sts, data = self.getPage(cItem["url"]) + if not sts: + return + # ----- Add marker under leagues / competitions folder ----- + params_marker = dict(cItem) + params_marker.update({"category": "none", "title": CY + "========== البطولات والدوريات ==========" + W}) + self.addMarker(params_marker) + leagues = re.findall(r']+class="[^"]*leagueBox[^"]*"[^>]*>.*?', data, re.DOTALL | re.IGNORECASE) + for item in leagues: + url = self.cm.ph.getSearchGroups(item, r'href="([^"]+)"')[0] + icon = self.cm.ph.getSearchGroups(item, r']+src="([^"]+)"')[0] + title = self.cm.ph.getSearchGroups(item, r"

      (.*?)

      ")[0] + if not url or not title: + continue + params = dict(cItem) + params.update({"good_for_fav": True, "category": "list_league_teams", "title": self.cleanHtmlStr(title), "url": self.getFullUrl(url), "icon": self.getFullIconUrl(icon)}) + self.addDir(params) + + def listLeagueTeams(self, cItem): + """ + جلب الفرق في الدوري + """ + printDBG("BtolatCom.listLeagueTeams [%s]" % cItem) + sts, data = self.getPage(cItem["url"]) + if not sts: + return + # ----- Add marker under teams / clubs folder ----- + params_marker = dict(cItem) + params_marker.update({"category": "none", "title": Y + "========== الأندية والفرق ==========" + W}) + self.addMarker(params_marker) + block = re.search(r'
      (.*?)
      \s*', data, re.DOTALL) + if not block: + printDBG("importantTeams block not found") + return + teams = re.findall(r']+href="(/team/[^"]+)"[^>]*class="importantTeam"[^>]*>.*?]+src="([^"]+)".*?>.*?

      (.*?)

      ', block.group(1), re.DOTALL | re.IGNORECASE) + if not teams: + printDBG("No teams found inside importantTeams") + return + for team_url, team_icon, team_name in teams: + params = dict(cItem) + params.update({"category": "list_team_players", "title": self.cleanHtmlStr(team_name), "url": self.getFullUrl(team_url), "team_url": self.getFullUrl(team_url), "icon": self.getFullIconUrl(team_icon)}) + self.addDir(params) + + def listTeamPlayers(self, cItem): + printDBG("BtolatCom.listTeamPlayers [%s]" % cItem) + team_url = cItem.get("team_url") or cItem.get("url") + if "/team/squad/" not in team_url: + team_url = team_url.replace("/team/", "/team/squad/") + sts, data = self.getPage(team_url) + if not sts: + return + # Extract players table + block = re.search(r"]+squadTable[^>]*>(.*?)", data, re.DOTALL | re.IGNORECASE) + if not block: + printDBG("squadTable not found") + return + players_rows = re.findall(r"(.*?)", block.group(1), re.DOTALL | re.IGNORECASE) + for row in players_rows: + # Link, image and name + match = re.search(r'([^<]+)', row, re.DOTALL | re.IGNORECASE) + if not match: + continue + player_url = match.group(1).strip() + player_icon = match.group(2).strip() + player_name = match.group(3).strip() + # Shirt number + match_number = re.search(r"([^<]+)", row) + player_number = match_number.group(1).strip() if match_number else "" + # Position, nationality, birth date + match_td = re.findall(r".*?([^<]+)", row, re.DOTALL) + player_pos = match_td[0].strip() if len(match_td) > 0 else "" + player_nat = match_td[1].strip() if len(match_td) > 1 else "" + player_birth = match_td[2].strip() if len(match_td) > 2 else "" + # Description shown on hover with colors + desc = "{}الاسم:{} {} | {}رقم القميص:{} {} | {}المركز:{} {} | {}الجنسية:{} {} | {}تاريخ الميلاد:{} {}".format(Y, W, player_name, Y, W, player_number, Y, W, player_pos, Y, W, player_nat, Y, W, player_birth) + params = dict(cItem) + params.update({"good_for_fav": True, "category": "list_player_videos", "title": self.cleanHtmlStr(player_name), "url": self.getFullUrl(player_url), "icon": self.getFullIconUrl(player_icon), "desc": desc}) + self.addDir(params) + + def getLinksForVideo(self, cItem): + printDBG("BtolatCom.getLinksForVideo [%s]" % cItem) + videoLinks = [] + url = cItem.get("url", "").strip() + if not url: + return videoLinks + # 1) Get page + sts, data = self.getPage(url) + if not sts: + return videoLinks + # =============================== + # 2) YouTube + # =============================== + m = re.search(r'https?://(?:www\.)?youtube\.com/embed/([^\?&"\']+)', data) + if m: + youtube_id = m.group(1) + # Use YouTube host to resolve multiple qualities + videoLinks.append({"name": "YouTube", "url": "https://www.youtube.com/watch?v=%s" % youtube_id, "need_resolve": 1}) + # =============================== + # 3) Twitter / X via JSON endpoint + # =============================== + tweet_id = None + # Try to extract tweet_id from any format + for pattern in [r"https?://(?:www\.)?(?:twitter|x)\.com/[^/]+/status/(\d+)", r'data-tweet-id=["\'](\d+)["\']', r'embed/Tweet\.html[^"\']+id=(\d+)']: + m = re.search(pattern, data) + if m: + tweet_id = m.group(1) + break + if tweet_id: + printDBG("BtolatCom.getLinksForVideo - Twitter id: %s" % tweet_id) + tweet_features = "tfw_video_hls_dynamic_manifests_15082:true_bitrate" + json_url = ("https://cdn.syndication.twimg.com/tweet-result?" "id=%s&lang=ar&token=4ufmaqlvqaj&features=%s") % (tweet_id, tweet_features) + sts, jdata = self.getPage(json_url) + if sts: + try: + jres = json.loads(jdata) + temp_links = [] + hls_link = None + for media in jres.get("mediaDetails", []): + if media.get("type") == "video": + variants = media.get("video_info", {}).get("variants", []) + used = set() + for v in variants: + url = v.get("url") + if not url or url in used: + continue + used.add(url) + if "m3u8" in url: + # Store HLS to be added last + hls_link = {"name": "Twitter HLS", "url": url, "need_resolve": 0} + else: + bitrate = v.get("bitrate", 0) + if bitrate >= 8000000: + name = "Twitter MP4 1080P" + elif bitrate >= 2000000: + name = "Twitter MP4 720P" + elif bitrate >= 800000: + name = "Twitter MP4 480P" + else: + name = "Twitter MP4 %d" % (bitrate // 1000) + temp_links.append({"name": name, "url": url, "need_resolve": 0, "bitrate": bitrate}) + # Sort MP4 from highest to lowest quality + temp_links.sort(key=lambda x: x.get("bitrate", 0), reverse=True) + # Remove bitrate key after sorting + for l in temp_links: + l.pop("bitrate", None) + videoLinks.extend(temp_links) + # Add HLS at the end + if hls_link: + videoLinks.append(hls_link) + except Exception as e: + printDBG("BtolatCom.getLinksForVideo - Twitter JSON parse error: %s" % str(e)) + # =============================== + # 4) vortexvisionworks / videohatkora via embed page + # =============================== + if not videoLinks: + embed_url = None + m = re.search(r']+src=["\']([^"\']+embed[^"\']+)["\']', data) + if m: + embed_url = m.group(1) + if embed_url.startswith("//"): + embed_url = "https:" + embed_url + elif embed_url.startswith("/"): + embed_url = self.MAIN_URL + embed_url + if embed_url: + printDBG("BtolatCom embed url: %s" % embed_url) + # Define headers + parsed = urlparse(embed_url) + embed_headers = dict(self.defaultParams.get("header", {})) + embed_headers["Referer"] = embed_url + embed_headers["Origin"] = "%s://%s" % (parsed.scheme, parsed.netloc) + sts, embed_data = self.getPage(embed_url, {"header": embed_headers}) + if sts: + try: + # Search only for the main HLS URL + hls_match = re.search(r"hls\s*:\s*['\"](//[^'\"]+\.m3u8[^'\"]*)['\"]", embed_data) + if hls_match: + link = hls_match.group(1) + if link.startswith("//"): + link = "https:" + link + elif link.startswith("/"): + link = self.MAIN_URL + link + # Base URL + base_link = link + # Modify URL for other qualities + link_360 = base_link.replace("0.m3u8", "360p.m3u8") + link_720 = base_link.replace("0.m3u8", "720p.m3u8") + videoLinks.append({"name": "720p - Vortexvisionworks", "url": link_720, "need_resolve": 0, "headers": {"User-Agent": self.USER_AGENT, "Referer": embed_url, "Origin": embed_headers["Origin"], "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive"}}) + videoLinks.append({"name": "360p - Vortexvisionworks", "url": link_360, "need_resolve": 0, "headers": {"User-Agent": self.USER_AGENT, "Referer": embed_url, "Origin": embed_headers["Origin"], "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive"}}) + except Exception as e: + printDBG("Btolat embed HLS parse error: %s" % str(e)) + # =============================== + # 5) Auto-create empty metadata file to prevent FileNotFoundError + # =============================== + title_safe = cItem.get("title", "video").replace("/", "_") + metadata_dir = "/hdd/IPTVCache/MovieMetaData" + if not os.path.exists(metadata_dir): + os.makedirs(metadata_dir) + metadata_file = os.path.join(metadata_dir, "botolat_%s.iptv" % title_safe) + if not os.path.exists(metadata_file): + try: + with codecs.open(metadata_file, "w", "utf-8", "replace") as fp: + fp.write("") # ملف فارغ + printDBG("Created IPTV metadata file: %s" % metadata_file) + except Exception as e: + printDBG("Failed to create IPTV metadata file: %s | %s" % (metadata_file, str(e))) + if videoLinks: + self.saveIPTVMeta(cItem.get("title", "video"), videoLinks) + return videoLinks + + def saveIPTVMeta(self, title, videoLinks): + # Take the first valid link + if not videoLinks: + return + first_link = videoLinks[0]["url"] + # File path + safe_title = title.replace("/", "_").replace("\\", "_") + file_path = os.path.join("/hdd/IPTVCache/MovieMetaData", "botolat_%s.iptv" % safe_title) + try: + if not os.path.exists(os.path.dirname(file_path)): + os.makedirs(os.path.dirname(file_path)) + except: + pass + meta = {"host": "botolat", "title": title, "file_path": first_link} + try: + with codecs.open(file_path, "w", "utf-8") as fp: + json.dump(meta, fp, ensure_ascii=False) + printDBG("Created IPTV metadata file: %s" % file_path) + except Exception as e: + printDBG("Error creating IPTV metadata file: %s" % str(e)) + + def getTwitterVideoLinks(self, tweet_id): + printDBG("BtolatCom.getTwitterVideoLinks id[%s]" % tweet_id) + linksTab = [] + api = "https://cdn.syndication.twimg.com/tweet-result?id=%s&lang=ar" % tweet_id + sts, data = self.getPage(api) + if not sts: + return linksTab + try: + j = json.loads(data) + except Exception: + printDBG("Twitter JSON load error") + return linksTab + videos = [] + + def findVideos(obj): + if isinstance(obj, dict): + if obj.get("type") == "video" and "video_info" in obj: + variants = obj["video_info"].get("variants", []) + for v in variants: + url = v.get("url", "") + ctype = v.get("content_type", "") + bitrate = v.get("bitrate", 0) + if url.startswith("http"): + videos.append((url, ctype, bitrate)) + for v in obj.values(): + findVideos(v) + elif isinstance(obj, list): + for i in obj: + findVideos(i) + + findVideos(j) + used = set() + for url, ctype, bitrate in videos: + if url in used: + continue + used.add(url) + if "mpegURL" in ctype or url.endswith(".m3u8"): + name = "Twitter HLS" + else: + name = "Twitter %s" % (str(int(bitrate / 1000)) + "kbps" if bitrate else "MP4") + linksTab.append({"name": name, "url": url, "need_resolve": 0}) + printDBG("BtolatCom.getTwitterVideoLinks found links: %d" % len(linksTab)) + return linksTab + + def getVideoLinks(self, baseUrl): + printDBG("BtolatCom.getVideoLinks [%s]" % baseUrl) + baseUrl = strwithmeta(baseUrl) + urlTab = [] + # If YouTube link + if "youtube.com" in baseUrl or "youtu.be" in baseUrl: + printDBG("BtolatCom.getVideoLinks - YouTube link detected: %s" % baseUrl) + return self.up.getVideoLinkExt(baseUrl) + # If Twitter link + elif "twitter.com" in baseUrl or "platform.twitter.com" in baseUrl: + printDBG("BtolatCom.getVideoLinks - Twitter link detected: %s" % baseUrl) + urlTab.append({"name": "Twitter", "url": baseUrl, "need_resolve": 1}) + return urlTab + # If direct link + elif baseUrl.endswith((".mp4", ".m3u8", ".webm", ".flv")): + printDBG("BtolatCom.getVideoLinks - Direct video link detected: %s" % baseUrl) + # Extract headers from baseUrl if present + headers = {} + if hasattr(baseUrl, "meta") and baseUrl.meta: + headers = baseUrl.meta.get("headers", {}) + # Set link name based on source + name = "مباشر" + if "vortexvisionworks.com" in baseUrl: + name = "Vortex Vision HLS" + elif "videohatkora.com" in baseUrl: + name = "Video Hatkora HLS" + elif "gooforkoora.com" in baseUrl: + name = "Goofor Koora HLS" + elif "btolat.com" in baseUrl: + name = "Btolat HLS" + # Add default headers if missing + headers = {} + if hasattr(baseUrl, "meta") and baseUrl.meta: + headers = dict(baseUrl.meta.get("headers", {})) + if not headers: + headers = {"User-Agent": self.USER_AGENT, "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive"} + urlTab.append({"name": name, "url": str(baseUrl), "headers": headers}) + return urlTab + return urlTab + + def listSearchResult(self, cItem, searchPattern, searchType): + printDBG("BtolatCom.listSearchResult") + url = "https://www.btolat.com/news/Search" + post_data = {"word": searchPattern} + headers = {"User-Agent": self.USER_AGENT, "Content-Type": "application/json; charset=utf-8", "Accept": "*/*", "Origin": self.MAIN_URL, "Referer": self.MAIN_URL, "X-Requested-With": "XMLHttpRequest"} + found = False + try: + response = requests.post(url, headers=headers, json=post_data, timeout=15) + if response.status_code != 200: + printDBG("Search HTTP Error: %s" % response.status_code) + return + data = response.json() + for item in data: + seName = item.get("SeName", "") + if not seName.startswith("video/"): + continue + found = True + title = self.cleanHtmlStr(item.get("Title", "")) + img = self.getFullIconUrl(item.get("FullSizeImageUrl", "")) + video_url = self.getFullUrl(seName) + post_since = self.cleanHtmlStr(item.get("PostSince", "")) + desc = Y + "Published since (نُشر منذ): " + W + post_since if post_since else "" + params = {"good_for_fav": True, "category": "video", "title": title, "url": video_url, "icon": img, "desc": desc} + self.addVideo(params) + # ✅ If no videos found + if not found: + params = dict(cItem) + params.update({"category": "none", "title": Y + "لا توجد نتائج فيديو حاليا" + W}) + self.addMarker(params) + except Exception as e: + printDBG("Search Exception: %s" % str(e)) + + def getArticleContent(self, cItem): + printDBG("BtolatCom.getArticleContent [%s]" % cItem) + retTab = [] + url = cItem.get("url", "") + if not url: + return retTab + sts, data = self.getPage(url) + if not sts: + return retTab + icon = cItem.get("icon", "") + title = cItem.get("title", "") + # ===== Video state ===== + if cItem.get("category") == "video": + player_url = self.cm.ph.getSearchGroups(data, r']*>')[0].strip() or "" + if player_url: + return self.getArticleContent({"url": self.getFullUrl(player_url), "icon": icon, "title": title}) + # ===== Team / club state ===== + if cItem.get("category") == "list_league_team_videos": + team_url = cItem.get("team_url", "") or cItem.get("url", "") + if team_url: + sts, team_data = self.getPage(team_url) + if sts and team_data: + team_block = self.cm.ph.getDataBeetwenMarkers(team_data, '
      ", True)[1] + # Coach + coach = self.cm.ph.getSearchGroups(team_block, r'(?s)المدير الفني.*?]*itemprop="name"[^>]*>([^<]+)') + coach_name = coach[0].strip() if coach else "" + # Trophies + trophies_block = self.cm.ph.getDataBeetwenMarkers(team_data, "

      البطولات التي يشارك فيها", "

      ")[1] if "

      البطولات التي يشارك فيها" in team_data else "" + trophies = re.findall(r"(?s)

      .*?([^<]+)", trophies_block) + trophies_text = "\n".join([t.strip() for t in trophies]) if trophies else "" + fields = [("اسم الفريق", self.cm.ph.getSearchGroups(team_block, r']*itemprop="name"[^>]*>([^<]+)

      ')), ("تاريخ التأسيس", self.cm.ph.getSearchGroups(team_block, r'تاريخ التأسيس.*?]*itemprop="foundingDate"[^>]*>([^<]+)')), ("المدير الفني", [coach_name]), ("البلد", self.cm.ph.getSearchGroups(team_block, r'البلد.*?]*itemprop="addressCountry"[^>]*>([^<]+)')), ("الملعب", self.cm.ph.getSearchGroups(team_block, r'الملعب.*?]*itemprop="name"[^>]*>([^<]+)')), ("البطولات", [trophies_text])] # trophies displayed one per line + final_text = "" + for label, match_list in fields: + value = match_list[0].strip() if match_list else "" + if value: + final_text += "{}{}: {}{}\n".format(Y, label, W, value) + if not final_text: + final_text = CY + "لم يتم العثور على معلومات الفريق." + W + retTab.append({"title": fields[0][1][0].strip() if fields[0][1] else title, "text": final_text.strip(), "images": [{"title": fields[0][1][0].strip() if fields[0][1] else title, "url": icon}], "other_info": {}}) + return retTab + # ===== Normal player state ===== + info_block = self.cm.ph.getDataBeetwenMarkers(data, '
      ")[1] if data else "" + fields = [ + ("الاسم", self.cm.ph.getSearchGroups(data, r']*itemprop="name"[^>]*>([^<]+)')), + ("المركز", self.cm.ph.getSearchGroups(data, r'itemprop="jobTitle"[^>]*>([^<]+)')), + ("الفريق", self.cm.ph.getSearchGroups(self.cm.ph.getDataBeetwenMarkers(info_block, 'itemprop="memberOf"', "")[1] if info_block else "", r"]*>([^<]+)")), + ("الجنسية", self.cm.ph.getSearchGroups(info_block, r"الجنسيه.*?]*>([^<]+)") if info_block else []), + ("محل الميلاد", self.cm.ph.getSearchGroups(info_block, r'itemprop="addressCountry"[^>]*>([^<]+)]*>([^<]+)') if info_block else []), + ("العمر", self.cm.ph.getSearchGroups(info_block, r"السن.*?]*>([^<]+)") if info_block else []), + ("الطول", self.cm.ph.getSearchGroups(info_block, r'itemprop="height"[^>]*>([^<]+)') if info_block else []), + ("الوزن", self.cm.ph.getSearchGroups(info_block, r'itemprop="weight"[^>]*>([^<]+)') if info_block else []), + ] + final_text = "" + for label, match_list in fields: + value = match_list[0].strip() if match_list else "" + if value: + final_text += "{}{}: {}{}\n".format(Y, label, W, value) + if not final_text: + final_text = CY + "لم يتم العثور على معلومات اللاعب." + W + retTab.append({"title": fields[0][1][0].strip() if fields[0][1] else title, "text": final_text.strip(), "images": [{"title": fields[0][1][0].strip() if fields[0][1] else title, "url": icon}], "other_info": {}}) + return retTab + + def handleService(self, index, refresh=0, searchPattern="", searchType=""): + printDBG("BtolatCom.handleService start") + CBaseHostClass.handleService(self, index, refresh, searchPattern, searchType) + name = self.currItem.get("name", "") + category = self.currItem.get("category", "") + mode = self.currItem.get("mode", "") + printDBG("handleService: |||| name[%s], category[%s] " % (name, category)) + self.currList = [] + # MAIN MENU + if name is None and category == "": + self.listMainMenu({"name": "category"}) + # AllLeague PATH + elif category == "list_videos": + self.listVideos(self.currItem) + elif category == "list_leagues": + self.listLeagues(self.currItem) + elif category == "list_league_videos": + self.listLeagueVideos(self.currItem) + elif category == "list_league_team_videos": + self.listLeagueTeamVideos(self.currItem) + elif category == "list_all_league_videos": + self.listAllLeagueVideos(self.currItem) + # TopScorers PATH + elif category == "list_top_scorers": + self.listTopScorersMenu(self.currItem) + elif category == "list_league_top_scorers": + self.listLeagueTopScorers(self.currItem) + # Professionals PATH + elif category == "list_professionals": + self.listProfessionals(self.currItem) + # PLAYERS PATH + elif category == "list_players": + self.listPlayersMenu(self.currItem) + elif category == "list_league_teams": + self.listLeagueTeams(self.currItem) + elif category == "list_team_players": + self.listTeamPlayers(self.currItem) + elif category == "list_player_videos": + self.listPlayerVideos(self.currItem) + # SEARCH + elif category in ["search", "search_next_page"]: + cItem = dict(self.currItem) + cItem.update({"search_item": False, "name": "category"}) + self.listSearchResult(cItem, searchPattern, searchType) + # SEARCH HISTORY + elif category == "search_history": + self.listsHistory({"name": "history", "category": "search"}, "desc", _("Type: ")) + else: + printExc() + CBaseHostClass.endHandleService(self, index, refresh) + + +class IPTVHost(CHostBase): + def __init__(self): + CHostBase.__init__(self, BtolatCom(), True, []) + + def withArticleContent(self, cItem): + if cItem.get("type") == "video": + return True + url = cItem.get("url", "") + if "/player/" in url: + return True + if "/team/" in url: + return True + if "/players/" in url: + return True + return False diff --git a/IPTVPlayer/hosts/hostbrstej.py b/IPTVPlayer/hosts/hostbrstej.py index ebaa04e0..9d2ee6d1 100644 --- a/IPTVPlayer/hosts/hostbrstej.py +++ b/IPTVPlayer/hosts/hostbrstej.py @@ -25,9 +25,9 @@ W = E2ColoR("white") G = E2ColoR("green") R = E2ColoR("red") -################################################### +################################################### def gettytul(): return "https://hd1.brstej.com" @@ -380,33 +380,59 @@ def listSeasonEpisodes(self, cItem): def listSearchResult(self, cItem, searchPattern, searchType): printDBG("Brstej.listSearchResult |%s|" % searchPattern) url = self.getFullUrl("/ajax-search.php") + custom_headers = { + "Referer": "https://hd1.brstej.com/", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", + "Accept": "text/html, */*; q=0.01", + "Accept-Encoding": "gzip, deflate, br", + "Accept-Language": "en,ar;q=0.9", + "X-Requested-With": "XMLHttpRequest", + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + "Origin": "https://hd1.brstej.com", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + } post_data = {"queryString": searchPattern} - sts, data = self.getPage(url, post_data=post_data) - if not sts: + addParams = dict(self.defaultParams) + if "header" in addParams: + addParams["header"].update(custom_headers) + else: + addParams["header"] = custom_headers + addParams["cloudflare_params"] = {"cookie_file": self.COOKIE_FILE, "User-Agent": custom_headers["User-Agent"]} + sts, data = self.getPage(url, addParams=addParams, post_data=post_data) + if not sts or not data: + printDBG("Brstej.listSearchResult - No data received") + return + items = re.findall(r']*data-video-id=["\']([^"\']+)["\'][^>]*>.*?]+href=["\']([^"\']+)["\'][^>]*>([^<]+)', data, re.S | re.I) + if not items: + printDBG("Brstej.listSearchResult - No items found") + printDBG("Response: %s..." % data[:500]) return - items = re.findall(r']*data-video-id=["\']([^"\']+?)["\'][^>]*>.*?]+href=["\']([^"\']+?)["\'][^>]*>([^<]+)', data, re.S) added = set() for vid, item_url, title in items: - title = self.cleanTitle(title) + title = self.cleanTitle(title.strip()) item_url = self.getFullUrl(item_url.strip()) - if not title or not item_url: - continue - if item_url in added: + if not title or not item_url or item_url in added: continue added.add(item_url) icon = "" desc = "" sts_vid, vid_data = self.getPage(item_url) - if sts_vid: - icon_match = re.search(r']+src=[\'"]([^\'"]*thumbs/[^\'"]+)[\'"]', vid_data) - if icon_match: - icon = self.getFullIconUrl(icon_match.group(1).strip()) + if sts_vid and vid_data: + og_image = re.search(r']+property=["\']og:image["\'][^>]+content=["\']([^"\']+)["\']', vid_data, re.I) + if og_image: + icon = self.getFullIconUrl(og_image.group(1).strip()) + else: + thumb_match = re.search(r']+src=[\'"]([^\'"]*thumbs/[^\'"]+)[\'"]', vid_data, re.I) + if thumb_match: + icon = self.getFullIconUrl(thumb_match.group(1).strip()) duration = "" - quality = "" - dur_match = re.search(r']*class=["\']pm-label-duration["\'][^>]*>([^<]+)', vid_data) + dur_match = re.search(r']*class=["\']pm-label-duration["\'][^>]*>([^<]+)', vid_data, re.I) if dur_match: duration = dur_match.group(1).strip() - ribon_match = re.search(r']*class=["\']ribon["\'][^>]*>.*?]*class=["\']hot["\'][^>]*>.*?]*class=["\']after["\'][^>]*>.*?\s*([^<>\s].*?)\s*]*class=["\']before["\']', vid_data, re.S) + quality = "" + ribon_match = re.search(r']*class=["\']ribon["\'][^>]*>.*?]*class=["\']hot["\'][^>]*>.*?]*class=["\']after["\'][^>]*>([^<>]+?)\s*]*class=["\']before["\']', vid_data, re.S | re.I) if ribon_match: quality = ribon_match.group(1).strip() desc_parts = [] @@ -415,12 +441,37 @@ def listSearchResult(self, cItem, searchPattern, searchType): if duration: desc_parts.append(Y + "Duration: %s" % duration + W) desc = " | ".join(desc_parts) - is_series = bool(re.search(r'class="SeasonsEpisodes"', vid_data)) + title_lower = title.lower() + url_lower = item_url.lower() + if re.search(r"\bفيلم\b|movie\b|film\b", title_lower, re.I) and not re.search(r"مسلسل|حلقة", title_lower, re.I): + is_series = False + elif re.search(r"مسلسل|حلقة|season|episode|part\s*\d+", title_lower, re.I): + is_series = True + elif re.search(r"series|season|episode", url_lower, re.I): + is_series = True + else: + seasons_block = re.search(r']*id=["\']?seasons[^"\'>]*["\']?[^>]*>.*?
      ', vid_data, re.S | re.I) + if not seasons_block: + seasons_block = re.search(r']*class=["\'][^"\']*SeasonsEpisodes[^"\']*["\'][^>]*>(?:(?!]*class=["\']similar|recommended|also|قد-يعجبك).)*?', vid_data, re.S | re.I) + is_series = bool(seasons_block) + else: + title_lower = title.lower() + if re.search(r"مسلسل|حلقة|season|episode|part\s*\d+", title_lower, re.I): + is_series = True + else: + is_series = False + params = { + "good_for_fav": True, + "title": title, + "url": item_url, + "icon": icon, + "desc": desc, + } if is_series: - params = {"good_for_fav": True, "title": title, "url": item_url, "icon": icon, "desc": desc, "category": "explore_item", "type": "category"} + params.update({"category": "explore_item", "type": "category"}) self.addDir(params) else: - params = {"good_for_fav": True, "title": title, "url": item_url, "icon": icon, "desc": desc, "type": "video"} + params.update({"type": "video"}) self.addVideo(params) def getLinksForVideo(self, cItem): @@ -511,6 +562,7 @@ def baseN(num, b): res = digits[num % b] + res num //= b return res or "0" + for i in range(len(k) - 1, -1, -1): if k[i]: p = re.sub(r"\b%s\b" % baseN(i, a), k[i], p) @@ -519,6 +571,7 @@ def baseN(num, b): def localGetDomain(url): parsed_uri = urlparse(url) return "{uri.scheme}://{uri.netloc}/".format(uri=parsed_uri) + urlTab = [] try: printDBG("HDUP extractor start -> %s" % embed_url) @@ -573,6 +626,7 @@ def baseN(num, b): res = digits[num % b] + res num //= b return res or "0" + for i in range(len(k) - 1, -1, -1): if k[i]: p = re.sub(r"\b%s\b" % baseN(i, a), k[i], p) @@ -581,6 +635,7 @@ def baseN(num, b): def localGetDomain(url): parsed_uri = urlparse(url) return "{uri.scheme}://{uri.netloc}/".format(uri=parsed_uri) + urlTab = [] try: headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", "Referer": "https://hd1.brstej.com/"} @@ -692,10 +747,12 @@ def baseN(num, b): res = digits[num % b] + res num //= b return res or "0" + for i in range(int(c) - 1, -1, -1): if k[i]: p = re.sub(r"\b%s\b" % baseN(i, int(a)), k[i], p) return p + decoded_js = js_unpack(p, a, c, k.split("|")) else: decoded_js = html @@ -727,10 +784,12 @@ def baseN(num, base): res = digits[num % base] + res num //= base return res or "0" + for i in range(c - 1, -1, -1): if k[i]: p = re.sub(r"\b%s\b" % baseN(i, a), k[i], p) return p + decoded_js = html packed_match = re.search(r"eval\(function\(p,a,c,k,e,d\).+?\}\('(.+?)',(\d+),(\d+),'(.+?)'\.split\('\|'\)", html, re.S) if packed_match: @@ -741,7 +800,7 @@ def baseN(num, base): url = url.replace("\\/", "/").replace("\\", "") query = "" if "?" in url: - query = url[url.find("?"):] + query = url[url.find("?") :] if "master.m3u8" in url: sts, data = self.getPage(url, params) if sts: diff --git a/IPTVPlayer/hosts/hostcimalina.py b/IPTVPlayer/hosts/hostcimalina.py new file mode 100644 index 00000000..2c92840e --- /dev/null +++ b/IPTVPlayer/hosts/hostcimalina.py @@ -0,0 +1,747 @@ +# -*- coding: utf-8 -*- + +# Last modified: 9/5/2026 +# cimalina Host (Created By Dr HYTHAM MAHMOUD) + +from Components.config import ConfigSelection, ConfigText, config, getConfigListEntry +from Plugins.Extensions.IPTVPlayer.components.ihost import CBaseHostClass, CHostBase +from Plugins.Extensions.IPTVPlayer.components.iptvplayerinit import TranslateTXT as _ +from Plugins.Extensions.IPTVPlayer.tools.iptvtools import printDBG, printExc, MergeDicts +from Plugins.Extensions.IPTVPlayer.tools.iptvtypes import strwithmeta + +import re + +try: + import json +except Exception: + json = None + +try: + import base64 +except Exception: + base64 = None + +try: + basestring +except NameError: + basestring = str + +try: + from urllib.parse import quote_plus as urllib_quote_plus +except ImportError: + from urllib import quote_plus as urllib_quote_plus + +try: + from html import unescape as html_unescape +except Exception: + try: + from HTMLParser import HTMLParser + + html_unescape = HTMLParser().unescape + except Exception: + + def html_unescape(txt): + return txt + + +config.plugins.iptvplayer.cimalina_proxy = ConfigSelection(default="None", choices=[("None", _("None")), ("proxy_1", _("Alternative proxy server (1)")), ("proxy_2", _("Alternative proxy server (2)"))]) +config.plugins.iptvplayer.cimalina_alt_domain = ConfigText(default="", fixed_size=False) + + +def GetConfigList(): + optionList = [] + optionList.append(getConfigListEntry(_("Use proxy server:"), config.plugins.iptvplayer.cimalina_proxy)) + if config.plugins.iptvplayer.cimalina_proxy.value == "None": + optionList.append(getConfigListEntry(_("Alternative domain:"), config.plugins.iptvplayer.cimalina_alt_domain)) + return optionList + + +def gettytul(): + return "CimaLina" + + +class CimaLina(CBaseHostClass): + DOMAIN_CACHE = None + + def __init__(self): + CBaseHostClass.__init__(self, {"history": "cimalina", "cookie": "cimalina.cookie"}) + self.MAIN_URL = None + self.DEFAULT_ICON_URL = "https://up6.cc/2026/05/177764060279971.png" + + self.HEADER = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", "Accept-Language": "en-US,en;q=0.9,ar;q=0.8,en-GB;q=0.7", "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", "DNT": 1} + + self.AJAX_HEADER = dict(self.HEADER) + self.AJAX_HEADER.update({"X-Requested-With": "XMLHttpRequest"}) + + self.MEGAMAX_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36" + + self.defaultParams = {"header": self.HEADER, "use_cookie": True, "load_cookie": True, "save_cookie": True, "cookiefile": self.COOKIE_FILE, "return_data": True} + + self.cacheLinks = {} + self.cacheMegamax = {} + self.cacheHostNames = {} + self.cacheArticles = {} + self.cacheCatItems = {} + self.cachePages = {} + self.homePageData = None + + def getProxy(self): + proxy = config.plugins.iptvplayer.cimalina_proxy.value + if proxy != "None": + if proxy == "proxy_1": + try: + proxy = config.plugins.iptvplayer.alternativeproxy1.value + except Exception: + proxy = None + else: + try: + proxy = config.plugins.iptvplayer.alternativeproxy2.value + except Exception: + proxy = None + else: + proxy = None + return proxy + + def getPage(self, baseUrl, addParams=None, post_data=None): + if addParams is None: + addParams = dict(self.defaultParams) + else: + addParams = dict(addParams) + + for k, v in self.defaultParams.items(): + if k not in addParams: + addParams[k] = v + + proxy = self.getProxy() + if proxy and "http_proxy" not in addParams: + addParams = MergeDicts(addParams, {"http_proxy": proxy}) + + cacheKey = None + canCache = post_data is None and addParams.get("header", {}) == self.HEADER + if canCache: + cacheKey = str(baseUrl) + if cacheKey in self.cachePages: + return True, self.cachePages[cacheKey] + + sts, data = self.cm.getPage(baseUrl, addParams, post_data) + if sts and canCache and data: + self.cachePages[cacheKey] = data + return sts, data + + def _getHostNameCached(self, url): + url = self._normUrl(url) + if not url: + return "" + if url in self.cacheHostNames: + return self.cacheHostNames[url] + host = "" + try: + host = self.up.getHostName(url, True) + except Exception: + pass + self.cacheHostNames[url] = host + return host + + def _getHomePageData(self): + if self.homePageData is not None: + return True, self.homePageData + sts, data = self.getPage(self.getMainUrl()) + if sts: + self.homePageData = data + return sts, data + + def selectDomain(self): + if CimaLina.DOMAIN_CACHE: + self.setMainUrl(CimaLina.DOMAIN_CACHE) + self.MAIN_URL = self.getMainUrl() + return + + domains = ["https://2.cema-lin.shop/", "https://cema-lin.shop/", "https://cimalena.cfd/", "https://cimalina.live/"] + + altDomain = config.plugins.iptvplayer.cimalina_alt_domain.value.strip() + if self.cm.isValidUrl(altDomain): + if not altDomain.endswith("/"): + altDomain += "/" + domains.insert(0, altDomain) + + for domain in domains: + sts, data = self.getPage(domain) + if sts and data and ("سيما" in data or "Cima" in data or "cima" in data): + try: + self.setMainUrl(self.cm.meta["url"]) + except Exception: + self.setMainUrl(domain) + self.MAIN_URL = self.getMainUrl() + CimaLina.DOMAIN_CACHE = self.MAIN_URL + return + + self.setMainUrl(domains[0]) + self.MAIN_URL = self.getMainUrl() + CimaLina.DOMAIN_CACHE = self.MAIN_URL + + def getFullIconUrl(self, url): + url = (url or "").strip() + url = CBaseHostClass.getFullIconUrl(self, url) + if not url: + return "" + proxy = self.getProxy() + if proxy: + url = strwithmeta(url, {"iptv_http_proxy": proxy}) + return url + + def listMainMenu(self, cItem): + printDBG("CimaLina.listMainMenu") + tab = [{"category": "movies", "title": _("الأفلام"), "icon": self.DEFAULT_ICON_URL}, {"category": "series", "title": _("المسلسلات"), "icon": self.DEFAULT_ICON_URL}, {"category": "search", "title": _("Search"), "search_item": True}, {"category": "search_history", "title": _("Search history")}] + self.listsTab(tab, cItem) + + def listCatItems(self, cItem, nextCategory): + printDBG("CimaLina.listCatItems") + cat = self.currItem.get("category", "") + + if cat in self.cacheCatItems: + cachedItems = self.cacheCatItems[cat] + else: + sts, data = self._getHomePageData() + if not sts: + return + + menuIds = {"movies": "menu-item-380427", "series": "menu-item-380436"} + menuId = menuIds.get(cat, "") + + section = "" + if menuId: + try: + startPattern = re.compile(r'<[^>]+class=["\'][^"\']*%s[^"\']*["\']' % re.escape(menuId), re.I) + section = self.cm.ph.getDataBeetwenReMarkers(data, startPattern, re.compile("
    ", re.I), True)[1] + except Exception: + section = "" + + cachedItems = [] + items = self.cm.ph.getAllItemsBeetwenMarkers(section, "") + for item in items: + url = self.getFullUrl(self.cm.ph.getSearchGroups(item, r'href=["\']([^"\']+?)["\']')[0]) + title = self.cleanHtmlStr(item) + if title and title not in ["anime4up", "DMCA", "اتصل بنا"] and url: + cachedItems.append({"title": title, "url": url}) + + self.cacheCatItems[cat] = cachedItems + + for item in cachedItems: + params = dict(cItem) + params.update({"category": nextCategory, "media_type": cat, "good_for_fav": True, "title": item["title"], "url": item["url"], "icon": cItem.get("icon", self.DEFAULT_ICON_URL), "desc": ""}) + self.addDir(params) + + def listItems(self, cItem, nextCategory): + printDBG("CimaLina.listItems") + page = cItem.get("page", 1) + mediaType = cItem.get("media_type", "") + url = cItem.get("url", "") + + sts, data = self.getPage(url) + if not sts: + return + + pagination = self.cm.ph.getDataBeetwenMarkers(data, '
    ", False)[1] + nextPageUrl = self.getFullUrl(self.cm.ph.getSearchGroups(pagination, r'href=["\']([^"\']+?)["\'][^>]*?>\s*%s\s*<' % (page + 1))[0]) + + section = self.cm.ph.getDataBeetwenMarkers(data, "moviesBlocks", "footer", False)[1] + items = self.cm.ph.getAllItemsBeetwenMarkers(section, "") + + for item in items: + if "movie" not in item: + continue + + icon = self.getFullIconUrl(self.cm.ph.getSearchGroups(item, r'src=["\']([^"\']+?)["\']')[0]) + itemUrl = self.getFullUrl(self.cm.ph.getSearchGroups(item, r'href=["\']([^"\']+?)["\']')[0]) + title = self.cleanHtmlStr(self.cm.ph.getDataBeetwenNodes(item, (""), (""), False)[1]) + + if not title or not itemUrl: + continue + + desc = "" + rating = self.cleanHtmlStr(self.cm.ph.getSearchGroups(item, r"imdbRating[^>]*>\s*([^<]+?)\s*<")[0]) + if rating: + desc += "Rating: %s | " % rating + + genre = self.cleanHtmlStr(self.cm.ph.getSearchGroups(item, r"category[^>]*>\s*([^<]+?)\s*<")[0]) + if genre: + desc += "Genre: %s | " % genre + + year = self.cm.ph.getSearchGroups(item, r"([12][0-9]{3})")[0] + if year: + desc += "Year: %s" % year + + desc = desc.strip(" |") + + params = dict(cItem) + params.update({"good_for_fav": True, "title": title, "url": itemUrl, "prev_url": itemUrl, "icon": icon or self.DEFAULT_ICON_URL, "desc": desc}) + + if "/assemblies/" in itemUrl or "/selary/" in itemUrl: + params["category"] = nextCategory + self.addDir(params) + elif mediaType == "movies": + params["urlSeparateRequest"] = 1 + self.addVideo(params) + else: + params["category"] = nextCategory + self.addDir(params) + + if nextPageUrl: + params = dict(cItem) + params.update({"title": _("Next page"), "url": nextPageUrl, "page": page + 1}) + self.addDir(params) + + def exploreItems(self, cItem): + printDBG("CimaLina.exploreItems") + page = cItem.get("page", 1) + url = cItem.get("url", "") + + sts, data = self.getPage(url) + if not sts: + return + + cItem["prev_url"] = url + storyDesc = self.cleanHtmlStr(self.cm.ph.getDataBeetwenNodes(data, ("", "StoryMovie"), (""), False)[1]) + + if "/selary/" in url or "/assemblies/" in url: + section = self.cm.ph.getDataBeetwenMarkers(data, "moviesBlocks", "footer", False)[1] + pagination = self.cm.ph.getDataBeetwenMarkers(section, '
    ", False)[1] + nextPageUrl = self.getFullUrl(self.cm.ph.getSearchGroups(pagination, r'href=["\']([^"\']+?)["\'][^>]*?>\s*%s\s*<' % (page + 1))[0]) + + items = self.cm.ph.getAllItemsBeetwenMarkers(section, "") + for item in items: + if "movie" not in item: + continue + + icon = self.getFullIconUrl(self.cm.ph.getSearchGroups(item, r'src=["\']([^"\']+?)["\']')[0]) + if not icon: + icon = cItem.get("icon", self.DEFAULT_ICON_URL) + + itemUrl = self.getFullUrl(self.cm.ph.getSearchGroups(item, r'href=["\']([^"\']+?)["\']')[0]) + title = self.cleanHtmlStr(self.cm.ph.getDataBeetwenNodes(item, (""), (""), False)[1]) + + if not title or not itemUrl: + continue + + params = dict(cItem) + params.update({"good_for_fav": True, "title": title, "url": itemUrl, "prev_url": itemUrl, "icon": icon, "desc": storyDesc, "urlSeparateRequest": 1}) + self.addVideo(params) + + if nextPageUrl: + params = dict(cItem) + params.update({"title": _("Next page"), "url": nextPageUrl, "page": page + 1}) + self.addMore(params) + else: + params = dict(cItem) + params.update({"good_for_fav": True, "title": cItem.get("title", ""), "url": url, "prev_url": url, "icon": cItem.get("icon", self.DEFAULT_ICON_URL), "desc": storyDesc, "urlSeparateRequest": 1}) + self.addVideo(params) + + def listSearchResult(self, cItem, searchPattern, searchType): + printDBG("CimaLina.listSearchResult") + if searchType == "all": + url = self.getFullUrl("/?s=%s" % urllib_quote_plus(searchPattern)) + elif searchType == "movies": + url = self.getFullUrl("/?s=%s" % urllib_quote_plus("فيلم " + searchPattern)) + else: + url = self.getFullUrl("/?s=%s" % urllib_quote_plus("مسلسل " + searchPattern)) + + params = {"name": "category", "media_type": searchType, "good_for_fav": False, "url": url} + self.listItems(params, "explore_item") + + def _normUrl(self, url): + if not url: + return "" + url = html_unescape(url).strip() + url = url.replace("\\/", "/").replace("&", "&") + if url.startswith("//"): + url = "https:" + url + return self.getFullUrl(url) + + def _isMegamaxUrl(self, url): + low = (url or "").lower() + return "megamax.me/" in low or "megamax.cam/" in low + + def _appendUniqueLink(self, retTab, name, url, need_resolve=1): + url = self._normUrl(url) + if not url or not self.cm.isValidUrl(url): + return False + + for item in retTab: + if str(item.get("url", "")) == str(url): + return False + + retTab.append({"name": name, "url": url, "need_resolve": need_resolve}) + return True + + def _extractIframeSrc(self, html): + html = html_unescape(html or "") + if not html: + return "" + + patterns = [r']+src=["\']([^"\']+?)["\']', r"]+src=([^\s>]+)", r'src=["\']([^"\']+?)["\']', r"src=([^\s>]+)"] + for pattern in patterns: + url = self.cm.ph.getSearchGroups(html, pattern)[0] + if url: + url = url.replace(""", '"').replace("'", "'").strip() + url = url.strip("'\"") + return self._normUrl(url) + return "" + + def _decodeBase64JsonValue(self, value): + if not value or not base64 or not json: + return {} + try: + value = html_unescape(value).strip() + missing = len(value) % 4 + if missing: + value += "=" * (4 - missing) + + txt = base64.b64decode(value) + if not isinstance(txt, type("")): + try: + txt = txt.decode("utf-8") + except Exception: + txt = txt.decode("utf-8", "ignore") + return json.loads(txt) + except Exception: + printExc() + return {} + + def _extractWatchForm(self, data): + if not data: + return "" + + patterns = [r'(]+id=["\']formWatch["\'][\s\S]+?)', r'(]+class=["\'][^"\']*formWatch[^"\']*["\'][\s\S]+?)', r'(]+method=["\']post["\'][\s\S]+?)'] + for pattern in patterns: + try: + m = re.search(pattern, data, re.I) + if m: + return m.group(1) + except Exception: + pass + return "" + + def _getInputValue(self, formData, name): + if not formData: + return "" + patterns = [r'name=["\']%s["\'][^>]*value=["\']([^"\']+?)["\']' % re.escape(name), r'value=["\']([^"\']+?)["\'][^>]*name=["\']%s["\']' % re.escape(name)] + for pattern in patterns: + val = self.cm.ph.getSearchGroups(formData, pattern)[0] + if val: + return html_unescape(val).strip() + return "" + + def _findMegamaxInertiaVersion(self, data): + if not data: + return "" + patterns = [r'"version"\s*:\s*"([^"]+?)"', r"'version'\s*:\s*'([^']+?)'", r'X-Inertia-Version["\']?\s*[:=]\s*["\']([^"\']+?)["\']'] + for pattern in patterns: + try: + m = re.search(pattern, data, re.I) + if m: + return m.group(1) + except Exception: + pass + return "" + + def _parseMegamaxInertiaJson(self, rawJson, cItemTitle): + retTab = [] + if not json or not rawJson: + return retTab + + try: + obj = json.loads(rawJson) + streams = obj.get("props", {}).get("streams", {}) + if streams.get("status") != "success": + return retTab + + for qualityObj in streams.get("data", []): + label = qualityObj.get("label", "Default") + for mirror in qualityObj.get("mirrors", []): + driver = mirror.get("driver", "") + symbol = mirror.get("symbol", "") + link = mirror.get("link", "") + if not link: + continue + link = self._normUrl(link) + if not self.cm.isValidUrl(link): + continue + displayName = symbol if symbol else driver + self._appendUniqueLink(retTab, "%s [%s] - %s" % (cItemTitle, label, displayName), link, 1) + except Exception: + printExc() + + return retTab + + def _expandMegamaxToMirrors(self, iframeUrl, cItemTitle): + retTab = [] + iframeUrl = self._normUrl(iframeUrl) + if not iframeUrl: + return retTab + + if iframeUrl in self.cacheMegamax: + return list(self.cacheMegamax[iframeUrl]) + + pageParams = {"return_data": True, "use_cookie": True, "load_cookie": True, "save_cookie": True, "cookiefile": self.COOKIE_FILE, "header": {"User-Agent": self.MEGAMAX_UA}} + + sts, htmlData = self.getPage(iframeUrl, pageParams) + if not sts: + return retTab + + version = self._findMegamaxInertiaVersion(htmlData) + + inertiaHeaders = {"User-Agent": self.MEGAMAX_UA, "Referer": iframeUrl, "Sec-Fetch-Mode": "cors", "X-Inertia": "true", "X-Inertia-Partial-Component": "files/mirror/video", "X-Inertia-Partial-Data": "streams"} + if version: + inertiaHeaders["X-Inertia-Version"] = version + + inertiaParams = {"return_data": True, "use_cookie": True, "load_cookie": True, "save_cookie": True, "cookiefile": self.COOKIE_FILE, "header": inertiaHeaders} + + sts, jsonData = self.getPage(iframeUrl, inertiaParams) + if sts and jsonData: + retTab = self._parseMegamaxInertiaJson(jsonData, cItemTitle) + + self.cacheMegamax[iframeUrl] = list(retTab) + return retTab + + def _appendMegamaxLinks(self, retTab, cItemTitle, iframeUrl, label="Default"): + expanded = self._expandMegamaxToMirrors(iframeUrl, cItemTitle) + if expanded: + added = False + for item in expanded: + if self._appendUniqueLink(retTab, item.get("name", cItemTitle), item.get("url", ""), item.get("need_resolve", 1)): + added = True + if added: + return True + + lazyUrl = strwithmeta(self._normUrl(iframeUrl), {"cimalina_megamax_lazy": "1", "cimalina_title": cItemTitle, "cimalina_label": label}) + hostName = self._getHostNameCached(iframeUrl) or "Megamax" + return self._appendUniqueLink(retTab, "%s [%s] - %s" % (cItemTitle, label, hostName), lazyUrl, 1) + + def _appendLinksFromEncodedMap(self, retTab, encodedData, cItemTitle): + try: + dataMap = self._decodeBase64JsonValue(encodedData) + if not isinstance(dataMap, dict): + return + + idx = 0 + for key in dataMap: + idx += 1 + link = self._normUrl(dataMap[key]) + if not link: + continue + + label = self.cleanHtmlStr(key) or ("Server %d" % idx) + + if self._isMegamaxUrl(link): + self._appendMegamaxLinks(retTab, cItemTitle, link, label) + else: + hostName = self._getHostNameCached(link) + self._appendUniqueLink(retTab, "%s [%s] - %s" % (cItemTitle, label, hostName), link, 1) + except Exception: + printExc() + + def _parseServersPage(self, pageData, cItem): + retTab = [] + cItemTitle = cItem.get("title", "") + + serversSection = self.cm.ph.getDataBeetwenMarkers(pageData, ("", "serversList"), (""), True)[1] + + if serversSection: + items = self.cm.ph.getAllItemsBeetwenMarkers(serversSection, "") + for item in items: + serverName = self.cleanHtmlStr(item) or "Server" + serverData = self.cm.ph.getSearchGroups(item, r'data-server=["\']([^"\']+?)["\']')[0] + if not serverData: + serverData = self.cm.ph.getSearchGroups(item, r"data-server=([^>]+)")[0] + + iframeUrl = self._extractIframeSrc(serverData) + if not iframeUrl: + rawUrl = self.cm.ph.getSearchGroups(serverData, r'(https?://[^\s"\']+|//[^\s"\']+)')[0] + iframeUrl = self._normUrl(rawUrl) + + if not iframeUrl: + continue + + if self._isMegamaxUrl(iframeUrl): + self._appendMegamaxLinks(retTab, cItemTitle, iframeUrl, serverName) + else: + hostName = self._getHostNameCached(iframeUrl) + self._appendUniqueLink(retTab, "%s [%s] - %s" % (cItemTitle, serverName, hostName), iframeUrl, 1) + + if not retTab: + embedSection = self.cm.ph.getDataBeetwenMarkers(pageData, ("", "embedServer"), (""), True)[1] + iframeUrl = self._extractIframeSrc(embedSection) + if iframeUrl: + if self._isMegamaxUrl(iframeUrl): + self._appendMegamaxLinks(retTab, cItemTitle, iframeUrl, "Default") + else: + hostName = self._getHostNameCached(iframeUrl) + self._appendUniqueLink(retTab, "%s [Default] - %s" % (cItemTitle, hostName), iframeUrl, 1) + + return retTab + + def _removeDefaultFallbackIfNeeded(self, retTab): + if len(retTab) <= 1: + return retTab + + cleanTab = [] + for item in retTab: + name = item.get("name", "") + if " [Default] - " in name: + continue + cleanTab.append(item) + + if cleanTab: + return cleanTab + return retTab + + def getLinksForVideo(self, cItem): + printDBG("CimaLina.getLinksForVideo [%s]" % cItem) + + url = cItem.get("prev_url", cItem.get("url", "")) + cacheKey = str(url) + if cacheKey in self.cacheLinks: + return self.cacheLinks[cacheKey] + + retTab = [] + ajaxParams = dict(self.defaultParams) + ajaxParams["header"] = self.AJAX_HEADER + + sts, data = self.getPage(url) + if not sts: + return retTab + + formData = self._extractWatchForm(data) + if formData: + actionUrl = self.getFullUrl(self.cm.ph.getSearchGroups(formData, r'action=["\']([^"\']+?)["\']')[0]) + servers = self._getInputValue(formData, "servers") + downloads = self._getInputValue(formData, "downloads") + + if actionUrl: + sts2, postData = self.getPage(actionUrl, ajaxParams, {"servers": servers, "downloads": downloads, "submit": ""}) + if sts2 and postData: + retTab.extend(self._parseServersPage(postData, cItem)) + + self._appendLinksFromEncodedMap(retTab, servers, cItem.get("title", "")) + self._appendLinksFromEncodedMap(retTab, downloads, cItem.get("title", "")) + + if not retTab: + retTab.extend(self._parseServersPage(data, cItem)) + + retTab = self._removeDefaultFallbackIfNeeded(retTab) + self.cacheLinks[cacheKey] = retTab + printDBG("CimaLina.getLinksForVideo -> final count[%d]" % len(retTab)) + return retTab + + def getVideoLinks(self, videoUrl): + printDBG("CimaLina.getVideoLinks [%s]" % videoUrl) + if not self.cm.isValidUrl(videoUrl): + return [] + + meta = {} + try: + meta = getattr(videoUrl, "meta", {}) + except Exception: + meta = {} + + if str(meta.get("cimalina_megamax_lazy", "")) == "1" or self._isMegamaxUrl(videoUrl): + title = meta.get("cimalina_title", "Megamax") + mirrors = self._expandMegamaxToMirrors(str(videoUrl), title) + for item in mirrors: + link = item.get("url", "") + if not link: + continue + try: + videoTab = self.up.getVideoLinkExt(link) + if videoTab: + return videoTab + except Exception: + printExc() + try: + return self.up.getVideoLinkExt(str(videoUrl)) + except Exception: + printExc() + return [] + + return self.up.getVideoLinkExt(videoUrl) + + def getArticleContent(self, cItem): + printDBG("CimaLina.getArticleContent") + otherInfo = {} + url = cItem.get("prev_url", cItem.get("url", "")) + + if url in self.cacheArticles: + return self.cacheArticles[url] + + sts, data = self.getPage(url) + if not sts: + return [] + + section = self.cm.ph.getDataBeetwenMarkers(data, ("", "MovieDetails"), ("", "socialSharer"), True)[1] + + duration = self.cleanHtmlStr(self.cm.ph.getDataBeetwenNodes(data, ("مده العرض", ">"), (""), False)[1]) + if duration: + otherInfo["duration"] = duration + + year = self.cleanHtmlStr(self.cm.ph.getDataBeetwenNodes(data, ("release-year", ">"), (""), False)[1]) + if year: + otherInfo["year"] = year + + genre = self.cleanHtmlStr(self.cm.ph.getDataBeetwenNodes(section, ("", "genre"), (""), False)[1]) + if genre: + otherInfo["genre"] = genre + + category = self.cleanHtmlStr(self.cm.ph.getDataBeetwenNodes(section, ("", "category"), (""), False)[1]) + if category: + otherInfo["category"] = category + + desc = self.cleanHtmlStr(self.cm.ph.getDataBeetwenNodes(data, ("", "StoryMovie"), (""), False)[1]) + if not desc: + desc = cItem.get("desc", "") + + article = [{"title": cItem.get("title", ""), "text": desc, "images": [{"title": "", "url": cItem.get("icon", "")}], "other_info": otherInfo}] + self.cacheArticles[url] = article + return article + + def handleService(self, index, refresh=0, searchPattern="", searchType=""): + printDBG("CimaLina.handleService start") + CBaseHostClass.handleService(self, index, refresh, searchPattern, searchType) + + if self.MAIN_URL is None: + self.selectDomain() + + name = self.currItem.get("name", "") + category = self.currItem.get("category", "") + + self.currList = [] + + try: + if not name and not category: + self.listMainMenu({"name": "category"}) + elif category in ("movies", "series"): + self.listCatItems(self.currItem, "listItems") + elif category == "listItems": + self.listItems(self.currItem, "explore_item") + elif category == "explore_item": + self.exploreItems(self.currItem) + elif category in ("search", "search_next_page"): + params = dict(self.currItem) + params.update({"search_item": False, "name": "category"}) + self.listSearchResult(params, searchPattern, searchType) + elif category == "search_history": + self.listsHistory({"name": "history", "category": "search"}, "desc", _("Type: ")) + except Exception: + printExc() + + CBaseHostClass.endHandleService(self, index, refresh) + + +class IPTVHost(CHostBase): + def __init__(self): + CHostBase.__init__(self, CimaLina(), True) + + def getSearchTypes(self): + return [("All", "all"), ("Movies", "movies"), ("Tv Series", "series")] + + def withArticleContent(self, cItem): + return "prev_url" in cItem or cItem.get("category", "") == "explore_item" diff --git a/IPTVPlayer/hosts/hostegydead.py b/IPTVPlayer/hosts/hostegydead.py index 195728e8..2dd90f28 100644 --- a/IPTVPlayer/hosts/hostegydead.py +++ b/IPTVPlayer/hosts/hostegydead.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Original File from: 24/10/2025 - popking (odem2014) -# Last modified: 20/04/2026 - Mohamed Elsafty (angel_heart) +# Last modified: 24/10/2025 - popking (odem2014) +# Last modified: 17/05/2026 - Mohamed Elsafty (angel_heart) # typical import for a standard host ################################################### # LOCAL import @@ -41,7 +41,7 @@ def GetConfigList(): def gettytul(): - return "https://tv8.egydead.live" # main url of host + return "https://c4u1r.sbs" # main url of host class EgyDead(CBaseHostClass): @@ -329,6 +329,8 @@ def exploreItems(self, cItem): if not sts or not data1: return info_text, story, full_desc = self._extractMetadata(data1) + # --- Work Title --- + work_title = cItem.get("title", "").strip() if '
    ' in data1: printDBG("Season page detected — listing episodes...") cItem = dict(cItem) @@ -338,7 +340,6 @@ def exploreItems(self, cItem): params.update({"header": {"Content-Type": "application/x-www-form-urlencoded", "Referer": url}}) post_data = {"View": "1"} sts, data = self.getPage(url, params, post_data) - sts, data = self.getPage(url, params, post_data) if not sts or not data: return if "salery-list" in data1: @@ -363,8 +364,13 @@ def exploreItems(self, cItem): if not title: title = self.cm.ph.getSearchGroups(item, r">([^<]+)")[0].strip() title = self.cleanHtmlStr(title) + # --- Combine Movie Title + Server Name --- + if work_title: + video_title = "%s%s%s - [%s]" % (E2ColoR("yellow"), work_title, E2ColoR("white"), title) + else: + video_title = title params = dict(cItem) - params.update({"title": title, "url": video_url, "category": "video", "type": "video", "desc": full_desc}) + params.update({"title": video_title, "url": video_url, "category": "video", "type": "video", "desc": full_desc}) self.addVideo(params) def listEpisodes(self, cItem, data1): diff --git a/IPTVPlayer/hosts/hostfootyroom.py b/IPTVPlayer/hosts/hostfootyroom.py index fb2118be..54b52473 100644 --- a/IPTVPlayer/hosts/hostfootyroom.py +++ b/IPTVPlayer/hosts/hostfootyroom.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- -# Last modified: 14/1/2026 +# Last modified: 13/1/2026 # footyroom Host (Created By Dr HYTHAM MAHMOUD) -import re -import json - from Plugins.Extensions.IPTVPlayer.components.ihost import CHostBase, CBaseHostClass from Plugins.Extensions.IPTVPlayer.tools.iptvtools import printDBG, printExc +import re +import json def GetConfigList(): @@ -24,12 +23,68 @@ def __init__(self): CBaseHostClass.__init__(self, {"history": "footyroom.co", "cookie": "footyroom.co.cookie"}) self.MAIN_URL = "https://footyroom.co/" self.DEFAULT_ICON_URL = "https://cdn.footyroom.co/pics/iphone/1024x1024.png" + self.USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari" self.HTTP_HEADER = self.cm.getDefaultHeader(browser="chrome") - self.HTTP_HEADER.update({"Referer": self.MAIN_URL}) + self.HTTP_HEADER.update({"User-Agent": self.USER_AGENT, "Referer": self.MAIN_URL}) self.defaultParams = {"header": self.HTTP_HEADER, "use_cookie": True, "load_cookie": True, "save_cookie": True, "cookiefile": self.COOKIE_FILE} self._countriesCache = None self._matchesCache = {} # key: base_url → value: list of all matches + # ----------------- MENU TRANSLATIONS ----------------- + _MENU_TR = { + # Countries / Sections + "England": "إنجلترا", + "Spain": "إسبانيا", + "Italy": "إيطاليا", + "Germany": "ألمانيا", + "France": "فرنسا", + "Portugal": "البرتغال", + "Netherlands": "هولندا", + "Turkey": "تركيا", + "Saudi Arabia": "السعودية", + "USA": "الولايات المتحدة", + "United States": "الولايات المتحدة", + "Australia": "أستراليا", + "Russia": "روسيا", + "Europe": "أوروبا", + "International": "دولي", + # Generic + "Competition": "بطولة", + # Competitions + "Premier League": "الدوري الإنجليزي الممتاز", + "La Liga": "الدوري الإسباني", + "Serie A": "الدوري الإيطالي", + "Bundesliga": "الدوري الألماني", + "Ligue 1": "الدوري الفرنسي", + "League Cup": "كأس الرابطة", + "Community Shield": "درع المجتمع", + "FA Cup": "كأس الاتحاد الإنجليزي", + "AFC Champions League": "دوري أبطال آسيا", + "World Cup": "كأس العالم", + "Copa America": "كوبا أمريكا", + "Africa Cup of Nations": "كأس أمم أفريقيا", + "International Friendlies": "مباريات دولية ودية", + "Club Friendlies": "مباريات الأندية الودية", + "Club World Cup": "كأس العالم للأندية", + "World Cup U-20": "كأس العالم تحت 20", + "AFF Suzuki Cup": "كأس اتحاد آسيان (AFF)", + "Asian Cup": "كأس آسيا", + "CONCACAF Gold Cup": "الكأس الذهبية (كونكاكاف)", + "CONCACAF Champions League": "دوري أبطال كونكاكاف", + } + + def _trMenuTitle(self, title): + """Translate main/sub menu titles only (countries/competitions).""" + t = (title or "").strip() + if not t: + return t + # Exact match first + if t in self._MENU_TR: + return self._MENU_TR[t] + # Light normalization + t2 = re.sub(r"\s+", " ", t).strip() + return self._MENU_TR.get(t2, t) + def _t(self, s): """Clean HTML text""" if not s: @@ -75,7 +130,7 @@ def _absoluteUrl(self, url): return "" if url.startswith("//"): return "https:" + url - if url.startswith("https://"): + if url.startswith("http://") or url.startswith("https://"): return url return self.getFullUrl(url) @@ -238,11 +293,6 @@ def _loadAllMatchesForCompetition(self, base_url): if new_count == 0: printDBG("FootyRoom: No new matches on page %d, stopping" % page_num) break - # إذا جمعنا عدد كافي للـ pagination (مثلاً 100+)، يمكن التوقف - # لكن نترك الخيار لجلب كل شيء حتى MAX_PAGES - # إذا تريد توقف بدري: uncomment السطر التالي - # if len(all_matches) >= 100: - # break printDBG("FootyRoom: Total matches collected: %d" % len(all_matches)) return all_matches @@ -252,7 +302,7 @@ def listMainMenu(self, cItem): self.currList = [] countries = self._getCountries(force=True) for idx, item in enumerate(countries): - self.addDir({"name": "category", "title": item.get("title", ""), "category": "list_competitions", "country_idx": str(idx), "icon": self.DEFAULT_ICON_URL}) + self.addDir({"name": "category", "title": self._trMenuTitle(item.get("title", "")), "category": "list_competitions", "country_idx": str(idx), "icon": self.DEFAULT_ICON_URL}) def listCompetitions(self, cItem): """عرض البطولات لدولة معينة""" @@ -267,7 +317,7 @@ def listCompetitions(self, cItem): comps = countries[idx].get("comps", []) or [] printDBG("FootyRoom.listCompetitions: country=%s comps=%d" % (cItem.get("title", ""), len(comps))) for comp in comps: - title = (comp.get("title") or "").strip() or "Competition" + title = self._trMenuTitle((comp.get("title") or "").strip() or "Competition") url = (comp.get("url") or "").strip() if not url: continue @@ -300,10 +350,10 @@ def listMatches(self, cItem): # عرض المباريات for match in page_matches: self.addVideo({"name": "video", "title": match.get("title", ""), "url": match.get("url", ""), "icon": match.get("icon", self.DEFAULT_ICON_URL)}) - # عرض Next page إذا يوجد مباريات أكثر + # عرض الصفحة التالية إذا يوجد مباريات أكثر if end_idx < total_matches: params = dict(cItem) - params.update({"title": "Next page", "category": "list_matches", "page": page + 1}) + params.update({"title": "الصفحة التالية", "category": "list_matches", "page": page + 1}) self.addDir(params) # ----------------- VIDEO LINKS ----------------- diff --git a/IPTVPlayer/hosts/hostgroups.txt b/IPTVPlayer/hosts/hostgroups.txt index 50a06abe..71b120d0 100644 --- a/IPTVPlayer/hosts/hostgroups.txt +++ b/IPTVPlayer/hosts/hostgroups.txt @@ -193,6 +193,10 @@ "ifilm", "asi1tv", "krmzy", + "alooytv", + "aradrama", + "botolat", + "cimalina", "wecima", "mycima", "q3isk", diff --git a/IPTVPlayer/hosts/hosthwnaturkya.py b/IPTVPlayer/hosts/hosthwnaturkya.py index 6f5f4e65..81e1ea6b 100644 --- a/IPTVPlayer/hosts/hosthwnaturkya.py +++ b/IPTVPlayer/hosts/hosthwnaturkya.py @@ -297,7 +297,7 @@ def listSearchResult(self, cItem, searchPattern, searchType): self.listItems(params, "explore_item") def CleanTitleName(self, title, sDesc="", showEP=False): - title_display = re.sub(r"\s+", " ", title).strip() + title_display = re.sub(r"\s+|\n|\t", " ", title).strip() desc = sDesc if showEP: ep_match = re.search(r"الحلقة\s*(\d+)", title_display) diff --git a/IPTVPlayer/hosts/hostlodynet.py b/IPTVPlayer/hosts/hostlodynet.py index cf7e904c..2ba7396b 100644 --- a/IPTVPlayer/hosts/hostlodynet.py +++ b/IPTVPlayer/hosts/hostlodynet.py @@ -35,18 +35,28 @@ def __init__(self): } CBaseHostClass.__init__(self, params) self.MAIN_URL = "https://lodynet.watch" - self.DEFAULT_ICON_URL = ( - "https://lodynet.watch/wp-content/themes/Lodynet2020/Img/Logo.webp" - ) + self.DEFAULT_ICON_URL = "https://lodynet.watch/wp-content/themes/Lodynet2020/Img/Logo.webp" self.USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36" # ========================================================================================== def searchItems(self): if self._historyLenTextFunction: return [ - {"category": "search", "title": "بحث", "search_item": True, }, - {"category": "search_history", "title": "سجل البحث", "desc": "تاريخ العبارات التي تم البحث عنها.", }, - {"category": "delete_history", "title": "حذف سجل البحث", "desc": self._historyLenTextFunction, }, + { + "category": "search", + "title": "بحث", + "search_item": True, + }, + { + "category": "search_history", + "title": "سجل البحث", + "desc": "تاريخ العبارات التي تم البحث عنها.", + }, + { + "category": "delete_history", + "title": "حذف سجل البحث", + "desc": self._historyLenTextFunction, + }, ] else: return [] @@ -74,9 +84,7 @@ def getFullUrl(self, url): url = self.MAIN_URL + url elif not url.startswith("http"): url = self.MAIN_URL + "/" + url - if any( - ext in url.lower() for ext in [".jpg", ".jpeg", ".png", ".webp", ".gif"] - ): + if any(ext in url.lower() for ext in [".jpg", ".jpeg", ".png", ".webp", ".gif"]): if not url.startswith("http"): return self.DEFAULT_ICON_URL url = self.encodeUrl(url) @@ -116,10 +124,24 @@ def listMainMenu(self, cItem): MAIN_CAT_TAB = [ {"category": "sub_menu", "title": "مسلسلات", "mode": "10", "sub_mode": 0}, {"category": "sub_menu", "title": "أفلام", "mode": "10", "sub_mode": 1}, - {"category": "list_items", "title": "برامج و حفلات", "url": "/category/البرامج-و-حفلات-tv/", }, + { + "category": "list_items", + "title": "برامج و حفلات", + "url": "/category/البرامج-و-حفلات-tv/", + }, {"category": "sub_menu", "title": "أغاني", "mode": "10", "sub_mode": 2}, - {"category": "list_items", "title": "المضاف حديثاً", "url": "/", "sub_mode": "newly", }, - {"category": "list_actors", "title": "الممثلين", "url": "/all_actors/", "sub_mode": "/all_actors/", }, + { + "category": "list_items", + "title": "المضاف حديثاً", + "url": "/", + "sub_mode": "newly", + }, + { + "category": "list_actors", + "title": "الممثلين", + "url": "/all_actors/", + "sub_mode": "/all_actors/", + }, ] self.listsTab(MAIN_CAT_TAB, cItem) search_items = self.searchItems() @@ -135,62 +157,258 @@ def listSubMenu(self, cItem): SUB_CAT_TAB = [] if gnr == 0: SUB_CAT_TAB = [ - {"category": "list_items", "title": "مسلسلات هندية", "url": "/category/مسلسلات-هندية-مترجمة/", }, - {"category": "list_items", "title": "مسلسلات هندية مدبلجة", "url": "/dubbed-indian-series-p5/", }, - {"category": "list_items", "title": "مسلسلات ويب هندية", "url": "/category/مسلسل-ويب-هندية/", }, - {"category": "list_items", "title": "مسلسلات هندية 2020", "url": "/release-year/مسلسلات-هندية-2020-a/", }, - {"category": "list_items", "title": "مسلسلات هندية 2019", "url": "/release-year/مسلسلات-هندية-2019/", }, - {"category": "list_items", "title": "مسلسلات هندية 2018", "url": "/release-year/مسلسلات-هندية-2018/", }, - {"category": "list_items", "title": "مسلسلات تركية", "url": "/category/مسلسلات-تركي/", }, - {"category": "list_items", "title": "مسلسلات تركية مدبلجة", "url": "/dubbed-turkish-series-g/", }, - {"category": "list_items", "title": "مسلسلات كورية", "url": "/korean-series-b/", }, - {"category": "list_items", "title": "مسلسلات صينية", "url": "/category/مسلسلات-صينية-مترجمة/", }, - {"category": "list_items", "title": "مسلسلات تايلاندية", "url": "/مشاهدة-مسلسلات-تايلندية/", }, - {"category": "list_items", "title": "مسلسلات باكستانية", "url": "/category/المسلسلات-باكستانية/", }, - {"category": "list_items", "title": "مسلسلات آسيوية حديثة", "url": "/tag/new-asia/", }, - {"category": "list_items", "title": "مسلسلات مكسيكية", "url": "/category/مسلسلات-مكسيكية-a/", }, - {"category": "list_items", "title": "مسلسلات أجنبية", "url": "/category/مسلسلات-اجنبية/", }, + { + "category": "list_items", + "title": "مسلسلات هندية", + "url": "/category/مسلسلات-هندية-مترجمة/", + }, + { + "category": "list_items", + "title": "مسلسلات هندية مدبلجة", + "url": "/dubbed-indian-series-p5/", + }, + { + "category": "list_items", + "title": "مسلسلات ويب هندية", + "url": "/category/مسلسل-ويب-هندية/", + }, + { + "category": "list_items", + "title": "مسلسلات هندية 2020", + "url": "/release-year/مسلسلات-هندية-2020-a/", + }, + { + "category": "list_items", + "title": "مسلسلات هندية 2019", + "url": "/release-year/مسلسلات-هندية-2019/", + }, + { + "category": "list_items", + "title": "مسلسلات هندية 2018", + "url": "/release-year/مسلسلات-هندية-2018/", + }, + { + "category": "list_items", + "title": "مسلسلات تركية", + "url": "/category/مسلسلات-تركي/", + }, + { + "category": "list_items", + "title": "مسلسلات تركية مدبلجة", + "url": "/dubbed-turkish-series-g/", + }, + { + "category": "list_items", + "title": "مسلسلات كورية", + "url": "/korean-series-b/", + }, + { + "category": "list_items", + "title": "مسلسلات صينية", + "url": "/category/مسلسلات-صينية-مترجمة/", + }, + { + "category": "list_items", + "title": "مسلسلات تايلاندية", + "url": "/مشاهدة-مسلسلات-تايلندية/", + }, + { + "category": "list_items", + "title": "مسلسلات باكستانية", + "url": "/category/المسلسلات-باكستانية/", + }, + { + "category": "list_items", + "title": "مسلسلات آسيوية حديثة", + "url": "/tag/new-asia/", + }, + { + "category": "list_items", + "title": "مسلسلات مكسيكية", + "url": "/category/مسلسلات-مكسيكية-a/", + }, + { + "category": "list_items", + "title": "مسلسلات أجنبية", + "url": "/category/مسلسلات-اجنبية/", + }, ] elif gnr == 1: SUB_CAT_TAB = [ - {"category": "list_items", "title": "افلام هندية", "url": "/category/افلام-هندية/", }, - {"category": "list_items", "title": "أفلام هندية مدبلجة", "url": "/category/أفلام-هندية-مدبلجة/", }, - {"category": "list_items", "title": "افلام هندية جنوبية", "url": "/tag/الافلام-الهندية-الجنوبية/", }, - {"category": "list_items", "title": "أفلام هندي 2025", "url": "/release-year/أفلام-هندي-2025/", }, - {"category": "list_items", "title": "أفلام هندي 2024", "url": "/release-year/أفلام-هندي-2024/", }, - {"category": "list_items", "title": "أفلام هندي 2023", "url": "/release-year/أفلام-هندية-2023/", }, - {"category": "list_items", "title": "أفلام هندي 2021", "url": "/release-year/movies-hindi-2021/", }, - {"category": "list_items", "title": "أفلام هندي 2020", "url": "/release-year/افلام-هندي-2020-a/", }, - {"category": "list_items", "title": "افلام هندي 2019", "url": "/release-year/افلام-هندي-2019/", }, - {"category": "list_items", "title": "افلام هندي 2018", "url": "/release-year/افلام-هندي-2018/", }, - {"category": "list_items", "title": "افلام هندي 2017", "url": "/release-year/افلام-هندي-2017/", }, - {"category": "list_items", "title": "افلام هندي 2016", "url": "/release-year/2016/", }, - {"category": "list_items", "title": "افلام هندية 4K", "url": "/tag/افلام-هندية-مترجمة-بجودة-4k/", }, - {"category": "list_items", "title": "أميتاب باتشان", "url": "/actor/أميتاب-باتشان/", }, - {"category": "list_items", "title": "اعمال شاروخان", "url": "/actor/شاه-روخ-خان-a/", }, - {"category": "list_items", "title": "أعمال سلمان خان", "url": "/actor/سلمان-خان-a/", }, - {"category": "list_items", "title": "أعمال عامر خان", "url": "/actor/عامر-خان-a/", }, - {"category": "list_items", "title": "أعمال شاهد كابور", "url": "/actor/شاهيد-كابور/", }, - {"category": "list_items", "title": "أعمال رانبير كابور", "url": "/actor/رانبير-كابور/", }, - {"category": "list_items", "title": "أعمال ديبيكا بادكون", "url": "/actor/ديبيكا-بادكون/", }, - {"category": "list_items", "title": "أعمال جينيفر ونجت", "url": "/actor/جينيفر-ونجت/", }, - {"category": "list_items", "title": "أعمال هريتيك روشان", "url": "/actor/هريتيك-روشان/", }, - {"category": "list_items", "title": "أعمال اكشاي كومار", "url": "/actor/اكشاي-كومار/", }, - {"category": "list_items", "title": "أعمال تابسي بانو", "url": "/actor/تابسي-بانو/", }, - {"category": "list_items", "title": "أعمال سانجاي دوت", "url": "/actor/سانجاي-دوت-a/", }, - {"category": "list_items", "title": "ترجمات احمد بشير", "url": "/tag/جميع-الأفلام-في-هذا-القسم-من-ترجمة-أحمد/", }, - {"category": "list_items", "title": "افلام تركية مترجم", "url": "/category/افلام-تركية-مترجم/", }, - {"category": "list_items", "title": "افلام باكستانية", "url": "/tag/افلام-باكستانية-مترجمة/", }, - {"category": "list_items", "title": "افلام اسيوية", "url": "/category/افلام-اسيوية-a/", }, - {"category": "list_items", "title": "افلام اجنبي", "url": "/category/افلام-اجنبية-مترجمة-a/", }, + { + "category": "list_items", + "title": "افلام هندية", + "url": "/category/افلام-هندية/", + }, + { + "category": "list_items", + "title": "أفلام هندية مدبلجة", + "url": "/category/أفلام-هندية-مدبلجة/", + }, + { + "category": "list_items", + "title": "افلام هندية جنوبية", + "url": "/tag/الافلام-الهندية-الجنوبية/", + }, + { + "category": "list_items", + "title": "أفلام هندي 2025", + "url": "/release-year/أفلام-هندي-2025/", + }, + { + "category": "list_items", + "title": "أفلام هندي 2024", + "url": "/release-year/أفلام-هندي-2024/", + }, + { + "category": "list_items", + "title": "أفلام هندي 2023", + "url": "/release-year/أفلام-هندية-2023/", + }, + { + "category": "list_items", + "title": "أفلام هندي 2021", + "url": "/release-year/movies-hindi-2021/", + }, + { + "category": "list_items", + "title": "أفلام هندي 2020", + "url": "/release-year/افلام-هندي-2020-a/", + }, + { + "category": "list_items", + "title": "افلام هندي 2019", + "url": "/release-year/افلام-هندي-2019/", + }, + { + "category": "list_items", + "title": "افلام هندي 2018", + "url": "/release-year/افلام-هندي-2018/", + }, + { + "category": "list_items", + "title": "افلام هندي 2017", + "url": "/release-year/افلام-هندي-2017/", + }, + { + "category": "list_items", + "title": "افلام هندي 2016", + "url": "/release-year/2016/", + }, + { + "category": "list_items", + "title": "افلام هندية 4K", + "url": "/tag/افلام-هندية-مترجمة-بجودة-4k/", + }, + { + "category": "list_items", + "title": "أميتاب باتشان", + "url": "/actor/أميتاب-باتشان/", + }, + { + "category": "list_items", + "title": "اعمال شاروخان", + "url": "/actor/شاه-روخ-خان-a/", + }, + { + "category": "list_items", + "title": "أعمال سلمان خان", + "url": "/actor/سلمان-خان-a/", + }, + { + "category": "list_items", + "title": "أعمال عامر خان", + "url": "/actor/عامر-خان-a/", + }, + { + "category": "list_items", + "title": "أعمال شاهد كابور", + "url": "/actor/شاهيد-كابور/", + }, + { + "category": "list_items", + "title": "أعمال رانبير كابور", + "url": "/actor/رانبير-كابور/", + }, + { + "category": "list_items", + "title": "أعمال ديبيكا بادكون", + "url": "/actor/ديبيكا-بادكون/", + }, + { + "category": "list_items", + "title": "أعمال جينيفر ونجت", + "url": "/actor/جينيفر-ونجت/", + }, + { + "category": "list_items", + "title": "أعمال هريتيك روشان", + "url": "/actor/هريتيك-روشان/", + }, + { + "category": "list_items", + "title": "أعمال اكشاي كومار", + "url": "/actor/اكشاي-كومار/", + }, + { + "category": "list_items", + "title": "أعمال تابسي بانو", + "url": "/actor/تابسي-بانو/", + }, + { + "category": "list_items", + "title": "أعمال سانجاي دوت", + "url": "/actor/سانجاي-دوت-a/", + }, + { + "category": "list_items", + "title": "ترجمات احمد بشير", + "url": "/tag/جميع-الأفلام-في-هذا-القسم-من-ترجمة-أحمد/", + }, + { + "category": "list_items", + "title": "افلام تركية مترجم", + "url": "/category/افلام-تركية-مترجم/", + }, + { + "category": "list_items", + "title": "افلام باكستانية", + "url": "/tag/افلام-باكستانية-مترجمة/", + }, + { + "category": "list_items", + "title": "افلام اسيوية", + "url": "/category/افلام-اسيوية-a/", + }, + { + "category": "list_items", + "title": "افلام اجنبي", + "url": "/category/افلام-اجنبية-مترجمة-a/", + }, {"category": "list_items", "title": "انيمي", "url": "/category/انيمي/"}, ] elif gnr == 2: SUB_CAT_TAB = [ - {"category": "list_items", "title": "أغاني المسلسلات", "url": "/category/اغاني/اغاني-المسلسلات-الهندية/", }, - {"category": "list_items", "title": "تصاميم مسلسلات هندية", "url": "/category/تصاميم-مسلسلات-هندية/", }, - {"category": "list_items", "title": "أغاني الأفلام", "url": "/category/اغاني-الافلام/", }, - {"category": "list_items", "title": "اغاني هندية mp3", "url": "/category/اغاني/اغاني-هندية-mp3/", }, + { + "category": "list_items", + "title": "أغاني المسلسلات", + "url": "/category/اغاني/اغاني-المسلسلات-الهندية/", + }, + { + "category": "list_items", + "title": "تصاميم مسلسلات هندية", + "url": "/category/تصاميم-مسلسلات-هندية/", + }, + { + "category": "list_items", + "title": "أغاني الأفلام", + "url": "/category/اغاني-الافلام/", + }, + { + "category": "list_items", + "title": "اغاني هندية mp3", + "url": "/category/اغاني/اغاني-هندية-mp3/", + }, ] self.listsTab(SUB_CAT_TAB, cItem) @@ -209,9 +427,7 @@ def listItems(self, cItem): sts, data = self.getPage(url) if not sts: return - items = re.findall( - r'
    (.*?)
    \s*\s*
    ', data, re.S - ) + items = re.findall(r'
    (.*?)
    \s*\s*
    ', data, re.S) for item in items: title = re.search(r'title="([^"]+)"', item) link = re.search(r'href="([^"]+)"', item) @@ -220,11 +436,7 @@ def listItems(self, cItem): continue title = title.group(1).strip() item_url = self.getFullUrl(link.group(1)) - icon = ( - self.getFullUrl(img.group(1)) - if img and img.group(1) - else self.DEFAULT_ICON_URL - ) + icon = self.getFullUrl(img.group(1)) if img and img.group(1) else self.DEFAULT_ICON_URL desc = self.extractDescFromNewly(item) if self.determineContentType(title, item_url) == "series": self.addDir( @@ -264,9 +476,7 @@ def listItems(self, cItem): ) return # === دعم زر عرض المزيد (GetExpansion) === - more = re.search( - r"GetExpansion\(\s*(\d+)\s*,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)", data - ) + more = re.search(r"GetExpansion\(\s*(\d+)\s*,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)", data) if more: indicator = more.group(1) exp_type = more.group(2) @@ -282,10 +492,7 @@ def listItems(self, cItem): "url": url, } ) - printDBG( - "Expansion button added: indicator=%s type=%s id=%s" - % (indicator, exp_type, exp_id) - ) + printDBG("Expansion button added: indicator=%s type=%s id=%s" % (indicator, exp_type, exp_id)) # ========================================================================================== def listEpisodes(self, cItem): @@ -295,20 +502,14 @@ def listEpisodes(self, cItem): if not sts: return items_added = 0 - blocks = re.findall( - r'(
    .*?
    \s*\s*
    )', data, re.S - ) + blocks = re.findall(r'(
    .*?
    \s*\s*)', data, re.S) for block in blocks: title = re.search(r'title="([^"]+)"', block) link = re.search(r'href="([^"]+)"', block) img = re.search(r'data-src="([^"]*)"', block) if not title or not link: continue - icon = ( - self.getFullUrl(img.group(1)) - if img and img.group(1) - else self.DEFAULT_ICON_URL - ) + icon = self.getFullUrl(img.group(1)) if img and img.group(1) else self.DEFAULT_ICON_URL self.addVideo( { "title": title.group(1).strip(), @@ -320,9 +521,7 @@ def listEpisodes(self, cItem): ) items_added += 1 # === دعم GetExpansion (عرض المزيد) - للأقسام التي تستخدمه === - more = re.search( - r"GetExpansion\(\s*(\d+)\s*,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)", data - ) + more = re.search(r"GetExpansion\(\s*(\d+)\s*,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)", data) if more and items_added >= 10: indicator = more.group(1) exp_type = more.group(2) @@ -340,10 +539,7 @@ def listEpisodes(self, cItem): "icon": self.DEFAULT_ICON_URL, } ) - printDBG( - "Expansion button added for episodes: indicator=%s type=%s id=%s" - % (indicator, exp_type, exp_id) - ) + printDBG("Expansion button added for episodes: indicator=%s type=%s id=%s" % (indicator, exp_type, exp_id)) return # ========================================================================================== @@ -351,10 +547,7 @@ def loadMore(self, cItem): printDBG("Lodynet.loadMore [%s]" % cItem) # === معالجة GetExpansion (عرض المزيد) === if cItem.get("is_expansion"): - API_URL = ( - self.MAIN_URL - + "/wp-content/themes/Lodynet2020/Api/RequestExpansion.php" - ) + API_URL = self.MAIN_URL + "/wp-content/themes/Lodynet2020/Api/RequestExpansion.php" post_data = { "indicator": cItem.get("indicator", ""), "type": cItem.get("exp_type", ""), @@ -382,31 +575,16 @@ def loadMore(self, cItem): if url_path.startswith("http"): full_url = url_path else: - full_url = self.MAIN_URL + ( - url_path - if url_path.startswith("/") - else "/" + url_path - ) + full_url = self.MAIN_URL + (url_path if url_path.startswith("/") else "/" + url_path) icon = item.get("cover", "") - icon = ( - self.getFullUrl(icon) if icon else self.DEFAULT_ICON_URL - ) + icon = self.getFullUrl(icon) if icon else self.DEFAULT_ICON_URL desc_parts = [] if item.get("ribbon"): - desc_parts.append( - "\\c00????00 النوع: \\c00????FF" - + item.get("ribbon") - ) + desc_parts.append("\\c00????00 النوع: \\c00????FF" + item.get("ribbon")) if item.get("Date"): - desc_parts.append( - "\\c00????00 وقت النشر: \\c00????FF" - + self.getTimeAgo(item.get("Date")) - ) + desc_parts.append("\\c00????00 وقت النشر: \\c00????FF" + self.getTimeAgo(item.get("Date"))) if item.get("episode"): - desc_parts.append( - "\\c00????00 الحلقة: \\c00????FF" - + str(item.get("episode")) - ) + desc_parts.append("\\c00????00 الحلقة: \\c00????FF" + str(item.get("episode"))) full_desc = "\\n".join(desc_parts) if desc_parts else "" self.addDir( { @@ -427,31 +605,16 @@ def loadMore(self, cItem): if url_path.startswith("http"): full_url = url_path else: - full_url = self.MAIN_URL + ( - url_path - if url_path.startswith("/") - else "/" + url_path - ) + full_url = self.MAIN_URL + (url_path if url_path.startswith("/") else "/" + url_path) icon = item.get("cover", "") - icon = ( - self.getFullUrl(icon) if icon else self.DEFAULT_ICON_URL - ) + icon = self.getFullUrl(icon) if icon else self.DEFAULT_ICON_URL desc_parts = [] if item.get("ribbon"): - desc_parts.append( - "\\c00????00 النوع: \\c00????FF" - + item.get("ribbon") - ) + desc_parts.append("\\c00????00 النوع: \\c00????FF" + item.get("ribbon")) if item.get("Date"): - desc_parts.append( - "\\c00????00 وقت النشر: \\c00????FF" - + self.getTimeAgo(item.get("Date")) - ) + desc_parts.append("\\c00????00 وقت النشر: \\c00????FF" + self.getTimeAgo(item.get("Date"))) if item.get("episode"): - desc_parts.append( - "\\c00????00 الحلقة: \\c00????FF" - + str(item.get("episode")) - ) + desc_parts.append("\\c00????00 الحلقة: \\c00????FF" + str(item.get("episode"))) full_desc = "\\n".join(desc_parts) if desc_parts else "" params = { "title": title, @@ -463,10 +626,7 @@ def loadMore(self, cItem): if cItem.get("is_episodes"): self.addVideo(params) else: - if ( - self.determineContentType(title, full_url) - == "series" - ): + if self.determineContentType(title, full_url) == "series": params["category"] = "list_episodes" self.addDir(params) else: @@ -507,9 +667,7 @@ def determineContentType(self, title, url=""): printDBG(f"Actor URL pattern detected: {result}") return result if any(x in title_lower for x in ["ممثل", "نجم", "ممثلة", "فنان", "فنانة"]): - if "/actor/" in url_lower or any( - x in url_lower for x in ["/actors/", "/celebrity/"] - ): + if "/actor/" in url_lower or any(x in url_lower for x in ["/actors/", "/celebrity/"]): result = "actor" printDBG(f"Actor title + URL detected: {result}") return result @@ -601,17 +759,11 @@ def determineContentType(self, title, url=""): result = "series" printDBG(f"Season pattern detected: {result}") return result - if any( - keyword in url_lower - for keyword in ["/series/", "/مسلسلات/", "/مسلسل/", "/seasons/"] - ): + if any(keyword in url_lower for keyword in ["/series/", "/مسلسلات/", "/مسلسل/", "/seasons/"]): result = "series" printDBG(f"Series URL pattern detected: {result}") return result - if any( - keyword in url_lower - for keyword in ["/movies/", "/أفلام/", "/فيلم/", "/film/"] - ): + if any(keyword in url_lower for keyword in ["/movies/", "/أفلام/", "/فيلم/", "/film/"]): result = "movie" printDBG(f"Movie URL pattern detected: {result}") return result @@ -623,10 +775,7 @@ def determineContentType(self, title, url=""): "/series-", "/مسلسل-", ] - ) and not any( - keyword in url_lower - for keyword in ["/افلام/", "/movies/", "/أغاني/", "/music/"] - ): + ) and not any(keyword in url_lower for keyword in ["/افلام/", "/movies/", "/أغاني/", "/music/"]): result = "series" printDBG(f"Foreign series section detected: {result}") return result @@ -673,25 +822,17 @@ def extractDescFromNewly(self, html_block): desc_parts = [] type_match = re.search(r'NewlyRibbon">([^<]+)', html_block) if type_match: - desc_parts.append( - "\\c00????00 النوع: \\c00????FF" + type_match.group(1).strip() - ) + desc_parts.append("\\c00????00 النوع: \\c00????FF" + type_match.group(1).strip()) time_match = re.search(r'NewlyTimeAgo[^>]*data-date="([^"]+)"', html_block) if time_match: ago = self.getTimeAgo(time_match.group(1).strip()) desc_parts.append("\\c00????00 وقت النشر: \\c00????FF" + ago) episode_match = re.search(r"NewlyEpNumber[^>]*>.*?(\d+)", html_block) if episode_match: - desc_parts.append( - "\\c00????00 الحلقة: \\c00????FF" + episode_match.group(1).strip() - ) - summary_match = re.search( - r'class="NewlySummary"[^>]*>([^<]+)', html_block - ) + desc_parts.append("\\c00????00 الحلقة: \\c00????FF" + episode_match.group(1).strip()) + summary_match = re.search(r'class="NewlySummary"[^>]*>([^<]+)', html_block) if summary_match: - desc_parts.append( - "\\c00????00 الملخص: \\c00FFFFFF" + summary_match.group(1).strip() - ) + desc_parts.append("\\c00????00 الملخص: \\c00FFFFFF" + summary_match.group(1).strip()) return "\n".join(desc_parts) if desc_parts else "\\c00????00 محتوى مضاف حديثاً" # ========================================================================================== @@ -716,16 +857,12 @@ def getLinksForVideo(self, cItem): break printDBG("PostID = %s" % post_id) # ===== Servers ===== - servers = re.findall( - r']+id="ServerWatch(\d+)"[^>]*>([^<]+)', data - ) + servers = re.findall(r']+id="ServerWatch(\d+)"[^>]*>([^<]+)', data) printDBG("Found %d server buttons" % len(servers)) if not post_id or not servers: printDBG("No servers or no post_id") return [] - api_url = ( - self.MAIN_URL + "/wp-content/themes/Lodynet2020/Api/RequestServerEmbed.php" - ) + api_url = self.MAIN_URL + "/wp-content/themes/Lodynet2020/Api/RequestServerEmbed.php" green_servers = [ "ViD LO", "Vinovo", @@ -765,9 +902,7 @@ def getVideoLinks(self, videoUrl): printDBG("Lodynet.getVideoLinks [%s]" % videoUrl) videoUrlStr = str(videoUrl) if "ok.ru" in videoUrlStr: - printDBG( - "Direct OK.ru URL detected in getVideoLinks, using custom resolver" - ) + printDBG("Direct OK.ru URL detected in getVideoLinks, using custom resolver") return self.getOkRuLinks(videoUrlStr) if hasattr(videoUrl, "meta"): post_data = videoUrl.meta.get("post_data") @@ -778,10 +913,7 @@ def getVideoLinks(self, videoUrl): post_data, videoUrl.meta.get("Referer", self.MAIN_URL), ) - if any( - x in videoUrlStr - for x in ["vidlo.us", "viidshar.com", "govad.xyz", "vadbam.net"] - ): + if any(x in videoUrlStr for x in ["vidlo.us", "viidshar.com", "govad.xyz", "vadbam.net"]): return self.getVidloDirectLinks(videoUrlStr) if videoUrlStr.endswith(".mp4"): return [{"name": "Direct MP4", "url": videoUrlStr}] @@ -823,11 +955,7 @@ def getOkRuLinks(self, baseUrl): data_options = json.loads(data_options_str) flashvars = data_options.get("flashvars", {}) metadata_str = flashvars.get("metadata", "") - metadata_str = ( - metadata_str.replace('\\"', '"') - .replace("\\\\", "\\") - .replace("\\u0026", "&") - ) + metadata_str = metadata_str.replace('\\"', '"').replace("\\\\", "\\").replace("\\u0026", "&") metadata = json.loads(metadata_str) except Exception as e: printDBG("========= Failed to parse metadata JSON: %s" % e) @@ -864,9 +992,7 @@ def getOkRuLinks(self, baseUrl): bitrate = video.get("bitrate") or "unknown" res = video.get("res") or video.get("resolution") or "unknown" codecs = video.get("codecs") or "avc1,mp4a" - display = ( - f"{display_quality} - MP4 - bitrate: {bitrate} res: {res} {codecs}" - ) + display = f"{display_quality} - MP4 - bitrate: {bitrate} res: {res} {codecs}" all_links.append( { "name": display, @@ -951,11 +1077,7 @@ def getOkRuLinks(self, baseUrl): # ===================== Standardize MP4 name and infer resolution/bitrate ===================== for item in all_links: if not item.get("is_hls", False) and not item.get("is_dash", False): - qv = ( - int(item["quality_val"]) - if str(item["quality_val"]).isdigit() - else 0 - ) + qv = int(item["quality_val"]) if str(item["quality_val"]).isdigit() else 0 # Infer resolution from quality_val if "res" not in item["name"] or "unknown" in item["name"]: if qv == 2160: @@ -997,9 +1119,7 @@ def getOkRuLinks(self, baseUrl): else: bitrate = "unknown" # Rebuild display name - item["name"] = ( - f"{item['name'].split(' - ')[0]} - MP4 - bitrate: {bitrate} res: {res} avc1,mp4a" - ) + item["name"] = f"{item['name'].split(' - ')[0]} - MP4 - bitrate: {bitrate} res: {res} avc1,mp4a" # ===================== Sort links by quality descending ===================== def sort_key(item): @@ -1029,9 +1149,7 @@ def processAPIRequest(self, api_url, post_data, referer): } try: printDBG("Sending POST request to API...") - response = requests.post( - api_url, data=post_data, headers=headers, timeout=30 - ) + response = requests.post(api_url, data=post_data, headers=headers, timeout=30) response_text = response.text.strip() printDBG("API Response: %s" % response_text) printDBG("Response length: %d chars" % len(response_text)) @@ -1106,11 +1224,7 @@ def listActors(self, cItem): re.S, ) for title, item_url, img in items: - if any( - x in str(value) - for value in [title, item_url, img] - for x in ["+ CategoryItem.", "CategoryItem."] - ): + if any(x in str(value) for value in [title, item_url, img] for x in ["+ CategoryItem.", "CategoryItem."]): continue full_url = self.getFullUrl(item_url) icon = self.getFullUrl(img) if img else self.DEFAULT_ICON_URL @@ -1123,9 +1237,7 @@ def listActors(self, cItem): "good_for_fav": True, } ) - more = re.search( - r"GetExpansion\(\s*(\d+)\s*,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)", data - ) + more = re.search(r"GetExpansion\(\s*(\d+)\s*,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)", data) if more: indicator = more.group(1) exp_type = more.group(2) @@ -1153,20 +1265,14 @@ def listActorMovies(self, cItem): if not sts: return items_added = 0 - blocks = re.findall( - r'(
    .*?
    \s*\s*)', data, re.S - ) + blocks = re.findall(r'(
    .*?
    \s*\s*)', data, re.S) for block in blocks: title = re.search(r'title="([^"]+)"', block) link = re.search(r'href="([^"]+)"', block) img = re.search(r'data-src="([^"]*)"', block) if not title or not link: continue - icon = ( - self.getFullUrl(img.group(1)) - if img and img.group(1) - else self.DEFAULT_ICON_URL - ) + icon = self.getFullUrl(img.group(1)) if img and img.group(1) else self.DEFAULT_ICON_URL full_url = self.getFullUrl(link.group(1)) self.addVideo( { @@ -1178,9 +1284,7 @@ def listActorMovies(self, cItem): } ) items_added += 1 - more = re.search( - r"GetExpansion\(\s*(\d+)\s*,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)", data - ) + more = re.search(r"GetExpansion\(\s*(\d+)\s*,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)", data) if more and items_added >= 10: indicator = more.group(1) exp_type = more.group(2) @@ -1198,20 +1302,14 @@ def listActorMovies(self, cItem): "icon": self.DEFAULT_ICON_URL, } ) - printDBG( - "Expansion button added for actor movies: indicator=%s" % indicator - ) + printDBG("Expansion button added for actor movies: indicator=%s" % indicator) # ========================================================================================== def listSearchResult(self, cItem, searchPattern, searchType): printDBG("Lodynet.listSearchResult [%s]" % searchPattern) if not searchPattern: return - search_url = ( - self.MAIN_URL - + "/wp-content/themes/Lodynet2020/Api/RequestSearch.php?value=" - + quote_plus(searchPattern) - ) + search_url = self.MAIN_URL + "/wp-content/themes/Lodynet2020/Api/RequestSearch.php?value=" + quote_plus(searchPattern) printDBG("Search URL: %s" % search_url) headers = { "User-Agent": self.USER_AGENT, @@ -1245,9 +1343,7 @@ def listSearchResult(self, cItem, searchPattern, searchType): continue try: if "\\u" in title: - title = title.encode("utf-8").decode( - "unicode_escape" - ) + title = title.encode("utf-8").decode("unicode_escape") elif "&#x" in title: title = html.unescape(title) except Exception: @@ -1277,12 +1373,8 @@ def listSearchResult(self, cItem, searchPattern, searchType): # الوصف desc_parts = [] if category: - desc_parts.append( - "\\c00????00القسم: \\c00????FF" + category - ) - desc = ( - "\n".join(desc_parts) if desc_parts else "نتيجة بحث" - ) + desc_parts.append("\\c00????00القسم: \\c00????FF" + category) + desc = "\n".join(desc_parts) if desc_parts else "نتيجة بحث" is_actor = False if "/actor/" in full_url.lower(): is_actor = True @@ -1298,10 +1390,7 @@ def listSearchResult(self, cItem, searchPattern, searchType): ] ): is_actor = True - if any( - x in title.lower() - for x in ["خان", "كابور", "باتشان", "شاه", "راي"] - ): + if any(x in title.lower() for x in ["خان", "كابور", "باتشان", "شاه", "راي"]): if "/actor/" in full_url.lower(): is_actor = True if is_actor: @@ -1317,9 +1406,7 @@ def listSearchResult(self, cItem, searchPattern, searchType): ) printDBG("Added actor as folder: %s" % title) else: - content_type = self.determineContentType( - title, full_url - ) + content_type = self.determineContentType(title, full_url) if content_type == "series": self.addDir( { @@ -1347,8 +1434,7 @@ def listSearchResult(self, cItem, searchPattern, searchType): "category": "search", "title": "\\c00FF0000لم يتم العثور على نتائج", "url": "", - "desc": "لم يتم العثور على أي نتائج للبحث: " - + searchPattern, + "desc": "لم يتم العثور على أي نتائج للبحث: " + searchPattern, } ) else: @@ -1403,27 +1489,18 @@ def getArticleContent(self, cItem): title = cItem.get("title", "") icon = cItem.get("icon", self.DEFAULT_ICON_URL) summary = "" - content_block = self.cm.ph.getDataBeetwenMarkers( - data, '
    ", False - )[1] + content_block = self.cm.ph.getDataBeetwenMarkers(data, '
    ", False)[1] if content_block: if "ملخص أحداث الحلقة" in content_block: summary = content_block.split("ملخص أحداث الحلقة")[-1] elif "تبدأ الحلقة" in content_block: summary = "تبدأ الحلقة" + content_block.split("تبدأ الحلقة")[-1] else: - summary = self.cm.ph.getDataBeetwenMarkers( - content_block, "

    ", "

    ", False - )[1] + summary = self.cm.ph.getDataBeetwenMarkers(content_block, "

    ", "

    ", False)[1] if summary: summary = summary.split("قراءة المزيد")[0] summary = re.sub(r"<[^>]+>", "", summary) - summary = ( - summary.replace("–", "-") - .replace("“", '"') - .replace("”", '"') - .replace(" ", " ") - ) + summary = summary.replace("–", "-").replace("“", '"').replace("”", '"').replace(" ", " ") summary = summary.strip() old_desc = cItem.get("desc", "") final_text = "" @@ -1486,9 +1563,7 @@ def getVidloDirectLinks(self, baseUrl): printDBG("تم العثور على مصادر الفيديو") video_objects = re.findall(r"\{[^{}]*\}", sources_content) for obj in video_objects: - url_match = re.search( - r'file["\']?\s*:\s*["\'](https?://[^"\']+)["\']', obj - ) + url_match = re.search(r'file["\']?\s*:\s*["\'](https?://[^"\']+)["\']', obj) label_match = re.search(r'label["\']?\s*:\s*["\']([^"\']+)["\']', obj) if url_match: video_url = url_match.group(1) @@ -1511,16 +1586,11 @@ def getVidloDirectLinks(self, baseUrl): label = "HLS Stream" elif "720p" in video_url.lower() or "/hd/" in video_url.lower(): label = "HD [720p]" - elif ( - "1080p" in video_url.lower() - or "/fullhd/" in video_url.lower() - ): + elif "1080p" in video_url.lower() or "/fullhd/" in video_url.lower(): label = "FULL HD [1080p]" elif "480p" in video_url.lower() or "/sd/" in video_url.lower(): label = "SD [480p]" - elif ( - "360p" in video_url.lower() or "/low/" in video_url.lower() - ): + elif "360p" in video_url.lower() or "/low/" in video_url.lower(): label = "LOW [360p]" elif "576p" in video_url.lower(): label = "576p" @@ -1654,9 +1724,7 @@ def listsHistory( pattern = histItem search_type = None params = dict(baseItem) - params.update( - {"title": pattern, "search_type": search_type, desc_key: plot} - ) + params.update({"title": pattern, "search_type": search_type, desc_key: plot}) self.addDir(params) except Exception: printExc() @@ -1671,8 +1739,6 @@ def __init__(self): CHostBase.__init__(self, Lodynet(), True, []) def withArticleContent(self, cItem): - if "video" == cItem.get("type", "") or "list_episodes" == cItem.get( - "category", "" - ): + if "video" == cItem.get("type", "") or "list_episodes" == cItem.get("category", ""): return True return False diff --git a/IPTVPlayer/hosts/hostmovizhome.py b/IPTVPlayer/hosts/hostmovizhome.py index 4872a004..bcf5bf68 100644 --- a/IPTVPlayer/hosts/hostmovizhome.py +++ b/IPTVPlayer/hosts/hostmovizhome.py @@ -1,7 +1,7 @@ # # -*- coding: utf-8 -*- # # MovizHome plugin for oe-mirrors IPTVPlayer # # Author : MohamedOS (Modified By Mohamed Elsafty) -# # Last modified: 22/12/2025 +# # Last modified: 17/05/2026 # # Description: A plugin to access MovizHome website content # ############################################################### # # LOCAL import @@ -10,11 +10,13 @@ from Plugins.Extensions.IPTVPlayer.components.ihost import CHostBase, CBaseHostClass from Plugins.Extensions.IPTVPlayer.tools.iptvtools import printDBG, printExc, E2ColoR from Plugins.Extensions.IPTVPlayer.tools.iptvtypes import strwithmeta + # ############################################################### # # FOREIGN import # ############################################################### import re import html + try: from html import unescape except ImportError: @@ -26,14 +28,14 @@ except ImportError: from urllib import quote ############################################################### -Y = E2ColoR('yellow') -W = E2ColoR('white') -LB = E2ColoR('lightblue') -G = E2ColoR('green') -R = E2ColoR('red') -############################################################### +Y = E2ColoR("yellow") +W = E2ColoR("white") +LB = E2ColoR("lightblue") +G = E2ColoR("green") +R = E2ColoR("red") +############################################################### def GetConfigList(): return [] @@ -54,7 +56,7 @@ def __init__(self): def getPage(self, baseUrl, addParams=None, post_data=None): if isinstance(baseUrl, str): try: - baseUrl = quote(baseUrl, safe=':/?&=%') + baseUrl = quote(baseUrl, safe=":/?&=%") except Exception: pass if addParams is None: @@ -64,16 +66,8 @@ def getPage(self, baseUrl, addParams=None, post_data=None): def listMainMenu(self): base = "https://movizhome.click" - self.addDir({"name": "category", "category": "list_items", "title": "مضاف حديثا", "url": self.getFullUrl("/recent/")}) - menu_data = { - "افلام اجنبيه": ["category/افلام-movis/افلام-اجنبية", [("الأكشن", "genre/اكشن"), ("الرعب", "genre/رعب"), ("الخيال العلمي", "genre/خيال-علمي"), ("الفانتازيا", "genre/fantasy-movies"), ("رومانسية", "genre/رومانسية"), ("الكوميديا", "genre/كوميديا"), ("الإثارة والتشويق", "genre/إثارة"), ("الجريمة", "genre/جريمة"), ("المغامرات", "genre/مغامرات"), ("الدراما", "genre/دراما"), ("كلاسيكية", "tag/افلام-كلاسيكية"), ("الغموض", "genre/غموض"), ("عائلية", "genre/عائلي"), ("تاريخية", "genre/تاريخي"), ("الويسترن", "genre/ويسترن")]], - "افلام هندية": ["category/افلام-movis/افلام-هنديه", [("التيلجو", "tag/telugu"), ("تاميلية", "tag/افلام-تاميلية"), ("ماليالامية", "tag/افلام-ماليالامية"), ("كنادية", "tag/افلام-كنادية"), ("بنجابية", "tag/punjabi"), ("ماراثية", "tag/افلام-ماراثية"), ("بنغالية", "tag/افلام-بنغالية"), ("كجراتية", "tag/افلام-كجراتية")]], - "افلام آسيوية": ["category/افلام-movis/افلام-اسيويه", [("كورية", "language/الكورية"), ("يابانية", "language/اليابانية"), ("صينية", "language/الصينية"), ("تايلاندية", "language/التايلاندية"), ("إندونيسية", "language/الاندونيسية"), ("فلبينية", "language/الفلبينية"), ("منغولية", "language/المغولية")]], - "افلام فرنسية": ["category/افلام-movis/افلام-فرنسية/", []], - "افلام تركية": ["category/افلام-movis/افلام-تركية", []], - "افلام مدبلجة": ["category/افلام-movis/افلام-مدبلجة", []], - "انيميشن": ["category/افلام-كارتون/", []] - } + self.addDir({"name": "category", "category": "list_items", "title": _("مضاف حديثا"), "url": self.getFullUrl("/recent/")}) + menu_data = {"افلام اجنبيه": ["category/افلام-movis/افلام-اجنبية", [("الأكشن", "genre/اكشن"), ("الرعب", "genre/رعب"), ("الخيال العلمي", "genre/خيال-علمي"), ("الفانتازيا", "genre/fantasy-movies"), ("رومانسية", "genre/رومانسية"), ("الكوميديا", "genre/كوميديا"), ("الإثارة والتشويق", "genre/إثارة"), ("الجريمة", "genre/جريمة"), ("المغامرات", "genre/مغامرات"), ("الدراما", "genre/دراما"), ("كلاسيكية", "tag/افلام-كلاسيكية"), ("الغموض", "genre/غموض"), ("عائلية", "genre/عائلي"), ("تاريخية", "genre/تاريخي"), ("الويسترن", "genre/ويسترن")]], "افلام هندية": ["category/افلام-movis/افلام-هنديه", [("التيلجو", "tag/telugu"), ("تاميلية", "tag/افلام-تاميلية"), ("ماليالامية", "tag/افلام-ماليالامية"), ("كنادية", "tag/افلام-كنادية"), ("بنجابية", "tag/punjabi"), ("ماراثية", "tag/افلام-ماراثية"), ("بنغالية", "tag/افلام-بنغالية"), ("كجراتية", "tag/افلام-كجراتية")]], "افلام آسيوية": ["category/افلام-movis/افلام-اسيويه", [("كورية", "language/الكورية"), ("يابانية", "language/اليابانية"), ("صينية", "language/الصينية"), ("تايلاندية", "language/التايلاندية"), ("إندونيسية", "language/الاندونيسية"), ("فلبينية", "language/الفلبينية"), ("منغولية", "language/المغولية")]], "افلام فرنسية": ["category/افلام-movis/افلام-فرنسية/", []], "افلام تركية": ["category/افلام-movis/افلام-تركية", []], "افلام مدبلجة": ["category/افلام-movis/افلام-مدبلجة", []], "انيميشن": ["category/افلام-كارتون/", []]} for title, (main_path, subs) in menu_data.items(): sub_items = [{"title": "افلام %s" % s[0], "url": "%s/%s" % (base, s[1])} for s in subs] self.addDir({"name": "category", "category": "list_sub_menu", "title": title, "url": "%s/%s" % (base, main_path), "subs": sub_items, "good_for_fav": True}) @@ -103,7 +97,7 @@ def listItems(self, cItem, nextCategory=None): sts, htm = self.getPage(cItem["url"]) if not sts: return - data = self.cm.ph.getAllItemsBeetwenMarkers(htm, '
    ', '', withMarkers=True) + data = self.cm.ph.getAllItemsBeetwenMarkers(htm, '
    ', "", withMarkers=True) if not data: return for item in data: @@ -113,6 +107,7 @@ def listItems(self, cItem, nextCategory=None): continue try: title = unescape(self.cleanHtmlStr(self.cm.ph.getSearchGroups(item, 'title="([^"]+)"')[0])) + title = re.sub(r"^(فيلم|مسلسل|انمي|برنامج)\s+", "", title).strip() except: try: title = unescape(self.cleanHtmlStr(self.cm.ph.getSearchGroups(item, '

    ([^<]+)

    ')[0])) @@ -124,7 +119,7 @@ def listItems(self, cItem, nextCategory=None): icon = "" genre = self.cm.ph.getSearchGroups(item, r']*class="genre"[^>]*>\s*([^<]+)') quality = self.cm.ph.getSearchGroups(item, r']*class="quality"[^>]*>\s*([^<]+)') - imdb = self.cm.ph.getSearchGroups(item, r'imdbRating[^>]*>.*?([\d\.]+)') + imdb = self.cm.ph.getSearchGroups(item, r"imdbRating[^>]*>.*?([\d\.]+)") genre = genre[0].strip() if genre else "" quality = quality[0].strip() if quality else "" imdb = imdb[0].strip() if imdb else "" @@ -147,34 +142,34 @@ def listItems(self, cItem, nextCategory=None): nextPage = self.cm.ph.getSearchGroups(htm, ']+class="next page-numbers"[^>]+href="([^"]+)"') if nextPage: params = dict(cItem) - next_title = Y + _("Next Page") + " ▶▶▶" + W - params.update({"title": next_title, "url": self.getFullUrl(nextPage[0]), "good_for_fav": False, "category": "list_items"}) + params.update({"title": "%s%s%s" % (Y, ("Next Page") + " ▶▶▶", W), "url": self.getFullUrl(nextPage[0]), "good_for_fav": False, "category": "list_items"}) self.addDir(params) def listSearchResult(self, cItem, searchPattern, searchType): - addParams = {'header': {'User-Agent': self.cm.getDefaultHeader()['User-Agent']}, 'use_cookie': True, 'load_cookie': True, 'save_cookie': True, 'cookiefile': self.COOKIE_FILE, 'CFProtection': True} - sts, data = self.getPage('https://movizhome.click/?s=%s' % searchPattern, addParams) + addParams = {"header": {"User-Agent": self.cm.getDefaultHeader()["User-Agent"]}, "use_cookie": True, "load_cookie": True, "save_cookie": True, "cookiefile": self.COOKIE_FILE, "CFProtection": True} + sts, data = self.getPage("https://movizhome.click/?s=%s" % searchPattern, addParams) if not sts: return items = re.findall(r'
    (.*?)\s*
    ', data, re.S) - printDBG('SEARCH ITEMS COUNT: %d' % len(items)) + printDBG("SEARCH ITEMS COUNT: %d" % len(items)) for item in items: try: url = self.cm.ph.getSearchGroups(item, r']+href="([^"]+)"')[0] except: - url = '' + url = "" try: title = self.cm.ph.getSearchGroups(item, r'

    (.*?)

    ')[0] title = self.cleanHtmlStr(title) + title = re.sub(r"^(فيلم|مسلسل|انمي|برنامج)\s+", "", title).strip() except: - title = '' + title = "" try: icon = self.cm.ph.getSearchGroups(item, r'data-src="([^"]+)"')[0] except: - icon = '' + icon = "" genre = self.cm.ph.getSearchGroups(item, r']*class="genre"[^>]*>\s*([^<]+)') quality = self.cm.ph.getSearchGroups(item, r']*class="quality"[^>]*>\s*([^<]+)') - imdb = self.cm.ph.getSearchGroups(item, r'imdbRating[^>]*>.*?([\d\.]+)') + imdb = self.cm.ph.getSearchGroups(item, r"imdbRating[^>]*>.*?([\d\.]+)") genre = genre[0].strip() if genre else "" quality = quality[0].strip() if quality else "" imdb = imdb[0].strip() if imdb else "" @@ -192,21 +187,21 @@ def listSearchResult(self, cItem, searchPattern, searchType): desc_lines.append("%sIMDB Rating:%s %s%s%s" % (Y, W, imdb_color, imdb, W)) desc = "\n".join(desc_lines) params = dict(cItem) - params.update({'title': title, 'url': self.cm.getFullUrl(url), 'icon': self.cm.getFullUrl(icon), 'desc': desc, 'short_desc': desc, 'good_for_fav': True, 'category': 'video'}) + params.update({"title": title, "url": self.cm.getFullUrl(url), "icon": self.cm.getFullUrl(icon), "desc": desc, "short_desc": desc, "good_for_fav": True, "category": "video"}) self.addVideo(params) def getLinksForVideo(self, cItem): printDBG("MovizHome.getLinksForVideo %s" % cItem) urlTab = [] - sts, data = self.getPage(cItem['url'] + "watch/") + sts, data = self.getPage(cItem["url"] + "watch/") if not sts: return [] - servers = re.findall(r']+data-watch="([^"]+)"[^>]*>.*?\s*([^<]+)', data, re.S) - for link, name in servers: + servers = re.findall(r']+data-watch="([^"]+)"[^>]*>(.*?)', data, re.S) + for link, item in servers: + name = self.cleanHtmlStr(self.cm.ph.getSearchGroups(item, r']*id="serverName"[^>]*>([^<]+)')[0]) link = self.getFullUrl(link) - name = self.cleanHtmlStr(name) printDBG("SERVER FOUND: %s -> %s" % (name, link)) - urlTab.append({'name': name, 'url': link, 'need_resolve': 1}) + urlTab.append({"name": name, "url": link, "need_resolve": 1}) return urlTab def getVideoLinks(self, videoUrl): @@ -228,19 +223,19 @@ def getArticleContent(self, cItem): genre_li = re.findall(r'
  • \s*[\s\S]*?نوع الفيلم : ([\s\S]*?)
  • ', data, re.S) genre_list = [] if genre_li: - genre_list = re.findall(r']+>([^<]+)', genre_li[0]) + genre_list = re.findall(r"]+>([^<]+)", genre_li[0]) genre_str = " - ".join(genre_list) if genre_list else "" lang_list = re.findall(r'
  • \s*[\s\S]*?لغة الفيلم : ([\s\S]*?)
  • ', data, re.S) if lang_list: - lang_list = re.findall(r']+>([^<]+)', lang_list[0]) + lang_list = re.findall(r"]+>([^<]+)", lang_list[0]) language_str = " - ".join(lang_list) if lang_list else "" - country = re.findall(r'دولة : [\s\S]*?]+>([^<]+)', data) + country = re.findall(r"دولة : [\s\S]*?]+>([^<]+)", data) country_str = country[0] if country else "" - quality = re.findall(r'جودة الفيلم : [\s\S]*?]+>([^<]+)', data) + quality = re.findall(r"جودة الفيلم : [\s\S]*?]+>([^<]+)", data) quality_str = quality[0] if quality else "" - date = re.findall(r'تاريخ اصدار الفيلم : [\s\S]*?]+>([^<]+)', data) + date = re.findall(r"تاريخ اصدار الفيلم : [\s\S]*?]+>([^<]+)", data) date_str = date[0] if date else "" - user_age = re.findall(r'التصنيف العمرى الفيلم : [\s\S]*?]+>([^<]+)', data) + user_age = re.findall(r"التصنيف العمرى الفيلم : [\s\S]*?]+>([^<]+)", data) user_age_str = user_age[0] if user_age else "" imdb = re.findall(r'
    ([\d\.]+)', data) imdb_str = imdb[0] if imdb else "" @@ -266,7 +261,7 @@ def getArticleContent(self, cItem): icon = self.getFullIconUrl(icon_match[0]) if icon_match else cItem.get("icon", "") actors = re.findall(r'itemprop="actor.*?itemprop="name">([^<]+)', data) if actors: - otherInfo['actors'] = ", ".join(actors) + otherInfo["actors"] = ", ".join(actors) return [{"title": cItem["title"], "text": full_desc, "images": [{"title": "", "url": icon}], "other_info": otherInfo}] def handleService(self, index, refresh=0, searchPattern="", searchType=""): diff --git a/IPTVPlayer/hosts/hostmycima.py b/IPTVPlayer/hosts/hostmycima.py index 3fe67cd2..cc70781f 100644 --- a/IPTVPlayer/hosts/hostmycima.py +++ b/IPTVPlayer/hosts/hostmycima.py @@ -6,71 +6,57 @@ ################################################### # localization library from Plugins.Extensions.IPTVPlayer.components.iptvplayerinit import TranslateTXT as _ + # host main class from Plugins.Extensions.IPTVPlayer.components.ihost import CHostBase, CBaseHostClass + # tools - write on log, write exception infos and merge dicts -from Plugins.Extensions.IPTVPlayer.tools.iptvtools import printDBG, printExc, MergeDicts, E2ColoR +from Plugins.Extensions.IPTVPlayer.tools.iptvtools import printDBG, printExc, E2ColoR + # add metadata to url -from Plugins.Extensions.IPTVPlayer.tools.iptvtypes import strwithmeta # library for json (instead of standard json.loads and json.dumps) -from Plugins.Extensions.IPTVPlayer.libs.e2ijson import loads as json_loads, dumps as json_dumps # read informations in m3u8 -from Plugins.Extensions.IPTVPlayer.libs.urlparserhelper import getDirectM3U8Playlist ################################################### -from Plugins.Extensions.IPTVPlayer.p2p3.UrlParse import urljoin from Plugins.Extensions.IPTVPlayer.p2p3.UrlLib import urllib_quote_plus + ################################################### # FOREIGN import ################################################### import re import time import base64 -################################################### +################################################### def GetConfigList(): return [] def gettytul(): - return 'https://mycima.fan/' # main url of host + return "https://mycima.boo/" # main url of host class MyCima(CBaseHostClass): - def __init__(self): - CBaseHostClass.__init__(self, {'history': 'mycima', 'cookie': 'mycima.cookie'}) + CBaseHostClass.__init__(self, {"history": "mycima", "cookie": "mycima.cookie"}) self.MAIN_URL = gettytul() - self.SEARCH_URL = self.MAIN_URL + 'search/' + self.SEARCH_URL = self.MAIN_URL + "search/" self.DEFAULT_ICON_URL = "https://raw.githubusercontent.com/oe-mirrors/e2iplayer/gh-pages/Thumbnails/mycima.jpg" - self.HEADER = self.cm.getDefaultHeader(browser='chrome') - self.defaultParams = { - 'header': self.HEADER, - 'use_cookie': True, - 'load_cookie': True, - 'save_cookie': True, - 'cookiefile': self.COOKIE_FILE - } + self.HEADER = self.cm.getDefaultHeader(browser="chrome") + self.defaultParams = {"header": self.HEADER, "use_cookie": True, "load_cookie": True, "save_cookie": True, "cookiefile": self.COOKIE_FILE} def getPage(self, baseUrl, addParams=None, post_data=None): # Handle unicode URLs safely if any(ord(c) > 127 for c in baseUrl): baseUrl = urllib_quote_plus(baseUrl, safe="://") - if addParams is None: addParams = dict(self.defaultParams) - # Always re-init CF parameters - addParams["cloudflare_params"] = { - "cookie_file": self.COOKIE_FILE, - "User-Agent": self.HEADER.get("User-Agent") - } - + addParams["cloudflare_params"] = {"cookie_file": self.COOKIE_FILE, "User-Agent": self.HEADER.get("User-Agent")} # For GET requests: reload cookie each time if post_data is None: - addParams["load_cookie"] = False # don’t reuse - addParams["save_cookie"] = True # save new one - + addParams["load_cookie"] = False # don’t reuse + addParams["save_cookie"] = True # save new one max_retries = 3 for attempt in range(max_retries): try: @@ -78,313 +64,221 @@ def getPage(self, baseUrl, addParams=None, post_data=None): if sts and data: return sts, data except Exception as e: - printDBG('MyCima.getPage retry %d failed: %s' % (attempt + 1, str(e))) - + printDBG("MyCima.getPage retry %d failed: %s" % (attempt + 1, str(e))) # Cloudflare timing window delay time.sleep(1.5) - printDBG(f"[MyCima] Retrying {baseUrl} failed after {max_retries} attempts due to timeout.") - return False, '' + return False, "" ################################################### # MAIN MENU ################################################### - def listMainMenu(self, cItem): - printDBG('MyCima.listMainMenu') - MAIN_CAT_TAB = [ - {'category': 'movies_categories', 'title': 'الافلام'}, - {'category': 'series_categories', 'title': 'المسلسلات'}, - {'category': 'anime_categories', 'title': 'الكارتون'}, - {'category': 'other_categories', 'title': 'Others'} - ] + printDBG("MyCima.listMainMenu") + MAIN_CAT_TAB = [{"category": "movies_categories", "title": "الافلام"}, {"category": "series_categories", "title": "المسلسلات"}, {"category": "anime_categories", "title": "الكارتون"}, {"category": "other_categories", "title": "Others"}] self.listsTab(MAIN_CAT_TAB, cItem) - # Define subcategories for each folder - self.MOVIES_CAT_TAB = [ - {'category': 'list_movies', 'title': 'افلام اجنبية', 'url': self.getFullUrl('/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%a7%d8%ac%d9%86%d8%a8%d9%8a/')}, - {'category': 'list_movies', 'title': 'افلام عربية', 'url': self.getFullUrl('/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%b9%d8%b1%d8%a8%d9%8a/')}, - {'category': 'list_movies', 'title': 'افلام هندية', 'url': self.getFullUrl('/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d9%87%d9%86%d8%af%d9%8a/')}, - {'category': 'list_movies', 'title': 'افلام تركية', 'url': self.getFullUrl('/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%aa%d8%b1%d9%83%d9%8a%d8%a9/')}, - {'category': 'list_movies', 'title': 'افلام اسيوية', 'url': self.getFullUrl('/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%a7%d8%b3%d9%8a%d9%88%d9%8a%d8%a9/')} - ] - + self.MOVIES_CAT_TAB = [{"category": "list_movies", "title": "افلام اجنبية", "url": self.getFullUrl("/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%a7%d8%ac%d9%86%d8%a8%d9%8a/")}, {"category": "list_movies", "title": "افلام عربية", "url": self.getFullUrl("/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%b9%d8%b1%d8%a8%d9%8a/")}, {"category": "list_movies", "title": "افلام هندية", "url": self.getFullUrl("/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d9%87%d9%86%d8%af%d9%8a/")}, {"category": "list_movies", "title": "افلام تركية", "url": self.getFullUrl("/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%aa%d8%b1%d9%83%d9%8a%d8%a9/")}, {"category": "list_movies", "title": "افلام اسيوية", "url": self.getFullUrl("/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%a7%d8%b3%d9%8a%d9%88%d9%8a%d8%a9/")}] self.SERIES_CAT_TAB = [ - {'category': 'list_series', 'title': 'مسلسلات اجنبية', 'url': self.getFullUrl('/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%a7%d8%ac%d9%86%d8%a8%d9%8a/')}, - {'category': 'list_series', 'title': 'مسلسلات عربية', 'url': self.getFullUrl('/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b9%d8%b1%d8%a8%d9%8a/')}, - {'category': 'list_series', 'title': 'مسلسلات هندية', 'url': self.getFullUrl('/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d9%87%d9%86%d8%af%d9%8a%d8%a9/')}, - {'category': 'list_series', 'title': 'مسلسلات تركية', 'url': self.getFullUrl('/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%aa%d8%b1%d9%83%d9%8a%d8%a9/')}, - {'category': 'list_series', 'title': 'مسلسلات اسيوية', 'url': self.getFullUrl('/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%a7%d8%b3%d9%8a%d9%88%d9%8a%d8%a9/')}, - {'category': 'list_series', 'title': 'مسلسلات مدبلجة', 'url': self.getFullUrl('/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d9%85%d8%af%d8%a8%d9%84%d8%ac%d8%a9/')}, - {'category': 'list_series', 'title': 'مسلسلات رمضان 2026', 'url': self.getFullUrl('/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b1%d9%85%d8%b6%d8%a7%d9%86-2026/')}, - {'category': 'list_series', 'title': 'مسلسلات رمضان 2025', 'url': self.getFullUrl('/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b1%d9%85%d8%b6%d8%a7%d9%86-2025/')}, - {'category': 'list_series', 'title': 'مسلسلات رمضان 2024', 'url': self.getFullUrl('/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b1%d9%85%d8%b6%d8%a7%d9%86-2024/')}, - {'category': 'list_series', 'title': 'مسلسلات رمضان 2023', 'url': self.getFullUrl('/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b1%d9%85%d8%b6%d8%a7%d9%86-2023/')}, - ] - - self.ANIME_CAT_TAB = [ - {'category': 'list_anime', 'title': 'افلام كارتون', 'url': self.getFullUrl('/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%a7%d9%86%d9%85%d9%8a/')}, - {'category': 'list_anime', 'title': 'مسلسلات كارتون', 'url': self.getFullUrl('/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%a7%d9%86%d9%85%d9%8a/')} - ] - - self.OTHER_CAT_TAB = [ - {'category': 'list_other', 'title': 'مصارعة', 'url': self.getFullUrl('/category/%d8%b9%d8%b1%d9%88%d8%b6-%d9%85%d8%b5%d8%a7%d8%b1%d8%b9%d8%a9/')}, - {'category': 'list_other', 'title': 'برامج تليفزيونية', 'url': self.getFullUrl('/category/%d8%a8%d8%b1%d8%a7%d9%85%d8%ac-%d8%aa%d9%84%d9%81%d8%b2%d9%8a%d9%88%d9%86%d9%8a%d8%a9/')} + {"category": "list_series", "title": "مسلسلات اجنبية", "url": self.getFullUrl("/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%a7%d8%ac%d9%86%d8%a8%d9%8a/")}, + {"category": "list_series", "title": "مسلسلات عربية", "url": self.getFullUrl("/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b9%d8%b1%d8%a8%d9%8a/")}, + {"category": "list_series", "title": "مسلسلات هندية", "url": self.getFullUrl("/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d9%87%d9%86%d8%af%d9%8a%d8%a9/")}, + {"category": "list_series", "title": "مسلسلات تركية", "url": self.getFullUrl("/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%aa%d8%b1%d9%83%d9%8a%d8%a9/")}, + {"category": "list_series", "title": "مسلسلات اسيوية", "url": self.getFullUrl("/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%a7%d8%b3%d9%8a%d9%88%d9%8a%d8%a9/")}, + {"category": "list_series", "title": "مسلسلات مدبلجة", "url": self.getFullUrl("/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d9%85%d8%af%d8%a8%d9%84%d8%ac%d8%a9/")}, + {"category": "list_series", "title": "مسلسلات رمضان 2026", "url": self.getFullUrl("/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b1%d9%85%d8%b6%d8%a7%d9%86-2026/")}, + {"category": "list_series", "title": "مسلسلات رمضان 2025", "url": self.getFullUrl("/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b1%d9%85%d8%b6%d8%a7%d9%86-2025/")}, + {"category": "list_series", "title": "مسلسلات رمضان 2024", "url": self.getFullUrl("/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b1%d9%85%d8%b6%d8%a7%d9%86-2024/")}, + {"category": "list_series", "title": "مسلسلات رمضان 2023", "url": self.getFullUrl("/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%b1%d9%85%d8%b6%d8%a7%d9%86-2023/")}, ] + self.ANIME_CAT_TAB = [{"category": "list_anime", "title": "افلام كارتون", "url": self.getFullUrl("/category/%d8%a7%d9%81%d9%84%d8%a7%d9%85-%d8%a7%d9%86%d9%85%d9%8a/")}, {"category": "list_anime", "title": "مسلسلات كارتون", "url": self.getFullUrl("/category/%d9%85%d8%b3%d9%84%d8%b3%d9%84%d8%a7%d8%aa-%d8%a7%d9%86%d9%85%d9%8a/")}] + self.OTHER_CAT_TAB = [{"category": "list_other", "title": "مصارعة", "url": self.getFullUrl("/category/%d8%b9%d8%b1%d9%88%d8%b6-%d9%85%d8%b5%d8%a7%d8%b1%d8%b9%d8%a9/")}, {"category": "list_other", "title": "برامج تليفزيونية", "url": self.getFullUrl("/category/%d8%a8%d8%b1%d8%a7%d9%85%d8%ac-%d8%aa%d9%84%d9%81%d8%b2%d9%8a%d9%88%d9%86%d9%8a%d8%a9/")}] def listMoviesCategories(self, cItem): - printDBG('MyCima.listMoviesCategories') + printDBG("MyCima.listMoviesCategories") self.listsTab(self.MOVIES_CAT_TAB, cItem) def listSeriesCategories(self, cItem): - printDBG('MyCima.listMoviesCategories') + printDBG("MyCima.listMoviesCategories") self.listsTab(self.SERIES_CAT_TAB, cItem) def listAnimeCategories(self, cItem): - printDBG('MyCima.listAnimeCategories') + printDBG("MyCima.listAnimeCategories") self.listsTab(self.ANIME_CAT_TAB, cItem) def listOtherCategories(self, cItem): - printDBG('MyCima.listOtherCategories') + printDBG("MyCima.listOtherCategories") self.listsTab(self.OTHER_CAT_TAB, cItem) ################################################### # LIST UNITS FROM CATEGORY PAGE (WITH PAGINATION) ################################################### def listUnits(self, cItem): - printDBG('MyCima.listUnits >>> %s' % cItem) - - sts, data = self.getPage(cItem['url']) + printDBG("MyCima.listUnits >>> %s" % cItem) + sts, data = self.getPage(cItem["url"]) # printDBG('data.listUnits >>> %s' % data) if not sts or not data: - printDBG('listUnits: failed to load page') + printDBG("listUnits: failed to load page") return - ################################################### # MAIN MOVIE BLOCK ################################################### main_block = self.cm.ph.getDataBeetwenMarkers(data, '
    ', False)[1] # printDBG('main_block.listUnits >>> %s' % main_block) if not main_block: - printDBG('listUnits: No main_block found') + printDBG("listUnits: No main_block found") return - ################################################### # MOVIE BOXES ################################################### items = self.cm.ph.getAllItemsBeetwenMarkers(main_block, '
    ') # items2 = self.cm.ph.getAllItemsBeetwenMarkers(main_block, '
    ')[1] # printDBG('items2.listUnits >>> %s' % items2) - printDBG('listUnits: Found %d items' % len(items)) - + printDBG("listUnits: Found %d items" % len(items)) for item in items: # --- URL --- url = self.cm.ph.getSearchGroups(item, r'href="([^"]+)"')[0] if not url: continue - # --- ICON FIX --- icon = self.cm.ph.getSearchGroups(item, r'data-lazy-style="[^"]*url\(([^)]+)\)')[0] if not icon: icon = self.cm.ph.getSearchGroups(item, r'src="([^"]+)"')[0] icon = self.getFullUrl(icon) - printDBG('icon.listUnits >>> %s' % icon) - + printDBG("icon.listUnits >>> %s" % icon) # --- TITLE --- - title = self.cleanHtmlStr( - self.cm.ph.getSearchGroups(item, r'title="([^"]+)"')[0] - ) + title = self.cleanHtmlStr(self.cm.ph.getSearchGroups(item, r'title="([^"]+)"')[0]) if not title: - title = self.cleanHtmlStr( - self.cm.ph.getDataBeetwenMarkers(item, '', False)[1] - ) - - title = ( - title.replace("مشاهدة فيلم", "") - .replace("مشاهدة", "") - .replace("فيلم", "") - .replace("مسلسل", "") - .replace("مترجمة اون لاين", "") - .replace("مترجم اون لاين", "") - .replace("مترجمة", "") - .replace("مترجم", "") - .replace("اون لاين", "") - .replace("مدبلجة", "") - .replace("مدبلج", "") - .strip() - ) - printDBG('title.listUnits >>> %s' % title) - desc = self.cleanHtmlStr( - self.cm.ph.getDataBeetwenMarkers(item, '', '', False)[1] - ) + title = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(item, "", False)[1]) + title = title.replace("مشاهدة فيلم", "").replace("مشاهدة", "").replace("فيلم", "").replace("مسلسل", "").replace("مترجمة اون لاين", "").replace("مترجم اون لاين", "").replace("مترجمة", "").replace("مترجم", "").replace("اون لاين", "").replace("مدبلجة", "").replace("مدبلج", "").strip() + printDBG("title.listUnits >>> %s" % title) + desc = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(item, '', "", False)[1]) if not desc: desc = title desc = desc.replace(" ", "").strip() - year = self.cleanHtmlStr( - self.cm.ph.getDataBeetwenMarkers(item, '', '', False)[1] - ) + year = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(item, '', "", False)[1]) year = year.replace("(", "").replace(")", "").strip() - desc = desc + '(' + year + ')' + desc = desc + "(" + year + ")" # --- QUALITY --- - quality = self.cleanHtmlStr( - self.cm.ph.getDataBeetwenMarkers(item, '', '', False)[1] - ) - printDBG('quality.listUnits >>> %s' % quality) - + quality = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(item, '', "", False)[1]) + printDBG("quality.listUnits >>> %s" % quality) ################################################### # COLORIZE TITLE (movie name + year) ################################################### - match = re.search(r'(.+?)\s*\(?(\d{4})\)?$', title) + match = re.search(r"(.+?)\s*\(?(\d{4})\)?$", title) if match: movie_title = match.group(1).strip() movie_year = match.group(2).strip() - colored_title = ( - f"{E2ColoR('yellow')}{movie_title} " - f"{E2ColoR('cyan')}{movie_year}" - f"{E2ColoR('white')}" - ) + colored_title = f"{E2ColoR('yellow')}{movie_title} " f"{E2ColoR('cyan')}{movie_year}" f"{E2ColoR('white')}" else: colored_title = f"{E2ColoR('yellow')}{title}{E2ColoR('white')}" - ################################################### # COLORIZE QUALITY ################################################### - q_color = 'white' - if re.search(r'4K|1080|BluRay', quality, re.I): - q_color = 'green' - elif re.search(r'720|HDRip|WEB', quality, re.I): - q_color = 'yellow' - elif re.search(r'CAM|TS|HDCAM', quality, re.I): - q_color = 'red' - - colored_quality = ( - f"{E2ColoR(q_color)}{quality if quality else 'N/A'}{E2ColoR('white')}" - ) - + q_color = "white" + if re.search(r"4K|1080|BluRay", quality, re.I): + q_color = "green" + elif re.search(r"720|HDRip|WEB", quality, re.I): + q_color = "yellow" + elif re.search(r"CAM|TS|HDCAM", quality, re.I): + q_color = "red" + colored_quality = f"{E2ColoR(q_color)}{quality if quality else 'N/A'}{E2ColoR('white')}" ################################################### # FINAL ITEM ################################################### params = dict(cItem) - params.update({ - 'title': colored_title, - 'url': self.getFullUrl(url), - 'icon': icon, - 'desc': f"{colored_quality} | {desc}", - 'category': 'explore_items' - }) + params.update({"title": colored_title, "url": self.getFullUrl(url), "icon": icon, "desc": f"{colored_quality} | {desc}", "category": "explore_items"}) self.addDir(params) - if len(items) == 0: - printDBG('listUnits: No media-card items found') - + printDBG("listUnits: No media-card items found") ################################################### # PAGINATION ################################################### - pagination_block = self.cm.ph.getDataBeetwenMarkers(data, '', False)[1] + pagination_block = self.cm.ph.getDataBeetwenMarkers(data, '", False)[1] if pagination_block: next_url = self.cm.ph.getSearchGroups(pagination_block, r']+class="next page-numbers"[^>]+href="([^"]+)"')[0] prev_url = self.cm.ph.getSearchGroups(pagination_block, r']+class="prev page-numbers"[^>]+href="([^"]+)"')[0] - if prev_url: params = dict(cItem) - params.update({ - 'title': '<<< ' + _('Previous'), - 'url': self.getFullUrl(prev_url), - 'category': 'list_movies' - }) + params.update({"title": "<<< " + _("Previous"), "url": self.getFullUrl(prev_url), "category": "list_movies"}) self.addDir(params) - printDBG('listUnits: Found previous page %s' % prev_url) - + printDBG("listUnits: Found previous page %s" % prev_url) if next_url: params = dict(cItem) - params.update({ - 'title': _('Next') + ' >>>', - 'url': self.getFullUrl(next_url), - 'category': 'list_movies' - }) + params.update({"title": _("Next") + " >>>", "url": self.getFullUrl(next_url), "category": "list_movies"}) self.addDir(params) - printDBG('listUnits: Found next page %s' % next_url) + printDBG("listUnits: Found next page %s" % next_url) ################################################### # EXPLORE ITEM (get list of servers) ################################################### def exploreItems(self, cItem): - printDBG('MyCima.exploreItems >>> %s' % cItem) - - url = cItem['url'] - printDBG('url.exploreItems >>> %s' % url) - + printDBG("MyCima.exploreItems >>> %s" % cItem) + url = cItem["url"] + printDBG("url.exploreItems >>> %s" % url) sts, data = self.getPage(url) # printDBG('data.exploreItems >>> %s' % data) if not sts: return - # Extract the watch list block - watch_list = self.cm.ph.getDataBeetwenMarkers(data, '
    ', False)[1] - printDBG('watch_list.exploreItems >>> %s' % watch_list) - + watch_list = self.cm.ph.getDataBeetwenMarkers(data, '
    ", False)[1] + printDBG("watch_list.exploreItems >>> %s" % watch_list) if not watch_list: - printDBG('No watch list found') + printDBG("No watch list found") return - # Extract all
  • items - li_items = self.cm.ph.getAllItemsBeetwenMarkers(watch_list, '') - printDBG('Found %d servers' % len(li_items)) - + li_items = self.cm.ph.getAllItemsBeetwenMarkers(watch_list, "") + printDBG("Found %d servers" % len(li_items)) for item in li_items: # --- Extract data-watch attribute --- data_watch = self.cm.ph.getSearchGroups(item, r'data-watch="([^"]+)"')[0] - printDBG('data_watch.exploreItems >>> %s' % data_watch) + printDBG("data_watch.exploreItems >>> %s" % data_watch) if not data_watch: continue - # --- Extract the base64 part --- # Example: https://sharevid.online/play/aHR0cHM6Ly9taXZhbHlvLmNvbS92L2s5YnAxbmZvNWU4MA==/ - b64_part = self.cm.ph.getSearchGroups(data_watch, r'/play/([^/]+)')[0] - printDBG('b64_part.exploreItems >>> %s' % b64_part) + b64_part = self.cm.ph.getSearchGroups(data_watch, r"/play/([^/]+)")[0] + printDBG("b64_part.exploreItems >>> %s" % b64_part) if not b64_part: continue - # --- Decode Base64 safely --- try: decoded_bytes = base64.b64decode(b64_part) - decoded_url = decoded_bytes.decode('utf-8', errors='ignore') + decoded_url = decoded_bytes.decode("utf-8", errors="ignore") url = decoded_url.strip() except Exception as e: - printDBG('Base64 decode error: %s' % e) + printDBG("Base64 decode error: %s" % e) continue - - printDBG('decoded_url.exploreItems >>> %s' % url) - + printDBG("decoded_url.exploreItems >>> %s" % url) # --- Extract server name (text inside
  • ) --- # Example:
  • vikingfile
  • title = self.cleanHtmlStr(item) if not title: - title = 'Unknown Server' - printDBG('title.exploreItems >>> %s' % title) - + title = "Unknown Server" + printDBG("title.exploreItems >>> %s" % title) # --- Final video entry --- params = dict(cItem) - params.update({ - 'title': title, - 'url': url, - 'category': 'video', - 'type': 'video', - }) + params.update( + { + "title": title, + "url": url, + "category": "video", + "type": "video", + } + ) self.addVideo(params) - if len(li_items) == 0: - printDBG('No
  • items found in server-list') + printDBG("No
  • items found in server-list") ################################################### # GET LINKS FOR VIDEO ################################################### def getLinksForVideo(self, cItem): - printDBG('MyCima.getLinksForVideo [%s]' % cItem) - url = cItem.get('url', '') + printDBG("MyCima.getLinksForVideo [%s]" % cItem) + url = cItem.get("url", "") if not url: return [] - return [{'name': 'MyCima - %s' % cItem.get('title', ''), 'url': url, 'need_resolve': 1}] + return [{"name": "MyCima - %s" % cItem.get("title", ""), "url": url, "need_resolve": 1}] def getVideoLinks(self, url): printDBG("MyCima.getVideoLinks [%s]" % url) @@ -399,62 +293,55 @@ def getVideoLinks(self, url): def listSearchResult(self, cItem, searchPattern, searchType): printDBG("MyCima.listSearchResult cItem[%s], searchPattern[%s] searchType[%s]" % (cItem, searchPattern, searchType)) cItem = dict(cItem) - cItem['url'] = self.SEARCH_URL + urllib_quote_plus(searchPattern) + cItem["url"] = self.SEARCH_URL + urllib_quote_plus(searchPattern) self.listUnits(cItem) - def handleService(self, index, refresh=0, searchPattern='', searchType=''): - printDBG('MyCima.handleService start') - + def handleService(self, index, refresh=0, searchPattern="", searchType=""): + printDBG("MyCima.handleService start") CBaseHostClass.handleService(self, index, refresh, searchPattern, searchType) - - name = self.currItem.get("name", '') - category = self.currItem.get("category", '') - + name = self.currItem.get("name", "") + category = self.currItem.get("category", "") printDBG("handleService: >> name[%s], category[%s] " % (name, category)) self.currList = [] - # MAIN MENU if name is None: - self.listMainMenu({'name': 'category'}) - elif category == 'explore_items': + self.listMainMenu({"name": "category"}) + elif category == "explore_items": self.exploreItems(self.currItem) - elif category == 'movies_categories': + elif category == "movies_categories": self.listMoviesCategories(self.currItem) - elif category == 'series_categories': + elif category == "series_categories": self.listSeriesCategories(self.currItem) - elif category == 'anime_categories': + elif category == "anime_categories": self.listAnimeCategories(self.currItem) - elif category == 'other_categories': + elif category == "other_categories": self.listOtherCategories(self.currItem) - elif category == 'list_movies': + elif category == "list_movies": self.listUnits(self.currItem) - elif category == 'list_series': + elif category == "list_series": self.listUnits(self.currItem) - elif category == 'list_anime': + elif category == "list_anime": self.listUnits(self.currItem) - elif category == 'list_other': + elif category == "list_other": self.listUnits(self.currItem) - # SEARCH elif category in ["search", "search_next_page"]: cItem = dict(self.currItem) - cItem.update({'search_item': False, 'name': 'category'}) + cItem.update({"search_item": False, "name": "category"}) self.listSearchResult(cItem, searchPattern, searchType) # HISTORY SEARCH elif category == "search_history": - self.listsHistory({'name': 'history', 'category': 'search'}, 'desc', _("Type: ")) + self.listsHistory({"name": "history", "category": "search"}, "desc", _("Type: ")) else: printExc() - CBaseHostClass.endHandleService(self, index, refresh) class IPTVHost(CHostBase): - def __init__(self): CHostBase.__init__(self, MyCima(), True, []) def withArticleContent(self, cItem): - if 'video' == cItem.get('type', '') or 'explore_item' == cItem.get('category', ''): + if "video" == cItem.get("type", "") or "explore_item" == cItem.get("category", ""): return True return False diff --git a/IPTVPlayer/hosts/hostq3isk.py b/IPTVPlayer/hosts/hostq3isk.py index 211111c4..3a046857 100644 --- a/IPTVPlayer/hosts/hostq3isk.py +++ b/IPTVPlayer/hosts/hostq3isk.py @@ -133,23 +133,13 @@ def exploreItems(self, cItem): ################################################### info_desc = "" work_title = "" - main_info_block = self.cm.ph.getDataBeetwenMarkers( - data, '
    ', '
    ', '
    ", "", False)[1] - ) + work_title = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(data, "

    ", "

    ", False)[1]) # --- Story --- - story = self.cleanHtmlStr( - self.cm.ph.getDataBeetwenMarkers( - main_info_block, '
    ', "
    ", False - )[1] - ) + story = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(main_info_block, '
    ', "
    ", False)[1]) tax_info = {} - all_tax_blocks = re.findall( - r'
    (.*?)
    ', main_info_block, re.S - ) + all_tax_blocks = re.findall(r'
    (.*?)
    ', main_info_block, re.S) label_map = { "الممثلين": "Cast", "السنة": "Year", @@ -170,9 +160,7 @@ def exploreItems(self, cItem): info_parts = [] for key in field_order: if key in tax_info and tax_info[key]: - info_parts.append( - f"{E2ColoR('yellow')}{key}:{E2ColoR('white')} {tax_info[key]}" - ) + info_parts.append(f"{E2ColoR('yellow')}{key}:{E2ColoR('white')} {tax_info[key]}") if info_parts: info_desc = " | ".join(info_parts) if story: @@ -180,9 +168,7 @@ def exploreItems(self, cItem): ################################################### # MAIN SERVERS BLOCK ################################################### - main_block = self.cm.ph.getDataBeetwenMarkers( - data, '
      ', "
    ", True - )[1] + main_block = self.cm.ph.getDataBeetwenMarkers(data, '
      ', "
    ", True)[1] printDBG("main_block.exploreItems >>> %s" % main_block) ################################################### # PARSE ITEMS CORRECTLY @@ -200,9 +186,7 @@ def exploreItems(self, cItem): continue video_url = self.getFullUrl(video_url) # --- TITLE --- - server_name = self.cleanHtmlStr( - self.cm.ph.getDataBeetwenMarkers(item, "", "", False)[1] - ) + server_name = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(item, "", "", False)[1]) if not server_name: server_name = _("Server") if work_title: @@ -231,18 +215,14 @@ def listSeriesUnits(self, cItem): ################################################### # MAIN SERIES BLOCK ################################################### - main_block = self.cm.ph.getDataBeetwenMarkers( - data, '
    ', '
  • ", info_part, re.S) for item in info_items: @@ -222,21 +229,21 @@ def showMovieDetails(self, cItem): if not label or not value: continue key = "" - if any(x in label for x in ['تصنيف', 'قسم']): + if any(x in label for x in ["تصنيف", "قسم"]): key = "{}Category:{}".format(Y, W) - elif 'نوع' in label: + elif "نوع" in label: key = "{}Genre:{}".format(Y, W) - elif 'جودة' in label: + elif "جودة" in label: key = "{}Quality:{}".format(Y, W) - elif any(x in label for x in ['تاريخ', 'السنة']): + elif any(x in label for x in ["تاريخ", "السنة"]): key = "{}Year:{}".format(Y, W) - elif 'لغة' in label: + elif "لغة" in label: key = "{}Language:{}".format(Y, W) - elif any(x in label for x in ['الدولة', 'البلد']): + elif any(x in label for x in ["الدولة", "البلد"]): key = "{}Country:{}".format(Y, W) - elif 'مدة' in label: + elif "مدة" in label: key = "{}Runtime:{}".format(Y, W) - elif 'بطولة' in label: + elif "بطولة" in label: key = "{}Stars:{}".format(Y, W) if key and value: meta_parts.append("{} {}".format(key, value)) @@ -246,15 +253,7 @@ def showMovieDetails(self, cItem): full_desc = "{}\n{}Story:{} {}".format(one_line_info, L, W, story) else: full_desc = one_line_info - params = { - "category": "explore_item", - "title": "{}".format(cItem.get("title", "")), - "url": url, - "icon": cItem.get("icon", ""), - "desc": full_desc, - "good_for_fav": True, - "type": "category" - } + params = {"category": "explore_item", "title": "{}".format(cItem.get("title", "")), "url": url, "icon": cItem.get("icon", ""), "desc": full_desc, "good_for_fav": True, "type": "category"} self.addDir(params) def listSeriesItems(self, cItem): @@ -265,12 +264,12 @@ def listSeriesItems(self, cItem): ############################################################ # Extract series block (All episodes) ############################################################ - tmp = self.cm.ph.getDataBeetwenMarkers(data, "
    ", True) + tmp = self.cm.ph.getDataBeetwenMarkers(data, '
    ", True) if not data_items: # fallback if needed - parts = tmp.split("
    ", "")[1]) - quality = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, "
  • ", "
  • ")[1]) - runtime = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, "
  • ", "
  • ")[1]) - year = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, "
  • ", "
  • ")[1]) - imdb = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, "
  • ", "
  • ")[1]) + genre = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
  • ', "
  • ")[1]) + quality = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
  • ', "
  • ")[1]) + runtime = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
  • ', "
  • ")[1]) + year = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
  • ', "
  • ")[1]) + imdb = self.cleanHtmlStr(self.cm.ph.getDataBeetwenMarkers(m, '
  • ', "
  • ")[1]) desc_list = [] if genre: desc_list.append("{}Genre:{} {}".format(Y, W, genre)) @@ -338,7 +337,7 @@ def listSeriesItems(self, cItem): printDBG(str(params)) self.addDir(params) # === PAGINATION HANDLING === - pagination = self.cm.ph.getDataBeetwenMarkers(data, "
    ", "
    ", True)[1] + pagination = self.cm.ph.getDataBeetwenMarkers(data, '", True)[1] next_page = self.cm.ph.getSearchGroups(pagination, r"]+class=\"[^\"]*next[^\"]*\"[^>]+href=\"([^\"]+)\"")[0] if not next_page: next_page = self.cm.ph.getSearchGroups(pagination, r"]+href=\"([^\"]+)\"[^>]*>[^<]*?(?:Next|←|»|«|التالي)[^<]*?")[0] @@ -372,8 +371,8 @@ def exploreItems(self, cItem): ########################################################## if "series-movies" in data: printDBG("Detected Series/Assembly page") - series_block = self.cm.ph.getDataBeetwenMarkers(data, "
    ")[1] - items = self.cm.ph.getAllItemsBeetwenMarkers(series_block, "
    ") + series_block = self.cm.ph.getDataBeetwenMarkers(data, '
    ")[1] + items = self.cm.ph.getAllItemsBeetwenMarkers(series_block, '
    ") for item in items: url = self.cm.ph.getSearchGroups(item, r"""href=["']([^"']+)["']""")[0] title = self.cm.ph.getSearchGroups(item, r"""title=["']([^"']+)["']""")[0] @@ -382,19 +381,13 @@ def exploreItems(self, cItem): icon = self.cm.ph.getSearchGroups(item, r"""src=["']([^"']+)["']""")[0] if not url: continue - params = MergeDicts(cItem, { - "title": self.cleanHtmlStr(title), - "url": self.getFullUrl(url), - "icon": self._fixUrl(icon), - "type": "category", - "category": "explore_item" - }) + params = MergeDicts(cItem, {"title": self.cleanHtmlStr(title), "url": self.getFullUrl(url), "icon": self._fixUrl(icon), "type": "category", "category": "explore_item"}) self.addDir(params) return ########################################################## # Extract Hosts (Format: Movie Title + Server Name) ########################################################## - server_block = self.cm.ph.getDataBeetwenMarkers(data, "