diff --git a/addon.xml b/addon.xml index 39c9855..bf7e081 100644 --- a/addon.xml +++ b/addon.xml @@ -1,7 +1,7 @@ diff --git a/changelog.txt b/changelog.txt index bab6f09..d9b6f7e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,8 @@ +1.0.0 - 19/11/2022 +- Adding new BSPlayer Engines (OpenSubtitles, GetSubtitle) + 0.3.0 - 15/05/2022 -- Migrating to python 3, support kodi 19 +- Migrating to python 3, support Kodi 19 0.2.1 - 07/11/2015 - Fixed android non rar support, rearrange code diff --git a/resources/language/English/strings.po b/resources/language/English/strings.po index 96400bc..c6dc5ea 100644 --- a/resources/language/English/strings.po +++ b/resources/language/English/strings.po @@ -20,4 +20,20 @@ msgstr "" msgctxt "#32002" msgid "Manual search not supported." +msgstr "" + +msgctxt "#32003" +msgid "Username" +msgstr "" + +msgctxt "#32004" +msgid "Password" +msgstr "" + +msgctxt "#32005" +msgid "Your OpenSubtitles.org username or password is empty" +msgstr "" + +msgctxt "#32006" +msgid "OpenSubtitles Login Details" msgstr "" \ No newline at end of file diff --git a/resources/lib/bsplayer.py b/resources/lib/bsplayer.py index 3042c24..fdd7b9f 100644 --- a/resources/lib/bsplayer.py +++ b/resources/lib/bsplayer.py @@ -1,24 +1,29 @@ -import gzip +import re +import gzip +import zlib +import json import socket import random from time import sleep +from base64 import b64decode from xml.etree import ElementTree from urllib.request import Request +from abc import ABC, abstractmethod +from urllib.parse import urlencode, urlparse, parse_qsl from .utils import movie_size_and_hash, get_session, log -class BSPlayer(object): - VERSION = "2.67" - DOMAIN = "api.bsplayer-subtitles.com" - SUB_DOMAINS = [ - 's1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', - 's101', 's102', 's103', 's104', 's105', 's106', 's107', 's108', 's109' - ] - - def __init__(self, search_url=None, proxies=None): - self.session = get_session(proxies=proxies) - self.search_url = search_url or self.get_sub_domain() +class BSPlayerSubtitleEngine(ABC): + def __init__(self, search_url, proxies=None, username="", password="", + app_id="BSPlayer v2.7", user_agent="BSPlayer/2.x (1106.12378)"): + self.proxies = proxies + self.username = username + self.password = password + self.app_id = app_id + self.user_agent = user_agent + self.session = get_session(proxies=self.proxies) + self.search_url = search_url self.token = None def __enter__(self): @@ -28,6 +33,51 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): return self.logout() + @abstractmethod + def login(self): + pass + + @abstractmethod + def logout(self): + pass + + @abstractmethod + def search_subtitles(self, movie_path, language_ids="heb,eng", logout=False): + pass + + def download_subtitles(self, download_url, dest_path): + session = get_session(proxies=self.proxies, http_10=True) + session.addheaders = [("User-Agent", "Mozilla/4.0 (compatible; Synapse)"), + ("Content-Length", 0)] + res = session.open(download_url) + if res: + gf = gzip.GzipFile(fileobj=res) + with open(dest_path, "wb") as f: + f.write(gf.read()) + f.flush() + gf.close() + log("BSPlayerSubtitleEngine.download_subtitles", f"File {repr(download_url)} Download Successfully.") + return True + log("BSPlayerSubtitleEngine.download_subtitles", f"File {repr(download_url)} Download Failed.") + return False + + +class BSPlayer(BSPlayerSubtitleEngine): + DOMAIN = "api.bsplayer-subtitles.com" + SUB_DOMAINS = [ + "s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8", + "s101", "s102", "s103", "s104", "s105", "s106", "s107", "s108", "s109" + ] + + def __init__(self, search_url=None, proxies=None, username="", password="", + app_id="BSPlayer v2.7", user_agent="BSPlayer/2.x (1106.12378)"): + search_url = search_url or self.get_sub_domain() + super().__init__( + search_url=search_url, proxies=proxies, + username=username, password=password, + app_id=app_id, user_agent=user_agent + ) + def get_sub_domain(self, tries=5): for t in range(tries): domain = f"{random.choice(self.SUB_DOMAINS)}.{self.DOMAIN}" @@ -38,36 +88,36 @@ def get_sub_domain(self, tries=5): continue raise Exception("API Domain not found") - def api_request(self, func_name='logIn', params='', tries=5): + def api_request(self, func_name, params="", tries=5, delay=1): headers = { - 'User-Agent': 'BSPlayer/2.x (1022.12360)', - 'Content-Type': 'text/xml; charset=utf-8', - 'Connection': 'close', - 'SOAPAction': f'"http://{self.DOMAIN}/v1.php#{func_name}"' + "User-Agent": self.user_agent, + "Content-Type": "text/xml; charset=utf-8", + "Connection": "close", + "SOAPAction": f'"http://{self.DOMAIN}/v1.php#{func_name}"' } - data = ( - '\n' - '' - '' - f'{params}' - ) - log('BSPlayer.api_request', 'Sending request: %s.' % func_name) + log("BSPlayer.api_request", f"Sending request: {func_name}.") for i in range(tries): + data = ( + '\n' + '' + '' + f'{params}' + ) try: req = Request(self.search_url, data=data.encode(), headers=headers, method="POST") res = self.session.open(req) return ElementTree.fromstring(res.read()) except Exception as ex: - log("BSPlayer.api_request", "ERROR: %s." % ex) - if func_name == 'logIn': + log("BSPlayer.api_request", f"ERROR: {ex}.") + if func_name == "logIn": self.search_url = self.get_sub_domain() - sleep(1) - log('BSPlayer.api_request', 'ERROR: Too many tries (%d)...' % tries) - raise Exception('Too many tries...') + sleep(delay) + log("BSPlayer.api_request", f"ERROR: Too many tries ({tries})...") + raise Exception("Too many tries...") def login(self): # If already logged in @@ -75,18 +125,19 @@ def login(self): return True root = self.api_request( - func_name='logIn', + func_name="logIn", params=( - '' - '' - f'BSPlayer v{self.VERSION}' + f"{self.username}" + f"{self.password}" + f"{self.app_id}" ) ) - res = root.find('.//return') - if res.find('status').text == 'OK': - self.token = res.find('data').text + res = root.find(".//return") + if res.find("status").text.upper() == "OK": + self.token = res.find("data").text log("BSPlayer.login", "Logged In Successfully.") return True + log("BSPlayer.login", "Logged In Failed.") return False def logout(self): @@ -95,17 +146,17 @@ def logout(self): return True root = self.api_request( - func_name='logOut', - params=f'{self.token}' + func_name="logOut", + params=f"{self.token}" ) - res = root.find('.//return') + res = root.find(".//return") self.token = None - if res.find('status').text == 'OK': + if res.find("status").text.upper() == "OK": log("BSPlayer.logout", "Logged Out Successfully.") return True return False - def search_subtitles(self, movie_path, language_ids='heb,eng', logout=False): + def search_subtitles(self, movie_path, language_ids="heb,eng", logout=False): if not self.login(): return None @@ -115,53 +166,313 @@ def search_subtitles(self, movie_path, language_ids='heb,eng', logout=False): try: movie_size, movie_hash = movie_size_and_hash(movie_path) except Exception as ex: - print(ex) - exit(1) - log('BSPlayer.search_subtitles', f'Movie Size: {movie_size}, Movie Hash: {movie_hash}.') + log("BSPlayer.search_subtitles", f"Error Calculating Movie Size / Hash: {ex}.") + return [] + + log("BSPlayer.search_subtitles", f"Movie Size: {movie_size}, Movie Hash: {movie_hash}.") root = self.api_request( - func_name='searchSubtitles', + func_name="searchSubtitles", params=( - f'{self.token}' - f'{movie_hash}' - f'{movie_size}' - f'{language_ids}' - '*' + f"{self.token}" + f"{movie_hash}" + f"{movie_size}" + f"{language_ids}" + "*" ) ) - res = root.find('.//return/result') - if res.find('status').text != 'OK': + res = root.find(".//return/result") + status = res.find("status").text.upper() + if status != "OK": + log("BSPlayer.search_subtitles", f"Status: {status}.") return [] - items = root.findall('.//return/data/item') + items = root.findall(".//return/data/item") subtitles = [] if items: - log("BSPlayer.search_subtitles", "Subtitles Found.") for item in items: - subtitles.append(dict( - subID=item.find('subID').text, - subDownloadLink=item.find('subDownloadLink').text, - subLang=item.find('subLang').text, - subName=item.find('subName').text, - subFormat=item.find('subFormat').text, - subRating=item.find('subRating').text or '0' - )) + subtitle = dict( + subID=item.find("subID").text, + subDownloadLink=item.find("subDownloadLink").text, + subLang=item.find("subLang").text, + subName=item.find("subName").text, + subFormat=item.find("subFormat").text, + subRating=item.find("subRating").text or "0" + ) + subtitles.append(subtitle) + log("BSPlayer.search_subtitles", f"Subtitles Found: {json.dumps(subtitles)}.") if logout: self.logout() return subtitles - @staticmethod - def download_subtitles(download_url, dest_path, proxies=None): - session = get_session(proxies=proxies, http_10=True) - session.addheaders = [('User-Agent', 'Mozilla/4.0 (compatible; Synapse)'), - ('Content-Length', 0)] - res = session.open(download_url) - if res: - gf = gzip.GzipFile(fileobj=res) - with open(dest_path, 'wb') as f: - f.write(gf.read()) + +class OpenSubtitles(BSPlayerSubtitleEngine): + DOMAIN = "bsplayer.api.opensubtitles.org" + + def __init__(self, username, password, search_url=None, proxies=None, + app_id="BSPlayer v2.78", user_agent="XmlRpc"): + search_url = search_url or f"http://{self.DOMAIN}/xml-rpc" + super().__init__( + search_url=search_url, proxies=proxies, + username=username, password=password, + app_id=app_id, user_agent=user_agent + ) + + def api_request(self, func_name, params="", tries=5, delay=1): + headers = { + "User-Agent": self.user_agent, + "Accept": "text/*", + "Content-Type": "text/xml", + "Pragma": "no-cache", + "Connection": "close", + } + + log("OpenSubtitles.api_request", f"Sending request: {func_name}.") + for i in range(tries): + data = ( + '\n' + f'{func_name}\n' + f'{params}' + '' + ) + try: + req = Request(self.search_url, data=data.encode(), headers=headers, method="POST") + res = self.session.open(req) + return ElementTree.fromstring(res.read()) + except Exception as ex: + log("OpenSubtitles.api_request", f"ERROR: {ex}.") + sleep(delay) + log("OpenSubtitles.api_request", f"ERROR: Too many tries ({tries})...") + raise Exception("Too many tries...") + + def login(self): + # If already logged in + if self.token: + return True + + root = self.api_request( + func_name="LogIn", + params=( + f'{self.username}' + f'{self.password}' + f'en{self.app_id}' + ) + ) + + res = { + member.find("name").text: member.find("value/*").text + for member in root.findall(".//struct//member") + } + if res.get("status", "").upper() == "200 OK": + self.token = res["token"] + log("OpenSubtitles.login", "Logged In Successfully.") + return True + log("OpenSubtitles.login", "Logged In Failed.") + return False + + def logout(self): + # If already logged out / not logged in + if not self.token: + return True + + root = self.api_request( + func_name="LogOut", + params=f"{self.token}" + ) + res = { + member.find("name").text: member.find("value/*").text + for member in root.findall(".//struct//member") + } + if res.get("status", "").upper() == "200 OK": + self.token = None + log("OpenSubtitles.logout", "Logged Out Successfully.") + return True + return False + + def search_subtitles(self, movie_path, language_ids="heb,eng", logout=False): + if not self.login(): + return None + + if isinstance(language_ids, (tuple, list, set)): + language_ids = ",".join(language_ids) + + try: + movie_size, movie_hash = movie_size_and_hash(movie_path) + except Exception as ex: + log("OpenSubtitles.search_subtitles", f"Error Calculating Movie Size / Hash: {ex}.") + return [] + + log("OpenSubtitles.search_subtitles", f"Movie Size: {movie_size}, Movie Hash: {movie_hash}.") + root = self.api_request( + func_name="SearchSubtitles", + params=( + f"{self.token}" + "" + "imdbid" + f"moviebytesize{movie_size}.000000" + f"moviehash{movie_hash}" + f"sublanguageid{language_ids}" + "" + ) + ) + res = { + member.find("name").text: member.find("value/*").text + for member in root.findall(".//struct//member") + } + status = res.get("status", "").upper() + if status != "200 OK": + log("OpenSubtitles.search_subtitles", f"Status: {status}.") + return [] + + items = [ + { + member.find("name").text: member.find("value/*").text + for member in item.findall(".//struct//member") + } for item in root.findall(".//member/value/array/data/value") + ] + subtitles = [] + if items: + for item in items: + subtitle = dict( + subID=item.get("IDSubtitle"), + subDownloadLink=item.get("SubDownloadLink"), + subLang=item.get("sublanguageid"), + subName=item.get("SubFileName"), + subFormat=item.get("subFormat"), + subRating=item.get("SubRating") or "0" + ) + subtitles.append(subtitle) + log("OpenSubtitles.search_subtitles", f"Subtitles Found: {json.dumps(subtitles)}.") + + if logout: + self.logout() + + return subtitles + + +class GetSubtitle(BSPlayerSubtitleEngine): + DOMAIN = "api.getsubtitle.com" + + def __init__(self, search_url=None, proxies=None, username="", password="", + app_id="", user_agent="gSOAP/2.7"): + search_url = search_url or f"http://{self.DOMAIN}/server.php" + super().__init__( + search_url=search_url, proxies=proxies, + username=username, password=password, + app_id=app_id, user_agent=user_agent + ) + + def login(self): + log("GetSubtitle.login", "Logged In Successfully.") + return True + + def logout(self): + log("GetSubtitle.logout", "Logged Out Successfully.") + return True + + def api_request(self, func_name, params="", tries=5, delay=1): + headers = { + "User-Agent": self.user_agent, + "Content-Type": "text/xml; charset=utf-8", + "Connection": "close", + "SOAPAction": f'"{func_name}_wsdl#{func_name}"', + } + soap_env = ( + '\n' + '' + ) + log("GetSubtitle.api_request", f"Sending request: {func_name}.") + for i in range(tries): + namespace = re.search(fr'xmlns:(?Pns\d+)="{func_name}_wsdl"', soap_env).group('ns') + data = soap_env + ( + f'' + f'<{namespace}:{func_name}>{params}' + ) + try: + req = Request(self.search_url, data=data.encode(), headers=headers, method="POST") + res = self.session.open(req) + return ElementTree.fromstring(res.read()) + except Exception as ex: + log("GetSubtitle.api_request", f"ERROR: {ex}.") + sleep(delay) + log("GetSubtitle.api_request", f"ERROR: Too many tries ({tries})...") + raise Exception("Too many tries...") + + def search_subtitles(self, movie_path, language_ids="heb,eng", logout=False): + if isinstance(language_ids, (tuple, list, set)): + language_ids = ",".join(language_ids) + + try: + movie_size, movie_hash = movie_size_and_hash(movie_path) + except Exception as ex: + log("GetSubtitle.search_subtitles", f"Error Calculating Movie Size / Hash: {ex}.") + return [] + + log("GetSubtitle.search_subtitles", f"Movie Size: {movie_size}, Movie Hash: {movie_hash}.") + root = self.api_request( + func_name="searchSubtitlesByHash", + params=( + f"{movie_hash}" + f"{language_ids}" + "0" + "100" + ) + ) + subtitles = [] + for item in root.findall(".//return/item") or []: + filename = item.find("file_name").text + cod_subtitle_file = item.find("cod_subtitle_file").text + query_string = urlencode(dict( + cod_subtitle_file=cod_subtitle_file, + movie_hash=movie_hash + )) + subtitle = dict( + subID=cod_subtitle_file, + subDownloadLink=f"http://api.getsubtitle.com/?{query_string}", + subLang=item.find("desc_reduzido").text, + subName=filename, + subFormat=filename.split(".")[-1], + subRating="0" + ) + subtitles.append(subtitle) + log("GetSubtitle.search_subtitles", f"Subtitles Found: {json.dumps(subtitles)}.") + + if logout: + self.logout() + + return subtitles + + def download_subtitles(self, download_url, dest_path): + url = urlparse(download_url) + params = dict(parse_qsl(url.query)) + root = self.api_request( + func_name="downloadSubtitles", + params=( + '' + f'{params["movie_hash"]}' + f'{params["cod_subtitle_file"]}' + '' + ) + ) + res = root.find(".//return/item/data") + if res is not None: + with open(dest_path, "wb") as f: + f.write(zlib.decompress(b64decode(res.text))) f.flush() - gf.close() + log("GetSubtitle.download_subtitles", f"File {repr(download_url)} Download Successfully.") return True + log("GetSubtitle.download_subtitles", f"File {repr(download_url)} Download Failed.") return False diff --git a/resources/lib/utils.py b/resources/lib/utils.py index fb645db..8bc2662 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -9,7 +9,7 @@ def log(module, msg): - xbmc.log(f"### [{module}] - {msg}", level=xbmc.LOGDEBUG) + xbmc.log(f"### [BSPlayer::{module}] - {msg}", level=xbmc.LOGDEBUG) def notify(script_name, language, string_id): diff --git a/resources/settings.xml b/resources/settings.xml new file mode 100644 index 0000000..1dd19fb --- /dev/null +++ b/resources/settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/service.py b/service.py index c9d0408..af034c8 100644 --- a/service.py +++ b/service.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + import sys import shutil from os import path @@ -10,14 +11,14 @@ import xbmcaddon import xbmcplugin -from resources.lib.bsplayer import BSPlayer +from resources.lib.bsplayer import BSPlayer, OpenSubtitles, GetSubtitle from resources.lib.utils import log, notify, get_params, get_video_path, get_languages_dict __addon__ = xbmcaddon.Addon() -__author__ = __addon__.getAddonInfo('author') -__scriptid__ = __addon__.getAddonInfo('id') -__scriptname__ = __addon__.getAddonInfo('name') -__version__ = __addon__.getAddonInfo('version') +__author__ = __addon__.getAddonInfo("author") +__scriptid__ = __addon__.getAddonInfo("id") +__scriptname__ = __addon__.getAddonInfo("name") +__version__ = __addon__.getAddonInfo("version") __language__ = __addon__.getLocalizedString __cwd__ = xbmc.translatePath(__addon__.getAddonInfo("path")) @@ -25,54 +26,79 @@ __resource__ = xbmc.translatePath(path.join(__cwd__, "resources", "lib")) __temp__ = xbmc.translatePath(path.join(__profile__, "temp", "")) +engines = { + "BSPlayer": BSPlayer, + "OpenSubtitles": OpenSubtitles, + "GetSubtitle": GetSubtitle +} params = get_params() -log(f"BSPlayer.params", f"Current Action: {params['action']}.") -if params['action'] == 'search': +log(f"Service.params", f"Current Action: {params['action']}.") +if params["action"] == "search": video_path = get_video_path() - if video_path.startswith('http://') or video_path.startswith('https://'): + if video_path.startswith("http://") or video_path.startswith("https://"): notify(__scriptname__, __language__, 32001) - log("BSPlayer.get_video_path", "Streaming not supported.") + log("Service.get_video_path", "Streaming not supported.") + + log("Service.video_path", f"Current Video Path: {video_path}.") + languages = get_languages_dict(params["languages"]) + log("Service.languages", f"Current Languages: {languages}.") - log("BSPlayer.video_path", f"Current Video Path: {video_path}.") - languages = get_languages_dict(params['languages']) - log("BSPlayer.languages", f"Current Languages: {languages}.") + for engine_name, engine in engines.items(): + kwargs = {} + if engine_name == "OpenSubtitles": + username = __addon__.getSetting("OSuser") + password = __addon__.getSetting("OSpass") + kwargs = {"username": username, "password": password} + if not username or not password: + notify(__scriptname__, __language__, 32002) - with BSPlayer() as bsp: - subtitles = bsp.search_subtitles(video_path, language_ids=list(languages.keys())) - log("BSPlayer.subtitles", f"Subtitles found: {subtitles}.") - for subtitle in sorted(subtitles, key=lambda s: s['subLang']): - list_item = xbmcgui.ListItem( - label=languages[subtitle['subLang']], - label2=subtitle['subName'], - ) - list_item.setArt({ - "icon": f"{float(subtitle['subRating']) / 2}", - "thumb": xbmc.convertLanguage(subtitle["subLang"], xbmc.ISO_639_1) - }) + try: + with engine(**kwargs) as bsp: + subtitles = bsp.search_subtitles(video_path, language_ids=list(languages.keys())) + log("Service.subtitles", f"Subtitles found: {subtitles}.") + for subtitle in sorted(subtitles, key=lambda s: s['subLang']): + list_item = xbmcgui.ListItem( + label=engine_name, + label2=subtitle["subName"], + ) + list_item.setArt({ + "icon": f"{float(subtitle['subRating']) / 2}", + "thumb": xbmc.convertLanguage(subtitle["subLang"], xbmc.ISO_639_1) + }) - query = parse.urlencode(dict( - action='download', - link=subtitle['subDownloadLink'], - file_name=subtitle['subName'], - format=subtitle['subFormat'] - )) - plugin_url = f"plugin://{__scriptid__}/?{query}" - log("BSPlayer.plugin_url", f"Plugin Url Created: {plugin_url}.") - xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=plugin_url, listitem=list_item, isFolder=False) -elif params['action'] == 'manualsearch': + query = parse.urlencode(dict( + action="download", + engine=engine_name, + link=subtitle["subDownloadLink"], + file_name=subtitle["subName"], + format=subtitle["subFormat"] + )) + plugin_url = f"plugin://{__scriptid__}/?{query}" + log("Service.plugin_url", f"Plugin Url Created: {plugin_url}.") + xbmcplugin.addDirectoryItem( + handle=int(sys.argv[1]), + url=plugin_url, listitem=list_item, + isFolder=False + ) + except Exception as ex: + log("Service.search", f"{engine_name} Error: {ex}.") + +elif params["action"] == "manualsearch": notify(__scriptname__, __language__, 32002) - log("BSPlayer.manualsearch", "Manual search not supported.") -elif params['action'] == 'download': + log("Service.manualsearch", "Manual search not supported.") + +elif params["action"] == "download": if xbmcvfs.exists(__temp__): shutil.rmtree(__temp__) xbmcvfs.mkdirs(__temp__) - if params['format'] in ["srt", "sub", "txt", "smi", "ssa", "ass"]: - subtitle_path = path.join(__temp__, params['file_name']) - if BSPlayer.download_subtitles(params['link'], subtitle_path): - log("BSPlayer.download_subtitles", f"Subtitles Download Successfully From: {params['link']}") + if params["format"] in ["srt", "sub", "txt", "smi", "ssa", "ass"]: + subtitle_path = path.join(__temp__, params["file_name"]) + engine = engines[params["engine"]]() + if engine.download_subtitles(download_url=params["link"], dest_path=subtitle_path): + log("Service.download_subtitles", f"Subtitles Download Successfully From: {params['link']}") list_item = xbmcgui.ListItem(label=subtitle_path) - log("BSPlayer.download", f"Downloaded Subtitle Path: {subtitle_path}") + log("Service.download", f"Downloaded Subtitle Path: {subtitle_path}") xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=subtitle_path, listitem=list_item, isFolder=False) xbmcplugin.endOfDirectory(int(sys.argv[1]))