Add support for playing from the Guide with IPTV Manager (#29)
This commit is contained in:
parent
b5e36047f5
commit
c8781424c1
@ -23,6 +23,7 @@ De volgende features worden ondersteund:
|
|||||||
* Programma's rechtstreeks afspelen vanuit de tv-gids
|
* Programma's rechtstreeks afspelen vanuit de tv-gids
|
||||||
* Doorzoeken van alle programma's
|
* Doorzoeken van alle programma's
|
||||||
* Afspelen van gerelateerde Youtube content
|
* Afspelen van gerelateerde Youtube content
|
||||||
|
* Integratie met [IPTV Manager](https://github.com/add-ons/service.iptv.manager)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
@ -148,6 +148,10 @@ msgctxt "#30717"
|
|||||||
msgid "This program is not available in the catalogue."
|
msgid "This program is not available in the catalogue."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30718"
|
||||||
|
msgid "There is no live stream available for {channel}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
|
||||||
### SETTINGS
|
### SETTINGS
|
||||||
msgctxt "#30800"
|
msgctxt "#30800"
|
||||||
|
@ -149,6 +149,11 @@ msgctxt "#30717"
|
|||||||
msgid "This program is not available in the catalogue."
|
msgid "This program is not available in the catalogue."
|
||||||
msgstr "Dit programma is niet beschikbaar in de catalogus."
|
msgstr "Dit programma is niet beschikbaar in de catalogus."
|
||||||
|
|
||||||
|
msgctxt "#30718"
|
||||||
|
msgid "There is no live stream available for {channel}."
|
||||||
|
msgstr "Er is geen live stream beschikbaar voor {channel}."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### SETTINGS
|
### SETTINGS
|
||||||
msgctxt "#30800"
|
msgctxt "#30800"
|
||||||
|
@ -106,8 +106,22 @@ def show_search(query=None):
|
|||||||
Search().show_search(query)
|
Search().show_search(query)
|
||||||
|
|
||||||
|
|
||||||
|
@routing.route('/play/live/<channel>')
|
||||||
|
def play_live(channel):
|
||||||
|
""" Play the requested item """
|
||||||
|
from resources.lib.modules.player import Player
|
||||||
|
Player().live(channel)
|
||||||
|
|
||||||
|
|
||||||
|
@routing.route('/play/epg/<channel>/<timestamp>')
|
||||||
|
def play_epg(channel, timestamp):
|
||||||
|
""" Play the requested item """
|
||||||
|
from resources.lib.modules.tvguide import TvGuide
|
||||||
|
TvGuide().play_epg_datetime(channel, timestamp)
|
||||||
|
|
||||||
|
|
||||||
@routing.route('/play/catalog/<uuid>')
|
@routing.route('/play/catalog/<uuid>')
|
||||||
def play(uuid):
|
def play_catalog(uuid):
|
||||||
""" Play the requested item """
|
""" Play the requested item """
|
||||||
from resources.lib.modules.player import Player
|
from resources.lib.modules.player import Player
|
||||||
Player().play(uuid)
|
Player().play(uuid)
|
||||||
@ -132,6 +146,20 @@ def metadata_update():
|
|||||||
Metadata().update()
|
Metadata().update()
|
||||||
|
|
||||||
|
|
||||||
|
@routing.route('/iptv/channels')
|
||||||
|
def iptv_channels():
|
||||||
|
""" Generate channel data for the Kodi PVR integration """
|
||||||
|
from resources.lib.modules.iptvmanager import IPTVManager
|
||||||
|
IPTVManager(int(routing.args['port'][0])).send_channels() # pylint: disable=too-many-function-args
|
||||||
|
|
||||||
|
|
||||||
|
@routing.route('/iptv/epg')
|
||||||
|
def iptv_epg():
|
||||||
|
""" Generate EPG data for the Kodi PVR integration """
|
||||||
|
from resources.lib.modules.iptvmanager import IPTVManager
|
||||||
|
IPTVManager(int(routing.args['port'][0])).send_epg() # pylint: disable=too-many-function-args
|
||||||
|
|
||||||
|
|
||||||
def run(params):
|
def run(params):
|
||||||
""" Run the routing plugin """
|
""" Run the routing plugin """
|
||||||
routing.run(params)
|
routing.run(params)
|
||||||
|
81
resources/lib/modules/iptvmanager.py
Normal file
81
resources/lib/modules/iptvmanager.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Implementation of IPTVManager class"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import, division, unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from resources.lib import kodiutils
|
||||||
|
from resources.lib.viervijfzes import CHANNELS
|
||||||
|
from resources.lib.viervijfzes.epg import EpgApi
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class IPTVManager:
|
||||||
|
"""Interface to IPTV Manager"""
|
||||||
|
|
||||||
|
def __init__(self, port):
|
||||||
|
"""Initialize IPTV Manager object"""
|
||||||
|
self.port = port
|
||||||
|
|
||||||
|
def via_socket(func): # pylint: disable=no-self-argument
|
||||||
|
"""Send the output of the wrapped function to socket"""
|
||||||
|
|
||||||
|
def send(self):
|
||||||
|
"""Decorator to send over a socket"""
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.connect(('127.0.0.1', self.port))
|
||||||
|
try:
|
||||||
|
sock.send(json.dumps(func())) # pylint: disable=not-callable
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
return send
|
||||||
|
|
||||||
|
@via_socket
|
||||||
|
def send_channels(): # pylint: disable=no-method-argument
|
||||||
|
"""Return JSON-STREAMS formatted information to IPTV Manager"""
|
||||||
|
streams = []
|
||||||
|
for key, channel in CHANNELS.items():
|
||||||
|
item = dict(
|
||||||
|
id=channel.get('iptv_id'),
|
||||||
|
name=channel.get('name'),
|
||||||
|
logo='special://home/addons/{addon}/resources/logos/{logo}'.format(addon=kodiutils.addon_id(), logo=channel.get('logo')),
|
||||||
|
preset=channel.get('iptv_preset'),
|
||||||
|
stream='plugin://plugin.video.viervijfzes/play/live/{channel}'.format(channel=key),
|
||||||
|
vod='plugin://plugin.video.viervijfzes/play/epg/{channel}/{{date}}'.format(channel=key)
|
||||||
|
)
|
||||||
|
|
||||||
|
streams.append(item)
|
||||||
|
|
||||||
|
return dict(version=1, streams=streams)
|
||||||
|
|
||||||
|
@via_socket
|
||||||
|
def send_epg(): # pylint: disable=no-method-argument
|
||||||
|
"""Return JSON-EPG formatted information to IPTV Manager"""
|
||||||
|
epg_api = EpgApi()
|
||||||
|
|
||||||
|
results = dict()
|
||||||
|
for key, channel in CHANNELS.items():
|
||||||
|
iptv_id = channel.get('iptv_id')
|
||||||
|
|
||||||
|
results[iptv_id] = []
|
||||||
|
for date in ['yesterday', 'today', 'tomorrow']:
|
||||||
|
epg = epg_api.get_epg(key, date)
|
||||||
|
|
||||||
|
results[iptv_id].extend([
|
||||||
|
dict(
|
||||||
|
start=program.start.isoformat(),
|
||||||
|
stop=(program.start + timedelta(seconds=program.duration)).isoformat(),
|
||||||
|
title=program.program_title,
|
||||||
|
description=program.description,
|
||||||
|
image=program.cover,
|
||||||
|
available=bool(program.video_url))
|
||||||
|
for program in epg if program.duration
|
||||||
|
])
|
||||||
|
|
||||||
|
return dict(version=1, epg=results)
|
@ -124,7 +124,7 @@ class Menu:
|
|||||||
else:
|
else:
|
||||||
# We have an UUID and can play this item directly
|
# We have an UUID and can play this item directly
|
||||||
# This is not preferred since we will lack metadata
|
# This is not preferred since we will lack metadata
|
||||||
path = kodiutils.url_for('play', uuid=item.uuid)
|
path = kodiutils.url_for('play_catalog', uuid=item.uuid)
|
||||||
|
|
||||||
return TitleItem(title=info_dict['title'],
|
return TitleItem(title=info_dict['title'],
|
||||||
path=path,
|
path=path,
|
||||||
|
@ -7,6 +7,7 @@ import logging
|
|||||||
|
|
||||||
from resources.lib import kodiutils
|
from resources.lib import kodiutils
|
||||||
from resources.lib.modules.menu import Menu
|
from resources.lib.modules.menu import Menu
|
||||||
|
from resources.lib.viervijfzes import CHANNELS
|
||||||
from resources.lib.viervijfzes.auth import AuthApi
|
from resources.lib.viervijfzes.auth import AuthApi
|
||||||
from resources.lib.viervijfzes.auth_awsidp import InvalidLoginException, AuthenticationException
|
from resources.lib.viervijfzes.auth_awsidp import InvalidLoginException, AuthenticationException
|
||||||
from resources.lib.viervijfzes.content import ContentApi, UnavailableException, GeoblockedException
|
from resources.lib.viervijfzes.content import ContentApi, UnavailableException, GeoblockedException
|
||||||
@ -25,6 +26,15 @@ class Player:
|
|||||||
# Workaround for Raspberry Pi 3 and older
|
# Workaround for Raspberry Pi 3 and older
|
||||||
kodiutils.set_global_setting('videoplayer.useomxplayer', True)
|
kodiutils.set_global_setting('videoplayer.useomxplayer', True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def live(channel):
|
||||||
|
""" Play the live channel.
|
||||||
|
:type channel: string
|
||||||
|
"""
|
||||||
|
channel_name = CHANNELS.get(channel, dict(name=channel))
|
||||||
|
kodiutils.ok_dialog(message=kodiutils.localize(30718, channel=channel_name.get('name'))) # There is no live stream available for {channel}.
|
||||||
|
kodiutils.end_of_directory()
|
||||||
|
|
||||||
def play_from_page(self, channel, path):
|
def play_from_page(self, channel, path):
|
||||||
""" Play the requested item.
|
""" Play the requested item.
|
||||||
:type channel: string
|
:type channel: string
|
||||||
@ -34,6 +44,10 @@ class Player:
|
|||||||
episode = self._api.get_episode(channel, path)
|
episode = self._api.get_episode(channel, path)
|
||||||
resolved_stream = None
|
resolved_stream = None
|
||||||
|
|
||||||
|
if episode is None:
|
||||||
|
kodiutils.ok_dialog(message=kodiutils.localize(30712))
|
||||||
|
return
|
||||||
|
|
||||||
if episode.stream:
|
if episode.stream:
|
||||||
# We already have a resolved stream. Nice!
|
# We already have a resolved stream. Nice!
|
||||||
# We don't need credentials for these streams.
|
# We don't need credentials for these streams.
|
||||||
|
@ -8,6 +8,7 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
from resources.lib import kodiutils
|
from resources.lib import kodiutils
|
||||||
from resources.lib.kodiutils import TitleItem
|
from resources.lib.kodiutils import TitleItem
|
||||||
|
from resources.lib.modules.player import Player
|
||||||
from resources.lib.viervijfzes import STREAM_DICT
|
from resources.lib.viervijfzes import STREAM_DICT
|
||||||
from resources.lib.viervijfzes.content import UnavailableException
|
from resources.lib.viervijfzes.content import UnavailableException
|
||||||
from resources.lib.viervijfzes.epg import EpgApi
|
from resources.lib.viervijfzes.epg import EpgApi
|
||||||
@ -171,9 +172,13 @@ class TvGuide:
|
|||||||
"""
|
"""
|
||||||
broadcast = self._epg.get_broadcast(channel, timestamp)
|
broadcast = self._epg.get_broadcast(channel, timestamp)
|
||||||
if not broadcast:
|
if not broadcast:
|
||||||
kodiutils.ok_dialog(heading=kodiutils.localize(30711), message=kodiutils.localize(30713)) # The requested video was not found in the guide.
|
kodiutils.ok_dialog(message=kodiutils.localize(30713)) # The requested video was not found in the guide.
|
||||||
kodiutils.end_of_directory()
|
kodiutils.end_of_directory()
|
||||||
return
|
return
|
||||||
|
|
||||||
kodiutils.container_update(
|
if not broadcast.video_url:
|
||||||
kodiutils.url_for('play', uuid=broadcast.video_url))
|
kodiutils.ok_dialog(message=kodiutils.localize(30712)) # The video is unavailable and can't be played right now.
|
||||||
|
kodiutils.end_of_directory()
|
||||||
|
return
|
||||||
|
|
||||||
|
Player().play_from_page(channel, broadcast.video_url)
|
||||||
|
@ -11,6 +11,8 @@ CHANNELS = OrderedDict([
|
|||||||
logo='vier.png',
|
logo='vier.png',
|
||||||
background='vier-background.jpg',
|
background='vier-background.jpg',
|
||||||
studio_icon='vier',
|
studio_icon='vier',
|
||||||
|
iptv_preset=4,
|
||||||
|
iptv_id='vier.be',
|
||||||
youtube=[
|
youtube=[
|
||||||
dict(
|
dict(
|
||||||
label='VIER / VIJF',
|
label='VIER / VIJF',
|
||||||
@ -25,6 +27,8 @@ CHANNELS = OrderedDict([
|
|||||||
logo='vijf.png',
|
logo='vijf.png',
|
||||||
background='vijf-background.jpg',
|
background='vijf-background.jpg',
|
||||||
studio_icon='vijf',
|
studio_icon='vijf',
|
||||||
|
iptv_preset=5,
|
||||||
|
iptv_id='vijf.be',
|
||||||
youtube=[
|
youtube=[
|
||||||
dict(
|
dict(
|
||||||
label='VIER / VIJF',
|
label='VIER / VIJF',
|
||||||
@ -39,6 +43,8 @@ CHANNELS = OrderedDict([
|
|||||||
logo='zes.png',
|
logo='zes.png',
|
||||||
background='zes-background.jpg',
|
background='zes-background.jpg',
|
||||||
studio_icon='zes',
|
studio_icon='zes',
|
||||||
|
iptv_preset=6,
|
||||||
|
iptv_id='zes.be',
|
||||||
youtube=[],
|
youtube=[],
|
||||||
))
|
))
|
||||||
])
|
])
|
||||||
|
@ -7,7 +7,8 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import dateutil
|
import dateutil.parser
|
||||||
|
import dateutil.tz
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
_LOGGER = logging.getLogger('epg-api')
|
_LOGGER = logging.getLogger('epg-api')
|
||||||
@ -90,8 +91,8 @@ class EpgApi:
|
|||||||
duration = int(data.get('duration')) if data.get('duration') else None
|
duration = int(data.get('duration')) if data.get('duration') else None
|
||||||
|
|
||||||
# Check if this broadcast is currently airing
|
# Check if this broadcast is currently airing
|
||||||
timestamp = datetime.now()
|
timestamp = datetime.now().replace(tzinfo=dateutil.tz.gettz('CET'))
|
||||||
start = datetime.fromtimestamp(data.get('timestamp'))
|
start = datetime.fromtimestamp(data.get('timestamp')).replace(tzinfo=dateutil.tz.gettz('CET'))
|
||||||
if duration:
|
if duration:
|
||||||
airing = bool(start <= timestamp < (start + timedelta(seconds=duration)))
|
airing = bool(start <= timestamp < (start + timedelta(seconds=duration)))
|
||||||
else:
|
else:
|
||||||
@ -132,7 +133,7 @@ class EpgApi:
|
|||||||
:rtype: EpgProgram
|
:rtype: EpgProgram
|
||||||
"""
|
"""
|
||||||
# Parse to a real datetime
|
# Parse to a real datetime
|
||||||
timestamp = dateutil.parser.parse(timestamp)
|
timestamp = dateutil.parser.parse(timestamp).replace(tzinfo=dateutil.tz.gettz('CET'))
|
||||||
|
|
||||||
# Load guide info for this date
|
# Load guide info for this date
|
||||||
programs = self.get_epg(channel=channel, date=timestamp.strftime('%Y-%m-%d'))
|
programs = self.get_epg(channel=channel, date=timestamp.strftime('%Y-%m-%d'))
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||||
<settings>
|
<settings>
|
||||||
<setting id="metadata_last_updated" visible="false"/>
|
<setting id="metadata_last_updated" visible="false"/>
|
||||||
|
<setting id="iptv.enabled" default="true" visible="false"/>
|
||||||
|
<setting id="iptv.channels_uri" default="plugin://plugin.video.viervijfzes/iptv/channels" visible="false"/>
|
||||||
|
<setting id="iptv.epg_uri" default="plugin://plugin.video.viervijfzes/iptv/epg" visible="false"/>
|
||||||
<category label="30800"> <!-- Credentials -->
|
<category label="30800"> <!-- Credentials -->
|
||||||
<setting label="30801" type="lsep"/> <!-- Credentials -->
|
<setting label="30801" type="lsep"/> <!-- Credentials -->
|
||||||
<setting label="30803" type="text" id="username"/>
|
<setting label="30803" type="text" id="username"/>
|
||||||
|
Loading…
Reference in New Issue
Block a user