KoD 1.7.6

- fix di routine ai canali/server\n- disabilitati cb01anime e tantifilm\n- aggiunta opzione mostra server nel menu contestuale della libreria\n- più opzioni per quanto riguarda l'aggiornamento della videoteca\n\n
This commit is contained in:
marco
2022-09-15 19:15:51 +02:00
parent 749b54a772
commit 164efd8af7
41 changed files with 1062 additions and 941 deletions
+5 -4
View File
@@ -1,4 +1,4 @@
<addon id="plugin.video.kod" name="Kodi on Demand" version="1.7.5" provider-name="KoD Team"> <addon id="plugin.video.kod" name="Kodi on Demand" version="1.7.6" provider-name="KoD Team">
<requires> <requires>
<!-- <import addon="script.module.libtorrent" optional="true"/> --> <!-- <import addon="script.module.libtorrent" optional="true"/> -->
<import addon="metadata.themoviedb.org"/> <import addon="metadata.themoviedb.org"/>
@@ -27,9 +27,10 @@
<screenshot>resources/media/screenshot-2.png</screenshot> <screenshot>resources/media/screenshot-2.png</screenshot>
<screenshot>resources/media/screenshot-3.png</screenshot> <screenshot>resources/media/screenshot-3.png</screenshot>
</assets> </assets>
<news>- Aggiunti nuovi canali: 1337x e filmstreaming <news>- fix di routine ai canali/server
- fix cinemalibero, altadefinizione01 - disabilitati cb01anime e tantifilm
- workaround per puntante non funzionanti quando si aggiorna la videoteca - aggiunta opzione &quot;mostra server&quot; nel menu contestuale della libreria
- più opzioni per quanto riguarda l'aggiornamento della videoteca
</news> </news>
<description lang="it">Naviga velocemente sul web e guarda i contenuti presenti</description> <description lang="it">Naviga velocemente sul web e guarda i contenuti presenti</description>
<disclaimer>[COLOR red]The owners and submitters to this addon do not host or distribute any of the content displayed by these addons nor do they have any affiliation with the content providers.[/COLOR] <disclaimer>[COLOR red]The owners and submitters to this addon do not host or distribute any of the content displayed by these addons nor do they have any affiliation with the content providers.[/COLOR]
+1 -1
View File
@@ -8,7 +8,7 @@
"animeuniverse": "https://www.animeuniverse.it", "animeuniverse": "https://www.animeuniverse.it",
"animeworld": "https://www.animeworld.tv", "animeworld": "https://www.animeworld.tv",
"aniplay": "https://aniplay.it", "aniplay": "https://aniplay.it",
"casacinema": "https://www.casacinema.page", "casacinema": "https://www.casacinema.lol",
"cb01anime": "https://www.cineblog01.red", "cb01anime": "https://www.cineblog01.red",
"cinemalibero": "https://cinemalibero.cafe", "cinemalibero": "https://cinemalibero.cafe",
"cinetecadibologna": "http://cinestore.cinetecadibologna.it", "cinetecadibologna": "http://cinestore.cinetecadibologna.it",
+21 -11
View File
@@ -20,12 +20,14 @@ headers = [['Referer', host]]
@support.menu @support.menu
def mainlist(item): def mainlist(item):
film = ['/genere/film/', film = ['/category/film/',
('Al Cinema', ['/al-cinema/', 'peliculas']), ('Al Cinema', ['/al-cinema/', 'peliculas']),
('Generi', ['', 'genres']), ('Generi', ['', 'genres']),
('Sub-ITA', ['/sub-ita/', 'peliculas'])] # ('Sub-ITA', ['/sub-ita/', 'peliculas'])
]
tvshow = ['/genere/serie-tv/'] tvshow = ['/category/serie-tv/',
('Aggiornamenti Serie TV', ['/aggiornamenti-serie-tv/', 'peliculas']),]
search = '' search = ''
@@ -37,12 +39,19 @@ def genres(item):
action = 'peliculas' action = 'peliculas'
blacklist = ['Scegli il Genere', 'Film', 'Serie TV', 'Sub-Ita', 'Anime'] blacklist = ['Scegli il Genere', 'Film', 'Serie TV', 'Sub-Ita', 'Anime']
patronMenu = r'<option value="(?P<url>[^"]+)">(?P<title>[^<]+)' patronMenu = r'<option value="(?P<url>[^"]+)">(?P<title>[^<]+)'
def itemlistHook(itemlist):
itl = []
for item in itemlist:
if len(item.fulltitle) != 3:
itl.append(item)
return itl
return locals() return locals()
def search(item, text): def search(item, text):
logger.debug(text) logger.debug(text)
item.url = "{}/?s={}".format(host, text) item.url = "{}/search/{}/feed/rss2/".format(host, text)
item.args = 'search'
try: try:
return peliculas(item) return peliculas(item)
@@ -59,9 +68,10 @@ def peliculas(item):
n = '22' if '/?s=' in item.url else '8' n = '22' if '/?s=' in item.url else '8'
item.contentType = "undefined" item.contentType = "undefined"
action = 'check' action = 'check'
# patron = r'data-src="(?P<thumb>http[^"]+)(?:[^>]+>){' + n + r'}\s*<a href="(?P<url>[^"]+)[^>]+>\s*(?P<title>[^\[\(\<]+)(?:\[(?P<quality>[^\]]+)\])?\s*(?:\((?P<lang>[a-zA-z-]+)\))?\s*(?:\((?P<year>\d+)\))?\s*</a>\s*</h2>' patron = r'src="(?P<poster>http[^"]+)(?:[^>]+>){4}\s*<a href="(?P<url>[^"]+)[^>]+>\s*(?P<title>[^\[\(\<]+)(?:\[(?P<quality>[^\]]+)\])?\s*(?:\((?P<lang>[a-zA-z-]+)\))?\s*(?:\((?P<year>\d+)\))?\s*</a>\s*</h2>'
patron = r'data-src="(?P<poster>http[^"]+)(?:[^>]+>){7,18}\s*<a href="(?P<url>[^"]+)[^>]+>\s*(?P<title>[^\[\(\<]+)(?:\[(?P<quality>[^\]]+)\])?\s*(?:\((?P<lang>[a-zA-z-]+)\))?\s*(?:\((?P<year>\d+)\))?\s*</a>\s*</h2>' if item.args == 'search':
patronNext = r'href="([^"]+)[^>]+>»' patron = r'<title>(?P<title>[^\[\(\<]+)(?:\[(?P<quality>[^\]]+)\])?\s*(?:\((?P<lang>[a-zA-z-]+)\))?\s*(?:\((?P<year>\d+)\))?\s*[^>]+>\s*<link>(?P<url>[^<]+)'
patronNext = r'href="([^"]+)[^>]+>Successivo'
return locals() return locals()
@@ -70,15 +80,15 @@ def episodios(item):
item.quality = '' item.quality = ''
data = item.data data = item.data
action='findvideos' action='findvideos'
# patronBlock = r'[Ss]tagione.*?\s(?P<lang>(?:[Ss][Uu][Bb][-]?)?[Ii][Tt][Aa])(?: in )?(?P<quality>[^<]*)?(?:[^>]+>){4}(?P<block>.*?)/p>' # debug=True
patronBlock = r'<strong>\s*\w+\s*[Ss]tagione.*?(?P<lang>(?:[Ss][Uu][Bb][-]?)?[Ii][Tt][Aa])(?: in )?(?P<quality>[^<]*)?(?:[^>]+>){4}(?P<block>.*?)/p>' patronBlock = r'<span>\s*\w+\s*[Ss]tagione.*?(?P<lang>(?:[Ss][Uu][Bb][-]?)?[Ii][Tt][Aa])(?: in )?(?P<quality>[^<]*)?(?:[^>]+>){4}(?P<block>.*?)/p>'
patron = r'(?P<season>\d+)&[^:]+;(?P<episode>\d+)(?P<data>.*?)(?:<br|$)' patron = r'(?P<season>\d+)x(?P<episode>\d+)[^>]+>(?P<data>.*?)(?:</table)'
return locals() return locals()
def check(item): def check(item):
item.data = httptools.downloadpage(item.url).data item.data = httptools.downloadpage(item.url).data
if 'rel="tag">Serie TV' in item.data: if 'Stagione' in item.data:
item.contentType = 'tvshow' item.contentType = 'tvshow'
return episodios(item) return episodios(item)
else: else:
+2 -2
View File
@@ -111,9 +111,9 @@ def episodios(item):
data = item.data data = item.data
if '<h6>Streaming</h6>' in data: if '<h6>Streaming</h6>' in data:
patron = r'<td style[^>]+>\s*.*?(?:<span[^>]+)?<strong>(?P<title>[^<]+)<\/strong>.*?<td style[^>]+>\s*<a href="(?P<url>[^"]+)"[^>]+>' patron = r'<td style[^>]+>\s*.*?(?:<span[^>]+)?<strong>(?P<title>[^<]+)<\/strong>.*?<td style[^>]+>\s*<a href="(?P<url>[^"]+)"[^>]+>(?P<episode>\d+)'
else: else:
patron = r'<a\s*href="(?P<url>[^"]+)"\s*title="(?P<title>[^"]+)"\s*class="btn btn-dark mb-1">' patron = r'<a\s*href="(?P<url>[^"]+)"\s*title="(?P<title>[^"]+)"\s*class="btn btn-dark mb-1">(?P<episode>\d+)'
def itemHook(item): def itemHook(item):
support.info(item) support.info(item)
if item.url.startswith('//'): item.url= 'https:' + item.url if item.url.startswith('//'): item.url= 'https:' + item.url
+2 -1
View File
@@ -6,6 +6,7 @@
from lib import js2py from lib import js2py
from core import support from core import support
from platformcode import config from platformcode import config
from platformcode.logger import debug
host = support.config.get_channel_url() host = support.config.get_channel_url()
__channel__ = 'animesaturn' __channel__ = 'animesaturn'
@@ -170,7 +171,7 @@ def check(item):
@support.scrape @support.scrape
def episodios(item): def episodios(item):
if item.contentType != 'movie': anime = True if item.contentType != 'movie': anime = True
patron = r'episodi-link-button">\s*<a href="(?P<url>[^"]+)"[^>]+>\s*(?P<title>[^<]+)</a>' patron = r'episodi-link-button">\s*<a href="(?P<url>[^"]+)"[^>]+>\s*(?P<title>[^\d<]+(?P<episode>\d+))\s*</a>'
return locals() return locals()
+3 -1
View File
@@ -224,7 +224,9 @@ def findvideos(item):
from hashlib import md5 from hashlib import md5
# Calculate Token # Calculate Token
client_ip = support.httptools.downloadpage('https://scws.xyz/videos/{}'.format(item.scws_id), headers=headers).json.get('client_ip') client_ip = support.httptools.downloadpage('https://scws.xyz/videos/{}'.format(item.scws_id), headers=headers).json.get(
'client_ip')
logger.debug(client_ip)
expires = int(time() + 172800) expires = int(time() + 172800)
token = b64encode(md5('{}{} Yc8U6r8KjAKAepEA'.format(expires, client_ip).encode('utf-8')).digest()).decode('utf-8').replace('=', '').replace('+', '-').replace('/', '_') token = b64encode(md5('{}{} Yc8U6r8KjAKAepEA'.format(expires, client_ip).encode('utf-8')).digest()).decode('utf-8').replace('=', '').replace('+', '-').replace('/', '_')
+5 -2
View File
@@ -134,8 +134,11 @@ def episodios(item):
action = 'findvideos' action = 'findvideos'
item.contentType = 'tvshow' item.contentType = 'tvshow'
blacklist = [''] blacklist = ['']
patron = r'(?P<episode>\d+(?:&#215;|×)?\d+\-\d+|\d+(?:&#215;|×)\d+)[;]?(?:(?P<title>[^<]+)<(?P<data>.*?)|(\2[ ])(?:<(\3.*?)))(?:<br />|</p>)' # debug = True
patronBlock = r'<strong>(?P<block>(?:.+?Stagione*.+?(?P<lang>[Ii][Tt][Aa]|[Ss][Uu][Bb][\-]?[iI][tT][aA]))?(?:.+?|</strong>)(/?:</span>)?</p>.*?</p>)' patron = r'"season-no">(?P<season>\d+)x(?P<episode>\d+)(?:[^>]+>){5}\s*(?P<title>[^<]+)(?P<data>.*?)</table>'
# patron = r'(?P<episode>\d+(?:&#215;|×)?\d+\-\d+|\d+(?:&#215;|×)\d+)[;]?(?:(?P<title>[^<]+)<(?P<data>.*?)|(\2[ ])(?:<(\3.*?)))(?:<br />|</p>)'
patronBlock = r'<span>(?:.+?Stagione*.+?(?P<lang>[Ii][Tt][Aa]|[Ss][Uu][Bb][\-]?[iI][tT][aA]))?.*?</span>.*?class="content(?P<block>.*?)(?:"accordion-item|<script>)'
# patronBlock = r'<strong>(?P<block>(?:.+?Stagione*.+?(?P<lang>[Ii][Tt][Aa]|[Ss][Uu][Bb][\-]?[iI][tT][aA]))?(?:.+?|</strong>)(/?:</span>)?</p>.*?</p>)'
return locals() return locals()
+8 -8
View File
@@ -1,10 +1,10 @@
{ {
"id": "cb01anime", "id": "cb01anime",
"name": "Cb01anime", "name": "Cb01anime",
"language": ["ita", "vos", "sub-ita"], "language": ["ita", "vos", "sub-ita"],
"active": true, "active": false,
"thumbnail": "cb01anime.png", "thumbnail": "cb01anime.png",
"banner": "cb01anime.png", "banner": "cb01anime.png",
"categories": ["anime"], "categories": ["anime"],
"settings": [] "settings": []
} }
+21 -18
View File
@@ -97,25 +97,21 @@ def peliculas(item):
@support.scrape @support.scrape
def episodios(item): def episodios(item):
data = item.data data = item.data
# support.dbg()
# debugBlock = True
if item.args == 'anime': if item.args == 'anime':
logger.debug("Anime :", item) logger.debug("Anime :", item)
# blacklist = ['Clipwatching', 'Verystream', 'Easybytez', 'Flix555', 'Cloudvideo']
patron = r'<a target=(?P<url>[^>]+>(?P<title>Episodio\s(?P<episode>\d+))(?::)?(?:(?P<title2>[^<]+))?.*?(?:<br|</p))' patron = r'<a target=(?P<url>[^>]+>(?P<title>Episodio\s(?P<episode>\d+))(?::)?(?:(?P<title2>[^<]+))?.*?(?:<br|</p))'
patronBlock = r'(?:Stagione (?P<season>\d+))?(?:</span><br />|</span></p>|strong></p>)(?P<block>.*?)(?:<div style="margin-left|<span class="txt_dow">)' patronBlock = r'(?:Stagione (?P<season>\d+))?(?:</span><br />|</span></p>|strong></p>)(?P<block>.*?)(?:<div style="margin-left|<span class="txt_dow">)'
item.contentType = 'tvshow' item.contentType = 'tvshow'
elif item.args == 'wrestling': elif item.args == 'sport':
logger.debug("Sport :", item)
logger.debug("Wrestling :", item)
# debugBlock = True
patron = r'(?:/>|<p>)\s*(?P<title>[^-]+)-(?P<data>.+?)(?:<br|</p)' patron = r'(?:/>|<p>)\s*(?P<title>[^-]+)-(?P<data>.+?)(?:<br|</p)'
patronBlock = r'</strong>\s*</p>(?P<block>.*?</p>)' patronBlock = r'</strong>\s*</p>(?P<block>.*?</p>)'
item.contentType = 'tvshow' item.contentType = 'tvshow'
elif item.args == 'serie' or item.contentType == 'tvshow': elif item.args == 'serie' or item.contentType == 'tvshow':
logger.debug("Serie :", item) logger.debug("Serie :", item)
# debugBlock = True # debug=True
patron = r'(?:/>|<p>)\s*(?:(?P<episode>\d+(?:x|×|&#215;)\d+|Puntata \d+)[;]?[ ]?(?P<title>[^<-]+))?(?P<data>.*?)(?:<br|</p)' patron = r'(?:/>|<p>)\s*(?:(?P<episode>\d+(?:x|×|&#215;)\d+|Puntata \d+)(?:-(?P<episode2>\d+))?[;]?[ ]?(?P<title>[^<-]+))?(?P<data>.*?)(?:<br|</p)'
patronBlock = r'Stagione\s(?:[Uu]nica)?(?:(?P<lang>iTA|ITA|Sub-ITA|Sub-iTA))?.*?</strong>(?P<block>.+?)(?:strong>|<div class="at-below)' patronBlock = r'Stagione\s(?:[Uu]nica)?(?:(?P<lang>iTA|ITA|Sub-ITA|Sub-iTA))?.*?</strong>(?P<block>.+?)(?:strong>|<div class="at-below)'
item.contentType = 'tvshow' item.contentType = 'tvshow'
else: else:
@@ -128,8 +124,9 @@ def episodios(item):
def itemlistHook(itl): def itemlistHook(itl):
ret = [] ret = []
if item.args == 'wrestling': if item.args == 'sport':
return itl return itl
# support.dbg()
for it in itl: for it in itl:
ep = scrapertools.find_single_match(it.title, r'(\d+x\d+)') ep = scrapertools.find_single_match(it.title, r'(\d+x\d+)')
if not ep and 'http' in it.data: # stagione intera if not ep and 'http' in it.data: # stagione intera
@@ -211,22 +208,28 @@ def newest(categoria):
def check(item): def check(item):
logger.debug() logger.debug()
data = support.match(item.url, headers=headers).data data = support.match(item.url, headers=headers).data
# support.dbg()
if data: if data:
ck = support.match(data, patron=r'Supportaci condividendo quest[oa] ([^:]+)').match.lower() ck = str(support.match(data, patronBlock=r'Genere:(.*?)</span>', patron=r'tag">([^<]+)').matches).lower()
if ck == 'film':
item.contentType = 'movie'
item.data = data
# item.action = 'findvideos'
return findvideos(item)
elif ck in ['serie tv', 'wrestling wwe', 'anime']: if 'serie tv' in ck or 'anime' in ck:# in ['serie tv', 'wrestling wwe', 'anime']:
if 'anime' in ck:
item.args = 'anime'
elif 'sport' in ck:
item.args = 'sport'
else:
item.args = 'serie'
item.contentType = 'tvshow' item.contentType = 'tvshow'
item.args = ck.split()[0]
item.data = data item.data = data
itemlist = episodios(item) itemlist = episodios(item)
if not itemlist: if not itemlist:
item.data = data item.data = data
return findvideos(item) return findvideos(item)
else:
item.contentType = 'movie'
item.data = data
# item.action = 'findvideos'
return findvideos(item)
return itemlist return itemlist
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "dreamsub", "id": "dreamsub",
"name": "DreamSub", "name": "DreamSub",
"language": ["ita", "sub-ita"], "language": ["ita", "sub-ita"],
"active": true, "active": false,
"thumbnail": "dreamsub.png", "thumbnail": "dreamsub.png",
"banner": "dreamsub.png", "banner": "dreamsub.png",
"categories": ["anime", "vos"], "categories": ["anime", "vos"],
+3 -1
View File
@@ -64,9 +64,11 @@ def newest(categoria):
@support.scrape @support.scrape
def peliculas(item): def peliculas(item):
# debug=True
if item.args == 'last': if item.args == 'last':
patronBlock = r'inseriti:(?P<block>.*?)<div class="block-showmore' patronBlock = r'inseriti:(?P<block>.*?)<div class="block-showmore'
patron = r'item-movie">[^>]+><a href="(?P<url>[^"]+)[^>]+><img data-src="(?P<thumb>[^"]+)(?:[^>]+>){6}(?P<title>[^<]+)(?:[^>]+>){6}(?P<quality>[^<]+)' patron = r'item-movie">[^>]+><a href="(?P<url>[^"]+)[^>]+><img data-src="(?P<thumb>[^"]+)(?:[^>]+>){6}(?P<title>[^<]+)(?:[^>]+>){4}(?P<year>\d+)?(?:[^>]+>){2}(?P<quality>[^<]+)'
# patron = r'item-movie">[^>]+><a href="(?P<url>[^"]+)[^>]+><img data-src="(?P<thumb>[^"]+)(?:[^>]+>){6}(?P<title>[^<]+)(?:[^>]+>){6}(?P<quality>[^<]+)'
patronNext = r'<a href="([^"]+)">&rarr' patronNext = r'<a href="([^"]+)">&rarr'
return locals() return locals()
+2 -2
View File
@@ -44,10 +44,10 @@ def peliculas(item):
@support.scrape @support.scrape
def episodios(item): def episodios(item):
patronBlock = r'<div class="tab-pane fade" id="season-(?P<season>.)"(?P<block>.*?)</div>' patronBlock = r'<div class="tab-pane fade" id="season-(?P<season>.)"(?P<block>.*?)</ul>\s*</div>'
patron = r'<a href="#" allowfullscreen data-link="(?P<url>[^"]+).*?title="(?P<title>[^"]+)(?P<lang>[sS][uU][bB]-?[iI][tT][aA])?\s*">(?P<episode>[^<]+)' patron = r'<a href="#" allowfullscreen data-link="(?P<url>[^"]+).*?title="(?P<title>[^"]+)(?P<lang>[sS][uU][bB]-?[iI][tT][aA])?\s*">(?P<episode>[^<]+)'
action = 'findvideos' action = 'findvideos'
# debug = True # debugBlock = True
return locals() return locals()
+1
View File
@@ -46,6 +46,7 @@ def mainlist(item):
@support.scrape @support.scrape
def peliculas(item): def peliculas(item):
# debug = True # debug = True
action = 'findvideos'
sceneTitle = item.args[2] sceneTitle = item.args[2]
if item.args[1] in ['tvshow', 'anime', 'music', 'other']: if item.args[1] in ['tvshow', 'anime', 'music', 'other']:
patron = r'>[^"<]+' patron = r'>[^"<]+'
+12 -9
View File
@@ -25,16 +25,19 @@ def mainlist(item):
@support.scrape @support.scrape
def peliculas(item): def peliculas(item):
data = item.data if item.text:
if item.args == 'sala': data = support.httptools.downloadpage(host + '/?s=' + item.text, post={'story': item.text, 'do': 'search', 'subaction': 'search'}).data
patronBlock = r'insala(?P<block>.*?)<header>' patron = '<img src="(?P<thumb>[^"]+)(?:[^>]+>){8}\s*<a href="(?P<url>[^"]+)[^>]+>(?P<title>[^<]+)(?:[^>]+>){4}IMDb\s(?P<rating>[^<]+)(?:[^>]+>){2}(?P<year>\d+)'
patron = r'<img src="(?P<thumb>[^"]+)[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>\s*(?P<rating>[^<]+)[^>]+>[^>]+>(?P<quality>[^<]+)[^>]+>[^>]+>[^>]+>[^>]+><a href="(?P<url>[^"]+)">(?P<title>[^<]+)[^>]+>[^>]+>[^>]+>(?P<year>\d{4})'
elif item.args == 'az':
patron = r'<img src="(?P<thumb>[^"]+)[^>]+>[^>]+>[^>]+>[^>]+><a href="(?P<url>[^"]+)[^>]+>(?P<title>[^<]+)<[^>]+>[^>]+>[^>]+>.*?<span class="labelimdb">(?P<rating>[^>]+)<'
else: else:
patron = r'<img src="(?P<thumb>[^"]+)[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>\s*(?P<rating>[^<]+)[^>]+>[^>]+>(?P<quality>[^<]+)[^>]+>[^>]+>[^>]+>[^>]+><a href="(?P<url>[^"]+)">(?P<title>[^<]+)[^>]+>[^>]+>[^>]+>(?P<year>\d{4})[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>\s*(?P<plot>[^<]+)<[^>]+>' if item.args == 'sala':
patronBlock = r'insala(?P<block>.*?)<header>'
patron = r'<img src="(?P<thumb>[^"]+)[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>\s*(?P<rating>[^<]+)[^>]+>[^>]+>(?P<quality>[^<]+)[^>]+>[^>]+>[^>]+>[^>]+><a href="(?P<url>[^"]+)">(?P<title>[^<]+)[^>]+>[^>]+>[^>]+>(?P<year>\d{4})'
elif item.args == 'az':
patron = r'<img src="(?P<thumb>[^"]+)[^>]+>[^>]+>[^>]+>[^>]+><a href="(?P<url>[^"]+)[^>]+>(?P<title>[^<]+)<[^>]+>[^>]+>[^>]+>.*?<span class="labelimdb">(?P<rating>[^>]+)<'
else:
patron = r'<img src="(?P<thumb>[^"]+)[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>\s*(?P<rating>[^<]+)[^>]+>[^>]+>(?P<quality>[^<]+)[^>]+>[^>]+>[^>]+>[^>]+><a href="(?P<url>[^"]+)">(?P<title>[^<]+)[^>]+>[^>]+>[^>]+>(?P<year>\d{4})[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>\s*(?P<plot>[^<]+)<[^>]+>'
patronNext = 'href="([^>]+)"' patronNext = 'href="([^>]+)"'
return locals() return locals()
@@ -56,7 +59,7 @@ def genres(item):
def search(item, text): def search(item, text):
info(text) info(text)
item.data = support.httptools.downloadpage(host + '/?s=' + text, post={'story': text, 'do': 'search', 'subaction': 'search'}).data item.text = text
try: try:
return peliculas(item) return peliculas(item)
except: except:
+6 -7
View File
@@ -92,7 +92,7 @@ def live(item):
plot += '\n\nA Seguire:\n[B]{}[/B]\n{}'.format(guide.get('nextListing', {}).get('mediasetlisting$epgTitle', ''),guide.get('nextListing', {}).get('description', '')) plot += '\n\nA Seguire:\n[B]{}[/B]\n{}'.format(guide.get('nextListing', {}).get('mediasetlisting$epgTitle', ''),guide.get('nextListing', {}).get('description', ''))
itemlist.append(item.clone(title=support.typo(title, 'bold'), itemlist.append(item.clone(title=support.typo(title, 'bold'),
fulltitle=title, callSign=it['callSign'], fulltitle=title, callSign=it['callSign'],
urls=[guide['publicUrl']], # urls=[guide['publicUrl']],
plot=plot, plot=plot,
url=url, url=url,
action='findvideos', action='findvideos',
@@ -243,7 +243,7 @@ def findvideos(item):
lic_url = 'https://widevine.entitlement.theplatform.eu/wv/web/ModularDrm/getRawWidevineLicense?releasePid={pid}&account=http://access.auth.theplatform.com/data/Account/2702976343&schema=1.0&token={token}|Accept=*/*&Content-Type=&User-Agent={ua}|R{{SSM}}|' lic_url = 'https://widevine.entitlement.theplatform.eu/wv/web/ModularDrm/getRawWidevineLicense?releasePid={pid}&account=http://access.auth.theplatform.com/data/Account/2702976343&schema=1.0&token={token}|Accept=*/*&Content-Type=&User-Agent={ua}|R{{SSM}}|'
url = '' url = ''
# support.dbg()
if item.urls: if item.urls:
url = '' url = ''
pid = '' pid = ''
@@ -265,12 +265,12 @@ def findvideos(item):
return support.server(item, itemlist=[item], Download=False, Videolibrary=False) return support.server(item, itemlist=[item], Download=False, Videolibrary=False)
elif item.video_id: elif item.video_id:
payload = '{"contentId":"' + item.video_id + ' ","streamType":"VOD","delivery":"Streaming","createDevice":true}' payload = {"contentId":item.video_id, "streamType":"VOD", "delivery":"Streaming", "createDevice":"true", "overrideAppName":"web//mediasetplay-web/5.2.4-6ad16a4"}
res = session.post('https://api-ott-prod-fe.mediaset.net/PROD/play/playback/check/v2.0?sid=' + sid, data=payload).json()['response']['mediaSelector'] res = session.post('https://api-ott-prod-fe.mediaset.net/PROD/play/playback/check/v2.0?sid=' + sid, json=payload).json()['response']['mediaSelector']
else: else:
payload = '{"channelCode":"' + item.callSign + '","streamType":"LIVE","delivery":"Streaming","createDevice":true}' payload = {"channelCode":item.callSign, "streamType":"LIVE", "delivery":"Streaming", "createDevice":"true", "overrideAppName":"web//mediasetplay-web/5.2.4-6ad16a4"}
res = session.post('https://api-ott-prod-fe.mediaset.net/PROD/play/playback/check/v2.0?sid=' + sid, data=payload).json()['response']['mediaSelector'] res = session.post('https://api-ott-prod-fe.mediaset.net/PROD/play/playback/check/v2.0?sid=' + sid, json=payload).json()['response']['mediaSelector']
url = res['url'] url = res['url']
mpd = True if 'dash' in res['formats'].lower() else False mpd = True if 'dash' in res['formats'].lower() else False
@@ -292,7 +292,6 @@ def findvideos(item):
def get_from_id(item): def get_from_id(item):
logger.debug()
sessionKey = session.get(sessionUrl.format(uuid=str(uuid.uuid4())), verify=False).json()['sessionKey'] sessionKey = session.get(sessionUrl.format(uuid=str(uuid.uuid4())), verify=False).json()['sessionKey']
session.headers.update({'x-session': sessionKey}) session.headers.update({'x-session': sessionKey})
res = session.get(entry.format(id=item.args)).json() res = session.get(entry.format(id=item.args)).json()
+2 -1
View File
@@ -252,7 +252,8 @@ def play(item):
return [] return []
# Calculate Token # Calculate Token
client_ip = httptools.downloadpage('https://api.ipify.org/').data client_ip = support.httptools.downloadpage('https://scws.xyz/videos/{}'.format(scws_id), headers=headers).json.get('client_ip')
logger.debug(client_ip)
expires = int(time() + 172800) expires = int(time() + 172800)
token = b64encode(md5('{}{} Yc8U6r8KjAKAepEA'.format(expires, client_ip).encode('utf-8')).digest()).decode('utf-8').replace('=', '').replace('+', '-').replace('/', '_') token = b64encode(md5('{}{} Yc8U6r8KjAKAepEA'.format(expires, client_ip).encode('utf-8')).digest()).decode('utf-8').replace('=', '').replace('+', '-').replace('/', '_')
+7 -7
View File
@@ -1,11 +1,11 @@
{ {
"id": "tantifilm", "id": "tantifilm",
"name": "Tantifilm", "name": "Tantifilm",
"language": ["ita"], "language": ["ita"],
"active": true, "active": false,
"thumbnail": "tantifilm.png", "thumbnail": "tantifilm.png",
"banner": "tantifilm.png", "banner": "tantifilm.png",
"categories": ["tvshow", "movie", "anime"], "categories": ["tvshow", "movie", "anime"],
"not_active":["include_in_newest_anime", "include_in_newest_peliculas"], "not_active": ["include_in_newest_anime", "include_in_newest_peliculas"],
"settings": [] "settings": []
} }
+9 -6
View File
@@ -1,10 +1,12 @@
from platformcode import config, logger from platformcode import config, logger
import xbmc, sys, xbmcgui, os import xbmc, sys, xbmcgui, os
librerias = xbmc.translatePath(os.path.join(config.get_runtime_path(), 'lib')) librerias = xbmc.translatePath(os.path.join(config.get_runtime_path(), 'lib'))
sys.path.insert(0, librerias) sys.path.insert(0, librerias)
from core import jsontools, support from core import jsontools, support
from core.item import Item
addon_id = config.get_addon_core().getAddonInfo('id') addon_id = config.get_addon_core().getAddonInfo('id')
@@ -15,6 +17,7 @@ f.close()
def build_menu(): def build_menu():
# from core.support import dbg;dbg()
tmdbid = xbmc.getInfoLabel('ListItem.Property(tmdb_id)') tmdbid = xbmc.getInfoLabel('ListItem.Property(tmdb_id)')
mediatype = xbmc.getInfoLabel('ListItem.DBTYPE') mediatype = xbmc.getInfoLabel('ListItem.DBTYPE')
title = xbmc.getInfoLabel('ListItem.Title') title = xbmc.getInfoLabel('ListItem.Title')
@@ -24,9 +27,9 @@ def build_menu():
containerPath = xbmc.getInfoLabel('Container.FolderPath') containerPath = xbmc.getInfoLabel('Container.FolderPath')
logstr = "Selected ListItem is: 'IMDB: {}' - TMDB: {}' - 'Title: {}' - 'Year: {}'' - 'Type: {}'".format(imdb, tmdbid, title, year, mediatype) logstr = "Selected ListItem is: 'IMDB: {}' - TMDB: {}' - 'Title: {}' - 'Year: {}'' - 'Type: {}'".format(imdb, tmdbid, title, year, mediatype)
logger.info(logstr) logger.debug(logstr)
logger.info(filePath) logger.debug(filePath)
logger.info(containerPath) logger.debug(containerPath)
contextmenuitems = [] contextmenuitems = []
contextmenuactions = [] contextmenuactions = []
@@ -35,20 +38,20 @@ def build_menu():
logger.debug('check contextmenu', itemmodule) logger.debug('check contextmenu', itemmodule)
module = __import__(itemmodule, None, None, [itemmodule]) module = __import__(itemmodule, None, None, [itemmodule])
logger.info('Add contextmenu item ->', itemmodule) logger.debug('Add contextmenu item ->', itemmodule)
module_item_actions = module.get_menu_items() module_item_actions = module.get_menu_items()
contextmenuitems.extend([item for item, fn in module_item_actions]) contextmenuitems.extend([item for item, fn in module_item_actions])
contextmenuactions.extend([fn for item, fn in module_item_actions]) contextmenuactions.extend([fn for item, fn in module_item_actions])
if len(contextmenuitems) == 0: if len(contextmenuitems) == 0:
logger.info('No contextmodule found, build an empty one') logger.debug('No contextmodule found, build an empty one')
contextmenuitems.append(empty_item()) contextmenuitems.append(empty_item())
contextmenuactions.append(lambda: None) contextmenuactions.append(lambda: None)
ret = xbmcgui.Dialog().contextmenu(contextmenuitems) ret = xbmcgui.Dialog().contextmenu(contextmenuitems)
if ret > -1: if ret > -1:
logger.info('Contextmenu module index', ret, ', label=' + contextmenuitems[ret]) logger.debug('Contextmenu module index', ret, ', label=' + contextmenuitems[ret])
contextmenuactions[ret]() contextmenuactions[ret]()
+4 -1
View File
@@ -66,6 +66,9 @@ class CipherSuiteAdapter(host_header_ssl.HostHeaderSSLAdapter):
try: try:
ip = doh.query(domain)[0] ip = doh.query(domain)[0]
logger.info('Query DoH: ' + domain + ' = ' + str(ip)) logger.info('Query DoH: ' + domain + ' = ' + str(ip))
# IPv6 address
if ':' in ip:
ip = '[' + ip + ']'
self.writeToCache(domain, ip) self.writeToCache(domain, ip)
except Exception: except Exception:
logger.error('Failed to resolve hostname, fallback to normal dns') logger.error('Failed to resolve hostname, fallback to normal dns')
@@ -96,7 +99,7 @@ class CipherSuiteAdapter(host_header_ssl.HostHeaderSSLAdapter):
domain = parse.netloc domain = parse.netloc
else: else:
raise requests.exceptions.URLRequired raise requests.exceptions.URLRequired
if not scrapertools.find_single_match(domain, '\d+\.\d+\.\d+\.\d+'): if not scrapertools.find_single_match(domain, '\d+\.\d+\.\d+\.\d+') and ':' not in domain:
ip = self.getIp(domain) ip = self.getIp(domain)
else: else:
ip = None ip = None
+7 -1
View File
@@ -1033,7 +1033,13 @@ class Tmdb(object):
if len(results) > 1: if len(results) > 1:
from lib.fuzzy_match import algorithims from lib.fuzzy_match import algorithims
if self.search_type == 'multi': if self.search_type == 'multi':
results.sort(key=lambda r: algorithims.trigram(text_simple, r.get('name', '') if r.get('media_type') == 'tv' else r.get('title', '')), reverse=True) if self.search_year:
for r in results:
if (r.get('release_date', '') and r.get('release_date', '')[:4] == self.search_year) or (r.get('first_air_date', '') and r.get('first_air_date', '')[:4] == self.search_year):
results = [r]
break
if len(results) > 1:
results.sort(key=lambda r: algorithims.trigram(text_simple, r.get('name', '') if r.get('media_type') == 'tv' else r.get('title', '')), reverse=True)
else: else:
results.sort(key=lambda r: algorithims.trigram(text_simple, r.get('name', '') if self.search_type == 'tv' else r.get('title', '')), reverse=True) results.sort(key=lambda r: algorithims.trigram(text_simple, r.get('name', '') if self.search_type == 'tv' else r.get('title', '')), reverse=True)
+78 -520
View File
@@ -1,20 +1,14 @@
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
import logging import logging
import re
import requests import requests
import sys import sys
import ssl import ssl
from collections import OrderedDict
from copy import deepcopy
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from requests.sessions import Session from requests.sessions import Session
from requests_toolbelt.utils import dump from requests_toolbelt.utils import dump
from time import sleep
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
try: try:
@@ -28,37 +22,23 @@ except ImportError:
import copy_reg as copyreg import copy_reg as copyreg
try: try:
from HTMLParser import HTMLParser from urlparse import urlparse
except ImportError: except ImportError:
if sys.version_info >= (3, 4): from urllib.parse import urlparse
import html
else:
from html.parser import HTMLParser
try:
from urlparse import urlparse, urljoin
except ImportError:
from urllib.parse import urlparse, urljoin
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
from .exceptions import ( from .exceptions import (
CloudflareLoopProtection, CloudflareLoopProtection,
CloudflareCode1020, CloudflareIUAMError
CloudflareIUAMError,
CloudflareSolveError,
CloudflareChallengeError,
CloudflareCaptchaError,
CloudflareCaptchaProvider
) )
from .interpreters import JavaScriptInterpreter from .cloudflare import Cloudflare
from .captcha import Captcha
from .user_agent import User_Agent from .user_agent import User_Agent
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
__version__ = '1.2.58' __version__ = '1.2.62'
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
@@ -79,6 +59,8 @@ class CipherSuiteAdapter(HTTPAdapter):
self.ssl_context = kwargs.pop('ssl_context', None) self.ssl_context = kwargs.pop('ssl_context', None)
self.cipherSuite = kwargs.pop('cipherSuite', None) self.cipherSuite = kwargs.pop('cipherSuite', None)
self.source_address = kwargs.pop('source_address', None) self.source_address = kwargs.pop('source_address', None)
self.server_hostname = kwargs.pop('server_hostname', None)
self.ecdhCurve = kwargs.pop('ecdhCurve', 'prime256v1')
if self.source_address: if self.source_address:
if isinstance(self.source_address, str): if isinstance(self.source_address, str):
@@ -91,14 +73,32 @@ class CipherSuiteAdapter(HTTPAdapter):
if not self.ssl_context: if not self.ssl_context:
self.ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) self.ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
self.ssl_context.orig_wrap_socket = self.ssl_context.wrap_socket
self.ssl_context.wrap_socket = self.wrap_socket
if self.server_hostname:
self.ssl_context.server_hostname = self.server_hostname
self.ssl_context.set_ciphers(self.cipherSuite) self.ssl_context.set_ciphers(self.cipherSuite)
self.ssl_context.set_ecdh_curve('prime256v1') self.ssl_context.set_ecdh_curve(self.ecdhCurve)
self.ssl_context.options |= (ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1) self.ssl_context.options |= (ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 | ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1)
super(CipherSuiteAdapter, self).__init__(**kwargs) super(CipherSuiteAdapter, self).__init__(**kwargs)
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
def wrap_socket(self, *args, **kwargs):
if hasattr(self.ssl_context, 'server_hostname') and self.ssl_context.server_hostname:
kwargs['server_hostname'] = self.ssl_context.server_hostname
self.ssl_context.check_hostname = False
else:
self.ssl_context.check_hostname = True
return self.ssl_context.orig_wrap_socket(*args, **kwargs)
# ------------------------------------------------------------------------------- #
def init_poolmanager(self, *args, **kwargs): def init_poolmanager(self, *args, **kwargs):
kwargs['ssl_context'] = self.ssl_context kwargs['ssl_context'] = self.ssl_context
kwargs['source_address'] = self.source_address kwargs['source_address'] = self.source_address
@@ -118,15 +118,21 @@ class CloudScraper(Session):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.debug = kwargs.pop('debug', False) self.debug = kwargs.pop('debug', False)
self.disableCloudflareV1 = kwargs.pop('disableCloudflareV1', False)
self.delay = kwargs.pop('delay', None) self.delay = kwargs.pop('delay', None)
self.cipherSuite = kwargs.pop('cipherSuite', None)
self.ssl_context = kwargs.pop('ssl_context', None)
self.interpreter = kwargs.pop('interpreter', 'native')
self.captcha = kwargs.pop('captcha', {}) self.captcha = kwargs.pop('captcha', {})
self.doubleDown = kwargs.pop('doubleDown', True)
self.interpreter = kwargs.pop('interpreter', 'native')
self.requestPreHook = kwargs.pop('requestPreHook', None) self.requestPreHook = kwargs.pop('requestPreHook', None)
self.requestPostHook = kwargs.pop('requestPostHook', None) self.requestPostHook = kwargs.pop('requestPostHook', None)
self.cipherSuite = kwargs.pop('cipherSuite', None)
self.ecdhCurve = kwargs.pop('ecdhCurve', 'prime256v1')
self.source_address = kwargs.pop('source_address', None) self.source_address = kwargs.pop('source_address', None)
self.doubleDown = kwargs.pop('doubleDown', True) self.server_hostname = kwargs.pop('server_hostname', None)
self.ssl_context = kwargs.pop('ssl_context', None)
self.allow_brotli = kwargs.pop( self.allow_brotli = kwargs.pop(
'allow_brotli', 'allow_brotli',
@@ -159,8 +165,10 @@ class CloudScraper(Session):
'https://', 'https://',
CipherSuiteAdapter( CipherSuiteAdapter(
cipherSuite=self.cipherSuite, cipherSuite=self.cipherSuite,
ssl_context=self.ssl_context, ecdhCurve=self.ecdhCurve,
source_address=self.source_address server_hostname=self.server_hostname,
source_address=self.source_address,
ssl_context=self.ssl_context
) )
) )
@@ -199,21 +207,7 @@ class CloudScraper(Session):
try: try:
print(dump.dump_all(req).decode('utf-8', errors='backslashreplace')) print(dump.dump_all(req).decode('utf-8', errors='backslashreplace'))
except ValueError as e: except ValueError as e:
print("Debug Error: {}".format(getattr(e, 'message', e))) print(f"Debug Error: {getattr(e, 'message', e)}")
# ------------------------------------------------------------------------------- #
# Unescape / decode html entities
# ------------------------------------------------------------------------------- #
@staticmethod
def unescape(html_text):
if sys.version_info >= (3, 0):
if sys.version_info >= (3, 4):
return html.unescape(html_text)
return HTMLParser().unescape(html_text)
return HTMLParser().unescape(html_text)
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
# Decode Brotli on older versions of urllib3 manually # Decode Brotli on older versions of urllib3 manually
@@ -225,10 +219,10 @@ class CloudScraper(Session):
resp._content = brotli.decompress(resp.content) resp._content = brotli.decompress(resp.content)
else: else:
logging.warning( logging.warning(
'You\'re running urllib3 {}, Brotli content detected, ' f'You\'re running urllib3 {requests.packages.urllib3.__version__}, Brotli content detected, '
'Which requires manual decompression, ' 'Which requires manual decompression, '
'But option allow_brotli is set to False, ' 'But option allow_brotli is set to False, '
'We will not continue to decompress.'.format(requests.packages.urllib3.__version__) 'We will not continue to decompress.'
) )
return resp return resp
@@ -275,480 +269,44 @@ class CloudScraper(Session):
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
if self.requestPostHook: if self.requestPostHook:
response = self.requestPostHook(self, response) newResponse = self.requestPostHook(self, response)
if self.debug: if response != newResponse: # Give me walrus in 3.7!!!
self.debugRequest(response) response = newResponse
if self.debug:
print('==== requestPostHook Debug ====')
self.debugRequest(response)
# ------------------------------------------------------------------------------- #
if not self.disableCloudflareV1:
cloudflareV1 = Cloudflare(self)
# Check if Cloudflare anti-bot is on
if self.is_Challenge_Request(response):
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
# Try to solve the challenge and send it back # Check if Cloudflare v1 anti-bot is on
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
if self._solveDepthCnt >= self.solveDepth: if cloudflareV1.is_Challenge_Request(response):
_ = self._solveDepthCnt # ------------------------------------------------------------------------------- #
self.simpleException( # Try to solve the challenge and send it back
CloudflareLoopProtection, # ------------------------------------------------------------------------------- #
"!!Loop Protection!! We have tried to solve {} time(s) in a row.".format(_)
)
self._solveDepthCnt += 1 if self._solveDepthCnt >= self.solveDepth:
_ = self._solveDepthCnt
self.simpleException(
CloudflareLoopProtection,
f"!!Loop Protection!! We have tried to solve {_} time(s) in a row."
)
response = self.Challenge_Response(response, **kwargs) self._solveDepthCnt += 1
else:
if not response.is_redirect and response.status_code not in [429, 503]: response = cloudflareV1.Challenge_Response(response, **kwargs)
self._solveDepthCnt = 0 else:
if not response.is_redirect and response.status_code not in [429, 503]:
self._solveDepthCnt = 0
return response return response
# ------------------------------------------------------------------------------- #
# check if the response contains a valid Cloudflare Bot Fight Mode challenge
# ------------------------------------------------------------------------------- #
@staticmethod
def is_BFM_Challenge(resp):
try:
return (
resp.headers.get('Server', '').startswith('cloudflare')
and re.search(
r"\/cdn-cgi\/bm\/cv\/\d+\/api\.js.*?"
r"window\['__CF\$cv\$params'\]\s*=\s*{",
resp.text,
re.M | re.S
)
)
except AttributeError:
pass
return False
# ------------------------------------------------------------------------------- #
# check if the response contains a valid Cloudflare challenge
# ------------------------------------------------------------------------------- #
@staticmethod
def is_IUAM_Challenge(resp):
try:
return (
resp.headers.get('Server', '').startswith('cloudflare')
and resp.status_code in [429, 503]
and re.search(
r'<form .*?="challenge-form" action="/.*?__cf_chl_jschl_tk__=\S+"',
resp.text,
re.M | re.S
)
)
except AttributeError:
pass
return False
# ------------------------------------------------------------------------------- #
# check if the response contains new Cloudflare challenge
# ------------------------------------------------------------------------------- #
@staticmethod
def is_New_IUAM_Challenge(resp):
try:
return (
resp.headers.get('Server', '').startswith('cloudflare')
and resp.status_code in [429, 503]
and re.search(
r'cpo.src\s*=\s*"/cdn-cgi/challenge-platform/\S+orchestrate/jsch/v1',
resp.text,
re.M | re.S
)
and re.search(r'window._cf_chl_enter\s*[\(=]', resp.text, re.M | re.S)
)
except AttributeError:
pass
return False
# ------------------------------------------------------------------------------- #
# check if the response contains a v2 hCaptcha Cloudflare challenge
# ------------------------------------------------------------------------------- #
@staticmethod
def is_New_Captcha_Challenge(resp):
try:
return (
CloudScraper.is_Captcha_Challenge(resp)
and re.search(
r'cpo.src\s*=\s*"/cdn-cgi/challenge-platform/\S+orchestrate/captcha/v1',
resp.text,
re.M | re.S
)
and re.search(r'\s*id="trk_captcha_js"', resp.text, re.M | re.S)
)
except AttributeError:
pass
return False
# ------------------------------------------------------------------------------- #
# check if the response contains a Cloudflare hCaptcha challenge
# ------------------------------------------------------------------------------- #
@staticmethod
def is_Captcha_Challenge(resp):
try:
return (
resp.headers.get('Server', '').startswith('cloudflare')
and resp.status_code == 403
and re.search(
r'action="/\S+__cf_chl_captcha_tk__=\S+',
resp.text,
re.M | re.DOTALL
)
)
except AttributeError:
pass
return False
# ------------------------------------------------------------------------------- #
# check if the response contains Firewall 1020 Error
# ------------------------------------------------------------------------------- #
@staticmethod
def is_Firewall_Blocked(resp):
try:
return (
resp.headers.get('Server', '').startswith('cloudflare')
and resp.status_code == 403
and re.search(
r'<span class="cf-error-code">1020</span>',
resp.text,
re.M | re.DOTALL
)
)
except AttributeError:
pass
return False
# ------------------------------------------------------------------------------- #
# Wrapper for is_Captcha_Challenge, is_IUAM_Challenge, is_Firewall_Blocked
# ------------------------------------------------------------------------------- #
def is_Challenge_Request(self, resp):
if self.is_Firewall_Blocked(resp):
self.simpleException(
CloudflareCode1020,
'Cloudflare has blocked this request (Code 1020 Detected).'
)
if self.is_New_Captcha_Challenge(resp):
self.simpleException(
CloudflareChallengeError,
'Detected a Cloudflare version 2 Captcha challenge, This feature is not available in the opensource (free) version.'
)
if self.is_New_IUAM_Challenge(resp):
self.simpleException(
CloudflareChallengeError,
'Detected a Cloudflare version 2 challenge, This feature is not available in the opensource (free) version.'
)
if self.is_Captcha_Challenge(resp) or self.is_IUAM_Challenge(resp):
if self.debug:
print('Detected a Cloudflare version 1 challenge.')
return True
return False
# ------------------------------------------------------------------------------- #
# Try to solve cloudflare javascript challenge.
# ------------------------------------------------------------------------------- #
def IUAM_Challenge_Response(self, body, url, interpreter):
try:
formPayload = re.search(
r'<form (?P<form>.*?="challenge-form" '
r'action="(?P<challengeUUID>.*?'
r'__cf_chl_jschl_tk__=\S+)"(.*?)</form>)',
body,
re.M | re.DOTALL
).groupdict()
if not all(key in formPayload for key in ['form', 'challengeUUID']):
self.simpleException(
CloudflareIUAMError,
"Cloudflare IUAM detected, unfortunately we can't extract the parameters correctly."
)
payload = OrderedDict()
for challengeParam in re.findall(r'^\s*<input\s(.*?)/>', formPayload['form'], re.M | re.S):
inputPayload = dict(re.findall(r'(\S+)="(\S+)"', challengeParam))
if inputPayload.get('name') in ['r', 'jschl_vc', 'pass']:
payload.update({inputPayload['name']: inputPayload['value']})
except AttributeError:
self.simpleException(
CloudflareIUAMError,
"Cloudflare IUAM detected, unfortunately we can't extract the parameters correctly."
)
hostParsed = urlparse(url)
try:
payload['jschl_answer'] = JavaScriptInterpreter.dynamicImport(
interpreter
).solveChallenge(body, hostParsed.netloc)
except Exception as e:
self.simpleException(
CloudflareIUAMError,
"Unable to parse Cloudflare anti-bots page: {}".format(getattr(e, 'message', e))
)
return {
'url': "{}://{}{}".format(hostParsed.scheme, hostParsed.netloc, self.unescape(formPayload['challengeUUID'])),
'data': payload
}
# ------------------------------------------------------------------------------- #
# Try to solve the Captcha challenge via 3rd party.
# ------------------------------------------------------------------------------- #
def captcha_Challenge_Response(self, provider, provider_params, body, url):
try:
formPayload = re.search(
r'<form (?P<form>.*?="challenge-form" '
r'action="(?P<challengeUUID>.*?__cf_chl_captcha_tk__=\S+)"(.*?)</form>)',
body,
re.M | re.DOTALL
).groupdict()
if not all(key in formPayload for key in ['form', 'challengeUUID']):
self.simpleException(
CloudflareCaptchaError,
"Cloudflare Captcha detected, unfortunately we can't extract the parameters correctly."
)
payload = OrderedDict(
re.findall(
r'(name="r"\svalue|data-ray|data-sitekey|name="cf_captcha_kind"\svalue)="(.*?)"',
formPayload['form']
)
)
captchaType = 'reCaptcha' if payload['name="cf_captcha_kind" value'] == 're' else 'hCaptcha'
except (AttributeError, KeyError):
self.simpleException(
CloudflareCaptchaError,
"Cloudflare Captcha detected, unfortunately we can't extract the parameters correctly."
)
# ------------------------------------------------------------------------------- #
# Pass proxy parameter to provider to solve captcha.
# ------------------------------------------------------------------------------- #
if self.proxies and self.proxies != self.captcha.get('proxy'):
self.captcha['proxy'] = self.proxies
# ------------------------------------------------------------------------------- #
# Pass User-Agent if provider supports it to solve captcha.
# ------------------------------------------------------------------------------- #
self.captcha['User-Agent'] = self.headers['User-Agent']
# ------------------------------------------------------------------------------- #
# Submit job to provider to request captcha solve.
# ------------------------------------------------------------------------------- #
captchaResponse = Captcha.dynamicImport(
provider.lower()
).solveCaptcha(
captchaType,
url,
payload['data-sitekey'],
provider_params
)
# ------------------------------------------------------------------------------- #
# Parse and handle the response of solved captcha.
# ------------------------------------------------------------------------------- #
dataPayload = OrderedDict([
('r', payload.get('name="r" value', '')),
('cf_captcha_kind', payload['name="cf_captcha_kind" value']),
('id', payload.get('data-ray')),
('g-recaptcha-response', captchaResponse)
])
if captchaType == 'hCaptcha':
dataPayload.update({'h-captcha-response': captchaResponse})
hostParsed = urlparse(url)
return {
'url': "{}://{}{}".format(hostParsed.scheme, hostParsed.netloc, self.unescape(formPayload['challengeUUID'])),
'data': dataPayload
}
# ------------------------------------------------------------------------------- #
# Attempt to handle and send the challenge response back to cloudflare
# ------------------------------------------------------------------------------- #
def Challenge_Response(self, resp, **kwargs):
if self.is_Captcha_Challenge(resp):
# ------------------------------------------------------------------------------- #
# double down on the request as some websites are only checking
# if cfuid is populated before issuing Captcha.
# ------------------------------------------------------------------------------- #
if self.doubleDown:
resp = self.decodeBrotli(
self.perform_request(resp.request.method, resp.url, **kwargs)
)
if not self.is_Captcha_Challenge(resp):
return resp
# ------------------------------------------------------------------------------- #
# if no captcha provider raise a runtime error.
# ------------------------------------------------------------------------------- #
if not self.captcha or not isinstance(self.captcha, dict) or not self.captcha.get('provider'):
self.simpleException(
CloudflareCaptchaProvider,
"Cloudflare Captcha detected, unfortunately you haven't loaded an anti Captcha provider "
"correctly via the 'captcha' parameter."
)
# ------------------------------------------------------------------------------- #
# if provider is return_response, return the response without doing anything.
# ------------------------------------------------------------------------------- #
if self.captcha.get('provider') == 'return_response':
return resp
# ------------------------------------------------------------------------------- #
# Submit request to parser wrapper to solve captcha
# ------------------------------------------------------------------------------- #
submit_url = self.captcha_Challenge_Response(
self.captcha.get('provider'),
self.captcha,
resp.text,
resp.url
)
else:
# ------------------------------------------------------------------------------- #
# Cloudflare requires a delay before solving the challenge
# ------------------------------------------------------------------------------- #
if not self.delay:
try:
delay = float(
re.search(
r'submit\(\);\r?\n\s*},\s*([0-9]+)',
resp.text
).group(1)
) / float(1000)
if isinstance(delay, (int, float)):
self.delay = delay
except (AttributeError, ValueError):
self.simpleException(
CloudflareIUAMError,
"Cloudflare IUAM possibility malformed, issue extracing delay value."
)
sleep(self.delay)
# ------------------------------------------------------------------------------- #
submit_url = self.IUAM_Challenge_Response(
resp.text,
resp.url,
self.interpreter
)
# ------------------------------------------------------------------------------- #
# Send the Challenge Response back to Cloudflare
# ------------------------------------------------------------------------------- #
if submit_url:
def updateAttr(obj, name, newValue):
try:
obj[name].update(newValue)
return obj[name]
except (AttributeError, KeyError):
obj[name] = {}
obj[name].update(newValue)
return obj[name]
cloudflare_kwargs = deepcopy(kwargs)
cloudflare_kwargs['allow_redirects'] = False
cloudflare_kwargs['data'] = updateAttr(
cloudflare_kwargs,
'data',
submit_url['data']
)
urlParsed = urlparse(resp.url)
cloudflare_kwargs['headers'] = updateAttr(
cloudflare_kwargs,
'headers',
{
'Origin': '{}://{}'.format(urlParsed.scheme, urlParsed.netloc),
'Referer': resp.url
}
)
challengeSubmitResponse = self.request(
'POST',
submit_url['url'],
**cloudflare_kwargs
)
if challengeSubmitResponse.status_code == 400:
self.simpleException(
CloudflareSolveError,
'Invalid challenge answer detected, Cloudflare broken?'
)
# ------------------------------------------------------------------------------- #
# Return response if Cloudflare is doing content pass through instead of 3xx
# else request with redirect URL also handle protocol scheme change http -> https
# ------------------------------------------------------------------------------- #
if not challengeSubmitResponse.is_redirect:
return challengeSubmitResponse
else:
cloudflare_kwargs = deepcopy(kwargs)
cloudflare_kwargs['headers'] = updateAttr(
cloudflare_kwargs,
'headers',
{'Referer': challengeSubmitResponse.url}
)
if not urlparse(challengeSubmitResponse.headers['Location']).netloc:
redirect_location = urljoin(
challengeSubmitResponse.url,
challengeSubmitResponse.headers['Location']
)
else:
redirect_location = challengeSubmitResponse.headers['Location']
return self.request(
resp.request.method,
redirect_location,
**cloudflare_kwargs
)
# ------------------------------------------------------------------------------- #
# We shouldn't be here...
# Re-request the original query and/or process again....
# ------------------------------------------------------------------------------- #
return self.request(resp.request.method, resp.url, **kwargs)
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
@classmethod @classmethod
@@ -761,7 +319,7 @@ class CloudScraper(Session):
if sess: if sess:
for attr in ['auth', 'cert', 'cookies', 'headers', 'hooks', 'params', 'proxies', 'data']: for attr in ['auth', 'cert', 'cookies', 'headers', 'hooks', 'params', 'proxies', 'data']:
val = getattr(sess, attr, None) val = getattr(sess, attr, None)
if val: if val is not None:
setattr(scraper, attr, val) setattr(scraper, attr, val)
return scraper return scraper
@@ -782,7 +340,7 @@ class CloudScraper(Session):
'doubleDown', 'doubleDown',
'captcha', 'captcha',
'interpreter', 'interpreter',
'source_address' 'source_address',
'requestPreHook', 'requestPreHook',
'requestPostHook' 'requestPostHook'
] if field in kwargs ] if field in kwargs
@@ -793,7 +351,7 @@ class CloudScraper(Session):
resp = scraper.get(url, **kwargs) resp = scraper.get(url, **kwargs)
resp.raise_for_status() resp.raise_for_status()
except Exception: except Exception:
logging.error('"{}" returned an error. Could not collect tokens.'.format(url)) logging.error(f'"{url}" returned an error. Could not collect tokens.')
raise raise
domain = urlparse(resp.url).netloc domain = urlparse(resp.url).netloc
@@ -801,11 +359,12 @@ class CloudScraper(Session):
cookie_domain = None cookie_domain = None
for d in scraper.cookies.list_domains(): for d in scraper.cookies.list_domains():
if d.startswith('.') and d in ('.{}'.format(domain)): if d.startswith('.') and d in (f'.{domain}'):
cookie_domain = d cookie_domain = d
break break
else: else:
cls.simpleException( cls.simpleException(
cls,
CloudflareIUAMError, CloudflareIUAMError,
"Unable to find Cloudflare cookies. Does the site actually " "Unable to find Cloudflare cookies. Does the site actually "
"have Cloudflare IUAM (I'm Under Attack Mode) enabled?" "have Cloudflare IUAM (I'm Under Attack Mode) enabled?"
@@ -813,7 +372,6 @@ class CloudScraper(Session):
return ( return (
{ {
'__cfduid': scraper.cookies.get('__cfduid', '', domain=cookie_domain),
'cf_clearance': scraper.cookies.get('cf_clearance', '', domain=cookie_domain) 'cf_clearance': scraper.cookies.get('cf_clearance', '', domain=cookie_domain)
}, },
scraper.headers['User-Agent'] scraper.headers['User-Agent']
@@ -834,9 +392,9 @@ class CloudScraper(Session):
if ssl.OPENSSL_VERSION_INFO < (1, 1, 1): if ssl.OPENSSL_VERSION_INFO < (1, 1, 1):
print( print(
"DEPRECATION: The OpenSSL being used by this python install ({}) does not meet the minimum supported " f"DEPRECATION: The OpenSSL being used by this python install ({ssl.OPENSSL_VERSION}) does not meet the minimum supported "
"version (>= OpenSSL 1.1.1) in order to support TLS 1.3 required by Cloudflare, " "version (>= OpenSSL 1.1.1) in order to support TLS 1.3 required by Cloudflare, "
"You may encounter an unexpected Captcha or cloudflare 1020 blocks.".format(ssl.OPENSSL_VERSION) "You may encounter an unexpected Captcha or cloudflare 1020 blocks."
) )
# ------------------------------------------------------------------------------- # # ------------------------------------------------------------------------------- #
+3 -3
View File
@@ -103,7 +103,7 @@ class captchaSolver(Captcha):
response = polling2.poll( response = polling2.poll(
lambda: self.session.get( lambda: self.session.get(
'{}/res.php'.format(self.host), f'{self.host}/res.php',
params={ params={
'key': self.api_key, 'key': self.api_key,
'action': 'reportbad', 'action': 'reportbad',
@@ -138,7 +138,7 @@ class captchaSolver(Captcha):
response = polling2.poll( response = polling2.poll(
lambda: self.session.get( lambda: self.session.get(
'{}/res.php'.format(self.host), f'{self.host}/res.php',
params={ params={
'key': self.api_key, 'key': self.api_key,
'action': 'get', 'action': 'get',
@@ -195,7 +195,7 @@ class captchaSolver(Captcha):
response = polling2.poll( response = polling2.poll(
lambda: self.session.post( lambda: self.session.post(
'{}/in.php'.format(self.host), f'{self.host}/in.php',
data=data, data=data,
allow_redirects=False, allow_redirects=False,
timeout=30 timeout=30
+1 -1
View File
@@ -36,7 +36,7 @@ class captchaSolver(reCaptcha):
def checkErrorStatus(response): def checkErrorStatus(response):
if response.status_code in [500, 502]: if response.status_code in [500, 502]:
raise reCaptchaServiceUnavailable( raise reCaptchaServiceUnavailable(
'9kw: Server Side Error {}'.format(response.status_code) f'9kw: Server Side Error {response.status_code}'
) )
error_codes = { error_codes = {
+2 -2
View File
@@ -25,12 +25,12 @@ class Captcha(ABC):
def dynamicImport(cls, name): def dynamicImport(cls, name):
if name not in captchaSolvers: if name not in captchaSolvers:
try: try:
__import__('{}.{}'.format(cls.__module__, name)) __import__(f'{cls.__module__}.{name}')
if not isinstance(captchaSolvers.get(name), Captcha): if not isinstance(captchaSolvers.get(name), Captcha):
raise ImportError('The anti captcha provider was not initialized.') raise ImportError('The anti captcha provider was not initialized.')
except ImportError as e: except ImportError as e:
sys.tracebacklimit = 0 sys.tracebacklimit = 0
logging.error('Unable to load {} anti captcha provider -> {}'.format(name, e)) logging.error(f'Unable to load {name} anti captcha provider -> {e}')
raise raise
return captchaSolvers[name] return captchaSolvers[name]
+5 -5
View File
@@ -36,7 +36,7 @@ class captchaSolver(Captcha):
def checkErrorStatus(response): def checkErrorStatus(response):
if response.status_code in [500, 502]: if response.status_code in [500, 502]:
raise CaptchaServiceUnavailable( raise CaptchaServiceUnavailable(
'CapMonster: Server Side Error {}'.format(response.status_code) f'CapMonster: Server Side Error {response.status_code}'
) )
payload = response.json() payload = response.json()
@@ -66,7 +66,7 @@ class captchaSolver(Captcha):
response = polling2.poll( response = polling2.poll(
lambda: self.session.post( lambda: self.session.post(
'{}/getTaskResult'.format(self.host), f'{self.host}/getTaskResult',
json={ json={
'clientKey': self.clientKey, 'clientKey': self.clientKey,
'taskId': taskID 'taskId': taskID
@@ -101,9 +101,9 @@ class captchaSolver(Captcha):
'task': { 'task': {
'websiteURL': url, 'websiteURL': url,
'websiteKey': siteKey, 'websiteKey': siteKey,
'softId': 37,
'type': 'NoCaptchaTask' if captchaType == 'reCaptcha' else 'HCaptchaTask' 'type': 'NoCaptchaTask' if captchaType == 'reCaptcha' else 'HCaptchaTask'
} },
'softId': 37
} }
if self.proxy: if self.proxy:
@@ -113,7 +113,7 @@ class captchaSolver(Captcha):
response = polling2.poll( response = polling2.poll(
lambda: self.session.post( lambda: self.session.post(
'{}/createTask'.format(self.host), f'{self.host}/createTask',
json=data, json=data,
allow_redirects=False, allow_redirects=False,
timeout=30 timeout=30
+4 -4
View File
@@ -68,7 +68,7 @@ class captchaSolver(Captcha):
response = polling2.poll( response = polling2.poll(
lambda: self.session.post( lambda: self.session.post(
'{}/user'.format(self.host), f'{self.host}/user',
headers={'Accept': 'application/json'}, headers={'Accept': 'application/json'},
data={ data={
'username': self.username, 'username': self.username,
@@ -100,7 +100,7 @@ class captchaSolver(Captcha):
response = polling2.poll( response = polling2.poll(
lambda: self.session.post( lambda: self.session.post(
'{}/captcha/{}/report'.format(self.host, jobID), f'{self.host}/captcha/{jobID}/report',
headers={'Accept': 'application/json'}, headers={'Accept': 'application/json'},
data={ data={
'username': self.username, 'username': self.username,
@@ -137,7 +137,7 @@ class captchaSolver(Captcha):
response = polling2.poll( response = polling2.poll(
lambda: self.session.get( lambda: self.session.get(
'{}/captcha/{}'.format(self.host, jobID), f'{self.host}/captcha/{jobID}',
headers={'Accept': 'application/json'} headers={'Accept': 'application/json'}
), ),
check_success=_checkRequest, check_success=_checkRequest,
@@ -203,7 +203,7 @@ class captchaSolver(Captcha):
response = polling2.poll( response = polling2.poll(
lambda: self.session.post( lambda: self.session.post(
'{}/captcha'.format(self.host), f'{self.host}/captcha',
headers={'Accept': 'application/json'}, headers={'Accept': 'application/json'},
data=data, data=data,
allow_redirects=False allow_redirects=False
+490
View File
@@ -0,0 +1,490 @@
# Cloudflare V1
import re
import sys
import time
from copy import deepcopy
from collections import OrderedDict
# ------------------------------------------------------------------------------- #
try:
from HTMLParser import HTMLParser
except ImportError:
if sys.version_info >= (3, 4):
import html
else:
from html.parser import HTMLParser
try:
from urlparse import urlparse, urljoin
except ImportError:
from urllib.parse import urlparse, urljoin
# ------------------------------------------------------------------------------- #
from .exceptions import (
CloudflareCode1020,
CloudflareIUAMError,
CloudflareSolveError,
CloudflareChallengeError,
CloudflareCaptchaError,
CloudflareCaptchaProvider
)
# ------------------------------------------------------------------------------- #
from .captcha import Captcha
from .interpreters import JavaScriptInterpreter
# ------------------------------------------------------------------------------- #
class Cloudflare():
def __init__(self, cloudscraper):
self.cloudscraper = cloudscraper
# ------------------------------------------------------------------------------- #
# Unescape / decode html entities
# ------------------------------------------------------------------------------- #
@staticmethod
def unescape(html_text):
if sys.version_info >= (3, 0):
if sys.version_info >= (3, 4):
return html.unescape(html_text)
return HTMLParser().unescape(html_text)
return HTMLParser().unescape(html_text)
# ------------------------------------------------------------------------------- #
# check if the response contains a valid Cloudflare challenge
# ------------------------------------------------------------------------------- #
@staticmethod
def is_IUAM_Challenge(resp):
try:
return (
resp.headers.get('Server', '').startswith('cloudflare')
and resp.status_code in [429, 503]
and re.search(
r'<form .*?="challenge-form" action="/.*?__cf_chl_jschl_tk__=\S+"',
resp.text,
re.M | re.S
)
)
except AttributeError:
pass
return False
# ------------------------------------------------------------------------------- #
# check if the response contains new Cloudflare challenge
# ------------------------------------------------------------------------------- #
@staticmethod
def is_New_IUAM_Challenge(resp):
try:
return (
resp.headers.get('Server', '').startswith('cloudflare')
and resp.status_code in [429, 503]
and re.search(
r'''cpo.src\s*=\s*['"]/cdn-cgi/challenge-platform/\S+orchestrate/jsch/v1''',
resp.text,
re.M | re.S
)
)
except AttributeError:
pass
return False
# ------------------------------------------------------------------------------- #
# check if the response contains a v2 hCaptcha Cloudflare challenge
# ------------------------------------------------------------------------------- #
def is_New_Captcha_Challenge(self, resp):
try:
return (
self.is_Captcha_Challenge(resp)
and re.search(
r'''cpo.src\s*=\s*['"]/cdn-cgi/challenge-platform/\S+orchestrate/(captcha|managed)/v1''',
resp.text,
re.M | re.S
)
)
except AttributeError:
pass
return False
# ------------------------------------------------------------------------------- #
# check if the response contains a Cloudflare hCaptcha challenge
# ------------------------------------------------------------------------------- #
@staticmethod
def is_Captcha_Challenge(resp):
try:
return (
resp.headers.get('Server', '').startswith('cloudflare')
and resp.status_code == 403
and re.search(
r'''action="/\S+__cf_chl(|_f)_tk=\S+''',
resp.text,
re.M | re.DOTALL
)
)
except AttributeError:
pass
return False
# ------------------------------------------------------------------------------- #
# check if the response contains Firewall 1020 Error
# ------------------------------------------------------------------------------- #
@staticmethod
def is_Firewall_Blocked(resp):
try:
return (
resp.headers.get('Server', '').startswith('cloudflare')
and resp.status_code == 403
and re.search(
r'<span class="cf-error-code">1020</span>',
resp.text,
re.M | re.DOTALL
)
)
except AttributeError:
pass
return False
# ------------------------------------------------------------------------------- #
# Wrapper for is_Captcha_Challenge, is_IUAM_Challenge, is_Firewall_Blocked
# ------------------------------------------------------------------------------- #
def is_Challenge_Request(self, resp):
if self.is_Firewall_Blocked(resp):
self.cloudscraper.simpleException(
CloudflareCode1020,
'Cloudflare has blocked this request (Code 1020 Detected).'
)
if self.is_New_Captcha_Challenge(resp):
self.cloudscraper.simpleException(
CloudflareChallengeError,
'Detected a Cloudflare version 2 Captcha challenge, This feature is not available in the opensource (free) version.'
)
if self.is_New_IUAM_Challenge(resp):
self.cloudscraper.simpleException(
CloudflareChallengeError,
'Detected a Cloudflare version 2 challenge, This feature is not available in the opensource (free) version.'
)
if self.is_Captcha_Challenge(resp) or self.is_IUAM_Challenge(resp):
if self.cloudscraper.debug:
print('Detected a Cloudflare version 1 challenge.')
return True
return False
# ------------------------------------------------------------------------------- #
# Try to solve cloudflare javascript challenge.
# ------------------------------------------------------------------------------- #
def IUAM_Challenge_Response(self, body, url, interpreter):
try:
formPayload = re.search(
r'<form (?P<form>.*?="challenge-form" '
r'action="(?P<challengeUUID>.*?'
r'__cf_chl_jschl_tk__=\S+)"(.*?)</form>)',
body,
re.M | re.DOTALL
).groupdict()
if not all(key in formPayload for key in ['form', 'challengeUUID']):
self.cloudscraper.simpleException(
CloudflareIUAMError,
"Cloudflare IUAM detected, unfortunately we can't extract the parameters correctly."
)
payload = OrderedDict()
for challengeParam in re.findall(r'^\s*<input\s(.*?)/>', formPayload['form'], re.M | re.S):
inputPayload = dict(re.findall(r'(\S+)="(\S+)"', challengeParam))
if inputPayload.get('name') in ['r', 'jschl_vc', 'pass']:
payload.update({inputPayload['name']: inputPayload['value']})
except AttributeError:
self.cloudscraper.simpleException(
CloudflareIUAMError,
"Cloudflare IUAM detected, unfortunately we can't extract the parameters correctly."
)
hostParsed = urlparse(url)
try:
payload['jschl_answer'] = JavaScriptInterpreter.dynamicImport(
interpreter
).solveChallenge(body, hostParsed.netloc)
except Exception as e:
self.cloudscraper.simpleException(
CloudflareIUAMError,
f"Unable to parse Cloudflare anti-bots page: {getattr(e, 'message', e)}"
)
return {
'url': f"{hostParsed.scheme}://{hostParsed.netloc}{self.unescape(formPayload['challengeUUID'])}",
'data': payload
}
# ------------------------------------------------------------------------------- #
# Try to solve the Captcha challenge via 3rd party.
# ------------------------------------------------------------------------------- #
def captcha_Challenge_Response(self, provider, provider_params, body, url):
try:
formPayload = re.search(
r'<form (?P<form>.*?="challenge-form" '
r'action="(?P<challengeUUID>.*?__cf_chl_captcha_tk__=\S+)"(.*?)</form>)',
body,
re.M | re.DOTALL
).groupdict()
if not all(key in formPayload for key in ['form', 'challengeUUID']):
self.cloudscraper.simpleException(
CloudflareCaptchaError,
"Cloudflare Captcha detected, unfortunately we can't extract the parameters correctly."
)
payload = OrderedDict(
re.findall(
r'(name="r"\svalue|data-ray|data-sitekey|name="cf_captcha_kind"\svalue)="(.*?)"',
formPayload['form']
)
)
captchaType = 'reCaptcha' if payload['name="cf_captcha_kind" value'] == 're' else 'hCaptcha'
except (AttributeError, KeyError):
self.cloudscraper.simpleException(
CloudflareCaptchaError,
"Cloudflare Captcha detected, unfortunately we can't extract the parameters correctly."
)
# ------------------------------------------------------------------------------- #
# Pass proxy parameter to provider to solve captcha.
# ------------------------------------------------------------------------------- #
if self.cloudscraper.proxies and self.cloudscraper.proxies != self.cloudscraper.captcha.get('proxy'):
self.cloudscraper.captcha['proxy'] = self.proxies
# ------------------------------------------------------------------------------- #
# Pass User-Agent if provider supports it to solve captcha.
# ------------------------------------------------------------------------------- #
self.cloudscraper.captcha['User-Agent'] = self.cloudscraper.headers['User-Agent']
# ------------------------------------------------------------------------------- #
# Submit job to provider to request captcha solve.
# ------------------------------------------------------------------------------- #
captchaResponse = Captcha.dynamicImport(
provider.lower()
).solveCaptcha(
captchaType,
url,
payload['data-sitekey'],
provider_params
)
# ------------------------------------------------------------------------------- #
# Parse and handle the response of solved captcha.
# ------------------------------------------------------------------------------- #
dataPayload = OrderedDict([
('r', payload.get('name="r" value', '')),
('cf_captcha_kind', payload['name="cf_captcha_kind" value']),
('id', payload.get('data-ray')),
('g-recaptcha-response', captchaResponse)
])
if captchaType == 'hCaptcha':
dataPayload.update({'h-captcha-response': captchaResponse})
hostParsed = urlparse(url)
return {
'url': f"{hostParsed.scheme}://{hostParsed.netloc}{self.unescape(formPayload['challengeUUID'])}",
'data': dataPayload
}
# ------------------------------------------------------------------------------- #
# Attempt to handle and send the challenge response back to cloudflare
# ------------------------------------------------------------------------------- #
def Challenge_Response(self, resp, **kwargs):
if self.is_Captcha_Challenge(resp):
# ------------------------------------------------------------------------------- #
# double down on the request as some websites are only checking
# if cfuid is populated before issuing Captcha.
# ------------------------------------------------------------------------------- #
if self.cloudscraper.doubleDown:
resp = self.cloudscraper.decodeBrotli(
self.cloudscraper.perform_request(resp.request.method, resp.url, **kwargs)
)
if not self.is_Captcha_Challenge(resp):
return resp
# ------------------------------------------------------------------------------- #
# if no captcha provider raise a runtime error.
# ------------------------------------------------------------------------------- #
if (
not self.cloudscraper.captcha
or not isinstance(self.cloudscraper.captcha, dict)
or not self.cloudscraper.captcha.get('provider')
):
self.cloudscraper.simpleException(
CloudflareCaptchaProvider,
"Cloudflare Captcha detected, unfortunately you haven't loaded an anti Captcha provider "
"correctly via the 'captcha' parameter."
)
# ------------------------------------------------------------------------------- #
# if provider is return_response, return the response without doing anything.
# ------------------------------------------------------------------------------- #
if self.cloudscraper.captcha.get('provider') == 'return_response':
return resp
# ------------------------------------------------------------------------------- #
# Submit request to parser wrapper to solve captcha
# ------------------------------------------------------------------------------- #
submit_url = self.captcha_Challenge_Response(
self.cloudscraper.captcha.get('provider'),
self.cloudscraper.captcha,
resp.text,
resp.url
)
else:
# ------------------------------------------------------------------------------- #
# Cloudflare requires a delay before solving the challenge
# ------------------------------------------------------------------------------- #
if not self.cloudscraper.delay:
try:
delay = float(
re.search(
r'submit\(\);\r?\n\s*},\s*([0-9]+)',
resp.text
).group(1)
) / float(1000)
if isinstance(delay, (int, float)):
self.cloudscraper.delay = delay
except (AttributeError, ValueError):
self.cloudscraper.simpleException(
CloudflareIUAMError,
"Cloudflare IUAM possibility malformed, issue extracing delay value."
)
time.sleep(self.cloudscraper.delay)
# ------------------------------------------------------------------------------- #
submit_url = self.IUAM_Challenge_Response(
resp.text,
resp.url,
self.cloudscraper.interpreter
)
# ------------------------------------------------------------------------------- #
# Send the Challenge Response back to Cloudflare
# ------------------------------------------------------------------------------- #
if submit_url:
def updateAttr(obj, name, newValue):
try:
obj[name].update(newValue)
return obj[name]
except (AttributeError, KeyError):
obj[name] = {}
obj[name].update(newValue)
return obj[name]
cloudflare_kwargs = deepcopy(kwargs)
cloudflare_kwargs['allow_redirects'] = False
cloudflare_kwargs['data'] = updateAttr(
cloudflare_kwargs,
'data',
submit_url['data']
)
urlParsed = urlparse(resp.url)
cloudflare_kwargs['headers'] = updateAttr(
cloudflare_kwargs,
'headers',
{
'Origin': f'{urlParsed.scheme}://{urlParsed.netloc}',
'Referer': resp.url
}
)
challengeSubmitResponse = self.cloudscraper.request(
'POST',
submit_url['url'],
**cloudflare_kwargs
)
if challengeSubmitResponse.status_code == 400:
self.cloudscraper.simpleException(
CloudflareSolveError,
'Invalid challenge answer detected, Cloudflare broken?'
)
# ------------------------------------------------------------------------------- #
# Return response if Cloudflare is doing content pass through instead of 3xx
# else request with redirect URL also handle protocol scheme change http -> https
# ------------------------------------------------------------------------------- #
if not challengeSubmitResponse.is_redirect:
return challengeSubmitResponse
else:
cloudflare_kwargs = deepcopy(kwargs)
cloudflare_kwargs['headers'] = updateAttr(
cloudflare_kwargs,
'headers',
{'Referer': challengeSubmitResponse.url}
)
if not urlparse(challengeSubmitResponse.headers['Location']).netloc:
redirect_location = urljoin(
challengeSubmitResponse.url,
challengeSubmitResponse.headers['Location']
)
else:
redirect_location = challengeSubmitResponse.headers['Location']
return self.cloudscraper.request(
resp.request.method,
redirect_location,
**cloudflare_kwargs
)
# ------------------------------------------------------------------------------- #
# We shouldn't be here...
# Re-request the original query and/or process again....
# ------------------------------------------------------------------------------- #
return self.cloudscraper.request(resp.request.method, resp.url, **kwargs)
# ------------------------------------------------------------------------------- #
+2 -2
View File
@@ -28,9 +28,9 @@ def _pythonVersion():
if interpreter == 'PyPy': if interpreter == 'PyPy':
interpreter_version = \ interpreter_version = \
'{}.{}.{}'.format(sys.pypy_version_info.major, sys.pypy_version_info.minor, sys.pypy_version_info.micro) f'{sys.pypy_version_info.major}.{sys.pypy_version_info.minor}.{sys.pypy_version_info.micro}'
if sys.pypy_version_info.releaselevel != 'final': if sys.pypy_version_info.releaselevel != 'final':
interpreter_version = '{}{}'.format(interpreter_version, sys.pypy_version_info.releaselevel) interpreter_version = f'{interpreter_version}{sys.pypy_version_info.releaselevel}'
return { return {
'name': interpreter, 'name': interpreter,
'version': interpreter_version 'version': interpreter_version
+3 -3
View File
@@ -93,14 +93,14 @@ class User_Agent():
else: else:
if self.browser and self.browser not in self.browsers: if self.browser and self.browser not in self.browsers:
sys.tracebacklimit = 0 sys.tracebacklimit = 0
raise RuntimeError('Sorry "{}" browser is not valid, valid browsers are [{}].'.format(self.browser), ", ".join(self.browsers)) raise RuntimeError(f'Sorry "{self.browser}" browser is not valid, valid browsers are [{", ".join(self.browsers)}].')
if not self.platform: if not self.platform:
self.platform = random.SystemRandom().choice(self.platforms) self.platform = random.SystemRandom().choice(self.platforms)
if self.platform not in self.platforms: if self.platform not in self.platforms:
sys.tracebacklimit = 0 sys.tracebacklimit = 0
raise RuntimeError('Sorry the platform "{}" is not valid, valid platforms are [{)}]'.format(self.platform, ", ".join(self.platforms))) raise RuntimeError(f'Sorry the platform "{self.platform}" is not valid, valid platforms are [{", ".join(self.platforms)}]')
filteredAgents = self.filterAgents(user_agents['user_agents']) filteredAgents = self.filterAgents(user_agents['user_agents'])
@@ -111,7 +111,7 @@ class User_Agent():
if not filteredAgents[self.browser]: if not filteredAgents[self.browser]:
sys.tracebacklimit = 0 sys.tracebacklimit = 0
raise RuntimeError('Sorry "{}" browser was not found with a platform of "{}".'.format(self.browser, self.platform)) raise RuntimeError(f'Sorry "{self.browser}" browser was not found with a platform of "{self.platform}".')
self.cipherSuite = user_agents['cipherSuite'][self.browser] self.cipherSuite = user_agents['cipherSuite'][self.browser]
self.headers = user_agents['headers'][self.browser] self.headers = user_agents['headers'][self.browser]
+5 -1
View File
@@ -24,7 +24,11 @@ else:
_urlopen = urllib2.urlopen _urlopen = urllib2.urlopen
_Request = urllib2.Request _Request = urllib2.Request
def query(name, type='A', server=DOH_SERVER, path="/dns-query", fallback=True): # ipv6 = ':' in _urlopen('https://api64.ipify.org/').read().decode()
ipv6 = False
def query(name, type='AAAA' if ipv6 else 'A', server=DOH_SERVER, path="/dns-query", fallback=True):
""" """
Returns domain name query results retrieved by using DNS over HTTPS protocol Returns domain name query results retrieved by using DNS over HTTPS protocol
# Reference: https://developers.cloudflare.com/1.1.1.1/dns-over-https/json-format/ # Reference: https://developers.cloudflare.com/1.1.1.1/dns-over-https/json-format/
+2 -1
View File
@@ -1,5 +1,6 @@
[ [
"platformcode.contextmenu.search", "platformcode.contextmenu.search",
"platformcode.contextmenu.tvshow_options", "platformcode.contextmenu.tvshow_options",
"platformcode.contextmenu.trailer" "platformcode.contextmenu.trailer",
"platformcode.contextmenu.show_servers"
] ]
+23
View File
@@ -0,0 +1,23 @@
import xbmc
from core.item import Item
from platformcode import config
def get_menu_items():
mediatype = xbmc.getInfoLabel('ListItem.DBTYPE')
filePath = xbmc.getInfoLabel('ListItem.FileNameAndPath')
res = []
if 'kod' in filePath and mediatype in ['movie', 'episode'] and config.get_setting('autoplay'):
res = [(config.get_localized_string(70192), execute)]
return res
def execute():
from core import filetools
from platformcode.launcher import run
filePath = xbmc.getInfoLabel('ListItem.FileNameAndPath')
item = Item().fromurl(filetools.read(filePath))
item.disableAutoplay = True
run(item)
+6 -5
View File
@@ -1782,11 +1782,12 @@ def prevent_busy():
def fakeVideo(sleep = False): def fakeVideo(sleep = False):
mediaurl = os.path.join(config.get_runtime_path(), "resources", "kod.mp4") if len(sys.argv) > 1:
xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, xbmcgui.ListItem(path=mediaurl)) mediaurl = os.path.join(config.get_runtime_path(), "resources", "kod.mp4")
while not is_playing(): xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, xbmcgui.ListItem(path=mediaurl))
xbmc.sleep(200) while not is_playing():
xbmc.Player().stop() xbmc.sleep(200)
xbmc.Player().stop()
def channelImport(channelId): def channelImport(channelId):
+2 -2
View File
@@ -50,7 +50,7 @@ def loadCommits(page=1):
# ret -> aggiornato, necessita reload service # ret -> aggiornato, necessita reload service
def check(background=False): def check(background=False):
if not addon.getSetting('addon_update_enabled'): if not config.get_setting('addon_update_enabled'):
return False, False return False, False
logger.info('Cerco aggiornamenti..') logger.info('Cerco aggiornamenti..')
commits = loadCommits() commits = loadCommits()
@@ -156,7 +156,7 @@ def check(background=False):
xbmc.sleep(1000) xbmc.sleep(1000)
updated = True updated = True
if addon.getSetting("addon_update_message"): if config.get_setting("addon_update_message"):
if background: if background:
platformtools.dialog_notification(config.get_localized_string(20000), config.get_localized_string(80040) % commits[0]['sha'][:7], time=3000, sound=False) platformtools.dialog_notification(config.get_localized_string(20000), config.get_localized_string(80040) % commits[0]['sha'][:7], time=3000, sound=False)
try: try:
+1 -1
View File
@@ -627,7 +627,7 @@ def set_content(content_type, silent=False, custom=False):
else: else:
seleccion = values.index('metadata.tvshows.themoviedb.org.python') seleccion = values.index('metadata.tvshows.themoviedb.org.python')
else: else:
seleccion = platformtools.dialog_select(config.get_localized_string(70094), scraper) seleccion = platformtools.dialog_select(config.get_localized_string(70094 if content_type == 'movie' else 70107), scraper)
# Configure scraper # Configure scraper
if seleccion != -1: if seleccion != -1:
@@ -2549,7 +2549,11 @@ msgid " Update waiting time"
msgstr "" msgstr ""
msgctxt "#60607" msgctxt "#60607"
msgid "When Kodi starts" msgid "Each time you start Kodi"
msgstr ""
msgctxt "#60608"
msgid "Each time you start Kodi and daily"
msgstr "" msgstr ""
msgctxt "#60609" msgctxt "#60609"
@@ -2548,8 +2548,12 @@ msgid " Update waiting time"
msgstr " Tempo di attesa aggiornamento" msgstr " Tempo di attesa aggiornamento"
msgctxt "#60607" msgctxt "#60607"
msgid "When Kodi starts" msgid "Each time you start Kodi"
msgstr "All'avvio di Kodi" msgstr "Ad ogni avvio di Kodi"
msgctxt "#60608"
msgid "Each time you start Kodi and daily"
msgstr "Ad ogni avvio di Kodi e giornaliero"
msgctxt "#60609" msgctxt "#60609"
msgid "10 sec" msgid "10 sec"
+6 -11
View File
@@ -23,22 +23,17 @@ def test_video_exists(page_url):
def get_video_url(page_url, premium=False, user="", password="", video_password=""): def get_video_url(page_url, premium=False, user="", password="", video_password=""):
global data, response global data, response
logger.info("(page_url='%s')" % page_url) logger.info("(page_url='%s')" % page_url)
# from core.support import dbg
# dbg()
video_urls = [] video_urls = []
id = scrapertools.find_single_match(page_url, '/e/(\w+)') id = scrapertools.find_single_match(page_url, '/e/(\w+)')
post = {"id": id} post = {"id": id}
token = scrapertools.find_single_match(data, '<meta name="csrf-token" content="([^"]+)') data = httptools.downloadpage("https://streamlare.com/api/video/stream/get", post=post).data.replace("\\","")
data = httptools.downloadpage("https://streamlare.com/api/video/stream/get", matches = scrapertools.find_multiple_matches(data, 'label":"([^"]+).*?file":"([^"]+)')
post=post, headers={'X-CSRF-TOKEN': token, 'X-Requested-With': 'XMLHttpRequest', for res, media_url in matches:
'X-XSRF-TOKEN': httptools.urlparse.unquote(response.cookies.get('XSRF-TOKEN'))}).json media_url += "|User-Agent=%s" %(httptools.get_user_agent())
# src = data["result"]["Original"]["src"] video_urls.append(["MP4", media_url])
# media_url = ''.join([chr(51 ^ c) for c in base64.b64decode(src)])
media_url = data["result"]["file"]
video_urls.append(["MP4", media_url])
return video_urls return video_urls
def get_filename(page_url): def get_filename(page_url):
from core import jsontools from core import jsontools
file = jsontools.load(scrapertools.decodeHtmlentities(httptools.downloadpage(page_url).data.split(':file="')[1].split('"')[0])) file = jsontools.load(scrapertools.decodeHtmlentities(httptools.downloadpage(page_url).data.split(':file="')[1].split('"')[0]))
+1 -1
View File
@@ -4,7 +4,7 @@
"ignore_urls": [], "ignore_urls": [],
"patterns": [ "patterns": [
{ {
"pattern": "(?:streamsb|sbembed|sbembed1|sbplay1|sbplay|pelistop|tubesb|playersb|embedsb|watchsb|streamas|sbfast|sbfull|viewsb|sbvideo|cloudemb|sbplay2|japopav|javplaya|ssbstream)\\.\\w{2,5}/(?:embed-|d/|e/)?([A-z0-9]+)", "pattern": "(?:streamsb|sbembed|sbembed1|sbplay1|sbplay|pelistop|tubesb|playersb|embedsb|watchsb|streamas|sbfast|sbfull|viewsb|sbvideo|cloudemb|sbplay2|japopav|javplaya|ssbstream|sbthe|sbspeed)\\.\\w{2,5}/(?:embed-|d/|e/)?([A-z0-9]+)",
"url": "https://streamsb.net/d/\\1" "url": "https://streamsb.net/d/\\1"
}, },
{ {
+7 -5
View File
@@ -122,7 +122,10 @@ def check_for_update(overwrite=True):
estado_verify_playcount_series = False estado_verify_playcount_series = False
try: try:
if overwrite or (config.get_setting("update", "videolibrary") != 0 and hoy.strftime('%Y-%m-%d') != config.get_setting('updatelibrary_last_check', 'videolibrary')): if overwrite or \
config.get_setting("update", "videolibrary") in [4, 5] or \
(config.get_setting("update", "videolibrary") not in [0, 4] and hoy.strftime('%Y-%m-%d') != config.get_setting('updatelibrary_last_check', 'videolibrary')):
config.set_setting("updatelibrary_last_check", hoy.strftime('%Y-%m-%d'), "videolibrary") config.set_setting("updatelibrary_last_check", hoy.strftime('%Y-%m-%d'), "videolibrary")
heading = config.get_localized_string(60601) heading = config.get_localized_string(60601)
@@ -434,8 +437,8 @@ class AddonMonitor(xbmc.Monitor):
def scheduleVideolibrary(self): def scheduleVideolibrary(self):
self.update_setting = config.get_setting("update", "videolibrary") self.update_setting = config.get_setting("update", "videolibrary")
# 2= daily 3=daily and when kodi starts # 2 = Daily, 3 = When Kodi starts and daily, 5 = Each time you start Kodi and daily
if self.update_setting == 2 or self.update_setting == 3: if self.update_setting in [2, 3, 5]:
self.update_hour = config.get_setting("everyday_delay", "videolibrary") * 4 self.update_hour = config.get_setting("everyday_delay", "videolibrary") * 4
schedule.every().day.at(str(self.update_hour).zfill(2) + ':00').do(run_threaded, check_for_update, (False,)).tag('videolibrary') schedule.every().day.at(str(self.update_hour).zfill(2) + ':00').do(run_threaded, check_for_update, (False,)).tag('videolibrary')
logger.debug('scheduled videolibrary at ' + str(self.update_hour).zfill(2) + ':00') logger.debug('scheduled videolibrary at ' + str(self.update_hour).zfill(2) + ':00')
@@ -498,8 +501,7 @@ if __name__ == "__main__":
updater.refreshLang() updater.refreshLang()
# prepare to replace strSettings # prepare to replace strSettings
path_settings = xbmc.translatePath( path_settings = xbmc.translatePath("special://profile/addon_data/metadata.tvshows.themoviedb.org/settings.xml")
"special://profile/addon_data/metadata.tvshows.themoviedb.org/settings.xml")
settings_data = filetools.read(path_settings) settings_data = filetools.read(path_settings)
strSettings = ' '.join(settings_data.split()).replace("> <", "><") strSettings = ' '.join(settings_data.split()).replace("> <", "><")
strSettings = strSettings.replace("\"", "\'") strSettings = strSettings.replace("\"", "\'")
+283 -282
View File
@@ -1,284 +1,285 @@
{ {
"id": "videolibrary", "id": "videolibrary",
"name": "Videoteca", "name": "Videoteca",
"active": false, "active": false,
"language": ["*"], "language": ["*"],
"settings": [ "settings": [{
{ "id": "update",
"id": "update", "type": "list",
"type": "list", "label": "@60601",
"label": "@60601", "default": 1,
"default": 1, "visible": true,
"visible": true, "lvalues": [
"lvalues": [ "@60602",
"@60602", "@60603",
"@60603", "@60604",
"@60604", "@60605",
"@60605" "@60607",
] "@60608"
}, ]
{ },
"id": "update_wait", {
"type": "list", "id": "update_wait",
"label": "@60606", "type": "list",
"default": 0, "label": "@60606",
"enabled": "eq(-1,@60603)|eq(-1,@60605)", "default": 0,
"lvalues": [ "enabled": "eq(-1,@60603)|eq(-1,@60605)|eq(-1,@60607)|eq(-1,@60608)",
"No", "lvalues": [
"@60609", "No",
"@60610", "@60609",
"@60611", "@60610",
"@60612" "@60611",
] "@60612"
}, ]
{ },
"id": "everyday_delay", {
"type": "list", "id": "everyday_delay",
"label": "@60613", "type": "list",
"default": 1, "label": "@60613",
"enabled": "eq(-2,@60604)|eq(-2,@60605)", "default": 1,
"lvalues": [ "enabled": "eq(-2,@60604)|eq(-2,@60605)|eq(-2,@60608)",
"00:00", "lvalues": [
"04:00", "00:00",
"08:00", "04:00",
"12:00", "08:00",
"16:00", "12:00",
"20:00" "16:00",
] "20:00"
}, ]
{ },
"id": "updatetvshows_interval", {
"type": "list", "id": "updatetvshows_interval",
"label": "@60614", "type": "list",
"default": 0, "label": "@60614",
"enabled": "!eq(-3,@60615)", "default": 0,
"lvalues": [ "enabled": "!eq(-3,@60615)",
"@60616", "lvalues": [
"@60617" "@60616",
] "@60617"
}, ]
{ },
"id": "search_new_content", {
"type": "list", "id": "search_new_content",
"label": "@60618", "type": "list",
"default": 0, "label": "@60618",
"visible": false, "default": 0,
"enabled": "!eq(-4,@60615)", "visible": false,
"lvalues": [ "enabled": "!eq(-4,@60615)",
"@60619", "lvalues": [
"@60620" "@60619",
] "@60620"
}, ]
{ },
"id": "local_episodes", {
"type": "bool", "id": "local_episodes",
"label": "@80042", "type": "bool",
"default": false "label": "@80042",
}, "default": false
{ },
"id": "lab_1", {
"type": "label", "id": "lab_1",
"label": "@60650", "type": "label",
"enabled": true, "label": "@60650",
"visible": true "enabled": true,
}, "visible": true
{ },
"id": "scraper_movies", {
"type": "list", "id": "scraper_movies",
"label": "@60651", "type": "list",
"enabled": false, "label": "@60651",
"default": 0, "enabled": false,
"visible": false, "default": 0,
"lvalues": [ "visible": false,
"TMDB", "lvalues": [
"None" "TMDB",
] "None"
}, ]
{ },
"id": "scraper_tvshows", {
"type": "list", "id": "scraper_tvshows",
"label": "@60652", "type": "list",
"default": 0, "label": "@60652",
"visible": false, "default": 0,
"lvalues": [ "visible": false,
"TMDB", "lvalues": [
"TVDB" "TMDB",
] "TVDB"
}, ]
{ },
"id": "tvdb_retry_eng", {
"type": "bool", "id": "tvdb_retry_eng",
"label": "@60653", "type": "bool",
"default": true, "label": "@60653",
"enabled": "eq(-1,TVDB)", "default": true,
"visible": false "enabled": "eq(-1,TVDB)",
}, "visible": false
{ },
"id": "info_language", {
"type": "list", "id": "info_language",
"label": "@60662", "type": "list",
"enabled": true, "label": "@60662",
"default": 4, "enabled": true,
"lvalues": [ "default": 4,
"de", "lvalues": [
"en", "de",
"es", "en",
"fr", "es",
"it", "fr",
"pt" "it",
] "pt"
}, ]
{ },
"id": "max_links", {
"type": "list", "id": "max_links",
"label": "@60624", "type": "list",
"default": 0, "label": "@60624",
"enabled": true, "default": 0,
"visible": true, "enabled": true,
"lvalues": [ "visible": true,
"@60625", "lvalues": [
"30", "@60625",
"60", "30",
"90", "60",
"120", "90",
"150", "120",
"180", "150",
"210" "180",
] "210"
}, ]
{ },
"id": "white_list_order", {
"type": "bool", "id": "white_list_order",
"label": "@60626", "type": "bool",
"enabled": true, "label": "@60626",
"visible": false, "enabled": true,
"default": false "visible": false,
}, "default": false
{ },
"id": "quit_channel_name", {
"type": "bool", "id": "quit_channel_name",
"label": "@60627", "type": "bool",
"enabled": true, "label": "@60627",
"visible": false, "enabled": true,
"default": false "visible": false,
}, "default": false
{ },
"id": "replace_VD", {
"type": "bool", "id": "replace_VD",
"label": "@60628", "type": "bool",
"enabled": "eq(-4,@60623)", "label": "@60628",
"visible": false, "enabled": "eq(-4,@60623)",
"default": false "visible": false,
}, "default": false
{ },
"id": "db_mode", {
"type": "bool", "id": "db_mode",
"label": "@60629", "type": "bool",
"default": false, "label": "@60629",
"enabled": true, "default": false,
"visible": true "enabled": true,
}, "visible": true
{ },
"id": "xbmc_host", {
"type": "text", "id": "xbmc_host",
"label": "@60632", "type": "text",
"visible": true, "label": "@60632",
"enabled": "eq(-1,true)" "visible": true,
}, "enabled": "eq(-1,true)"
{ },
"id": "xbmc_puerto", {
"type": "text", "id": "xbmc_puerto",
"label": "@60633", "type": "text",
"enabled": "eq(-2,true)", "label": "@60633",
"visible": true "enabled": "eq(-2,true)",
}, "visible": true
{ },
"id": "mark_as_watched", {
"type": "bool", "id": "mark_as_watched",
"label": "@60634", "type": "bool",
"default": true, "label": "@60634",
"enabled": true, "default": true,
"visible": true "enabled": true,
}, "visible": true
{ },
"id": "sync_trakt", {
"type": "label", "id": "sync_trakt",
"label": "@60637", "type": "label",
"enabled": true, "label": "@60637",
"visible": true "enabled": true,
}, "visible": true
{ },
"id": "sync_trakt_watched", {
"type": "bool", "id": "sync_trakt_watched",
"label": "@60638", "type": "bool",
"default": false, "label": "@60638",
"visible": false, "default": false,
"enabled": "eq(-3,true)" "visible": false,
}, "enabled": "eq(-3,true)"
{ },
"id": "sync_trakt_notification", {
"type": "bool", "id": "sync_trakt_notification",
"label": "@60639", "type": "bool",
"default": true, "label": "@60639",
"visible": true, "default": true,
"enabled": true "visible": true,
}, "enabled": true
{ },
"id": "sync_trakt_new_tvshow", {
"type": "bool", "id": "sync_trakt_new_tvshow",
"label": "@60640", "type": "bool",
"default": false, "label": "@60640",
"enabled": true, "default": false,
"visible": true "enabled": true,
}, "visible": true
{ },
"id": "sync_trakt_new_tvshow_wait", {
"type": "bool", "id": "sync_trakt_new_tvshow_wait",
"label": "@60641", "type": "bool",
"default": true, "label": "@60641",
"visible": true, "default": true,
"enabled": "eq(-1,true)" "visible": true,
}, "enabled": "eq(-1,true)"
{ },
"id": "show_all_seasons", {
"type": "bool", "id": "show_all_seasons",
"label": "@60642", "type": "bool",
"default": true "label": "@60642",
}, "default": true
{ },
"id": "no_pile_on_seasons", {
"type": "list", "id": "no_pile_on_seasons",
"label": "@60643", "type": "list",
"default": 1, "label": "@60643",
"lvalues": [ "default": 1,
"@60648", "lvalues": [
"@60644", "@60648",
"@60616" "@60644",
] "@60616"
}, ]
{ },
"id": "ask_channel", {
"type": "bool", "id": "ask_channel",
"label": "@60645", "type": "bool",
"default": false "label": "@60645",
}, "default": false
{ },
"id": "original_title_folder", {
"type": "bool", "id": "original_title_folder",
"label": "@60646", "type": "bool",
"default": false "label": "@60646",
}, "default": false
{ },
"id": "lowerize_title", {
"type": "bool", "id": "lowerize_title",
"label": "@70703", "type": "bool",
"default": false "label": "@70703",
}, "default": false
{ },
"id": "verify_playcount", {
"type": "bool", "id": "verify_playcount",
"label": "@70526", "type": "bool",
"default": false "label": "@70526",
} "default": false
] }
]
} }