183 lines
8.3 KiB
Python
183 lines
8.3 KiB
Python
|
|
#!/bin/bash
|
|||
|
|
BASE=/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader
|
|||
|
|
|
|||
|
|
# 1. Fix tools.py - replace with fixed version
|
|||
|
|
cat > "$BASE/xstream/resources/lib/tools.py" << 'ENDOFFILE'
|
|||
|
|
# 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', 'Ä'), ('\\u00e4', 'ä'),
|
|||
|
|
('\\u00d6', 'Ö'), ('\\u00f6', 'ö'), ('\\u00dc', 'Ü'), ('\\u00fc', 'ü'),
|
|||
|
|
('\\u00df', 'ß'), ('\\u2013', '-'), ('\\u00b2', '²'), ('\\u00b3', '³'),
|
|||
|
|
('\\u00e9', 'é'), ('\\u2018', "'"), ('\\u201e', '"'), ('\\u201c', '"'),
|
|||
|
|
('\\u00c9', 'É'), ('\\u2026', '...'), ('\\u202f', 'h'), ('\\u2019', "'"),
|
|||
|
|
('\\u0308', '̈'), ('\\u00e8', 'è'), ('#038;', ''), ('\\u00f8', 'ø'),
|
|||
|
|
('/', '/'), ('\\u00e1', 'á'), ('–', '-'), ('“', '"'), ('„', '"'),
|
|||
|
|
('’', "'"), ('…', '…'), ('\\u00bc', '¼'), ('\\u00bd', '½'), ('\\u00be', '¾'),
|
|||
|
|
('\\u2153', '⅓'), ('\\u002A', '*')):
|
|||
|
|
s = s.replace(*t)
|
|||
|
|
for h in (('\\/', '/'), ('&', '&'), (''', "'"), ("'", "'"),
|
|||
|
|
('Ä', 'Ä'), ('ä', 'ä'), ('Ö', 'Ö'), ('ö', 'ö'),
|
|||
|
|
('Ü', 'Ü'), ('ü', 'ü'), ('ß', 'ß'), ('²', '²'),
|
|||
|
|
('Ü', '³'), ('¼', '¼'), ('½', '½'), ('¾', '¾'),
|
|||
|
|
('⅓', '⅓'), ('∗', '*')):
|
|||
|
|
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
|
|||
|
|
ENDOFFILE
|
|||
|
|
echo "tools.py written"
|
|||
|
|
|
|||
|
|
# 2. Fix requestHandler.py
|
|||
|
|
python3 << 'PYEOF'
|
|||
|
|
with open('/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader/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)
|
|||
|
|
with open('/home/matthiasberner/Schreibtisch/kodi/xstreamDownloader/xstream/resources/lib/handler/requestHandler.py', 'w') as f:
|
|||
|
|
f.write(c)
|
|||
|
|
print('requestHandler fixed')
|
|||
|
|
else:
|
|||
|
|
print('requestHandler already fixed or pattern missing')
|
|||
|
|
PYEOF
|
|||
|
|
|
|||
|
|
# 3. Kill and restart server
|
|||
|
|
fuser -k 8765/tcp 2>/dev/null
|
|||
|
|
sleep 1
|
|||
|
|
cd /home/matthiasberner/Schreibtisch/kodi/xstreamDownloader
|
|||
|
|
python3 mini_app.py >> /tmp/server.log 2>&1 &
|
|||
|
|
sleep 3
|
|||
|
|
echo "=== Test ==="
|
|||
|
|
curl -s http://127.0.0.1:8765/api/sites | python3 -c "import sys,json; d=json.load(sys.stdin); print(f'Sites OK: {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 -8 /tmp/server.log
|