phate89 / plugin.video.rsich

A kodi plugin to watch live channels and program broadcast from the swiss tv channels.
GNU General Public License v2.0
5 stars 0 forks source link

[Suggestion] Add sport dedicated streaming section #2

Closed riccardo1991 closed 6 years ago

riccardo1991 commented 6 years ago

I have a suggestion, insert a section with the live streams for the sport events https://www.rsi.ch/sport/streaming/.

The endpoint to get the JSON containing the URLs is not working with these streams (the ones with a URN from swisstxt), but decompiling the Android apk I found the endpoint for such streams: https://event.api.swisstxt.ch/v1/events/rsi/byEventItemId?eids=<EVENT_ID> e.g. https://event.api.swisstxt.ch/v1/events/rsi/byEventItemId?eids=288158

Edit: I just found another interesting endpoint: https://event.api.swisstxt.ch/v1/events/rsi/live. This can even avoid to get the eventID. Now there is just a live ongoing, on the 23rd of June I will check if with multiple events ongoing they are all listed in this endpoint.

{"user":"RSI_Admin","streamType":"fullDvr","title":"Calcio: Mondiali, Tunisia - Inghilterra","overrides":null,"category":"present","duration":10620000,"description":"","startDate":"2018-06-18T17:38:00Z","endDate":"2018-06-18T20:35:00Z","imageUrl":"https://www.srf.ch/static/sportevents/2018-fussball-wm/web/prod/images/splash/2018-wc-splash.jpg","validFrom":"2018-06-18T17:38:00Z","validTo":"2018-07-17T18:00:00Z","highlightCount":0,"actual":false,"highlights":null,"vector":"none","med":false,"channel":"ENC_PC_20_A","tags":null,"sport":"arc","videoSrc":"RSI_La2_HD","eventItemId":"288194","client":"RSI","urn":"urn:swisstxt:video:rsi:288194","reserve":"","hdn1":"","hls":"https://srgssrgop20a-lh.akamaihd.net/i/enc20a_gop@141646/master.m3u8?dw=0","hds":"http://srgssrgop20a-lh.akamaihd.net/z/enc20a_gop@141646/manifest.f4m","system":"LiveScheduler","analyticsMetadata":{"media_urn":"urn:swisstxt:video:rsi:288194","media_segment_length":10620,"media_episode_length":10620,"media_episode":"Calcio: Mondiali, Tunisia - Inghilterra","media_show":"Livecenter","media_player_display":null,"media_special":"fifa.world.cup.2018","media_segment":"Calcio: Mondiali, Tunisia - Inghilterra","media_full_length":"full","media_is_geoblocked":true,"media_url":"https://srgssrgop20a-lh.akamaihd.net/i/enc20a_gop@141646/master.m3u8?dw=0","media_channel_name":"LA 2","media_channel_id":"la2","media_livestream_encoder":"20A","media_content_group":"sport","media_episode_id":"fb5b4d61-d6a4-44c2-9424-64355fb7b6c9","media_publication_date":"2018-06-08","media_publication_time":"15:15:19","media_is_tvsvizzera":"false","media_special_format":null,"media_quality":null,"media_since_publication_d":10,"media_since_publication_h":244,"media_thumbnail":"https://www.srf.ch/static/sportevents/2018-fussball-wm/web/prod/images/splash/2018-wc-splash.jpg","media_type":"video","media_is_livestream":"true","media_is_web_only":"false","media_show_id":"Livecenter","media_publication_datetime":"2018-06-08T15:15:19+02:00","media_camera_angle_name":null,"media_language":"ita","media_enterprise_units":"RSI","media_joker1":"Web-Sport","media_joker2":"Live streaming.Sport Live Streaming","media_joker3":"Calcio: Mondiali, Tunisia - Inghilterra"},"integration":{"eventItemId":null,"validTo":"2018-07-17T18:00:00Z","imageUrl":null,"evsMatchId":"108584","analyticsMetadata":null},"createdBy":"Paolo Pelloni","isNew":false,"id":"fb5b4d61-d6a4-44c2-9424-64355fb7b6c9"}
riccardo1991 commented 6 years ago

I tried by myself but I do not have the possibility to test the code now. Can you check if this mod works? Note: this is with 1 live event only since we do not know the returned JSON structure with multiple live.

from phate89lib import kodiutils, rutils, staticutils
import urllib, datetime, calendar, os
from urlparse import urlparse
import dateutil.parser
import xml.etree.ElementTree as ET
from tempfile import mkstemp

webutils=rutils.RUtils()
webutils.log=kodiutils.log
useragent = "RSI.CH Video Addon"
useragent_q = urllib.quote_plus(useragent)
webutils.USERAGENT = useragent

#site logic

def getVideoLinks(id):
        return getVideoLinksFromUrl('http://il.srgssr.ch/integrationlayer/1.0/ue/rsi/video/play/{id}.json'.format(id=id))

def getVideoLinksFromUrl(url):
    resp = webutils.getJson(url)
    if not resp:
        return False
    if not ('Video' in resp and 'Playlists' in resp['Video'] and 'Playlist' in resp['Video']['Playlists']):
        return False
    ret = []
    for pl in resp['Video']['Playlists']['Playlist']:
        for purl in pl['url']:
            plout= {}
            plout['protocol'] = pl['@protocol']
            plout['quality'] = purl['@quality']
            plout['url'] = purl['text']
            ret.append(plout)
    if 'Subtitles' in resp['Video'] and 'TTMLUrl' in resp['Video']['Subtitles']:
        ret.append({'protocol':'TTMLUrl', 'quality':'', 'url': resp['Video']['Subtitles']['TTMLUrl']})
    return ret

def getAuthString(url):
    #obtain the auth url
    sUrl=urlparse(url).path.split('/')
    if len(sUrl) < 3:
        return False
    auth=webutils.getJson('http://tp.srgssr.ch/akahd/token?acl={path}*'.format(path=urllib.quote_plus('/{p1}/{p2}/'.format(p1=sUrl[1],p2=sUrl[2]))))
    if 'token' in auth and 'authparams' in auth['token']:
        return '?' + auth['token']['authparams']
    else:
        return False;

def getCategories():
    jsn = webutils.getJson('http://il.srgssr.ch/integrationlayer/1.0/ue/rsi/tv/topic.json')
    if not jsn or 'Topics' not in jsn or 'Topic' not in jsn['Topics']:
        return False
    return jsn['Topics']['Topic']

def getProgramVideos(id, page=1, pageSize=10, parseAll=False):
    resp = webutils.getJson('http://il.srgssr.ch/integrationlayer/1.0/ue/rsi/assetSet/listByAssetGroup/{id}.json?pageNumber={page}&pageSize={pageSize}'.format(id=id,page=page,pageSize=pageSize))
    ret = getVideoInfo(resp)
    if parseAll and ret and len(ret)>0:
        ret.extend(getProgramVideos(id, int(page)+1, pageSize, True))
    return ret

def getDateVideos(id):
    resp = webutils.getJson('http://il.srgssr.ch/integrationlayer/1.0/ue/rsi/video/episodesByDate.json?day={id}'.format(id=id))
    return getVideoInfo(resp)

def getCategoryVideos(id, pageSize=10):
    resp = webutils.getJson('http://il.srgssr.ch/integrationlayer/1.0/ue/rsi/video/editorialPlayerLatestByTopic/{id}.json?pageSize={pageSize}'.format(id=id,pageSize=pageSize))
    return getVideoInfo(resp)

def getPicksVideos(pageSize=10):
    resp = webutils.getJson('http://il.srgssr.ch/integrationlayer/1.0/ue/rsi/video/editorialPlayerPicks.json?pageSize={pageSize}'.format(pageSize=pageSize))
    return getVideoInfo(resp)

def getVideoInfo(jsn):
    if not jsn:
        return False
    vids = []
    if 'AssetSets' in jsn and 'AssetSet' in jsn['AssetSets']:
        vids = jsn['AssetSets']['AssetSet']
    elif 'Videos' in jsn and 'Video' in jsn['Videos']:
        vids = jsn['Videos']['Video']
    else:
        return False
    ret = []
    for vid in vids:
        if 'Assets' in vid:
            if 'Video' in vid['Assets'] and len(vid['Assets']['Video']) > 0:
                vid = vid['Assets']['Video'][0]
            else:
                continue
        if 'AssetMetadatas' in vid and 'AssetMetadata' in vid['AssetMetadatas'] and len(vid['AssetMetadatas']['AssetMetadata'])>0:
            vd=vid['AssetMetadatas']['AssetMetadata'][0]
            title = ''
            if 'AssetSet' in vid and 'Show' in vid['AssetSet'] and 'title' in vid['AssetSet']['Show']:
                title = vid['AssetSet']['Show']['title'] + ' - '
            date = ''
            if ('createdDate' in vd):
                date = dateutil.parser.parse(vd['createdDate'])
            ret.append({'id': vd['id'], 'title': title + vd['title'], 'plot': vd['description'], 'date': vd['createdDate'], 'premiered': str(date), 'image': getImage(vid)})
    return ret

def getImage(vid):
    if 'Image' in vid and \
       'ImageRepresentations' in vid['Image'] and \
       'ImageRepresentation' in vid['Image']['ImageRepresentations'] and \
       len(vid['Image']['ImageRepresentations']['ImageRepresentation'])>0 and \
       'url' in vid['Image']['ImageRepresentations']['ImageRepresentation'][0]:
            return vid['Image']['ImageRepresentations']['ImageRepresentation'][0]['url']
    else:
            return 'http://ws.srf.ch/asset/image/audio/b6813d84-7d73-444a-92b0-9fd5eb4606cc/NOT_SPECIFIED.jpg'

def getPrograms(letter=''):
    jsn = webutils.getJson('http://il.srgssr.ch/integrationlayer/1.0/ue/rsi/tv/assetGroup/editorialPlayerAlphabetical.json')
    if not jsn or 'AssetGroups' not in jsn or 'Show' not in jsn['AssetGroups']:
        return False
    ret = []
    for pr in jsn['AssetGroups']['Show']:
        if letter =='' or letter[0].lower()==pr['title'][0].lower():
            ret.append({'id': pr['id'], 'title': pr['title'], 'plot': pr['description'], 'image': getImage(pr)})
    return ret

def getChannels():
    jsn = webutils.getJson('http://www.rsi.ch/play/tv/livemodule?layout=json')
    if not jsn or 'teaser' not in jsn:
        return False
    return jsn['teaser']

def ttmlToSrt(url):
    if not url:
        return False
    txt = webutils.getText(url)
    if not txt:
        return False
    root = ET.fromstring(txt.encode('utf-8'))
    if not root:
        return False
    count = 0
    output=''
    for sub in root[0][0]:
        count+=1
        startT=datetime.datetime.strptime(sub.attrib['begin'], '%H:%M:%S.%f').strftime('%H:%M:%S,%f')[:-3]
        endT=datetime.datetime.strptime(sub.attrib['end'], '%H:%M:%S.%f').strftime('%H:%M:%S,%f')[:-3]
        text=''
        for line in sub:
            if line.tag.endswith('span'):
                text+=line.text.strip()
            elif line.tag.endswith('br'):
                text+='\r\n'
        output+='{count}\r\n{start} --> {end}\r\n{text}\r\n\r\n'.format(count=count,start=startT,end=endT,text=text.encode('utf-8'))
    f, path = mkstemp()
    os.write(f, output)
    os.close(f)
    return path

#kodi logic

def loadList():
    kodiutils.addListItem(kodiutils.LANGUAGE(32012), params={"mode": "programs" })
    kodiutils.addListItem(kodiutils.LANGUAGE(32013), params={"mode": "live" })
    kodiutils.addListItem(kodiutils.LANGUAGE(32014), params={"mode": "categories" })
    kodiutils.addListItem(kodiutils.LANGUAGE(32015), params={"mode": "dates" })
    kodiutils.addListItem(kodiutils.LANGUAGE(32016), params={"mode": "picks" })
    kodiutils.addListItem(kodiutils.LANGUAGE(32017), params={"mode": "sport" })
    kodiutils.endScript()

def addProgramsItems():
    kodiutils.setContent('videos')
    progs = getPrograms()
    if progs:
        for p in progs:
            p['mediatype']='video'
            kodiutils.addListItem(p['title'], params={"id": p['id'], "mode": "program" }, videoInfo=p, thumb=p['image'])
    kodiutils.endScript()

def addLive():
    kodiutils.setContent('videos')
    chs = getChannels()
    if chs:
        for ch in chs:
            kodiutils.addListItem(ch['title'], params={"mode": "video", "id": ch['id'] }, thumb=ch['logoUrl'], videoInfo={'mediatype': 'video'}, isFolder=False)
    kodiutils.endScript()

def addCategories():
    cats = getCategories()
    if cats:
        for cat in cats:
            kodiutils.addListItem(cat['title'], params={"mode": "category", "id": cat['id'] })
    kodiutils.endScript()

def addDates():
    day = datetime.date.today()
    for x in range(0, 10):
        kodiutils.addListItem(kodiutils.LANGUAGE(32020).format(dayname=kodiutils.LANGUAGE(32022+day.weekday()),day=day.day,month=kodiutils.LANGUAGE(32028+day.month),year=day.year), params={"mode": "date", "id": str(day) })
        day = day - datetime.timedelta(days=1)
    for year in range( day.year, 1950, -1):
        kodiutils.addListItem(str(year), params={"mode": "year", "year": year })
    kodiutils.endScript()

def addYear(year):
    for month in range(1, 13):
        kodiutils.addListItem(kodiutils.LANGUAGE(32021).format(month=kodiutils.LANGUAGE(32028+month),year=year), params={"mode": "month", "month": month, "year": year })
    kodiutils.endScript()

def addMonth(month, year):
    for day in calendar.Calendar().itermonthdates(int(year), int(month)):
        kodiutils.addListItem(kodiutils.LANGUAGE(32020).format(dayname=kodiutils.LANGUAGE(32022+day.weekday()),day=day.day,month=kodiutils.LANGUAGE(32028+day.month),year=day.year), params={"mode": "date", "id": str(day) })
    kodiutils.endScript()

def addVideosItems(id='', type=1, page=1):
    kodiutils.setContent('videos')
    res = []
    loadAll = kodiutils.getSettingAsBool('loadAll')
    elPerPage = kodiutils.getSetting('elPerPage')
    if type==1:
        res = getProgramVideos(id, page, elPerPage, loadAll)
    elif type==2:
        res = getDateVideos(id)
    elif type==3:
        res = getCategoryVideos(id, elPerPage)
    elif type==4:
        res = getPicksVideos(elPerPage)
    if res:
        for ep in res:
            ep['mediatype']='video'
            kodiutils.addListItem(ep['title'], params={"id": ep['id'], "mode": "video" }, thumb=ep['image'], videoInfo=ep, isFolder=False)
        if type == 1 and (not loadAll) and len(getProgramVideos(id, int(page)+1, elPerPage))>0:
            kodiutils.addListItem(kodiutils.LANGUAGE(32011), params={"id": id, "mode": "program", "page": int(page) + 1})
    kodiutils.endScript()

def addSport():
    kodiutils.setContent('videos')
    liveSportList = getLiveSportLink()
    if liveSportList:
        for live in liveSportList:
            kodiutils.addListItem(live['title'], params={"mode": "watch_sport", "url": live['url'] }, thumb=live['thumbURL'], videoInfo={'mediatype': 'video'}, isFolder=False)
    kodiutils.endScript()

def getLiveSportLink():
        # Get list of live sport events.
        resp = webutils.getJson("https://event.api.swisstxt.ch/v1/events/rsi/live")
        # If there are no live events, return False.
        if not resp:
            return False

        # This is with an event only, to update when is available the structure of the live with multiple events.
        liveSports = []

        live1 = {}
        live1['url'] = resp['hls']
        live1['title'] = resp['title']
        live1['thumbURL'] = resp['imageUrl']

        liveSports.append(live1)
        return liveSports

def watchLiveSport(url):
    auth = getAuthString(url)
        kodiutils.setResolvedUrl('{url}?{auth}|User-Agent={ua}'.format(url=url,auth=auth,ua=urllib.quote_plus(useragent)))
    kodiutils.setResolvedUrl("",solved=False)

def watchVideo(id):
    links = getVideoLinks(id)
    neededQ = ""
    if kodiutils.getSettingAsBool('PreferHD'):
        neededQ = 'HD'
    else:
        neededQ = 'SD'
    fUrl = ''
    for url in links:
        if url['protocol'] == 'HTTP-HLS':
            fUrl = url['url']
            if url['quality'] == neededQ:
                break
    if fUrl:
        auth = getAuthString(fUrl)
        if auth:
            subs = []
            if kodiutils.getSettingAsBool('parseSubs'):
                for url in links:
                    if url['protocol'] == 'TTMLUrl': 
                        subpath = ttmlToSrt(url['url'])
                        if subpath:
                            subs.append(subpath)
            kodiutils.setResolvedUrl('{url}?{auth}|User-Agent={ua}'.format(url=fUrl,auth=auth,ua=urllib.quote_plus(useragent)), subs=subs)
    kodiutils.setResolvedUrl("",solved=False)

#main

params = staticutils.getParams()
if not params or 'mode' not in params:
    loadList()
else:
    if params['mode'] == "programs":
        addProgramsItems()
    elif params['mode'] == "program":
        addVideosItems(params['id'], type=1, page=params.get('page', 1))
    elif params['mode'] == "live":
        addLive()
    elif params['mode'] == "dates":
        addDates()
    elif params['mode'] == "date":
        addVideosItems(params['id'], type=2)
    elif params['mode'] == "year":
        addYear(params['year'])
    elif params['mode'] == "month":
        addMonth(params['month'], params['year'])
    elif params['mode'] == "categories":
        addCategories()
    elif params['mode'] == "category":
        addVideosItems(params['id'], type=3)
    elif params['mode'] == "picks":
        addVideosItems(type=4)
    elif params['mode'] == "video":
        watchVideo(params['id'])
    elif params['mode'] == "sport":
        addSport()
    elif params['mode'] == "watch_sport":
        watchLiveSport(params['url'])
    else:
        loadList()
phate89 commented 6 years ago

Ok I edited a little bit your code and pushed it.. if you can test it and give me feedback I'll push it to repo

riccardo1991 commented 6 years ago

Now there are no live events, so I get no results, I will test tomorrow when there is the football live.

PS Scrivo in inglese anche se sono italiano semmai qualcuno poi volesse intervenire o leggere 👍

riccardo1991 commented 6 years ago

I tried to work on this during my spare time today. I can get the live event data (title, url and image) but something is failing during the playback. I get the HLS link (.m3u8) as for the live channels and other videos. Can be that is there a different endpoint for getting the auth token for such events?

11:31:08.606 T:123145405353984   ERROR: ffmpeg[700006245000]: [hls,applehttp] Error when loading first segment 'https://srgssrgop20b-lh.akamaihd.net/i/enc20b_gop@141648/segment254871311_250_av-p.ts?sd=6&rebase=on&id=AgDAeUQCAhXy0T7NKFt89EFi22AxHYyDYGG3+OfVBb2P+btttNfcci6CeJax55T575lcbURwApaBlg%3d%3d'

11:31:08.630 T:123145405353984   DEBUG: Error, could not open file https://srgssrgop20b-lh.akamaihd.net/i/enc20b_gop@141648/master.m3u8??hdnts=exp=1529400666~acl=/i/enc20b_gop@141648/*~hmac=a8036cb598633af3da5baf9af1978b10dcc906d978d3f2d1e7dc76ac050ee067
riccardo1991 commented 6 years ago

So apparently it is working now. Don't know why didn't work yesterday. I also had a Kodi crush today reopening the stream while it was already playing. More investigation is needed but I think for now it's enough for an alpha/beta release.

schermata 2018-06-20 alle 17 39 42