diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..3661e88c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,33 @@ +name: Test Suite +on: + workflow_dispatch: + schedule: + - cron: '00 15 * * *' + +jobs: + tests: + runs-on: ubuntu-latest + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: 3.8 + + - 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 + env: + API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }} + with: + source_file: 'reports' + destination_repo: 'kodiondemand/kodiondemand.github.io' + user_email: 'tests@kod.bot' + user_name: 'bot' + commit_message: 'Test suite' \ No newline at end of file diff --git a/.github/workflows/updateDomains.yml b/.github/workflows/updateDomains.yml index 6117241b..4d798819 100644 --- a/.github/workflows/updateDomains.yml +++ b/.github/workflows/updateDomains.yml @@ -14,11 +14,13 @@ jobs: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: - python-version: 2.7 + python-version: 3.7 + + - name: Install dependencies + run: pip install requests - name: Update domains - run: | - python tools/updateDomains.py + run: python tools/updateDomains.py - name: Commit & Push changes uses: actions-js/push@master diff --git a/addon.xml b/addon.xml index d739da20..f26faabb 100644 --- a/addon.xml +++ b/addon.xml @@ -1,8 +1,9 @@ - + - + + @@ -26,9 +27,10 @@ resources/media/themes/ss/2.png resources/media/themes/ss/3.png - - correzioni di alcuni bug (citiamo ad esempio il crash con il refresh rate e l'impossibilita di entrare nel menu server bloccati) -- fix per cambio di struttura a qualche canale/server -- migliorie interne + - rimosso supporto a TVDB (l'accesso alle API diventerà a pagamento) +- aggiunto canale Discovery+ +- aggiunta possibilità di scegliere numerazioni alternative per le serie tv +- migliorie interne di vario tipo (tra cui un migliore riconoscimento dei contenuti nel caso siano scritti male) 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] [COLOR yellow]Kodi © is a registered trademark of the XBMC Foundation. We are not connected to or in any other way affiliated with Kodi, Team Kodi, or the XBMC Foundation. Furthermore, any software, addons, or products offered by us will receive no support in official Kodi channels, including the Kodi forums and various social networks.[/COLOR] diff --git a/channels.json b/channels.json index 55ae624e..faa6e0cd 100644 --- a/channels.json +++ b/channels.json @@ -1,59 +1,59 @@ { "direct": { - "altadefinizione01_link": "https://altadefinizione01.fitness", - "animealtadefinizione": "https://www.animealtadefinizione.it", - "animeforce": "https://www.animeforce.it", - "animeleggendari": "https://animeora.com", - "animesaturn": "https://www.animesaturn.it", - "animestream": "https://www.animeworld.tv", - "animesubita": "http://www.animesubita.org", - "animetubeita": "http://www.animetubeita.com", - "animeunity": "https://www.animeunity.it", - "animeuniverse": "https://www.animeuniverse.it/", - "animeworld": "https://www.animeworld.tv", - "casacinema": "https://www.casacinema.page", - "cb01anime": "https://www.cineblog01.red", - "cinemalibero": "https://cinemalibero.store", - "cinetecadibologna": "http://cinestore.cinetecadibologna.it", - "dreamsub": "https://dreamsub.stream", - "dsda": "https://www.dsda.press", - "eurostreaming": "https://eurostreaming.chat", - "fastsubita": "https://fastsubita.xyz", - "filmgratis": "https://www.filmaltadefinizione.me", - "filmigratis": "https://filmigratis.org", - "filmsenzalimiticc": "https://www.filmsenzalimiti01.online", - "filmstreaming01": "https://filmstreaming01.com", - "guardaserie_stream": "https://guardaserie.host", - "guardaseriecam": "https://guardaserie.cam", - "guardaserieclick": "https://www.guardaserie.directory", - "guardaserieicu": "https://guardaserie.shop", - "hd4me": "https://hd4me.net", - "ilcorsaronero": "https://ilcorsaronero.link", - "ilgeniodellostreaming": "https://ilgeniodellostreaming.cat", - "ilgeniodellostreaming_cam": "https://ilgeniodellostreaming.gold", - "italiaserie": "https://italiaserie.top", - "mediasetplay": "https://www.mediasetplay.mediaset.it", - "mondoserietv": "https://mondoserietv.fun", - "paramount": "https://www.paramountnetwork.it", - "piratestreaming": "https://www.piratestreaming.camp", - "polpotv": "https://roma.polpo.tv", - "raiplay": "https://www.raiplay.it", - "serietvonline": "https://serietvonline.cam", - "serietvsubita": "http://serietvsubita.xyz", - "serietvu": "https://www.serietvu.link", - "streamingcommunity": "https://streamingcommunity.net", - "streamtime": "https://t.me/s/StreamTime", - "toonitalia": "https://toonitalia.pro", + "altadefinizione01_link": "https://altadefinizione01.market", + "animealtadefinizione": "https://www.animealtadefinizione.it", + "animeforce": "https://www.animeforce.it", + "animeleggendari": "https://www.animebig.xyz", + "animesaturn": "https://www.animesaturn.it", + "animestream": "https://www.animeworld.tv", + "animesubita": "http://www.animesubita.org", + "animetubeita": "http://www.animetubeita.com", + "animeunity": "https://www.animeunity.it", + "animeuniverse": "https://www.animeuniverse.it", + "animeworld": "https://www.animeworld.tv", + "casacinema": "https://www.casacinema.page", + "cb01anime": "https://www.cineblog01.red", + "cinemalibero": "https://cinemalibero.win", + "cinetecadibologna": "http://cinestore.cinetecadibologna.it", + "discoveryplus": "https://www.discoveryplus.com", + "dreamsub": "https://dreamsub.stream", + "dsda": "https://www.dsda.press", + "eurostreaming": "https://eurostreaming.tube", + "filmgratis": "https://www.filmaltadefinizione.me", + "filmigratis": "https://filmigratis.org", + "filmsenzalimiticc": "https://www.filmsenzalimiti01.work", + "filmstreaming01": "https://filmstreaming01.com", + "guardaserie_stream": "https://guardaserie.host", + "guardaseriecam": "https://guardaserie.cam", + "guardaserieclick": "https://www.guardaserie.kim", + "guardaserieicu": "https://guardaserie.shop", + "hd4me": "https://hd4me.net", + "ilcorsaronero": "https://ilcorsaronero.link", + "ilgeniodellostreaming": "https://ilgeniodellostreaming.pub", + "ilgeniodellostreaming_cam": "https://ilgeniodellostreaming.gold", + "italiaserie": "https://italiaserie.bid", + "mediasetplay": "https://www.mediasetplay.mediaset.it", + "mondoserietv": "https://mondoserietv.fun", + "paramount": "https://www.paramountnetwork.it", + "piratestreaming": "https://www.piratestreaming.live", + "polpotv": "https://roma.polpo.tv", + "raiplay": "https://www.raiplay.it", + "serietvonline": "https://serietvonline.website", + "serietvsubita": "http://serietvsubita.xyz", + "serietvu": "https://www.serietvu.link", + "streamingcommunity": "https://streamingcommunity.net", + "streamtime": "https://t.me/s/StreamTime", + "toonitalia": "https://toonitalia.pro", "vvvvid": "https://www.vvvvid.it" - }, + }, "findhost": { - "altadefinizione01": "https://altadefinizione01-nuovo.info", - "altadefinizioneclick": "https://altadefinizione-nuovo.me", - "animealtadefinizione": "https://www.animealtadefinizione.it", - "cineblog01": "https://cb01.uno", - "film4k": "https://film4k-nuovo.link", - "filmpertutti": "https://filmpertutti.nuovo.live", - "seriehd": "https://nuovoindirizzo.info/seriehd/", + "altadefinizione01": "https://altadefinizione01-nuovo.info", + "altadefinizioneclick": "https://altadefinizione-nuovo.me", + "animealtadefinizione": "https://www.animealtadefinizione.it", + "cineblog01": "https://cb01.uno", + "film4k": "https://film4k-nuovo.link", + "filmpertutti": "https://filmpertutti.nuovo.live", + "seriehd": "https://nuovoindirizzo.info/seriehd", "tantifilm": "https://www.tantifilm.wiki" } } \ No newline at end of file diff --git a/channels/altadefinizioneclick.py b/channels/altadefinizioneclick.py index a910e579..5b363e22 100644 --- a/channels/altadefinizioneclick.py +++ b/channels/altadefinizioneclick.py @@ -23,7 +23,7 @@ from platformcode import config def findhost(url): data = support.httptools.downloadpage(url).data - host = support.scrapertools.find_single_match(data, '
\s*(?P[^<]+)<' @@ -80,7 +81,7 @@ def genres(item): elif item.args == 'years': patronBlock = r'<ul class="listSubCat" id="Anno">(?P<block>.*)<ul class="listSubCat" id="Qualita">' elif item.args == 'quality': - patronBlock = r'<ul class="listSubCat" id="Qualita">(?P<block>.*)</li> </ul> </div> </div> </div> <a' + patronBlock = r'<ul class="listSubCat" id="Qualita">(?P<block>.*)</li>\s*?</ul>\s*?</div>\s*?</div>\s*?</div>\s*?<a' elif item.args == 'lucky': # sono i titoli random nella pagina patronBlock = r'<h3 class="titleSidebox dado">FILM RANDOM</h3>(?P<block>.*)</section>' patron = r'<li><a href="(?P<url>[^"]+)">(?P<title>[^<[]+)(?:\[(?P<lang>.+?)\])?<' diff --git a/channels/casacinema.py b/channels/casacinema.py index 375d4ff7..8d432968 100644 --- a/channels/casacinema.py +++ b/channels/casacinema.py @@ -104,11 +104,11 @@ def peliculas(item): action = 'select' if item.args == 'newest': - patron = r'<li><a href="(?P<url>[^"]+)"[^=]+="(?P<thumb>[^"]+)"><div>\s+<div[^>]+>(?P<title>[^\(\[<]+)(?:\[(?P<quality1>HD)\])?[ ]?(?:\(|\[)?(?P<lang>Sub-ITA)?(?:\)|\])?[ ]?(?:\[(?P<quality>.+?)\])?[ ]?(?:\((?P<year>\d+)\))?<(?:[^>]+>.+?(?:title="Nuovi episodi">(?P<episode>\d+x\d+)[ ]?(?P<lang2>Sub-Ita)?|title="IMDb">(?P<rating>[^<]+)))?' + patron = r'<li><a href="(?P<url>[^"]+)"[^=]+="(?P<thumb>[^"]+)"><div>\s*?<div[^>]+>(?P<title>[^\(\[<]+)(?:\[(?P<quality1>HD)\])?[ ]?(?:\(|\[)?(?P<lang>Sub-ITA)?(?:\)|\])?[ ]?(?:\[(?P<quality>.+?)\])?[ ]?(?:\((?P<year>\d+)\))?<(?:[^>]+>.+?(?:title="Nuovi episodi">(?P<episode>\d+x\d+)[ ]?(?P<lang2>Sub-Ita)?|title="IMDb">(?P<rating>[^<]+)))?' else: - patron = r'<li><a href="(?P<url>[^"]+)"[^=]+="(?P<thumb>[^"]+)"><div>\s+<div[^>]+>(?P<title>[^\(\[<]+)(?:\[(?P<quality1>HD)\])?[ ]?(?:\(|\[)?(?P<lang>Sub-ITA)?(?:\)|\])?[ ]?(?:\[(?P<quality>.+?)\])?[ ]?(?:\((?P<year>\d+)\))?' + patron = r'<li><a href="(?P<url>[^"]+)"[^=]+="(?P<thumb>[^"]+)"><div>\s*?<div[^>]+>(?P<title>[^\(\[<]+)(?:\[(?P<quality1>HD)\])?\s?(?:[\(\[])?(?P<lang>Sub-ITA)?(?:[\)\]])?\s?(?:\[(?P<quality>.+?)\])?\s?(?:\((?P<year>\d+)\))?<' - patronNext = r'<a href="([^"]+)" >Pagina' + patronNext = r'<a href="([^"]+)"\s*>Pagina' # debug = True def itemHook(item): diff --git a/channels/cb01anime.py b/channels/cb01anime.py index d870e34e..9258bb02 100644 --- a/channels/cb01anime.py +++ b/channels/cb01anime.py @@ -37,7 +37,14 @@ def menu(item): def search(item, texto): support.info(texto) item.url = host + "/?s=" + texto - return peliculas(item) + try: + return peliculas(item) + except: + import sys + for line in sys.exc_info(): + support.info('search log:', line) + return [] + def newest(categoria): support.info(categoria) @@ -59,12 +66,13 @@ def newest(categoria): @support.scrape def peliculas(item): + # debug=True blacklist = Blacklist item.contentType = 'tvshow' if item.args == 'newest': - patron = r'<div id="blockvids">\s*<ul>\s*<li>\s*<a href="(?P<url>[^"]+)"[^>]+><img[^>]+src="(?P<thumb>[^"]+)"[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>(?P<title>[^\[]+)\[(?P<lang>[^\]]+)\]' + patron = r'<div id="blockvids">\s*<ul>\s*<li>\s*<a href="(?P<url>[^"]+)"[^>]+><img[^>]+src="(?P<thumb>[^"]+)"[^>]*>(?:[^>]+>){4}(?P<title>[^\[]+)\[(?P<lang>[^\]]+)\]' else: - patron = r'<div class="span4">\s*<a href="(?P<url>[^"]+)"><img src="(?P<thumb>[^"]+)"[^>]+><\/a>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+> +<h1>(?P<title>[^<\[]+)(?:\[(?P<lang>[^\]]+)\])?</h1></a>.*?-->(?:.*?<br />)?\s*(?P<plot>[^<]+)' + patron = r'<div class="span4">\s*<a href="(?P<url>[^"]+)"><img src="(?P<thumb>[^"]+)"[^>]+><\/a>(?:[^>]+>){7}\s*<h1>(?P<title>[^<\[]+)(?:\[(?P<lang>[^\]]+)\])?</h1></a>.*?-->(?:.*?<br />)?\s*(?P<plot>[^<]+)' patronNext = r'<link rel="next" href="([^"]+)"' action = 'check' return locals() diff --git a/channels/cineblog01.py b/channels/cineblog01.py index 80330ced..80886958 100644 --- a/channels/cineblog01.py +++ b/channels/cineblog01.py @@ -78,7 +78,7 @@ def search(item, text): if item.contentType == 'tvshow': item.url = host + '/serietv/' else: item.url = host try: - item.url = item.url + "?s=" + text.replace(' ', '+') + item.url = item.url + "/search/" + text.replace(' ', '+') return peliculas(item) # Continua la ricerca in caso di errore diff --git a/channels/cinemalibero.py b/channels/cinemalibero.py index 9ab8f297..30a54d34 100644 --- a/channels/cinemalibero.py +++ b/channels/cinemalibero.py @@ -59,7 +59,7 @@ def peliculas(item): patron = r'<a href="(?P<url>(?:https:\/\/.+?\/(?P<title>[^\/]+[a-zA-Z0-9\-]+)(?P<year>\d{4})))/".+?url\((?P<thumb>[^\)]+)\)">' elif item.contentType == 'tvshow': if item.args == 'update': - patron = r'<a href="(?P<url>[^"]+)"[^<]+?url\((?P<thumb>.+?)\)">\s+<div class="titolo">(?P<title>.+?)(?: – Serie TV)?(?:\([sSuUbBiItTaA\-]+\))?[ ]?(?P<year>\d{4})?</div>[ ](?:<div class="genere">)?(?:[\w]+?\.?\s?[\s|S]?[\dx\-S]+?\s\(?(?P<lang>[iItTaA]+|[sSuUbBiItTaA\-]+)\)?\s?(?P<quality>[HD]+)?|.+?\(?(?P<lang2>[sSuUbBiItTaA\-]+)?\)?</div>)' + patron = r'<a href="(?P<url>[^"]+)"[^<]+?url\((?P<thumb>.+?)\)">\s*?<div class="titolo">(?P<title>.+?)(?: – Serie TV)?(?:\([sSuUbBiItTaA\-]+\))?[ ]?(?P<year>\d{4})?</div>\s*?(?:<div class="genere">)?(?:[\w]+?\.?\s?[\s|S]?[\dx\-S]+?\s\(?(?P<lang>[iItTaA]+|[sSuUbBiItTaA\-]+)\)?\s?(?P<quality>[HD]+)?|.+?\(?(?P<lang2>[sSuUbBiItTaA\-]+)?\)?</div>)' pagination = 25 else: patron = r'<a href="(?P<url>[^"]+)"\s*title="(?P<title>[^"\(]+)(?:"|\()(?:(?P<year>\d+)[^"]+)?.*?url\((?P<thumb>[^\)]+)\)(?:.*?<div class="voto">[^>]+>[^>]+>\s*(?P<rating>[^<]+))?.*?<div class="titolo">[^>]+>(?:<div class="genere">[^ ]*(?:\s\d+)?\s*(?:\()?(?P<lang>[^\)< ]+))?' @@ -77,6 +77,8 @@ def peliculas(item): item.title += support.typo(item.lang2, '_ [] color kod') if item.args == 'update': item.title = item.title.replace('-', ' ') + if item.args == 'search': + item.contentType = 'tvshow' if 'serie-' in item.url else 'movie' return item diff --git a/channels/discoveryplus.json b/channels/discoveryplus.json new file mode 100644 index 00000000..ef5baa65 --- /dev/null +++ b/channels/discoveryplus.json @@ -0,0 +1,12 @@ +{ + "id": "discoveryplus", + "name": "Discovery + [free]", + "active": true, + "language": ["ita"], + "thumbnail": "discoveryplus.png", + "banner": "discoveryplus.png", + "categories": ["tvshow", "documentary", "live"], + "not_active": ["include_in_newest"], + "default_off": ["include_in_global_search"], + "settings": [] +} \ No newline at end of file diff --git a/channels/discoveryplus.py b/channels/discoveryplus.py new file mode 100644 index 00000000..75a3dbdd --- /dev/null +++ b/channels/discoveryplus.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------ +# Canale per Rai Play +# ------------------------------------------------------------ + +import requests +from core import support, jsontools +from platformcode import logger, config +typo = support.typo +session = requests.Session() +host = support.config.get_channel_url() + +def getToken(): + token = config.get_setting('token', 'discoveryplus', None) + if not token: + token = session.get('https://disco-api.discoveryplus.it/token?realm=dplayit').json()['data']['attributes']['token'] + config.set_setting('token', token, 'discoveryplus') + return token + +token = getToken() + +api = "https://disco-api.discoveryplus.it" +headers = {'User-Agent': 'Mozilla/50.0 (Windows NT 10.0; WOW64; rv:45.0) Gecko/20100101 Firefox/45.0', + 'Referer': host, + 'Cookie' : 'st=' + token} + +def Dict(item): + global pdict + pdict = session.get(api + '/cms/routes/{}?decorators=viewingHistory&include=default'.format(item.args), headers=headers).json()['included'] + + +@support.menu +def mainlist(item): + top = [('Dirette {bold}', ['', 'live']), + ('Programmi {bullet bold tv}', ['/programmi', 'peliculas']), + ('Generi {bullet bold tv}', ['', 'genres'])] + + search = '' + + return locals() + + +def liveDict(): + livedict = {} + for key in session.get(api + '/cms/routes/home?decorators=viewingHistory&include=default', headers=headers).json()['included']: + if key['type'] == 'channel' and key.get('attributes',{}).get('hasLiveStream', '') and 'Free' in key.get('attributes',{}).get('packages', []): + title = key['attributes']['name'] + livedict[title] = {} + livedict[title]['plot'] = key['attributes']['description'] + livedict[title]['url'] = '{}/canali/{}'.format(host, key['attributes']['alternateId']) + livedict[title]['id'] = key['id'] + return livedict + + +def search(item, text): + itemlist = [] + item.args = 'search' + item.text = text + try: + itemlist = peliculas(item) + except: + import sys + for line in sys.exc_info(): + support.logger.error("%s" % line) + + return itemlist + + +def live(item): + logger.debug() + itemlist =[] + for name, values in liveDict().items(): + itemlist.append(item.clone(title=typo(name), fulltitle=name, plot=values['plot'], url=values['url'], id=values['id'], action='play', forcethumb=True, no_return=True)) + return support.thumb(itemlist, live=True) + + +def genres(item): + item.action = 'peliculas' + itemlist = [ + item.clone(title='Attualità e inchiesta', url='/genere/attualita-e-inchiesta'), + item.clone(title='Beauty and style', url='/genere/beauty-and-style'), + item.clone(title='Serie TV', url='/genere/serie-tv'), + item.clone(title='Casa', url='/genere/casa'), + item.clone(title='Comedy', url='/genere/comedy'), + item.clone(title='Crime', url='/genere/crime'), + item.clone(title='Documentari', url='/genere/documentari'), + item.clone(title='Discovery + Originals', url='/genere/discoveryplus-original'), + item.clone(title='Food', url='/genere/food'), + item.clone(title='Medical', url='/genere/medical'), + item.clone(title='Motori', url='/genere/motori'), + item.clone(title='Natura', url='/genere/natura'), + item.clone(title='Paranormal', url='/genere/paranormal'), + item.clone(title='People', url='/genere/people'), + item.clone(title='Real Adventure', url='/genere/real-adventure'), + item.clone(title='Real Life', url='/genere/real-life'), + item.clone(title='Scienza e Spazio', url='/genere/scienza-e-spazio'), + item.clone(title='Sex and love', url='/genere/sex-and-love'), + item.clone(title='Sport', url='/genere/sport'), + item.clone(title='Talent Show', url='/genere/talent-show'), + ] + return itemlist + + +def peliculas(item): + logger.debug() + itemlist =[] + if 'search' in item.args: + pdict = session.get(api + '/content/shows?include=genres,images,primaryChannel.images,contentPackages&page[size]=12&query=' + item.text, headers=headers).json()['data'] + else: + pdict = session.get(api + '/cms/routes{}?decorators=viewingHistory&include=default'.format(item.url), headers=headers).json()['included'] + images = list(filter(lambda x: x['type'] == 'image', pdict)) + + for key in pdict: + if key['type'] == 'show' and 'Free' in str(key.get('relationships',{}).get('contentPackages',{}).get('data',[])): + title = key['attributes']['name'] + plot = key['attributes'].get('description','') + url = '{}/programmi/{}'.format(host, key['attributes']['alternateId']) + seasons = key['attributes']['seasonNumbers'] + thumbs = [image['attributes']['src'] for image in images if image['id'] == key['relationships']['images']['data'][0]['id']] + thumb = thumbs[0] if thumbs else item.thumbnail + fanarts = [image['attributes']['src'] for image in images if len(key['relationships']['images']['data']) > 1 and image['id'] == key['relationships']['images']['data'][1]['id']] + fanart = fanarts[0] if fanarts else item.fanart + itemlist.append( + item.clone(title=typo(title,'bold'), + fulltitle=title, + plot=plot, + url=url, + programid=key['attributes']['alternateId'], + id=key['id'], + seasons=seasons, + action='episodios', + thumbnail=thumb, + fanart=fanart, + contentType='tvshow')) + + return itemlist + + +def episodios(item): + logger.debug() + itemlist =[] + pdict = session.get(api + '/cms/routes/programmi/{}?decorators=viewingHistory&include=default'.format(item.programid), headers=headers).json()['included'] + + for key in pdict: + if key['type'] == 'collection' and key.get('attributes',{}).get('component',{}).get('id', '') == 'tabbed-content': + mandatory = key['attributes']['component']['mandatoryParams'] + for option in key['attributes']['component']['filters'][0]['options']: + url = '{}/cms/collections/{}?decorators=viewingHistory&include=default&{}&{}'.format(api, key['id'], mandatory, option['parameter']) + season = session.get(url, headers=headers).json() + if season.get('included', {}): + for episode in season['included']: + if episode['type'] == 'video' and 'Free' in episode['attributes']['packages']: + title = '{}x{:02d} - {}'.format(option['id'], episode['attributes']['episodeNumber'], episode['attributes']['name']) + plot = episode['attributes']['description'] + itemlist.append( + item.clone(title=typo(title,'bold'), + fulltitle=title, + plot=plot, + id=episode['id'], + action='play', + contentType='episode', + season=option['id'], + episode=episode['attributes']['episodeNumber'], + forcethumb=True, + no_return=True)) + + if itemlist: itemlist.sort(key=lambda it: (int(it.season), int(it.episode))) + return itemlist + + +def play(item): + if item.livefilter: + item.id = liveDict()[item.livefilter]['id'] + item.fulltitle = item.livefilter + item.forcethumb = True + item.no_return = True + support.thumb(item, live=True) + if item.contentType == 'episode': data = session.get('{}/playback/v2/videoPlaybackInfo/{}?usePreAuth=true'.format(api, item.id), headers=headers).json().get('data',{}).get('attributes',{}) + else: data = session.get('{}/playback/v2/channelPlaybackInfo/{}?usePreAuth=true'.format(api, item.id), headers=headers).json().get('data',{}).get('attributes',{}) + if data.get('protection', {}).get('drm_enabled',True): + url = data['streaming']['dash']['url'] + item.drm = 'com.widevine.alpha' + item.license = data['protection']['schemes']['widevine']['licenseUrl'] + '|PreAuthorization=' + data['protection']['drmToken'] + '|R{SSM}|' + else: + url = data['streaming']['hls']['url'] + return support.servertools.find_video_items(item, data=url) \ No newline at end of file diff --git a/channels/dreamsub.py b/channels/dreamsub.py index e0986595..815993d5 100644 --- a/channels/dreamsub.py +++ b/channels/dreamsub.py @@ -137,7 +137,7 @@ def findvideos(item): if 'sub' in lang.lower(): language = 'Sub-' + language quality = url.split('/')[-1].split('?')[0] - url += "|User-Agent=" + support.httptools.get_user_agent() + '&Referer=' + item.url + url += '|User-Agent=' + support.httptools.get_user_agent() + '&Referer=' + url itemlist.append(item.clone(action="play", title=language, url=url, contentLanguage = language, quality = quality, order = quality.replace('p','').zfill(4), server='directo',)) diff --git a/channels/eurostreaming.py b/channels/eurostreaming.py index 9508ca52..f4ec0f92 100644 --- a/channels/eurostreaming.py +++ b/channels/eurostreaming.py @@ -29,13 +29,14 @@ def mainlist(item): @support.scrape def peliculas(item): + # debug = True action = 'episodios' if item.args == 'newest': item.contentType = 'episode' patron = r'<span class="serieTitle" style="font-size:20px">(?P<title>[^<]+) –\s*<a href="(?P<url>[^"]+)"[^>]*>\s?(?P<episode>\d+[×x]\d+-\d+|\d+[×x]\d+) (?P<title2>[^<\(]+)\s?\(?(?P<lang>SUB ITA)?\)?</a>' pagination = '' else: - patron = r'<div class="post-thumb">.*?\s<img src="(?P<thumb>[^"]+)".*?><a href="(?P<url>[^"]+)"[^>]+>(?P<title>.+?)\s?(?: Serie Tv)?\s?\(?(?P<year>\d{4})?\)?<\/a><\/h2>' + patron = r'<div class="post-thumb">.*?<img src="(?P<thumb>[^"]+)".*?><a href="(?P<url>[^"]+)"[^>]+>(?P<title>.+?)\s?(?: Serie Tv)?\s?\(?(?P<year>\d{4})?\)?<\/a><\/h2>' patronNext=r'a class="next page-numbers" href="?([^>"]+)">Avanti »</a>' return locals() diff --git a/channels/filmpertutti.py b/channels/filmpertutti.py index 4c9e9164..e9f24e30 100644 --- a/channels/filmpertutti.py +++ b/channels/filmpertutti.py @@ -9,7 +9,7 @@ from platformcode import config def findhost(url): page = httptools.downloadpage(url).data - url = support.scrapertools.find_single_match(page, 'Il nuovo indirizzo di FILMPERTUTTI è <a href="([^"]+)') + url = support.scrapertools.find_single_match(page, 'Il nuovo indirizzo di FILMPERTUTTI è ?<a href="([^"]+)') return url host = config.get_channel_url(findhost) diff --git a/channels/hd4me.py b/channels/hd4me.py index bd9b3c56..4612ffeb 100644 --- a/channels/hd4me.py +++ b/channels/hd4me.py @@ -24,7 +24,7 @@ def peliculas(item): if item.args == 'alternative': patron = r'<a title="(?P<title>[^\(]+)\(\s*(?P<year>\d+)\)\s\D+(?P<quality>\d+p) ... (?P<lang>[^ ]+).*?[^"]+"\s*href="(?P<url>[^"]+)' else: - patron = r'<a href="(?P<url>[^"]+)" rel="?[0-9]+"? title="(?P<title>[^\(]+)(?!\()\s*\((?P<year>\d+)\)\s\D+(?P<quality>\d+p).{3}(?P<lang>[^ ]+).*?<img id="?cov"?.*?src="(?P<thumb>[^"]+)' + patron = r'<a href="(?P<url>[^"]+)" (?:rel="?[0-9]+"?)? title="(?P<title>[^\(]+)(?!\()\s*\((?P<year>\d+)\)\s(?:[^\]]+\])?\D+(?P<quality>\d+p).{3}(?P<lang>[^ ]+).*?<img id="?cov"?.*?src="(?P<thumb>[^"]+)' patronNext = r'rel="?next"? href="([^"]+)"' return locals() diff --git a/channels/ilgeniodellostreaming.py b/channels/ilgeniodellostreaming.py index 04b02f08..790622d0 100644 --- a/channels/ilgeniodellostreaming.py +++ b/channels/ilgeniodellostreaming.py @@ -48,6 +48,7 @@ def mainlist(item): def peliculas(item): info() # debugBlock = True + # debug=True if item.args == 'search': patronBlock = r'<div class="search-page">(?P<block>.*?)<footer class="main">' @@ -79,13 +80,13 @@ def peliculas(item): action = 'episodios' if item.args == 'update': action = 'findvideos' - patron = r'<div class="poster"><img src="(?P<thumb>[^"]+)"[^>]+>[^>]+><a href="(?P<url>[^"]+)">[^>]+>(?P<episode>[\d\-x]+)[^>]+>[^>]+>[^>]+>[^>]+>(?P<title>.+?)(?:\[(?P<lang>Sub-ITA|Sub-ita)\])?<[^>]+>[^>]+>[^>]+>[^>]+>(?P<quality>[HD]+)?(?:.+?)?/span><p class="serie"' + patron = r'<div class="poster"><img src="(?P<thumb>[^"]+)"(?:[^>]+>){2}<a href="(?P<url>[^"]+)">[^>]+>(?P<episode>[\d\-x]+)(?:[^>]+>){4}(?P<title>.+?)(?:\[(?P<lang>[SsuUbBiItTaA-]{7})\])?<(?:[^>]+>){4}(?P<quality>[HDWEBRIP-]+)?(?:.+?)?/span><p class="serie"' pagination = 25 def itemHook(item): item.contentType = 'episode' return item else: - patron = r'<div class="poster">\s?<a href="(?P<url>[^"]+)"><img src="(?P<thumb>[^"]+)" alt="[^"]+"><\/a>[^>]+>[^>]+>[^>]+> (?P<rating>[0-9.]+)<[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>(?P<title>.+?)[ ]?(?:\[(?P<lang>Sub-ITA|Sub-ita)\])?<[^>]+>[^>]+>[^>]+>(?P<year>[0-9]{4})?[^<]*(?:<.*?<div class="texto">(?P<plot>[^<]+))?' + patron = r'<div class="poster">\s?<a href="(?P<url>[^"]+)"><img src="(?P<thumb>[^"]+)" alt="[^"]+"><\/a>[^>]+>[^>]+>[^>]+> (?P<rating>[0-9.]+)<[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>(?P<title>.+?)[ ]?(?:\[(?P<lang>Sub-ITA|Sub-ita)\])?<[^>]+>[^>]+>[^>]+>(?P<year>[0-9]{4})?[^<]*(?:<.*?<div class="texto">(?P<plot>[^<]+)?)?' patronNext = '<span class="current">[^<]+<[^>]+><a href=[\'"]([^\'"]+)[\'"]' #support.regexDbg(item, patron, headers) @@ -128,13 +129,13 @@ def search(item, text): itemlist = [] text = text.replace(' ', '+') item.url = host + "?s=" + text - # try: - item.args = 'search' - return peliculas(item) - # except: - # import sys - # for line in sys.exc_info(): - # info("%s" % line) + try: + item.args = 'search' + return peliculas(item) + except: + import sys + for line in sys.exc_info(): + info("%s" % line) return [] diff --git a/channels/italiaserie.py b/channels/italiaserie.py index f071cbec..2b554c98 100644 --- a/channels/italiaserie.py +++ b/channels/italiaserie.py @@ -25,16 +25,18 @@ def mainlist(item): @support.scrape def peliculas(item): + # debug=True blacklist = ['Aggiornamento Episodi'] action = 'episodios' patron = r'<div class="post-thumb">\s*<a href="(?P<url>[^"]+)" title="(?P<title>[^"\[]+)[^>]+>\s*<img src="(?P<thumb>[^"]+)"[^>]+>' if item.args == 'update': pagination = '' - patron = r'br />(?:[^>]+>)?(?P<title>[^–]+)[^<]+<a href="(?P<url>[^"]+)">(?P<episode>[^ ]+)\s*(?P<title2>[^\(<]+)(?:\((?P<lang>[^\)]+))?' + #patron = r'br />(?:[^>]+>)?(?P<title>[^–]+)[^<]+<a href="(?P<url>[^"]+)">(?P<episode>[^ ]+)\s*(?P<title2>[^\(<]+)(?:\((?P<lang>[^\)]+))?' + patron = r'br[\s/]*>(?:\s*<[^>]+>)*(?P<title>[^–<]+)[^<]+<a href="(?P<url>[^"]+)"[^>]*>(?:[^,]{0,80}[, ]{2})*(?P<episode>[\S]+)\s*(?P<title2>[^\(<]+)(?:\((?P<lang>[^\)]+))?' action = 'episodios' if item.args == 'top': - patron = r'<a href="(?P<url>[^"]+)">(?P<title>[^<]+)</a>[^>]+>[^>]+>[^>]+><img.*?src="(?P<thumb>[^"]+)"[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>:\s*(?P<rating>[^/]+)' + patron = r'<a href="(?P<url>[^"]+)">(?P<title>[^<]+)</a>(?:[^>]+>){3}<img.*?src="(?P<thumb>[^"]+)"[^>]+>(?:[^>]+>){5}:\s*(?P<rating>[^/]+)' if item.args =='a-z': pagination = '' patron = r'<li ><a href="(?P<url>[^"]+)" title="(?P<title>[^"]+)"' diff --git a/channels/la7.py b/channels/la7.py index 9c3700ac..6c657871 100644 --- a/channels/la7.py +++ b/channels/la7.py @@ -71,7 +71,14 @@ def replay(item): def search(item, text): item.url = host + '/tutti-i-programmi' item.search = text - return peliculas(item) + try: + return peliculas(item) + except: + import sys + for line in sys.exc_info(): + support.info('search log:', line) + return [] + @support.scrape def peliculas(item): @@ -118,6 +125,11 @@ def episodios(item): def play(item): support.info() + if item.livefilter: + for it in live(item): + if it.fulltitle == item.livefilter: + item = it + break data = support.match(item).data match = support.match(data, patron='/content/entry/data/(.*?).mp4').match if match: diff --git a/channels/mediasetplay.py b/channels/mediasetplay.py index 433dfa8b..ab0b0ede 100644 --- a/channels/mediasetplay.py +++ b/channels/mediasetplay.py @@ -10,6 +10,7 @@ if sys.version_info[0] >= 3: from urllib.parse import urlencode, quote else: from urllib import urlencode, quote if sys.version_info[0] >= 3: from concurrent import futures else: from concurrent_py2 import futures +from collections import OrderedDict host = '' DRM = 'com.widevine.alpha' @@ -120,10 +121,9 @@ def menu(item): return itemlist -def live(item): - support.info() - itemlist = [] - json = current_session.get(item.url).json()['entries'] +def liveDict(): + livedict = OrderedDict({}) + json = current_session.get('https://feed.entertainment.tv.theplatform.eu/f/PR1GhC/mediaset-prod-all-stations?sort=ShortTitle').json()['entries'] for it in json: urls = [] if it['tuningInstruction'] and not it['mediasetstation$digitalOnly']: @@ -133,18 +133,25 @@ def live(item): else: for key in it['tuningInstruction']['urn:theplatform:tv:location:any']: urls += key['publicUrls'] - plot = support.typo(guide['currentListing']['mediasetlisting$epgTitle'],'bold') + '\n' + guide['currentListing']['mediasetlisting$shortDescription'] + '\n' + guide['currentListing']['description'] + '\n\n' + support.typo('A Seguire:' + guide['nextListing']['mediasetlisting$epgTitle'], 'bold') + title = it['title'] + livedict[title] = {} + livedict[title]['urls'] = urls + livedict[title]['plot'] = support.typo(guide['currentListing']['mediasetlisting$epgTitle'],'bold') + '\n' + guide['currentListing']['mediasetlisting$shortDescription'] + '\n' + guide['currentListing']['description'] + '\n\n' + support.typo('A Seguire:' + guide['nextListing']['mediasetlisting$epgTitle'], 'bold') + return livedict - itemlist.append(item.clone(title=support.typo(it['title'], 'bold'), - fulltitle=it['title'], - show=it['title'], - contentTitle=it['title'], - thumbnail=it['thumbnails']['channel_logo-100x100']['url'], - forcethumb=True, - urls=urls, - plot=plot, - action='play', - no_return=True)) +def live(item): + support.info() + itemlist = [] + for key, value in liveDict().items(): + itemlist.append(item.clone(title=support.typo(key, 'bold'), + fulltitle=key, + show=key, + contentTitle=key, + forcethumb=True, + urls=value['urls'], + plot=value['plot'], + action='play', + no_return=True)) return support.thumb(itemlist, live=True) @@ -204,7 +211,9 @@ def peliculas(item): plot=it['longDescription'] if 'longDescription' in it else it['description'] if 'description' in it else '', urls=urls, seriesid = it.get('seriesId',''), - url=it['mediasetprogram$pageUrl'])) + url=it['mediasetprogram$pageUrl'], + forcethumb=True, + no_return=True)) return itemlist @@ -246,7 +255,7 @@ def episodios(item): urls.append(key['publicUrl']) if urls: title = it['title'].split('-')[-1].strip() - if it['tvSeasonNumber'] and it['tvSeasonEpisodeNumber']: + if it['tvSeasonNumber'] and it['tvSeasonEpisodeNumber'] and 'puntata del' not in title.lower(): item.infoLabels['season'] = it['tvSeasonNumber'] item.infoLabels['episode'] = it['tvSeasonEpisodeNumber'] episode = '%dx%02d - ' % (it['tvSeasonNumber'], it['tvSeasonEpisodeNumber']) @@ -258,7 +267,10 @@ def episodios(item): fanart=it['thumbnails']['image_keyframe_poster-1280x720']['url'] if 'image_keyframe_poster-1280x720' in it['thumbnails'] else '', plot=it['longDescription'] if 'longDescription' in it else it['description'], urls=urls, - url=it['mediasetprogram$pageUrl'])) + url=it['mediasetprogram$pageUrl'], + year=it.get('year',''), + forcethumb=True, + no_return=True)) if episode: itemlist = sorted(itemlist, key=lambda it: it.title) support.videolibrary(itemlist, item) @@ -267,12 +279,17 @@ def episodios(item): def findvideos(item): support.info() - itemlist = [support.Item(server='directo', title='Direct', url=item.urls, action='play')] + itemlist = [support.Item(server='directo', title='Mediaset Play', url=item.urls, action='play')] return support.server(item, itemlist=itemlist, Download=False) def play(item): support.info() + if item.livefilter: + d = liveDict()[item.livefilter] + # support.dbg() + item = item.clone(title=support.typo(item.livefilter, 'bold'), fulltitle=item.livefilter, urls=d['urls'], plot=d['plot'], action='play', forcethumb=True, no_return=True) + support.thumb(item, live=True) if not item.urls: urls = item.url else: urls = item.urls data = '' @@ -285,6 +302,10 @@ def play(item): item.drm = DRM item.license = lic_url % support.match(sec_data, patron=r'pid=([^|]+)').match data = support.match(sec_data, patron=r'<video src="([^"]+)').match + break + else: + support.dbg() + data = url return support.servertools.find_video_items(item, data=data) diff --git a/channels/metalvideo.py b/channels/metalvideo.py index 441b4922..50c5817c 100644 --- a/channels/metalvideo.py +++ b/channels/metalvideo.py @@ -36,9 +36,10 @@ def mainlist(item): @support.scrape def peliculas(item): + # debug=True action = 'findvideos' - patron= r'<img src="[^"]+" alt="(?P<title>[^"]+)" data-echo="(?P<thumb>[^"]+)"[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+><a href="(?P<url>[^"]+)"' - patronNext = r'<a href="([^"]+)">»' + patron= r'<img src="[^"]+" alt="(?P<title>[^"]+)" data-echo="(?P<thumb>[^"]+)"(?:[^>]+>){7}<a href="(?P<url>[^"]+)"' + patronNext = r'<a href="([^"]+)">(?:»|»)' typeContentDict = {'': 'music'} return locals() diff --git a/channels/mondoserietv.py b/channels/mondoserietv.py index a3a9c194..f45baefa 100644 --- a/channels/mondoserietv.py +++ b/channels/mondoserietv.py @@ -82,7 +82,7 @@ def peliculas(item): # debug=True if item.args == 'last': patronBlock = r'<table>(?P<block>.*?)</table>' - patron = r'<tr><td><a href="(?P<url>[^"]+)">\s*[^>]+>(?P<title>.*?)(?:\s(?P<year>\d{4}))? (?:Streaming|</b>)' + patron = r'<tr><td><a href="(?P<url>[^"]+)">\s*[^>]+>(?P<title>.*?)(?:\s(?P<year>\d{4}))?\s*(?:Streaming|</b>)' elif item.args == 'lastep': patronBlock = r'<table>(?P<block>.*?)</table>' patron = r'<td>\s*<a href="[^>]+>(?P<title>.*?)(?:\s(?P<year>\d{4}))?\s(?:(?P<episode>(?:\d+x\d+|\d+)))\s*(?P<title2>[^<]+)(?P<url>.*?)<tr>' diff --git a/channels/paramount.py b/channels/paramount.py index aed03c53..f185b9de 100644 --- a/channels/paramount.py +++ b/channels/paramount.py @@ -5,6 +5,7 @@ import inspect from core import support, jsontools from platformcode import autorenumber, logger +from collections import OrderedDict host = support.config.get_channel_url() headers = [['Referer', host]] @@ -39,24 +40,31 @@ def search(item, text): return [] -def live(item): - logger.debug() - itemlist=[] +def liveDict(): + livedict = OrderedDict({}) urls=[] matches = support.match(host, patron=r'(/diretta-tv/[^"]+)"[^>]+>([^ ]+)').matches from datetime import date today = date.today() channels = jsontools.load(support.match(host + '/api/more/tvschedule/' + str(today.year) + str(today.month) + str(today.day)).data)['channels'] - ch_dict = {} + for channel in channels: - ch_dict[channel['label']] = channel['channelId'] + title = channel['label'] + livedict[title] = {} + livedict[title]['id'] = channel['channelId'] for url, title in matches: if url not in urls: urls.append(url) - info = jsontools.load(support.match(host +'/api/on-air?channelId=' + ch_dict[title]).data) - support.info(info) - plot= '[B]' + info['seriesTitle'] +'[/B]\n' + info['description'] if 'seriesTitle' in info else '' - itemlist.append(item.clone(title=support.typo(title,'bold'), contentTitle=title, fulltitle=title, show=title, url=host+url, plot=plot, action='play', forcethumb=True, no_return=True)) + livedict[title]['url'] = host + url + info = jsontools.load(support.match(host +'/api/on-air?channelId=' + livedict[title]['id']).data) + livedict[title]['plot']= '[B]' + info['seriesTitle'] +'[/B]\n' + info['description'] if 'seriesTitle' in info else '' + return livedict + +def live(item): + logger.debug() + itemlist=[] + for key, value in liveDict().items(): + itemlist.append(item.clone(title=support.typo(key,'bold'), contentTitle=key, fulltitle=key, show=key, url=value['url'], plot=value['plot'], action='play', forcethumb=True, no_return=True)) return support.thumb(itemlist, live=True) @@ -154,4 +162,9 @@ def findvideos(item): def play(item): logger.debug() - return support.servertools.find_video_items(item, data=item.url) \ No newline at end of file + item.server = 'paramount_server' + if item.livefilter: + d = liveDict()[item.livefilter] + item = item.clone(title=support.typo(item.livefilter, 'bold'), fulltitle=item.livefilter, url=d['url'], plot=d['plot'], action='play', forcethumb=True, no_return=True) + support.thumb(item, live=True) + return [item] \ No newline at end of file diff --git a/channels/raiplay.py b/channels/raiplay.py index 1f18a110..51a6adb5 100644 --- a/channels/raiplay.py +++ b/channels/raiplay.py @@ -4,12 +4,14 @@ # ------------------------------------------------------------ import requests, sys, inspect -from core import support -from platformcode import autorenumber, logger +from core import support, channeltools +from platformcode import autorenumber, logger, platformtools +from collections import OrderedDict if sys.version_info[0] >= 3: from concurrent import futures else: from concurrent_py2 import futures + current_session = requests.Session() host = support.config.get_channel_url() onair = host + '/palinsesto/onAir.json' @@ -17,19 +19,19 @@ onair = host + '/palinsesto/onAir.json' @support.menu def mainlist(item): - top = [('Dirette {bold}', ['/dl/RaiPlay/2016/PublishingBlock-9a2ff311-fcf0-4539-8f8f-c4fee2a71d58.html?json', 'live']), + top = [('Dirette {bold}', ['', 'live']), ('Replay {bold}', ['/dl/RaiPlay/2016/PublishingBlock-9a2ff311-fcf0-4539-8f8f-c4fee2a71d58.html?json', 'replay_menu'])] - menu = [('Film {bullet bold}', ['/film/index.json', 'menu']), - ('Serie TV {bullet bold}', ['/serietv/index.json', 'menu']), - ('Fiction {bullet bold}', ['/fiction/index.json', 'menu']), - ('Documentari {bullet bold}', ['/documentari/index.json', 'menu']), - ('Programmi TV{bullet bold}', ['/programmi/index.json', 'menu']), - ('Programmi per Bambini {bullet bold}', ['/bambini/index.json', 'menu']), - ('Teen {bullet bold}', ['/teen/index.json', 'learning']), - ('Learning {bullet bold}', ['/learning/index.json', 'learning']), - ('Teche Rai {bullet bold storia}', ['/techerai/index.json', 'menu']), - ('Musica e Teatro {bullet bold}', ['/performing-arts/index.json', 'menu']) + menu = [('Film {bullet bold}', ['/tipologia/film/index.json', 'menu']), + ('Serie TV {bullet bold}', ['/tipologia/serietv/index.json', 'menu']), + ('Fiction {bullet bold}', ['/tipologia/fiction/index.json', 'menu']), + ('Documentari {bullet bold}', ['/tipologia/documentari/index.json', 'menu']), + ('Programmi TV{bullet bold}', ['/tipologia/programmi/index.json', 'menu']), + ('Programmi per Bambini {bullet bold}', ['/tipologia/bambini/index.json', 'menu']), + ('Teen {bullet bold}', ['/tipologia/teen/index.json', 'learning']), + ('Learning {bullet bold}', ['/tipologia/learning/index.json', 'learning']), + ('Teche Rai {bullet bold storia}', ['/tipologia/techerai/index.json', 'menu']), + ('Musica e Teatro {bullet bold}', ['/tipologia/musica-e-teatro/index.json', 'menu']) ] search = '' @@ -157,24 +159,33 @@ def Type(item): return select(item) -def live(item): - support.info() - itemlist =[] - info={} - json = current_session.get(item.url).json()['dirette'] +def liveDict(): + livedict = OrderedDict({}) + info = {} + url = host + '/dirette.json' + json = current_session.get(url).json()['contents'] onAir = current_session.get(onair).json()['on_air'] - support.info(onAir) for key in onAir: channel = key['channel'] info[channel] = {} info[channel]['fanart'] = getUrl(key['currentItem']['image']) info[channel]['plot'] = support.typo(key['currentItem']['name'],'bold')+ '\n\n' + key['currentItem']['description'] - - for i, key in enumerate(json): + for key in json: channel = key['channel'] - itemlist.append(item.clone(title = support.typo(channel, 'bold'), fulltitle = channel, show = channel, url = key['video']['contentUrl'], - thumbnail = key['transparent-icon'].replace("[RESOLUTION]", "256x-"), forcethumb = True , fanart = info[channel]['fanart'], - plot = info[channel]['plot'], action = 'play', no_return=True)) + livedict[channel] = {} + livedict[channel]['url'] = key['video']['content_url'] + livedict[channel]['plot'] = info[channel]['plot'] + livedict[channel]['fanart'] = info[channel]['fanart'] + + return livedict + + +def live(item): + support.info() + itemlist =[] + for channel, value in liveDict().items(): + itemlist.append(item.clone(title = support.typo(channel, 'bold'), fulltitle = channel, show = channel, url = value['url'], + plot = value['plot'], action = 'play', fanart = value['fanart'], no_return=True)) return support.thumb(itemlist, live=True) @@ -331,7 +342,7 @@ def findvideos(item): else: url = item.url - itemlist.append(item.clone(server = 'directo', title = support.config.get_localized_string(30137), fanart = item.json, url = getUrl(url), action = 'play' )) + itemlist.append(item.clone(server = 'directo', title = 'Rai Play', url = getUrl(url) + '&output=56', action = 'play')) return support.server(item, itemlist=itemlist, Download=False) @@ -350,7 +361,7 @@ def getUrl(pathId): if url.endswith(".html?json"): url = url.replace(".html?json", ".json") elif url.endswith("/?json"): - url = url.replace("/?json","/index.json") + url = url.replace("/?json",".json") elif url.endswith("?json"): url = url.replace("?json",".json") @@ -407,3 +418,16 @@ def load_episodes(key, item): return itemlist +def play(item): + if item.livefilter: + d = liveDict() + item = item.clone(server='directo', fulltitle=item.livefilter, url=d[item.livefilter]['url'], plot=d[item.livefilter]['plot'], forcethumb=True, no_return=True) + support.thumb(item, live=True) + if '&output=56' in item.url: + match = support.match(item, patron=r'content"><!\[CDATA\[([^\]]+)(?:.*?"WIDEVINE","licenceUrl":"([^"]+))?').match + item.url = match[0] + if len(match) == 2: + item.drm = 'com.widevine.alpha' + item.license = match[1] + '|' + host + '|R{SSM}|' + logger.debug('PLAY URL', item.url) + return [item] \ No newline at end of file diff --git a/channels/seriehd.py b/channels/seriehd.py index 55b3c64b..da329225 100644 --- a/channels/seriehd.py +++ b/channels/seriehd.py @@ -69,13 +69,13 @@ def peliculas(item): if item.args == 'last': action = 'findvideos' - patron = r'singleUpdate">[^>]+>[^>]+>\s*<img src="(?P<thumb>[^"]+)"[^>]+>[^>]+>[^>]+>\s*<h2>(?P<title>[^<]+)<[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>\s*<a href="(?P<url>[^"]+)">[^>]+>[^>]+>[^>]+>\s*(?P<season>\d+)\D+(?P<episode>\d+)(?:[^\(]*\()?(?P<lang>[^\)]+)?(?:\))?' + patron = r'singleUpdate">(?:[^>]+>){2}\s*<img src="(?P<thumb>[^"]+)"(?:[^>]+>){3}\s*<h2>(?P<title>[^<]+)<(?:[^>]+>){14,16}\s*<a href="(?P<url>[^"]+)">(?:[^>]+>){3}\s*(?P<season>\d+)\D+(?P<episode>\d+)(?:[^\(]*\()?(?P<lang>[^\)]+)?(?:\))?' elif item.args == 'best': action='episodios' - patron = r'col-md-3">\s*<a href="(?P<url>[^"]+)">[^>]+>\s*<div class="infoVetrina">[^>]+>(?P<year>\d{4})[^>]+>[^>]+>(?P<title>[^<]+)<[^>]+>[^>]+>[^>]+>[^>]+>(?P<rating>[^<]+)[^>]+>[^>]+>[^>]+>[^>]+>\s*<img src="(?P<thumb>[^"]+)"' + patron = r'col-md-3">\s*<a href="(?P<url>[^"]+)">[^>]+>\s*<div class="infoVetrina">[^>]+>(?P<year>\d{4})(?:[^>]+>){2}(?P<title>[^<]+)<(?:[^>]+>){4}(?P<rating>[^<]+)(?:[^>]+>){4}\s*<img src="(?P<thumb>[^"]+)"' else: action='episodios' - patron = r'<a href="(?P<url>[^"]+)">[^>]+>\s*<div class="infoSeries">\s*<h2>(?P<title>[^<]+)<[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>(?P<rating>[^<]+)?[^>]+>[^>]+>[^>]+>\s*<img src="(?P<thumb>[^"]+)"[^>]+>[^>]+>[^>]+>(?P<quality>[^<]+)<[^>]+>[^>]+>(?P<year>\d{4})' + patron = r'<a href="(?P<url>[^"]+)">[^>]+>\s*<div class="infoSeries">\s*<h2>(?P<title>[^<]+)<(?:[^>]+>){5}(?P<rating>[^<]+)?(?:[^>]+>){3}\s*<img src="(?P<thumb>[^"]+)"(?:[^>]+>){3}(?P<quality>[^<]+)<(?:[^>]+>){2}(?P<year>\d{4})' patronNext=r'next page-numbers" href="([^"]+)"' return locals() diff --git a/channels/serietvonline.py b/channels/serietvonline.py index 69d34d9f..3b69688c 100644 --- a/channels/serietvonline.py +++ b/channels/serietvonline.py @@ -35,7 +35,7 @@ def mainlist(item): film = ['/ultimi-film-aggiunti/', - ('Lista', ['/lista-film/', 'peliculas', 'lista']) + ('A-Z', ['/lista-film/', 'peliculas', 'lista']) ] tvshow = ['', @@ -65,7 +65,7 @@ def peliculas(item): if item.args == 'search': patronBlock = r'>Lista Serie Tv</a></li></ul></div><div id="box_movies">(?P<block>.*?)<div id="paginador">' - patron = r'<div class="movie">[^>]+[^>]+>\s?<img src="(?P<thumb>[^"]+)" alt="(?P<title>.+?)\s?(?P<year>[\d\-]+)?"[^>]+>\s?<a href="(?P<url>[^"]+)">' + patron = r'<div class="movie">[^>]+[^>]+>\s*<img src="(?P<thumb>[^"]+)" alt="(?P<title>.+?)(?:(?P<year>\d{4})|")[^>]*>\s*<a href="([^"]+)' elif item.contentType == 'episode': pagination = 35 action = 'findvideos' @@ -85,7 +85,7 @@ def peliculas(item): pagination = 25 if item.args == 'lista': - patron = r'href="(?P<url>[^"]+)"[^>]+>(?P<title>.*?)(?P<year>\d{4})?<' + patron = r'href="(?P<url>[^"]+)"[^>]+>(?P<title>.+?)(?:\s(?P<year>\d{4})|<)' patronBlock = r'Lista dei film disponibili in streaming e anche in download\.</p>(?P<block>.*?)<div class="footer_c">' else: patron = r'<tr><td><a href="(?P<url>[^"]+)"(?:|.+?)?>(?:  )?[ ]?(?P<title>.*?)[ ]?(?P<quality>HD)?[ ]?(?P<year>\d+)?(?: | HD | Streaming | MD(?: iSTANCE)? )?</a>' diff --git a/channels/serietvsubita.py b/channels/serietvsubita.py index dd6fe970..a5429293 100644 --- a/channels/serietvsubita.py +++ b/channels/serietvsubita.py @@ -291,25 +291,30 @@ def newest(categoria): def search(item, texto): info(texto) itemlist = [] - + try: patron = r'<li class="cat-item cat-item-\d+"><a href="([^"]+)"\s?>([^<]+)</a>' - matches = support.match(item, patron=patron, headers=headers).matches - for i, (scrapedurl, scrapedtitle) in enumerate(matches): - if texto.upper() in scrapedtitle.upper(): - scrapedthumbnail = "" - scrapedplot = "" - title = cleantitle(scrapedtitle) - itemlist.append( - item.clone(action="episodios", - title=title, - url=scrapedurl, - thumbnail=scrapedthumbnail, - fulltitle=title, - show=title, - plot=scrapedplot, - contentType='episode', - originalUrl=scrapedurl)) - tmdb.set_infoLabels_itemlist(itemlist, seekTmdb=True) + matches = support.match(item, patron=patron, headers=headers).matches + for i, (scrapedurl, scrapedtitle) in enumerate(matches): + if texto.upper() in scrapedtitle.upper(): + scrapedthumbnail = "" + scrapedplot = "" + title = cleantitle(scrapedtitle) + itemlist.append( + item.clone(action="episodios", + title=title, + url=scrapedurl, + thumbnail=scrapedthumbnail, + fulltitle=title, + show=title, + plot=scrapedplot, + contentType='episode', + originalUrl=scrapedurl)) + tmdb.set_infoLabels_itemlist(itemlist, seekTmdb=True) + except: + import sys + for line in sys.exc_info(): + support.info('search log:', line) + return [] return itemlist diff --git a/channels/serietvu.py b/channels/serietvu.py index 446b12c5..e0c68d55 100644 --- a/channels/serietvu.py +++ b/channels/serietvu.py @@ -33,14 +33,15 @@ def mainlist(item): @support.scrape def peliculas(item): + # debug=True patronBlock = r'<div class="wrap">\s*<h.>.*?</h.>(?P<block>.*?)<footer>' if item.args != 'update': action = 'episodios' - patron = r'<div class="item">\s*<a href="(?P<url>[^"]+)" data-original="(?P<thumb>[^"]+)" class="lazy inner">[^>]+>[^>]+>[^>]+>[^>]+>(?P<title>[^<]+)<' + patron = r'<div class="item">\s*?<a href="(?P<url>[^"]+)" data-original="(?P<thumb>[^"]+)" class="lazy inner">(?:[^>]+>){4}(?P<title>[^<]+)<' else: action = 'findvideos' - patron = r'<div class="item">\s+?<a href="(?P<url>[^"]+)"\s+?data-original="(?P<thumb>[^"]+)"[^>]+>[^>]+>[^>]+>[^>]+>[^>]+>(?P<title>.+?)<[^>]+>\((?P<episode>[\dx\-]+)\s+?(?P<lang>Sub-Ita|[iITtAa]+)\)<' + patron = r'<div class="item">\s*?<a href="(?P<url>[^"]+)"\s*?data-original="(?P<thumb>[^"]+)"(?:[^>]+>){5}(?P<title>.+?)<[^>]+>\((?P<episode>[\dx\-]+)\s+?(?P<lang>Sub-Ita|[iITtAa]+)\)<' pagination = 25 patronNext = r'<li><a href="([^"]+)"\s+?>Pagina successiva' diff --git a/channels/vvvvid.py b/channels/vvvvid.py index 9828fa8a..537f31b0 100644 --- a/channels/vvvvid.py +++ b/channels/vvvvid.py @@ -220,7 +220,8 @@ def make_itemlist(itemlist, item, data): for key in data['data']: if search.lower() in encode(key['title']).lower(): infoLabels['year'] = key['date_published'] - infoLabels['title'] = infoLabels['tvshowtitle'] = key['title'] + infoLabels['title'] = key['title'] + if item.contentType != 'movie': infoLabels['tvshowtitle'] = key['title'] title = encode(key['title']) itemlist.append( item.clone(title = support.typo(title, 'bold'), diff --git a/core/__init__.py b/core/__init__.py index eed08f62..ccf1b26e 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -6,7 +6,28 @@ import sys # Appends the main plugin dir to the PYTHONPATH if an internal package cannot be imported. # Examples: In Plex Media Server all modules are under "Code.*" package, and in Enigma2 under "Plugins.Extensions.*" try: - # from core import logger import core except: sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +# Connect to database +from . import filetools +from platformcode import config +from collections import defaultdict +from lib.sqlitedict import SqliteDict + + +class nested_dict_sqlite(defaultdict): + 'like defaultdict but default_factory receives the key' + + def __missing__(self, key): + self[key] = value = self.default_factory(key) + return value + + def close(self): + for key in self.keys(): + self[key].close() + + +db_name = filetools.join(config.get_data_path(), "db.sqlite") +db = nested_dict_sqlite(lambda table: SqliteDict(db_name, table, 'c', True)) diff --git a/core/httptools.py b/core/httptools.py index 4df5e5a8..6ec584a2 100755 --- a/core/httptools.py +++ b/core/httptools.py @@ -419,7 +419,7 @@ def downloadpage(url, **opt): response['data'] = response['data'].decode('ISO-8859-1') if req.headers.get('Server', '').startswith('cloudflare') and response_code in [429, 503, 403]\ - and not opt.get('CF', False) and 'Please turn JavaScript on and reload the page' in response['data']: + and not opt.get('CF', False) and 'Ray ID' in response['data'] and not opt.get('post', None): logger.debug("CF retry... for domain: %s" % domain) from lib import proxytranslate gResp = proxytranslate.process_request_proxy(url) diff --git a/core/resolverdns.py b/core/resolverdns.py index 662284eb..8e87ecd2 100644 --- a/core/resolverdns.py +++ b/core/resolverdns.py @@ -12,16 +12,11 @@ else: from lib.requests_toolbelt.adapters import host_header_ssl from lib import doh -from platformcode import logger, config +from platformcode import logger import requests from core import scrapertools +from core import db -try: - import _sqlite3 as sql -except: - import sqlite3 as sql - -db = os.path.join(config.get_data_path(), 'kod_db.sqlite') if 'PROTOCOL_TLS' in ssl.__dict__: protocol = ssl.PROTOCOL_TLS elif 'PROTOCOL_SSLv23' in ssl.__dict__: @@ -48,8 +43,6 @@ class CustomContext(ssl.SSLContext): class CipherSuiteAdapter(host_header_ssl.HostHeaderSSLAdapter): def __init__(self, domain, CF=False, *args, **kwargs): - self.conn = sql.connect(db) - self.cur = self.conn.cursor() self.ssl_context = CustomContext(protocol, domain) self.CF = CF # if cloudscrape is in action self.cipherSuite = kwargs.pop('cipherSuite', DEFAULT_CIPHERS) @@ -57,18 +50,13 @@ class CipherSuiteAdapter(host_header_ssl.HostHeaderSSLAdapter): super(CipherSuiteAdapter, self).__init__(**kwargs) def flushDns(self, request, domain, **kwargs): - self.cur.execute('delete from dnscache where domain=?', (domain,)) - self.conn.commit() + del db['dnscache'][domain] return self.send(request, flushedDns=True, **kwargs) def getIp(self, domain): - ip = None - try: - self.cur.execute('select ip from dnscache where domain=?', (domain,)) - ip = self.cur.fetchall()[0][0] - logger.info('Cache DNS: ' + domain + ' = ' + str(ip)) - except: - pass + ip = db['dnscache'].get(domain, None) + logger.info('Cache DNS: ' + domain + ' = ' + str(ip)) + if not ip: # not cached try: ip = doh.query(domain)[0] @@ -81,15 +69,7 @@ class CipherSuiteAdapter(host_header_ssl.HostHeaderSSLAdapter): return ip def writeToCache(self, domain, ip): - try: - self.cur.execute('insert into dnscache values(?,?)', (domain, ip)) - except: - self.cur.execute("""CREATE TABLE IF NOT EXISTS dnscache( - "domain" TEXT NOT NULL UNIQUE, - "ip" TEXT NOT NULL, - PRIMARY KEY("domain") - );""") - self.conn.commit() + db['dnscache'][domain] = ip def init_poolmanager(self, *args, **kwargs): kwargs['ssl_context'] = self.ssl_context diff --git a/core/scraper.py b/core/scraper.py index c2951fc7..55158107 100644 --- a/core/scraper.py +++ b/core/scraper.py @@ -32,19 +32,21 @@ def find_and_set_infoLabels(item): # Get the default Scraper of the configuration according to the content type if item.contentType == "movie": - scraper_actual = ['tmdb'][config.get_setting("scraper_movies", "videolibrary")] + scraper_actual = 'tmdb' + # scraper_actual = ['tmdb'][config.get_setting("scraper_movies", "videolibrary")] tipo_contenido = "movie" title = item.contentTitle # Complete list of options for this type of content list_opciones_cuadro.append(scrapers_disponibles['tmdb']) else: - scraper_actual = ['tmdb', 'tvdb'][config.get_setting("scraper_tvshows", "videolibrary")] + scraper_actual = 'tmdb' + # scraper_actual = ['tmdb', 'tvdb'][config.get_setting("scraper_tvshows", "videolibrary")] tipo_contenido = "serie" title = item.contentSerieName # Complete list of options for this type of content list_opciones_cuadro.append(scrapers_disponibles['tmdb']) - list_opciones_cuadro.append(scrapers_disponibles['tvdb']) + # list_opciones_cuadro.append(scrapers_disponibles['tvdb']) # We import the scraper try: @@ -187,7 +189,7 @@ def callback_cuadro_completar(item, dict_values): return False -def get_nfo(item): +def get_nfo(item, search_groups=False): """ Returns the information necessary for the result to be scraped into the kodi video library, @@ -229,7 +231,7 @@ def get_nfo(item): if item.contentType == "movie": scraper_actual = ['tmdb'][config.get_setting("scraper_movies", "videolibrary")] else: scraper_actual = ['tmdb', 'tvdb'][config.get_setting("scraper_tvshows", "videolibrary")] scraper = __import__('core.%s' % scraper_actual, fromlist=["core.%s" % scraper_actual]) - return scraper.get_nfo(item) + return scraper.get_nfo(item, search_groups) def sort_episode_list(episodelist): diff --git a/core/support.py b/core/support.py index 620fdd61..b243e539 100755 --- a/core/support.py +++ b/core/support.py @@ -1367,7 +1367,7 @@ def thumb(item_itemlist_string=None, genre=False, live=False): for item in item_itemlist_string: item.thumbnail = "https://raw.githubusercontent.com/kodiondemand/media/master/live/" + item.fulltitle.lower().replace(' ','_') + '.png' else: - item_itemlist_string.thumbnail = "https://raw.githubusercontent.com/kodiondemand/media/master/live/" + item.fulltitle.lower().replace(' ','_') + '.png' + item_itemlist_string.thumbnail = "https://raw.githubusercontent.com/kodiondemand/media/master/live/" + item_itemlist_string.fulltitle.lower().replace(' ','_') + '.png' return item_itemlist_string icon_dict = {'movie':['film', 'movie'], diff --git a/core/tmdb.py b/core/tmdb.py index 0b6c709e..0d863929 100644 --- a/core/tmdb.py +++ b/core/tmdb.py @@ -3,7 +3,8 @@ # from future import standard_library # standard_library.install_aliases() # from builtins import str -import sys +import datetime +import sys, requests PY3 = False if sys.version_info[0] >= 3: PY3 = True; unicode = str; unichr = chr; long = int @@ -15,7 +16,7 @@ else: from future.builtins import range from future.builtins import object -import ast, copy, re, sqlite3, time, xbmcaddon +import ast, copy, re, time from core import filetools, httptools, jsontools, scrapertools from core.item import InfoLabels @@ -62,27 +63,7 @@ def_lang = info_language[config.get_setting("info_language", "videolibrary")] # ------------------------------------------------- -------------------------------------------------- ----------- otmdb_global = None -fname = filetools.join(config.get_data_path(), "kod_db.sqlite") - -def create_bd(): - conn = sqlite3.connect(fname) - c = conn.cursor() - c.execute('CREATE TABLE IF NOT EXISTS tmdb_cache (url TEXT PRIMARY KEY, response TEXT, added TEXT)') - conn.commit() - conn.close() - - -def drop_bd(): - conn = sqlite3.connect(fname) - c = conn.cursor() - c.execute('DROP TABLE IF EXISTS tmdb_cache') - conn.commit() - conn.close() - - return True - - -create_bd() +from core import db # The function name is the name of the decorator and receives the function that decorates. @@ -93,17 +74,11 @@ def cache_response(fn): # start_time = time.time() def wrapper(*args, **kwargs): - import base64 - - def check_expired(ts): - import datetime - + def check_expired(saved_date): valided = False cache_expire = config.get_setting("tmdb_cache_expire", default=0) - - saved_date = datetime.datetime.fromtimestamp(ts) - current_date = datetime.datetime.fromtimestamp(time.time()) + current_date = datetime.datetime.now() elapsed = current_date - saved_date # 1 day @@ -148,29 +123,18 @@ def cache_response(fn): result = fn(*args) else: - conn = sqlite3.connect(fname, timeout=15) - c = conn.cursor() url = re.sub('&year=-', '', args[0]) if PY3: url = str.encode(url) - url_base64 = base64.b64encode(url) - c.execute("SELECT response, added FROM tmdb_cache WHERE url=?", (url_base64,)) - row = c.fetchone() - if row and check_expired(float(row[1])): - result = eval(base64.b64decode(row[0])) + row = db['tmdb_cache'].get(url) + + if row and check_expired(row[1]): + result = row[0] # si no se ha obtenido información, llamamos a la funcion if not result: result = fn(*args) - result = str(result) - if PY3: result = str.encode(result) - result_base64 = base64.b64encode(result) - c.execute("INSERT OR REPLACE INTO tmdb_cache (url, response, added) VALUES (?, ?, ?)", - (url_base64, result_base64, time.time())) - - conn.commit() - - conn.close() + db['tmdb_cache'][url] = [result, datetime.datetime.now()] # elapsed_time = time.time() - start_time # logger.debug("TARDADO %s" % elapsed_time) @@ -550,7 +514,7 @@ def find_and_set_infoLabels(item): return False -def get_nfo(item): +def get_nfo(item, search_groups=False): """ Returns the information necessary for the result to be scraped into the kodi video library, for tmdb it works only by passing it the url. @param item: element that contains the data necessary to generate the info @@ -558,6 +522,23 @@ def get_nfo(item): @rtype: str @return: """ + + if search_groups: + from platformcode.autorenumber import RENUMBER, GROUP + path = filetools.join(config.get_data_path(), "settings_channels", item.channel + "_data.json") + if filetools.exists(path): + g = jsontools.load(filetools.read(path)).get(RENUMBER,{}).get(item.fulltitle.strip(),{}).get(GROUP,'') + if g: return g + '\n' + + groups = get_groups(item) + + if groups: + Id = select_group(groups) + if Id: + info_nfo = 'https://www.themoviedb.org/tv/{}/episode_group/{}\n'.format(item.infoLabels['tmdb_id'], Id) + return info_nfo + else: return + if "season" in item.infoLabels and "episode" in item.infoLabels: info_nfo = "https://www.themoviedb.org/tv/%s/season/%s/episode/%s\n" % (item.infoLabels['tmdb_id'], item.contentSeason, item.contentEpisodeNumber) else: @@ -565,6 +546,34 @@ def get_nfo(item): return info_nfo +def get_groups(item): + url = 'https://api.themoviedb.org/3/tv/{}/episode_groups?api_key=a1ab8b8669da03637a4b98fa39c39228&language={}'.format(item.infoLabels['tmdb_id'], def_lang) + groups = requests.get(url).json().get('results',[]) + return groups + +def select_group(groups): + selected = -1 + selections = [] + ids = [] + for group in groups: + name = '[B]{}[/B] Seasons: {} Episodes: {}'.format(group.get('name',''), group.get('group_count',''), group.get('episode_count','')) + description = group.get('description','') + if description: + name = '{}\n{}'.format(name, description) + ID = group.get('id','') + if ID: + selections.append(name) + ids.append(ID) + if selections and ids: + selected = platformtools.dialog_select_group(config.get_localized_string(70831), selections) + if selected > -1: + return ids[selected] + return '' + +def get_group(Id): + url = 'https://api.themoviedb.org/3/tv/episode_group/{}?api_key=a1ab8b8669da03637a4b98fa39c39228&language={}'.format(Id, def_lang) + group = requests.get(url).json().get('groups',[]) + return group def completar_codigos(item): """ @@ -1009,6 +1018,11 @@ class Tmdb(object): % (buscando, len(results), page, index_results)) return 0 + # We sort result based on fuzzy match to detect most similar + if len(results) > 1: + from lib.fuzzy_match import algorithims + results.sort(key=lambda r: algorithims.trigram(text_simple, r['title'] if self.busqueda_tipo == 'movie' else r['name']), reverse=True) + # We return the number of results of this page self.results = results self.total_results = total_results @@ -1430,6 +1444,11 @@ class Tmdb(object): return ret_dic + def get_list_episodes(self): + url = 'https://api.themoviedb.org/3/tv/{id}?api_key=a1ab8b8669da03637a4b98fa39c39228&language={lang}'.format(id=self.busqueda_id, lang=self.busqueda_idioma) + results = requests.get(url).json().get('seasons', []) + return results if 'Error' not in results else [] + def get_videos(self): """ :return: Returns an ordered list (language / resolution / type) of Dict objects in which each of its elements corresponds to a trailer, teaser or clip from youtube. diff --git a/core/tvdb.py b/core/tvdb.py index eb635d36..6bc3f9fd 100644 --- a/core/tvdb.py +++ b/core/tvdb.py @@ -298,7 +298,7 @@ def set_infoLabels_item(item): return len(item.infoLabels) -def get_nfo(item): +def get_nfo(item, search_groups=False): """ Returns the information necessary for the result to be scraped into the kodi video library, diff --git a/core/videolibrarytools.py b/core/videolibrarytools.py index 65956483..bababbf4 100644 --- a/core/videolibrarytools.py +++ b/core/videolibrarytools.py @@ -14,6 +14,7 @@ from core import filetools, scraper, scrapertools from core.item import Item from lib import generictools from platformcode import config, logger, platformtools +from platformcode.autorenumber import RENUMBER FOLDER_MOVIES = config.get_setting("folder_movies") FOLDER_TVSHOWS = config.get_setting("folder_tvshows") @@ -234,14 +235,14 @@ def update_renumber_options(item, head_nfo, path): if filetools.isfile(tvshow_path) and item.channel_prefs: for channel in item.channel_prefs: filename = filetools.join(config.get_data_path(), "settings_channels", channel + '_data.json') - - json_file = jsontools.load(filetools.read(filename)) - if 'TVSHOW_AUTORENUMBER' in json_file: - json = json_file['TVSHOW_AUTORENUMBER'] - if item.fulltitle in json: - item.channel_prefs[channel]['TVSHOW_AUTORENUMBER'] = json[item.fulltitle] - logger.debug('UPDATED=\n' + str(item.channel_prefs)) - filetools.write(tvshow_path, head_nfo + item.tojson()) + if filetools.isfile(filename): + json_file = jsontools.load(filetools.read(filename)) + if RENUMBER in json_file: + json = json_file[RENUMBER] + if item.fulltitle in json: + item.channel_prefs[channel][RENUMBER] = json[item.fulltitle] + logger.debug('UPDATED=\n' + str(item.channel_prefs)) + filetools.write(tvshow_path, head_nfo + item.tojson()) def add_renumber_options(item, head_nfo, path): from core import jsontools @@ -249,8 +250,8 @@ def add_renumber_options(item, head_nfo, path): ret = None filename = filetools.join(config.get_data_path(), "settings_channels", item.channel + '_data.json') json_file = jsontools.load(filetools.read(filename)) - if 'TVSHOW_AUTORENUMBER' in json_file: - json = json_file['TVSHOW_AUTORENUMBER'] + if RENUMBER in json_file: + json = json_file[RENUMBER] if item.fulltitle in json: ret = json[item.fulltitle] return ret @@ -258,11 +259,11 @@ def add_renumber_options(item, head_nfo, path): def check_renumber_options(item): from platformcode.autorenumber import load, write for key in item.channel_prefs: - if 'TVSHOW_AUTORENUMBER' in item.channel_prefs[key]: + if RENUMBER in item.channel_prefs[key]: item.channel = key json = load(item) if not json or item.fulltitle not in json: - json[item.fulltitle] = item.channel_prefs[key]['TVSHOW_AUTORENUMBER'] + json[item.fulltitle] = item.channel_prefs[key][RENUMBER] write(item, json) # head_nfo, tvshow_item = read_nfo(filetools.join(item.context[0]['nfo'])) @@ -275,7 +276,7 @@ def filter_list(episodelist, action=None, path=None): # if xbmc.getCondVisibility('system.platform.windows') > 0: path = path.replace('smb:','').replace('/','\\') channel_prefs = {} lang_sel = quality_sel = show_title = channel ='' - # from core.support import dbg;dbg() + if action: tvshow_path = filetools.join(path, "tvshow.nfo") head_nfo, tvshow_item = read_nfo(tvshow_path) @@ -294,7 +295,7 @@ def filter_list(episodelist, action=None, path=None): renumber = add_renumber_options(episodelist[0], head_nfo, tvshow_path) if renumber: - channel_prefs['TVSHOW_AUTORENUMBER'] = renumber + channel_prefs[RENUMBER] = renumber if action == 'get_seasons': if 'favourite_language' not in channel_prefs: @@ -499,7 +500,9 @@ def save_tvshow(item, episodelist, silent=False): if not filetools.exists(tvshow_path): # We create tvshow.nfo, if it does not exist, with the head_nfo, series info and watched episode marks logger.debug("Creating tvshow.nfo: " + tvshow_path) - head_nfo = scraper.get_nfo(item) + head_nfo = scraper.get_nfo(item, search_groups=True) + if not head_nfo: + return 0, 0, 0, '' item.infoLabels['mediatype'] = "tvshow" item.infoLabels['title'] = item.contentSerieName item_tvshow = Item(title=item.contentSerieName, channel="videolibrary", action="get_seasons", @@ -1004,21 +1007,20 @@ def add_movie(item): # If you do it in "Enter another name", TMDB will automatically search for the new title # If you do it in "Complete Information", it partially changes to the new title, but does not search TMDB. We have to do it # If the second screen is canceled, the variable "scraper_return" will be False. The user does not want to continue - item = generictools.update_title(item) # We call the method that updates the title with tmdb.find_and_set_infoLabels #if item.tmdb_stat: # del item.tmdb_stat # We clean the status so that it is not recorded in the Video Library - # if item: - new_item = item.clone(action="findvideos") - insertados, sobreescritos, fallidos, path = save_movie(new_item) + if item: + new_item = item.clone(action="findvideos") + insertados, sobreescritos, fallidos, path = save_movie(new_item) - if fallidos == 0: - platformtools.dialog_ok(config.get_localized_string(30131), - config.get_localized_string(30135) % new_item.contentTitle) # 'has been added to the video library' - else: - filetools.rmdirtree(path) - platformtools.dialog_ok(config.get_localized_string(30131), - config.get_localized_string(60066) % new_item.contentTitle) # "ERROR, the movie has NOT been added to the video library") + if fallidos == 0: + platformtools.dialog_ok(config.get_localized_string(30131), + config.get_localized_string(30135) % new_item.contentTitle) # 'has been added to the video library' + else: + filetools.rmdirtree(path) + platformtools.dialog_ok(config.get_localized_string(30131), + config.get_localized_string(60066) % new_item.contentTitle) # "ERROR, the movie has NOT been added to the video library") def add_tvshow(item, channel=None): @@ -1100,7 +1102,10 @@ def add_tvshow(item, channel=None): magnet_caching = False insertados, sobreescritos, fallidos, path = save_tvshow(item, itemlist) - if not insertados and not sobreescritos and not fallidos: + if not path: + pass + + elif not insertados and not sobreescritos and not fallidos: filetools.rmdirtree(path) platformtools.dialog_ok(config.get_localized_string(30131), config.get_localized_string(60067) % item.show) logger.error("The string %s could not be added to the video library. Could not get any episode" % item.show) diff --git a/default.py b/default.py index a8ccc0bb..eb72aed5 100644 --- a/default.py +++ b/default.py @@ -27,6 +27,5 @@ from platformcode import launcher if sys.argv[2] == "": launcher.start() - launcher.run() -else: - launcher.run() + +launcher.run() diff --git a/lib/fuzzy_match/__init__.py b/lib/fuzzy_match/__init__.py new file mode 100644 index 00000000..84e0db87 --- /dev/null +++ b/lib/fuzzy_match/__init__.py @@ -0,0 +1,4 @@ +# __init__.py + +# Version of the fuzzy-match package +__version__ = "0.0.1" \ No newline at end of file diff --git a/lib/fuzzy_match/algorithims.py b/lib/fuzzy_match/algorithims.py new file mode 100644 index 00000000..d1fe951b --- /dev/null +++ b/lib/fuzzy_match/algorithims.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# encoding: utf-8 +import math +from math import floor, ceil +import re +from collections import Counter +# import numpy as np + + + +def find_ngrams(string, split_num=3): + """ + Slice string into ngrams. + Returns array of ngrams for the given string. + + Arguments: + text: the string to find ngrams for. + split_num: the length the ngrams should be. Defaults to 3 (trigrams). + """ + try: + if not string: + return set() + + words = [' {} '.format(x) for x in re.split(r'\W+', str(string).lower()) if x.strip()] + + ngrams = set() + + for word in words: + for x in range(0, len(word) - split_num + 1): + ngrams.add(word[x:x+split_num]) + + return ngrams + + except: + return None + + +def trigram(text1, text2, split_num=3): + """ + Find the similarity between two strings using ngrams. + Returns float score value, 0.0 being completely different strings and 1.0 being equal strings. + + Arguments: + text1: main string to compare against. + text2: second string to compare to text1. + split_num: the length the ngrams should be. Defaults to 3 (trigrams). + """ + try: + ngrams1 = find_ngrams(text1, split_num) + ngrams2 = find_ngrams(text2, split_num) + + num_unique = len(ngrams1 | ngrams2) + num_equal = len(ngrams1 & ngrams2) + + score = round(float(num_equal) / float(num_unique), 6) + + return score + + except: + return None + + +def cosine(text1, text2): + """ + Find the similarity between two strings using cosine vectors. + Returns float score value, 0.0 being completely different strings and 1.0 being equal strings. + + Arguments: + text1: main string to compare against. + text2: second string to compare to text1. + """ + try: + vec1 = Counter(re.compile(r"\w+").findall(text1)) + vec2 = Counter(re.compile(r"\w+").findall(text2)) + intersection = set(vec1.keys()) & set(vec2.keys()) + numerator = sum([vec1[x] * vec2[x] for x in intersection]) + + sum1 = sum([vec1[x] ** 2 for x in list(vec1.keys())]) + sum2 = sum([vec2[x] ** 2 for x in list(vec2.keys())]) + denominator = math.sqrt(sum1) * math.sqrt(sum2) + + if not denominator: + return 0.0 + else: + return float(numerator) / denominator + + except: + return None + + +def levenshtein(text1, text2): + """ + Find the similarity between two strings using Levenshtein distance. + Returns float score value, 0.0 being completely different strings and 1.0 being equal strings. + + Arguments: + text1: main string to compare against. + text2: second string to compare to text1. + """ + try: + size_x = len(text1) + 1 + size_y = len(text2) + 1 + matrix = np.zeros ((size_x, size_y)) + for x in range(size_x): + matrix [x, 0] = x + for y in range(size_y): + matrix [0, y] = y + + for x in range(1, size_x): + for y in range(1, size_y): + if text1[x-1] == text2[y-1]: + matrix [x,y] = min( + matrix[x-1, y] + 1, + matrix[x-1, y-1], + matrix[x, y-1] + 1 + ) + else: + matrix [x,y] = min( + matrix[x-1,y] + 1, + matrix[x-1,y-1] + 1, + matrix[x,y-1] + 1 + ) + distance = matrix[size_x - 1, size_y - 1] + score = (max(len(text1), len(text2)) - distance) / max(len(text1), len(text2)) + return float(score) + + except: + return None + + +def jaro_winkler(s1, s2): + """ + Find the similarity between two strings using Jaro-Winkler distance. + Returns float score value, 0.0 being completely different strings and 1.0 being equal strings. + + Arguments: + text1: main string to compare against. + text2: second string to compare to text1. + """ + try: + if (s1 == s2): + return 1.0 + + len1 = len(s1) + len2 = len(s2) + max_dist = floor(max(len1, len2) / 2) - 1 + match = 0 + hash_s1 = [0] * len(s1) + hash_s2 = [0] * len(s2) + + for i in range(len1): + for j in range(max(0, i - max_dist), + min(len2, i + max_dist + 1)): + + if (s1[i] == s2[j] and hash_s2[j] == 0): + hash_s1[i] = 1 + hash_s2[j] = 1 + match += 1 + break + + if (match == 0): + return 0.0 + + t = 0 + point = 0 + + for i in range(len1): + if (hash_s1[i]): + + while (hash_s2[point] == 0): + point += 1 + + if (s1[i] != s2[point]): + point += 1 + t += 1 + t = t//2 + + return float(match/ len1 + match / len2 + + (match - t + 1) / match)/ 3.0 + except: + return None \ No newline at end of file diff --git a/lib/fuzzy_match/match.py b/lib/fuzzy_match/match.py new file mode 100644 index 00000000..c01480ae --- /dev/null +++ b/lib/fuzzy_match/match.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# encoding: utf-8 +import heapq +from . import algorithims + + + +def extract(query, choices, match_type='trigram', score_cutoff=0, limit=5): + """ + Find the similarity between a query item and a list of choices. + Returns a tuple of all choices and their associated similarity score. + + Arguments: + query: The string you are wanting to match. + choices: An iterable or dictionary-like object containing choices + to be matched against the query. + score_cutoff: Optional argument for score threshold. If the best + match is found, but it is not greater than this number, then + return None anyway ("not a good enough match"). Defaults to 0. + + """ + try: + if match_type == 'trigram': + match_type = algorithims.trigram + elif match_type == 'levenshtein': + match_type = algorithims.levenshtein + elif match_type == 'cosine': + match_type = algorithims.cosine + elif match_type == 'jaro_winkler': + match_type = algorithims.jaro_winkler + try: + if choices is None or len(choices) == 0: + return + except TypeError: + pass + + results = [] + + for i in choices: + score = (match_type(query, i)) + data = (i, score) + if score >= score_cutoff: + results.append(data) + + + return heapq.nlargest(limit, results, key=lambda i: i[1]) if limit is not None else \ + sorted(results, key=lambda i: i[1], reverse=True) + + # return results + + except: + return None + + +def extractOne(query, choices, match_type='trigram', score_cutoff=0): + """ + Finds the most similar item to query item from a list of choices. + Returns tuple of best choice and its associated similarity score. + + Arguments: + query: The string you are wanting to match. + choices: An iterable or dictionary-like object containing choices + to be matched against the query. + score_cutoff: Optional argument for score threshold. If the best + match is found, but it is not greater than this number, then + return None anyway ("not a good enough match"). Defaults to 0. + + """ + + try: + best_list = extract(query, choices, match_type, score_cutoff) + + best = max(best_list, key=lambda i: i[1]) + + return best + + except: + return None \ No newline at end of file diff --git a/lib/proxytranslate.py b/lib/proxytranslate.py index e063e6ba..492dda64 100644 --- a/lib/proxytranslate.py +++ b/lib/proxytranslate.py @@ -13,7 +13,10 @@ import re import time import requests -from platformcode import logger +try: + from platformcode import logger +except ImportError: + logger = None HEADERS = { 'Host': 'translate.google.com', @@ -21,9 +24,11 @@ HEADERS = { } MAX_CONECTION_THREAD = 10 +SL = 'en' +TL = 'it' BASE_URL_PROXY = 'https://translate.googleusercontent.com' -BASE_URL_TRANSLATE = 'https://translate.google.com/translate?hl=it&sl=en&tl=it&u=[TARGET_URL]&sandbox=0' # noqa: E501 +BASE_URL_TRANSLATE = 'https://translate.google.com/translate?hl=it&sl=' + SL + '&tl=' + TL + '&u=[TARGET_URL]&sandbox=0' # noqa: E501 def checker_url(html, url): @@ -43,7 +48,10 @@ def process_request_proxy(url): target_url = \ BASE_URL_TRANSLATE.replace('[TARGET_URL]', request.quote(url)) - logger.debug(target_url) + if logger: + logger.debug(target_url) + else: + print(target_url) return_html = requests.get(target_url, timeout=20, headers=HEADERS) @@ -52,10 +60,13 @@ def process_request_proxy(url): url_request = checker_url( return_html.text, - BASE_URL_PROXY + '/translate_p?hl=it&sl=en&tl=it&u=' + BASE_URL_PROXY + '/translate_p?hl=it&sl=' + SL + '&tl=' + TL + '&u=' ) - logger.debug(url_request) + if logger: + logger.debug(url_request) + else: + print(url_request) request_final = requests.get( url_request, @@ -66,7 +77,10 @@ def process_request_proxy(url): url_request_proxy = checker_url( request_final.text, 'translate.google') - logger.debug(url_request_proxy) + if logger: + logger.debug(url_request_proxy) + else: + print(url_request_proxy) data = None result = None @@ -80,12 +94,18 @@ def process_request_proxy(url): data = result.content.decode('utf-8', 'ignore') if not PY3: data = data.encode('utf-8') - logger.debug() + if logger: + logger.debug() data = re.sub('\s(\w+)=(?!")([^<>\s]+)', r' \1="\2"', data) data = re.sub('https://translate\.googleusercontent\.com/.*?u=(.*?)&usg=[A-Za-z0-9_-]+', '\\1', data) data = re.sub('https?://[a-zA-Z0-9]+--' + domain.replace('.', '-') + '\.translate\.goog(/[a-zA-Z0-9#/-]+)', 'https://' + domain + '\\1', data) + data = re.sub('\s+<', '<', data) + data = data.replace('&', '&').replace('https://translate.google.com/website?sl=' + SL + '&tl=' + TL + '&u=', '') - return {'url': url.strip(), 'result': result, 'data': data.replace('&', '&')} + return {'url': url.strip(), 'result': result, 'data': data} except Exception as e: - logger.error(e) + if logger: + logger.error(e) + else: + print(e) diff --git a/lib/sqlitedict.py b/lib/sqlitedict.py new file mode 100644 index 00000000..93884a4e --- /dev/null +++ b/lib/sqlitedict.py @@ -0,0 +1,601 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# This code is distributed under the terms and conditions +# from the Apache License, Version 2.0 +# +# http://opensource.org/licenses/apache2.0.php +# +# This code was inspired by: +# * http://code.activestate.com/recipes/576638-draft-for-an-sqlite3-based-dbm/ +# * http://code.activestate.com/recipes/526618/ + +""" +A lightweight wrapper around Python's sqlite3 database, with a dict-like interface +and multi-thread access support:: + +>>> mydict = SqliteDict('some.db', autocommit=True) # the mapping will be persisted to file `some.db` +>>> mydict['some_key'] = any_picklable_object +>>> print mydict['some_key'] +>>> print len(mydict) # etc... all dict functions work + +Pickle is used internally to serialize the values. Keys are strings. + +If you don't use autocommit (default is no autocommit for performance), then +don't forget to call `mydict.commit()` when done with a transaction. + +""" + +import sqlite3 +import os +import sys +import tempfile +import logging +import time +import traceback + +from threading import Thread + +__version__ = '1.7.0.dev0' + +major_version = sys.version_info[0] +if major_version < 3: # py <= 2.x + if sys.version_info[1] < 5: # py <= 2.4 + raise ImportError("sqlitedict requires python 2.5 or higher (python 3.3 or higher supported)") + + # necessary to use exec()_ as this would be a SyntaxError in python3. + # this is an exact port of six.reraise(): + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + class TimeoutError(OSError): + pass + + exec_("def reraise(tp, value, tb=None):\n" + " raise tp, value, tb\n") +else: + def reraise(tp, value, tb=None): + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + +try: + from cPickle import dumps, loads, HIGHEST_PROTOCOL as PICKLE_PROTOCOL +except ImportError: + from pickle import dumps, loads, HIGHEST_PROTOCOL as PICKLE_PROTOCOL + +# some Python 3 vs 2 imports +try: + from collections import UserDict as DictClass +except ImportError: + from UserDict import DictMixin as DictClass + +try: + from queue import Queue +except ImportError: + from Queue import Queue + + +logger = logging.getLogger(__name__) + + +def open(*args, **kwargs): + """See documentation of the SqliteDict class.""" + return SqliteDict(*args, **kwargs) + + +def encode(obj): + """Serialize an object using pickle to a binary format accepted by SQLite.""" + return sqlite3.Binary(dumps(obj, protocol=PICKLE_PROTOCOL)) + + +def decode(obj): + """Deserialize objects retrieved from SQLite.""" + return loads(bytes(obj)) + + +class SqliteDict(DictClass): + VALID_FLAGS = ['c', 'r', 'w', 'n'] + + def __init__(self, filename=None, tablename='unnamed', flag='c', + autocommit=False, journal_mode="DELETE", encode=encode, decode=decode, timeout=5): + """ + Initialize a thread-safe sqlite-backed dictionary. The dictionary will + be a table `tablename` in database file `filename`. A single file (=database) + may contain multiple tables. + + If no `filename` is given, a random file in temp will be used (and deleted + from temp once the dict is closed/deleted). + + If you enable `autocommit`, changes will be committed after each operation + (more inefficient but safer). Otherwise, changes are committed on `self.commit()`, + `self.clear()` and `self.close()`. + + Set `journal_mode` to 'OFF' if you're experiencing sqlite I/O problems + or if you need performance and don't care about crash-consistency. + + The `flag` parameter. Exactly one of: + 'c': default mode, open for read/write, creating the db/table if necessary. + 'w': open for r/w, but drop `tablename` contents first (start with empty table) + 'r': open as read-only + 'n': create a new database (erasing any existing tables, not just `tablename`!). + + The `encode` and `decode` parameters are used to customize how the values + are serialized and deserialized. + The `encode` parameter must be a function that takes a single Python + object and returns a serialized representation. + The `decode` function must be a function that takes the serialized + representation produced by `encode` and returns a deserialized Python + object. + The default is to use pickle. + + The `timeout` defines the maximum time (in seconds) to wait for initial Thread startup. + + """ + self.in_temp = filename is None + if self.in_temp: + fd, filename = tempfile.mkstemp(prefix='sqldict') + os.close(fd) + + if flag not in SqliteDict.VALID_FLAGS: + raise RuntimeError("Unrecognized flag: %s" % flag) + self.flag = flag + + if flag == 'n': + if os.path.exists(filename): + os.remove(filename) + + dirname = os.path.dirname(filename) + if dirname: + if not os.path.exists(dirname): + raise RuntimeError('Error! The directory does not exist, %s' % dirname) + + self.filename = filename + + # Use standard SQL escaping of double quote characters in identifiers, by doubling them. + # See https://github.com/RaRe-Technologies/sqlitedict/pull/113 + self.tablename = tablename.replace('"', '""') + + self.autocommit = autocommit + self.journal_mode = journal_mode + self.encode = encode + self.decode = decode + self.timeout = timeout + + logger.info("opening Sqlite table %r in %r" % (tablename, filename)) + self.conn = self._new_conn() + if self.flag == 'r': + if self.tablename not in SqliteDict.get_tablenames(self.filename): + msg = 'Refusing to create a new table "%s" in read-only DB mode' % tablename + raise RuntimeError(msg) + else: + MAKE_TABLE = 'CREATE TABLE IF NOT EXISTS "%s" (key TEXT PRIMARY KEY, value BLOB)' % self.tablename + self.conn.execute(MAKE_TABLE) + self.conn.commit() + if flag == 'w': + self.clear() + + def _new_conn(self): + return SqliteMultithread(self.filename, autocommit=self.autocommit, journal_mode=self.journal_mode, + timeout=self.timeout) + + def __enter__(self): + if not hasattr(self, 'conn') or self.conn is None: + self.conn = self._new_conn() + return self + + def __exit__(self, *exc_info): + self.close() + + def __str__(self): + return "SqliteDict(%s)" % (self.filename) + + def __repr__(self): + return str(self) # no need of something complex + + def __len__(self): + # `select count (*)` is super slow in sqlite (does a linear scan!!) + # As a result, len() is very slow too once the table size grows beyond trivial. + # We could keep the total count of rows ourselves, by means of triggers, + # but that seems too complicated and would slow down normal operation + # (insert/delete etc). + GET_LEN = 'SELECT COUNT(*) FROM "%s"' % self.tablename + rows = self.conn.select_one(GET_LEN)[0] + return rows if rows is not None else 0 + + def __bool__(self): + # No elements is False, otherwise True + GET_MAX = 'SELECT MAX(ROWID) FROM "%s"' % self.tablename + m = self.conn.select_one(GET_MAX)[0] + # Explicit better than implicit and bla bla + return True if m is not None else False + + def iterkeys(self): + GET_KEYS = 'SELECT key FROM "%s" ORDER BY rowid' % self.tablename + for key in self.conn.select(GET_KEYS): + yield key[0] + + def itervalues(self): + GET_VALUES = 'SELECT value FROM "%s" ORDER BY rowid' % self.tablename + for value in self.conn.select(GET_VALUES): + yield self.decode(value[0]) + + def iteritems(self): + GET_ITEMS = 'SELECT key, value FROM "%s" ORDER BY rowid' % self.tablename + for key, value in self.conn.select(GET_ITEMS): + yield key, self.decode(value) + + def keys(self): + return self.iterkeys() if major_version > 2 else list(self.iterkeys()) + + def values(self): + return self.itervalues() if major_version > 2 else list(self.itervalues()) + + def items(self): + return self.iteritems() if major_version > 2 else list(self.iteritems()) + + def __contains__(self, key): + HAS_ITEM = 'SELECT 1 FROM "%s" WHERE key = ?' % self.tablename + return self.conn.select_one(HAS_ITEM, (key,)) is not None + + def __getitem__(self, key): + GET_ITEM = 'SELECT value FROM "%s" WHERE key = ?' % self.tablename + item = self.conn.select_one(GET_ITEM, (key,)) + if item is None: + raise KeyError(key) + return self.decode(item[0]) + + def __setitem__(self, key, value): + if self.flag == 'r': + raise RuntimeError('Refusing to write to read-only SqliteDict') + + ADD_ITEM = 'REPLACE INTO "%s" (key, value) VALUES (?,?)' % self.tablename + self.conn.execute(ADD_ITEM, (key, self.encode(value))) + if self.autocommit: + self.commit() + + def __delitem__(self, key): + if self.flag == 'r': + raise RuntimeError('Refusing to delete from read-only SqliteDict') + + if key not in self: + raise KeyError(key) + DEL_ITEM = 'DELETE FROM "%s" WHERE key = ?' % self.tablename + self.conn.execute(DEL_ITEM, (key,)) + if self.autocommit: + self.commit() + + def update(self, items=(), **kwds): + if self.flag == 'r': + raise RuntimeError('Refusing to update read-only SqliteDict') + + try: + items = items.items() + except AttributeError: + pass + items = [(k, self.encode(v)) for k, v in items] + + UPDATE_ITEMS = 'REPLACE INTO "%s" (key, value) VALUES (?, ?)' % self.tablename + self.conn.executemany(UPDATE_ITEMS, items) + if kwds: + self.update(kwds) + if self.autocommit: + self.commit() + + def __iter__(self): + return self.iterkeys() + + def clear(self): + if self.flag == 'r': + raise RuntimeError('Refusing to clear read-only SqliteDict') + + # avoid VACUUM, as it gives "OperationalError: database schema has changed" + CLEAR_ALL = 'DELETE FROM "%s";' % self.tablename + self.conn.commit() + self.conn.execute(CLEAR_ALL) + self.conn.commit() + + @staticmethod + def get_tablenames(filename): + """get the names of the tables in an sqlite db as a list""" + if not os.path.isfile(filename): + raise IOError('file %s does not exist' % (filename)) + GET_TABLENAMES = 'SELECT name FROM sqlite_master WHERE type="table"' + with sqlite3.connect(filename) as conn: + cursor = conn.execute(GET_TABLENAMES) + res = cursor.fetchall() + + return [name[0] for name in res] + + def commit(self, blocking=True): + """ + Persist all data to disk. + + When `blocking` is False, the commit command is queued, but the data is + not guaranteed persisted (default implication when autocommit=True). + """ + if self.conn is not None: + self.conn.commit(blocking) + sync = commit + + def close(self, do_log=True, force=False): + if do_log: + logger.debug("closing %s" % self) + if hasattr(self, 'conn') and self.conn is not None: + if self.conn.autocommit and not force: + # typically calls to commit are non-blocking when autocommit is + # used. However, we need to block on close() to ensure any + # awaiting exceptions are handled and that all data is + # persisted to disk before returning. + self.conn.commit(blocking=True) + self.conn.close(force=force) + self.conn = None + if self.in_temp: + try: + os.remove(self.filename) + except Exception: + pass + + def terminate(self): + """Delete the underlying database file. Use with care.""" + if self.flag == 'r': + raise RuntimeError('Refusing to terminate read-only SqliteDict') + + self.close() + + if self.filename == ':memory:': + return + + logger.info("deleting %s" % self.filename) + try: + if os.path.isfile(self.filename): + os.remove(self.filename) + except (OSError, IOError): + logger.exception("failed to delete %s" % (self.filename)) + + def __del__(self): + # like close(), but assume globals are gone by now (do not log!) + try: + self.close(do_log=False, force=True) + except Exception: + # prevent error log flood in case of multiple SqliteDicts + # closed after connection lost (exceptions are always ignored + # in __del__ method. + pass + + +# Adding extra methods for python 2 compatibility (at import time) +if major_version == 2: + SqliteDict.__nonzero__ = SqliteDict.__bool__ + del SqliteDict.__bool__ # not needed and confusing + + +class SqliteMultithread(Thread): + """ + Wrap sqlite connection in a way that allows concurrent requests from multiple threads. + + This is done by internally queueing the requests and processing them sequentially + in a separate thread (in the same order they arrived). + + """ + def __init__(self, filename, autocommit, journal_mode, timeout): + super(SqliteMultithread, self).__init__() + self.filename = filename + self.autocommit = autocommit + self.journal_mode = journal_mode + # use request queue of unlimited size + self.reqs = Queue() + self.setDaemon(True) # python2.5-compatible + self.exception = None + self._sqlitedict_thread_initialized = None + self.timeout = timeout + self.log = logging.getLogger('sqlitedict.SqliteMultithread') + self.start() + + def run(self): + try: + if self.autocommit: + conn = sqlite3.connect(self.filename, isolation_level=None, check_same_thread=False) + else: + conn = sqlite3.connect(self.filename, check_same_thread=False) + except Exception: + self.log.exception("Failed to initialize connection for filename: %s" % self.filename) + self.exception = sys.exc_info() + raise + + try: + conn.execute('PRAGMA journal_mode = %s' % self.journal_mode) + conn.text_factory = str + cursor = conn.cursor() + conn.commit() + cursor.execute('PRAGMA synchronous=OFF') + except Exception: + self.log.exception("Failed to execute PRAGMA statements.") + self.exception = sys.exc_info() + raise + + self._sqlitedict_thread_initialized = True + + res = None + while True: + req, arg, res, outer_stack = self.reqs.get() + if req == '--close--': + assert res, ('--close-- without return queue', res) + break + elif req == '--commit--': + conn.commit() + if res: + res.put('--no more--') + else: + try: + cursor.execute(req, arg) + except Exception: + self.exception = (e_type, e_value, e_tb) = sys.exc_info() + inner_stack = traceback.extract_stack() + + # An exception occurred in our thread, but we may not + # immediately able to throw it in our calling thread, if it has + # no return `res` queue: log as level ERROR both the inner and + # outer exception immediately. + # + # Any iteration of res.get() or any next call will detect the + # inner exception and re-raise it in the calling Thread; though + # it may be confusing to see an exception for an unrelated + # statement, an ERROR log statement from the 'sqlitedict.*' + # namespace contains the original outer stack location. + self.log.error('Inner exception:') + for item in traceback.format_list(inner_stack): + self.log.error(item) + self.log.error('') # deliniate traceback & exception w/blank line + for item in traceback.format_exception_only(e_type, e_value): + self.log.error(item) + + self.log.error('') # exception & outer stack w/blank line + self.log.error('Outer stack:') + for item in traceback.format_list(outer_stack): + self.log.error(item) + self.log.error('Exception will be re-raised at next call.') + + if res: + for rec in cursor: + res.put(rec) + res.put('--no more--') + + if self.autocommit: + conn.commit() + + self.log.debug('received: %s, send: --no more--', req) + conn.close() + res.put('--no more--') + + def check_raise_error(self): + """ + Check for and raise exception for any previous sqlite query. + + For the `execute*` family of method calls, such calls are non-blocking and any + exception raised in the thread cannot be handled by the calling Thread (usually + MainThread). This method is called on `close`, and prior to any subsequent + calls to the `execute*` methods to check for and raise an exception in a + previous call to the MainThread. + """ + if self.exception: + e_type, e_value, e_tb = self.exception + + # clear self.exception, if the caller decides to handle such + # exception, we should not repeatedly re-raise it. + self.exception = None + + self.log.error('An exception occurred from a previous statement, view ' + 'the logging namespace "sqlitedict" for outer stack.') + + # The third argument to raise is the traceback object, and it is + # substituted instead of the current location as the place where + # the exception occurred, this is so that when using debuggers such + # as `pdb', or simply evaluating the naturally raised traceback, we + # retain the original (inner) location of where the exception + # occurred. + reraise(e_type, e_value, e_tb) + + def execute(self, req, arg=None, res=None): + """ + `execute` calls are non-blocking: just queue up the request and return immediately. + """ + self._wait_for_initialization() + self.check_raise_error() + + # NOTE: This might be a lot of information to pump into an input + # queue, affecting performance. I've also seen earlier versions of + # jython take a severe performance impact for throwing exceptions + # so often. + stack = traceback.extract_stack()[:-1] + self.reqs.put((req, arg or tuple(), res, stack)) + + def executemany(self, req, items): + for item in items: + self.execute(req, item) + self.check_raise_error() + + def select(self, req, arg=None): + """ + Unlike sqlite's native select, this select doesn't handle iteration efficiently. + + The result of `select` starts filling up with values as soon as the + request is dequeued, and although you can iterate over the result normally + (`for res in self.select(): ...`), the entire result will be in memory. + """ + res = Queue() # results of the select will appear as items in this queue + self.execute(req, arg, res) + while True: + rec = res.get() + self.check_raise_error() + if rec == '--no more--': + break + yield rec + + def select_one(self, req, arg=None): + """Return only the first row of the SELECT, or None if there are no matching rows.""" + try: + return next(iter(self.select(req, arg))) + except StopIteration: + return None + + def commit(self, blocking=True): + if blocking: + # by default, we await completion of commit() unless + # blocking=False. This ensures any available exceptions for any + # previous statement are thrown before returning, and that the + # data has actually persisted to disk! + self.select_one('--commit--') + else: + # otherwise, we fire and forget as usual. + self.execute('--commit--') + + def close(self, force=False): + if force: + # If a SqliteDict is being killed or garbage-collected, then select_one() + # could hang forever because run() might already have exited and therefore + # can't process the request. Instead, push the close command to the requests + # queue directly. If run() is still alive, it will exit gracefully. If not, + # then there's nothing we can do anyway. + self.reqs.put(('--close--', None, Queue(), None)) + else: + # we abuse 'select' to "iter" over a "--close--" statement so that we + # can confirm the completion of close before joining the thread and + # returning (by semaphore '--no more--' + self.select_one('--close--') + self.join() + + def _wait_for_initialization(self): + """ + Polls the 'initialized' flag to be set by the started Thread in run(). + """ + # A race condition may occur without waiting for initialization: + # __init__() finishes with the start() call, but the Thread needs some time to actually start working. + # If opening the database file fails in run(), an exception will occur and self.exception will be set. + # But if we run check_raise_error() before run() had a chance to set self.exception, it will report + # a false negative: An exception occured and the thread terminates but self.exception is unset. + # This leads to a deadlock while waiting for the results of execute(). + # By waiting for the Thread to set the initialized flag, we can ensure the thread has successfully + # opened the file - and possibly set self.exception to be detected by check_raise_error(). + + start_time = time.time() + while time.time() - start_time < self.timeout: + if self._sqlitedict_thread_initialized or self.exception: + return + time.sleep(0.1) + raise TimeoutError("SqliteMultithread failed to flag initialization withing %0.0f seconds." % self.timeout) + + +if __name__ == '__main__': + print(__version__) \ No newline at end of file diff --git a/platformcode/autorenumber.py b/platformcode/autorenumber.py index fe1b18a8..2f32c261 100644 --- a/platformcode/autorenumber.py +++ b/platformcode/autorenumber.py @@ -5,23 +5,22 @@ import xbmc, xbmcgui, re, base64, inspect, sys -from core import jsontools, tvdb, scrapertools, filetools +from core import jsontools, tmdb, scrapertools, filetools from core.item import Item from core.support import typo, match, dbg, Item from platformcode import config, platformtools, logger PY3 = True if sys.version_info[0] >= 3 else False # Json Var -TVSHOW_RENUMERATE = "TVSHOW_AUTORENUMBER" -ID = "ID" -SEASON = "Season" -EPISODE = "Episode" -SPECIAL = "Special" -MODE = "Mode" -EPLIST = "EpList" -CHECK = "ReCheck" -SPLIST = "SpList" -TYPE = "Type" +RENUMBER = 'TVSHOW_AUTORENUMBER' +ID = 'id' +SEASONSDICT = 'seasons' +SEASON = 'season' +EPISODE = 'episode' +EPISODES = 'episodes' +SPECIALEPISODES = 'specials' +MANUALMODE = 'manual' +GROUP = 'info' # helper Functions def check(item): @@ -41,21 +40,15 @@ def filename(item): def load(item): logger.debug() - try: - json_file = open(filename(item), "r").read() - json = jsontools.load(json_file)[TVSHOW_RENUMERATE] - - except: - json = {} - + try: json = jsontools.load(open(filename(item), "r").read())[RENUMBER] + except: json = {} return json def write(item, json): logger.debug() - json_file = open(filename(item), "r").read() - js = jsontools.load(json_file) - js[TVSHOW_RENUMERATE] = json + js = jsontools.load(open(filename(item), "r").read()) + js[RENUMBER] = json with open(filename(item), "w") as file: file.write(jsontools.dump(js)) file.close() @@ -69,15 +62,6 @@ def b64(json, mode = 'encode'): ret = jsontools.load(base64.b64decode(json)) return ret -def RepresentsInt(s): - # Controllo Numro Stagione - logger.debug() - try: - int(s) - return True - except ValueError: - return False - def find_episodes(item): logger.debug() ch = __import__('channels.' + item.channel, fromlist=["channels.%s" % item.channel]) @@ -88,6 +72,15 @@ def busy(state): if state: xbmc.executebuiltin('ActivateWindow(busydialognocancel)') else: xbmc.executebuiltin('Dialog.Close(busydialognocancel)') +def RepresentsInt(s): + # Controllo Numro Stagione + logger.debug() + try: + int(s) + return True + except ValueError: + return False + # Main def start(itemlist, item=None): if not itemlist: return @@ -106,48 +99,44 @@ class autorenumber(): def __init__(self, itemlist, item=None): self.item = item self.itemlist = itemlist + self.renumberdict = load(self.itemlist[0]) if self.itemlist else load(item) if item else {} + self.selectspecials = False + self.manual = False self.auto = False - self.dictSeries = load(self.itemlist[0]) if self.itemlist else load(item) if item else {} - self.Episodes = {} - self.sp = False if self.item: self.auto = config.get_setting('autorenumber', item.channel) self.title = self.item.fulltitle.strip() if match(self.itemlist[0].title, patron=r'[Ss]?(\d+)(?:x|_|\s+)[Ee]?[Pp]?(\d+)').match: item.exit = True return - elif self.item.channel in self.item.channel_prefs and TVSHOW_RENUMERATE in self.item.channel_prefs[item.channel] and self.title not in self.dictSeries: + elif self.item.channel in self.item.channel_prefs and RENUMBER in self.item.channel_prefs[item.channel] and self.title not in self.renumberdict: from core.videolibrarytools import check_renumber_options from specials.videolibrary import update_videolibrary check_renumber_options(self.item) update_videolibrary(self.item) - if self.title in self.dictSeries and ID in self.dictSeries[self.title] and self.dictSeries[self.title][ID] != '0': - self.id = self.dictSeries[self.title][ID] - self.Episodes = b64(self.dictSeries[self.title][EPISODE], 'decode') if EPISODE in self.dictSeries[self.title] else {} - self.Season = self.dictSeries[self.title][SEASON] - self.Mode = self.dictSeries[self.title].get(MODE, False) - self.Type = self.dictSeries[self.title].get(TYPE, False) - if self.item.renumber: - self.config() - else: - self.renumber() + self.series = self.renumberdict.get(self.title,{}) + self.id = self.series.get(ID, 0) + self.episodes = self.series.get(EPISODES,{}) + self.seasonsdict = self.series.get(SEASONSDICT,{}) + self.season = self.series.get(SEASON, -1) + self.episode = self.series.get(EPISODE, -1) + self.manual = self.series.get(MANUALMODE, False) + self.specials = self.series.get(SPECIALEPISODES, {}) + if self.id and self.episodes and self.season >= 0 and self.episode >= 0: + if self.item.renumber: self.config() + else:self.renumber() elif self.auto or self.item.renumber: - self.Episodes = {} + self.episodes = {} self.config() - else: for item in self.itemlist: item.context = [{"title": typo(config.get_localized_string(70585), 'bold'), - "action": "start", - "channel": "autorenumber", - "from_channel": item.channel, - "from_action": item.action}] + "action": "start", + "channel": "autorenumber", + "from_channel": item.channel, + "from_action": item.action}] def config(self): - self.id = '' - if self.title in self.dictSeries: - self.id = self.dictSeries[self.title].get(ID,'') - # Pulizia del Titolo if any( word in self.title.lower() for word in ['specials', 'speciali']): self.title = re.sub(r'\s*specials|\s*speciali', '', self.title.lower()) @@ -155,184 +144,149 @@ class autorenumber(): self.item.contentSerieName = self.title.rstrip('123456789 ') while not self.item.exit: - tvdb.find_and_set_infoLabels(self.item) - if self.item.infoLabels['tvdb_id']: self.item.exit = True - else: self.item = platformtools.dialog_info(self.item, 'tvdb') + self.item.infoLabels['tmdb_id'] = '' + self.item.infoLabels['year'] = '-' + self.item.contentType ='tvshow' + tmdb.find_and_set_infoLabels(self.item) + if self.item.infoLabels['tmdb_id']: self.item.exit = True + else: self.item = platformtools.dialog_info(self.item, 'tmdb') # Rinumerazione Automatica if (not self.id and self.auto) or self.item.renumber: - self.id = self.item.infoLabels['tvdb_id'] if 'tvdb_id' in self.item.infoLabels else '' + self.id = self.item.infoLabels['tmdb_id'] if 'tmdb_id' in self.item.infoLabels else 0 if self.id: - self.dictRenumber = {ID: self.id} - self.dictSeries[self.title] = self.dictRenumber - if any(word in self.title.lower() for word in ['specials', 'speciali']): season = '0' - elif RepresentsInt(self.title.split()[-1]): season = self.title.split()[-1] - else: season = '1' - self.Season = self.dictRenumber[SEASON] = season + self.series = {ID: self.id} + self.renumberdict[self.title] = self.series + if any(word in self.title.lower() for word in ['specials', 'speciali']): season = 0 + elif RepresentsInt(self.title.split()[-1]): season = int(self.title.split()[-1]) + else: season = 1 + self.season = self.series[SEASON] = season + self.episode = 1 self.renumber() + def renumber(self): if not self.item.renumber and self.itemlist: - if '|' in self.Season: - season = int(self.Season.split('|')[0]) - addNumber = int(self.Season.split('|')[-1]) - 1 - else: - season = int(self.Season) - addNumber = 0 for item in self.itemlist: if not match(item.title, patron=r'[Ss]?(\d+)(?:x|_|\s+)[Ee]?[Pp]?(\d+)').match: number = match(item.title, patron=r'(\d+)').match.lstrip('0') if number: - if number in self.Episodes: - if season > 0: item.title = typo(self.Episodes[number] + ' - ', 'bold') + item.title - else: item.title = typo('0x%s - ' % str(int(number) + addNumber), 'bold') + item.title - else: - self.makelist() - if season > 0: item.title = typo(self.Episodes[number] + ' - ', 'bold') + item.title - else: item.title = typo('0x%s - ' % str(int(number) + addNumber), 'bold') + item.title + if not number in self.episodes: self.makelist() + item.title = '{} - {}'.format(typo(self.episodes[number], 'bold'), item.title) else: self.makelist() def makelist(self): - FirstOfSeason= 0 - self.EpList = b64(self.dictSeries[self.title][EPLIST], 'decode') if EPLIST in self.dictSeries[self.title] else [] - self.Pages = self.dictSeries[self.title].get(CHECK, [1]) - self.Mode = self.dictSeries[self.title].get(MODE, False) - self.Type = self.dictSeries[self.title].get(TYPE, False) - Specials = {} - Seasons = {} - - if '|' in self.Season: - ep = int(self.Season.split('|')[-1]) - Season = int(self.Season.split('|')[0]) - else: - Season = int(self.Season) - ep = 1 - + self.epdict = {} + self.group = self.renumberdict[self.title].get(GROUP, None) busy(True) itemlist = find_episodes(self.item) busy(False) - if self.item.renumber: - self.s = Season - self.e = 1 - Season, Episode, self.Mode, Specials, Seasons, Exit = SelectreNumeration(self, itemlist) - if Exit: return - if ep != 1: self.Season = '%s|%s' % (Season, Episode) - else: self.Season = str(Season) - - elif self.Episodes and not self.Mode: - self.s = Season - self.e = ep - self.sp = True - Season, Episode, self.Mode, Specials, Seasons, Exit = SelectreNumeration(self, itemlist) - - if self.Mode: - if not Seasons: - self.s = 1 - self.e = 1 - Season, Episode, self.Mode, Specials, Seasons, Exit = SelectreNumeration(self, itemlist, True) - self.Episodes = Seasons + if self.item.renumber or self.manual: + self.item.renumber = False + self.season, self.episode, self.manual, self.specials, Manual, Exit = SelectreNumeration(self, itemlist) + if Exit: + self.item.exit = True + return + if self.manual: + self.episodes = Manual else: - # Ricava Informazioni da TVDB - checkpages = [] - exist = True - Page = self.Pages[-1] - Episode = ep + if self.group: + Id = self.group.split('/')[-1] + else: + Id = None + groups = tmdb.get_groups(self.item) + if groups: + Id = tmdb.select_group(groups) + + if Id: + self.group = 'https://www.themoviedb.org/tv/{}/episode_group/{}'.format(self.item.infoLabels['tmdb_id'], Id) + seasons = [] + groupedSeasons = tmdb.get_group(Id) + for groupedSeason in groupedSeasons: + seasons.append({'season_number':groupedSeason['order'], 'episode_count':len(groupedSeason['episodes'])}) + else: + seasons = tmdb.Tmdb(id_Tmdb=self.id).get_list_episodes() + count = 0 + for season in seasons: + s = season['season_number'] + c = season['episode_count'] + self.seasonsdict[str(s)] = c + if s > 0: + for e in range(1, c + 1): + count += 1 + self.epdict[count] = '{}x{:02d}'.format(s,e) + + firstep = 0 + if self.season > 1: + for c in range(1, self.season): + firstep += self.seasonsdict[str(c)] + firstep += self.episode - 1 + count = 0 + if self.epdict: + for item in itemlist: + if not match(re.sub(r'\[[^\]]+\]','',item.title), patron=r'[Ss]?(\d+)(?:x|_|\s+)[Ee]?[Pp]?(\d+)').match: + # Otiene Numerazione Episodi + scraped_ep = match(re.sub(r'\[[^\]]+\]','',item.title), patron=r'(\d+)').match + if scraped_ep: + episode = int(scraped_ep) + if episode == 0: + self.episodes[str(episode)] = '0x01' + elif str(episode) in self.specials: + self.episodes[str(episode)] = self.specials[str(episode)] + count += 1 + elif episode - count + firstep in self.epdict: + self.episodes[str(episode)] = self.epdict[episode - count + firstep] + else: + self.episodes[str(episode)] = '0x{:02d}'.format(count + 1) + count += 1 + + if self.episodes: self.renumberdict[self.title][EPISODES] = self.episodes + if self.group: self.renumberdict[self.title][GROUP] = self.group + self.renumberdict[self.title][MANUALMODE] = self.manual + self.renumberdict[self.title][SEASON] = self.season + self.renumberdict[self.title][EPISODE] = self.episode + self.renumberdict[self.title][SPECIALEPISODES] = self.specials + self.renumberdict[self.title][SEASONSDICT] = self.seasonsdict + write(self.item, self.renumberdict) + # if self.auto: self.renumber() - while exist: - data = tvdb.Tvdb(tvdb_id=self.id).get_list_episodes(self.id, Page) - if data: - for episode in data['data']: - if episode['firstAired'] and [episode['firstAired'], episode['airedSeason'], episode['airedEpisodeNumber']] not in self.EpList: - self.EpList.append([episode['firstAired'], episode['airedSeason'], episode['airedEpisodeNumber']]) - Page += 1 - else: - if Page not in checkpages: - checkpages.append(Page -1) - exist = False - self.Pages = [checkpages[-1]] - self.EpList.sort() - # Crea Dizionari per la Rinumerazione - if self.EpList: - self.specials = [] - self.regular = {} - self.complete = {} - allep = 1 - specialep = 0 - for episode in self.EpList: - self.complete[allep] = [str(episode[1]) + 'x' + str(episode[2]), episode[0]] - if episode[1] == 0: - self.specials.append(allep) - specialep = specialep + 1 - else: - self.regular[ep] = [str(episode[1]) + 'x' + str(episode[2]), str(episode[0]), allep - 1] - ep = ep + 1 - allep = allep + 1 - if Season > 1: - for numbers, data in self.regular.items(): - if data[0] == str(Season) + 'x1': - FirstOfSeason = numbers - 1 - else: FirstOfSeason = Episode - 1 - addiction = 0 - for item in itemlist: - if not match(re.sub(r'\[[^\]]+\]','',item.title), patron=r'[Ss]?(\d+)(?:x|_|\s+)[Ee]?[Pp]?(\d+)').match: - # Otiene Numerazione Episodi - scraped_ep = match(re.sub(r'\[[^\]]+\]','',item.title), patron=r'(\d+)').match - if scraped_ep: - episode = int(scraped_ep) - number = episode + FirstOfSeason - addiction - if episode == 0: - self.Episodes[str(episode)] = str(self.complete[self.regular[FirstOfSeason+1][2]][0]) - elif episode in Specials: - self.Episodes[str(episode)] = Specials[episode] - addiction += 1 - elif number <= len(self.regular) and number in self.regular: - self.Episodes[str(episode)] = str(self.regular[number][0]) - else: - try: self.Episodes[str(episode)] = str(self.complete[self.regular[number+2][2]][0]) - except: self.Episodes[str(episode)] = '0x0' - if self.Episodes: self.dictSeries[self.title][EPISODE] = b64(jsontools.dump(self.Episodes)) - self.dictSeries[self.title][EPLIST] = b64(jsontools.dump(self.EpList)) - self.dictSeries[self.title][MODE] = self.Mode - self.dictSeries[self.title][SEASON] = self.Season - self.dictSeries[self.title][CHECK] = self.Pages - write(self.item, self.dictSeries) - if self.auto: self.renumber() def SelectreNumeration(opt, itemlist, manual=False): class SelectreNumerationWindow(xbmcgui.WindowXMLDialog): def start(self, opt): - self.episodes = opt.Episodes if opt.Episodes else {} - self.dictSeries = opt.dictSeries + self.episodes = opt.episodes if opt.episodes else {} + self.renumberdict = opt.renumberdict self.item = opt.item self.title = opt.title - self.season = opt.s - self.episode = opt.e - self.mode = opt.Mode - self.sp = opt.sp + self.season = opt.season + self.episode = opt.episode + self.manual = opt.manual + self.sp = opt.selectspecials self.manual = opt.manual self.offset = 0 self.Exit = False self.itemlist = opt.itemlist self.count = 1 - self.specials = {} + self.specials = opt.specials self.items = [] self.selected = [] self.seasons = {} + self.seasonsdict = opt.seasonsdict self.doModal() - return self.season, self.episode, self.mode, self.specials, self.seasons, self.Exit + return self.season, self.episode, self.manual, self.specials, self.seasons, self.Exit def onInit(self): # Compatibility with Kodi 18 @@ -349,7 +303,7 @@ def SelectreNumeration(opt, itemlist, manual=False): if fanart: self.getControl(MBACKGROUND).setImage(fanart) self.getControl(INFO).setLabel(typo(config.get_localized_string(70822) + self.title, 'bold')) - self.mode = True + self.manual = True se = '1' ep = '1' @@ -434,17 +388,19 @@ def SelectreNumeration(opt, itemlist, manual=False): self.setFocusId(focus - 1) elif action in [UP]: if focus in [S]: - s += 1 - self.getControl(S).setLabel(str(s)) + if str(s + 1) in self.seasonsdict: + s += 1 + self.getControl(S).setLabel(str(s)) elif focus in [E]: - e += 1 - self.getControl(E).setLabel(str(e)) + if self.seasonsdict[str(s)] > e: + e += 1 + self.getControl(E).setLabel(str(e)) elif action in [DOWN]: if focus in [S]: - if s > 0: s -= 1 + if str(s - 1) in self.seasonsdict: s -= 1 self.getControl(S).setLabel(str(s)) elif focus in [E]: - if e > 0: e -= 1 + if e > 1: e -= 1 self.getControl(E).setLabel(str(e)) # MANUAL if focus in [MS, ME]: @@ -508,7 +464,7 @@ def SelectreNumeration(opt, itemlist, manual=False): # OPEN MANUAL elif control_id in [M]: self.getControl(INFO).setLabel(typo(config.get_localized_string(70823) + self.title, 'bold')) - self.mode = True + self.manual = True if self.episodes: items = [] se = '1' @@ -539,8 +495,8 @@ def SelectreNumeration(opt, itemlist, manual=False): # DELETE if control_id in [D]: self.Exit = True - self.dictSeries.pop(self.title) - write(self.item, self.dictSeries) + self.renumberdict.pop(self.title) + write(self.item, self.renumberdict) self.close() ## SPECIAL SECTION @@ -548,7 +504,7 @@ def SelectreNumeration(opt, itemlist, manual=False): p1 = self.getControl(SELECTED).getSelectedPosition() if control_id in [LIST]: item = self.getControl(LIST).getSelectedItem() - it = xbmcgui.ListItem(str(len(self.selected) + 1)) + it = xbmcgui.ListItem(str(len(self.selected) + len(self.specials) + 1)) it.setProperty('title', item.getLabel()) self.selected.append(it) index = self.getControl(SELECTED).getSelectedPosition() @@ -754,4 +710,4 @@ DOWN = 4 EXIT = 10 BACKSPACE = 92 -path = config.get_runtime_path() +path = config.get_runtime_path() \ No newline at end of file diff --git a/platformcode/launcher.py b/platformcode/launcher.py index 022cea2d..989e2d77 100644 --- a/platformcode/launcher.py +++ b/platformcode/launcher.py @@ -41,7 +41,12 @@ def run(item=None): logger.debug() if not item: # Extract item from sys.argv - if sys.argv[2]: + if sys.argv[2] and 'play' in sys.argv[2]: + sp = sys.argv[2].split('/') + if len(sp) == 3: + item = Item(channel=sp[1], livefilter=sp[2], action='play') + # If no item, this is mainlist + elif sys.argv[2]: sp = sys.argv[2].split('&') url = sp[0] item = Item().fromurl(url) @@ -49,7 +54,7 @@ def run(item=None): for e in sp[1:]: key, val = e.split('=') item.__setattr__(key, val) - # If no item, this is mainlist + else: item = Item(channel="channelselector", action="getmainlist", viewmode="movie") if not config.get_setting('show_once'): @@ -337,6 +342,10 @@ def run(item=None): else: if platformtools.dialog_yesno(config.get_localized_string(60038), config.get_localized_string(60015)): run(Item(channel="setting", action="report_menu")) + finally: + if not item.action.startswith('play'): + from core import db + db.close() def new_search(item, channel=None): @@ -469,6 +478,7 @@ def play_from_library(item): else: # Pop-up window from specials import videolibrary + from core.channeltools import get_channel_parameters p_dialog = platformtools.dialog_progress_bg(config.get_localized_string(20000), config.get_localized_string(60683)) p_dialog.update(0, '') item.play_from = 'window' @@ -499,9 +509,10 @@ def play_from_library(item): quality = '[B][' + item.quality + '][/B]' if item.quality else '' if item.server: path = filetools.join(config.get_runtime_path(), 'servers', item.server.lower() + '.json') - name = jsontools.load(open(path, "r").read())['name'] + name = jsontools.load(open(path, "rb").read())['name'] if name.startswith('@'): name = config.get_localized_string(int(name.replace('@',''))) - it = xbmcgui.ListItem('\n[B]%s[/B] %s - %s' % (name, quality, item.contentTitle)) + logger.debug(item) + it = xbmcgui.ListItem('\n[B]%s[/B] %s - %s [%s]' % (name, quality, item.contentTitle, get_channel_parameters(item.contentChannel)['title'])) it.setArt({'thumb':item.thumbnail}) options.append(it) else: diff --git a/platformcode/platformtools.py b/platformcode/platformtools.py index 9252f179..2d4c84e0 100644 --- a/platformcode/platformtools.py +++ b/platformcode/platformtools.py @@ -226,6 +226,45 @@ def dialog_info(item, scraper): dialog = TitleOrIDWindow('TitleOrIDWindow.xml', config.get_runtime_path()).Start(item, scraper) return dialog +def dialog_select_group(heading, _list, preselect=0): + class SelectGroup(xbmcgui.WindowXMLDialog): + def start(self, heading, _list, preselect): + self.selected = preselect + self.heading = heading + self.list = _list + self.doModal() + + return self.selected + + def onInit(self): + self.getControl(1).setText(self.heading) + itemlist = [] + for n, text in enumerate(self.list): + item = xbmcgui.ListItem(str(n)) + item.setProperty('title', text) + itemlist.append(item) + + self.getControl(2).addItems(itemlist) + self.setFocusId(2) + self.getControl(2).selectItem(self.selected) + + def onClick(self, control): + if control in [100]: + self.selected = -1 + self.close() + elif control in [2]: + self.selected = self.getControl(2).getSelectedPosition() + self.close() + + def onAction(self, action): + action = action.getId() + if action in [10, 92]: + self.selected = -1 + self.close() + + dialog = SelectGroup('SelectGroup.xml', config.get_runtime_path()).start(heading, _list, preselect) + return dialog + def itemlist_refresh(): # pos = Item().fromurl(xbmc.getInfoLabel('ListItem.FileNameAndPath')).itemlistPosition @@ -616,63 +655,71 @@ def is_playing(): def play_video(item, strm=False, force_direct=False, autoplay=False): logger.debug() logger.debug(item.tostring('\n')) - if item.channel == 'downloads': - logger.debug("Play local video: %s [%s]" % (item.title, item.url)) - xlistitem = xbmcgui.ListItem(path=item.url) - xlistitem.setArt({"thumb": item.thumbnail}) - set_infolabels(xlistitem, item, True) - set_player(item, xlistitem, item.url, True, None) # Fix Play From Download Section - return - default_action = config.get_setting("default_action") - logger.debug("default_action=%s" % default_action) - - # pass referer - if item.referer: - from core import httptools - httptools.default_headers['Referer'] = item.referer - - # Open the selection dialog to see the available options - opciones, video_urls, seleccion, salir = get_dialogo_opciones(item, default_action, strm, autoplay) - if salir: exit() - - # get default option of addon configuration - seleccion = get_seleccion(default_action, opciones, seleccion, video_urls) - if seleccion < 0: exit() # Canceled box - - logger.debug("selection=%d" % seleccion) - logger.debug("selection=%s" % opciones[seleccion]) - - # run the available option, jdwonloader, download, favorites, add to the video library ... IF IT IS NOT PLAY - salir = set_opcion(item, seleccion, opciones, video_urls) - if salir: - return - - # we get the selected video - mediaurl, view, mpd = get_video_seleccionado(item, seleccion, video_urls, autoplay) - if not mediaurl: return - - # video information is obtained. - xlistitem = xbmcgui.ListItem(path=item.url) - xlistitem.setArt({"thumb": item.contentThumbnail if item.contentThumbnail else item.thumbnail}) - set_infolabels(xlistitem, item, True) - - # if it is a video in mpd format, the listitem is configured to play it ith the inpustreamaddon addon implemented in Kodi 17 - # from core.support import dbg;dbg() - if mpd: - if not install_inputstream(): + def play(): + if item.channel == 'downloads': + logger.debug("Play local video: %s [%s]" % (item.title, item.url)) + xlistitem = xbmcgui.ListItem(path=item.url) + xlistitem.setArt({"thumb": item.thumbnail}) + set_infolabels(xlistitem, item, True) + set_player(item, xlistitem, item.url, True, None) # Fix Play From Download Section return - xlistitem.setProperty('inputstream' if PY3 else 'inputstreamaddon', 'inputstream.adaptive') - xlistitem.setProperty('inputstream.adaptive.manifest_type', 'mpd') - if item.drm and item.license: - install_widevine() - xlistitem.setProperty("inputstream.adaptive.license_type", item.drm) - xlistitem.setProperty("inputstream.adaptive.license_key", item.license) - xlistitem.setMimeType('application/dash+xml') - if force_direct: item.play_from = 'window' + default_action = config.get_setting("default_action") + logger.debug("default_action=%s" % default_action) - set_player(item, xlistitem, mediaurl, view, strm) + # pass referer + if item.referer: + from core import httptools + httptools.default_headers['Referer'] = item.referer + + # Open the selection dialog to see the available options + opciones, video_urls, seleccion, salir = get_dialogo_opciones(item, default_action, strm, autoplay) + if salir: return + + # get default option of addon configuration + seleccion = get_seleccion(default_action, opciones, seleccion, video_urls) + if seleccion < 0: return # Canceled box + + logger.debug("selection=%d" % seleccion) + logger.debug("selection=%s" % opciones[seleccion]) + + # run the available option, jdwonloader, download, favorites, add to the video library ... IF IT IS NOT PLAY + salir = set_opcion(item, seleccion, opciones, video_urls) + if salir: + return + + # we get the selected video + mediaurl, view, mpd = get_video_seleccionado(item, seleccion, video_urls, autoplay) + if not mediaurl: return + + # video information is obtained. + xlistitem = xbmcgui.ListItem(path=item.url) + xlistitem.setArt({"thumb": item.contentThumbnail if item.contentThumbnail else item.thumbnail}) + set_infolabels(xlistitem, item, True) + + # if it is a video in mpd format, the listitem is configured to play it ith the inpustreamaddon addon implemented in Kodi 17 + # from core.support import dbg;dbg() + if mpd: + if not install_inputstream(): + return + xlistitem.setProperty('inputstream' if PY3 else 'inputstreamaddon', 'inputstream.adaptive') + xlistitem.setProperty('inputstream.adaptive.manifest_type', 'mpd') + if item.drm and item.license: + install_widevine() + xlistitem.setProperty("inputstream.adaptive.license_type", item.drm) + xlistitem.setProperty("inputstream.adaptive.license_key", item.license) + xlistitem.setMimeType('application/dash+xml') + + if force_direct: item.play_from = 'window' + + set_player(item, xlistitem, mediaurl, view, strm) + return True + + if not play(): + # close db to ensure his thread will stop + from core import db + db.close() def stop_video(): @@ -1370,71 +1417,60 @@ def get_platform(): def get_played_time(item): - import sqlite3 - from core import filetools - db_name = filetools.join(config.get_data_path(), "kod_db.sqlite") - ID = item.infoLabels['tmdb_id'] - conn = sqlite3.connect(db_name, timeout=15) - c = conn.cursor() - c.execute('CREATE TABLE IF NOT EXISTS viewed (tmdb_id TEXT, season INT, episode INT, played_time REAL)') - conn.commit() - if ID: - if item.contentType == 'movie': c.execute("SELECT played_time FROM viewed WHERE tmdb_id=?", (ID,)) - elif 'season' in item.infoLabels: - S = item.infoLabels['season'] - E = item.infoLabels['episode'] - c.execute("SELECT played_time FROM viewed WHERE tmdb_id=? AND season=? AND episode=?", (ID, S, E)) - elif 'episode' in item.infoLabels: - E = item.infoLabels['episode'] - c.execute("SELECT played_time FROM viewed WHERE tmdb_id=? AND episode=?", (ID, E)) - result = c.fetchone() - if not result: played_time = 0 - else: played_time = result[0] - else: played_time = 0 - conn.close() + logger.debug() + from core import db + + if not item.infoLabels: + return 0 + ID = item.infoLabels.get('tmdb_id', '') + if not ID: + return 0 + + S = item.infoLabels.get('season', 0) + E = item.infoLabels.get('episode') + result = None + + if item.contentType == 'movie': + result = db['viewed'].get(ID) + elif S and E: + result = db['viewed'].get(ID, {}).get(str(S)+'x'+str(E)) + + if not result: played_time = 0 + else: played_time = result + return played_time + def set_played_time(item): - import sqlite3 - from core import filetools - ID = item.infoLabels['tmdb_id'] + logger.debug() + from core import db + played_time = item.played_time - db_name = filetools.join(config.get_data_path(), "kod_db.sqlite") - conn = sqlite3.connect(db_name, timeout=15) - c = conn.cursor() + if not item.infoLabels: + return + + ID = item.infoLabels.get('tmdb_id', '') + if not ID: + return + + S = item.infoLabels.get('season', 0) + E = item.infoLabels.get('episode') + + if item.contentType == 'movie': - c.execute("SELECT played_time FROM viewed WHERE tmdb_id=?", (ID,)) - result = c.fetchone() - if result: - if played_time > 0: c.execute("UPDATE viewed SET played_time=? WHERE tmdb_id=?", (item.played_time, ID)) - else: c.execute("DELETE from viewed WHERE tmdb_id=?", (ID,)) - else: c.execute("INSERT INTO viewed (tmdb_id, played_time) VALUES (?, ?)", (ID, item.played_time)) - elif 'season' in item.infoLabels: - S = item.infoLabels['season'] - E = item.infoLabels['episode'] - c.execute("SELECT played_time FROM viewed WHERE tmdb_id=? AND season = ? AND episode=?", (ID, S, E)) - result = c.fetchone() - if result: - if played_time > 0: c.execute("UPDATE viewed SET played_time=? WHERE tmdb_id=? AND season=? AND episode=?", (item.played_time, ID, S, E)) - else: c.execute("DELETE from viewed WHERE tmdb_id=? AND season=? AND episode=?", (ID, S, E)) - else: c.execute("INSERT INTO viewed (tmdb_id, season, episode, played_time) VALUES (?, ?, ?, ?)", (ID, S, E, item.played_time)) - elif 'episode' in item.infoLabels: - E = item.infoLabels['episode'] - c.execute("SELECT played_time FROM viewed WHERE tmdb_id=? AND episode=?", (ID, E)) - result = c.fetchone() - if result: - if played_time > 0: c.execute("UPDATE viewed SET played_time=? WHERE tmdb_id=? AND episode=?", (item.played_time, ID, E)) - else: c.execute("DELETE from viewed WHERE tmdb_id=? AND episode=?", (ID, E)) - else: c.execute("INSERT INTO viewed (tmdb_id, episode, played_time) VALUES (?, ?, ?)", (ID, E, item.played_time)) - conn.commit() - conn.close() + db['viewed'][ID] = played_time + elif E: + newDict = db['viewed'].get(ID, {}) + newDict[str(S) + 'x' + str(E)] = played_time + db['viewed'][ID] = newDict + def prevent_busy(item): logger.debug() if not item.autoplay and not item.window: if item.globalsearch: xbmc.Player().play(os.path.join(config.get_runtime_path(), "resources", "kod.mp4")) else: xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, xbmcgui.ListItem(path=os.path.join(config.get_runtime_path(), "resources", "kod.mp4"))) - xbmc.sleep(100) + xbmc.sleep(200) xbmc.Player().stop() # xbmc.executebuiltin('Action(Stop)') # xbmc.sleep(500) diff --git a/platformcode/xbmc_videolibrary.py b/platformcode/xbmc_videolibrary.py index 2e231fe4..353d7999 100644 --- a/platformcode/xbmc_videolibrary.py +++ b/platformcode/xbmc_videolibrary.py @@ -46,6 +46,7 @@ def mark_auto_as_watched(item): ND = next_dialogs[next_ep_type] try: next_episode = next_ep(item) except: next_episode = False + logger.debug(next_episode) while platformtools.is_playing(): actual_time = xbmc.Player().getTime() @@ -108,9 +109,6 @@ def mark_auto_as_watched(item): threading.Thread(target=mark_as_watched_subThread, args=[item]).start() - - - def sync_trakt_addon(path_folder): """ Updates the values ​​of episodes seen if @@ -351,6 +349,38 @@ def mark_season_as_watched_on_kodi(item, value=1): execute_sql_kodi(sql) +def set_watched_on_kod(data): + from specials import videolibrary + from core import videolibrarytools + data = jsontools.load(data) + Type = data.get('item', {}).get('type','') + ID = data.get('item', {}).get('id','') + if not Type or not ID: + return + playcount = data.get('playcount',0) + for Type in ['movie', 'episode']: + sql = 'select strFileName, strPath, uniqueid_value from %s_view where (id%s like "%s")' % (Type, Type.capitalize(), ID) + n, records = execute_sql_kodi(sql) + if records: + for filename, path, uniqueid_value in records: + if Type in ['movie']: + title = filename.replace('.strm', ' [' + uniqueid_value + ']') + filename = title +'.nfo' + else: + title = filename.replace('.strm', '') + filename = 'tvshow.nfo' + + path = filetools.join(path, filename) + head_nfo, item = videolibrarytools.read_nfo(path) + item.library_playcounts.update({title: playcount}) + filetools.write(path, head_nfo + item.tojson()) + + if item.infoLabels['mediatype'] == "tvshow": + for season in item.library_playcounts: + if "season" in season: + season_num = int(scrapertools.find_single_match(season, r'season (\d+)')) + item = videolibrary.check_season_playcount(item, season_num) + filetools.write(path, head_nfo + item.tojson()) def mark_content_as_watched_on_kod(path): from specials import videolibrary @@ -384,6 +414,7 @@ def mark_content_as_watched_on_kod(path): if "\\" in path: path = path.replace("/", "\\") head_nfo, item = videolibrarytools.read_nfo(path) # I read the content .nfo + old = item.clone() if not item: logger.error('.NFO not found: ' + path) return @@ -437,8 +468,9 @@ def mark_content_as_watched_on_kod(path): if "season" in season: # we look for the tags "season" inside playCounts season_num = int(scrapertools.find_single_match(season, r'season (\d+)')) # we save the season number item = videolibrary.check_season_playcount(item, season_num) # We call the method that updates Temps. and series - - filetools.write(path, head_nfo + item.tojson()) + if item.library_playcounts != old.library_playcounts: + logger.debug('scrivo') + filetools.write(path, head_nfo + item.tojson()) #logger.debug(item) @@ -626,37 +658,14 @@ def set_content(content_type, silent=False, custom=False): xbmc.executebuiltin('Addon.OpenSettings(metadata.universal)', True) else: # SERIES - scraper = [config.get_localized_string(70098), config.get_localized_string(70093)] + scraper = [config.get_localized_string(70093), config.get_localized_string(70098)] if not custom: - seleccion = 0 # tvdb + seleccion = 0 # tmdb else: seleccion = platformtools.dialog_select(config.get_localized_string(70107), scraper) - # Instalar The TVDB - if seleccion == -1 or seleccion == 0: - if not xbmc.getCondVisibility('System.HasAddon(metadata.tvdb.com)'): - if not silent: - #Ask if we want to install metadata.tvdb.com - install = platformtools.dialog_yesno(config.get_localized_string(60048),'') - else: - install = True - - if install: - try: - # Install metadata.tvdb.com - xbmc.executebuiltin('InstallAddon(metadata.tvdb.com)', True) - logger.debug("The TVDB series Scraper installed ") - except: - pass - - continuar = (install and xbmc.getCondVisibility('System.HasAddon(metadata.tvdb.com)')) - if not continuar: - msg_text = config.get_localized_string(60049) - if continuar: - xbmc.executebuiltin('Addon.OpenSettings(metadata.tvdb.com)', True) - # Instalar The Movie Database - elif seleccion == 1: + if seleccion == -1 or seleccion == 0: if continuar and not xbmc.getCondVisibility('System.HasAddon(metadata.tvshows.themoviedb.org)'): continuar = False if not silent: @@ -680,6 +689,29 @@ def set_content(content_type, silent=False, custom=False): if continuar: xbmc.executebuiltin('Addon.OpenSettings(metadata.tvshows.themoviedb.org)', True) + # Instalar The TVDB + elif seleccion == 1: + if not xbmc.getCondVisibility('System.HasAddon(metadata.tvdb.com)'): + if not silent: + #Ask if we want to install metadata.tvdb.com + install = platformtools.dialog_yesno(config.get_localized_string(60048),'') + else: + install = True + + if install: + try: + # Install metadata.tvdb.com + xbmc.executebuiltin('InstallAddon(metadata.tvdb.com)', True) + logger.debug("The TVDB series Scraper installed ") + except: + pass + + continuar = (install and xbmc.getCondVisibility('System.HasAddon(metadata.tvdb.com)')) + if not continuar: + msg_text = config.get_localized_string(60049) + if continuar: + xbmc.executebuiltin('Addon.OpenSettings(metadata.tvdb.com)', True) + idPath = 0 idParentPath = 0 if continuar: @@ -753,11 +785,11 @@ def set_content(content_type, silent=False, custom=False): strContent = 'tvshows' scanRecursive = 0 if seleccion == -1 or seleccion == 0: - strScraper = 'metadata.tvdb.com' - path_settings = xbmc.translatePath("special://profile/addon_data/metadata.tvdb.com/settings.xml") - elif seleccion == 1: strScraper = 'metadata.tvshows.themoviedb.org' path_settings = xbmc.translatePath("special://profile/addon_data/metadata.tvshows.themoviedb.org/settings.xml") + elif seleccion == 1: + strScraper = 'metadata.tvdb.com' + path_settings = xbmc.translatePath("special://profile/addon_data/metadata.tvdb.com/settings.xml") if not os.path.exists(path_settings): logger.debug("%s: %s" % (content_type, path_settings + " doesn't exist")) return continuar @@ -961,13 +993,14 @@ def clean(path_list=[]): sql = 'SELECT idPath FROM path where strPath LIKE "%s"' % sql_path logger.debug('sql: ' + sql) nun_records, records = execute_sql_kodi(sql) - idPath = records[0][0] - sql = 'DELETE from path WHERE idPath=%s' % idPath - logger.debug('sql: ' + sql) - nun_records, records = execute_sql_kodi(sql) - sql = 'DELETE from path WHERE idParentPath=%s' % idPath - logger.debug('sql: ' + sql) - nun_records, records = execute_sql_kodi(sql) + if records: + idPath = records[0][0] + sql = 'DELETE from path WHERE idPath=%s' % idPath + logger.debug('sql: ' + sql) + nun_records, records = execute_sql_kodi(sql) + sql = 'DELETE from path WHERE idParentPath=%s' % idPath + logger.debug('sql: ' + sql) + nun_records, records = execute_sql_kodi(sql) from core import videolibrarytools for path, folders, files in filetools.walk(videolibrarytools.MOVIES_PATH): @@ -1236,7 +1269,6 @@ def update_sources(new='', old=''): def ask_set_content(silent=False): logger.debug() logger.debug("videolibrary_kodi %s" % config.get_setting("videolibrary_kodi")) - def do_config(custom=False): if set_content("movie", True, custom) and set_content("tvshow", True, custom): platformtools.dialog_ok(config.get_localized_string(80026), config.get_localized_string(70104)) @@ -1271,11 +1303,11 @@ def ask_set_content(silent=False): config.set_setting("folder_movies", movies_folder) config.set_setting("folder_tvshows", tvshows_folder) config.verify_directories_created() - do_config(True) + do_config(False) # default path and folders else: platformtools.dialog_ok(config.get_localized_string(80026), config.get_localized_string(80030)) - do_config(True) + do_config(False) # default settings else: platformtools.dialog_ok(config.get_localized_string(80026), config.get_localized_string(80027)) @@ -1286,7 +1318,7 @@ def ask_set_content(silent=False): # configuration from the settings menu else: platformtools.dialog_ok(config.get_localized_string(80026), config.get_localized_string(80023)) - do_config(True) + do_config(False) def next_ep(item): diff --git a/resources/kod.mp4 b/resources/kod.mp4 index 8f3e37a7..a06928ae 100644 Binary files a/resources/kod.mp4 and b/resources/kod.mp4 differ diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index 5f46e6e7..db835439 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -701,7 +701,7 @@ msgid "This website [B]%s[/B] seems to be unavailable, try later. if the web pag msgstr "" msgctxt "#60014" -msgid "It may be due to a connection problem, the web page of the channel has changed its structure, or an internal error of KoD. If on browser it works, report the issue using [B]Help->Report an issue.[B]" +msgid "It may be due to a connection problem, the web page of the channel has changed its structure, or an internal error of KoD. If on browser it works, report the issue using [B]Help->Report an issue.[/B]" msgstr "" msgctxt "#60015" @@ -6123,6 +6123,9 @@ msgctxt "#70830" msgid "The series / episode number should only be changed if the series has relative numbering." msgstr "" +msgctxt "#70831" +msgid "This series has multiple types of numbering, choose the most suitable one " +msgstr "" # DNS start [ settings and declaration ] msgctxt "#707401" @@ -6370,7 +6373,7 @@ msgid "Configuration of Kodi video library" msgstr "" msgctxt "#80027" -msgid "You will be asked to configure The Movie Database for movies and The TVDB for TV shows" +msgid "You will be asked to configure The Movie Database for movies and tvshows" msgstr "" msgctxt "#80028" diff --git a/resources/language/resource.language.it_it/strings.po b/resources/language/resource.language.it_it/strings.po index ac6fa323..518dcde3 100644 --- a/resources/language/resource.language.it_it/strings.po +++ b/resources/language/resource.language.it_it/strings.po @@ -700,7 +700,7 @@ msgid "This website [B]%s[/B] seems to be unavailable, try later. if the web pag msgstr "Il sito [B]%s[/B] non sembra essere disponibile, riprova più tardi. Se la pagina web funziona correttamente segnala l'errore qui: https://github.com/kodiondemand/addon/issues" msgctxt "#60014" -msgid "It may be due to a connection problem, the web page of the channel has changed its structure, or an internal error of KoD. If on browser it works, report the issue using [B]Help->Report an issue.[B]" +msgid "It may be due to a connection problem, the web page of the channel has changed its structure, or an internal error of KoD. If on browser it works, report the issue using [B]Help->Report an issue.[/B]" msgstr "Potrebbe essere dovuto a un problema di connessione, la pagina web del canale ha cambiato la sua struttura, oppure un errore interno di KoD. Se sul browser funziona, segnala il problema andando in [B]Aiuto->Segnala un problema[/B]." msgctxt "#60015" @@ -6124,6 +6124,10 @@ msgctxt "#70830" msgid "The series / episode number should only be changed if the series has relative numbering." msgstr "Il numero della serie / episodio deve essere modificato solo se la serie ha una numerazione relativa." +msgctxt "#70831" +msgid "This series has multiple types of numbering, choose the most suitable one " +msgstr "Questa serie ha più tipi di numerazione, scegli quello più adatto" + # DNS start [ settings and declaration ] msgctxt "#707401" msgid "Enable DNS check alert" @@ -6370,8 +6374,8 @@ msgid "Configuration of Kodi video library" msgstr "Configurazione della libreria di Kodi" msgctxt "#80027" -msgid "You will be asked to configure The Movie Database for movies and The TVDB for TV shows" -msgstr "Ti verrà chiesto di configurare The Movie Database per i film e The TVDB per le serie TV" +msgid "You will be asked to configure The Movie Database for movies and tvshows" +msgstr "Ti verrà chiesto di configurare The Movie Database sia per i film che per le serie TV" msgctxt "#80028" msgid "The selected folders are already used by the Kodi library. Please change them properly" diff --git a/resources/skins/Default/720p/SelectGroup.xml b/resources/skins/Default/720p/SelectGroup.xml new file mode 100644 index 00000000..38c90e2f --- /dev/null +++ b/resources/skins/Default/720p/SelectGroup.xml @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="utf-8"?> +<window> + <allowoverlays>false</allowoverlays> + <animation type="WindowOpen" reversible="false"> + <effect type="slide" start="0,200" end="0,0" center="640,225" delay="160" tween="cubic" time="200" /> + <effect type="fade" delay="160" end="100" time="240" /> + </animation> + <animation type="WindowClose" reversible="false"> + <effect type="slide" start="0,0" end="0,200" center="640,225" easing="in" tween="cubic" time="200" /> + <effect type="fade" start="100" end="0" time="240" /> + </animation> + <controls> + <control type="group"> + <description>Container</description> + <left>200</left> + <top>60</top> + <width>860</width> + <height>600</height> + <control type="image"> + <description>Background</description> + <width>100%</width> + <height>100%</height> + <texture colordiffuse="FF232323">white.png</texture> + </control> + <control type="textbox" id="1"> + <description>Heading</description> + <top>0</top> + <left>0</left> + <height>60</height> + <width>100%</width> + <font>font13</font> + <textcolor>FFFFFFFF</textcolor> + <align>center</align> + <aligny>center</aligny> + <label></label> + </control> + <control type="group"> + <left>30</left> + <top>70</top> + <width>595</width> + <height>530</height> + <control type="list" id="2"> + <description>List</description> + <left>0</left> + <top>0</top> + <width>795</width> + <height>530</height> + <onup>100</onup> + <onright>4</onright> + <orientation>vertical</orientation> + <scrolltime>200</scrolltime> + <pagecontrol>4</pagecontrol> + <itemlayout height="150" width="795"> + <control type="textbox"> + <description>Selected Item</description> + <left>20</left> + <top>20</top> + <width>765</width> + <height>110</height> + <font>font13</font> + <textcolor>FFFFFFFF</textcolor> + <label>$INFO[ListItem.Property(title)]</label> + <align>center</align> + <aligny>center</aligny> + </control> + </itemlayout> + <focusedlayout height="150" width="795"> + <control type="image"> + <top>1</top> + <width>100%</width> + <height>100%</height> + <texture colordiffuse="FF0081C2">white.png</texture> + </control> + <control type="textbox"> + <description>Selected Item</description> + <left>20</left> + <top>20</top> + <width>765</width> + <height>110</height> + <font>font13</font> + <textcolor>FFFFFFFF</textcolor> + <label>$INFO[ListItem.Property(title)]</label> + <autoscroll time="3000" delay="3000" repeat="3000">True</autoscroll> + <align>center</align> + <aligny>center</aligny> + </control> + </focusedlayout> + </control> + <control type="scrollbar" id="4"> + <description>Scrollbar</description> + <left>595</left> + <top>60</top> + <width>5</width> + <height>100%</height> + <visible>true</visible> + <texturesliderbackground colordiffuse="FF232323">white.png</texturesliderbackground> + <texturesliderbar colordiffuse="FFFFFFFF">white.png</texturesliderbar> + <texturesliderbarfocus colordiffuse="FF0081C2">white.png</texturesliderbarfocus> + <textureslidernib colordiffuse="FF232323">white.png</textureslidernib> + <textureslidernibfocus colordiffuse="FF232323">white.png</textureslidernibfocus> + <orientation>vertical</orientation> + <showonepage>false</showonepage> + <onleft>2</onleft> + <onright>3</onright> + </control> + </control> + <control type="button" id="100"> + <description>Close Button</description> + <right>30</right> + <top>10</top> + <width>40</width> + <height>40</height> + <texturefocus colordiffuse="FFFFFFFF">close.png</texturefocus> + <texturenofocus colordiffuse="55FFFFFF">close.png</texturenofocus> + <ondown>2</ondown> + </control> + </control> + </controls> +</window> diff --git a/resources/subtitle.mp4 b/resources/subtitle.mp4 deleted file mode 100644 index 50cc054b..00000000 Binary files a/resources/subtitle.mp4 and /dev/null differ diff --git a/servers/paramount_server.py b/servers/paramount_server.py index 54b7c1e3..c02530e7 100644 --- a/servers/paramount_server.py +++ b/servers/paramount_server.py @@ -18,6 +18,7 @@ def test_video_exists(page_url): def get_video_url(page_url, premium=False, user="", password="", video_password=""): logger.debug("url=" + page_url) + # from core.support import dbg;dbg() qualities = [] video_urls = [] mgid = support.match(data, patron=r'uri":"([^"]+)"').match @@ -26,8 +27,9 @@ def get_video_url(page_url, premium=False, user="", password="", video_password= url = jsontools.load(support.match(rootUrl.replace('&device={device}','').format(uri = ID)).data)['package']['video']['item'][0]['rendition'][0]['src'] urls = support.match(url, patron=r'RESOLUTION=(\d+x\d+).*?(http[^ ]+)').matches for quality, url in urls: + quality = quality.split('x')[0] if quality not in qualities: qualities.append(quality) - video_urls.append(["m3u8 {}p [Paramount]".format(quality.split('x')[-1]), url]) + video_urls.append(["m3u8 {}p [Paramount]".format(quality), url]) video_urls.sort(key=lambda url: int(support.match(url[0], patron=r'(\d+)p').match)) return video_urls diff --git a/servers/youtube.py b/servers/youtube.py index b4cfea0a..15b15b7d 100644 --- a/servers/youtube.py +++ b/servers/youtube.py @@ -43,11 +43,14 @@ def get_video_url(page_url, premium=False, user="", password="", video_password= addon_dir = xbmc.translatePath( my_addon.getAddonInfo('path') ) sys.path.append(filetools.join( addon_dir, 'resources', 'lib' ) ) from youtube_resolver import resolve - for stream in resolve(page_url): - # title = scrapertools.find_single_match(stream['title'], '(\d+p)') - if scrapertools.find_single_match(stream['title'], r'(\d+p)'): - video_urls.append([re.sub(r'(\[[^\]]+\])', '', stream['title']), stream['url']]) - video_urls.sort(key=lambda it: int(it[0].split("p", 1)[0])) + try: + for stream in resolve(page_url): + # title = scrapertools.find_single_match(stream['title'], '(\d+p)') + if scrapertools.find_single_match(stream['title'], r'(\d+p)'): + video_urls.append([re.sub(r'(\[[^\]]+\])', '', stream['title']), stream['url']]) + video_urls.sort(key=lambda it: int(it[0].split("p", 1)[0])) + except: + pass return video_urls diff --git a/service.py b/service.py index d85bbf81..7ea5f065 100644 --- a/service.py +++ b/service.py @@ -24,7 +24,7 @@ sys.path.insert(0, librerias) from core import videolibrarytools, filetools, channeltools, httptools, scrapertools from lib import schedule -from platformcode import logger, platformtools, updater +from platformcode import logger, platformtools, updater, xbmc_videolibrary from specials import videolibrary from servers import torrent @@ -398,6 +398,11 @@ class AddonMonitor(xbmc.Monitor): xbmc.executebuiltin('Action(reloadkeymaps)') self.settings_pre = settings_post + def onNotification(self, sender, method, data): + if method == 'VideoLibrary.OnUpdate': + xbmc_videolibrary.set_watched_on_kod(data) + logger.debug('AGGIORNO') + def onScreensaverActivated(self): logger.debug('screensaver activated, un-scheduling screen-on jobs') schedule.clear('screenOn') @@ -446,6 +451,61 @@ if __name__ == "__main__": # handling old autoexec method if config.is_autorun_enabled(): config.enable_disable_autorun(True) + # port old db to new + old_db_name = filetools.join(config.get_data_path(), "kod_db.sqlite") + if filetools.isfile(old_db_name): + try: + import sqlite3 + from core import db + + old_db_conn = sqlite3.connect(old_db_name, timeout=15) + old_db = old_db_conn.cursor() + old_db.execute('select * from viewed') + + for ris in old_db.fetchall(): + if ris[1]: # tvshow + show = db['viewed'].get(ris[0], {}) + show[str(ris[1]) + 'x' + str(ris[2])] = ris[3] + db['viewed'][ris[0]] = show + else: # film + db['viewed'][ris[0]] = ris[3] + except: + pass + finally: + filetools.remove(old_db_name, True, False) + + # replace tvdb to tmdb for series + if config.get_setting('videolibrary_kodi') and config.get_setting('show_once'): + nun_records, records = xbmc_videolibrary.execute_sql_kodi('select * from path where strPath like "' + + filetools.join(config.get_setting('videolibrarypath'), config.get_setting('folder_tvshows')) + + '%" and strScraper="metadata.tvdb.com"') + if nun_records: + import xbmcaddon + # change language + tvdbLang = xbmcaddon.Addon(id="metadata.tvdb.com").getSetting('language') + newLang = tvdbLang + '-' + tvdbLang.upper() + xbmcaddon.Addon(id="metadata.tvshows.themoviedb.org").setSetting('language', newLang) + updater.refreshLang() + + # prepare to replace strSettings + path_settings = xbmc.translatePath( + "special://profile/addon_data/metadata.tvshows.themoviedb.org/settings.xml") + settings_data = filetools.read(path_settings) + strSettings = ' '.join(settings_data.split()).replace("> <", "><") + strSettings = strSettings.replace("\"", "\'") + + # update db + nun_records, records = xbmc_videolibrary.execute_sql_kodi( + 'update path set strScraper="metadata.tvshows.themoviedb.org", strSettings="' + strSettings + '" where strPath like "' + + filetools.join(config.get_setting('videolibrarypath'), config.get_setting('folder_tvshows')) + + '%" and strScraper="metadata.tvdb.com"') + + # scan new info + xbmc.executebuiltin('UpdateLibrary(video)') + xbmc.executebuiltin('CleanLibrary(video)') + while xbmc.getCondVisibility('Library.IsScanningVideo()'): + xbmc.sleep(1000) + monitor = AddonMonitor() # mark as stopped all downloads (if we are here, probably kodi just started) diff --git a/specials/community.py b/specials/community.py index 876a58fe..ef078d03 100644 --- a/specials/community.py +++ b/specials/community.py @@ -401,6 +401,7 @@ def findvideos(item): json = item.url['links'] else: json = item.url + for option in json: extra = set_extra_values(item, option, item.path) title = item.fulltitle + (' - '+option['title'] if 'title' in option else '') diff --git a/specials/globalsearch.py b/specials/globalsearch.py index 3dad2a2b..ab7ad77f 100644 --- a/specials/globalsearch.py +++ b/specials/globalsearch.py @@ -700,7 +700,7 @@ class SearchWindow(xbmcgui.WindowXML): else: self.Focus(SEARCH) self.setFocusId(RESULTS) - self.RESULTS.selectItem(self.epos) + self.RESULTS.selectItem(self.pos) elif self.EPISODES.isVisible(): self.episodes = [] self.Focus(SEARCH) diff --git a/specials/news.py b/specials/news.py index 49054e7a..6618a9b3 100644 --- a/specials/news.py +++ b/specials/news.py @@ -107,11 +107,11 @@ def mainlist(item): # itemlist.append(new_item) #if list_canales['documentales']: - thumbnail = get_thumb("documentary.png") - new_item = Item(channel=item.channel, action="novedades", extra="documentales", title=config.get_localized_string(60513), - thumbnail=thumbnail) - set_category_context(new_item) - itemlist.append(new_item) + # thumbnail = get_thumb("documentary.png") + # new_item = Item(channel=item.channel, action="novedades", extra="documentales", title=config.get_localized_string(60513), + # thumbnail=thumbnail) + # set_category_context(new_item) + # itemlist.append(new_item) thumbnail = get_thumb("setting_0.png") itemlist.append(Item(channel='shortcuts', action="SettingOnPosition", category=7, setting=1, title=typo(config.get_localized_string(70285), 'bold color kod'), thumbnail=thumbnail)) diff --git a/specials/url.py b/specials/url.py index 04e1544e..368d1400 100644 --- a/specials/url.py +++ b/specials/url.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from core import servertools -from core.support import match, info +from core.support import match, info, server from core.item import Item from platformcode import config, logger @@ -27,10 +27,10 @@ def search(item, text): itemlist = [] if "server" in item.args: - from core.support import server itemlist = server(item, text) elif "direct" in item.args: itemlist.append(Item(channel=item.channel, action="play", url=text, server="directo", title=config.get_localized_string(60092))) + itemlist = server(item, itemlist=itemlist) else: data = match(text).data itemlist = servertools.find_video_items(data=data) diff --git a/specials/videolibrary.json b/specials/videolibrary.json index 67036c14..c92a3cbb 100644 --- a/specials/videolibrary.json +++ b/specials/videolibrary.json @@ -87,6 +87,7 @@ "label": "@60651", "enabled": false, "default": 0, + "visible": false, "lvalues": [ "TMDB", "None" @@ -97,6 +98,7 @@ "type": "list", "label": "@60652", "default": 0, + "visible": false, "lvalues": [ "TMDB", "TVDB" diff --git a/specials/videolibrary.py b/specials/videolibrary.py index 21ff24eb..6c89a4a8 100644 --- a/specials/videolibrary.py +++ b/specials/videolibrary.py @@ -7,6 +7,7 @@ PY3 = False if sys.version_info[0] >= 3: PY3 = True; unicode = str; unichr = chr; long = int import xbmc, os, traceback +from time import time from core import filetools, scrapertools, videolibrarytools from core.support import typo, thumb @@ -49,6 +50,9 @@ def list_movies(item, silent=False): local= True break + # from core.support import dbg;dbg() + # for movie_path in movies_path: + # get_results(movie_path, root, 'movie', local) with futures.ThreadPoolExecutor() as executor: itlist = [executor.submit(get_results, movie_path, root, 'movie', local) for movie_path in movies_path] for res in futures.as_completed(itlist): @@ -67,6 +71,7 @@ def list_tvshows(item): lista = [] root = videolibrarytools.TVSHOWS_PATH + start = time() with futures.ThreadPoolExecutor() as executor: itlist = [executor.submit(get_results, filetools.join(root, folder, "tvshow.nfo"), root, 'tvshow') for folder in filetools.listdir(root)] for res in futures.as_completed(itlist): @@ -75,7 +80,7 @@ def list_tvshows(item): if item_tvshow.library_urls and len(item_tvshow.library_urls) > 0: itemlist += [item_tvshow] lista += [{'title':item_tvshow.contentTitle,'thumbnail':item_tvshow.thumbnail,'fanart':item_tvshow.fanart, 'active': value, 'nfo':item_tvshow.nfo}] - + logger.debug('load list',time() - start) if itemlist: itemlist = sorted(itemlist, key=lambda it: it.title.lower()) @@ -88,13 +93,8 @@ def list_tvshows(item): def get_results(nfo_path, root, Type, local=False): value = 0 - if Type == 'movie': folder = "folder_movies" - else: folder = "folder_tvshows" if filetools.exists(nfo_path): - # We synchronize the episodes seen from the Kodi video library with that of KoD - from platformcode import xbmc_videolibrary - xbmc_videolibrary.mark_content_as_watched_on_kod(nfo_path) head_nfo, item = videolibrarytools.read_nfo(nfo_path) # If you have not read the .nfo well, we will proceed to the next @@ -107,6 +107,7 @@ def get_results(nfo_path, root, Type, local=False): # continue loading the elements of the video library if Type == 'movie': + folder = "folder_movies" item.path = filetools.split(nfo_path)[0] item.nfo = nfo_path sep = '/' if '/' in nfo_path else '\\' @@ -118,7 +119,7 @@ def get_results(nfo_path, root, Type, local=False): if not filetools.exists(filetools.join(item.path, filetools.basename(strm_path))) and not local: return Item(), 0 # Contextual menu: Mark as seen / not seen - visto = item.library_playcounts.get(item.path.split(sep)[0], 0) + visto = item.library_playcounts.get(strm_path.strip('/').split('/')[0], 0) item.infoLabels["playcount"] = visto if visto > 0: seen_text = config.get_localized_string(60016) @@ -136,7 +137,7 @@ def get_results(nfo_path, root, Type, local=False): item.context = [{"title": seen_text, "action": "mark_content_as_watched", "channel": "videolibrary", "playcount": counter}, {"title": delete_text, "action": "delete", "channel": "videolibrary", "multichannel": multichannel}] else: - # Sometimes it gives random errors, for not finding the .nfo. Probably timing issues + folder = "folder_tvshows" try: item.title = item.contentTitle item.path = filetools.split(nfo_path)[0] @@ -558,6 +559,8 @@ def play(item): else: itemlist = [item.clone(url=item.url, server="local")] + if not itemlist: + return [] # For direct links in list format if isinstance(itemlist[0], list): item.video_urls = itemlist @@ -907,7 +910,9 @@ def mark_season_as_watched(item): # logger.debug("item:\n" + item.tostring('\n')) # Get dictionary of marked episodes - f = filetools.join(item.path, 'tvshow.nfo') + if not item.path: f = item.nfo + else: f = filetools.join(item.path, 'tvshow.nfo') + head_nfo, it = videolibrarytools.read_nfo(f) if not hasattr(it, 'library_playcounts'): it.library_playcounts = {} diff --git a/tests/run.sh b/tests/run.sh index 2e866511..8dd6f72a 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -2,7 +2,7 @@ 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 -python -m pip install --upgrade pip +python3 -m pip install --upgrade pip pip install sakee pip install html-testRunner pip install parameterized diff --git a/tests/template.html b/tests/template.html index 6579c1cb..d3c6c647 100644 --- a/tests/template.html +++ b/tests/template.html @@ -5,9 +5,16 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous"> + <style> + textarea { + font-family: monospace; + box-sizing: border-box; + width: 100%; + } + </style> </head> <body> - <div class="container"> + <div class="container-fluid"> <div class="row"> <div class="col-xs-12"> <h2 class="text-capitalize">{{ title }}</h2> @@ -55,7 +62,9 @@ {%- if (test_case.stdout or test_case.err or test_case.err) and test_case.outcome != test_case.SKIP %} <tr style="display:none;"> <td class="col-xs-9" colspan="3"> - {%- if test_case.stdout %}<p style="white-space: pre-line;">{{ test_case.stdout|e }}</p>{% endif %} + <textarea rows="40" readonly> + {%- if test_case.stdout %}{{ test_case.stdout|e }}{% endif %} + </textarea> {%- if test_case.err %}<p style="color:maroon;">{{ test_case.err[0].__name__ }}: {{ test_case.err[1] }}</p>{% endif %} </td> </tr> @@ -63,7 +72,9 @@ {%- if (test_case.stdout or test_case.err or test_case.err) and test_case.outcome == test_case.SKIP %} <tr style="display:none;"> <td class="col-xs-9" colspan="3"> - {%- if test_case.stdout %}<p style="white-space: pre-line;">{{ test_case.stdout|e }}</p>{% endif %} + <textarea rows="40" readonly> + {%- if test_case.stdout %}{{ test_case.stdout|e }}{% endif %} + </textarea> {%- if test_case.err %}<p style="color:maroon;">{{ test_case.err }}</p>{% endif %} </td> </tr> @@ -113,8 +124,10 @@ {%- if subtest.err or subtest.err %} <tr style="display:none;"> <td class="col-xs-9" colspan="3"> - {%- if subtest.err %}<p style="color:maroon;">{{ subtest.err[0].__name__ }}: {{ subtest.err[1] }}</p>{% endif %} - {%- if subtest.err %}<p style="color:maroon;">{{ subtest.test_exception_info }}</p>{% endif %} + <textarea rows="40" readonly> + {%- if subtest.err %}{{ subtest.err[0].__name__ }}: {{ subtest.err[1] }}{% endif %} + </textarea> + {%- if subtest.err %}<p style="color:maroon;">{{ subtest.test_exception_info }}</p>{% endif %} </td> </tr> {%- endif %} @@ -126,11 +139,6 @@ {%- endif %} {%- endif %} {%- endfor %} - <tr> - <td colspan="3"> - Total: {{ summaries[test_case_name].total }}, Pass: {{ summaries[test_case_name].success }}{% if summaries[test_case_name].failure %}, Fail: {{ summaries[test_case_name].failure }}{% endif %}{% if summaries[test_case_name].error %}, Error: {{ summaries[test_case_name].error }}{% endif %}{% if summaries[test_case_name].skip %}, Skip: {{ summaries[test_case_name].skip }}{% endif %} -- Duration: {{ summaries[test_case_name].duration }} - </td> - </tr> </tbody> </table> </div> @@ -171,30 +179,18 @@ } $('#showPassed').on('click', function(){ $(".success").toggle(this.checked); - if (this.checked == false) { - $(".success").next('tr').toggle(this.checked); - } hideOrShow() }); $('#showFailed').on('click', function(){ $(".danger").toggle(this.checked); - if (this.checked == false) { - $(".danger").next('tr').toggle(this.checked); - } hideOrShow() }); $('#showErrors').on('click', function(){ $(".warning").toggle(this.checked); - if (this.checked == false) { - $(".warning").next('tr').toggle(this.checked); - } hideOrShow() }); $('#showSkipped').on('click', function(){ $(".info").toggle(this.checked); - if (this.checked == false) { - $(".info").next('tr').toggle(this.checked); - } hideOrShow() }); }); diff --git a/tests/test_generic.py b/tests/test_generic.py index aa6a28f1..e678e44c 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -14,19 +14,18 @@ import unittest import xbmc if 'KOD_TST_CH' not in os.environ: + from sakee import addoninfo # custom paths def add_on_info(*args, **kwargs): - return xbmc.AddonData( + return addoninfo.AddonData( kodi_home_path=os.path.join(os.getcwd(), 'tests', 'home'), add_on_id='plugin.video.kod', add_on_path=os.getcwd(), kodi_profile_path=os.path.join(os.getcwd(), 'tests', 'home', 'userdata') ) - # override - xbmc.get_add_on_info_from_calling_script = add_on_info - + addoninfo.get_add_on_info_from_calling_script = add_on_info # functions that on kodi 19 moved to xbmcvfs try: @@ -49,10 +48,14 @@ sys.path.insert(0, librerias) from core.support import typo from core.item import Item from core.httptools import downloadpage -from core import servertools +from core import servertools, httptools import channelselector import re + +httptools.HTTPTOOLS_DEFAULT_DOWNLOAD_TIMEOUT = 60 + +outDir = os.path.join(os.getcwd(), 'reports') validUrlRegex = re.compile( r'^(?:http|ftp)s?://' # http:// or https:// r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... @@ -144,7 +147,7 @@ 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.info([c.channel for c in channel_list]) -ret = [] +results = [] logger.record = True for chItem in channel_list: @@ -179,8 +182,6 @@ for chItem in channel_list: itemlist = getattr(module, it.action)(it) menuItemlist[it.title] = itemlist - logMenu[it.title] = logger.recordedLog - logger.recordedLog = '' # some sites might have no link inside, but if all results are without servers, there's something wrong for resIt in itemlist: @@ -206,9 +207,15 @@ for chItem in channel_list: except: import traceback logger.error(traceback.format_exc()) - logMenu[it.title] = logger.recordedLog - logger.recordedLog = '' + logMenu[it.title] = logger.recordedLog + logger.recordedLog = '' + + # results.append( + # {'ch': ch, 'hasChannelConfig': hasChannelConfig, 'mainlist': [it.title for it in mainlist], + # 'menuItemlist': {k: [it.tojson() if type(it) == Item else it for it in menuItemlist[k]] for k in menuItemlist.keys()}, + # 'serversFound': {k: [it.tojson() if type(it) == Item else it for it in menuItemlist[k]] for k in menuItemlist.keys()}, + # 'module': str(module), 'logMenu': logMenu, 'error': error}) channels.append( {'ch': ch, 'hasChannelConfig': hasChannelConfig, 'mainlist': mainlist, 'menuItemlist': menuItemlist, 'serversFound': serversFound, 'module': module, 'logMenu': logMenu, 'error': error}) @@ -217,7 +224,10 @@ logger.record = False from specials import news dictNewsChannels, any_active = news.get_channels_list() -print(channels) +# if not os.path.isdir(outDir): +# os.mkdir(outDir) +# json.dump(results, open(os.path.join(outDir, 'result.json'), 'w')) + # only 1 server item for single server serverNames = [] serversFinal = [] @@ -350,6 +360,6 @@ if __name__ == '__main__': unittest.main(testRunner=HtmlTestRunner.HTMLTestRunner(report_name='report', add_timestamp=False, combine_reports=True, report_title='KoD Test Suite', template=os.path.join(config.get_runtime_path(), 'tests', 'template.html')), exit=False) import webbrowser - webbrowser.open(os.path.join(config.get_runtime_path(), 'reports', 'report.html')) + webbrowser.open(os.path.join(outDir, 'report.html')) else: unittest.main() diff --git a/tools/updateDomains.py b/tools/updateDomains.py index 98d1b0b4..21575c60 100644 --- a/tools/updateDomains.py +++ b/tools/updateDomains.py @@ -58,9 +58,20 @@ if __name__ == '__main__': # redirect elif str(rslt['code']).startswith('3'): # result[chann] = str(rslt['code']) +' - '+ rslt['redirect'][:-1] - if rslt['redirect'].endswith('/'): - rslt['redirect'] = rslt['redirect'][:-1] result[chann] = rslt['redirect'] + # cloudflare... + elif rslt['code'] in [429, 503, 403]: + from lib import proxytranslate + import re + + print('Cloudflare riconosciuto') + try: + page_data = proxytranslate.process_request_proxy(host).get('data', '') + result[chann] = re.search('<base href="([^"]+)', page_data).group(1) + rslt['code_new'] = 200 + except Exception as e: + import traceback + traceback.print_exc() # non-existent site elif rslt['code'] == -2: print('Host Sconosciuto - '+ str(rslt['code']) +' - '+ host) @@ -72,6 +83,8 @@ if __name__ == '__main__': print('Errore Sconosciuto - '+str(rslt['code']) +' - '+ host) print("check #### FINE #### rslt :%s " % (rslt)) + if result[chann].endswith('/'): + result[chann] = result[chann][:-1] result = {'findhost': data['findhost'], 'direct': result} # I write the updated file