diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3661e88c..63a17ccc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,28 +1,25 @@ name: Test Suite on: workflow_dispatch: - schedule: - - cron: '00 15 * * *' jobs: tests: - runs-on: ubuntu-latest + runs-on: macos-latest steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4.3.0 with: - python-version: 3.8 + python-version: 3.9 - name: Run tests run: | - export KODI_INTERACTIVE=0 ./tests/run.sh - name: Commit & Push changes - uses: dmnemec/copy_file_to_another_repo_action@v1.0.4 + uses: dmnemec/copy_file_to_another_repo_action@main env: API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }} with: diff --git a/addon.xml b/addon.xml index ba328b0f..a02a35f1 100644 --- a/addon.xml +++ b/addon.xml @@ -1,4 +1,4 @@ - + @@ -28,9 +28,6 @@ resources/media/screenshot-3.png - fix di routine ai canali/server -- disabilitati cb01anime e tantifilm -- aggiunta opzione "mostra server" nel menu contestuale della libreria -- più opzioni per quanto riguarda l'aggiornamento della videoteca Naviga velocemente sul web e guarda i contenuti presenti [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] diff --git a/channels.json b/channels.json index 9a194ff1..f4ba631e 100644 --- a/channels.json +++ b/channels.json @@ -16,7 +16,7 @@ "eurostreaming": "https://eurostreaming.credit", "eurostreaming_actor": "https://eurostreaming.care", "filmstreaming": "https://filmstreaming.sbs", - "guardaseriecam": "https://guardaserie.baby", + "guardaseriecam": "https://guardaserie.app", "hd4me": "https://hd4me.net", "ilcorsaronero": "https://ilcorsaronero.link", "ilgeniodellostreaming_cam": "https://ilgeniodellostreaming.sbs", diff --git a/channels/1337x.py b/channels/1337x.py index 328ea150..6466d02c 100644 --- a/channels/1337x.py +++ b/channels/1337x.py @@ -115,7 +115,7 @@ def peliculas(item): action = 'seasons' patron = r']+>){4}\s*]+>(?P[^<]+)' - if (item.args == 'search' or item.contentType != 'movie') and inspect.stack(0)[4][3] not in ['get_channel_results']: + if (item.args == 'search' or item.contentType != 'movie') and not support.stackCheck(['get_channel_results']): patronNext = None def itemlistHook(itemlist): lastUrl = support.match(data, patron=r'href="([^"]+)">Last').match diff --git a/channels/animeunity.py b/channels/animeunity.py index 641be941..c9ab31d1 100644 --- a/channels/animeunity.py +++ b/channels/animeunity.py @@ -4,14 +4,13 @@ # ------------------------------------------------------------ import cloudscraper, json, copy, inspect -from core import jsontools, support +from core import jsontools, support, httptools from platformcode import autorenumber -session = cloudscraper.create_scraper() - +# support.dbg() host = support.config.get_channel_url() -response = session.get(host + '/archivio') -csrf_token = support.match(response.text, patron='name="csrf-token" content="([^"]+)"').match +response = httptools.downloadpage(host + '/archivio') +csrf_token = support.match(response.data, patron='name="csrf-token" content="([^"]+)"').match headers = {'content-type': 'application/json;charset=UTF-8', 'x-csrf-token': csrf_token, 'Cookie' : '; '.join([x.name + '=' + x.value for x in response.cookies])} @@ -153,7 +152,7 @@ def peliculas(item): item.args['order'] = order_list[order] payload = json.dumps(item.args) - records = session.post(host + '/archivio/get-animes', headers=headers, data=payload).json()['records'] + records = httptools.downloadpage(host + '/archivio/get-animes', headers=headers, post=payload).json['records'] for it in records: if not it['title']: it['title'] = '' @@ -225,8 +224,28 @@ def episodios(item): def findvideos(item): - itemlist = [item.clone(title='StreamingCommunityWS', server='streamingcommunityws', url=str(item.scws_id))] - # itemlist = [item.clone(title='StreamingCommunityWS', server='streamingcommunityws', url=str(item.scws_id)), - # item.clone(title=support.config.get_localized_string(30137), server='directo', url=item.video_url)] - return support.server(item, itemlist=itemlist, referer=False) + if item.scws_id: + from time import time + from base64 import b64encode + from hashlib import md5 + client_ip = support.httptools.downloadpage('http://ip-api.com/json/').json.get('query') + + expires = int(time() + 172800) + token = b64encode(md5('{}{} Yc8U6r8KjAKAepEA'.format(expires, client_ip).encode('utf-8')).digest()).decode('utf-8').replace('=', '').replace('+', '-').replace('/', '_') + + url = 'https://scws.work/master/{}?token={}&expires={}&n=1'.format(item.scws_id, token, expires) + + itemlist = [item.clone(title=support.config.get_localized_string(30137), url=url, server='directo', action='play')] + + return support.server(item, itemlist=itemlist) + + +def play(item): + urls = list() + info = support.match(item.url, patron=r'(http.*?rendition=(\d+)[^\s]+)').matches + + if info: + for url, res in info: + urls.append(['hls [{}]'.format(res), url]) + return urls \ No newline at end of file diff --git a/channels/animeworld.json b/channels/animeworld.json index 05ccbc30..fdc856af 100644 --- a/channels/animeworld.json +++ b/channels/animeworld.json @@ -7,6 +7,15 @@ "banner": "animeworld.png", "categories": ["anime", "vos"], "settings": [ + { + "id": "lang", + "type": "list", + "label": "Lingua di Ricerca", + "default": 0, + "enabled": true, + "visible": true, + "lvalues": [ "Tutte", "Ita", "Sub-Ita"] + }, { "id": "order", "type": "list", diff --git a/channels/animeworld.py b/channels/animeworld.py index 5038b832..11ece706 100644 --- a/channels/animeworld.py +++ b/channels/animeworld.py @@ -4,7 +4,7 @@ # thanks to fatshotty # ---------------------------------------------------------- -from core import httptools, support, jsontools +from core import httptools, support, config, jsontools host = support.config.get_channel_url() __channel__ = 'animeworld' @@ -23,7 +23,7 @@ def get_data(item): # support.dbg() url = httptools.downloadpage(item.url, headers=headers, follow_redirects=True, only_headers=True).url data = support.match(url, headers=headers, follow_redirects=True).data - if 'AWCookieVerify' in data: + if 'SecurityAW' in data: get_cookie(data) data = get_data(item) return data @@ -37,8 +37,8 @@ def order(): @support.menu def mainlist(item): anime=['/filter?sort=', - ('ITA',['/filter?dub=1&sort=', 'menu', '1']), - ('SUB-ITA',['/filter?dub=0&sort=', 'menu', '0']), + ('ITA',['/filter?dub=1&sort=', 'menu', 'dub=1']), + ('SUB-ITA',['/filter?dub=0&sort=', 'menu', 'dub=0']), ('In Corso', ['/ongoing', 'peliculas','noorder']), ('Ultimi Episodi', ['/updated', 'peliculas', 'updated']), ('Nuove Aggiunte',['/newest', 'peliculas','noorder' ]), @@ -50,6 +50,7 @@ def mainlist(item): def genres(item): action = 'peliculas' data = get_data(item) + patronBlock = r'dropdown[^>]*>\s*Generi\s*<span.[^>]+>(?P<block>.*?)</ul>' patronMenu = r'<input.*?name="(?P<name>[^"]+)" value="(?P<value>[^"]+)"\s*>[^>]+>(?P<title>[^<]+)</label>' @@ -75,9 +76,10 @@ def menu(item): def submenu(item): action = 'peliculas' data = item.other + # debug=True patronMenu = r'<input.*?name="(?P<name>[^"]+)" value="(?P<value>[^"]+)"\s*>[^>]+>(?P<title>[^<]+)<\/label>' def itemHook(item): - item.url = host + '/filter?' + item.name + '=' + item.value + '&dub=' + item.args + ('&sort=' if item.name != 'sort' else '') + item.url = '{}/filter?{}={}&{}{}'.format(host, item.name, item.value, item.args, ('&sort=' if item.name != 'sort' else '')) return item return locals() @@ -85,9 +87,10 @@ def submenu(item): def newest(categoria): support.info(categoria) item = support.Item() + lang = config.get_setting('lang', channel=item.channel) try: if categoria == "anime": - item.url = host + '/updated' + item.url = host item.args = "updated" return peliculas(item) # Continua la ricerca in caso di errore @@ -98,13 +101,13 @@ def newest(categoria): return [] -def search(item, texto): - support.info(texto) +def search(item, text): + support.info(text) if item.search: - item.url = host + '/filter?dub=' + item.args + '&keyword=' + texto + '&sort=' + item.url = '{}/filter?{}&keyword={}&sort='.format(host, item.args, text) else: - item.args = 'noorder' - item.url = host + '/search?keyword=' + texto + lang = ['?', '?dub=1&', '?dub=0&'][config.get_setting('lang', channel=item.channel)] + item.url = '{}/filter{}&keyword={}&sort='.format(host, lang, text) item.contentType = 'tvshow' try: return peliculas(item) @@ -118,8 +121,8 @@ def search(item, texto): @support.scrape def peliculas(item): + data = get_data(item) anime = True - # debug = True if item.args not in ['noorder', 'updated'] and not item.url[-1].isdigit(): item.url += order() # usa l'ordinamento di configura canale data = get_data(item) @@ -185,7 +188,9 @@ def findvideos(item): else: dataJson = support.match(host + '/api/episode/info?id=' + epID + '&alt=0', headers=headers).data json = jsontools.load(dataJson) + title = support.match(json['grabber'], patron=r'server\d+.([^.]+)', string=True).match if title: itemlist.append(item.clone(action="play", title=title, url=json['grabber'].split('=')[-1], server='directo')) else: urls.append(json['grabber']) + # support.info(urls) return support.server(item, urls, itemlist) diff --git a/channels/aniplay.json b/channels/aniplay.json index eaeda3c4..afc23c7b 100644 --- a/channels/aniplay.json +++ b/channels/aniplay.json @@ -34,4 +34,4 @@ "lvalues": ["10", "20", "30", "40", "50", "60", "80", "90"] } ] -} +} \ No newline at end of file diff --git a/channels/guardaseriecam.py b/channels/guardaseriecam.py index 1b59b9ac..880e7a5a 100644 --- a/channels/guardaseriecam.py +++ b/channels/guardaseriecam.py @@ -45,9 +45,9 @@ def peliculas(item): @support.scrape def episodios(item): 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'(?P<data><a href="#" allowfullscreen data-link="[^"]+.*?title="(?P<title>[^"]+)(?P<lang>[sS][uU][bB]-?[iI][tT][aA])?\s*">(?P<episode>[^<]+).*?</li>)' action = 'findvideos' - # debugBlock = True + # debug = True return locals() @@ -68,4 +68,4 @@ def search(item, text): def findvideos(item): logger.debug() - return support.server(item, item.url) \ No newline at end of file + return support.server(item, item.data) diff --git a/channels/ilcorsaronero.py b/channels/ilcorsaronero.py index a16a5a3f..81b0da61 100644 --- a/channels/ilcorsaronero.py +++ b/channels/ilcorsaronero.py @@ -57,7 +57,9 @@ def peliculas(item): def itemHook(item): if not sceneTitle: item.title = item.title.replace('_', ' ') + item.fulltitle = item.fulltitle.replace('_', ' ') item.title = support.scrapertools.decodeHtmlentities(support.urlparse.unquote(item.title)) + return item if 'search' not in item.args: diff --git a/channels/italiafilm.py b/channels/italiafilm.py index 6793be38..279a9360 100644 --- a/channels/italiafilm.py +++ b/channels/italiafilm.py @@ -6,23 +6,22 @@ from core import support, httptools from platformcode import config -host = config.get_channel_url() -headers = [['Referer', host]] +host = config.get_channel_url() +headers = [['Referer', host]] @support.menu def mainlist(item): - menu = [ - ('Film', ['/film/', 'list', 'film']), - ('Per Genere', ['', 'list', 'genere']), - ('Al Cinema', ['/cinema/', 'list', 'film']), - ('Sottotitolati', ['/sub-ita/', 'list', 'film']), - ('Top del Mese', ['/top-del-mese.html', 'list', 'film']) - + menu = [ + ('Film', ['/film/', 'list', 'film']), + ('Per Genere', ['', 'list', 'genere']), + ('Al Cinema', ['/cinema/', 'list', 'film']), + ('Sub-ITA', ['/sub-ita/', 'list', 'film']), + ('Top del Mese', ['/top-del-mese.html', 'list', 'film']) ] search = '' - return locals() + return locals() @support.scrape @@ -64,7 +63,7 @@ def findvideos(item): urls = [] data = support.match(item).data matches = support.match(data, patron=r'<iframe.*?src="([^"]+)').matches - + for m in matches: if 'youtube' not in m and not m.endswith('.js'): urls += support.match(m, patron=r'data-link="([^"]+)').matches diff --git a/channels/la7.py b/channels/la7.py index fce2d748..57bdc3c2 100644 --- a/channels/la7.py +++ b/channels/la7.py @@ -4,7 +4,8 @@ # ------------------------------------------------------------ import requests -from core import support +from core import support, httptools +from platformcode import logger DRM = 'com.widevine.alpha' key_widevine = "https://la7.prod.conax.cloud/widevine/license" @@ -27,7 +28,7 @@ def mainlist(item): ('Replay {bold}', ['', 'replay_channels'])] menu = [('Programmi TV {bullet bold}', ['/tutti-i-programmi', 'peliculas', '', 'tvshow']), - ('Teche La7 {bullet bold}', ['/i-protagonisti', 'peliculas', '', 'tvshow'])] + ('Teche La7 {bullet bold}', ['/la7teche', 'peliculas', '', 'tvshow'])] search = '' return locals() @@ -83,13 +84,18 @@ def search(item, text): @support.scrape def peliculas(item): search = item.search + action = 'episodios' + pagination = 20 disabletmdb = True addVideolibrary = False downloadEnabled = False - action = 'episodios' + patron = r'<a href="(?P<url>[^"]+)"[^>]+><div class="[^"]+" data-background-image="(?P<t>[^"]+)"></div><div class="titolo">\s*(?P<title>[^<]+)<' + + if 'la7teche' in item.url: + patron = r'<a href="(?P<url>[^"]+)" title="(?P<title>[^"]+)" class="teche-i-img".*?url\(\'(?P<thumb>[^\']+)' + def itemHook(item): - item.thumbnail = 'http:' + item.t if item.t.startswith('//') else item.t if item.t else item.thumbnail item.fanart = item.thumb return item return locals() @@ -97,33 +103,18 @@ def peliculas(item): @support.scrape def episodios(item): - data = support.match(item).data - # debug = True action = 'findvideos' - if '>puntate<' in data: - patronBlock = r'>puntate<(?P<block>.*?)home-block-outbrain' - url = support.match(data, patron=r'>puntate<[^>]+>[^>]+>[^>]+><a href="([^"]+)"').match - data += support.match(host + url).data - else: - item.url += '/video' - data = support.match(item).data - - patron = r'(?:<a href="(?P<url>[^"]+)">[^>]+><div class="[^"]+" data-background-image="(?P<t>[^"]*)">[^>]+>[^>]+>[^>]+>(?:[^>]+>)?(?:[^>]+>){6}?)\s*(?P<title>[^<]+)<(?:[^>]+>[^>]+>[^>]+><div class="data">(?P<date>[^<]+))?|class="heading">[^>]+>(?P<Title>[^<]+).*?window.shareUrl = "(?P<Url>[^"]+)".*?poster:\s*"(?P<Thumb>[^"]+)", title: "(?P<desc>[^"]+)"' - patronNext = r'<a href="([^"]+)">›' addVideolibrary = False downloadEnabled = False - def itemHook(item): - if item.Thumb: item.t = item.Thumb - item.thumbnail = 'http:' + item.t if item.t.startswith('//') else item.t if item.t else item.thumbnail - if item.Title: item.title = support.typo(item.Title, 'bold') - if item.date: - item.title = support.re.sub(r'[Pp]untata (?:del )?\d+/\d+/\d+', '', item.title) - item.title += support.typo(item.date, '_ [] bold') - if item.desc: item.plot = item.desc - item.forcethumb = True - item.fanart = item.thumbnail - return item + if 'la7teche' in item.url: + patron = r'<a href="(?P<url>[^"]+)">\s*<div class="holder-bg">.*?data-background-image="(?P<thumb>[^"]+)(?:[^>]+>){4}\s*(?P<title>[^<]+)(?:(?:[^>]+>){2}\s*(?P<plot>[^<]+))?' + else: + data = str(support.match(item.url, patron=r'"home-block home-block--oggi(.*?)</section>').matches) + data += httptools.downloadpage(item.url + '/video').data + + patron = r'item[^>]+>\s*<a href="(?P<url>[^"]+)">.*?image="(?P<thumb>[^"]+)(?:[^>]+>){4,5}\s*(?P<title>[\d\w][^<]+)(?:(?:[^>]+>){7}\s*(?P<title2>[\d\w][^<]+))?' + patronNext = r'<a href="([^"]+)">›' return locals() diff --git a/channels/metalvideo.py b/channels/metalvideo.py index 4623a086..0044e61e 100644 --- a/channels/metalvideo.py +++ b/channels/metalvideo.py @@ -9,37 +9,33 @@ host = 'https://metalvideo.com' headers = {'X-Requested-With': 'XMLHttpRequest'} -@support.scrape +@support.menu def mainlist(item): + menu = [('Generi',['', 'genres']), + ('Ultimi Video',['/videos/latest', 'peliculas']), + ('Top Video',['/videos/top', 'peliculas']), + ('Cerca...',['','search',])] + return locals() + + +@support.scrape +def genres(item): item.url = host action = 'peliculas' - patronBlock = r'<ul class="dropdown-menu(?P<block>.*?)</ul>\s*</div' - patron = r'<a href="(?P<url>[^"]+)"(?: class="")?>(?P<title>[^<]+)<' + patronBlock = r'<div class="swiper-slide">(?P<block>.*?)<button' + patron = r'class="" href="(?P<url>[^"]+)[^>]+>(?P<title>[^<]+)<' def itemHook(item): item.thumbnail = support.thumb('music') item.contentType = 'music' return item - def itemlistHook(itemlist): - itemlist.pop(0) - itemlist.append( - support.Item( - channel=item.channel, - title=support.typo('Cerca...', 'bold'), - contentType='music', - url=item.url, - action='search', - thumbnail=support.thumb('search'))) - - support.channel_config(item, itemlist) - return itemlist return locals() @support.scrape def peliculas(item): # debug=True action = 'findvideos' - patron= r'<img src="[^"]+" alt="(?P<title>[^"]+)" data-echo="(?P<thumb>[^"]+)"(?:[^>]+>){7}<a href="(?P<url>[^"]+)"' - patronNext = r'<a href="([^"]+)">(?:»|»)' + patron= r'<a href="(?P<url>[^"]+)"[^>]+>\s*<img src="(?P<thumb>[^"]+)" alt="(?P<title>[^"]+)"[^>]*>' + patronNext = r'<a href="([^"]+)" data-load="[^"]+" class="[^"]+" title="Next' typeContentDict = {'': 'music'} def itemHook(item): item.contentType = 'music' @@ -49,12 +45,13 @@ def peliculas(item): def findvideos(item): - return support.server(item, Videolibrary=False) + data = support.match(item, patron=r'<source src="[^"]+').match + return support.server(item, Videolibrary=False, data=data) def search(item, text): support.info(text) - item.url = host + '/search.php?keywords=' + text + '&video-id=' + item.url = host + '/search?keyword=' + text try: return peliculas(item) # Continua la ricerca in caso di errore diff --git a/channels/raiplay.py b/channels/raiplay.py index f37eaa49..34810cb4 100644 --- a/channels/raiplay.py +++ b/channels/raiplay.py @@ -22,16 +22,17 @@ def mainlist(item): top = [('Dirette {bold}', ['/dirette', 'live', '/palinsesto/onAir.json']), ('Replay {bold}', ['/guidatv', 'replayMenu', '/guidatv.json'])] - menu = [('Film {bullet bold}', ['/film', 'menu', '/tipologia/film/index.json']), - ('Serie italiane {bullet bold}', ['/serietv', 'menu', '/tipologia/serieitaliane/index.json']), - # ('Fiction {bullet bold}', ['/fiction', 'menu', '/tipologia/fiction/index.json']), - ('Documentari {bullet bold}', ['/documentari', 'menu', '/tipologia/documentari/index.json']), - ('Programmi TV{bullet bold}', ['/programmi', 'menu', '/tipologia/programmi/index.json']), - ('Programmi per Bambini {bullet bold}', ['/bambini', 'menu', '/tipologia/bambini/index.json']), - ('Teen {bullet bold}', ['/teen', 'menu', '/tipologia/teen/index.json']), - ('Learning {bullet bold}', ['/learning', 'menu', '/tipologia/learning/index.json']), - ('Teche Rai {bullet bold storia}', ['/techerai', 'menu', '/tipologia/techerai/index.json']), - ('Musica e Teatro {bullet bold}', ['/musica-e-teatro', 'menu', '/tipologia/musica-e-teatro/index.json']) + menu = [('Film {bold}', ['/film', 'menu', '/tipologia/film/index.json']), + ('Serie italiane {bold}', ['/serieitaliane', 'menu', '/tipologia/serieitaliane/index.json']), + ('Serie Internazionali {bold}', ['/serieinternazionali', 'menu', '/tipologia/serieinternazionali/index.json']), + ('Programmi TV{bold}', ['/programmi', 'menu', '/tipologia/programmi/index.json']), + ('Documentari {bold}', ['/documentari', 'menu', '/tipologia/documentari/index.json']), + ('Bambini {bold}', ['/bambini', 'menu', '/tipologia/bambini/index.json']), + ('Teen {bold}', ['/teen', 'menu', '/tipologia/teen/index.json']), + ('Musica e Teatro {bold}', ['/musica-e-teatro', 'menu', '/tipologia/musica-e-teatro/index.json']), + ('Teche Rai {bold storia}', ['/techerai', 'menu', '/tipologia/techerai/index.json']), + ('Learning {bold}', ['/learning', 'menu', '/tipologia/learning/index.json']), + ('Rai Italy{bold tv}', ['/raiitaly', 'menu', '/tipologia/raiitaly/index.json']) ] search = '' @@ -41,6 +42,7 @@ def mainlist(item): def menu(item): logger.debug() + itemlist = [] item.disable_videolibrary = True action = 'peliculas' @@ -60,7 +62,6 @@ def menu(item): action = 'menu' thumb = support.thumb('genres') itemlist.append(item.clone(title=support.typo(it['name'], 'bold'), data=it.get('contents', item.data), thumbnail=thumb, action=action)) - return itemlist @@ -256,7 +257,7 @@ def getUrl(url): elif url.startswith("/"): url = host + url url = url.replace(".html?json", ".json").replace("/?json",".json").replace("?json",".json").replace(" ", "%20") - + logger.debug('URL', url) return url diff --git a/channels/streamingcommunity.py b/channels/streamingcommunity.py index d4ed9a54..7d8e8dd1 100644 --- a/channels/streamingcommunity.py +++ b/channels/streamingcommunity.py @@ -223,7 +223,7 @@ def episodios(item): action='findvideos', contentType='episode', contentSerieName=item.fulltitle, - url='{}/watch/{}?e={}'.format(host, se['title_id'], ep['id']))) + url='{}/iframe/{}?episode_id={}'.format(host, se['title_id'], ep['id']))) if config.get_setting('episode_info') and not support.stackCheck(['add_tvshow', 'get_newest']): support.tmdb.set_infoLabels_itemlist(itemlist, seekTmdb=True) @@ -235,10 +235,8 @@ def episodios(item): def findvideos(item): support.callAds('https://thaudray.com/5/3523301', host) - # Fix for old items in videolibrary - if item.episodeid and item.episodeid not in item.url: - item.url += item.episodeid - itemlist = [item.clone(title=channeltools.get_channel_parameters(item.channel)['title'], url=item.url, server='streamingcommunityws')] + itemlist = [item.clone(title=channeltools.get_channel_parameters(item.channel)['title'], + url=item.url.replace('/watch/', '/iframe/'), server='streamingcommunityws')] return support.server(item, itemlist=itemlist, referer=False) diff --git a/channels/tantifilm.py b/channels/tantifilm.py index e41e0936..74ea3327 100644 --- a/channels/tantifilm.py +++ b/channels/tantifilm.py @@ -3,9 +3,8 @@ # Canale per Tantifilm # ------------------------------------------------------------ -from core import scrapertools, httptools, support +from core import support from core.item import Item -from core.support import info from platformcode import logger from platformcode import config diff --git a/channels/toonitalia.py b/channels/toonitalia.py index 42771f74..e8937a08 100644 --- a/channels/toonitalia.py +++ b/channels/toonitalia.py @@ -13,12 +13,10 @@ headers = [['Referer', host]] @support.menu def mainlist(item): - # top = [('Novità',['', 'peliculas', 'new', 'tvshow']), - # ('Aggiornamenti', ['', 'peliculas', 'last', 'tvshow'])] - # tvshow = ['/category/serie-tv/'] - anime =['/category/anime/'] - # ('Sub-Ita',['/category/anime-sub-ita/', 'peliculas', 'sub']), - # ('Film Animati',['/category/film-animazione/','peliculas', '', 'movie'])] + anime =['/category/anime', + ('ITA',['/lista-anime-ita','peliculas',]), + ('Sub-ITA',['/lista-anime-sub-ita', 'peliculas'])] + # ('Film Animati',['/lista-anime-ita','peliculas', '', 'movie'])] search = '' return locals() @@ -39,52 +37,28 @@ def search(item, text): return [] -def newest(categoria): - support.info(categoria) - item = support.Item() - try: - item.contentType = 'undefined' - item.url= host - item.args= 'new' - return peliculas(item) - # Continua la ricerca in caso di errore - except: - import sys - for line in sys.exc_info(): - support.logger.error("{0}".format(line)) - return [] - @support.scrape def peliculas(item): - # debugBlock = True - # debug = True - # search = item.text - if item.contentType != 'movie': anime = True + anime = True action = 'check' - blacklist = ['-Film Animazione disponibili in attesa di recensione '] - if item.action == 'search': - pagination = '' - #patronBlock = '"lcp_catlist"[^>]+>(?P<block>.*)</ul>' - patronBlock = '<main[^>]+>(?P<block>.*?)</ma' - #patron = r'href="(?P<url>[^"]+)" title="(?P<title>[^"]+)"' - patron = r'<a href="(?P<url>[^"]+)"[^>]*>(?P<title>[^<]+)<[^>]+>[^>]+>\s*<div' - elif item.args == 'last': - patronBlock = '(?:Aggiornamenti|Update)</h2>(?P<block>.*?)</ul>' - patron = r'<a href="(?P<url>[^"]+)">\s*<img[^>]+src[set]{0,3}="(?P<thumbnail>[^ ]+)[^>]+>\s*<span[^>]+>(?P<title>[^<]+)' + deflang = 'ITA' if 'sub' not in item.url else 'Sub-ITA' + if 'lista' in item.url: + pagination = 20 + patron = r'<li><a href="(?P<url>[^"]+)">(?P<title>[^<]+)' + else: patronBlock = '<main[^>]+>(?P<block>.*)</main>' - # patron = r'<a href="(?P<url>[^"]+)" rel="bookmark">(?P<title>[^<]+)</a>[^>]+>[^>]+>[^>]+><img.*?src="(?P<thumb>[^"]+)".*?<p>(?P<plot>[^<]+)</p>.*?<span class="cat-links">Pubblicato in.*?.*?(?P<type>(?:[Ff]ilm|</artic))[^>]+>' - patron = r'<a href="(?P<url>[^"]+)" rel="bookmark">(?P<title>[^<]+)</a>(:?[^>]+>){3}(?:<img.*?src="(?P<thumb>[^"]+)")?.*?<p>(?P<plot>[^<]+)</p>.*?tag">.*?(?P<type>(?:[Ff]ilm|</art|Serie Tv))' + patron = r'(?i)<a href="(?P<url>[^"]+)" rel="bookmark">(?P<title>[^<]+)</a>(:?[^>]+>){3}(?:<img.*?src="(?P<thumb>[^"]+)")?.*?<p>(?P<plot>[^<]+)</p>.*?tag">.*?(?P<type>(?:film|serie|anime))(?P<cat>.*?)</span>' typeContentDict={'movie':['film']} typeActionDict={'findvideos':['film']} patronNext = '<a class="next page-numbers" href="([^"]+)">' def itemHook(item): support.info(item.title) - if item.args == 'sub': - item.title += support.typo('Sub-ITA', 'bold color kod _ []') + if 'sub/ita' in item.cat.lower(): + item.title = item.title.replace('[ITA]', '[Sub-ITA]') item.contentLanguage = 'Sub-ITA' return item return locals() @@ -101,18 +75,11 @@ def check(item): def episodios(item): anime = True patron = r'>\s*(?:(?P<season>\d+)(?:×|x|×))?(?P<episode>\d+)(?:\s+–\s+)?[ –]+(?P<title2>[^<]+)[ –]+<a (?P<data>.*?)(?:<br|</p)' - - # if inspect.stack(0)[1][3] not in ['find_episodes']: - # from platformcode import autorenumber - # autorenumber.start(itemlist, item) return locals() def findvideos(item): - servers = support.server(item, data=item.data) - return servers - - # return support.server(item, item.data if item.contentType != 'movie' else support.match(item.url, headers=headers).data ) + return support.server(item, data=item.data) def clean_title(title): diff --git a/channels/tunein.py b/channels/tunein.py index 3626891c..8ef6b2a0 100644 --- a/channels/tunein.py +++ b/channels/tunein.py @@ -3,93 +3,137 @@ # Canale per tunein # ------------------------------------------------------------ -from core import scrapertools, support +from core import httptools, support from platformcode import logger + host = 'http://api.radiotime.com' -headers = [['Referer', host]] +args = 'formats=mp3,aac,ogg,flash,html,hls,wma&partnerId=RadioTime&itemToken=' -@support.scrape +@support.menu def mainlist(item): - item.url = host - action = 'radio' - patron = r'text="(?P<title>[^"]+)" URL="(?P<url>[^"]+)"' - def itemHook(item): - item.thumbnail = support.thumb('music') - item.contentType = 'music' - return item - def itemlistHook(itemlist): - itemlist.append( - item.clone(title=support.typo('Cerca...', 'bold color kod'), action='search', thumbnail=support.thumb('search'))) - support.channel_config(item, itemlist) - return itemlist + menu = [('Musica {bullet music}' ,['/categories/music?{}'.format(args), 'radio', '', 'music']), + ('Sport {bullet music}' ,['/categories/sports?{}'.format(args), 'radio', '', 'music']), + ('Notizie e Dibattiti {bullet music}' ,['/categories/c57922?{}'.format(args), 'radio', '' 'music']), + ('Podcast {bullet music}' ,['/categories/c100000088?{}'.format(args), 'radio', '', 'music']), + ('Audiolibri {bullet music}' ,['/categories/c100006408?{}'.format(args), 'radio', '', 'music']), + ('Luogo {bullet music}' ,['/categories/regions?{}'.format(args), 'radio', '', 'music']), + ('Lingua {bullet music}' ,['/categories/languages?{}'.format(args), 'radio', '', 'music'])] + search ='' return locals() -def radio(item): - support.info() - itemlist = [] - data = support.match(item, patron= r'text="(?P<title>[^\("]+)(?:\((?P<location>[^\)]+)\))?" URL="(?P<url>[^"]+)" bitrate="(?P<quality>[^"]+)" reliability="[^"]+" guide_id="[^"]+" subtext="(?P<song>[^"]+)" genre_id="[^"]+" formats="(?P<type>[^"]+)" (?:playing="[^"]+" )?(?:playing_image="[^"]+" )?(?:show_id="[^"]+" )?(?:item="[^"]+" )?image="(?P<thumb>[^"]+)"') - if data.matches: - for title, location, url, quality, song, type, thumbnail in data.matches: - title = scrapertools.decodeHtmlentities(title) - itemlist.append( - item.clone(title = support.typo(title, 'bold') + support.typo(quality + ' kbps','_ [] bold color kod'), - thumbnail = thumbnail, - url = url, - contentType = 'music', - plot = support.typo(location, 'bold') + '\n' + song, - action = 'findvideos')) - else: - matches = support.match(data.data, patron= r'text="(?P<title>[^\("]+)(?:\([^\)]+\))?" URL="(?P<url>[^"]+)" (?:guide_id="[^"]+" )?(?:stream_type="[^"]+" )?topic_duration="(?P<duration>[^"]+)" subtext="(?P<plot>[^"]+)" item="[^"]+" image="(?P<thumb>[^"]+)"').matches - if matches: - for title, url, duration, plot, thumbnail in matches: - title = scrapertools.unescape(title) - infoLabels={} - infoLabels['duration'] = duration - itemlist.append( - item.clone(title = support.typo(title, 'bold'), - thumbnail = thumbnail, - infolLbels = infoLabels, - url = url, - contentType = 'music', - plot = plot, - action = 'findvideos')) - else: - matches = support.match(data.data, patron= r'text="(?P<title>[^"]+)" URL="(?P<url>[^"]+)"').matches - for title, url in matches: - title = scrapertools.unescape(title) - itemlist.append( - item.clone(channel = item.channel, - title = support.typo(title, 'bold'), - thumbnail = item.thumbnail, - url = url, - action = 'radio')) - support.nextPage(itemlist, item, data.data, r'(?P<url>[^"]+)" key="nextStations') - return itemlist - - -def findvideos(item): - import xbmc - itemlist = [] - item.action = 'play' - urls = support.match(item.url).data.strip().split() - for url in urls: - item.url= url - item.server = 'directo' - itemlist.append(item) - return itemlist - - def search(item, text): support.info(text) - item.url = host + '/Search.ashx?query=' +text + itemlist = list() + try: - return radio(item) + js = httptools.downloadpage('{}/profiles?fullTextSearch=true&query={}&{}'.format(host, text, args)).json + data = js.get('Items', {}) + for c in data: + if c.get('Pivots',{}).get('More',{}).get('Url', ''): + data = httptools.downloadpage(c.get('Pivots',{}).get('More',{}).get('Url', '')).json.get('Items',{}) + else: + data = c.get('Children') + if data: + itemlist.extend(buildItemList(item, data)) + + if js.get('Paging', {}).get('Next'): + support.nextPage(itemlist, item, next_page=js.get('Paging', {}).get('Next')) + return itemlist # Continua la ricerca in caso di errore except: import sys for line in sys.exc_info(): - logger.error("%s" % line) + logger.error(line) return [] + + +def radio(item): + itemlist = list() + js = dict() + if item.data: + data = item.data + else: + js = httptools.downloadpage(item.url).json + data = js.get('Items', {}) + + itemlist = buildItemList(item, data) + if js.get('Paging', {}).get('Next'): + support.nextPage(itemlist, item, next_page=js.get('Paging', {}).get('Next')) + + return itemlist + + + +def buildItemList(item, data): + itemlist = list() + # support.dbg() + for c in data: + item.data = '' + item.action = 'radio' + token = c.get('Context',{}).get('Token','') + if not token: + token = c.get('Actions', {}).get('Context',{}).get('Token','') + if not c.get('Title', c.get('AccessibilityTitle')) or 'premium' in c.get('Title', c.get('AccessibilityTitle')).lower(): + continue + + if c.get('Children'): + if len(data) > 1: + if c.get('Pivots',{}).get('More',{}).get('Url', ''): + itm = item.clone(title=c.get('Title', c.get('AccessibilityTitle')), + url=c.get('Pivots',{}).get('More',{}).get('Url', ''), + token=token) + else: + itm = item.clone(title=c.get('Title', c.get('AccessibilityTitle')), + data=c.get('Children'), + token=token) + else: + if c.get('Pivots',{}).get('More',{}).get('Url', ''): + data = httptools.downloadpage(c.get('Pivots',{}).get('More',{}).get('Url', '')).json.get('Items', {}) + else: + data = c.get('Children') + return buildItemList(item, data) + + elif c.get('GuideId'): + title = c.get('Title', c.get('AccessibilityTitle')) + plot = '[B]{}[/B]\n{}'.format(c.get('Subtitle', ''), c.get('Description', '')) + thumbnail = c.get('Image', '') + if c.get('GuideId').startswith('s'): + itm = item.clone(title=title, + plot=plot, + thumbnail=thumbnail, + url = 'http://opml.radiotime.com/Tune.ashx?render=json&id={}&{}{}'.format(c.get('GuideId'), args, token), + action = 'findvideos') + + else: + itm = item.clone(title=title, + plot=plot, + thumbnail=thumbnail, + url = c.get('Actions', {}).get('Browse',{}).get('Url','')) + + + elif c.get('Actions', {}).get('Browse',{}).get('Url',''): + title = c.get('Title', c.get('AccessibilityTitle')) + itm = item.clone(title = title, + url = c.get('Actions', {}).get('Browse',{}).get('Url','')) + + + itemlist.append(itm) + + return itemlist + + +def findvideos(item): + item.action = 'play' + + js = httptools.downloadpage(item.url, cloudscraper=True).json.get('body', {}) + video_urls = list() + for it in js: + video_urls.append(['m3u8 [{}]'.format(it.get('bitrate')), it.get('url')]) + + item.referer = False + item.server = 'directo' + item.video_urls = video_urls + return [item] diff --git a/core/httptools.py b/core/httptools.py index 7ea8c2d7..9d797008 100755 --- a/core/httptools.py +++ b/core/httptools.py @@ -287,10 +287,11 @@ def downloadpage(url, **opt): # Headers passed as parameters if opt.get('headers', None) is not None: + opt['headers'] = dict(opt['headers']) if not opt.get('replace_headers', False): - req_headers.update(dict(opt['headers'])) + req_headers.update(opt['headers']) else: - req_headers = dict(opt['headers']) + req_headers = opt['headers'] if domain in directIP.keys() and not opt.get('disable_directIP', False): req_headers['Host'] = domain @@ -429,8 +430,9 @@ def downloadpage(url, **opt): else: logger.debug("CF retry with proxy for domain: %s" % domain) if not opt.get('headers'): - opt['headers'] = [] - opt['headers'].extend([['Px-Host', domain], ['Px-Token', cf_proxy['token']]]) + opt['headers'] = {} + opt['headers']['Px-Host'] = domain + opt['headers']['Px-Token'] = cf_proxy['token'] opt['real-url'] = url ret = downloadpage(urlparse.urlunparse((parse.scheme, cf_proxy['url'], parse.path, parse.params, parse.query, parse.fragment)), **opt) ret.url = url diff --git a/core/support.py b/core/support.py index c3417129..7be69de3 100755 --- a/core/support.py +++ b/core/support.py @@ -621,7 +621,7 @@ def scrape(func): else: autorenumber.start(itemlist) if itemlist and action != 'play' and 'patronMenu' not in args and 'patronGenreMenu' not in args \ - and not stackCheck(['add_tvshow', 'get_newest']) and (function not in ['episodios', 'mainlist'] + and not stackCheck(['add_tvshow', 'get_newest']) and not disabletmdb and (function not in ['episodios', 'mainlist'] or (function in ['episodios', 'seasons'] and config.get_setting('episode_info') and itemlist[0].season)): # dbg() tmdb.set_infoLabels_itemlist(itemlist, seekTmdb=True) diff --git a/lib/generictools.py b/lib/generictools.py index 44789305..27d13ae8 100644 --- a/lib/generictools.py +++ b/lib/generictools.py @@ -120,7 +120,7 @@ def update_title(item): if item.from_title_tmdb: del item.from_title_tmdb if not item.from_update and item.from_title: del item.from_title - if item.contentSerieName: # We copy the title to serve as a reference in the "Complete Information" menu + if item.contentSerieName and item.contentType == 'tvshow': # We copy the title to serve as a reference in the "Complete Information" menu item.infoLabels['originaltitle'] = item.contentSerieName item.contentTitle = item.contentSerieName else: diff --git a/lib/js2py/base.py b/lib/js2py/base.py index cf1eca08..f4ee721c 100644 --- a/lib/js2py/base.py +++ b/lib/js2py/base.py @@ -126,7 +126,7 @@ def HJs(val): except Exception as e: message = 'your Python function failed! ' try: - message += e.message + message += str(e) except: pass raise MakeError('Error', message) @@ -319,7 +319,7 @@ class PyJs(object): #prop = prop.value if self.Class == 'Undefined' or self.Class == 'Null': raise MakeError('TypeError', - 'Undefined and null dont have properties!') + 'Undefined and null dont have properties (tried getting property %s)' % repr(prop)) if not isinstance(prop, basestring): prop = prop.to_string().value if not isinstance(prop, basestring): raise RuntimeError('Bug') @@ -361,7 +361,7 @@ class PyJs(object): * / % + - << >> & ^ |''' if self.Class == 'Undefined' or self.Class == 'Null': raise MakeError('TypeError', - 'Undefined and null dont have properties!') + 'Undefined and null don\'t have properties (tried setting property %s)' % repr(prop)) if not isinstance(prop, basestring): prop = prop.to_string().value if NUMPY_AVAILABLE and prop.isdigit(): @@ -991,7 +991,8 @@ class PyJs(object): cand = self.get(prop) if not cand.is_callable(): raise MakeError('TypeError', - '%s is not a function' % cand.typeof()) + '%s is not a function (tried calling property %s of %s)' % ( + cand.typeof(), repr(prop), repr(self.Class))) return cand.call(self, args) def to_python(self): @@ -1304,7 +1305,7 @@ class PyObjectWrapper(PyJs): except Exception as e: message = 'your Python function failed! ' try: - message += e.message + message += str(e) except: pass raise MakeError('Error', message) @@ -1464,9 +1465,11 @@ class PyJsFunction(PyJs): except NotImplementedError: raise except RuntimeError as e: # maximum recursion - raise MakeError( - 'RangeError', e.message if - not isinstance(e, NotImplementedError) else 'Not implemented!') + try: + msg = e.message + except: + msg = repr(e) + raise MakeError('RangeError', msg) def has_instance(self, other): # I am not sure here so instanceof may not work lol. diff --git a/lib/js2py/constructors/jsdate.py b/lib/js2py/constructors/jsdate.py index 98de6431..b6b70a81 100644 --- a/lib/js2py/constructors/jsdate.py +++ b/lib/js2py/constructors/jsdate.py @@ -32,8 +32,7 @@ def UTC(year, month, date, hours, minutes, seconds, ms): # todo complete this mili = args[6].to_number() if l > 6 else Js(0) if not y.is_nan() and 0 <= y.value <= 99: y = y + Js(1900) - t = TimeClip(MakeDate(MakeDay(y, m, dt), MakeTime(h, mi, sec, mili))) - return PyJsDate(t, prototype=DatePrototype) + return TimeClip(MakeDate(MakeDay(y, m, dt), MakeTime(h, mi, sec, mili))) @Js @@ -76,11 +75,12 @@ class PyJsDate(PyJs): # todo fix this problematic datetime part def to_local_dt(self): - return datetime.datetime.utcfromtimestamp( - UTCToLocal(self.value) // 1000) + return datetime.datetime(1970, 1, 1) + datetime.timedelta( + seconds=UTCToLocal(self.value) // 1000) def to_utc_dt(self): - return datetime.datetime.utcfromtimestamp(self.value // 1000) + return datetime.datetime(1970, 1, 1) + datetime.timedelta( + seconds=self.value // 1000) def local_strftime(self, pattern): if self.value is NaN: @@ -118,21 +118,40 @@ class PyJsDate(PyJs): def parse_date(py_string): # todo support all date string formats - try: + date_formats = ( + "%Y-%m-%d", + "%m/%d/%Y", + "%b %d %Y", + ) + # Supports these hour formats and with or hour. + hour_formats = ( + "T%H:%M:%S.%f", + "T%H:%M:%S", + ) + ('',) + # Supports with or without Z indicator. + z_formats = ("Z",) + ('',) + supported_formats = [ + d + t + z + for d in date_formats + for t in hour_formats + for z in z_formats + ] + for date_format in supported_formats: try: - dt = datetime.datetime.strptime(py_string, "%Y-%m-%dT%H:%M:%S.%fZ") - except: - dt = datetime.datetime.strptime(py_string, "%Y-%m-%dT%H:%M:%SZ") - return MakeDate( - MakeDay(Js(dt.year), Js(dt.month - 1), Js(dt.day)), - MakeTime( - Js(dt.hour), Js(dt.minute), Js(dt.second), - Js(dt.microsecond // 1000))) - except: - raise MakeError( - 'TypeError', - 'Could not parse date %s - unsupported date format. Currently only supported format is RFC3339 utc. Sorry!' - % py_string) + dt = datetime.datetime.strptime(py_string, date_format) + except ValueError: + continue + else: + return MakeDate( + MakeDay(Js(dt.year), Js(dt.month - 1), Js(dt.day)), + MakeTime( + Js(dt.hour), Js(dt.minute), Js(dt.second), + Js(dt.microsecond // 1000))) + + raise MakeError( + 'TypeError', + 'Could not parse date %s - unsupported date format. Currently only supported formats are RFC3339 utc, ISO Date, Short Date, and Long Date. Sorry!' + % py_string) def date_constructor(*args): @@ -332,7 +351,7 @@ class DateProto: check_date(this) t = UTCToLocal(this.value) tim = MakeTime( - HourFromTime(t), MinFromTime(t), SecFromTime(t), ms.to_int()) + Js(HourFromTime(t)), Js(MinFromTime(t)), Js(SecFromTime(t)), ms) u = TimeClip(LocalToUTC(MakeDate(Day(t), tim))) this.value = u return u @@ -341,12 +360,164 @@ class DateProto: check_date(this) t = this.value tim = MakeTime( - HourFromTime(t), MinFromTime(t), SecFromTime(t), ms.to_int()) + Js(HourFromTime(t)), Js(MinFromTime(t)), Js(SecFromTime(t)), ms) u = TimeClip(MakeDate(Day(t), tim)) this.value = u return u - # todo Complete all setters! + def setSeconds(sec, ms=None): + check_date(this) + t = UTCToLocal(this.value) + s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate( + Day(t), MakeTime(Js(HourFromTime(t)), Js(MinFromTime(t)), s, milli)) + u = TimeClip(LocalToUTC(date)) + this.value = u + return u + + def setUTCSeconds(sec, ms=None): + check_date(this) + t = this.value + s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate( + Day(t), MakeTime(Js(HourFromTime(t)), Js(MinFromTime(t)), s, milli)) + v = TimeClip(date) + this.value = v + return v + + def setMinutes(min, sec=None, ms=None): + check_date(this) + t = UTCToLocal(this.value) + m = min.to_number() + if not sec is None: s = Js(SecFromTime(t)) + else: s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate(Day(t), MakeTime(Js(HourFromTime(t)), m, s, milli)) + u = TimeClip(LocalToUTC(date)) + this.value = u + return u + + def setUTCMinutes(min, sec=None, ms=None): + check_date(this) + t = this.value + m = min.to_number() + if not sec is None: s = Js(SecFromTime(t)) + else: s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate(Day(t), MakeTime(Js(HourFromTime(t)), m, s, milli)) + v = TimeClip(date) + this.value = v + return v + + def setHours(hour, min=None, sec=None, ms=None): + check_date(this) + t = UTCToLocal(this.value) + h = hour.to_number() + if not min is None: m = Js(MinFromTime(t)) + else: m = min.to_number() + if not sec is None: s = Js(SecFromTime(t)) + else: s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate(Day(t), MakeTime(h, m, s, milli)) + u = TimeClip(LocalToUTC(date)) + this.value = u + return u + + def setUTCHours(hour, min=None, sec=None, ms=None): + check_date(this) + t = this.value + h = hour.to_number() + if not min is None: m = Js(MinFromTime(t)) + else: m = min.to_number() + if not sec is None: s = Js(SecFromTime(t)) + else: s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate(Day(t), MakeTime(h, m, s, milli)) + v = TimeClip(date) + this.value = v + return v + + def setDate(date): + check_date(this) + t = UTCToLocal(this.value) + dt = date.to_number() + newDate = MakeDate( + MakeDay(Js(YearFromTime(t)), Js(MonthFromTime(t)), dt), TimeWithinDay(t)) + u = TimeClip(LocalToUTC(newDate)) + this.value = u + return u + + def setUTCDate(date): + check_date(this) + t = this.value + dt = date.to_number() + newDate = MakeDate( + MakeDay(Js(YearFromTime(t)), Js(MonthFromTime(t)), dt), TimeWithinDay(t)) + v = TimeClip(newDate) + this.value = v + return v + + def setMonth(month, date=None): + check_date(this) + t = UTCToLocal(this.value) + m = month.to_number() + if not date is None: dt = Js(DateFromTime(t)) + else: dt = date.to_number() + newDate = MakeDate( + MakeDay(Js(YearFromTime(t)), m, dt), TimeWithinDay(t)) + u = TimeClip(LocalToUTC(newDate)) + this.value = u + return u + + def setUTCMonth(month, date=None): + check_date(this) + t = this.value + m = month.to_number() + if not date is None: dt = Js(DateFromTime(t)) + else: dt = date.to_number() + newDate = MakeDate( + MakeDay(Js(YearFromTime(t)), m, dt), TimeWithinDay(t)) + v = TimeClip(newDate) + this.value = v + return v + + def setFullYear(year, month=None, date=None): + check_date(this) + if not this.value is NaN: t = UTCToLocal(this.value) + else: t = 0 + y = year.to_number() + if not month is None: m = Js(MonthFromTime(t)) + else: m = month.to_number() + if not date is None: dt = Js(DateFromTime(t)) + else: dt = date.to_number() + newDate = MakeDate( + MakeDay(y, m, dt), TimeWithinDay(t)) + u = TimeClip(LocalToUTC(newDate)) + this.value = u + return u + + def setUTCFullYear(year, month=None, date=None): + check_date(this) + if not this.value is NaN: t = UTCToLocal(this.value) + else: t = 0 + y = year.to_number() + if not month is None: m = Js(MonthFromTime(t)) + else: m = month.to_number() + if not date is None: dt = Js(DateFromTime(t)) + else: dt = date.to_number() + newDate = MakeDate( + MakeDay(y, m, dt), TimeWithinDay(t)) + v = TimeClip(newDate) + this.value = v + return v def toUTCString(): check_date(this) diff --git a/lib/js2py/constructors/time_helpers.py b/lib/js2py/constructors/time_helpers.py index a744d8ca..f3d21f3e 100644 --- a/lib/js2py/constructors/time_helpers.py +++ b/lib/js2py/constructors/time_helpers.py @@ -36,8 +36,8 @@ def DaylightSavingTA(t): return t try: return int( - LOCAL_ZONE.dst(datetime.datetime.utcfromtimestamp( - t // 1000)).seconds) * 1000 + LOCAL_ZONE.dst(datetime.datetime(1970, 1, 1) + datetime.timedelta( + seconds=t // 1000)).seconds) * 1000 except: warnings.warn( 'Invalid datetime date, assumed DST time, may be inaccurate...', diff --git a/lib/js2py/evaljs.py b/lib/js2py/evaljs.py index ef4d7d95..f4649c4d 100644 --- a/lib/js2py/evaljs.py +++ b/lib/js2py/evaljs.py @@ -53,7 +53,7 @@ def write_file_contents(path_or_file, contents): if hasattr(path_or_file, 'write'): path_or_file.write(contents) else: - with open(path_as_local(path_or_file), 'w') as f: + with codecs.open(path_as_local(path_or_file), "w", "utf-8") as f: f.write(contents) @@ -238,6 +238,10 @@ class EvalJs(object): self.execute_debug(code) return self['PyJsEvalResult'] + @property + def context(self): + return self._context + def __getattr__(self, var): return getattr(self._var, var) @@ -268,14 +272,3 @@ class EvalJs(object): else: sys.stderr.write('EXCEPTION: ' + str(e) + '\n') time.sleep(0.01) - - -#print x - -if __name__ == '__main__': - #with open('C:\Users\Piotrek\Desktop\esprima.js', 'rb') as f: - # x = f.read() - e = EvalJs() - e.execute('square(x)') - #e.execute(x) - e.console() diff --git a/lib/js2py/host/console.py b/lib/js2py/host/console.py index 50a08632..269eef3d 100644 --- a/lib/js2py/host/console.py +++ b/lib/js2py/host/console.py @@ -6,7 +6,7 @@ def console(): @Js def log(): - print(arguments[0]) + print(" ".join(repr(element) for element in arguments.to_list())) console.put('log', log) console.put('debug', log) diff --git a/lib/js2py/internals/__init__.py b/lib/js2py/internals/__init__.py index e69de29b..5473e711 100644 --- a/lib/js2py/internals/__init__.py +++ b/lib/js2py/internals/__init__.py @@ -0,0 +1 @@ +from .seval import eval_js_vm \ No newline at end of file diff --git a/lib/js2py/internals/base.py b/lib/js2py/internals/base.py index a02a2122..938dec37 100644 --- a/lib/js2py/internals/base.py +++ b/lib/js2py/internals/base.py @@ -602,11 +602,12 @@ class PyJsDate(PyJs): # todo fix this problematic datetime part def to_local_dt(self): - return datetime.datetime.utcfromtimestamp( - self.UTCToLocal(self.value) // 1000) + return datetime.datetime(1970, 1, 1) + datetime.timedelta( + seconds=self.UTCToLocal(self.value) // 1000) def to_utc_dt(self): - return datetime.datetime.utcfromtimestamp(self.value // 1000) + return datetime.datetime(1970, 1, 1) + datetime.timedelta( + seconds=self.value // 1000) def local_strftime(self, pattern): if self.value is NaN: diff --git a/lib/js2py/internals/constructors/jsdate.py b/lib/js2py/internals/constructors/jsdate.py index 98de6431..5aed830e 100644 --- a/lib/js2py/internals/constructors/jsdate.py +++ b/lib/js2py/internals/constructors/jsdate.py @@ -32,8 +32,7 @@ def UTC(year, month, date, hours, minutes, seconds, ms): # todo complete this mili = args[6].to_number() if l > 6 else Js(0) if not y.is_nan() and 0 <= y.value <= 99: y = y + Js(1900) - t = TimeClip(MakeDate(MakeDay(y, m, dt), MakeTime(h, mi, sec, mili))) - return PyJsDate(t, prototype=DatePrototype) + return TimeClip(MakeDate(MakeDay(y, m, dt), MakeTime(h, mi, sec, mili))) @Js @@ -76,11 +75,12 @@ class PyJsDate(PyJs): # todo fix this problematic datetime part def to_local_dt(self): - return datetime.datetime.utcfromtimestamp( - UTCToLocal(self.value) // 1000) + return datetime.datetime(1970, 1, 1) + datetime.timedelta( + seconds=UTCToLocal(self.value) // 1000) def to_utc_dt(self): - return datetime.datetime.utcfromtimestamp(self.value // 1000) + return datetime.datetime(1970, 1, 1) + datetime.timedelta( + seconds=self.value // 1000) def local_strftime(self, pattern): if self.value is NaN: @@ -332,7 +332,7 @@ class DateProto: check_date(this) t = UTCToLocal(this.value) tim = MakeTime( - HourFromTime(t), MinFromTime(t), SecFromTime(t), ms.to_int()) + Js(HourFromTime(t)), Js(MinFromTime(t)), Js(SecFromTime(t)), ms) u = TimeClip(LocalToUTC(MakeDate(Day(t), tim))) this.value = u return u @@ -341,12 +341,164 @@ class DateProto: check_date(this) t = this.value tim = MakeTime( - HourFromTime(t), MinFromTime(t), SecFromTime(t), ms.to_int()) + Js(HourFromTime(t)), Js(MinFromTime(t)), Js(SecFromTime(t)), ms) u = TimeClip(MakeDate(Day(t), tim)) this.value = u return u - # todo Complete all setters! + def setSeconds(sec, ms=None): + check_date(this) + t = UTCToLocal(this.value) + s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate( + Day(t), MakeTime(Js(HourFromTime(t)), Js(MinFromTime(t)), s, milli)) + u = TimeClip(LocalToUTC(date)) + this.value = u + return u + + def setUTCSeconds(sec, ms=None): + check_date(this) + t = this.value + s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate( + Day(t), MakeTime(Js(HourFromTime(t)), Js(MinFromTime(t)), s, milli)) + v = TimeClip(date) + this.value = v + return v + + def setMinutes(min, sec=None, ms=None): + check_date(this) + t = UTCToLocal(this.value) + m = min.to_number() + if not sec is None: s = Js(SecFromTime(t)) + else: s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate(Day(t), MakeTime(Js(HourFromTime(t)), m, s, milli)) + u = TimeClip(LocalToUTC(date)) + this.value = u + return u + + def setUTCMinutes(min, sec=None, ms=None): + check_date(this) + t = this.value + m = min.to_number() + if not sec is None: s = Js(SecFromTime(t)) + else: s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate(Day(t), MakeTime(Js(HourFromTime(t)), m, s, milli)) + v = TimeClip(date) + this.value = v + return v + + def setHours(hour, min=None, sec=None, ms=None): + check_date(this) + t = UTCToLocal(this.value) + h = hour.to_number() + if not min is None: m = Js(MinFromTime(t)) + else: m = min.to_number() + if not sec is None: s = Js(SecFromTime(t)) + else: s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate(Day(t), MakeTime(h, m, s, milli)) + u = TimeClip(LocalToUTC(date)) + this.value = u + return u + + def setUTCHours(hour, min=None, sec=None, ms=None): + check_date(this) + t = this.value + h = hour.to_number() + if not min is None: m = Js(MinFromTime(t)) + else: m = min.to_number() + if not sec is None: s = Js(SecFromTime(t)) + else: s = sec.to_number() + if not ms is None: milli = Js(msFromTime(t)) + else: milli = ms.to_number() + date = MakeDate(Day(t), MakeTime(h, m, s, milli)) + v = TimeClip(date) + this.value = v + return v + + def setDate(date): + check_date(this) + t = UTCToLocal(this.value) + dt = date.to_number() + newDate = MakeDate( + MakeDay(Js(YearFromTime(t)), Js(MonthFromTime(t)), dt), TimeWithinDay(t)) + u = TimeClip(LocalToUTC(newDate)) + this.value = u + return u + + def setUTCDate(date): + check_date(this) + t = this.value + dt = date.to_number() + newDate = MakeDate( + MakeDay(Js(YearFromTime(t)), Js(MonthFromTime(t)), dt), TimeWithinDay(t)) + v = TimeClip(newDate) + this.value = v + return v + + def setMonth(month, date=None): + check_date(this) + t = UTCToLocal(this.value) + m = month.to_number() + if not date is None: dt = Js(DateFromTime(t)) + else: dt = date.to_number() + newDate = MakeDate( + MakeDay(Js(YearFromTime(t)), m, dt), TimeWithinDay(t)) + u = TimeClip(LocalToUTC(newDate)) + this.value = u + return u + + def setUTCMonth(month, date=None): + check_date(this) + t = this.value + m = month.to_number() + if not date is None: dt = Js(DateFromTime(t)) + else: dt = date.to_number() + newDate = MakeDate( + MakeDay(Js(YearFromTime(t)), m, dt), TimeWithinDay(t)) + v = TimeClip(newDate) + this.value = v + return v + + def setFullYear(year, month=None, date=None): + check_date(this) + if not this.value is NaN: t = UTCToLocal(this.value) + else: t = 0 + y = year.to_number() + if not month is None: m = Js(MonthFromTime(t)) + else: m = month.to_number() + if not date is None: dt = Js(DateFromTime(t)) + else: dt = date.to_number() + newDate = MakeDate( + MakeDay(y, m, dt), TimeWithinDay(t)) + u = TimeClip(LocalToUTC(newDate)) + this.value = u + return u + + def setUTCFullYear(year, month=None, date=None): + check_date(this) + if not this.value is NaN: t = UTCToLocal(this.value) + else: t = 0 + y = year.to_number() + if not month is None: m = Js(MonthFromTime(t)) + else: m = month.to_number() + if not date is None: dt = Js(DateFromTime(t)) + else: dt = date.to_number() + newDate = MakeDate( + MakeDay(y, m, dt), TimeWithinDay(t)) + v = TimeClip(newDate) + this.value = v + return v def toUTCString(): check_date(this) diff --git a/lib/js2py/internals/constructors/jsmath.py b/lib/js2py/internals/constructors/jsmath.py index 3eb44616..c6fde67e 100644 --- a/lib/js2py/internals/constructors/jsmath.py +++ b/lib/js2py/internals/constructors/jsmath.py @@ -16,7 +16,8 @@ CONSTANTS = { 'SQRT1_2': 0.7071067811865476, 'SQRT2': 1.4142135623730951 } - +def is_infinity(x): + return x - 1e10 == x class MathFunctions: def abs(this, args): @@ -65,22 +66,22 @@ class MathFunctions: def ceil(this, args): x = get_arg(args, 0) a = to_number(x) - if a != a: # it must be a nan - return NaN + if not is_finite(x): + return x return float(math.ceil(a)) def floor(this, args): x = get_arg(args, 0) a = to_number(x) - if a != a: # it must be a nan - return NaN + if not is_finite(x): + return x return float(math.floor(a)) def round(this, args): x = get_arg(args, 0) a = to_number(x) - if a != a: # it must be a nan - return NaN + if not is_finite(x): + return x return float(round(a)) def sin(this, args): diff --git a/lib/js2py/internals/constructors/jsstring.py b/lib/js2py/internals/constructors/jsstring.py index f2b43831..20d92a4b 100644 --- a/lib/js2py/internals/constructors/jsstring.py +++ b/lib/js2py/internals/constructors/jsstring.py @@ -1,6 +1,6 @@ from ..conversions import * from ..func_utils import * - +from six import unichr def fromCharCode(this, args): res = u'' diff --git a/lib/js2py/internals/constructors/time_helpers.py b/lib/js2py/internals/constructors/time_helpers.py index eda95fb6..2d694b58 100644 --- a/lib/js2py/internals/constructors/time_helpers.py +++ b/lib/js2py/internals/constructors/time_helpers.py @@ -38,8 +38,8 @@ def DaylightSavingTA(t): return t try: return int( - LOCAL_ZONE.dst(datetime.datetime.utcfromtimestamp( - t // 1000)).seconds) * 1000 + LOCAL_ZONE.dst(datetime.datetime(1970, 1, 1) + datetime.timedelta( + seconds=t // 1000)).seconds) * 1000 except: warnings.warn( 'Invalid datetime date, assumed DST time, may be inaccurate...', diff --git a/lib/js2py/internals/opcodes.py b/lib/js2py/internals/opcodes.py index 15c57ccd..2f6c65f5 100644 --- a/lib/js2py/internals/opcodes.py +++ b/lib/js2py/internals/opcodes.py @@ -798,7 +798,7 @@ OP_CODES = {} g = '' for g in globals(): try: - if not issubclass(globals()[g], OP_CODE) or g is 'OP_CODE': + if not issubclass(globals()[g], OP_CODE) or g == 'OP_CODE': continue except: continue diff --git a/lib/js2py/internals/prototypes/jsstring.py b/lib/js2py/internals/prototypes/jsstring.py index be38802e..e8473af8 100644 --- a/lib/js2py/internals/prototypes/jsstring.py +++ b/lib/js2py/internals/prototypes/jsstring.py @@ -22,6 +22,11 @@ def replacement_template(rep, source, span, npar): res += '$' n += 2 continue + elif rep[n + 1] == '&': + # replace with matched string + res += source[span[0]:span[1]] + n += 2 + continue elif rep[n + 1] == '`': # replace with string that is BEFORE match res += source[:span[0]] diff --git a/lib/js2py/internals/speed.py b/lib/js2py/internals/speed.py index 3ca4079e..482eb7ea 100644 --- a/lib/js2py/internals/speed.py +++ b/lib/js2py/internals/speed.py @@ -1,7 +1,14 @@ +from __future__ import print_function + from timeit import timeit from collections import namedtuple from array import array -from itertools import izip +try: + #python 2 code + from itertools import izip as zip +except ImportError: + pass + from collections import deque @@ -47,7 +54,7 @@ t = [] Type = None try: - print timeit( + print(timeit( """ t.append(4) @@ -56,7 +63,7 @@ t.pop() """, - "from __main__ import X,Y,namedtuple,array,t,add,Type, izip", - number=1000000) + "from __main__ import X,Y,namedtuple,array,t,add,Type, zip", + number=1000000)) except: raise diff --git a/lib/js2py/legecy_translators/constants.py b/lib/js2py/legecy_translators/constants.py index ea048f12..8ada25ad 100644 --- a/lib/js2py/legecy_translators/constants.py +++ b/lib/js2py/legecy_translators/constants.py @@ -1,3 +1,4 @@ +from __future__ import print_function from string import ascii_lowercase, digits ################################## StringName = u'PyJsConstantString%d_' @@ -305,4 +306,4 @@ if __name__ == '__main__': ''') t, d = remove_constants(test) - print t, d + print(t, d) diff --git a/lib/js2py/legecy_translators/exps.py b/lib/js2py/legecy_translators/exps.py index 3c8f793c..bcd09b19 100644 --- a/lib/js2py/legecy_translators/exps.py +++ b/lib/js2py/legecy_translators/exps.py @@ -16,6 +16,8 @@ If case of parsing errors it must return a pos of error. NOTES: Strings and other literals are not present so each = means assignment """ +from __future__ import print_function + from utils import * from jsparser import * @@ -80,4 +82,4 @@ def bass_translator(s): if __name__ == '__main__': - print bass_translator('3.ddsd = 40') + print(bass_translator('3.ddsd = 40')) diff --git a/lib/js2py/legecy_translators/flow.py b/lib/js2py/legecy_translators/flow.py index 9e2bb0fa..60d99aa3 100644 --- a/lib/js2py/legecy_translators/flow.py +++ b/lib/js2py/legecy_translators/flow.py @@ -9,6 +9,8 @@ FOR 123 FOR iter CONTINUE, BREAK, RETURN, LABEL, THROW, TRY, SWITCH """ +from __future__ import print_function + from utils import * from jsparser import * from nodevisitor import exp_translator @@ -477,4 +479,4 @@ def translate_flow(source): if __name__ == '__main__': #print do_dowhile('do {} while(k+f)', 0)[0] #print 'e: "%s"'%do_expression('++(c?g:h); mj', 0)[0] - print translate_flow('a; yimport test')[0] + print(translate_flow('a; yimport test')[0]) diff --git a/lib/js2py/legecy_translators/functions.py b/lib/js2py/legecy_translators/functions.py index 4825eee0..ceffc9e4 100644 --- a/lib/js2py/legecy_translators/functions.py +++ b/lib/js2py/legecy_translators/functions.py @@ -1,4 +1,6 @@ """This module removes JS functions from source code""" +from __future__ import print_function + from jsparser import * from utils import * @@ -94,5 +96,5 @@ def remove_functions(source, all_inline=False): if __name__ == '__main__': - print remove_functions( - '5+5 function n (functiona ,functionaj) {dsd s, dsdd}') + print(remove_functions( + '5+5 function n (functiona ,functionaj) {dsd s, dsdd}')) diff --git a/lib/js2py/legecy_translators/jsparser.py b/lib/js2py/legecy_translators/jsparser.py index a09537d3..7452b078 100644 --- a/lib/js2py/legecy_translators/jsparser.py +++ b/lib/js2py/legecy_translators/jsparser.py @@ -45,6 +45,7 @@ TODO """ +from __future__ import print_function from utils import * @@ -64,7 +65,7 @@ OP_METHODS = { def dbg(source): try: - with open('C:\Users\Piotrek\Desktop\dbg.py', 'w') as f: + with open(r'C:\Users\Piotrek\Desktop\dbg.py', 'w') as f: f.write(source) except: pass @@ -77,13 +78,13 @@ def indent(lines, ind=4): def inject_before_lval(source, lval, code): if source.count(lval) > 1: dbg(source) - print - print lval + print() + print(lval) raise RuntimeError('To many lvals (%s)' % lval) elif not source.count(lval): dbg(source) - print - print lval + print() + print(lval) assert lval not in source raise RuntimeError('No lval found "%s"' % lval) end = source.index(lval) diff --git a/lib/js2py/legecy_translators/nodevisitor.py b/lib/js2py/legecy_translators/nodevisitor.py index 4d50545c..5e20654b 100644 --- a/lib/js2py/legecy_translators/nodevisitor.py +++ b/lib/js2py/legecy_translators/nodevisitor.py @@ -1,3 +1,5 @@ +from __future__ import print_function + from jsparser import * from utils import * import re @@ -557,6 +559,6 @@ if __name__ == '__main__': #print 'Here', trans('(eee ) . ii [ PyJsMarker ] [ jkj ] ( j , j ) . # jiji (h , ji , i)(non )( )()()()') for e in xrange(3): - print exp_translator('jk = kk.ik++') + print(exp_translator('jk = kk.ik++')) #First line translated with PyJs: PyJsStrictEq(PyJsAdd((Js(100)*Js(50)),Js(30)), Js("5030")), yay! - print exp_translator('delete a.f') + print(exp_translator('delete a.f')) diff --git a/lib/js2py/legecy_translators/objects.py b/lib/js2py/legecy_translators/objects.py index 2abda3e8..5cb9012a 100644 --- a/lib/js2py/legecy_translators/objects.py +++ b/lib/js2py/legecy_translators/objects.py @@ -1,6 +1,8 @@ """ This module removes all objects/arrays from JS source code and replace them with LVALS. Also it has s function translating removed object/array to python code. Use this module just after removing constants. Later move on to removing functions""" +from __future__ import print_function + OBJECT_LVAL = 'PyJsLvalObject%d_' ARRAY_LVAL = 'PyJsLvalArray%d_' from utils import * @@ -180,7 +182,7 @@ def translate_object(obj, lval, obj_count=1, arr_count=1): try: key, value = spl except: #len(spl)> 2 - print 'Unusual case ' + repr(e) + print('Unusual case ' + repr(e)) key = spl[0] value = ':'.join(spl[1:]) key = key.strip() @@ -293,8 +295,8 @@ if __name__ == '__main__': #print remove_objects(test) #print list(bracket_split(' {}')) - print - print remove_arrays( + print() + print(remove_arrays( 'typeof a&&!db.test(a)&&!ib[(bb.exec(a)||["",""], [][[5][5]])[1].toLowerCase()])' - ) - print is_object('', ')') + )) + print(is_object('', ')')) diff --git a/lib/js2py/legecy_translators/translator.py b/lib/js2py/legecy_translators/translator.py index 3573f06c..c0ad8459 100644 --- a/lib/js2py/legecy_translators/translator.py +++ b/lib/js2py/legecy_translators/translator.py @@ -1,3 +1,5 @@ +from __future__ import print_function + from flow import translate_flow from constants import remove_constants, recover_constants from objects import remove_objects, remove_arrays, translate_object, translate_array, set_func_translator @@ -148,4 +150,4 @@ if __name__ == '__main__': #res = translate_js(jq) res = translate_js(t) dbg(SANDBOX % indent(res)) - print 'Done' + print('Done') diff --git a/lib/js2py/node_import.py b/lib/js2py/node_import.py index 605c4b3a..21844314 100644 --- a/lib/js2py/node_import.py +++ b/lib/js2py/node_import.py @@ -1,10 +1,16 @@ __all__ = ['require'] + import subprocess, os, codecs, glob from .evaljs import translate_js, DEFAULT_HEADER +from .translators.friendly_nodes import is_valid_py_name import six +import tempfile +import hashlib +import random + DID_INIT = False -DIRNAME = os.path.dirname(os.path.abspath(__file__)) -PY_NODE_MODULES_PATH = os.path.join(DIRNAME, 'py_node_modules') +DIRNAME = tempfile.mkdtemp() +PY_NODE_MODULES_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'py_node_modules') def _init(): @@ -46,23 +52,33 @@ GET_FROM_GLOBALS_FUNC = ''' ''' + def _get_module_py_name(module_name): return module_name.replace('-', '_') + def _get_module_var_name(module_name): - return _get_module_py_name(module_name).rpartition('/')[-1] + cand = _get_module_py_name(module_name).rpartition('/')[-1] + if not is_valid_py_name(cand): + raise ValueError( + "Invalid Python module name %s (generated from %s). Unsupported/invalid npm module specification?" % ( + repr(cand), repr(module_name))) + return cand -def _get_and_translate_npm_module(module_name, include_polyfill=False, update=False): +def _get_and_translate_npm_module(module_name, include_polyfill=False, update=False, maybe_version_str=""): assert isinstance(module_name, str), 'module_name must be a string!' + py_name = _get_module_py_name(module_name) module_filename = '%s.py' % py_name var_name = _get_module_var_name(module_name) if not os.path.exists(os.path.join(PY_NODE_MODULES_PATH, module_filename)) or update: _init() - in_file_name = 'tmp0in439341018923js2py.js' - out_file_name = 'tmp0out439341018923js2py.js' + module_hash = hashlib.sha1(module_name.encode("utf-8")).hexdigest()[:15] + version = random.randrange(10000000000000) + in_file_name = 'in_%s_%d.js' % (module_hash, version) + out_file_name = 'out_%s_%d.js' % (module_hash, version) code = ADD_TO_GLOBALS_FUNC if include_polyfill: code += "\n;require('babel-polyfill');\n" @@ -74,6 +90,8 @@ def _get_and_translate_npm_module(module_name, include_polyfill=False, update=Fa f.write(code.encode('utf-8') if six.PY3 else code) pkg_name = module_name.partition('/')[0] + if maybe_version_str: + pkg_name += '@' + maybe_version_str # make sure the module is installed assert subprocess.call( 'cd %s;npm install %s' % (repr(DIRNAME), pkg_name), @@ -93,7 +111,7 @@ def _get_and_translate_npm_module(module_name, include_polyfill=False, update=Fa with codecs.open(os.path.join(DIRNAME, out_file_name), "r", "utf-8") as f: js_code = f.read() - os.remove(os.path.join(DIRNAME, out_file_name)) + print("Bundled JS library dumped at: %s" % os.path.join(DIRNAME, out_file_name)) if len(js_code) < 50: raise RuntimeError("Candidate JS bundle too short - likely browserify issue.") js_code += GET_FROM_GLOBALS_FUNC @@ -117,21 +135,25 @@ def _get_and_translate_npm_module(module_name, include_polyfill=False, update=Fa return py_code -def require(module_name, include_polyfill=False, update=False, context=None): +def require(module_name, include_polyfill=True, update=False, context=None): """ Installs the provided npm module, exports a js bundle via browserify, converts to ECMA 5.1 via babel and finally translates the generated JS bundle to Python via Js2Py. Returns a pure python object that behaves like the installed module. Nice! - :param module_name: Name of the npm module to require. For example 'esprima'. + :param module_name: Name of the npm module to require. For example 'esprima'. Supports specific versions via @ + specification. Eg: 'crypto-js@3.3'. :param include_polyfill: Whether the babel-polyfill should be included as part of the translation. May be needed - for some modules that use unsupported features. + for some modules that use unsupported features of JS6 such as Map or typed arrays. :param update: Whether to force update the translation. Otherwise uses a cached version if exists. :param context: Optional context in which the translated module should be executed in. If provided, the header (js2py imports) will be skipped as it is assumed that the context already has all the necessary imports. :return: The JsObjectWrapper containing the translated module object. Can be used like a standard python object. """ - py_code = _get_and_translate_npm_module(module_name, include_polyfill=include_polyfill, update=update) + module_name, maybe_version = (module_name+"@@@").split('@')[:2] + + py_code = _get_and_translate_npm_module(module_name, include_polyfill=include_polyfill, update=update, + maybe_version_str=maybe_version) # this is a bit hacky but we need to strip the default header from the generated code... if context is not None: if not py_code.startswith(DEFAULT_HEADER): @@ -141,5 +163,5 @@ def require(module_name, include_polyfill=False, update=False, context=None): assert py_code.startswith(DEFAULT_HEADER), "Unexpected header." py_code = py_code[len(DEFAULT_HEADER):] context = {} if context is None else context - exec (py_code, context) + exec(py_code, context) return context['var'][_get_module_var_name(module_name)].to_py() diff --git a/lib/js2py/prototypes/jsstring.py b/lib/js2py/prototypes/jsstring.py index ee320709..a313bfb9 100644 --- a/lib/js2py/prototypes/jsstring.py +++ b/lib/js2py/prototypes/jsstring.py @@ -17,6 +17,11 @@ def replacement_template(rep, source, span, npar): res += '$' n += 2 continue + elif rep[n + 1] == '&': + # replace with matched string + res += source[span[0]:span[1]] + n += 2 + continue elif rep[n + 1] == '`': # replace with string that is BEFORE match res += source[:span[0]] diff --git a/lib/js2py/translators/translating_nodes.py b/lib/js2py/translators/translating_nodes.py index 371c8ede..4e2b5760 100644 --- a/lib/js2py/translators/translating_nodes.py +++ b/lib/js2py/translators/translating_nodes.py @@ -14,26 +14,36 @@ if six.PY3: LINE_LEN_LIMIT = 400 # 200 # or any other value - the larger the smaller probability of errors :) -class ForController: +class LoopController: def __init__(self): - self.inside = [False] - self.update = '' + self.update = [""] + self.label_to_update_idx = {} - def enter_for(self, update): - self.inside.append(True) - self.update = update + def enter(self, update=""): + self.update.append(update) - def leave_for(self): - self.inside.pop() + def leave(self): + self.update.pop() + + def get_update(self, label=None): + if label is None: + return self.update[-1] + if label not in self.label_to_update_idx: + raise SyntaxError("Undefined label %s" % label) + if self.label_to_update_idx[label] >= len(self.update): + raise SyntaxError("%s is not a iteration statement label?" % label) + return self.update[self.label_to_update_idx[label]] + + def register_label(self, label): + if label in self.label_to_update_idx: + raise SyntaxError("label %s already used") + self.label_to_update_idx[label] = len(self.update) + + def deregister_label(self, label): + del self.label_to_update_idx[label] - def enter_other(self): - self.inside.append(False) - def leave_other(self): - self.inside.pop() - def is_inside(self): - return self.inside[-1] class InlineStack: @@ -86,9 +96,10 @@ class ContextStack: def clean_stacks(): - global Context, inline_stack + global Context, inline_stack, loop_controller Context = ContextStack() inline_stack = InlineStack() + loop_controller = LoopController() def to_key(literal_or_identifier): @@ -108,6 +119,13 @@ def to_key(literal_or_identifier): else: return unicode(k) +def is_iteration_statement(cand): + if not isinstance(cand, dict): + # Multiple statements. + return False + return cand.get("type", "?") in {"ForStatement", "ForInStatement", "WhileStatement", "DoWhileStatement"} + + def trans(ele, standard=False): """Translates esprima syntax tree to python by delegating to appropriate translating node""" @@ -367,9 +385,14 @@ def BreakStatement(type, label): def ContinueStatement(type, label): if label: - return 'raise %s("Continued")\n' % (get_continue_label(label['name'])) + maybe_update_expr = loop_controller.get_update(label=label['name']) + continue_stmt = 'raise %s("Continued")\n' % (get_continue_label(label['name'])) else: - return 'continue\n' + maybe_update_expr = loop_controller.get_update() + continue_stmt = "continue\n" + if maybe_update_expr: + return "# continue update\n%s\n%s" % (maybe_update_expr, continue_stmt) + return continue_stmt def ReturnStatement(type, argument): @@ -386,24 +409,28 @@ def DebuggerStatement(type): def DoWhileStatement(type, body, test): - inside = trans(body) + 'if not %s:\n' % trans(test) + indent('break\n') + loop_controller.enter() + body_code = trans(body) + loop_controller.leave() + inside = body_code + 'if not %s:\n' % trans(test) + indent('break\n') result = 'while 1:\n' + indent(inside) return result def ForStatement(type, init, test, update, body): - update = indent(trans(update)) if update else '' + update = trans(update) if update else '' init = trans(init) if init else '' if not init.endswith('\n'): init += '\n' test = trans(test) if test else '1' + loop_controller.enter(update) if not update: result = '#for JS loop\n%swhile %s:\n%s%s\n' % ( init, test, indent(trans(body)), update) else: result = '#for JS loop\n%swhile %s:\n' % (init, test) - body = 'try:\n%sfinally:\n %s\n' % (indent(trans(body)), update) - result += indent(body) + result += indent("%s# update\n%s\n" % (trans(body), update)) + loop_controller.leave() return result @@ -422,7 +449,9 @@ def ForInStatement(type, left, right, body, each): name = left['name'] else: raise RuntimeError('Unusual ForIn loop') + loop_controller.enter() res += indent('var.put(%s, PyJsTemp)\n' % repr(name) + trans(body)) + loop_controller.leave() return res @@ -438,20 +467,23 @@ def IfStatement(type, test, consequent, alternate): def LabeledStatement(type, label, body): # todo consider using smarter approach! + label_name = label['name'] + loop_controller.register_label(label_name) inside = trans(body) + loop_controller.deregister_label(label_name) defs = '' - if inside.startswith('while ') or inside.startswith( - 'for ') or inside.startswith('#for'): + if is_iteration_statement(body) and (inside.startswith('while ') or inside.startswith( + 'for ') or inside.startswith('#for')): # we have to add contine label as well... # 3 or 1 since #for loop type has more lines before real for. sep = 1 if not inside.startswith('#for') else 3 - cont_label = get_continue_label(label['name']) + cont_label = get_continue_label(label_name) temp = inside.split('\n') injected = 'try:\n' + '\n'.join(temp[sep:]) injected += 'except %s:\n pass\n' % cont_label inside = '\n'.join(temp[:sep]) + '\n' + indent(injected) defs += 'class %s(Exception): pass\n' % cont_label - break_label = get_break_label(label['name']) + break_label = get_break_label(label_name) inside = 'try:\n%sexcept %s:\n pass\n' % (indent(inside), break_label) defs += 'class %s(Exception): pass\n' % break_label return defs + inside @@ -546,7 +578,11 @@ def VariableDeclaration(type, declarations, kind): def WhileStatement(type, test, body): - result = 'while %s:\n' % trans(test) + indent(trans(body)) + test_code = trans(test) + loop_controller.enter() + body_code = trans(body) + loop_controller.leave() + result = 'while %s:\n' % test_code + indent(body_code) return result diff --git a/lib/js2py/translators/translator.py b/lib/js2py/translators/translator.py index 16ed5bdb..5ec47002 100644 --- a/lib/js2py/translators/translator.py +++ b/lib/js2py/translators/translator.py @@ -55,16 +55,19 @@ def dbg(x): """does nothing, legacy dummy function""" return '' +# Another way of doing that would be with my auto esprima translation but its much slower: +# parsed = esprima.parse(js).to_dict() +def pyjsparser_parse_fn(code): + parser = pyjsparser.PyJsParser() + return parser.parse(code) -def translate_js(js, HEADER=DEFAULT_HEADER, use_compilation_plan=False): +def translate_js(js, HEADER=DEFAULT_HEADER, use_compilation_plan=False, parse_fn=pyjsparser_parse_fn): """js has to be a javascript source code. returns equivalent python code.""" if use_compilation_plan and not '//' in js and not '/*' in js: return translate_js_with_compilation_plan(js, HEADER=HEADER) - parser = pyjsparser.PyJsParser() - parsed = parser.parse(js) # js to esprima syntax tree - # Another way of doing that would be with my auto esprima translation but its much slower and causes import problems: - # parsed = esprima.parse(js).to_dict() + + parsed = parse_fn(js) translating_nodes.clean_stacks() return HEADER + translating_nodes.trans( parsed) # syntax tree to python code diff --git a/lib/js2py/utils/injector.py b/lib/js2py/utils/injector.py index ea236d5e..88e0d93e 100644 --- a/lib/js2py/utils/injector.py +++ b/lib/js2py/utils/injector.py @@ -26,17 +26,19 @@ def fix_js_args(func): return func code = append_arguments(six.get_function_code(func), ('this', 'arguments')) - return types.FunctionType( + result = types.FunctionType( code, six.get_function_globals(func), func.__name__, closure=six.get_function_closure(func)) + return result def append_arguments(code_obj, new_locals): co_varnames = code_obj.co_varnames # Old locals co_names = code_obj.co_names # Old globals - co_names += tuple(e for e in new_locals if e not in co_names) + new_args = tuple(e for e in new_locals if e not in co_names) + co_names += new_args co_argcount = code_obj.co_argcount # Argument count co_code = code_obj.co_code # The actual bytecode as a string @@ -76,26 +78,51 @@ def append_arguments(code_obj, new_locals): names_to_varnames = dict( (co_names.index(name), varnames.index(name)) for name in new_locals) + is_new_bytecode = sys.version_info >= (3, 11) # Now we modify the actual bytecode modified = [] + drop_future_cache = False for inst in instructions(code_obj): + if is_new_bytecode and inst.opname == "CACHE": + assert inst.arg == 0 + if not drop_future_cache: + modified.extend(write_instruction(inst.opcode, inst.arg)) + else: + # We need to inject NOOP to not break jumps :( + modified.extend(write_instruction(dis.opmap["NOP"], 0)) + + continue op, arg = inst.opcode, inst.arg # If the instruction is a LOAD_GLOBAL, we have to check to see if # it's one of the globals that we are replacing. Either way, # update its arg using the appropriate dict. + drop_future_cache = False if inst.opcode == LOAD_GLOBAL: - if inst.arg in names_to_varnames: + idx = inst.arg + if is_new_bytecode: + idx = idx // 2 + if idx in names_to_varnames: op = LOAD_FAST - arg = names_to_varnames[inst.arg] - elif inst.arg in name_translations: - arg = name_translations[inst.arg] + arg = names_to_varnames[idx] + # Cache is not present after LOAD_FAST and needs to be removed. + drop_future_cache = True + elif idx in name_translations: + tgt = name_translations[idx] + if is_new_bytecode: + tgt = 2*tgt + (inst.arg % 2) + arg = tgt else: - raise ValueError("a name was lost in translation") + raise(ValueError("a name was lost in translation last instruction %s" % str(inst))) # If it accesses co_varnames or co_names then update its argument. elif inst.opcode in opcode.haslocal: arg = varname_translations[inst.arg] elif inst.opcode in opcode.hasname: + # for example STORE_GLOBAL arg = name_translations[inst.arg] + elif is_new_bytecode and inst.opcode in opcode.hasfree: + # Python 3.11+ adds refs at the end (after locals), for whatever reason... + if inst.argval not in code_obj.co_varnames[:code_obj.co_argcount]: # we do not need to remap existing arguments, they are not shifted by new ones. + arg = inst.arg + len(new_locals) modified.extend(write_instruction(op, arg)) if six.PY2: code = ''.join(modified) @@ -113,23 +140,26 @@ def append_arguments(code_obj, new_locals): code_obj.co_filename, code_obj.co_name, code_obj.co_firstlineno, code_obj.co_lnotab, code_obj.co_freevars, code_obj.co_cellvars) - # Done modifying codestring - make the code object if hasattr(code_obj, "replace"): # Python 3.8+ - return code_obj.replace( + code_obj = code_obj.replace( co_argcount=co_argcount + new_locals_len, co_nlocals=code_obj.co_nlocals + new_locals_len, co_code=code, co_names=names, co_varnames=varnames) + return code_obj else: return types.CodeType(*args) -def instructions(code_obj): - # easy for python 3.4+ - if sys.version_info >= (3, 4): +def instructions(code_obj, show_cache=True): + if sys.version_info >= (3, 11): + # Python 3.11 introduced "cache instructions", hidden by default. + for inst in dis.Bytecode(code_obj, show_caches=show_cache): + yield inst + elif sys.version_info >= (3, 4): # easy for python 3.4+ for inst in dis.Bytecode(code_obj): yield inst else: @@ -171,7 +201,7 @@ def write_instruction(op, arg): chr((arg >> 8) & 255) ] else: - raise ValueError("Invalid oparg: {0} is too large".format(oparg)) + raise ValueError("Invalid oparg: {0} is too large".format(arg)) else: # python 3.6+ uses wordcode instead of bytecode and they already supply all the EXTENDEND_ARG ops :) if arg is None: return [chr(op), 0] @@ -191,6 +221,7 @@ def write_instruction(op, arg): # raise ValueError("Invalid oparg: {0} is too large".format(oparg)) + def check(code_obj): old_bytecode = code_obj.co_code insts = list(instructions(code_obj)) @@ -221,24 +252,99 @@ def check(code_obj): 'Your python version made changes to the bytecode') + + +def signature(func): + code_obj = six.get_function_code(func) + return (code_obj.co_nlocals, code_obj.co_argcount, code_obj.co_nlocals, code_obj.co_stacksize, + code_obj.co_flags, code_obj.co_names, code_obj.co_varnames, + code_obj.co_filename, + code_obj.co_freevars, code_obj.co_cellvars) + check(six.get_function_code(check)) + + +def compare_func(fake_func, gt_func): + print(signature(fake_func)) + print(signature(gt_func)) + assert signature(fake_func) == signature(gt_func) + fake_ins = list(instructions(six.get_function_code(fake_func), show_cache=False)) + real_ins = list(instructions(six.get_function_code(gt_func), show_cache=False)) + offset = 0 + pos = 0 + for e in fake_ins: + if e.opname == "NOP": + offset += 1 # ignore NOPs that are inserted in place of old cache. + else: + real = real_ins[pos] + fake = e + print("POS %d OFFSET: %d FAKE VS REAL" % (pos, offset)) + print(fake) + print(real) + assert fake.opcode == real.opcode + if fake.opcode in dis.hasjabs or fake.opcode in dis.hasjrel: + pass + else: + assert fake.arg == real.arg + assert fake.argval == real.argval or fake.opname in ["LOAD_CONST"] + assert fake.is_jump_target == real.is_jump_target + + pos += 1 + assert pos == len(real_ins), (pos, len(real_ins)) + print("DONE, looks good.") + + if __name__ == '__main__': - x = 'Wrong' - dick = 3000 + import faulthandler - def func(a): - print(x, y, z, a) - print(dick) - d = (x, ) - for e in (e for e in x): - print(e) - return x, y, z + faulthandler.enable() - func2 = types.FunctionType( - append_arguments(six.get_function_code(func), ('x', 'y', 'z')), - six.get_function_globals(func), - func.__name__, - closure=six.get_function_closure(func)) - args = (2, 2, 3, 4), 3, 4 - assert func2(1, *args) == args + def func(cmpfn): + if not this.Class in ('Array', 'Arguments'): + return this.to_object() # do nothing + arr = [] + for i in xrange(len(this)): + arr.append(this.get(six.text_type(i))) + + if not arr: + return this + if not cmpfn.is_callable(): + cmpfn = None + cmp = lambda a, b: sort_compare(a, b, cmpfn) + if six.PY3: + key = functools.cmp_to_key(cmp) + arr.sort(key=key) + else: + arr.sort(cmp=cmp) + for i in xrange(len(arr)): + this.put(six.text_type(i), arr[i]) + + return this + + + def func_gt(cmpfn, this, arguments): + if not this.Class in ('Array', 'Arguments'): + return this.to_object() # do nothing + arr = [] + for i in xrange(len(this)): + arr.append(this.get(six.text_type(i))) + + if not arr: + return this + if not cmpfn.is_callable(): + cmpfn = None + cmp = lambda a, b: sort_compare(a, b, cmpfn) + if six.PY3: + key = functools.cmp_to_key(cmp) + arr.sort(key=key) + else: + arr.sort(cmp=cmp) + for i in xrange(len(arr)): + this.put(six.text_type(i), arr[i]) + + return this + + + func2 = fix_js_args(func) + compare_func(func2, func_gt) diff --git a/lib/xmltodict.py b/lib/xmltodict.py new file mode 100644 index 00000000..f41e61fb --- /dev/null +++ b/lib/xmltodict.py @@ -0,0 +1,546 @@ +#!/usr/bin/env python +"Makes working with XML feel like you are working with JSON" + +try: + from defusedexpat import pyexpat as expat +except ImportError: + from xml.parsers import expat +from xml.sax.saxutils import XMLGenerator +from xml.sax.xmlreader import AttributesImpl +try: # pragma no cover + from cStringIO import StringIO +except ImportError: # pragma no cover + try: + from StringIO import StringIO + except ImportError: + from io import StringIO + +_dict = dict +import platform +if tuple(map(int, platform.python_version_tuple()[:2])) < (3, 7): + from collections import OrderedDict as _dict + +from inspect import isgenerator + +try: # pragma no cover + _basestring = basestring +except NameError: # pragma no cover + _basestring = str +try: # pragma no cover + _unicode = unicode +except NameError: # pragma no cover + _unicode = str + +__author__ = 'Martin Blech' +__version__ = '0.13.0' +__license__ = 'MIT' + + +class ParsingInterrupted(Exception): + pass + + +class _DictSAXHandler(object): + def __init__(self, + item_depth=0, + item_callback=lambda *args: True, + xml_attribs=True, + attr_prefix='@', + cdata_key='#text', + force_cdata=False, + cdata_separator='', + postprocessor=None, + dict_constructor=_dict, + strip_whitespace=True, + namespace_separator=':', + namespaces=None, + force_list=None, + comment_key='#comment'): + self.path = [] + self.stack = [] + self.data = [] + self.item = None + self.item_depth = item_depth + self.xml_attribs = xml_attribs + self.item_callback = item_callback + self.attr_prefix = attr_prefix + self.cdata_key = cdata_key + self.force_cdata = force_cdata + self.cdata_separator = cdata_separator + self.postprocessor = postprocessor + self.dict_constructor = dict_constructor + self.strip_whitespace = strip_whitespace + self.namespace_separator = namespace_separator + self.namespaces = namespaces + self.namespace_declarations = dict_constructor() + self.force_list = force_list + self.comment_key = comment_key + + def _build_name(self, full_name): + if self.namespaces is None: + return full_name + i = full_name.rfind(self.namespace_separator) + if i == -1: + return full_name + namespace, name = full_name[:i], full_name[i+1:] + try: + short_namespace = self.namespaces[namespace] + except KeyError: + short_namespace = namespace + if not short_namespace: + return name + else: + return self.namespace_separator.join((short_namespace, name)) + + def _attrs_to_dict(self, attrs): + if isinstance(attrs, dict): + return attrs + return self.dict_constructor(zip(attrs[0::2], attrs[1::2])) + + def startNamespaceDecl(self, prefix, uri): + self.namespace_declarations[prefix or ''] = uri + + def startElement(self, full_name, attrs): + name = self._build_name(full_name) + attrs = self._attrs_to_dict(attrs) + if attrs and self.namespace_declarations: + attrs['xmlns'] = self.namespace_declarations + self.namespace_declarations = self.dict_constructor() + self.path.append((name, attrs or None)) + if len(self.path) >= self.item_depth: + self.stack.append((self.item, self.data)) + if self.xml_attribs: + attr_entries = [] + for key, value in attrs.items(): + key = self.attr_prefix+self._build_name(key) + if self.postprocessor: + entry = self.postprocessor(self.path, key, value) + else: + entry = (key, value) + if entry: + attr_entries.append(entry) + attrs = self.dict_constructor(attr_entries) + else: + attrs = None + self.item = attrs or None + self.data = [] + + def endElement(self, full_name): + name = self._build_name(full_name) + if len(self.path) == self.item_depth: + item = self.item + if item is None: + item = (None if not self.data + else self.cdata_separator.join(self.data)) + + should_continue = self.item_callback(self.path, item) + if not should_continue: + raise ParsingInterrupted() + if self.stack: + data = (None if not self.data + else self.cdata_separator.join(self.data)) + item = self.item + self.item, self.data = self.stack.pop() + if self.strip_whitespace and data and item: + data = data.strip() or None + if data and self.force_cdata and item is None: + item = self.dict_constructor() + if item is not None: + if data: + self.push_data(item, self.cdata_key, data) + self.item = self.push_data(self.item, name, item) + else: + self.item = self.push_data(self.item, name, data) + else: + self.item = None + self.data = [] + self.path.pop() + + def characters(self, data): + if not self.data: + self.data = [data] + else: + self.data.append(data) + + def comments(self, data): + if self.strip_whitespace: + data = data.strip() + self.item = self.push_data(self.item, self.comment_key, data) + + def push_data(self, item, key, data): + if self.postprocessor is not None: + result = self.postprocessor(self.path, key, data) + if result is None: + return item + key, data = result + if item is None: + item = self.dict_constructor() + try: + value = item[key] + if isinstance(value, list): + value.append(data) + else: + item[key] = [value, data] + except KeyError: + if self._should_force_list(key, data): + item[key] = [data] + else: + item[key] = data + return item + + def _should_force_list(self, key, value): + if not self.force_list: + return False + if isinstance(self.force_list, bool): + return self.force_list + try: + return key in self.force_list + except TypeError: + return self.force_list(self.path[:-1], key, value) + + +def parse(xml_input, encoding=None, expat=expat, process_namespaces=False, + namespace_separator=':', disable_entities=True, process_comments=False, **kwargs): + """Parse the given XML input and convert it into a dictionary. + + `xml_input` can either be a `string`, a file-like object, or a generator of strings. + + If `xml_attribs` is `True`, element attributes are put in the dictionary + among regular child elements, using `@` as a prefix to avoid collisions. If + set to `False`, they are just ignored. + + Simple example:: + + >>> import xmltodict + >>> doc = xmltodict.parse(\"\"\" + ... <a prop="x"> + ... <b>1</b> + ... <b>2</b> + ... </a> + ... \"\"\") + >>> doc['a']['@prop'] + u'x' + >>> doc['a']['b'] + [u'1', u'2'] + + If `item_depth` is `0`, the function returns a dictionary for the root + element (default behavior). Otherwise, it calls `item_callback` every time + an item at the specified depth is found and returns `None` in the end + (streaming mode). + + The callback function receives two parameters: the `path` from the document + root to the item (name-attribs pairs), and the `item` (dict). If the + callback's return value is false-ish, parsing will be stopped with the + :class:`ParsingInterrupted` exception. + + Streaming example:: + + >>> def handle(path, item): + ... print('path:%s item:%s' % (path, item)) + ... return True + ... + >>> xmltodict.parse(\"\"\" + ... <a prop="x"> + ... <b>1</b> + ... <b>2</b> + ... </a>\"\"\", item_depth=2, item_callback=handle) + path:[(u'a', {u'prop': u'x'}), (u'b', None)] item:1 + path:[(u'a', {u'prop': u'x'}), (u'b', None)] item:2 + + The optional argument `postprocessor` is a function that takes `path`, + `key` and `value` as positional arguments and returns a new `(key, value)` + pair where both `key` and `value` may have changed. Usage example:: + + >>> def postprocessor(path, key, value): + ... try: + ... return key + ':int', int(value) + ... except (ValueError, TypeError): + ... return key, value + >>> xmltodict.parse('<a><b>1</b><b>2</b><b>x</b></a>', + ... postprocessor=postprocessor) + {'a': {'b:int': [1, 2], 'b': 'x'}} + + You can pass an alternate version of `expat` (such as `defusedexpat`) by + using the `expat` parameter. E.g: + + >>> import defusedexpat + >>> xmltodict.parse('<a>hello</a>', expat=defusedexpat.pyexpat) + {'a': 'hello'} + + You can use the force_list argument to force lists to be created even + when there is only a single child of a given level of hierarchy. The + force_list argument is a tuple of keys. If the key for a given level + of hierarchy is in the force_list argument, that level of hierarchy + will have a list as a child (even if there is only one sub-element). + The index_keys operation takes precedence over this. This is applied + after any user-supplied postprocessor has already run. + + For example, given this input: + <servers> + <server> + <name>host1</name> + <os>Linux</os> + <interfaces> + <interface> + <name>em0</name> + <ip_address>10.0.0.1</ip_address> + </interface> + </interfaces> + </server> + </servers> + + If called with force_list=('interface',), it will produce + this dictionary: + {'servers': + {'server': + {'name': 'host1', + 'os': 'Linux'}, + 'interfaces': + {'interface': + [ {'name': 'em0', 'ip_address': '10.0.0.1' } ] } } } + + `force_list` can also be a callable that receives `path`, `key` and + `value`. This is helpful in cases where the logic that decides whether + a list should be forced is more complex. + + + If `process_comment` is `True` then comment will be added with comment_key + (default=`'#comment'`) to then tag which contains comment + + For example, given this input: + <a> + <b> + <!-- b comment --> + <c> + <!-- c comment --> + 1 + </c> + <d>2</d> + </b> + </a> + + If called with process_comment=True, it will produce + this dictionary: + 'a': { + 'b': { + '#comment': 'b comment', + 'c': { + + '#comment': 'c comment', + '#text': '1', + }, + 'd': '2', + }, + } + """ + handler = _DictSAXHandler(namespace_separator=namespace_separator, + **kwargs) + if isinstance(xml_input, _unicode): + if not encoding: + encoding = 'utf-8' + xml_input = xml_input.encode(encoding) + if not process_namespaces: + namespace_separator = None + parser = expat.ParserCreate( + encoding, + namespace_separator + ) + try: + parser.ordered_attributes = True + except AttributeError: + # Jython's expat does not support ordered_attributes + pass + parser.StartNamespaceDeclHandler = handler.startNamespaceDecl + parser.StartElementHandler = handler.startElement + parser.EndElementHandler = handler.endElement + parser.CharacterDataHandler = handler.characters + if process_comments: + parser.CommentHandler = handler.comments + parser.buffer_text = True + if disable_entities: + try: + # Attempt to disable DTD in Jython's expat parser (Xerces-J). + feature = "http://apache.org/xml/features/disallow-doctype-decl" + parser._reader.setFeature(feature, True) + except AttributeError: + # For CPython / expat parser. + # Anything not handled ends up here and entities aren't expanded. + parser.DefaultHandler = lambda x: None + # Expects an integer return; zero means failure -> expat.ExpatError. + parser.ExternalEntityRefHandler = lambda *x: 1 + if hasattr(xml_input, 'read'): + parser.ParseFile(xml_input) + elif isgenerator(xml_input): + for chunk in xml_input: + parser.Parse(chunk, False) + parser.Parse(b'', True) + else: + parser.Parse(xml_input, True) + return handler.item + + +def _process_namespace(name, namespaces, ns_sep=':', attr_prefix='@'): + if not namespaces: + return name + try: + ns, name = name.rsplit(ns_sep, 1) + except ValueError: + pass + else: + ns_res = namespaces.get(ns.strip(attr_prefix)) + name = '{}{}{}{}'.format( + attr_prefix if ns.startswith(attr_prefix) else '', + ns_res, ns_sep, name) if ns_res else name + return name + + +def _emit(key, value, content_handler, + attr_prefix='@', + cdata_key='#text', + depth=0, + preprocessor=None, + pretty=False, + newl='\n', + indent='\t', + namespace_separator=':', + namespaces=None, + full_document=True, + expand_iter=None): + key = _process_namespace(key, namespaces, namespace_separator, attr_prefix) + if preprocessor is not None: + result = preprocessor(key, value) + if result is None: + return + key, value = result + if (not hasattr(value, '__iter__') + or isinstance(value, _basestring) + or isinstance(value, dict)): + value = [value] + for index, v in enumerate(value): + if full_document and depth == 0 and index > 0: + raise ValueError('document with multiple roots') + if v is None: + v = _dict() + elif isinstance(v, bool): + if v: + v = _unicode('true') + else: + v = _unicode('false') + elif not isinstance(v, dict): + if expand_iter and hasattr(v, '__iter__') and not isinstance(v, _basestring): + v = _dict(((expand_iter, v),)) + else: + v = _unicode(v) + if isinstance(v, _basestring): + v = _dict(((cdata_key, v),)) + cdata = None + attrs = _dict() + children = [] + for ik, iv in v.items(): + if ik == cdata_key: + cdata = iv + continue + if ik.startswith(attr_prefix): + ik = _process_namespace(ik, namespaces, namespace_separator, + attr_prefix) + if ik == '@xmlns' and isinstance(iv, dict): + for k, v in iv.items(): + attr = 'xmlns{}'.format(':{}'.format(k) if k else '') + attrs[attr] = _unicode(v) + continue + if not isinstance(iv, _unicode): + iv = _unicode(iv) + attrs[ik[len(attr_prefix):]] = iv + continue + children.append((ik, iv)) + if type(indent) is int: + indent = ' ' * indent + if pretty: + content_handler.ignorableWhitespace(depth * indent) + content_handler.startElement(key, AttributesImpl(attrs)) + if pretty and children: + content_handler.ignorableWhitespace(newl) + for child_key, child_value in children: + _emit(child_key, child_value, content_handler, + attr_prefix, cdata_key, depth+1, preprocessor, + pretty, newl, indent, namespaces=namespaces, + namespace_separator=namespace_separator, + expand_iter=expand_iter) + if cdata is not None: + content_handler.characters(cdata) + if pretty and children: + content_handler.ignorableWhitespace(depth * indent) + content_handler.endElement(key) + if pretty and depth: + content_handler.ignorableWhitespace(newl) + + +def unparse(input_dict, output=None, encoding='utf-8', full_document=True, + short_empty_elements=False, + **kwargs): + """Emit an XML document for the given `input_dict` (reverse of `parse`). + + The resulting XML document is returned as a string, but if `output` (a + file-like object) is specified, it is written there instead. + + Dictionary keys prefixed with `attr_prefix` (default=`'@'`) are interpreted + as XML node attributes, whereas keys equal to `cdata_key` + (default=`'#text'`) are treated as character data. + + The `pretty` parameter (default=`False`) enables pretty-printing. In this + mode, lines are terminated with `'\n'` and indented with `'\t'`, but this + can be customized with the `newl` and `indent` parameters. + + """ + if full_document and len(input_dict) != 1: + raise ValueError('Document must have exactly one root.') + must_return = False + if output is None: + output = StringIO() + must_return = True + if short_empty_elements: + content_handler = XMLGenerator(output, encoding, True) + else: + content_handler = XMLGenerator(output, encoding) + if full_document: + content_handler.startDocument() + for key, value in input_dict.items(): + _emit(key, value, content_handler, full_document=full_document, + **kwargs) + if full_document: + content_handler.endDocument() + if must_return: + value = output.getvalue() + try: # pragma no cover + value = value.decode(encoding) + except AttributeError: # pragma no cover + pass + return value + + +if __name__ == '__main__': # pragma: no cover + import sys + import marshal + try: + stdin = sys.stdin.buffer + stdout = sys.stdout.buffer + except AttributeError: + stdin = sys.stdin + stdout = sys.stdout + + (item_depth,) = sys.argv[1:] + item_depth = int(item_depth) + + def handle_item(path, item): + marshal.dump((path, item), stdout) + return True + + try: + root = parse(stdin, + item_depth=item_depth, + item_callback=handle_item, + dict_constructor=dict) + if item_depth == 0: + handle_item([], root) + except KeyboardInterrupt: + pass \ No newline at end of file diff --git a/platformcode/platformtools.py b/platformcode/platformtools.py index d18776bc..bbb637af 100644 --- a/platformcode/platformtools.py +++ b/platformcode/platformtools.py @@ -1107,7 +1107,7 @@ def play_video(item, strm=False, force_direct=False, autoplay=False): # logger.error('Failed to resolve hostname, fallback to normal dns') from core import support # support.dbg() - if '|' not in mediaurl and item.referer != False: + if '|' not in mediaurl and item.referer != False and 'youtube' not in mediaurl: mediaurl = mediaurl + '|' + urllib.urlencode(headers) # video information is obtained. @@ -1618,6 +1618,8 @@ def play_torrent(item, xlistitem, mediaurl): xbmc.sleep(3000) xbmc.executebuiltin("PlayMedia(" + torrent_options[selection][1] % mediaurl + ")") + + def resume_playback(played_time): class ResumePlayback(xbmcgui.WindowXMLDialog): Close = False @@ -1651,14 +1653,26 @@ def resume_playback(played_time): self.set_values(False) self.close() + if played_time and played_time > 30: - Dialog = ResumePlayback('ResumePlayback.xml', config.get_runtime_path(), played_time=played_time) - Dialog.show() - t = 0 - while not Dialog.is_close() and t < 100: - t += 1 - xbmc.sleep(100) - if not Dialog.Resume: played_time = 0 + if config.get_setting('resume_menu') == 0: + Dialog = ResumePlayback('ResumePlayback.xml', config.get_runtime_path(), played_time=played_time) + Dialog.show() + t = 0 + while not Dialog.is_close() and t < 100: + t += 1 + xbmc.sleep(100) + if not Dialog.Resume: played_time = 0 + else: + m, s = divmod(played_time, 60) + h, m = divmod(m, 60) + idx = xbmcgui.Dialog().contextmenu( + [ + xbmc.getLocalizedString(12022).format('%02d:%02d:%02d' % (h, m, s)), + xbmc.getLocalizedString(12021) + ]) + if idx in [-1, 0]: played_time = 0 + else: played_time = 0 xbmc.sleep(300) return played_time @@ -2031,4 +2045,4 @@ def serverWindow(item, itemlist, runDirectly=True): from platformcode.launcher import run run(selection) else: - return selection \ No newline at end of file + return selection diff --git a/platformcode/xbmc_videolibrary.py b/platformcode/xbmc_videolibrary.py index 1bb27ab1..8c37451a 100644 --- a/platformcode/xbmc_videolibrary.py +++ b/platformcode/xbmc_videolibrary.py @@ -617,7 +617,10 @@ def set_content(content_type, silent=False, custom=False): values.append(r['addonid']) if not custom: if content_type == 'movie': - seleccion = values.index('metadata.themoviedb.org') + if PY3: + seleccion = values.index('metadata.themoviedb.org.python') + else: + seleccion = values.index('metadata.themoviedb.org') else: seleccion = values.index('metadata.tvshows.themoviedb.org.python') else: diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 51c428d8..c2eebb4e 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -199,6 +199,18 @@ msgctxt "#30046" msgid "Resume from start" msgstr "" +msgctxt "#30047" +msgid "Show resume from How:" +msgstr "Mostra riprendi da come:" + +msgctxt "#30048" +msgid "Custom window" +msgstr "Finestra personalizzata" + +msgctxt "#30049" +msgid "Skin window" +msgstr "Finestra della skin" + msgctxt "#30050" msgid "Server connection error" msgstr "" diff --git a/resources/language/resource.language.it_it/strings.po b/resources/language/resource.language.it_it/strings.po index 5b7af2e6..d4fdd658 100644 --- a/resources/language/resource.language.it_it/strings.po +++ b/resources/language/resource.language.it_it/strings.po @@ -199,6 +199,18 @@ msgctxt "#30046" msgid "Resume from start" msgstr "Riprendi dall'inizio" +msgctxt "#30047" +msgid "Show resume from How:" +msgstr "Mostra riprendi da come:" + +msgctxt "#30048" +msgid "Custom window" +msgstr "Finestra personalizzata" + +msgctxt "#30049" +msgid "Skin window" +msgstr "Finestra della skin" + msgctxt "#30050" msgid "Server connection error" msgstr "Errore connessione server" diff --git a/resources/settings.xml b/resources/settings.xml index 6726ecba..91257d4b 100644 --- a/resources/settings.xml +++ b/resources/settings.xml @@ -24,6 +24,7 @@ <setting id="servers_favorites" visible="true" type="action" label="60551" action="RunPlugin(plugin://plugin.video.kod/?ew0KICAgICJhY3Rpb24iOiAic2VydmVyc19mYXZvcml0ZXMiLA0KICAgICJjaGFubmVsIjogInNldHRpbmciDQp9==)"/> <setting id="servers_blacklist" visible="true" type="action" label="60550" action="RunPlugin(plugin://plugin.video.kod/?ew0KICAgICJhY3Rpb24iOiAic2VydmVyc19ibGFja2xpc3QiLA0KICAgICJjaGFubmVsIjogInNldHRpbmciDQp9==)"/> <setting id="window_type" type="select" lvalues="60622|60623" label="60621" default="0"/> + <setting id="resume_menu" type="select" lvalues="30048|30049" label="30047" default="0"/> <!-- <setting id="hide_servers" type="bool" label="70747" default="false" visible="eq(-1,true)" subsetting="true"/> --> <setting id="checklinks" type="bool" label="30020" default="false"/> <setting id="checklinks_number" type="slider" option="int" range="5,5,20" label="30021" default="5" visible="eq(-1,true)" subsetting="true"/> diff --git a/servers/doodstream.json b/servers/doodstream.json index dc0c485e..b88db6d7 100644 --- a/servers/doodstream.json +++ b/servers/doodstream.json @@ -4,8 +4,8 @@ "ignore_urls": [], "patterns": [ { - "pattern": "dood(?:stream)?.[^/]+/+(?:e|d)/([a-z0-9]+)", - "url": "https://dood.yt/e/\\1" + "pattern": "(do*d(?:stream)?.[^/]+)/+(?:e|d)/([a-z0-9]+)", + "url": "https://\\1/e/\\2" } ] }, diff --git a/servers/doodstream.py b/servers/doodstream.py index fe49c7c4..1b191bfc 100644 --- a/servers/doodstream.py +++ b/servers/doodstream.py @@ -1,15 +1,20 @@ # -*- coding: utf-8 -*- -import time, string, random -from core import httptools, support, servertools +import time, string, random, sys +from core import httptools, support from platformcode import logger, config +if sys.version_info[0] >= 3: + from urllib.parse import urlparse +else: + from urllib import urlparse + def test_video_exists(page_url): global data logger.debug('page url=', page_url) - response = httptools.downloadpage(page_url) + response = httptools.downloadpage(page_url, cloudscraper=True) if response.code == 404 or 'dsplayer' not in response.data: return False, config.get_localized_string(70449) % 'DooD Stream' else: @@ -22,14 +27,13 @@ def get_video_url(page_url, premium=False, user="", password="", video_password= logger.debug("URL", page_url) video_urls = [] - host = 'https://' + servertools.get_server_host('doodstream')[0] - headers = {'User-Agent': httptools.get_user_agent(), 'Referer': page_url} - support.dbg() + host = 'https://{}'.format(urlparse(page_url).netloc) + headers = {'User-Agent': httptools.get_user_agent(), 'Referer': host} match = support.match(data, patron=r'''dsplayer\.hotkeys[^']+'([^']+).+?function\s*makePlay.+?return[^?]+([^"]+)''').match if match: url, token = match - ret = httptools.downloadpage(host + url, headers=headers).data + ret = httptools.downloadpage(host + url, headers=headers, cloudscraper=True).data video_urls.append(['mp4 [DooD Stream]', '{}{}{}{}|Referer={}'.format(randomize(ret), url, token, int(time.time() * 1000), host)]) diff --git a/servers/filemoon.py b/servers/filemoon.py index cbf61d58..843c8a29 100644 --- a/servers/filemoon.py +++ b/servers/filemoon.py @@ -20,5 +20,5 @@ def get_video_url(page_url, premium=False, user="", password="", video_password= packed = support.match(data, patron=r'(eval\(function\(p.*?)</').match if packed: data = jsunpack.unpack(packed).replace("\\", "") - video_urls = support.get_jwplayer_mediaurl(data, 'filemoon') + video_urls = support.get_jwplayer_mediaurl(data, 'filemoon', hls=True) return video_urls diff --git a/servers/mixdrop.json b/servers/mixdrop.json index 047daab6..d118abbf 100644 --- a/servers/mixdrop.json +++ b/servers/mixdrop.json @@ -4,7 +4,7 @@ "ignore_urls": [], "patterns": [ { - "pattern": "mixdro?ps?.[^/]+/(?:f|e)/([a-z0-9]+)", + "pattern": "mixdro?o?ps?.[^/]+/(?:f|e)/([a-z0-9]+)", "url": "https://mixdrop.co/e/\\1" }, { diff --git a/servers/streamhide.json b/servers/streamhide.json new file mode 100644 index 00000000..a6b765c2 --- /dev/null +++ b/servers/streamhide.json @@ -0,0 +1,27 @@ +{ + "active": true, + "find_videos": { + "ignore_urls": [], + + "patterns": [ + { + "pattern": "ahvsh.com/[a-z]/([\\d\\w]+)", + "url": "https://ahvsh.com/e/\\1" + } + ] + }, + "free": true, + "id": "streamhide", + "name": "StreamHide", + "settings": [ + { + "default": false, + "enabled": true, + "id": "black_list", + "label": "@70708", + "type": "bool", + "visible": true + } + ], + "cloudflare": true +} diff --git a/servers/streamhide.py b/servers/streamhide.py new file mode 100644 index 00000000..b48bc975 --- /dev/null +++ b/servers/streamhide.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +## + + + +from core import httptools, support +from core import scrapertools +from platformcode import logger, config + + +def test_video_exists(page_url): + logger.debug("(page_url='%s')" % page_url) + global data + + data = httptools.downloadpage(page_url).data + + if "File is no longer available" in data: + return False, config.get_localized_string(70449) % "StreamHide" + + return True, "" + + +def get_video_url(page_url, premium=False, user="", password="", video_password=""): + logger.debug("url=" + page_url) + global data + + return support.get_jwplayer_mediaurl(data, 'StreamHide', hls=True) diff --git a/servers/streamingcommunityws.py b/servers/streamingcommunityws.py index f3c0708b..1c74607d 100755 --- a/servers/streamingcommunityws.py +++ b/servers/streamingcommunityws.py @@ -1,68 +1,77 @@ # -*- coding: utf-8 -*- +import urllib.parse +import ast +import xbmc + from core import httptools, support, filetools from platformcode import logger, config -UA = httptools.random_useragent() +from concurrent import futures +from urllib.parse import urlparse + +vttsupport = False if int(xbmc.getInfoLabel('System.BuildVersion').split('.')[0]) < 20 else True def test_video_exists(page_url): - global scws_id - logger.debug('page url=', page_url) - scws_id = '' + global iframe + global iframeParams - if page_url.isdigit(): - scws_id = page_url - else: - page = httptools.downloadpage(page_url) - if page.url == page_url: # se non esiste, reindirizza all'ultimo url chiamato esistente - scws_id = support.scrapertools.find_single_match(page.data, r'scws_id[^:]+:(\d+)') - else: - return 'StreamingCommunity', 'Prossimamente' + iframe = support.scrapertools.decodeHtmlentities(support.match(page_url, patron='<iframe [^>]+src="([^"]+)').match) + iframeParams = support.match(iframe, patron='window\.masterPlaylistParams\s=\s({.*?})').match + + if not iframeParams: + return 'StreamingCommunity', 'Prossimamente' - if not scws_id: - return False, config.get_localized_string(70449) % 'StreamingCommunityWS' return True, "" def get_video_url(page_url, premium=False, user="", password="", video_password=""): - from time import time - from base64 import b64encode - from hashlib import md5 - - global scws_id + urls = list() + subs = list() + local_subs = list() video_urls = list() - # clientIp = httptools.downloadpage(f'https://scws.work/videos/{scws_id}').json.get('client_ip') - clientIp = httptools.downloadpage('http://ip-api.com/json/').json.get('query') - if clientIp: - expires = int(time() + 172800) - token = b64encode(md5('{}{} Yc8U6r8KjAKAepEA'.format(expires, clientIp).encode('utf-8')).digest()).decode('utf-8').replace('=', '').replace('+', '-').replace('/', '_') - url = 'https://scws.work/master/{}?token={}&expires={}&n=1'.format(scws_id, token, expires) - if page_url.isdigit(): - video_urls.append(['m3u8', '{}|User-Agent={}'.format(url, UA)]) - else: - video_urls = compose(url) + scws_id = urlparse(iframe).path.split('/')[-1] + masterPlaylistParams = ast.literal_eval(iframeParams) + url = 'https://scws.work/v2/playlist/{}?{}&n=1'.format(scws_id, urllib.parse.urlencode(masterPlaylistParams)) - return video_urls + info = support.match(url, patron=r'LANGUAGE="([^"]+)",\s*URI="([^"]+)|(http.*?rendition=(\d+)[^\s]+)').matches -def compose(url): - subs = [] - video_urls = [] - info = support.match(url, patron=r'LANGUAGE="([^"]+)",\s*URI="([^"]+)|RESOLUTION=\d+x(\d+).*?(http[^"\s]+)', headers={'User-Agent':UA}).matches - if info and not logger.testMode: # ai test non piace questa parte - for lang, sub, res, url in info: - if sub: - while True: - match = support.match(sub, patron=r'(http[^\s\n]+)').match - if match: - sub = httptools.downloadpage(match).data - else: - break + if info: + for lang, sub, url, res in info: + if sub: if lang == 'auto': lang = 'ita-forced' - s = config.get_temp_file(lang +'.srt') - subs.append(s) - filetools.write(s, support.vttToSrt(sub)) - elif url: - video_urls.append(['m3u8 [{}]'.format(res), '{}|User-Agent={}'.format(url, UA), 0, subs]) + subs.append([lang, sub]) + elif not 'token=&' in url: + urls.append([res, url]) + + if subs: + local_subs = subs_downloader(subs) + video_urls = [['m3u8 [{}]'.format(res), url, 0, local_subs] for res, url in urls] + else: + video_urls = [['m3u8 [{}]'.format(res), url] for res, url in urls] else: - video_urls.append(['m3u8', '{}|User-Agent={}'.format(url, UA)]) + video_urls = [['hls', url]] + return video_urls + + +def subs_downloader(subs): + def subs_downloader_thread(n, s): + lang, url = s + match = support.match(url, patron=r'(http[^\s\n]+)').match + if match: + data = httptools.downloadpage(match).data + if lang == 'auto': lang = 'ita-forced' + sub = config.get_temp_file('{}.{}'.format(lang, 'vtt' if vttsupport else 'str')) + filetools.write(sub, data if vttsupport else support.vttToSrt(data)) + return n, sub + + local_subs = list() + + with futures.ThreadPoolExecutor() as executor: + itlist = [executor.submit(subs_downloader_thread, n, s) for n, s in enumerate(subs)] + for res in futures.as_completed(itlist): + if res.result(): + local_subs.append(res.result()) + + return [s[1] for s in sorted(local_subs, key=lambda n: n[0])] diff --git a/servers/streamsb.json b/servers/streamsb.json index efa87696..f95d85de 100644 --- a/servers/streamsb.json +++ b/servers/streamsb.json @@ -3,16 +3,16 @@ "find_videos": { "ignore_urls": [], "patterns": [{ - "pattern": "(?:streamsb|sblanh|sbembed|sbembed1|sbplay1|sbplay|pelistop|tubesb|playersb|embedsb|watchsb|streamas|sbfast|sbfull|viewsb|sbvideo|cloudemb|sbplay2|japopav|javplaya|ssbstream|sbthe|sbspeed|sbanh|sblongvu|sbchill|sbhight|sbbrisk)\\.\\w{2,5}/(?:embed-|d/|e/)?([A-z0-9]+)", - "url": "https://streamas.cloud/e/\\1.html" + "pattern": "(?:streamsb|sblanh|sblona|sbembed|sbembed1|sbplay1|sbplay|pelistop|tubesb|playersb|embedsb|watchsb|streamas|sbfast|sbfull|viewsb|sbvideo|cloudemb|sbplay2|japopav|javplaya|ssbstream|sbthe|sbspeed|sbanh|sblongvu|sbchill|sbhight|sbbrisk)\\.\\w{2,5}/(?:embed-|d/|e/)?([A-z0-9]+)", + "url": "https://streamas.cloud/d/\\1.html" }, { "pattern": "(?:cloudemb.com)/([A-z0-9]+)", - "url": "https://streamas.cloud/e/\\1.html" + "url": "https://streamas.cloud/d/\\1.html" }, { "pattern": "animeworld.biz/(?:embed-|d/|e/)?([A-z0-9]+)", - "url": "https://streamas.cloud/e/\\1.html" + "url": "https://streamas.cloud/d/\\1.html" } ] }, diff --git a/servers/streamsb.py b/servers/streamsb.py index 29adc0e2..66a4c9ff 100644 --- a/servers/streamsb.py +++ b/servers/streamsb.py @@ -1,53 +1,81 @@ -from core import httptools +import re +from core import httptools, support, scrapertools from platformcode import config, logger, platformtools -import random, string -import codecs +try: + import urllib.parse as urllib +except ImportError: + import urllib +import re, sys + +if sys.version_info[0] >= 3: + from concurrent import futures +else: + from concurrent_py2 import futures + +from base64 import b64encode + + +host = 'https://streamas.cloud' def get_sources(page_url): - code = page_url.split('/')[-1].split('.html')[0] - rand1 = "".join([random.choice(string.ascii_letters) for y in range(12)]) - rand2 = "".join([random.choice(string.ascii_letters) for y in range(12)]) - _0x470d0b = '{}||{}||{}||streamsb'.format(rand1, code, rand2) - - prefix = 'https://streamas.cloud/sources' - suffix = '/' + codecs.getencoder('hex')(_0x470d0b.encode())[0].decode() - number = config.get_setting('number', server='streamsb') - sources = prefix + str(number) + suffix - # does not lite other headers different than watchsb and useragent - ret = httptools.downloadpage(sources, headers={'watchsb': 'sbstream', 'User-Agent': httptools.get_user_agent()}, replace_headers=True).json - if not ret: # probably number changed - wait = platformtools.dialog_progress('StreamSB', config.get_localized_string(60293)) - for number in range(100): - if httptools.downloadpage(prefix + str(number) + '/').code == 200: - config.set_setting('number', server='streamsb', value=number) - sources = prefix + str(number) + suffix - # does not lite other headers different than watchsb and useragent - ret = httptools.downloadpage(sources, - headers={'watchsb': 'sbstream', 'User-Agent': httptools.get_user_agent()}, - replace_headers=True).json - break - wait.close() - logger.debug(ret) - return ret + sources = support.match(page_url, headers={'watchsb': 'sbstream', 'User-Agent': httptools.get_user_agent()}, replace_headers=True, patron=r'download_video([^"]+).*?<span>\s*(\d+)').matches + if sources: + sources = {s[1]: s[0].replace('(','').replace(')','').replace("'",'').split(',') for s in sources} + return sources def test_video_exists(page_url): global sources sources = get_sources(page_url) - if 'error' in sources: - return False, config.get_localized_string(70449) % "StreamSB" - else: + if sources: return True, "" + else: + return False, config.get_localized_string(70449) % "StreamSB" def get_video_url(page_url, premium=False, user="", password="", video_password=""): global sources - file = sources['stream_data']['file'] - backup = sources['stream_data']['backup'] - return [["m3u8 [StreamSB]", file], ["m3u8-altern [StreamSB]", backup]] + video_urls = list() + if sources: + action = config.get_setting('default_action') + if action == 0: + progress = platformtools.dialog_progress_bg("StreamSB", message="Risoluzione URLs") + step = int(100 / len(sources)) + percent = 0 + for res, url in sources.items(): + progress.update(percent, "Risoluzione URL: {}p".format(res)) + r, u = resolve_url(res, url) + percent += step + progress.update(percent, "Risoluzione URL: {}p".format(res)) + video_urls.append(['{} [{}]'.format(u.split('.')[-1], r), u]) + progress.close() + else: + res = sorted([* sources])[0 if action == 1 else -1] + progress = platformtools.dialog_progress_bg("StreamSB", message="Risoluzione URL: {}p".format(res)) + url = sources[res] + r, u = resolve_url(res, url) + progress.close() + video_urls.append(['{} [{}]'.format(u.split(',')[-1], r), u]) + + return video_urls -def get_filename(page_url): - return get_sources(page_url)['stream_data']['title'] +def get_payloads(data, token): + # support.dbg() + payloads = {'g-recaptcha-response': token} + for name, value in support.match(data, patron=r'input type="hidden" name="([^"]+)" value="([^"]+)').matches: + payloads[name] = value + return payloads + +def resolve_url(res, params): + url = '' + source_url = '{}/dl?op=download_orig&id={}&mode={}&hash={}'.format(host, params[0], params[1], params[2]) + data = httptools.downloadpage(source_url).data + co = b64encode((host + ':443').encode('utf-8')).decode('utf-8').replace('=', '') + token = scrapertools.girc(data, host, co) + payload = get_payloads(data, token) + if token: + url = support.match(source_url, patron=r'href="([^"]+)"\s*class="btn\s*btn-light', post=payload).match + return res, url \ No newline at end of file diff --git a/servers/youtube.py b/servers/youtube.py index afcc2a2a..9218ee7f 100644 --- a/servers/youtube.py +++ b/servers/youtube.py @@ -21,7 +21,6 @@ def get_video_url(page_url, premium=False, user="", password="", video_password= logger.debug("(page_url='%s')" % page_url) video_urls = [] - video_id = scrapertools.find_single_match(page_url, '(?:v=|embed/)([A-z0-9_-]{11})') inputstream = platformtools.install_inputstream() try: diff --git a/specials/community.py b/specials/community.py index 5448bb08..7595c46c 100644 --- a/specials/community.py +++ b/specials/community.py @@ -186,7 +186,7 @@ def peliculas(item, json='', key='', itemlist=[]): itlist = filterkey = [] action = 'findvideos' - if inspect.stack()[1][3] not in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', + if inspect.stack(0)[1][3] not in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', 'search'] and not item.filterkey and not item.disable_pagination: Pagination = int(defp) if defp.isdigit() else '' else: @@ -243,7 +243,7 @@ def peliculas(item, json='', key='', itemlist=[]): # if item.sort: # itemlist.sort(key=lambda x: x.title.lower(), reverse=False) if Pagination and len(itemlist) >= Pagination: - if inspect.stack()[1][3] != 'get_newest': + if inspect.stack(0)[1][3] != 'get_newest': item.title = support.typo(config.get_localized_string(30992), 'color kod bold') item.page = pag + 1 item.thumbnail = support.thumb() @@ -279,13 +279,13 @@ def get_seasons(item): contentType='season' if show_seasons else 'tvshow', path=extra.path)) - if inspect.stack()[2][3] in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', + if inspect.stack(0)[2][3] in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', 'get_newest'] or show_seasons == False: itlist = [] for item in itemlist: itlist = episodios(item) itemlist = itlist - if inspect.stack()[2][3] not in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', + if inspect.stack(0)[2][3] not in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', 'get_newest'] and defp and not item.disable_pagination: itemlist = pagination(item, itemlist) @@ -322,7 +322,7 @@ def episodios(item, json='', key='', itemlist=[]): ep = 1 season = infoLabels['season'] if 'season' in infoLabels else item.contentSeason if item.contentSeason else 1 - if inspect.stack()[1][3] not in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', + if inspect.stack(0)[1][3] not in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', 'search'] and not show_seasons: Pagination = int(defp) if defp.isdigit() else '' else: @@ -374,7 +374,7 @@ def episodios(item, json='', key='', itemlist=[]): path=item.path)) # if showseason - if inspect.stack()[1][3] not in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', 'get_newest', 'search']: + if inspect.stack(0)[1][3] not in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', 'get_newest', 'search']: if show_seasons and not item.filterseason: itm.contentType = 'season' season_list = [] @@ -399,15 +399,15 @@ def episodios(item, json='', key='', itemlist=[]): support.videolibrary(itemlist, item) support.download(itemlist, item) - elif defp and inspect.stack()[1][3] not in ['get_seasons'] and not item.disable_pagination: + elif defp and inspect.stack(0)[1][3] not in ['get_seasons'] and not item.disable_pagination: if Pagination and len(itemlist) >= Pagination: - if inspect.stack()[1][3] != 'get_newest': + if inspect.stack(0)[1][3] != 'get_newest': item.title = support.typo(config.get_localized_string(30992), 'color kod bold') item.page = pag + 1 item.thumbnail = support.thumb() itemlist.append(item) - if inspect.stack()[1][3] not in ['get_seasons'] and not show_seasons: + if inspect.stack(0)[1][3] not in ['get_seasons'] and not show_seasons: support.videolibrary(itemlist, item) support.download(itemlist, item) return itemlist @@ -769,7 +769,7 @@ def set_extra_values(item, json, path): ret.subtitle = json[key] if not ret.thumb: - if 'get_search_menu' in inspect.stack()[1][3]: + if 'get_search_menu' in inspect.stack(0)[1][3]: ret.thumb = get_thumb('search.png') else: ret.thumb = item.thumbnail @@ -830,7 +830,7 @@ def pagination(item, itemlist=[]): encoded_itemlist = [] for it in itemlist: encoded_itemlist.append(it.tourl()) - if inspect.stack()[1][3] not in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', 'search']: + if inspect.stack(0)[1][3] not in ['add_tvshow', 'get_episodes', 'update', 'find_episodes', 'search']: Pagination = int(defp) if defp.isdigit() else '' else: Pagination = '' @@ -843,7 +843,7 @@ def pagination(item, itemlist=[]): itlist.append(item) if Pagination and len(itemlist) >= Pagination: - if inspect.stack()[1][3] != 'get_newest': + if inspect.stack(0)[1][3] != 'get_newest': itlist.append( Item(channel=item.channel, action='pagination', diff --git a/specials/globalsearch.py b/specials/globalsearch.py index bc1dfaad..5e6089b5 100644 --- a/specials/globalsearch.py +++ b/specials/globalsearch.py @@ -416,7 +416,6 @@ class SearchWindow(xbmcgui.WindowXML): self.count += 1 return channel, valid, other if other else results - self.update(channel, valid, other if other else results) # update_lock.release() def makeItem(self, url): @@ -440,6 +439,7 @@ class SearchWindow(xbmcgui.WindowXML): return it def update(self, channel, valid, results): + update_lock.acquire() self.LOADING.setVisible(False) if self.exit: return @@ -462,7 +462,8 @@ class SearchWindow(xbmcgui.WindowXML): for result in valid: resultsList += result.tourl() + '|' item.setProperty('items', resultsList) - self.channels[0].setProperty('results', str(len(resultsList.split('|')) - 1 )) + res = len(resultsList.split('|')) + self.channels[0].setProperty('results', str(res - 1 if res > 0 else 0)) if self.CHANNELS.getSelectedPosition() == 0: items = [] @@ -487,7 +488,7 @@ class SearchWindow(xbmcgui.WindowXML): }) for result in results: resultsList += result.tourl() + '|' - item.setProperty('items',resultsList) + item.setProperty('items', resultsList) self.results[name] = len(self.results) self.channels.append(item) else: @@ -497,7 +498,8 @@ class SearchWindow(xbmcgui.WindowXML): resultsList += result.tourl() + '|' item.setProperty('items',resultsList) logger.log(self.channels[int(self.results[name])]) - self.channels[int(self.results[name])].setProperty('results', str(len(resultsList.split('|')) - 1)) + res = len(resultsList.split('|')) + self.channels[int(self.results[name])].setProperty('results', str(res - 1 if res > 0 else 0)) pos = self.CHANNELS.getSelectedPosition() self.CHANNELS.reset() self.CHANNELS.addItems(self.channels) @@ -511,6 +513,7 @@ class SearchWindow(xbmcgui.WindowXML): if result: items.append(self.makeItem(result)) self.RESULTS.reset() self.RESULTS.addItems(items) + update_lock.release() def onInit(self): self.time = time.time() @@ -689,6 +692,7 @@ class SearchWindow(xbmcgui.WindowXML): self.itemsResult = getattr(self.channel, item.action)(item) if self.itemsResult and self.itemsResult[0].server: from platformcode.launcher import findvideos + busy(False) findvideos(self.item, self.itemsResult) return except: diff --git a/tests/run.sh b/tests/run.sh index a8a3a802..4bb05dbc 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -3,10 +3,10 @@ rm tests/home/userdata/addon_data/plugin.video.kod/settings_channels/*.json rm tests/home/userdata/addon_data/plugin.video.kod/settings_servers/*.json rm tests/home/userdata/addon_data/plugin.video.kod/cookies.dat rm tests/home/userdata/addon_data/plugin.video.kod/kod_db.sqlite -python3.9 -m pip install --upgrade pip -pip3.9 install -U sakee -pip3.9 install -U html-testRunner -pip3.9 install -U parameterized +python -m pip install --upgrade pip +pip install -U sakee +pip install -e git+https://github.com/mac12m99/HtmlTestRunner.git@master#egg=html-testRunner +pip install -U parameterized export PYTHONPATH=$PWD export KODI_INTERACTIVE=0 export KODI_HOME=$PWD/tests/home @@ -14,4 +14,4 @@ if (( $# >= 1 )) then export KOD_TST_CH=$1 fi -python3.9 tests/test_generic.py \ No newline at end of file +python tests/test_generic.py \ No newline at end of file diff --git a/tests/test_generic.py b/tests/test_generic.py index 7817de7d..86c8178a 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -12,7 +12,7 @@ import random import sys import time import unittest - +import datetime import xbmc if 'KOD_TST_CH' not in os.environ: @@ -54,7 +54,7 @@ from core import servertools, httptools import channelselector import re - +logger.DEBUG_ENABLED = False httptools.HTTPTOOLS_DEFAULT_DOWNLOAD_TIMEOUT = 10 outDir = os.path.join(os.getcwd(), 'reports') @@ -66,7 +66,7 @@ validUrlRegex = re.compile( r'(?::\d+)?' # optional port r'(?:/?|[/?]\S+)$', re.IGNORECASE) -chBlackList = ['url', 'mediasetplay', 'metalvideo', 'accuradio'] +chBlackList = ['url', 'mediasetplay', 'metalvideo', 'accuradio', 'cinetecadibologna', 'tunein'] srvBlacklist = ['mega', 'hdmario', 'torrent', 'youtube'] chNumRis = { 'altadefinizione01': { @@ -146,13 +146,14 @@ chNumRis = { def wait(): - time.sleep(random.randint(1, 3)) + pass + # time.sleep(random.randint(1, 3)) servers = [] channels = [] - channel_list = channelselector.filterchannels("all") if 'KOD_TST_CH' not in os.environ else [Item(channel=os.environ['KOD_TST_CH'], action="mainlist")] +logger.DEBUG_ENABLED = True logger.info([c.channel for c in channel_list]) results = [] @@ -181,7 +182,9 @@ for chItem in channel_list: for it in mainlist: wait() try: - print('preparing ' + ch + ' -> ' + it.title) + now = datetime.datetime.now() + current_time = now.strftime("%H:%M:%S") + print(current_time + 'preparing ' + ch + ' -> ' + it.title) if it.action == 'channel_config': hasChannelConfig = True @@ -197,7 +200,12 @@ for chItem in channel_list: # if more search action (ex: movie, tvshow), firstcontent need to be changed in every menu if itemlist and itemlist[0].action in ('findvideos', 'episodios'): - firstContent = re.match('[ \w]*', itemlist[0].fulltitle).group(0) + for it2 in itemlist: + # some sites refuse to search if the search term is too short + title = it2.fulltitle if it2.contentType == 'movie' else it2.contentSerieName + if len(title) > 5: + firstContent = re.match('[ \w]*', title).group(0) + break # some sites might have no link inside, but if all results are without servers, there's something wrong for resIt in itemlist: