Skip to content

Instantly share code, notes, and snippets.

@CarstenG2
Created February 14, 2026 14:24
Show Gist options
  • Select an option

  • Save CarstenG2/33be0a4a8be3d04d7b8b7eb70b3a48fb to your computer and use it in GitHub Desktop.

Select an option

Save CarstenG2/33be0a4a8be3d04d7b8b7eb70b3a48fb to your computer and use it in GitHub Desktop.
xShip Medien-Info: audio language, multi-track, countdown, optimized MP4 probing (Issue #48)
# 2023-05-10
# edit 2025-06-12
import sys, json
from urllib.parse import parse_qs, urlsplit
from resources.lib import control
params = dict(control.parse_qsl(control.urlsplit(sys.argv[2]).query))
action = params.get('action')
name = params.get('name')
table = params.get('table')
title = params.get('title')
source = params.get('source')
# ------ navigator --------------
if action == None or action == 'root':
from resources.lib.indexers import navigator
navigator.navigator().root()
elif action == 'pluginInfo':
from resources.lib import supportinfo
supportinfo.pluginInfo()
elif action == 'movieNavigator':
from resources.lib.indexers import navigator
navigator.navigator().movies()
elif action == 'tvNavigator':
from resources.lib.indexers import navigator
navigator.navigator().tvshows()
elif action == 'toolNavigator':
from resources.lib.indexers import navigator
navigator.navigator().tools()
elif action == 'downloadNavigator':
from resources.lib.indexers import navigator
navigator.navigator().downloads()
# -------------------------------------------
elif action == 'download':
image = params.get('image')
from resources.lib import downloader
from resources.lib import sources
try: downloader.download(name, image, sources.sources().sourcesResolve(json.loads(source)[0], True))
except: pass
elif action in ('sendToJD', 'sendToJD2', 'sendToMyJD', 'sendToPyLoad'):
raw_url = json.loads(source)[0]['url']
if raw_url:
# Extract best URL for download managers:
# If URL has Kodi-style |headers with a Referer that has a path,
# use the Referer (it's the hoster page URL that JD can resolve).
# Otherwise use the bare URL without |headers.
url = raw_url
if '|' in raw_url:
base_url, header_str = raw_url.split('|', 1)
headers = dict(parse_qs(header_str, keep_blank_values=True))
referer = headers.get('Referer', [''])[0]
if referer and urlsplit(referer).path not in ('', '/'):
url = referer
else:
url = base_url
if action == 'sendToJD':
from resources.lib.handler.jdownloaderHandler import cJDownloaderHandler
cJDownloaderHandler().sendToJDownloader(url)
elif action == 'sendToJD2':
from resources.lib.handler.jdownloader2Handler import cJDownloader2Handler
cJDownloader2Handler().sendToJDownloader2(url)
elif action == 'sendToMyJD':
from resources.lib.handler.myjdownloaderHandler import cMyJDownloaderHandler
cMyJDownloaderHandler().sendToMyJDownloader(url, name)
elif action == 'sendToPyLoad':
from resources.lib.handler.pyLoadHandler import cPyLoadHandler
cPyLoadHandler().sendToPyLoad(name, url)
elif action == 'mediaInfo':
import xbmcgui
dialog = xbmcgui.DialogProgress()
dialog.create('Medien-Info', 'Löse Stream-URL auf... (20 Sek.)')
dialog.update(0)
from resources.lib import sources
sources.sources().mediaInfo(source, dialog)
elif action == 'playExtern':
import json
if not control.visible(): control.busy()
try:
sysmeta = {}
for key, value in params.items():
if key == 'action': continue
elif key == 'year' or key == 'season' or key == 'episode': value = int(value)
if value == 0: continue
sysmeta.update({key : value})
if int(params.get('season')) == 0:
mediatype = 'movie'
else:
mediatype = 'tvshow'
sysmeta.update({'mediatype': mediatype})
# if control.getSetting('hosts.mode') == '2':
# sysmeta.update({'select': '2'})
# else:
# sysmeta.update({'select': '1'})
sysmeta.update({'select': control.getSetting('hosts.mode')})
sysmeta = json.dumps(sysmeta)
params.update({'sysmeta': sysmeta})
from resources.lib import sources
sources.sources().play(params)
except:
pass
elif action == 'playURL':
try:
import resolveurl
import xbmcgui, xbmc
#url = 'https://streamvid.net/embed-uhgo683xes41'
#url = 'https://moflix-stream.click/v/gcd0aueegeia'
url = xbmcgui.Dialog().input("URL Input")
hmf = resolveurl.HostedMediaFile(url=url, include_disabled=True, include_universal=False)
try:
if hmf.valid_url(): url = hmf.resolve()
except:
pass
item = xbmcgui.ListItem('URL-direkt')
kodiver = int(xbmc.getInfoLabel("System.BuildVersion").split(".")[0])
if ".m3u8" in url or '.mpd' in url:
item.setProperty("inputstream", "inputstream.adaptive")
if '.mpd' in url:
if kodiver < 21: item.setProperty('inputstream.adaptive.manifest_type', 'mpd')
item.setMimeType('application/dash+xml')
else:
if kodiver < 21: item.setProperty('inputstream.adaptive.manifest_type', 'hls')
item.setMimeType("application/vnd.apple.mpegurl")
item.setContentLookup(False)
if '|' in url:
stream_url, strhdr = url.split('|')
item.setProperty('inputstream.adaptive.stream_headers', strhdr)
if kodiver > 19: item.setProperty('inputstream.adaptive.manifest_headers', strhdr)
# item.setPath(stream_url)
url = stream_url
item.setPath(url)
xbmc.Player().play(url, item)
except:
#print('Kein Video Link gefunden')
control.infoDialog("Keinen Video Link gefunden", sound=True, icon='WARNING', time=1000)
elif action == 'UpdatePlayCount':
from resources.lib import playcountDB
playcountDB.UpdatePlaycount(params)
control.execute('Container.Refresh')
# listings -------------------------------
elif action == 'listings':
from resources.lib.indexers import listings
listings.listings().get(params)
elif action == 'movieYears':
from resources.lib.indexers import listings
listings.listings().movieYears()
elif action == 'movieGenres':
from resources.lib.indexers import listings
listings.listings().movieGenres()
elif action == 'tvGenres':
from resources.lib.indexers import listings
listings.listings().tvGenres()
# search ----------------------
elif action == 'searchNew':
from resources.lib import searchDB
searchDB.search_new(table)
elif action == 'searchClear':
from resources.lib import searchDB
searchDB.remove_all_query(table)
# if len(searchDB.getSearchTerms()) == 0:
# control.execute('Action(ParentDir)')
elif action == 'searchDelTerm':
from resources.lib import searchDB
searchDB.remove_query(name, table)
# if len(searchDB.getSearchTerms()) == 0:
# control.execute('Action(ParentDir)')
# person ----------------------
elif action == 'person':
from resources.lib.indexers import person
person.person().get(params)
elif action == 'personSearch':
from resources.lib.indexers import person
person.person().search()
elif action == 'personCredits':
from resources.lib.indexers import person
person.person().getCredits(params)
elif action == 'playfromPerson':
if not control.visible(): control.busy()
sysmeta = json.loads(params['sysmeta'])
if sysmeta['mediatype'] == 'movie':
from resources.lib.indexers import movies
sysmeta = movies.movies().super_meta(sysmeta['tmdb_id'])
sysmeta = json.dumps(sysmeta)
else:
from resources.lib.indexers import tvshows
sysmeta = tvshows.tvshows().super_meta(sysmeta['tmdb_id'])
sysmeta = control.quote_plus(json.dumps(sysmeta))
params.update({'sysmeta': sysmeta})
from resources.lib import sources
sources.sources().play(params)
# movies ----------------------
elif action == 'movies':
from resources.lib.indexers import movies
movies.movies().get(params)
elif action == 'moviesSearch':
from resources.lib.indexers import movies
movies.movies().search()
# tvshows ---------------------------------
elif action == 'tvshows': # 'tvshowPage'
from resources.lib.indexers import tvshows
tvshows.tvshows().get(params)
elif action == 'tvshowsSearch':
from resources.lib.indexers import tvshows
tvshows.tvshows().search()
# seasons ---------------------------------
elif action == 'seasons':
from resources.lib.indexers import seasons
seasons.seasons().get(params) # params
# episodes ---------------------------------
elif action == 'episodes':
from resources.lib.indexers import episodes
episodes.episodes().get(params)
# sources ---------------------------------
elif action == 'play':
if not control.visible(): control.busy()
from resources.lib import sources
sources.sources().play(params)
elif action == 'addItem':
from resources.lib import sources
sources.sources().addItem(title)
elif action == 'playItem':
if not control.visible(): control.busy()
from resources.lib import sources
sources.sources().playItem(title, source)
# Settings ------------------------------
elif action == "settings": # alle Quellen aktivieren / deaktivieren
from resources import settings
settings.run(params)
elif action == 'addonSettings':
# query = None
query = params.get('query')
control.openSettings(query)
elif action == 'resetSettings':
status = control.resetSettings()
if status:
control.reload_profile()
control.sleep(500)
control.execute('RunAddon("%s")' % control.addonId)
elif action == 'resolverSettings':
import resolveurl as resolver
resolver.display_settings()
# try:
# import pydevd
# if pydevd.connected: pydevd.kill_all_pydev_threads()
# except:
# pass
# finally:
# exit()
# Media info probing for xShip streams
# Detects stream type (HLS/DASH/MP4) and extracts resolution, codec, FPS, audio, bitrate, duration
import re
import time
import threading
from resources.lib import log_utils, control
TOTAL_TIMEOUT = 20 # seconds budget for entire probe
def _fetchWithDeadline(dialog, pct, msg, func, deadline):
"""Run func() in a background thread, showing countdown to deadline on dialog."""
result = [None]
error = [None]
def run():
try: result[0] = func()
except Exception as e: error[0] = e
t = threading.Thread(target=run)
t.start()
while t.is_alive():
remaining = int(deadline - time.time())
if remaining <= 0:
break
dialog.update(pct, '%s (%d Sek.)' % (msg, remaining))
if dialog.iscanceled():
return None
t.join(timeout=1)
t.join(timeout=0.5)
if error[0]:
raise error[0]
return result[0]
def _remaining(deadline):
"""Seconds left until deadline, minimum 2."""
return max(2, deadline - time.time())
def getMediaInfo(url, dialog, deadline=None):
"""Detect stream type and probe media info. Returns formatted info string or None."""
import requests
if not deadline:
deadline = time.time() + TOTAL_TIMEOUT
stream_url = url.split('|')[0]
url_lower = stream_url.lower()
# 1. Check URL extension first (fast path)
if '.m3u8' in url_lower:
return _probeHLS(url, dialog, deadline)
if '.mpd' in url_lower:
return _probeDASH(url, dialog, deadline)
# 2. Fetch a small chunk to detect by Content-Type + content sniffing
headers = _parseHeaders(url)
try:
r = _fetchWithDeadline(dialog, 55, 'Erkenne Stream-Typ...',
lambda: requests.get(stream_url, headers=headers, timeout=_remaining(deadline), verify=False, stream=True),
deadline)
if r is None:
return None
if r.status_code >= 400:
return 'Stream nicht erreichbar (HTTP %d)' % r.status_code
ct = r.headers.get('Content-Type', '').lower()
content_length = r.headers.get('Content-Length', '')
# Read first 8KB for sniffing
peek = next(r.iter_content(chunk_size=8192), b'')
r.close()
# Detect HLS by Content-Type or content
if 'mpegurl' in ct or 'apple' in ct or peek.lstrip().startswith(b'#EXTM3U'):
return _probeHLS(url, dialog, deadline)
# Detect DASH by Content-Type or content
if 'dash' in ct or (peek.lstrip().startswith(b'<?xml') and b'<MPD' in peek):
return _probeDASH(url, dialog, deadline)
# Otherwise: direct file (MP4, MKV, etc.)
return _probeDirect(url, dialog, deadline, content_length)
except Exception as e:
log_utils.log('getMediaInfo Error: %s' % str(e), log_utils.LOGERROR)
return 'Stream-Typ konnte nicht erkannt werden'
def _probeHLS(url, dialog=None, deadline=None):
import requests
if not deadline:
deadline = time.time() + TOTAL_TIMEOUT
try:
stream_url = url.split('|')[0]
headers = _parseHeaders(url)
r = requests.get(stream_url, headers=headers, timeout=_remaining(deadline), verify=False)
content = r.text
if '#EXT-X-STREAM-INF' not in content:
return 'HLS Stream (Single-Bitrate)\n\nKeine Auflösungsinfo im Manifest verfügbar.'
lines = content.strip().split('\n')
variants = []
for line in lines:
if line.startswith('#EXT-X-STREAM-INF'):
res_match = re.search(r'RESOLUTION=(\d+)x(\d+)', line)
bw_match = re.search(r'BANDWIDTH=(\d+)', line)
codec_match = re.search(r'CODECS="([^"]+)"', line)
width = int(res_match.group(1)) if res_match else 0
height = int(res_match.group(2)) if res_match else 0
bandwidth = int(bw_match.group(1)) if bw_match else 0
codecs = codec_match.group(1) if codec_match else ''
variants.append((width, height, bandwidth, codecs))
if not variants:
return 'HLS Stream\n\nKeine Auflösungsinfo gefunden.'
variants.sort(key=lambda x: x[1], reverse=True)
best = variants[0]
result = 'Typ: HLS Stream\n'
result += 'Auflösung: %dx%d (%s)\n' % (best[0], best[1], _resLabel(best[1]))
if best[3]:
vc = [c.strip() for c in best[3].split(',') if c.strip()[:3].lower() in ('avc', 'hev', 'hvc', 'av0', 'vp0', 'vp8', 'vp9', 'mp4')]
ac = [c.strip() for c in best[3].split(',') if c.strip() not in vc]
# mp4a is audio, not video
vc_final = [c for c in vc if not c.strip().lower().startswith('mp4a')]
ac_final = ac + [c for c in vc if c.strip().lower().startswith('mp4a')]
if vc_final: result += 'Video-Codec: %s\n' % _codecName(','.join(vc_final))
# Parse #EXT-X-MEDIA:TYPE=AUDIO lines for language info
audio_tracks = []
for line in lines:
if line.startswith('#EXT-X-MEDIA') and 'TYPE=AUDIO' in line:
lang_m = re.search(r'LANGUAGE="([^"]*)"', line)
lang = lang_m.group(1) if lang_m else ''
audio_tracks.append(lang)
if audio_tracks:
codec_str = _codecName(','.join(ac_final)) if ac_final else ''
seen_langs = set()
for lang in audio_tracks:
lang_key = lang.lower()
if lang_key in seen_langs:
continue
seen_langs.add(lang_key)
lang_str = _langName(lang)
if lang_str and codec_str:
result += 'Audio: %s — %s\n' % (codec_str, lang_str)
elif codec_str:
result += 'Audio: %s\n' % codec_str
elif lang_str:
result += 'Audio: %s\n' % lang_str
elif ac_final:
result += 'Audio: %s\n' % _codecName(','.join(ac_final))
if best[2]: result += 'Bitrate: %s\n' % _fmtBitrate(best[2])
return result.rstrip()
except Exception as e:
log_utils.log('_probeHLS Error: %s' % str(e), log_utils.LOGERROR)
return None
def _probeDASH(url, dialog=None, deadline=None):
import requests
import xml.etree.ElementTree as ET
if not deadline:
deadline = time.time() + TOTAL_TIMEOUT
try:
stream_url = url.split('|')[0]
headers = _parseHeaders(url)
r = requests.get(stream_url, headers=headers, timeout=_remaining(deadline), verify=False)
root = ET.fromstring(r.content)
# Handle XML namespace
ns = ''
ns_match = re.match(r'\{(.+?)\}', root.tag)
if ns_match:
ns = '{%s}' % ns_match.group(1)
variants = []
for rep in root.iter('%sRepresentation' % ns):
width = rep.get('width')
height = rep.get('height')
bandwidth = rep.get('bandwidth')
codecs = rep.get('codecs', '')
mime = rep.get('mimeType', '')
# Also check parent AdaptationSet
parent = None
for adapt in root.iter('%sAdaptationSet' % ns):
if rep in list(adapt):
parent = adapt
break
if not mime and parent is not None:
mime = parent.get('mimeType', '')
if not codecs and parent is not None:
codecs = parent.get('codecs', '')
if width and height:
variants.append((int(width), int(height), int(bandwidth) if bandwidth else 0, codecs, mime))
if not variants:
return 'DASH Stream\n\nKeine Auflösungsinfo im Manifest verfügbar.'
# Deduplicate and sort by height descending
seen = set()
unique = []
for v in variants:
key = (v[0], v[1], v[2])
if key not in seen:
seen.add(key)
unique.append(v)
unique.sort(key=lambda x: (x[1], x[2]), reverse=True)
best = unique[0]
result = 'Typ: DASH Stream\n'
result += 'Auflösung: %dx%d (%s)\n' % (best[0], best[1], _resLabel(best[1]))
if best[3]: result += 'Video-Codec: %s\n' % _codecName(best[3])
if best[2]: result += 'Bitrate: %s\n' % _fmtBitrate(best[2])
# Audio info from audio AdaptationSets
for adapt in root.iter('%sAdaptationSet' % ns):
mime = adapt.get('mimeType', '')
if 'audio' not in mime:
continue
lang = adapt.get('lang', '')
lang_str = _langName(lang)
for rep in adapt.iter('%sRepresentation' % ns):
ac = rep.get('codecs', '') or adapt.get('codecs', '')
if ac:
if lang_str:
result += 'Audio: %s — %s\n' % (_codecName(ac), lang_str)
else:
result += 'Audio: %s\n' % _codecName(ac)
break
return result.rstrip()
except Exception as e:
log_utils.log('_probeDASH Error: %s' % str(e), log_utils.LOGERROR)
return None
def _fetchRange(url, headers, start, end, dialog, deadline, pct, msg):
"""Fetch a byte range, showing progress. Returns bytes or None."""
import requests
h = dict(headers)
h['Range'] = 'bytes=%d-%d' % (start, end)
try:
r = _fetchWithDeadline(dialog, pct, msg,
lambda: requests.get(url, headers=h, timeout=_remaining(deadline), verify=False, stream=True),
deadline)
if r is None or r.status_code >= 400:
return None
except:
return None
data = b''
want = end - start + 1
for chunk in r.iter_content(chunk_size=65536):
data += chunk
remaining = int(deadline - time.time())
dialog.update(pct, '%s %.0f KB (%d Sek.)' % (msg, len(data) / 1024.0, max(remaining, 0)))
if len(data) >= want:
break
if dialog.iscanceled() or remaining <= 0:
break
r.close()
return data
def _findMoov(data):
"""Scan top-level MP4 boxes to find moov offset and size.
Returns (offset, size) or (None, None)."""
import struct
p = 0
while p < len(data) - 8:
try:
box_size = struct.unpack('>I', data[p:p+4])[0]
box_type = data[p+4:p+8]
except:
break
if box_size < 8:
break
if box_type == b'moov':
return p, box_size
if box_type == b'mdat':
return None, None # moov is after mdat (at end of file)
p += box_size
return None, None
def _probeDirect(url, dialog, deadline, file_size_str=''):
import requests, struct
try:
stream_url = url.split('|')[0]
headers = _parseHeaders(url)
# Step 1: fetch first 64 KB to scan box headers
INITIAL = 65536
data = _fetchRange(stream_url, headers, 0, INITIAL - 1, dialog, deadline, 65, 'Lade Datei-Header...')
if data is None:
return 'Typ: Direkter Stream\n\nServer nicht erreichbar'
# Extract file size from response (need a quick HEAD-like check)
content_type = ''
total_size = 0
try:
h_check = dict(headers)
h_check['Range'] = 'bytes=0-0'
r_check = requests.head(stream_url, headers=headers, timeout=_remaining(deadline), verify=False)
content_type = r_check.headers.get('Content-Type', '')
cr = r_check.headers.get('Content-Range', '')
if '/' in cr:
try: total_size = int(cr.split('/')[-1])
except: pass
if not total_size:
try: total_size = int(r_check.headers.get('Content-Length', '0'))
except: pass
except:
pass
if not total_size and file_size_str:
try: total_size = int(file_size_str)
except: pass
# Step 2: scan top-level boxes to locate moov
moov_off, moov_size = _findMoov(data)
if moov_off is not None:
# moov found at start — fetch more if we don't have it all
moov_end = moov_off + moov_size
if moov_end > len(data):
dialog.update(70, 'Lade moov-Box... (%d KB)' % (moov_size // 1024))
extra = _fetchRange(stream_url, headers, len(data), moov_end - 1,
dialog, deadline, 70, 'Lade moov-Box...')
if extra:
data = data + extra
elif total_size > INITIAL:
# moov not at start (mdat first) — try from end
tail_size = min(total_size, 262144) # start with 256 KB from end
tail_start = total_size - tail_size
tail = _fetchRange(stream_url, headers, tail_start, total_size - 1,
dialog, deadline, 75, 'Lade Datei-Ende...')
if tail:
moov_off, moov_size = _findMoov(tail)
if moov_off is not None:
moov_end = moov_off + moov_size
if moov_end > len(tail):
# moov larger than our tail chunk — fetch the full moov
abs_moov_start = tail_start + moov_off
extra = _fetchRange(stream_url, headers, abs_moov_start, abs_moov_start + moov_size - 1,
dialog, deadline, 80, 'Lade moov-Box...')
if extra:
data = extra
else:
data = tail
else:
data = tail
else:
data = tail
dialog.update(85, 'Analysiere Video-Header...')
width, height, codec, duration_sec, fps, audio_traks = _parseMp4(data)
result = 'Typ: Direkter Stream\n'
if width and height:
label = _resLabel(height)
result += 'Auflösung: %dx%d (%s)\n' % (width, height, label)
else:
result += 'Auflösung: nicht aus Datei-Header ermittelbar\n'
if codec:
result += 'Video-Codec: %s\n' % _codecName(codec)
if fps:
if fps == int(fps):
result += 'FPS: %d\n' % int(fps)
else:
fps_str = '%.3f' % fps
result += 'FPS: %s\n' % fps_str.rstrip('0').rstrip('.')
for at in audio_traks:
parts = [_codecName(at.get('audio_codec', ''))]
if at.get('audio_channels'):
parts.append(_channelLabel(at['audio_channels']))
if at.get('audio_samplerate'):
parts.append('%.1f kHz' % (at['audio_samplerate'] / 1000.0))
lang_str = _langName(at.get('lang', ''))
if lang_str:
result += 'Audio: %s — %s\n' % (', '.join(parts), lang_str)
else:
result += 'Audio: %s\n' % ', '.join(parts)
if total_size > 0 and duration_sec and duration_sec > 0:
bitrate = int(total_size * 8 / duration_sec)
result += 'Bitrate: %s (Durchschnitt)\n' % _fmtBitrate(bitrate)
if duration_sec and duration_sec > 0:
hours = int(duration_sec) // 3600
mins = (int(duration_sec) % 3600) // 60
secs = int(duration_sec) % 60
if hours > 0:
result += 'Dauer: %d:%02d:%02d\n' % (hours, mins, secs)
else:
result += 'Dauer: %d:%02d\n' % (mins, secs)
if content_type and 'octet' not in content_type:
result += 'Dateityp: %s\n' % content_type
if total_size > 0:
size_mb = total_size / (1024.0 * 1024.0)
if size_mb >= 1024:
result += 'Dateigröße: %.1f GB\n' % (size_mb / 1024.0)
else:
result += 'Dateigröße: %.0f MB\n' % size_mb
return result.rstrip()
except Exception as e:
log_utils.log('_probeDirect Error: %s' % str(e), log_utils.LOGERROR)
return None
def _parseMp4(data):
"""Parse MP4 box structure to extract video/audio info.
Returns (width, height, codec, duration_sec, fps, audio_traks)."""
import struct
if len(data) < 8:
return None, None, None, None, None, []
width = height = None
codec = None
duration_sec = None
fps = None
current_trak = {}
video_trak = None
audio_traks = []
def read_boxes(data, start, end, depth=0):
nonlocal duration_sec, current_trak, video_trak, audio_traks
if depth > 10:
return
p = start
while p < end - 8:
try:
box_size = struct.unpack('>I', data[p:p+4])[0]
box_type = data[p+4:p+8]
except:
break
if box_size < 8:
break
if p + box_size > end:
box_size = end - p
if box_type == b'moov':
read_boxes(data, p + 8, p + box_size, depth + 1)
elif box_type == b'trak':
current_trak = {}
read_boxes(data, p + 8, p + box_size, depth + 1)
if 'codec' in current_trak and not video_trak:
video_trak = current_trak
elif 'audio_codec' in current_trak:
audio_traks.append(current_trak)
elif box_type in (b'mdia', b'minf', b'stbl'):
read_boxes(data, p + 8, p + box_size, depth + 1)
# mvhd — movie header with duration
elif box_type == b'mvhd':
version = data[p + 8] if p + 9 <= end else 0
if version == 0 and p + 28 <= end:
ts = struct.unpack('>I', data[p+20:p+24])[0]
dur = struct.unpack('>I', data[p+24:p+28])[0]
if ts > 0 and dur > 0:
duration_sec = float(dur) / ts
elif version == 1 and p + 40 <= end:
ts = struct.unpack('>I', data[p+28:p+32])[0]
dur = struct.unpack('>Q', data[p+32:p+40])[0]
if ts > 0 and dur > 0:
duration_sec = float(dur) / ts
# tkhd — track header with display dimensions (fallback)
elif box_type == b'tkhd' and box_size >= 84:
version = data[p + 8] if p + 9 <= end else 0
if version == 0 and p + 92 <= end:
w_raw = struct.unpack('>I', data[p+84:p+88])[0]
h_raw = struct.unpack('>I', data[p+88:p+92])[0]
w, h = w_raw >> 16, h_raw >> 16
if 120 <= w <= 7680 and 90 <= h <= 4320:
current_trak['tkhd_w'] = w
current_trak['tkhd_h'] = h
elif version == 1 and p + 104 <= end:
w_raw = struct.unpack('>I', data[p+96:p+100])[0]
h_raw = struct.unpack('>I', data[p+100:p+104])[0]
w, h = w_raw >> 16, h_raw >> 16
if 120 <= w <= 7680 and 90 <= h <= 4320:
current_trak['tkhd_w'] = w
current_trak['tkhd_h'] = h
# mdhd — media header with track timescale (for FPS) + language
elif box_type == b'mdhd':
version = data[p + 8] if p + 9 <= end else 0
if version == 0 and p + 24 <= end:
ts = struct.unpack('>I', data[p+20:p+24])[0]
if ts > 0:
current_trak['mdhd_ts'] = ts
if p + 30 <= end:
lang = struct.unpack('>H', data[p+28:p+30])[0]
lang_str = chr(((lang >> 10) & 0x1F) + 0x60) + chr(((lang >> 5) & 0x1F) + 0x60) + chr((lang & 0x1F) + 0x60)
current_trak['lang'] = lang_str
elif version == 1 and p + 32 <= end:
ts = struct.unpack('>I', data[p+28:p+32])[0]
if ts > 0:
current_trak['mdhd_ts'] = ts
if p + 42 <= end:
lang = struct.unpack('>H', data[p+40:p+42])[0]
lang_str = chr(((lang >> 10) & 0x1F) + 0x60) + chr(((lang >> 5) & 0x1F) + 0x60) + chr((lang & 0x1F) + 0x60)
current_trak['lang'] = lang_str
# stsd — sample description: codec + resolution/audio info
elif box_type == b'stsd' and box_size > 24:
if p + 24 <= end:
codec_tag = data[p+20:p+24]
is_video = False
if codec_tag in (b'avc1', b'avc3'):
current_trak['codec'] = 'avc1'
is_video = True
elif codec_tag in (b'hev1', b'hvc1'):
current_trak['codec'] = 'hev1'
is_video = True
elif codec_tag in (b'av01',):
current_trak['codec'] = 'av01'
is_video = True
elif codec_tag in (b'vp09',):
current_trak['codec'] = 'vp09'
is_video = True
elif codec_tag == b'mp4v':
current_trak['codec'] = 'mp4v'
is_video = True
# Video sample entry: width(uint16) at p+48, height at p+50
if is_video and p + 52 <= end:
coded_w = struct.unpack('>H', data[p+48:p+50])[0]
coded_h = struct.unpack('>H', data[p+50:p+52])[0]
if 16 <= coded_w <= 7680 and 16 <= coded_h <= 4320:
current_trak['stsd_w'] = coded_w
current_trak['stsd_h'] = coded_h
# Audio sample entry: channels at p+40, samplerate at p+48
if not is_video:
audio_tags = {b'mp4a': 'mp4a', b'ac-3': 'ac-3', b'ec-3': 'ec-3',
b'dtsh': 'dtsh', b'dtsl': 'dtsl', b'Opus': 'opus',
b'opus': 'opus', b'fLaC': 'flac'}
if codec_tag in audio_tags:
current_trak['audio_codec'] = audio_tags[codec_tag]
if p + 52 <= end:
ch = struct.unpack('>H', data[p+40:p+42])[0]
sr = struct.unpack('>I', data[p+48:p+52])[0] >> 16
if 1 <= ch <= 16:
current_trak['audio_channels'] = ch
if 8000 <= sr <= 192000:
current_trak['audio_samplerate'] = sr
# stts — sample-to-time: first delta gives frame duration
elif box_type == b'stts' and p + 24 <= end:
entry_count = struct.unpack('>I', data[p+12:p+16])[0]
if entry_count >= 1:
delta = struct.unpack('>I', data[p+20:p+24])[0]
if delta > 0:
current_trak['stts_delta'] = delta
p += box_size
try:
read_boxes(data, 0, len(data))
except:
pass
if video_trak:
codec = video_trak.get('codec')
if 'stsd_w' in video_trak:
width = video_trak['stsd_w']
height = video_trak['stsd_h']
elif 'tkhd_w' in video_trak:
width = video_trak['tkhd_w']
height = video_trak['tkhd_h']
mdhd_ts = video_trak.get('mdhd_ts')
stts_delta = video_trak.get('stts_delta')
if mdhd_ts and stts_delta:
fps = round(float(mdhd_ts) / stts_delta, 3)
return width, height, codec, duration_sec, fps, audio_traks
# --- Helper functions ---
def _resLabel(h):
if h >= 2160: return '4K UHD'
if h >= 1440: return 'QHD'
if h >= 1080: return 'Full HD'
if h >= 720: return 'HD'
if h >= 480: return 'SD'
return '%dp' % h
def _channelLabel(ch):
if ch == 1: return 'Mono'
if ch == 2: return 'Stereo'
if ch == 6: return '5.1'
if ch == 8: return '7.1'
return '%d Kanäle' % ch
def _codecName(raw):
if not raw: return ''
parts = [c.strip() for c in raw.split(',')]
names = []
for p in parts:
pl = p.lower()
if pl.startswith('avc') or pl.startswith('h264') or pl == 'h.264':
names.append('H.264')
elif pl.startswith('hev') or pl.startswith('hvc') or pl.startswith('h265') or pl == 'h.265' or pl == 'hevc':
names.append('H.265')
elif pl.startswith('av01') or pl == 'av1':
names.append('AV1')
elif pl.startswith('vp9') or pl.startswith('vp09'):
names.append('VP9')
elif pl.startswith('mp4a.6b') or pl == 'mp3':
names.append('MP3')
elif pl.startswith('mp4a') or pl == 'aac':
names.append('AAC')
elif pl.startswith('ec-3') or pl.startswith('eac') or pl == 'e-ac-3':
names.append('Dolby Digital+')
elif pl.startswith('ac-3') or pl.startswith('ac3'):
names.append('Dolby Digital')
elif pl.startswith('dts'):
names.append('DTS')
elif pl.startswith('opus'):
names.append('Opus')
elif pl.startswith('flac'):
names.append('FLAC')
else:
names.append(p)
seen = set()
unique = []
for n in names:
if n not in seen:
seen.add(n)
unique.append(n)
return ' + '.join(unique)
def _langName(code):
if not code: return ''
names = {
'de': 'Deutsch', 'deu': 'Deutsch', 'ger': 'Deutsch',
'en': 'Englisch', 'eng': 'Englisch',
'fr': 'Französisch', 'fra': 'Französisch', 'fre': 'Französisch',
'es': 'Spanisch', 'spa': 'Spanisch',
'it': 'Italienisch', 'ita': 'Italienisch',
'ja': 'Japanisch', 'jpn': 'Japanisch',
'ko': 'Koreanisch', 'kor': 'Koreanisch',
'pt': 'Portugiesisch', 'por': 'Portugiesisch',
'ru': 'Russisch', 'rus': 'Russisch',
'tr': 'Türkisch', 'tur': 'Türkisch',
'zh': 'Chinesisch', 'zho': 'Chinesisch', 'chi': 'Chinesisch',
'ar': 'Arabisch', 'ara': 'Arabisch',
'hi': 'Hindi', 'hin': 'Hindi',
'nl': 'Niederländisch', 'nld': 'Niederländisch', 'dut': 'Niederländisch',
'pl': 'Polnisch', 'pol': 'Polnisch',
'sv': 'Schwedisch', 'swe': 'Schwedisch',
'da': 'Dänisch', 'dan': 'Dänisch',
'no': 'Norwegisch', 'nor': 'Norwegisch',
'fi': 'Finnisch', 'fin': 'Finnisch',
'cs': 'Tschechisch', 'ces': 'Tschechisch', 'cze': 'Tschechisch',
'el': 'Griechisch', 'ell': 'Griechisch', 'gre': 'Griechisch',
'he': 'Hebräisch', 'heb': 'Hebräisch',
'th': 'Thailändisch', 'tha': 'Thailändisch',
'uk': 'Ukrainisch', 'ukr': 'Ukrainisch',
'und': '',
}
return names.get(code.lower(), code.upper())
def _fmtBitrate(bw):
if not bw or bw <= 0: return ''
if bw >= 1000000:
return '%.1f Mbit/s' % (bw / 1000000.0)
return '%d kbit/s' % (bw / 1000)
def _parseHeaders(url):
headers = {}
if '|' in url:
try:
header_str = url.split('|', 1)[1]
headers = dict([item.split('=', 1) for item in header_str.split('&')])
for h in headers:
headers[h] = control.unquote_plus(headers[h])
except:
pass
return headers
# edit 2025-06-12
import sys
import re, json, random, time
from concurrent.futures import ThreadPoolExecutor
from resources.lib import log_utils, utils, control
from resources.lib.control import py2_decode, py2_encode, quote_plus, parse_qsl
import resolveurl as resolver
# from functools import reduce
from resources.lib.control import getKodiVersion
if int(getKodiVersion()) >= 20: from infotagger.listitem import ListItemInfoTag
# für self.sysmeta - zur späteren verwendung als meta
_params = dict(parse_qsl(sys.argv[2].replace('?',''))) if len(sys.argv) > 1 else dict()
class sources:
def __init__(self):
self.getConstants()
self.sources = []
self.current = int(time.time())
if 'sysmeta' in _params: self.sysmeta = _params['sysmeta'] # string zur späteren verwendung als meta
self.watcher = False
self.executor = ThreadPoolExecutor(max_workers=20)
self.url = None
def get(self, params):
data = json.loads(params['sysmeta'])
self.mediatype = data.get('mediatype')
self.aliases = data.get('aliases') if 'aliases' in data else []
title = py2_encode(data.get('title'))
originaltitle = py2_encode(data.get('originaltitle')) if 'originaltitle' in data else title
year = data.get('year') if 'year' in data else None
imdb = data.get('imdb_id') if 'imdb_id' in data else data.get('imdbnumber') if 'imdbnumber' in data else None
if not imdb and 'imdb' in data: imdb = data.get('imdb')
tmdb = data.get('tmdb_id') if 'tmdb_id' in data else None
#if tmdb and not imdb: print 'hallo' #TODO
season = data.get('season') if 'season' in data else 0
episode = data.get('episode') if 'episode' in data else 0
premiered = data.get('premiered') if 'premiered' in data else None
meta = params['sysmeta']
select = data.get('select') if 'select' in data else None
return title, year, imdb, season, episode, originaltitle, premiered, meta, select
def play(self, params):
title, year, imdb, season, episode, originaltitle, premiered, meta, select = self.get(params)
try:
url = None
#Liste der gefundenen Streams
items = self.getSources(title, year, imdb, season, episode, originaltitle, premiered)
select = control.getSetting('hosts.mode') if select == None else select
## unnötig
#select = '1' if control.getSetting('downloads') == 'true' and not (control.getSetting('download.movie.path') == '' or control.getSetting('download.tv.path') == '') else select
# # TODO überprüfen wofür mal gedacht
# if control.window.getProperty('PseudoTVRunning') == 'True':
# return control.resolveUrl(int(sys.argv[1]), True, control.item(path=str(self.sourcesDirect(items))))
if len(items) > 0:
# Auswahl Verzeichnis
if select == '1' and 'plugin' in control.infoLabel('Container.PluginName'):
control.window.clearProperty(self.itemsProperty)
control.window.setProperty(self.itemsProperty, json.dumps(items))
control.window.clearProperty(self.metaProperty)
control.window.setProperty(self.metaProperty, meta)
control.sleep(2)
return control.execute('Container.Update(%s?action=addItem&title=%s)' % (sys.argv[0], quote_plus(title)))
# Auswahl Dialog
elif select == '0' or select == '1':
url = self.sourcesDialog(items)
if url == 'close://': return
# Autoplay
else:
url = self.sourcesDirect(items)
if url == None: return self.errorForSources()
try: meta = json.loads(meta)
except: pass
from resources.lib.player import player
player().run(title, url, meta)
except Exception as e:
log_utils.log('Error %s' % str(e), log_utils.LOGERROR)
# Liste gefundene Streams Indexseite|Hoster
def addItem(self, title):
control.playlist.clear()
items = control.window.getProperty(self.itemsProperty)
items = json.loads(items)
if items == None or len(items) == 0: control.idle() ; sys.exit()
sysaddon = sys.argv[0]
syshandle = int(sys.argv[1])
systitle = sysname = quote_plus(title)
meta = control.window.getProperty(self.metaProperty)
meta = json.loads(meta)
#TODO
if meta['mediatype'] == 'movie':
# downloads = True if control.getSetting('downloads') == 'true' and control.exists(control.translatePath(control.getSetting('download.movie.path'))) else False
downloads = True if control.getSetting('downloads') == 'true' and control.getSetting('download.movie.path') else False
else:
# downloads = True if control.getSetting('downloads') == 'true' and control.exists(control.translatePath(control.getSetting('download.tv.path'))) else False
downloads = True if control.getSetting('downloads') == 'true' and control.getSetting('download.tv.path') else False
addonPoster, addonBanner = control.addonPoster(), control.addonBanner()
addonFanart, settingFanart = control.addonFanart(), control.getSetting('fanart')
if 'backdrop_url' in meta and 'http' in meta['backdrop_url']: fanart = meta['backdrop_url']
elif 'fanart' in meta and 'http' in meta['fanart']: fanart = meta['fanart']
else: fanart = addonFanart
if 'cover_url' in meta and 'http' in meta['cover_url']: poster = meta['cover_url']
elif 'poster' in meta and 'http' in meta['poster']: poster = meta['poster']
else: poster = addonPoster
sysimage = poster
if 'season' in meta and 'episode' in meta:
sysname += quote_plus(' S%02dE%02d' % (int(meta['season']), int(meta['episode'])))
elif 'year' in meta:
sysname += quote_plus(' (%s)' % meta['year'])
for i in range(len(items)):
try:
label = items[i]['label']
syssource = quote_plus(json.dumps([items[i]]))
item = control.item(label=label, offscreen=True)
item.setProperty('IsPlayable', 'true')
item.setArt({'poster': poster, 'banner': addonBanner})
if settingFanart == 'true': item.setProperty('Fanart_Image', fanart)
cm = []
if downloads:
cm.append(("Download", 'RunPlugin(%s?action=download&name=%s&image=%s&source=%s)' % (sysaddon, sysname, sysimage, syssource)))
if control.getSetting('jd_enabled') == 'true':
cm.append(("Sende zum JDownloader", 'RunPlugin(%s?action=sendToJD&name=%s&source=%s)' % (sysaddon, sysname, syssource)))
if control.getSetting('jd2_enabled') == 'true':
cm.append(("Sende zum JDownloader2", 'RunPlugin(%s?action=sendToJD2&name=%s&source=%s)' % (sysaddon, sysname, syssource)))
if control.getSetting('myjd_enabled') == 'true':
cm.append(("Sende zu My.JDownloader", 'RunPlugin(%s?action=sendToMyJD&name=%s&source=%s)' % (sysaddon, sysname, syssource)))
if control.getSetting('pyload_enabled') == 'true':
cm.append(("Sende zu PyLoad", 'RunPlugin(%s?action=sendToPyLoad&name=%s&source=%s)' % (sysaddon, sysname, syssource)))
cm.append(("Medien-Info", 'RunPlugin(%s?action=mediaInfo&source=%s)' % (sysaddon, syssource)))
cm.append(('Einstellungen', 'RunPlugin(%s?action=addonSettings)' % sysaddon))
item.addContextMenuItems(cm)
url = "%s?action=playItem&title=%s&source=%s" % (sysaddon, systitle, syssource)
# ## Notwendig für Library Exporte ##
# ## Amazon Scraper Details ##
# if "amazon" in label.lower():
# aid = re.search(r'asin%3D(.*?)%22%2C', url)
# url = "plugin://plugin.video.amazon-test/?mode=PlayVideo&asin=" + aid.group(1)
##https: // codedocs.xyz / AlwinEsch / kodi / group__python__xbmcgui__listitem.html # ga0b71166869bda87ad744942888fb5f14
name = '%s%sStaffel: %s Episode: %s' % (title, "\n", meta['season'], meta['episode']) if 'season' in meta else title
plot = meta['plot'] if 'plot' in meta and len(meta['plot'].strip()) >= 1 else ''
plot = '[COLOR blue]%s[/COLOR]%s%s' % (name, "\n\n", py2_encode(plot))
if 'duration' in meta:
infolable = {'plot': plot,'duration': meta['duration']}
else:
infolable = {'plot': plot}
# TODO
# if 'cast' in meta and meta['cast']: item.setCast(meta['cast'])
# # # remove unsupported InfoLabels
meta.pop('cast', None) # ersetzt durch item.setCast(i['cast'])
meta.pop('number_of_seasons', None)
meta.pop('imdb_id', None)
meta.pop('tvdb_id', None)
meta.pop('tmdb_id', None)
## Quality Video Stream from source.append quality - items[i]['quality']
video_streaminfo ={}
if "4k" in items[i]['quality'].lower():
video_streaminfo.update({'width': 3840, 'height': 2160})
elif "1080p" in items[i]['quality'].lower():
video_streaminfo.update({'width': 1920, 'height': 1080})
elif "hd" in items[i]['quality'].lower() or "720p" in items[i]['quality'].lower():
video_streaminfo.update({'width': 1280,'height': 720})
else:
# video_streaminfo.update({"width": 720, "height": 576})
video_streaminfo.update({})
## Codec for Video Stream from extra info - items[i]['info']
if 'hevc' in items[i]['label'].lower():
video_streaminfo.update({'codec': 'hevc'})
elif '265' in items[i]['label'].lower():
video_streaminfo.update({'codec': 'h265'})
elif 'mkv' in items[i]['label'].lower():
video_streaminfo.update({'codec': 'mkv'})
elif 'mp4' in items[i]['label'].lower():
video_streaminfo.update({'codec': 'mp4'})
else:
# video_streaminfo.update({'codec': 'h264'})
video_streaminfo.update({'codec': ''})
## Quality & Channels Audio Stream from extra info - items[i]['info']
audio_streaminfo = {}
if 'dts' in items[i]['label'].lower():
audio_streaminfo.update({'codec': 'dts'})
elif 'plus' in items[i]['label'].lower() or 'e-ac3' in items[i]['label'].lower():
audio_streaminfo.update({'codec': 'eac3'})
elif 'dolby' in items[i]['label'].lower() or 'ac3' in items[i]['label'].lower():
audio_streaminfo.update({'codec': 'ac3'})
else:
# audio_streaminfo.update({'codec': 'aac'})
audio_streaminfo.update({'codec': ''})
## Channel update ##
if '7.1' in items[i].get('info','').lower():
audio_streaminfo.update({'channels': 8})
elif '5.1' in items[i].get('info','').lower():
audio_streaminfo.update({'channels': 6})
else:
# audio_streaminfo.update({'channels': 2})
audio_streaminfo.update({'channels': ''})
if int(getKodiVersion()) <= 19:
item.setInfo(type='Video', infoLabels=infolable)
item.addStreamInfo('video', video_streaminfo)
item.addStreamInfo('audio', audio_streaminfo)
else:
info_tag = ListItemInfoTag(item, 'video')
info_tag.set_info(infolable)
stream_details = {
'video': [video_streaminfo],
'audio': [audio_streaminfo]}
info_tag.set_stream_details(stream_details)
# info_tag.set_cast(aActors)
control.addItem(handle=syshandle, url=url, listitem=item, isFolder=False)
except:
pass
control.content(syshandle, 'videos')
control.plugincategory(syshandle, control.addonVersion)
control.endofdirectory(syshandle, cacheToDisc=True)
def playItem(self, title, source):
isDebug = False
if isDebug: log_utils.log('start playItem', log_utils.LOGWARNING)
try:
meta = control.window.getProperty(self.metaProperty)
meta = json.loads(meta)
header = control.addonInfo('name')
# control.idle() #ok
progressDialog = control.progressDialog if control.getSetting('progress.dialog') == '0' else control.progressDialogBG
progressDialog.create(header, '')
progressDialog.update(0)
item = json.loads(source)[0]
#if isDebug: log_utils.log('playItem 237', log_utils.LOGWARNING)
if item['source'] == None: raise Exception()
future = self.executor.submit(self.sourcesResolve, item)
waiting_time = 30
while waiting_time > 0:
try:
if control.abortRequested: return sys.exit()
if progressDialog.iscanceled(): return progressDialog.close()
except:
pass
if future.done(): break
control.sleep(1)
waiting_time = waiting_time - 1
progressDialog.update(int(100 - 100. / 30 * waiting_time), str(item['label']))
#if isDebug: log_utils.log('playItem 252', log_utils.LOGWARNING)
if control.condVisibility('Window.IsActive(virtualkeyboard)') or \
control.condVisibility('Window.IsActive(yesnoDialog)'):
# or control.condVisibility('Window.IsActive(PopupRecapInfoWindow)'):
waiting_time = waiting_time + 1 # dont count down while dialog is presented
if future.done(): break
try: progressDialog.close()
except: pass
if isDebug: log_utils.log('playItem 261', log_utils.LOGWARNING)
control.execute('Dialog.Close(virtualkeyboard)')
control.execute('Dialog.Close(yesnoDialog)')
if isDebug: log_utils.log('playItem url: %s' % self.url, log_utils.LOGWARNING)
if self.url == None:
#self.errorForSources()
return
from resources.lib.player import player
player().run(title, self.url, meta)
return self.url
except Exception as e:
log_utils.log('Error %s' % str(e), log_utils.LOGERROR)
def getSources(self, title, year, imdb, season, episode, originaltitle, premiered, quality='HD', timeout=30):
#TODO
# self._getHostDict()
control.idle() #ok
progressDialog = control.progressDialog if control.getSetting('progress.dialog') == '0' else control.progressDialogBG
progressDialog.create(control.addonInfo('name'), '')
progressDialog.update(0)
progressDialog.update(0, "Quellen werden vorbereitet")
sourceDict = self.sourceDict
sourceDict = [(i[0], i[1], i[1].priority) for i in sourceDict]
random.shuffle(sourceDict)
sourceDict = sorted(sourceDict, key=lambda i: i[2])
content = 'movies' if season == 0 or season == '' or season == None else 'shows'
aliases, localtitle = utils.getAliases(imdb, content)
if localtitle and title != localtitle and originaltitle != localtitle:
if not title in aliases: aliases.append(title)
title = localtitle
for i in self.aliases:
if not i in aliases:
aliases.append(i)
titles = utils.get_titles_for_search(title, originaltitle, aliases)
futures = {self.executor.submit(self._getSource, titles, year, season, episode, imdb, provider[0], provider[1]): provider[0] for provider in sourceDict}
provider_names = {provider[0].upper() for provider in sourceDict}
string4 = "Total"
try: timeout = int(control.getSetting('scrapers.timeout'))
except: pass
quality = control.getSetting('hosts.quality')
if quality == '': quality = '0'
source_4k = 0
source_1080 = 0
source_720 = 0
source_sd = 0
total = d_total = 0
total_format = '[COLOR %s][B]%s[/B][/COLOR]'
pdiag_format = ' 4K: %s | 1080p: %s | 720p: %s | SD: %s | %s: %s '.split('|')
for i in range(0, 4 * timeout):
try:
if control.abortRequested: return sys.exit()
try:
if progressDialog.iscanceled(): break
except:
pass
if len(self.sources) > 0:
if quality in ['0']:
source_4k = len([e for e in self.sources if e['quality'] == '4K'])
source_1080 = len([e for e in self.sources if e['quality'] in ['1440p','1080p']])
source_720 = len([e for e in self.sources if e['quality'] in ['720p','HD']])
source_sd = len([e for e in self.sources if e['quality'] not in ['4K','1440p','1080p','720p','HD']])
elif quality in ['1']:
source_1080 = len([e for e in self.sources if e['quality'] in ['1440p','1080p']])
source_720 = len([e for e in self.sources if e['quality'] in ['720p','HD']])
source_sd = len([e for e in self.sources if e['quality'] not in ['4K','1440p','1080p','720p','HD']])
elif quality in ['2']:
source_1080 = len([e for e in self.sources if e['quality'] in ['1080p']])
source_720 = len([e for e in self.sources if e['quality'] in ['720p','HD']])
source_sd = len([e for e in self.sources if e['quality'] not in ['4K','1440p','1080p','720p','HD']])
elif quality in ['3']:
source_720 = len([e for e in self.sources if e['quality'] in ['720p','HD']])
source_sd = len([e for e in self.sources if e['quality'] not in ['4K','1440p','1080p','720p','HD']])
else:
source_sd = len([e for e in self.sources if e['quality'] not in ['4K','1440p','1080p','720p','HD']])
total = source_4k + source_1080 + source_720 + source_sd
source_4k_label = total_format % ('red', source_4k) if source_4k == 0 else total_format % ('lime', source_4k)
source_1080_label = total_format % ('red', source_1080) if source_1080 == 0 else total_format % ('lime', source_1080)
source_720_label = total_format % ('red', source_720) if source_720 == 0 else total_format % ('lime', source_720)
source_sd_label = total_format % ('red', source_sd) if source_sd == 0 else total_format % ('lime', source_sd)
source_total_label = total_format % ('red', total) if total == 0 else total_format % ('lime', total)
try:
info = [name.upper() for future, name in futures.items() if not future.done()]
percent = int(100 * float(i) / (2 * timeout) + 1)
if quality in ['0']:
line1 = '|'.join(pdiag_format) % (source_4k_label, source_1080_label, source_720_label, source_sd_label, str(string4), source_total_label)
elif quality in ['1']:
line1 = '|'.join(pdiag_format[1:]) % (source_1080_label, source_720_label, source_sd_label, str(string4), source_total_label)
elif quality in ['2']:
line1 = '|'.join(pdiag_format[1:]) % (source_1080_label, source_720_label, source_sd_label, str(string4), source_total_label)
elif quality in ['3']:
line1 = '|'.join(pdiag_format[2:]) % (source_720_label, source_sd_label, str(string4), source_total_label)
else:
line1 = '|'.join(pdiag_format[3:]) % (source_sd_label, str(string4), source_total_label)
if (i / 2) < timeout:
string = "Verbleibende Indexseiten: %s"
else:
string = 'Waiting for: %s'
if len(info) > 6: line = line1 + string % (str(len(info)))
elif len(info) > 1: line = line1 + string % (', '.join(info))
elif len(info) == 1: line = line1 + string % (''.join(info))
else: line = line1 + 'Suche beendet!'
progressDialog.update(max(1, percent), line)
if len(info) == 0: break
except Exception as e:
log_utils.log('Exception Raised: %s' % str(e), log_utils.LOGERROR)
control.sleep(1)
except:
pass
time.sleep(1)
try: progressDialog.close()
except: pass
self.sourcesFilter()
return self.sources
def _getSource(self, titles, year, season, episode, imdb, source, call):
try:
sources = call.run(titles, year, season, episode, imdb) # kasi self.hostDict
if sources == None or sources == []: raise Exception()
sources = [json.loads(t) for t in set(json.dumps(d, sort_keys=True) for d in sources)]
for i in sources:
i.update({'provider': source})
if not 'priority' in i: i.update({'priority': 100})
if not 'prioHoster' in i: i.update({'prioHoster': 100})
self.sources.extend(sources)
except:
pass
def sourcesFilter(self):
# hostblockDict = utils.getHostDict()
# self.sources = [i for i in self.sources if i['source'].split('.')[0] not in str(hostblockDict)] # Hoster ausschließen (Liste)
quality = control.getSetting('hosts.quality')
if quality == '': quality = '0'
random.shuffle(self.sources)
self.sources = sorted(self.sources, key=lambda k: k['prioHoster'], reverse=False)
for i in range(len(self.sources)):
q = self.sources[i]['quality']
if q.lower() == 'hd': self.sources[i].update({'quality': '720p'})
filter = []
if quality in ['0']: filter += [i for i in self.sources if i['quality'] == '4K']
if quality in ['0', '1']: filter += [i for i in self.sources if i['quality'] == '1440p']
if quality in ['0', '1', '2']: filter += [i for i in self.sources if i['quality'] == '1080p']
if quality in ['0', '1', '2', '3']: filter += [i for i in self.sources if i['quality'] == '720p']
#filter += [i for i in self.sources if i['quality'] in ['SD', 'SCR', 'CAM']]
filter += [i for i in self.sources if i['quality'] not in ['4k', '1440p', '1080p', '720p']]
self.sources = filter
if control.getSetting('hosts.sort.provider') == 'true':
self.sources = sorted(self.sources, key=lambda k: k['provider'])
if control.getSetting('hosts.sort.priority') == 'true' and self.mediatype == 'tvshow': self.sources = sorted(self.sources, key=lambda k: k['priority'], reverse=False)
if str(control.getSetting('hosts.limit')) == 'true':
self.sources = self.sources[:int(control.getSetting('hosts.limit.num'))]
else:
self.sources = self.sources[:100]
for i in range(len(self.sources)):
p = self.sources[i]['provider']
q = self.sources[i]['quality']
s = self.sources[i]['source']
## s = s.rsplit('.', 1)[0]
l = self.sources[i]['language']
try: f = (' | '.join(['[I]%s [/I]' % info.strip() for info in self.sources[i]['info'].split('|')]))
except: f = ''
label = '%02d | [B]%s[/B] | ' % (int(i + 1), p)
if q in ['4K', '1440p', '1080p', '720p']: label += '%s | [B][I]%s [/I][/B] | %s' % (s, q, f)
elif q == 'SD': label += '%s | %s' % (s, f)
else: label += '%s | %s | [I]%s [/I]' % (s, f, q)
label = label.replace('| 0 |', '|').replace(' | [I]0 [/I]', '')
label = re.sub(r'\[I\]\s+\[/I\]', ' ', label)
label = re.sub(r'\|\s+\|', '|', label)
label = re.sub(r'\|(?:\s+|)$', '', label)
self.sources[i]['label'] = label.upper()
# ## EMBY shown as premium link ##
# if self.sources[i]['provider']=="emby" or self.sources[i]['provider']=="amazon" or self.sources[i]['provider']=="netflix" or self.sources[i]['provider']=="maxdome":
# prem_identify = 'blue'
# self.sources[i]['label'] = ('[COLOR %s]' % (prem_identify)) + label.upper() + '[/COLOR]'
self.sources = [i for i in self.sources if 'label' in i]
return self.sources
def sourcesResolve(self, item, info=False):
try:
self.url = None
url = item['url']
direct = item['direct']
local = item.get('local', False)
provider = item['provider']
call = [i[1] for i in self.sourceDict if i[0] == provider][0]
url = call.resolve(url)
if not direct == True:
try:
hmf = resolver.HostedMediaFile(url=url, include_disabled=True, include_universal=False)
if hmf.valid_url():
url = hmf.resolve()
if url == False or url == None or url == '': url = None # raise Exception()
except:
url = None
if url == None or (not '://' in str(url) and not local):
log_utils.log('Kein Video Link gefunden: Provider %s / %s / %s ' % (item['provider'], item['source'] , str(item['source'])), log_utils.LOGERROR)
raise Exception()
# if not utils.test_stream(url):
# log_utils.log('URL Test Error: %s' % url, log_utils.LOGERROR)
# raise Exception()
# url = utils.m3u8_check(url)
if url:
self.url = url
return url
else:
raise Exception()
except:
if info: self.errorForSources()
return
def sourcesDialog(self, items):
labels = [i['label'] for i in items]
select = control.selectDialog(labels)
if select == -1: return 'close://'
next = [y for x,y in enumerate(items) if x >= select]
prev = [y for x,y in enumerate(items) if x < select][::-1]
items = [items[select]]
items = [i for i in items+next+prev][:40]
header = control.addonInfo('name')
header2 = header.upper()
progressDialog = control.progressDialog if control.getSetting('progress.dialog') == '0' else control.progressDialogBG
progressDialog.create(header, '')
progressDialog.update(0)
block = None
try:
for i in range(len(items)):
try:
if items[i]['source'] == block: raise Exception()
future = self.executor.submit(self.sourcesResolve, items[i])
try:
if progressDialog.iscanceled(): break
progressDialog.update(int((100 / float(len(items))) * i), str(items[i]['label']))
except:
progressDialog.update(int((100 / float(len(items))) * i), str(header2) + str(items[i]['label']))
waiting_time = 30
while waiting_time > 0:
try:
if control.abortRequested: return sys.exit() #xbmc.Monitor().abortRequested()
if progressDialog.iscanceled(): return progressDialog.close()
except:
pass
if future.done(): break
control.sleep(1)
waiting_time = waiting_time - 1
if control.condVisibility('Window.IsActive(virtualkeyboard)') or \
control.condVisibility('Window.IsActive(yesnoDialog)') or \
control.condVisibility('Window.IsActive(ProgressDialog)'):
waiting_time = waiting_time + 1 #dont count down while dialog is presented ## control.condVisibility('Window.IsActive(PopupRecapInfoWindow)') or \
if not future.done(): block = items[i]['source']
if self.url == None: raise Exception()
self.selectedSource = items[i]['label']
try: progressDialog.close()
except: pass
control.execute('Dialog.Close(virtualkeyboard)')
control.execute('Dialog.Close(yesnoDialog)')
return self.url
except:
pass
try: progressDialog.close()
except: pass
except Exception as e:
try: progressDialog.close()
except: pass
log_utils.log('Error %s' % str(e), log_utils.LOGINFO)
def sourcesDirect(self, items):
# TODO - OK
# filter = [i for i in items if i['source'].lower() in self.hostcapDict and i['debrid'] == '']
# items = [i for i in items if not i in filter]
# items = [i for i in items if ('autoplay' in i and i['autoplay'] == True) or not 'autoplay' in i]
u = None
header = control.addonInfo('name')
header2 = header.upper()
try:
control.sleep(1)
progressDialog = control.progressDialog if control.getSetting('progress.dialog') == '0' else control.progressDialogBG
progressDialog.create(header, '')
progressDialog.update(0)
except:
pass
for i in range(len(items)):
try:
if progressDialog.iscanceled(): break
progressDialog.update(int((100 / float(len(items))) * i), str(items[i]['label']))
except:
progressDialog.update(int((100 / float(len(items))) * i), str(header2) + str(items[i]['label']))
try:
if control.abortRequested: return sys.exit()
url = self.sourcesResolve(items[i])
if u == None: u = url
if not url == None: break
except:
pass
try: progressDialog.close()
except: pass
return u
def mediaInfo(self, source, dialog=None):
import xbmcgui
try:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except: pass
try:
item = json.loads(source)[0]
if item['source'] is None:
raise Exception()
import time as _time
from resources.lib.mediainfo import TOTAL_TIMEOUT
deadline = _time.time() + TOTAL_TIMEOUT
if dialog is None:
dialog = xbmcgui.DialogProgress()
dialog.create('Medien-Info', 'Löse Stream-URL auf... (%d Sek.)' % TOTAL_TIMEOUT)
dialog.update(0)
future = self.executor.submit(self.sourcesResolve, item)
# Wait for resolve with responsive cancel (check every 250ms)
for i in range(120): # 120 * 250ms = 30s max
remaining = int(deadline - _time.time())
if remaining > 0:
dialog.update(int(50.0 * i / 120), 'Löse Stream-URL auf... (%d Sek.)' % remaining)
else:
dialog.update(int(50.0 * i / 120), 'Löse Stream-URL auf...')
try:
if dialog.iscanceled():
try: dialog.close()
except: pass
return
except: pass
if future.done():
break
control.sleep(250)
# Don't count down while resolver shows interactive dialogs
if control.condVisibility('Window.IsActive(virtualkeyboard)') or \
control.condVisibility('Window.IsActive(yesnoDialog)'):
continue
url = self.url if future.done() else None
control.execute('Dialog.Close(virtualkeyboard)')
control.execute('Dialog.Close(yesnoDialog)')
try:
if dialog.iscanceled():
try: dialog.close()
except: pass
return
except: pass
if url is None:
try: dialog.close()
except: pass
control.infoDialog("Stream-URL konnte nicht aufgelöst werden", sound=False, icon='INFO')
return
dialog.update(50, 'Analysiere Stream...')
from resources.lib import mediainfo
info = mediainfo.getMediaInfo(url, dialog, deadline)
try: dialog.close()
except: pass
if info:
xbmcgui.Dialog().textviewer('Medien-Info', info)
else:
control.infoDialog("Auflösung konnte nicht ermittelt werden", sound=False, icon='INFO')
except Exception as e:
try:
if dialog: dialog.close()
except: pass
log_utils.log('mediaInfo Error: %s' % str(e), log_utils.LOGERROR)
control.infoDialog("Auflösung konnte nicht ermittelt werden", sound=False, icon='INFO')
def errorForSources(self):
control.infoDialog("Keine Streams verfügbar oder ausgewählt", sound=False, icon='INFO')
def getTitle(self, title):
title = utils.normalize(title)
return title
def getConstants(self):
self.itemsProperty = '%s.container.items' % control.Addon.getAddonInfo('id')
self.metaProperty = '%s.container.meta' % control.Addon.getAddonInfo('id')
from scrapers import sources
self.sourceDict = sources()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment