From dbba7f617f94d19d159d0f4952abff9b9d565a38 Mon Sep 17 00:00:00 2001 From: Ingo-FP-Angel Date: Tue, 31 Oct 2023 11:21:01 +0100 Subject: [PATCH] [plugin.video.filmfriend] 1.0.1 --- plugin.video.filmfriend/addon.xml | 12 +- plugin.video.filmfriend/changelog.txt | 4 + plugin.video.filmfriend/default.py | 13 +- .../resource.language.de_de/strings.po | 4 + .../resource.language.en_gb/strings.po | 4 + .../resources/lib/jsonparser.py | 136 ++++++++++++------ .../resources/lib/login.py | 20 +-- 7 files changed, 130 insertions(+), 63 deletions(-) create mode 100644 plugin.video.filmfriend/changelog.txt diff --git a/plugin.video.filmfriend/addon.xml b/plugin.video.filmfriend/addon.xml index e83d76e422..4b6e8a75b2 100644 --- a/plugin.video.filmfriend/addon.xml +++ b/plugin.video.filmfriend/addon.xml @@ -1,8 +1,9 @@ - + + @@ -13,11 +14,16 @@ all en de fr GPL-2.0-only - https://forum.kodi.tv/showthread.php?tid=353903 - https://github.com/sarbes/plugin.video.filmfriend + https://forum.kodi.tv/showthread.php?tid=374894 + https://github.com/Ingo-FP-Angel/repo-plugins/tree/matrix/plugin.video.filmfriend + v1.0.1 (2023-10-31) + [fix] make the plugin work again + [new] play videos from your personal watchlist + https://www.filmfriend.de/ This video add-on provides access to shows and movies of Filmfriend.de. This video add-on provides access to shows and movies of Filmfriend.de. A valid library account is reqired in order to use this add-on. + This add-on is unofficial. Dieses Add-on bietet Zugriff auf Filme und Serien von Filmfriend.de. Dieses Add-on bietet Zugriff auf Filme und Serien von Filmfriend.de. Ein Bibliotheksaccount ist zur Nutzung dieses Dienstes nötig. Dieses Add-on ist inoffiziell. diff --git a/plugin.video.filmfriend/changelog.txt b/plugin.video.filmfriend/changelog.txt new file mode 100644 index 0000000000..6d11fa9f2e --- /dev/null +++ b/plugin.video.filmfriend/changelog.txt @@ -0,0 +1,4 @@ +v1.0.1 +- [fix] Make the plugin work again +- [fix] Refresh access_token before usage if expired +- [feat] Add watchlist support diff --git a/plugin.video.filmfriend/default.py b/plugin.video.filmfriend/default.py index 4175192038..69ea5572c9 100644 --- a/plugin.video.filmfriend/default.py +++ b/plugin.video.filmfriend/default.py @@ -13,6 +13,7 @@ def __init__(self): 'listSearch': self.listSearch, 'listMain': self.listMain, 'listVideos': self.listVideos, + 'listWatchList': self.listWatchList, }) self.searchModes = { @@ -29,18 +30,22 @@ def listMain(self): l.append({'metadata':{'name':self.translation(32031)}, 'type':'dir', 'params':{'mode':'listSearch', 'content':'videos', 'params':'?facets=Kind&facets=VideoKind&facets=Categories&facets=Genres&facets=GameSituations&facets=AgeRecommendation&facets=AudioLanguages&facets=AudioDescriptionLanguages&facets=SubtitleLanguages&facets=ClosedCaptionLanguages&kinds=Video&videoKinds=Movie&&&&orderBy=MonthlyImpressionScore&sortDirection=Descending&skip=0&take=20'}}) l.append({'metadata':{'name':self.translation(30600)}, 'type':'dir', 'params':{'mode':'listSearch', 'content':'tvshows', 'params':'?facets=Kind&facets=VideoKind&facets=Categories&facets=Genres&facets=GameSituations&facets=AgeRecommendation&facets=AudioLanguages&facets=AudioDescriptionLanguages&facets=SubtitleLanguages&facets=ClosedCaptionLanguages&kinds=Series&&languageIsoCode=EN&orderBy=EnglishOrder&sortDirection=Ascending&skip=0&take=500'}}) l.append({'metadata':{'name':self.translation(30601)}, 'type':'dir', 'params':{'mode':'listSearch', 'content':'movies', 'params':'?facets=Kind&facets=VideoKind&facets=Categories&facets=Genres&facets=GameSituations&facets=AgeRecommendation&facets=AudioLanguages&facets=AudioDescriptionLanguages&facets=SubtitleLanguages&facets=ClosedCaptionLanguages&kinds=Video&videoKinds=Movie&categories=d36cbed2-7569-4b94-9080-03ce79c2ecee&orderBy=EnglishOrder&sortDirection=Ascending&skip=0&take=500'}}) + l.append({'metadata':{'name':self.translation(30602)}, 'type':'dir', 'params':{'mode':'listWatchList', 'content':'videos', 'params':'?totalCount=true&take=500&sortOrder=RecentlyAdded'}}) l.append({'metadata':{'name':self.translation(32139)}, 'params':{'mode':'libMediathekSearch', 'searchMode':'listVideoSearch'}, 'type':'dir'}) return {'items':l,'name':'root'} - + def listSearch(self): return jsonParser.parseSearch(self.params['params'],self.params['content']) - + + def listWatchList(self): + return jsonParser.parseWatchList(self.params['params'],self.params['content']) + def listVideoSearch(self,searchString): return jsonParser.parseSearch(f'?search={searchString}&facets=Kind&facets=VideoKind&facets=Categories&facets=Genres&facets=GameSituations&facets=AgeRecommendation&facets=AudioLanguages&facets=AudioDescriptionLanguages&facets=SubtitleLanguages&facets=ClosedCaptionLanguages&kinds=Video&kinds=Series&kinds=Person&videoKinds=Movie&languageIsoCode=EN&orderBy=Score&sortDirection=Descending&skip=0&take=30') - + def listVideos(self): return jsonParser.parseVideos(self.params['id'],self.params['content']) - + def playVideo(self): return jsonParser.getVideoUrl(self.params['video']) diff --git a/plugin.video.filmfriend/resources/language/resource.language.de_de/strings.po b/plugin.video.filmfriend/resources/language/resource.language.de_de/strings.po index d8e24c9c14..2ea1e0ada0 100644 --- a/plugin.video.filmfriend/resources/language/resource.language.de_de/strings.po +++ b/plugin.video.filmfriend/resources/language/resource.language.de_de/strings.po @@ -110,3 +110,7 @@ msgstr "Serien" msgctxt "#30601" msgid "Movies" msgstr "Filme" + +msgctxt "#30602" +msgid "Watchlist" +msgstr "Watchlist" diff --git a/plugin.video.filmfriend/resources/language/resource.language.en_gb/strings.po b/plugin.video.filmfriend/resources/language/resource.language.en_gb/strings.po index e64d7dd411..c2e77ed6e9 100644 --- a/plugin.video.filmfriend/resources/language/resource.language.en_gb/strings.po +++ b/plugin.video.filmfriend/resources/language/resource.language.en_gb/strings.po @@ -110,3 +110,7 @@ msgstr "Shows" msgctxt "#30601" msgid "Movies" msgstr "Movies" + +msgctxt "#30602" +msgid "Watchlist" +msgstr "Watchlist" diff --git a/plugin.video.filmfriend/resources/lib/jsonparser.py b/plugin.video.filmfriend/resources/lib/jsonparser.py index 9e657232ea..32431a3284 100644 --- a/plugin.video.filmfriend/resources/lib/jsonparser.py +++ b/plugin.video.filmfriend/resources/lib/jsonparser.py @@ -2,6 +2,8 @@ import requests import json import libmediathek4utils as lm4utils +import pyjwt as jwt +import time base = 'https://api.vod.filmwerte.de/api/v1/' @@ -58,14 +60,37 @@ else: lang = languages[s] +def fetchJson(url,headers=None): + response = requests.get(url,headers=headers) + if response.status_code > 299: + raise RuntimeError(f"Fetching '{url}' failed with code '{response.status_code}' and optional message '{response.text}'") + + return response.json() + def parseMain(): j = requests.get(f'{base}tenant-groups/21960588-0518-4dd3-89e5-f25ba5bf5631/navigation').json() +def parseWatchList(params,content='videos'): + _checkTokenExpired() + headers = { + 'Authorization':f'Bearer {lm4utils.getSetting("access_token")}' + } + + j = fetchJson(f'https://api.tenant.frontend.vod.filmwerte.de/v11/{lm4utils.getSetting("tenant")}/watchlist{params}',headers) + return parseResponse(j,content) + def parseSearch(params,content='videos'): - j = requests.get(f'{base}/tenant-groups/fba2f8b5-6a3a-4da3-b555-21613a88d3ef/search{params}').json() + j = fetchJson(f'{base}/tenant-groups/fba2f8b5-6a3a-4da3-b555-21613a88d3ef/search{params}') + return parseResponse(j,content) + +def parseResponse(responseJson,content='videos'): res = {'items':[],'content':content,'pagination':{'currentPage':0}} - for item in j['results']: - result = item['result'] + for item in responseJson['results']: + result = item + if 'result' in item: + result = item['result'] + else: + result = item[item['kind'].lower()] if item['kind'] == 'Series': d = {'type':'tvshow', 'params':{'mode':'listSearch', 'content':'tvshows'}, 'metadata':{'art':{}}} d['metadata']['name'] = _getString(result,'title') @@ -96,7 +121,7 @@ def parseSearch(params,content='videos'): res['items'].append(d) - elif item['kind'] == 'Video': + elif item['kind'] == 'Video' or item['kind'] == 'Movie': d = {'type':'movie', 'params':{'mode':'playVideo'}, 'metadata':{'art':{},'actors':[],'directors':[],'artists':[],'writers':[],'genres':[],'credits':[]}} d['metadata']['name'] = _getString(result,'title') if 'originalTitle' in result: @@ -116,18 +141,18 @@ def parseSearch(params,content='videos'): d['metadata']['aired'] = result['releaseDate'][:10] d['metadata']['art'] = _getArt(result) - - for participant in result['participations']: - if participant['kind'] in ['Actor', 'Voice']: - d['metadata']['actors'].append({'role':participant.get('englishDescription',''),'name':_getName(participant)}) - elif participant['kind'] in ['Director', 'Producer']: - d['metadata']['directors'].append(_getName(participant)) - elif participant['kind'] == 'Composer': - d['metadata']['artists'].append(_getName(participant)) - elif participant['kind'] in ['Writer', 'Editor']: - d['metadata']['writers'].append(_getName(participant)) - elif participant['kind'] in ['Misc', 'Camera']: - d['metadata']['credits'].append(_getName(participant)) + if 'participations' in result: + for participant in result['participations']: + if participant['kind'] in ['Actor', 'Voice']: + d['metadata']['actors'].append({'role':participant.get('englishDescription',''),'name':_getName(participant)}) + elif participant['kind'] in ['Director', 'Producer']: + d['metadata']['directors'].append(_getName(participant)) + elif participant['kind'] == 'Composer': + d['metadata']['artists'].append(_getName(participant)) + elif participant['kind'] in ['Writer', 'Editor']: + d['metadata']['writers'].append(_getName(participant)) + elif participant['kind'] in ['Misc', 'Camera']: + d['metadata']['credits'].append(_getName(participant)) if 'genres' in result: for genre in result['genres']: d['metadata']['genres'].append(_getString(genre,'name')) @@ -136,34 +161,37 @@ def parseSearch(params,content='videos'): res['items'].append(d) return res -def getVideoUrl(video): +def getVideoUrl(videoId): + _checkTokenExpired() headers = { 'Authorization':f'Bearer {lm4utils.getSetting("access_token")}' } - r = requests.get(f'{base}customers(end-user)/me',headers=headers).text - if r == '': - token = _getNewToken() - if not token: - lm4utils.displayMsg(lm4utils.getTranslation(30507),lm4utils.getTranslation(30508)) - return {} - headers = { - 'Authorization':f'Bearer {token}' - } - r = requests.get(f'{base}customers(end-user)/me',headers=headers).text - - j = json.loads(r) - id = j['id'] - - j = requests.get(f'{base}customers(tenant)/{lm4utils.getSetting("tenant")}/customers(end-user)/{id}/videos/{video}/uri?pin=undefined',headers=headers).json() - url = f'{j["mpegDashUri"]}' + videoInfo = requests.get(f'https://api.tenant.frontend.vod.filmwerte.de/v11/{lm4utils.getSetting("tenant")}/movies/{videoId}/uri',headers=headers).json() + url = f'{videoInfo["mpegDash"]}' wvheaders = '&content-type=' - licenseserverurl = f'{j["widevineLicenseServerUri"]}|{wvheaders}|R{{SSM}}|' + licenseserverurl = f'{videoInfo["widevineLicenseServerUri"]}|{wvheaders}|R{{SSM}}|' return {'media':[{'url':url, 'licenseserverurl':licenseserverurl, 'type': 'video', 'stream':'DASH'}]} +def _checkTokenExpired(): + tokenString = lm4utils.getSetting("access_token") + isExpired = False + try: + token = jwt.decode(tokenString, key="", algorithms=["RSA256"], options={"verify_signature":False, "verify_aud":False}) + expiry = token['exp'] + if expiry <= time.time(): + isExpired = True + except jwt.exceptions.ExpiredSignatureError: + isExpired = True + + if isExpired: + lm4utils.log("Access token for filmfried.de expired. Will fetch new token.") + _getNewToken() + def _getNewToken(): refresh_token = lm4utils.getSetting('refresh_token') if refresh_token == '': + lm4utils.log("Cannot fetch new access token for filmfried.de. Refresh token is missing.") return False files = {'client_id':(None, f'tenant-{lm4utils.getSetting("tenant")}-filmwerte-vod-frontend'),'grant_type':(None, 'refresh_token'),'refresh_token':(None, refresh_token),'scope':(None, 'filmwerte-vod-api offline_access')} j = requests.post('https://api.vod.filmwerte.de/connect/token', files=files).json() @@ -179,7 +207,10 @@ def _getString(d,k): else: return d[english[k]] except: - return '' + try: + return d.get(k) + except: + return '' def _getName(participant): if 'firstName' in participant['person']: @@ -192,15 +223,28 @@ def _getArt(item): fanart = '' poster = '' banner = '' - for art in item['artworks']: - if art['kind'] == 'Thumbnail' and thumb == '': - thumb = art['uri']['thumbnail2x'] - elif art['kind'] == 'Thumbnail' and fanart == '': - fanart = art['uri']['resolution4x'] - elif art['kind'] == 'Background': - fanart = art['uri']['resolution1080'] - elif art['kind'] == 'CoverPortrait' and poster == '': - poster = art['uri']['thumbnail4x'] - elif art['kind'] == 'Teaser' and banner == '': - banner = art['uri']['resolution720'] + if 'artworkUris' in item: + for art in item['artworkUris']: + if art['kind'] == 'Thumbnail' and thumb == '': + thumb = art['resolution2x'] + elif art['kind'] == 'Thumbnail' and fanart == '': + fanart = art['resolution4x'] + elif art['kind'] == 'Background': + fanart = art['resolution1080'] + elif art['kind'] == 'CoverPortrait' and poster == '': + poster = art['resolution4x'] + elif art['kind'] == 'Teaser' and banner == '': + banner = art['resolution720'] + else: + for art in item['artworks']: + if art['kind'] == 'Thumbnail' and thumb == '': + thumb = art['uri']['thumbnail2x'] + elif art['kind'] == 'Thumbnail' and fanart == '': + fanart = art['uri']['resolution4x'] + elif art['kind'] == 'Background': + fanart = art['uri']['resolution1080'] + elif art['kind'] == 'CoverPortrait' and poster == '': + poster = art['uri']['thumbnail4x'] + elif art['kind'] == 'Teaser' and banner == '': + banner = art['uri']['resolution720'] return {'thumb':thumb, 'fanart':fanart, 'poster':poster, 'banner':banner} diff --git a/plugin.video.filmfriend/resources/lib/login.py b/plugin.video.filmfriend/resources/lib/login.py index 0f7426ee72..2fbd15dab6 100644 --- a/plugin.video.filmfriend/resources/lib/login.py +++ b/plugin.video.filmfriend/resources/lib/login.py @@ -5,19 +5,19 @@ import json import libmediathek4utils as lm4utils -base = 'https://api.vod.filmwerte.de/api/v1/' - +base = 'https://api.tenant-group.frontend.vod.filmwerte.de/v7/' +providerBase = 'https://api.tenant.frontend.vod.filmwerte.de/v11/' def pick(): - j = requests.get(f'{base}tenant-groups/fba2f8b5-6a3a-4da3-b555-21613a88d3ef/tenants?orderBy=DisplayCategory&sortDirection=Ascending&skip=&take=1000').json() + j = requests.get(f'{base}fba2f8b5-6a3a-4da3-b555-21613a88d3ef/sign-in').json() l = [] - for item in j['items']: + for item in j['tenants']: l.append(xbmcgui.ListItem(f'{item["displayCategory"]} - {item["displayName"]}')) i = xbmcgui.Dialog().select(lm4utils.getTranslation(30010), l) - domain = j['items'][int(i)]['domain'] - tenant = j['items'][int(i)]['id'] - library = j['items'][int(i)]['displayName'] + domain = j['tenants'][int(i)]['clients']['web']['domain'] + tenant = j['tenants'][int(i)]['id'] + library = j['tenants'][int(i)]['displayName'] username = xbmcgui.Dialog().input(lm4utils.getTranslation(30500)) if username == '': @@ -29,16 +29,16 @@ def pick(): lm4utils.displayMsg(lm4utils.getTranslation(30504), lm4utils.getTranslation(30505)) return - r = requests.get(f'{base}customers(tenant)/{tenant}/identity-providers?orderBy=&sortDirection=') + r = requests.get(f'{providerBase}{tenant}/sign-in') if r.text == '': lm4utils.displayMsg(lm4utils.getTranslation(30506), lm4utils.getTranslation(30507)) return j = r.json() - provider = j['items'][0]['id'] + provider = j['delegated'][0]['provider'] client_id = f'tenant-{tenant}-filmwerte-vod-frontend' - files = {'client_id':(None, client_id),'provider':(None, provider),'username':(None, username),'password':(None, password)} + files = {'client_id':(None, client_id),'provider':(None, provider),'username':(None, username),'password':(None, password),'scope':(None, 'filmwerte-vod-api offline_access')} j = requests.post('http://api.vod.filmwerte.de/connect/authorize-external', files=files).json() if 'error' in j: if j['error'] == 'InvalidCredentials':