Skip to content

Commit

Permalink
[plugin.video.filmfriend] 1.0.1
Browse files Browse the repository at this point in the history
  • Loading branch information
Ingo-FP-Angel committed Oct 31, 2023
1 parent 64605b1 commit 89890f3
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 63 deletions.
12 changes: 9 additions & 3 deletions plugin.video.filmfriend/addon.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon id="plugin.video.filmfriend" name="Filmfriend.de" version="1.0.0" provider-name="sarbes">
<addon id="plugin.video.filmfriend" name="Filmfriend.de" version="1.0.1" provider-name="Ingo-FP-Angel,sabres">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.libmediathek4" version="1.0.0"/>
<import addon="script.module.pyjwt" version="1.7.1"/>
<import addon="script.module.requests" version="2.19.1"/>
<import addon="inputstream.adaptive" version="2.5.4"/>
</requires>
Expand All @@ -13,11 +14,16 @@
<platform>all</platform>
<language>en de fr</language>
<license>GPL-2.0-only</license>
<forum>https://forum.kodi.tv/showthread.php?tid=353903</forum>
<source>https://github.com/sarbes/plugin.video.filmfriend</source>
<forum>https://forum.kodi.tv/showthread.php?tid=374894</forum>
<source>https://github.com/Ingo-FP-Angel/repo-plugins/tree/matrix/plugin.video.filmfriend</source>
<news>v1.0.1 (2023-10-31)
[fix] make the plugin work again
[new] play videos from your personal watchlist
</news>
<website>https://www.filmfriend.de/</website>
<summary lang="en_GB">This video add-on provides access to shows and movies of Filmfriend.de.</summary>
<description lang="en_GB">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.</description>
<disclaimer lang="en_GB">This add-on is unofficial.</disclaimer>
<summary lang="de_DE">Dieses Add-on bietet Zugriff auf Filme und Serien von Filmfriend.de.</summary>
<description lang="de_DE">Dieses Add-on bietet Zugriff auf Filme und Serien von Filmfriend.de. Ein Bibliotheksaccount ist zur Nutzung dieses Dienstes nötig.</description>
<disclaimer lang="de_DE">Dieses Add-on ist inoffiziell.</disclaimer>
Expand Down
4 changes: 4 additions & 0 deletions plugin.video.filmfriend/changelog.txt
Original file line number Diff line number Diff line change
@@ -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
13 changes: 9 additions & 4 deletions plugin.video.filmfriend/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def __init__(self):
'listSearch': self.listSearch,
'listMain': self.listMain,
'listVideos': self.listVideos,
'listWatchList': self.listWatchList,
})

self.searchModes = {
Expand All @@ -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'])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,7 @@ msgstr "Serien"
msgctxt "#30601"
msgid "Movies"
msgstr "Filme"

msgctxt "#30602"
msgid "Watchlist"
msgstr "Watchlist"
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,7 @@ msgstr "Shows"
msgctxt "#30601"
msgid "Movies"
msgstr "Movies"

msgctxt "#30602"
msgid "Watchlist"
msgstr "Watchlist"
136 changes: 90 additions & 46 deletions plugin.video.filmfriend/resources/lib/jsonparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/'

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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:
Expand All @@ -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'))
Expand All @@ -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()
Expand All @@ -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']:
Expand All @@ -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}
20 changes: 10 additions & 10 deletions plugin.video.filmfriend/resources/lib/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 == '':
Expand All @@ -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':
Expand Down

0 comments on commit 89890f3

Please sign in to comment.