#!/bin/bash BASE=/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader # 1. Write fixed tools.py cat > "$BASE/xstream/resources/lib/tools.py" << 'HEREDOC' # tools.py - Fixed: all string inputs normalized via _e() import hashlib, re, os, pyaes, base64 from urllib.parse import quote, unquote, quote_plus, unquote_plus, urlparse from html.entities import name2codepoint from difflib import SequenceMatcher from functools import lru_cache def _e(s): if s is None: return '' if isinstance(s, bytes): return s.decode('utf-8') if not isinstance(s, str): return str(s) return s def platform(): return 'Linux' def changelog(): pass def textBox(heading, announce): pass def infoDialog(message, heading='', icon='', time=5000, sound=False): pass class cParser: @staticmethod def _get_compiled_pattern(pattern, flags=0): return re.compile(pattern, flags) @staticmethod def _replaceSpecialCharacters(s): try: for t in (('\\/', '/'), ('&', '&'), ('\\u00c4', 'A'), ('\\u00e4', 'a'), ('\\u00d6', 'O'), ('\\u00f6', 'o'), ('\\u00dc', 'U'), ('\\u00fc', 'u'), ('\\u00df', 's'), ('\\u2013', '-'), ('\\u00b2', '2'), ('\\u00b3', '3'), ('\\u00e9', 'e'), ('\\u2018', "'"), ('\\u201e', '"'), ('\\u201c', '"'), ('\\u00c9', 'E'), ('\\u2026', '...'), ('\\u202f', 'h'), ('\\u2019', "'"), ('\\u0308', ''), ('\\u00e8', 'e'), ('#038;', ''), ('\\u00f8', 'o'), ('/', '/'), ('\\u00e1', 'a'), ('–', '-'), ('“', '"'), ('„', '"'), ('’', "'"), ('…', '...'), ('\\u00bc', '1/4'), ('\\u00bd', '1/2'), ('\\u00be', '3/4'), ('\\u2153', '1/3'), ('\\u002A', '*')): s = s.replace(*t) for h in (('\\/', '/'), ('&', '&'), (''', "'"), ("'", "'"), ('Ä', 'A'), ('ä', 'a'), ('Ö', 'O'), ('ö', 'o'), ('Ü', 'U'), ('ü', 'u'), ('ß', 's'), ('²', '2'), ('¼', '1/4'), ('½', '1/2'), ('¾', '3/4'), ('⅓', '1/3'), ('∗', '*')): s = s.replace(*h) except: pass return s @staticmethod def parseSingleResult(sHtmlContent, pattern, ignoreCase=False): if sHtmlContent: flags = re.S | re.M if ignoreCase: flags |= re.I matches = cParser._get_compiled_pattern(pattern, flags).search(_e(sHtmlContent)) if matches: if matches.lastindex is not None and matches.lastindex >= 1: return True, cParser._replaceSpecialCharacters(matches.group(1)) return True, cParser._replaceSpecialCharacters(matches.group(0)) return False, None @staticmethod def parse(sHtmlContent, pattern, iMinFoundValue=1, ignoreCase=False): if sHtmlContent: flags = re.DOTALL if ignoreCase: flags |= re.I aMatches = cParser._get_compiled_pattern(pattern, flags).findall(_e(sHtmlContent)) if len(aMatches) >= iMinFoundValue: if isinstance(aMatches[0], tuple): aMatches = [tuple(cParser._replaceSpecialCharacters(x) if isinstance(x, str) and x is not None else '' for x in match) for match in aMatches] else: aMatches = [cParser._replaceSpecialCharacters(x) if isinstance(x, str) and x is not None else '' for x in aMatches] return True, aMatches return False, None @staticmethod def replace(pattern, sReplaceString, sValue): return cParser._get_compiled_pattern(pattern).sub(sReplaceString, _e(sValue)) @staticmethod def search(pattern, sValue, ignoreCase=True): flags = re.IGNORECASE if ignoreCase else 0 return cParser._get_compiled_pattern(pattern, flags).search(_e(sValue)) @staticmethod def escape(sValue): return re.escape(_e(sValue)) @staticmethod def getNumberFromString(sValue): aMatches = re.compile(r'\d+').findall(_e(sValue)) return int(aMatches[0]) if aMatches else 0 @staticmethod def urlparse(sUrl): return urlparse(_e(sUrl).replace('www.', '')).netloc.title() @staticmethod def urlDecode(sUrl): return unquote(_e(sUrl)) @staticmethod def urlEncode(sUrl, safe=''): return quote(_e(sUrl), safe) @staticmethod def quote(sUrl): return quote(_e(sUrl)) @staticmethod def unquotePlus(sUrl): return unquote_plus(_e(sUrl)) @staticmethod def quotePlus(sUrl): return quote_plus(_e(sUrl)) @staticmethod def B64decode(text): return base64.b64decode(_e(text)).decode('utf-8') class cUtil: @staticmethod def removeHtmlTags(sValue, sReplace=''): return re.compile(r'<.*?>').sub(sReplace, _e(sValue)) @staticmethod def unescape(text): def fixup(m): t = m.group(0) if not t.endswith(';'): t += ';' if t[:2] == '&#': try: if t[:3] == '&#x': return chr(int(t[3:-1], 16)) return chr(int(t[2:-1])) except ValueError: pass else: try: return chr(name2codepoint[t[1:-1]]) except KeyError: pass return t t = _e(text) try: t = t.decode('utf-8') except: pass return re.compile(r'&(\w+;|#x?\d+;?)').sub(fixup, t.strip()) @staticmethod def cleanse_text(text): return cUtil.removeHtmlTags(text if text else '') @staticmethod def evp_decode(cipher_text, passphrase, salt=None): ct = _e(cipher_text).encode('latin-1') if isinstance(cipher_text, str) else cipher_text pwd = _e(passphrase).encode('latin-1') if isinstance(passphrase, str) else passphrase if not salt: salt = ct[8:16]; ct = ct[16:] else: salt = _e(salt).encode('latin-1') if isinstance(salt, str) else salt key, iv = cUtil.evpKDF(pwd, salt) decrypter = pyaes.Decrypter(pyaes.AESModeOfOperationCBC(key, iv)) plain_text = decrypter.feed(ct) + decrypter.feed() return plain_text.decode("utf-8") @staticmethod def evpKDF(pwd, salt, key_size=32, iv_size=16): temp = b''; fd = b'' while len(fd) < key_size + iv_size: h = hashlib.md5(); h.update(temp + pwd + salt) temp = h.digest(); fd += temp return fd[0:key_size], fd[key_size:key_size + iv_size] @staticmethod def isSimilar(sSearch, sText, threshold=0.9): return SequenceMatcher(None, _e(sSearch), _e(sText)).ratio() >= threshold @staticmethod @lru_cache(maxsize=200000) def get_seq_match_ratio(token1, token2): return SequenceMatcher(None, _e(token1), _e(token2)).ratio() @staticmethod def isSimilarByToken(sSearch, sText, threshold=0.9): tokens_sSearch = _e(sSearch).split(); tokens_sText = _e(sText).split() if not tokens_sSearch: return False best_ratios = [max(cUtil.get_seq_match_ratio(token, token2) for token2 in tokens_sText) for token in tokens_sSearch] return (sum(best_ratios) / len(best_ratios)) >= threshold HEREDOC echo "tools.py written" # 2. Fix requestHandler.py python3 -c " with open('$BASE/xstream/resources/lib/handler/requestHandler.py') as f: c = f.read() old = ' self._sUrl = self.__cleanupUrl(sUrl)' new = ' if not isinstance(sUrl, str): sUrl = \"\"\n self._sUrl = self.__cleanupUrl(sUrl)' if old in c and 'not isinstance' not in c: c = c.replace(old, new) open('$BASE/xstream/resources/lib/handler/requestHandler.py','w').write(c) print('requestHandler fixed') else: print('requestHandler already fixed or pattern missing') " # 3. Kill and restart server fuser -k 8765/tcp 2>/dev/null sleep 1 cd "$BASE" python3 mini_app.py >> /tmp/server.log 2>&1 & sleep 4 # 4. Test echo "=== Tests ===" curl -s http://127.0.0.1:8765/api/sites | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Sites: {len(d)}')" 2>&1 curl -s http://127.0.0.1:8765/api/sites/hdfilme/entries | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Entries: {len(d)}')" 2>&1 tail -10 /tmp/server.log