509 lines
21 KiB
Python
509 lines
21 KiB
Python
# Copyright (C) 2024 Lunatixz
|
|
#
|
|
#
|
|
# This file is part of PseudoTV Live.
|
|
#
|
|
# PseudoTV Live is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# PseudoTV Live is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with PseudoTV Live. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import os, sys, re, json, struct, errno, zlib
|
|
import shutil, subprocess, io, platform
|
|
import codecs, random
|
|
import uuid, base64, binascii, hashlib
|
|
import time, datetime, calendar
|
|
import heapq, requests, pyqrcode
|
|
import xml.sax.saxutils
|
|
|
|
from six.moves import urllib
|
|
from io import StringIO, BytesIO
|
|
from threading import Lock, Thread, Event, Timer, BoundedSemaphore
|
|
from threading import enumerate as thread_enumerate
|
|
from xml.dom.minidom import parse, parseString, Document
|
|
from xml.etree.ElementTree import ElementTree, Element, SubElement, XMLParser, fromstringlist, fromstring, tostring
|
|
from xml.etree.ElementTree import parse as ETparse
|
|
from typing import Dict, List, Union, Optional
|
|
|
|
from variables import *
|
|
from kodi_six import xbmc, xbmcaddon, xbmcplugin, xbmcgui, xbmcvfs
|
|
from contextlib import contextmanager, closing
|
|
from socket import gethostbyname, gethostname
|
|
from itertools import cycle, chain, zip_longest, islice
|
|
from xml.sax.saxutils import escape, unescape
|
|
from operator import itemgetter
|
|
|
|
from logger import *
|
|
from cache import Cache, cacheit
|
|
from pool import killit, timeit, poolit, executeit, timerit, threadit
|
|
from kodi import *
|
|
from fileaccess import FileAccess, FileLock
|
|
from collections import defaultdict, Counter, OrderedDict
|
|
from six.moves import urllib
|
|
from math import ceil, floor
|
|
from infotagger.listitem import ListItemInfoTag
|
|
from requests.adapters import HTTPAdapter, Retry
|
|
|
|
DIALOG = Dialog()
|
|
PROPERTIES = DIALOG.properties
|
|
SETTINGS = DIALOG.settings
|
|
LISTITEMS = DIALOG.listitems
|
|
BUILTIN = DIALOG.builtin
|
|
|
|
def slugify(s, lowercase=False):
|
|
if lowercase: s = s.lower()
|
|
s = s.strip()
|
|
s = re.sub(r'[^\w\s-]', '', s)
|
|
s = re.sub(r'[\s_-]+', '_', s)
|
|
s = re.sub(r'^-+|-+$', '', s)
|
|
return s
|
|
|
|
def validString(s):
|
|
return "".join(x for x in s if (x.isalnum() or x not in '\\/:*?"<>|'))
|
|
|
|
def stripNumber(s):
|
|
return re.sub(r'\d+','',s)
|
|
|
|
def stripRegion(s):
|
|
match = re.compile(r'(.*) \((.*)\)', re.IGNORECASE).search(s)
|
|
try: return match.group(1)
|
|
except: return s
|
|
|
|
def chanceBool(percent=25):
|
|
return random.randrange(100) <= percent
|
|
|
|
def decodePlot(text: str = '') -> dict:
|
|
plot = re.search(r'\[COLOR item=\"(.+?)\"]\[/COLOR]', text)
|
|
if plot: return loadJSON(decodeString(plot.group(1)))
|
|
return {}
|
|
|
|
def encodePlot(plot, text):
|
|
return '%s [COLOR item="%s"][/COLOR]'%(plot,encodeString(dumpJSON(text)))
|
|
|
|
def escapeString(text, table=HTML_ESCAPE):
|
|
return escape(text,table)
|
|
|
|
def unescapeString(text, table=HTML_ESCAPE):
|
|
return unescape(text,{v:k for k, v in list(table.items())})
|
|
|
|
def getJSON(file):
|
|
data = {}
|
|
try:
|
|
fle = FileAccess.open(file,'r')
|
|
data = loadJSON(fle.read())
|
|
except Exception as e: log('Globals: getJSON failed! %s\nfile = %s'%(e,file), xbmc.LOGERROR)
|
|
fle.close()
|
|
return data
|
|
|
|
def setJSON(file, data):
|
|
with FileLock():
|
|
fle = FileAccess.open(file, 'w')
|
|
fle.write(dumpJSON(data, idnt=4, sortkey=False))
|
|
fle.close()
|
|
return True
|
|
|
|
def requestURL(url, params={}, payload={}, header=HEADER, timeout=FIFTEEN, json_data=False, cache=None, checksum=ADDON_VERSION, life=datetime.timedelta(minutes=15)):
|
|
def __error(json_data):
|
|
return {} if json_data else ""
|
|
|
|
def __getCache(key,json_data,cache,checksum):
|
|
return (cache.get('requestURL.%s'%(key), checksum, json_data) or __error(json_data))
|
|
|
|
def __setCache(key,results,json_data,cache,checksum,life):
|
|
return cache.set('requestURL.%s'%(key), results, checksum, life, json_data)
|
|
|
|
complete = False
|
|
cacheKey = '.'.join([url,dumpJSON(params),dumpJSON(payload),dumpJSON(header)])
|
|
session = requests.Session()
|
|
retries = Retry(total=3, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
|
|
adapter = HTTPAdapter(max_retries=retries)
|
|
session.mount("http://", adapter)
|
|
session.mount("https://", adapter)
|
|
try:
|
|
headers = HEADER.copy()
|
|
headers.update(header)
|
|
if payload: response = session.post(url, data=dumpJSON(payload), headers=headers, timeout=timeout)
|
|
else: response = session.get(url, params=params, headers=headers, timeout=timeout)
|
|
response.raise_for_status() # Raise an exception for HTTP errors
|
|
log("Globals: requestURL, url = %s, status = %s"%(url,response.status_code))
|
|
complete = True
|
|
|
|
if json_data: results = response.json()
|
|
else: results = response.content
|
|
if results and cache: return __setCache(cacheKey,results,json_data,cache,checksum,life)
|
|
else: return results
|
|
|
|
except requests.exceptions.ConnectionError as e:
|
|
log("Globals: requestURL, failed! Error connecting to the server: %s"%('Returning cache' if cache else 'No Response'))
|
|
return __getCache(cacheKey,json_data,cache,checksum) if cache else __error(json_data)
|
|
|
|
except requests.exceptions.HTTPError as e:
|
|
log("Globals: requestURL, failed! HTTP error occurred: %s"%('Returning cache' if cache else 'No Response'))
|
|
return __getCache(cacheKey,json_data,cache,checksum) if cache else __error(json_data)
|
|
|
|
except Exception as e:
|
|
log("Globals: requestURL, failed! An error occurred: %s"%(e), xbmc.LOGERROR)
|
|
return __error(json_data)
|
|
|
|
finally:
|
|
if not complete and payload:
|
|
queueURL({"url":url, "params":params, "payload":payload, "header":header, "timeout":timeout, "json_data":json_data, "cache":cache, "checksum":checksum, "life":life}) #retry post
|
|
|
|
def queueURL(param):
|
|
queuePool = (SETTINGS.getCacheSetting('queueURL', json_data=True) or {})
|
|
params = queuePool.setdefault('params',[])
|
|
params.append(param)
|
|
queuePool['params'] = setDictLST(params)
|
|
log("Globals: queueURL, saving = %s\n%s"%(len(queuePool['params']),param))
|
|
SETTINGS.setCacheSetting('queueURL', queuePool, json_data=True)
|
|
|
|
def setURL(url, file):
|
|
try:
|
|
contents = requestURL(url)
|
|
fle = FileAccess.open(file, 'w')
|
|
fle.write(contents)
|
|
fle.close()
|
|
return FileAccess.exists(file)
|
|
except Exception as e: log('Globals: setURL failed! %s\nurl = %s'%(e,url), xbmc.LOGERROR)
|
|
|
|
def diffLSTDICT(old, new):
|
|
set1 = {dumpJSON(d, sortkey=True) for d in old}
|
|
set2 = {dumpJSON(d, sortkey=True) for d in new}
|
|
return {"added": [loadJSON(s) for s in set2 - set1], "removed": [loadJSON(s) for s in set1 - set2]}
|
|
|
|
def getChannelID(name, path, number):
|
|
if isinstance(path, list): path = '|'.join(path)
|
|
tmpid = '%s.%s.%s.%s'%(number, name, hashlib.md5(path.encode(DEFAULT_ENCODING)),SETTINGS.getMYUUID())
|
|
return '%s@%s'%((binascii.hexlify(tmpid.encode(DEFAULT_ENCODING))[:32]).decode(DEFAULT_ENCODING),slugify(ADDON_NAME))
|
|
|
|
def getRecordID(name, path, number):
|
|
if isinstance(path, list): path = '|'.join(path)
|
|
tmpid = '%s.%s.%s.%s'%(number, name, hashlib.md5(path.encode(DEFAULT_ENCODING)),SETTINGS.getMYUUID())
|
|
return '%s@%s'%((binascii.hexlify(tmpid.encode(DEFAULT_ENCODING))[:16]).decode(DEFAULT_ENCODING),slugify(ADDON_NAME))
|
|
|
|
def splitYear(label):
|
|
try:
|
|
match = re.compile(r'(.*) \((.*)\)', re.IGNORECASE).search(label)
|
|
if match and match.group(2):
|
|
label, year = match.groups()
|
|
if year.isdigit():
|
|
return label, int(year)
|
|
except: pass
|
|
return label, None
|
|
|
|
def getChannelSuffix(name, type):
|
|
name = validString(name)
|
|
if type == "TV Genres" and not LANGUAGE(32014).lower() in name.lower(): suffix = LANGUAGE(32014) #TV
|
|
elif type == "Movie Genres" and not LANGUAGE(32015).lower() in name.lower(): suffix = LANGUAGE(32015) #Movies
|
|
elif type == "Mixed Genres" and not LANGUAGE(32010).lower() in name.lower(): suffix = LANGUAGE(32010) #Mixed
|
|
elif type == "Music Genres" and not LANGUAGE(32016).lower() in name.lower(): suffix = LANGUAGE(32016) #Music
|
|
else: return name
|
|
return '%s %s'%(name,suffix)
|
|
|
|
def cleanChannelSuffix(name, type):
|
|
if type == "TV Genres" : name = name.split(' %s'%LANGUAGE(32014))[0]#TV
|
|
elif type == "Movie Genres" : name = name.split(' %s'%LANGUAGE(32015))[0]#Movies
|
|
elif type == "Mixed Genres" : name = name.split(' %s'%LANGUAGE(32010))[0]#Mixed
|
|
elif type == "Music Genres" : name = name.split(' %s'%LANGUAGE(32016))[0]#Music
|
|
return name
|
|
|
|
def getLabel(item, addYear=False):
|
|
label = (item.get('name') or item.get('label') or item.get('showtitle') or item.get('title'))
|
|
if not label: return ''
|
|
label, year = splitYear(label)
|
|
year = (item.get('year') or year)
|
|
if year and addYear: return '%s (%s)'%(label, year)
|
|
return label
|
|
|
|
def hasFile(file):
|
|
if not file.startswith(tuple(VFS_TYPES + WEB_TYPES)): state = FileAccess.exists(file)
|
|
elif file.startswith('plugin://'): state = hasAddon(file)
|
|
else: state = True
|
|
log("Globals: hasFile, file = %s (%s)"%(file,state))
|
|
return state
|
|
|
|
def hasAddon(id, install=False, enable=False, force=False, notify=False):
|
|
if '://' in id: id = getIDbyPath(id)
|
|
if BUILTIN.getInfoBool('HasAddon(%s)'%(id),'System'):
|
|
if BUILTIN.getInfoBool('AddonIsEnabled(%s)'%(id),'System'): return True
|
|
elif enable:
|
|
if not force:
|
|
if not DIALOG.yesnoDialog(message=LANGUAGE(32156)%(id)): return False
|
|
return BUILTIN.executebuiltin('EnableAddon(%s)'%(id),wait=True)
|
|
elif install: return BUILTIN.executebuiltin('InstallAddon(%s)'%(id),wait=True)
|
|
if notify: DIALOG.notificationDialog(LANGUAGE(32034)%(id))
|
|
return False
|
|
|
|
def diffRuntime(dur, roundto=15):
|
|
def ceil_dt(dt, delta):
|
|
return dt + (datetime.datetime.min - dt) % delta
|
|
now = datetime.datetime.fromtimestamp(dur)
|
|
return (ceil_dt(now, datetime.timedelta(minutes=roundto)) - now).total_seconds()
|
|
|
|
def roundTimeDown(dt, offset=30): # round the given time down to the nearest
|
|
n = datetime.datetime.fromtimestamp(dt)
|
|
delta = datetime.timedelta(minutes=offset)
|
|
if n.minute > (offset-1): n = n.replace(minute=offset, second=0, microsecond=0)
|
|
else: n = n.replace(minute=0, second=0, microsecond=0)
|
|
return time.mktime(n.timetuple())
|
|
|
|
def roundTimeUp(dt=None, roundTo=60):
|
|
if dt == None : dt = datetime.datetime.now()
|
|
seconds = (dt.replace(tzinfo=None) - dt.min).seconds
|
|
rounding = (seconds+roundTo/2) // roundTo * roundTo
|
|
return dt + datetime.timedelta(0,rounding-seconds,-dt.microsecond)
|
|
|
|
def strpTime(datestring, format=DTJSONFORMAT): #convert pvr infolabel datetime string to datetime obj, thread safe!
|
|
try: return datetime.datetime.strptime(datestring, format)
|
|
except TypeError: return datetime.datetime.fromtimestamp(time.mktime(time.strptime(datestring, format)))
|
|
except: return ''
|
|
|
|
def epochTime(timestamp, tz=True): #convert pvr json datetime string to datetime obj
|
|
if tz: timestamp -= getTimeoffset()
|
|
return datetime.datetime.fromtimestamp(timestamp)
|
|
|
|
def getTimeoffset():
|
|
return (int((datetime.datetime.now() - datetime.datetime.utcnow()).days * 86400 + round((datetime.datetime.now() - datetime.datetime.utcnow()).seconds, -1)))
|
|
|
|
def getUTCstamp():
|
|
return time.time() - getTimeoffset()
|
|
|
|
def getGMTstamp():
|
|
return time.time()
|
|
|
|
def randomShuffle(items=[]):
|
|
if len(items) > 0:
|
|
#reseed random for a "greater sudo random"
|
|
random.seed(random.randint(0,999999999999))
|
|
random.shuffle(items)
|
|
return items
|
|
|
|
def isStack(path): #is path a stack
|
|
return path.startswith('stack://')
|
|
|
|
def splitStacks(path): #split stack path for indv. files.
|
|
if not isStack(path): return [path]
|
|
return [_f for _f in ((path.split('stack://')[1]).split(' , ')) if _f]
|
|
|
|
def escapeDirJSON(path):
|
|
mydir = path
|
|
if (mydir.find(":")): mydir = mydir.replace("\\", "\\\\")
|
|
return mydir
|
|
|
|
def KODI_LIVETV_SETTINGS(): #recommended Kodi LiveTV settings
|
|
return {'pvrmanager.preselectplayingchannel' :'true',
|
|
'pvrmanager.syncchannelgroups' :'true',
|
|
'pvrmanager.backendchannelorder' :'true',
|
|
'pvrmanager.usebackendchannelnumbers':'true',
|
|
'pvrplayback.autoplaynextprogramme' :'true',
|
|
# 'pvrmenu.iconpath':'',
|
|
# 'pvrplayback.switchtofullscreenchanneltypes':1,
|
|
# 'pvrplayback.confirmchannelswitch':'true',
|
|
# 'epg.selectaction':2,
|
|
# 'epg.epgupdate':120,
|
|
'pvrmanager.startgroupchannelnumbersfromone':'false'}
|
|
|
|
def togglePVR(state=True, reverse=False, wait=FIFTEEN):
|
|
if SETTINGS.getSettingBool('Enable_PVR_RELOAD'):
|
|
isEnabled = BUILTIN.getInfoBool('AddonIsEnabled(%s)'%(PVR_CLIENT_ID),'System')
|
|
if (state and isEnabled) or (not state and not isEnabled): return
|
|
elif not PROPERTIES.isRunning('togglePVR'):
|
|
with PROPERTIES.chkRunning('togglePVR'):
|
|
BUILTIN.executebuiltin("Dialog.Close(all)")
|
|
log('globals: togglePVR, state = %s, reverse = %s, wait = %s'%(state,reverse,wait))
|
|
BUILTIN.executeJSONRPC('{"jsonrpc":"2.0","method":"Addons.SetAddonEnabled","params":{"addonid":"%s","enabled":%s}, "id": 1}'%(PVR_CLIENT_ID,str(state).lower()))
|
|
if reverse:
|
|
with BUILTIN.busy_dialog():
|
|
MONITOR().waitForAbort(1.0)
|
|
timerit(togglePVR)(wait,[not bool(state)])
|
|
DIALOG.notificationWait('%s: %s'%(PVR_CLIENT_NAME,LANGUAGE(32125)),wait=wait)
|
|
else: DIALOG.notificationWait(LANGUAGE(30023)%(PVR_CLIENT_NAME))
|
|
|
|
def isRadio(item):
|
|
if item.get('radio',False) or item.get('type') == "Music Genres": return True
|
|
for path in item.get('path',[item.get('file','')]):
|
|
if path.lower().startswith(('musicdb://','special://profile/playlists/music/','special://musicplaylists/')): return True
|
|
return False
|
|
|
|
def isMixed_XSP(item):
|
|
for path in item.get('path',[item.get('file','')]):
|
|
if path.lower().startswith('special://profile/playlists/mixed/'): return True
|
|
return False
|
|
|
|
def cleanLabel(text):
|
|
text = re.sub(r'\[COLOR=(.+?)\]', '', text)
|
|
text = re.sub(r'\[/COLOR\]', '', text)
|
|
text = text.replace("[B]",'').replace("[/B]",'')
|
|
text = text.replace("[I]",'').replace("[/I]",'')
|
|
return text.replace(":",'')
|
|
|
|
def cleanImage(image=LOGO):
|
|
if not image: image = LOGO
|
|
if not image.startswith(('image://','resource://','special://','smb://','nfs://','https://','http://')):
|
|
realPath = FileAccess.translatePath('special://home/addons/')
|
|
if image.startswith(realPath):# convert real path. to vfs
|
|
image = image.replace(realPath,'special://home/addons/').replace('\\','/')
|
|
elif image.startswith(realPath.replace('\\','/')):
|
|
image = image.replace(realPath.replace('\\','/'),'special://home/addons/').replace('\\','/')
|
|
return image
|
|
|
|
def cleanGroups(citem, enableGrouping=SETTINGS.getSettingBool('Enable_Grouping')):
|
|
if not enableGrouping:
|
|
citem['group'] = [ADDON_NAME]
|
|
else:
|
|
citem['group'].append(ADDON_NAME)
|
|
if citem.get('favorite',False) and not LANGUAGE(32019) in citem['group']:
|
|
citem['group'].append(LANGUAGE(32019))
|
|
elif not citem.get('favorite',False) and LANGUAGE(32019) in citem['group']:
|
|
citem['group'].remove(LANGUAGE(32019))
|
|
return sorted(set(citem['group']))
|
|
|
|
def cleanMPAA(mpaa):
|
|
orgMPA = mpaa
|
|
mpaa = mpaa.lower()
|
|
if ':' in mpaa: mpaa = re.split(':',mpaa)[1] #todo prop. regex
|
|
if 'rated ' in mpaa: mpaa = re.split('rated ',mpaa)[1] #todo prop. regex
|
|
#todo regex, detect other region rating formats
|
|
# re.compile(':(.*)', re.IGNORECASE).search(text))
|
|
text = mpaa.upper()
|
|
try:
|
|
text = re.sub('/ US', '' , text)
|
|
text = re.sub('Rated ', '', text)
|
|
mpaa = text.strip()
|
|
except:
|
|
mpaa = mpaa.strip()
|
|
return mpaa
|
|
|
|
def getIDbyPath(url):
|
|
try:
|
|
if url.startswith('special://'): return re.compile('special://home/addons/(.*?)/resources', re.IGNORECASE).search(url).group(1)
|
|
elif url.startswith('plugin://'): return re.compile('plugin://(.*?)/', re.IGNORECASE).search(url).group(1)
|
|
except Exception as e: log('Globals: getIDbyPath failed! url = %s, %s'%(url,e), xbmc.LOGERROR)
|
|
return url
|
|
|
|
def combineDicts(dict1={}, dict2={}):
|
|
for k,v in list(dict1.items()):
|
|
if dict2.get(k): k = dict2.pop(k)
|
|
dict1.update(dict2)
|
|
return dict1
|
|
|
|
def mergeDictLST(dict1={},dict2={}):
|
|
for k, v in list(dict2.items()):
|
|
dict1.setdefault(k,[]).extend(v)
|
|
setDictLST()
|
|
return dict1
|
|
|
|
def lstSetDictLst(lst=[]):
|
|
items = dict()
|
|
for key, dictlst in list(lst.items()):
|
|
if isinstance(dictlst, list): dictlst = setDictLST(dictlst)
|
|
items[key] = dictlst
|
|
return items
|
|
|
|
def compareDict(dict1,dict2,sortKey):
|
|
a = sorted(dict1, key=itemgetter(sortKey))
|
|
b = sorted(dict2, key=itemgetter(sortKey))
|
|
return a == b
|
|
|
|
def subZoom(number,percentage,multi=100):
|
|
return round(number * (percentage*multi) / 100)
|
|
|
|
def addZoom(number,percentage,multi=100):
|
|
return round((number - (number * (percentage*multi) / 100)) + number)
|
|
|
|
def frange(start,stop,inc):
|
|
return [x/10.0 for x in range(start,stop,inc)]
|
|
|
|
def timeString2Seconds(string): #hh:mm:ss
|
|
try: return int(sum(x*y for x, y in zip(list(map(float, string.split(':')[::-1])), (1, 60, 3600, 86400))))
|
|
except: return -1
|
|
|
|
def chunkLst(lst, n):
|
|
for i in range(0, len(lst), n):
|
|
yield lst[i:i + n]
|
|
|
|
def chunkDict(items, n):
|
|
it = iter(items)
|
|
for i in range(0, len(items), n):
|
|
yield {k:items[k] for k in islice(it, n)}
|
|
|
|
def roundupDIV(p, q):
|
|
try:
|
|
d, r = divmod(p, q)
|
|
if r: d += 1
|
|
return d
|
|
except ZeroDivisionError:
|
|
return 1
|
|
|
|
def interleave(seqs, sets=1, repeats=False):
|
|
#evenly interleave multi-lists of different sizes, while preserving seq order and by sets of x
|
|
# In [[1,2,3,4],['a','b','c'],['A','B','C','D','E']]
|
|
# repeats = False
|
|
# Out sets=0 [1, 2, 3, 4, 'a', 'b', 'c', 'A', 'B', 'C', 'D', 'E']
|
|
# Out sets=1 [1, 'a', 'A', 2, 'b', 'B', 3, 'c', 'C', 4, 'D', 'E']
|
|
# Out sets=2 [1, 2, 'a', 'b', 'A', 'B', 3, 4, 'c', 'C', 'D', 'E']
|
|
# repeats = True
|
|
# Out sets=0 [1, 2, 3, 4, 'a', 'b', 'c', 'A', 'B', 'C', 'D', 'E']
|
|
# Out sets=1 [1, 'a', 'A', 2, 'b', 'B', 3, 'c', 'C', 4, 'a', 'D', 1, 'b', 'E']
|
|
# Out sets=2 [1, 2, 'a', 'b', 'A', 'B', 3, 4, 'c', 'a', 'C', 'D', 1, 2, 'b', 'c', 'E', 'A']
|
|
if sets > 0:
|
|
# if repeats:
|
|
# # Create cyclical iterators for each list
|
|
# cyclical_iterators = [cycle(lst) for lst in seqs]
|
|
# interleaved = []
|
|
# # Determine the length of the longest list
|
|
# max_len = max(len(lst) for lst in seqs)
|
|
# # Calculate the number of blocks needed
|
|
# num_blocks = (max_len + sets - 1) // sets
|
|
# # Interleave in blocks
|
|
# for i in range(num_blocks):
|
|
# for iterator in cyclical_iterators:
|
|
# # Use islice to take a block of elements from the current iterator
|
|
# block = list(islice(iterator, sets))
|
|
# interleaved.extend(block)
|
|
# return interleaved
|
|
# else:
|
|
seqs = [list(zip_longest(*[iter(seqs)] * sets, fillvalue=None)) for seqs in seqs]
|
|
return list([_f for _f in sum([_f for _f in chain.from_iterable(zip_longest(*seqs)) if _f], ()) if _f])
|
|
else: return list(chain.from_iterable(seqs))
|
|
|
|
def percentDiff(org, new):
|
|
try: return (abs(float(org) - float(new)) / float(new)) * 100.0
|
|
except ZeroDivisionError: return -1
|
|
|
|
def pagination(list, end):
|
|
for start in range(0, len(list), end):
|
|
yield seq[start:start+end]
|
|
|
|
def isCenterlized():
|
|
default = 'special://profile/addon_data/plugin.video.pseudotv.live/cache'
|
|
if REAL_SETTINGS.getSetting('User_Folder') == default:
|
|
return False
|
|
return True
|
|
|
|
def isFiller(item={}):
|
|
for genre in item.get('genre',[]):
|
|
if genre.lower() in ['pre-roll','post-roll']: return True
|
|
return False
|
|
|
|
def isShort(item={}, minDuration=SETTINGS.getSettingInt('Seek_Tolerance')):
|
|
if item.get('duration', minDuration) < minDuration: return True
|
|
else: return False
|
|
|
|
def isEnding(progress=100):
|
|
if progress >= SETTINGS.getSettingInt('Seek_Threshold'): return True
|
|
else: return False
|
|
|
|
def chkLogo(old, new=LOGO):
|
|
if new.endswith('wlogo.png') and not old.endswith('wlogo.png'): return old
|
|
return new |