Compare commits
57 Commits
Author | SHA1 | Date |
---|---|---|
cralin | 0f064703bf | 5 months ago |
cralin | 6794c8f767 | 6 months ago |
cralin | fb4a044e2d | 7 months ago |
cralin | fa90d2df81 | 10 months ago |
cralin | c90d58c6d6 | 10 months ago |
cralin | 85fd6e3b81 | 1 year ago |
cralin | 7d53a929a3 | 1 year ago |
cralin | 41e76c936c | 1 year ago |
cralin | f8c13c795f | 1 year ago |
cralin | f05c54b860 | 1 year ago |
cralin | 72d811ff21 | 1 year ago |
cralin | cfb83e6974 | 1 year ago |
cralin | 1fd587dfeb | 1 year ago |
cralin | ac13ec4edb | 1 year ago |
cralin | c2c9e1454c | 1 year ago |
cralin | a3b7be37dc | 1 year ago |
cralin | e28f6c766f | 1 year ago |
cralin | a4aeed662a | 1 year ago |
cralin | f707ca2965 | 2 years ago |
cralin | d4f8eff6d1 | 2 years ago |
cralin | eb44b4c532 | 2 years ago |
cralin | fe5ea8191c | 2 years ago |
cralin | 200561c72c | 2 years ago |
cralin | f3af8e011f | 2 years ago |
cralin | 4f3d77d625 | 2 years ago |
cralin | 89cf0521be | 2 years ago |
cralin | c52262f85e | 2 years ago |
cralin | 253f9463f5 | 2 years ago |
cralin | 3eeaa8768f | 2 years ago |
cralin | eff2f88161 | 2 years ago |
cralin | b900d72769 | 2 years ago |
cralin | 0ef76b3bc9 | 2 years ago |
cralin | 67dcd0daed | 2 years ago |
cralin | ee457f3b0d | 2 years ago |
cralin | 65d0c95417 | 2 years ago |
cralin | 610e3b894f | 3 years ago |
cralin | e4be641304 | 3 years ago |
cralin | 91d9118c7e | 3 years ago |
cralin | 2bf67432fd | 3 years ago |
cralin | 37edacef5f | 3 years ago |
cralin | df48daeb74 | 3 years ago |
cralin | ee28f338b1 | 3 years ago |
cralin | d2c1b57e30 | 3 years ago |
cralin | 57eb275888 | 3 years ago |
cralin | a41e0463b8 | 3 years ago |
cralin | af00a5c066 | 3 years ago |
cralin | 950cc82b9f | 3 years ago |
cralin | 175c9d7611 | 3 years ago |
cralin | d4df91a2af | 3 years ago |
cralin | 8b471e76e9 | 3 years ago |
cralin | 20a1cabcc2 | 3 years ago |
cralin | d43de52002 | 3 years ago |
cralin | 377e2f43b8 | 3 years ago |
cralin | 6d8aece0c9 | 3 years ago |
cralin | 1d3d044e99 | 3 years ago |
cralin | 78d8813993 | 3 years ago |
cralin | 0d056e43bf | 3 years ago |
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
-->
|
||||
|
||||
<addon id="plugin.video.TVOnline.ro" provider-name="cralin" name="TVOnline.ro" version="19.3.12">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="3.0.0"/>
|
||||
<import addon="script.module.requests" version="2.22.0"/>
|
||||
<import addon="script.module.inputstreamhelper" version="0.4.7"/>
|
||||
</requires>
|
||||
<extension point="xbmc.python.pluginsource" library="main.py">
|
||||
<provides>video</provides>
|
||||
</extension>
|
||||
<extension point="xbmc.service" library="service.py"/>
|
||||
<extension point="xbmc.addon.metadata">
|
||||
<summary lang="en">Watch romanian live TV channels.</summary>
|
||||
<description lang="en_GB">
|
||||
Kodi addon for accessing live TV channels from various romanian online platforms.
|
||||
|
||||
Supported platforms:
|
||||
- DigiOnline.ro
|
||||
- voyo.protv.ro
|
||||
- tvrplus.ro
|
||||
- primaplay.ro
|
||||
|
||||
For each supported platform, user registration and login credentials are required in order to access the media content.
|
||||
|
||||
Visit https://www.digiromania.ro for more details.
|
||||
Visit https://voyo.protv.ro for more details.
|
||||
Visit https://www.tvrplus.ro for more details.
|
||||
Visit https://www.primaplay.ro for more details.
|
||||
</description>
|
||||
<disclaimer lang="en_GB">
|
||||
The author is not connected to or in any other way affiliated with Kodi, Team Kodi, the XBMC foundation or any of the online platforms used by this add-on.
|
||||
</disclaimer>
|
||||
<assets>
|
||||
<icon>resources/media/icon.png</icon>
|
||||
<fanart>resources/media/fanart.jpg</fanart>
|
||||
<screenshot></screenshot>
|
||||
</assets>
|
||||
<news>
|
||||
|
||||
</news>
|
||||
</extension>
|
||||
</addon>
|
||||
|
@ -0,0 +1,383 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
|
||||
import sys
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import parse_qsl
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
import xbmcvfs
|
||||
import xbmcaddon
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import requests
|
||||
import logging
|
||||
import logging.handlers
|
||||
import resources.lib.common.vars as common_vars
|
||||
import resources.lib.common.functions as common_functions
|
||||
import resources.lib.digionline.functions as digionline_functions
|
||||
import resources.lib.voyo.functions as voyo_functions
|
||||
import resources.lib.primaplay.functions as primaplay_functions
|
||||
import resources.lib.tvrplus.functions as tvrplus_functions
|
||||
import http.cookiejar
|
||||
|
||||
|
||||
__SystemBuildVersion__ = xbmc.getInfoLabel('System.BuildVersion')
|
||||
__SystemBuildDate__ = xbmc.getInfoLabel('System.BuildDate')
|
||||
|
||||
# Kodi uses the following sys.argv arguments:
|
||||
# [0] - The base URL for this add-on, e.g. 'plugin://plugin.video.demo1/'.
|
||||
# [1] - The process handle for this add-on, as a numeric string.
|
||||
# [2] - The query string passed to this add-on, e.g. '?foo=bar&baz=quux'.
|
||||
|
||||
# Get the plugin url in plugin:// notation.
|
||||
common_vars.__plugin_url__ = sys.argv[0]
|
||||
|
||||
# Get the plugin handle as an integer number.
|
||||
common_vars.__handle__ = sys.argv[1]
|
||||
|
||||
MyAddon = xbmcaddon.Addon(id=common_vars.__AddonID__)
|
||||
|
||||
# The version of the runing Addon
|
||||
__AddonVersion__ = MyAddon.getAddonInfo('version')
|
||||
|
||||
# Initialize the Addon data directory
|
||||
MyAddon_DataDir = xbmcvfs.translatePath(MyAddon.getAddonInfo('profile'))
|
||||
if not os.path.exists(MyAddon_DataDir):
|
||||
os.makedirs(MyAddon_DataDir)
|
||||
|
||||
# Read the user preferences stored in the addon configuration
|
||||
common_functions.read_AddonSettings(MyAddon, common_vars.__ServiceID__)
|
||||
|
||||
# Log file name
|
||||
addon_logfile_name = os.path.join(MyAddon_DataDir, common_vars.__AddonLogFilename__)
|
||||
|
||||
# Configure logging
|
||||
if common_vars.__config_DebugEnabled__ == 'true':
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
#logger = logging.getLogger('plugin.video.DigiOnline.log')
|
||||
common_vars.__logger__ = logging.getLogger(common_vars.__AddonID__)
|
||||
common_vars.__logger__.propagate = False
|
||||
|
||||
# Create a rotating file handler
|
||||
# TODO: Extend the settings.xml to allow the user to choose the values for maxBytes and backupCount
|
||||
# TODO: Set the values for maxBytes and backupCount to values defined in the addon settings
|
||||
handler = logging.handlers.RotatingFileHandler(addon_logfile_name, mode='a', maxBytes=104857600, backupCount=2, encoding='utf-8', delay=False)
|
||||
if common_vars.__config_DebugEnabled__ == 'true':
|
||||
handler.setLevel(logging.DEBUG)
|
||||
else:
|
||||
handler.setLevel(logging.INFO)
|
||||
|
||||
# Create a logging format to be used
|
||||
formatter = logging.Formatter('%(asctime)s %(funcName)s %(levelname)s: %(message)s', datefmt='%Y%m%d_%H%M%S')
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
# add the file handler to the common_vars.__logger__
|
||||
common_vars.__logger__.addHandler(handler)
|
||||
|
||||
# Initialize the CookieJar variable
|
||||
digionline_functions.init_AddonCookieJar(common_vars.__AddonID__, MyAddon_DataDir)
|
||||
voyo_functions.init_AddonCookieJar(common_vars.__AddonID__, MyAddon_DataDir)
|
||||
primaplay_functions.init_AddonCookieJar(common_vars.__AddonID__, MyAddon_DataDir)
|
||||
tvrplus_functions.init_AddonCookieJar(common_vars.__AddonID__, MyAddon_DataDir)
|
||||
|
||||
# Start a new requests sessions and initialize the cookiejar
|
||||
common_vars.__digionline_Session__ = requests.Session()
|
||||
common_vars.__voyo_Session__ = requests.Session()
|
||||
common_vars.__primaplay_Session__ = requests.Session()
|
||||
common_vars.__tvrplus_Session__ = requests.Session()
|
||||
|
||||
# Put all session cookeis in the cookiejar
|
||||
common_vars.__digionline_Session__.cookies = common_vars.__digionline_CookieJar__
|
||||
common_vars.__voyo_Session__.cookies = common_vars.__voyo_CookieJar__
|
||||
common_vars.__primaplay_Session__.cookies = common_vars.__primaplay_CookieJar__
|
||||
common_vars.__tvrplus_Session__.cookies = common_vars.__tvrplus_CookieJar__
|
||||
|
||||
def list_enabled_accounts():
|
||||
####
|
||||
#
|
||||
# Create in the Kodi interface a virtual folder containing the list of accounts enabled in the add-on settings.
|
||||
#
|
||||
####
|
||||
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Set plugin category.
|
||||
xbmcplugin.setPluginCategory(int(common_vars.__handle__), 'TVOnline.ro')
|
||||
|
||||
# Set plugin content.
|
||||
xbmcplugin.setContent(int(common_vars.__handle__), 'videos')
|
||||
|
||||
# digionline.ro
|
||||
if common_vars.__config_digionline_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'digionline.ro\' ==> Enabled')
|
||||
|
||||
# Create a list item with a text label and a thumbnail image.
|
||||
list_item = xbmcgui.ListItem(label='digionline.ro')
|
||||
|
||||
# Set additional info for the list item.
|
||||
# For available properties see https://codedocs.xyz/xbmc/xbmc/group__python__xbmcgui__listitem.html#ga0b71166869bda87ad744942888fb5f14
|
||||
# 'mediatype' is needed for a skin to display info for this ListItem correctly.
|
||||
list_item.setInfo('video', {'title': 'digionline.ro',
|
||||
'mediatype': 'video'})
|
||||
|
||||
# Create a URL for a plugin recursive call.
|
||||
# Example: plugin://plugin.video.example/?action=listing&account=digionline.ro&behaveas=phone
|
||||
url = common_functions.get_url(account='digionline.ro', behaveas=common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__], action='list_categories')
|
||||
common_vars.__logger__.debug('URL for plugin recursive call: ' + url)
|
||||
|
||||
# This means that this item opens a sub-list of lower level items.
|
||||
is_folder = True
|
||||
|
||||
# Add our item to the Kodi virtual folder listing.
|
||||
xbmcplugin.addDirectoryItem(int(common_vars.__handle__), url, list_item, is_folder)
|
||||
|
||||
else:
|
||||
common_vars.__logger__.debug('\'digionline.ro\' ==> Disabled')
|
||||
|
||||
# voyo.ro
|
||||
if common_vars.__config_voyo_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'voyo.ro\' ==> Enabled')
|
||||
|
||||
# Create a list item with a text label and a thumbnail image.
|
||||
list_item = xbmcgui.ListItem(label='voyo.ro')
|
||||
|
||||
# Set additional info for the list item.
|
||||
# For available properties see https://codedocs.xyz/xbmc/xbmc/group__python__xbmcgui__listitem.html#ga0b71166869bda87ad744942888fb5f14
|
||||
# 'mediatype' is needed for a skin to display info for this ListItem correctly.
|
||||
list_item.setInfo('video', {'title': 'voyo.ro',
|
||||
'mediatype': 'video'})
|
||||
|
||||
# Create a URL for a plugin recursive call.
|
||||
# Example: plugin://plugin.video.example/?action=listing&account=digionline.ro
|
||||
url = common_functions.get_url(action='list_channels', account='voyo.ro')
|
||||
common_vars.__logger__.debug('URL for plugin recursive call: ' + url)
|
||||
|
||||
# This means that this item opens a sub-list of lower level items.
|
||||
is_folder = True
|
||||
|
||||
# Add our item to the Kodi virtual folder listing.
|
||||
xbmcplugin.addDirectoryItem(int(common_vars.__handle__), url, list_item, is_folder)
|
||||
|
||||
else:
|
||||
common_vars.__logger__.debug('\'voyo.ro\' ==> Disabled')
|
||||
|
||||
# primaplay.ro
|
||||
if common_vars.__config_primaplay_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'primaplay.ro\' ==> Enabled')
|
||||
|
||||
# Create a list item with a text label and a thumbnail image.
|
||||
list_item = xbmcgui.ListItem(label='primaplay.ro')
|
||||
|
||||
# Set additional info for the list item.
|
||||
# For available properties see https://codedocs.xyz/xbmc/xbmc/group__python__xbmcgui__listitem.html#ga0b71166869bda87ad744942888fb5f14
|
||||
# 'mediatype' is needed for a skin to display info for this ListItem correctly.
|
||||
list_item.setInfo('video', {'title': 'primaplay.ro',
|
||||
'mediatype': 'video'})
|
||||
|
||||
# Create a URL for a plugin recursive call.
|
||||
# Example: plugin://plugin.video.example/?action=listing&account=digionline.ro
|
||||
url = common_functions.get_url(action='list_channels', account='primaplay.ro')
|
||||
common_vars.__logger__.debug('URL for plugin recursive call: ' + url)
|
||||
|
||||
# This means that this item opens a sub-list of lower level items.
|
||||
is_folder = True
|
||||
|
||||
# Add our item to the Kodi virtual folder listing.
|
||||
xbmcplugin.addDirectoryItem(int(common_vars.__handle__), url, list_item, is_folder)
|
||||
|
||||
else:
|
||||
common_vars.__logger__.debug('\'primaplay.ro\' ==> Disabled')
|
||||
|
||||
# tvrplus.ro
|
||||
if common_vars.__config_tvrplus_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'tvrplus.ro\' ==> Enabled')
|
||||
|
||||
# Create a list item with a text label and a thumbnail image.
|
||||
list_item = xbmcgui.ListItem(label='tvrplus.ro')
|
||||
|
||||
# Set additional info for the list item.
|
||||
# For available properties see https://codedocs.xyz/xbmc/xbmc/group__python__xbmcgui__listitem.html#ga0b71166869bda87ad744942888fb5f14
|
||||
# 'mediatype' is needed for a skin to display info for this ListItem correctly.
|
||||
list_item.setInfo('video', {'title': 'tvrplus.ro',
|
||||
'mediatype': 'video'})
|
||||
|
||||
# Create a URL for a plugin recursive call.
|
||||
# Example: plugin://plugin.video.example/?action=listing&account=digionline.ro
|
||||
url = common_functions.get_url(action='list_channels', account='tvrplus.ro')
|
||||
common_vars.__logger__.debug('URL for plugin recursive call: ' + url)
|
||||
|
||||
# This means that this item opens a sub-list of lower level items.
|
||||
is_folder = True
|
||||
|
||||
# Add our item to the Kodi virtual folder listing.
|
||||
xbmcplugin.addDirectoryItem(int(common_vars.__handle__), url, list_item, is_folder)
|
||||
|
||||
else:
|
||||
common_vars.__logger__.debug('\'tvrplus.ro\' ==> Disabled')
|
||||
|
||||
|
||||
# Add a sort method for the virtual folder items (alphabetically, ignore articles)
|
||||
# See: https://romanvm.github.io/Kodistubs/_autosummary/xbmcplugin.html
|
||||
xbmcplugin.addSortMethod(int(common_vars.__handle__), xbmcplugin.SORT_METHOD_LABEL)
|
||||
|
||||
# Finish creating a virtual folder.
|
||||
xbmcplugin.endOfDirectory(int(common_vars.__handle__))
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def router(paramstring):
|
||||
####
|
||||
#
|
||||
# Router function that calls other functions depending on the provided paramster
|
||||
#
|
||||
# Parameters:
|
||||
# paramstring: URL encoded plugin paramstring
|
||||
#
|
||||
####
|
||||
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Parse a URL-encoded paramstring to the dictionary of {<parameter>: <value>} elements
|
||||
params = dict(parse_qsl(paramstring))
|
||||
|
||||
# Check the parameters passed to the plugin
|
||||
if params:
|
||||
if params['action'] == 'list_categories':
|
||||
# Display the list of categories in a provided account.
|
||||
|
||||
# digionline.ro
|
||||
if params['account'] == 'digionline.ro':
|
||||
if common_vars.__config_digionline_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'digionline.ro\' ==> Enabled')
|
||||
digionline_functions.digionline__listCategories(params['behaveas'], common_vars.__AddonID__, common_vars.__digionline_Session__, MyAddon_DataDir)
|
||||
else:
|
||||
common_vars.__logger__.debug('\'digionline.ro\' ==> Disabled')
|
||||
|
||||
elif params['action'] == 'list_channels':
|
||||
# Display the list of channels in the provided category from provided account.
|
||||
|
||||
# digionline.ro
|
||||
if params['account'] == 'digionline.ro':
|
||||
if common_vars.__config_digionline_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'digionline.ro\' ==> Enabled')
|
||||
if params['behaveas'] == "Phone":
|
||||
digionline_functions.digionline__phone_listChannels(params['category_name'], params['channel_list'], common_vars.__AddonID__, common_vars.__digionline_Session__, MyAddon_DataDir)
|
||||
if params['behaveas'] == "TV":
|
||||
digionline_functions.digionline__tv_listChannels(params['id_category'], params['category_name'], common_vars.__AddonID__, common_vars.__digionline_Session__, MyAddon_DataDir)
|
||||
else:
|
||||
common_vars.__logger__.debug('\'digionline.ro\' ==> Disabled')
|
||||
|
||||
# voyo.ro
|
||||
if params['account'] == 'voyo.ro':
|
||||
if common_vars.__config_voyo_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'voyo.ro\' ==> Enabled')
|
||||
voyo_functions.list_channels(common_vars.__AddonID__, common_vars.__voyo_CookieJar__, common_vars.__voyo_Session__, MyAddon_DataDir)
|
||||
else:
|
||||
common_vars.__logger__.debug('\'voyo.ro\' ==> Disabled')
|
||||
|
||||
# primaplay.ro
|
||||
if params['account'] == 'primaplay.ro':
|
||||
if common_vars.__config_primaplay_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'primaplay.ro\' ==> Enabled')
|
||||
primaplay_functions.list_channels(common_vars.__AddonID__, common_vars.__primaplay_CookieJar__, common_vars.__primaplay_Session__, MyAddon_DataDir)
|
||||
else:
|
||||
common_vars.__logger__.debug('\'primaplay.ro\' ==> Disabled')
|
||||
|
||||
# tvrplus.ro
|
||||
if params['account'] == 'tvrplus.ro':
|
||||
if common_vars.__config_tvrplus_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'tvrplus.ro\' ==> Enabled')
|
||||
tvrplus_functions.list_channels(common_vars.__AddonID__, common_vars.__tvrplus_CookieJar__, common_vars.__tvrplus_Session__, MyAddon_DataDir)
|
||||
else:
|
||||
common_vars.__logger__.debug('\'tvrplus.ro\' ==> Disabled')
|
||||
|
||||
|
||||
elif params['action'] == 'play':
|
||||
# Play a video from the provided URL.
|
||||
|
||||
# digionline.ro
|
||||
if params['account'] == 'digionline.ro':
|
||||
if common_vars.__config_digionline_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'digionline.ro\' ==> Enabled')
|
||||
digionline_functions.digionline__playVideo(params['behaveas'], params['channel_id'], common_vars.__AddonID__, common_vars.__digionline_Session__, MyAddon_DataDir)
|
||||
else:
|
||||
common_vars.__logger__.debug('\'digionline.ro\' ==> Disabled')
|
||||
xbmcgui.Dialog().ok('\'Digionline.ro\' not enabled', 'The credentials for this media source are not enabled.')
|
||||
|
||||
# voyo.ro
|
||||
if params['account'] == 'voyo.ro':
|
||||
if common_vars.__config_voyo_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'voyo.ro\' ==> Enabled')
|
||||
voyo_functions.play_video(params['channel_endpoint'], common_vars.__AddonID__, common_vars.__voyo_CookieJar__, common_vars.__voyo_Session__, MyAddon_DataDir)
|
||||
else:
|
||||
common_vars.__logger__.debug('\'voyo.ro\' ==> Disabled')
|
||||
xbmcgui.Dialog().ok('\'voyo.ro\' not enabled', 'The credentials for this media source are not enabled.')
|
||||
|
||||
# primaplay.ro
|
||||
if params['account'] == 'primaplay.ro':
|
||||
if common_vars.__config_primaplay_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'primaplay.ro\' ==> Enabled')
|
||||
primaplay_functions.play_video(params['channel_endpoint'], common_vars.__AddonID__, common_vars.__primaplay_CookieJar__, common_vars.__primaplay_Session__, MyAddon_DataDir)
|
||||
else:
|
||||
common_vars.__logger__.debug('\'primaplay.ro\' ==> Disabled')
|
||||
xbmcgui.Dialog().ok('\'primaplay.ro\' not enabled', 'The credentials for this media source are not enabled.')
|
||||
|
||||
# tvrplus.ro
|
||||
if params['account'] == 'tvrplus.ro':
|
||||
if common_vars.__config_tvrplus_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('\'tvrplus.ro\' ==> Enabled')
|
||||
tvrplus_functions.play_video(params['channel_endpoint'], common_vars.__AddonID__, common_vars.__tvrplus_CookieJar__, common_vars.__tvrplus_Session__, MyAddon_DataDir)
|
||||
else:
|
||||
common_vars.__logger__.debug('\'tvrplus.ro\' ==> Disabled')
|
||||
xbmcgui.Dialog().ok('\'tvrplus.ro\' not enabled', 'This media source is not enabled.')
|
||||
|
||||
else:
|
||||
# Raise an exception if the provided paramstring does not contain a supported action
|
||||
# This helps to catch coding errors,
|
||||
raise ValueError('Invalid paramstring: {0}!'.format(paramstring))
|
||||
else:
|
||||
# If the plugin is called from Kodi UI without any parameters:
|
||||
|
||||
# Display the list of accounts enabled in the add-on
|
||||
list_enabled_accounts()
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
common_vars.__logger__.debug('=== SYSINFO === Addon version: ' + str(__AddonVersion__))
|
||||
common_vars.__logger__.debug('=== SYSINFO === System.BuildVersion: ' + str(__SystemBuildVersion__))
|
||||
common_vars.__logger__.debug('=== SYSINFO === System.BuildDate: ' + str(__SystemBuildDate__))
|
||||
common_vars.__logger__.debug('=== ADDONINFO === Called with parameters: ' + str(parse_qsl(sys.argv[2][1:])))
|
||||
|
||||
# Read the user preferences stored in the addon configuration
|
||||
common_functions.read_AddonSettings(MyAddon, common_vars.__AddonID__)
|
||||
|
||||
# Call the router function and pass the plugin call parameters to it.
|
||||
router(sys.argv[2][1:])
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
@ -0,0 +1,18 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
@ -0,0 +1,191 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: plugin.video.TVOnline.ro\n"
|
||||
"Report-Msgid-Bugs-To: https://github.com/cralin/plugin.video.TVOnline.ro\n"
|
||||
"POT-Creation-Date: 2022-11-17 20:00+0000\n"
|
||||
"PO-Revision-Date: 2022-11-17 20:00+0000\n"
|
||||
"Last-Translator: cralin <>\n"
|
||||
"Language-Team: English\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: en\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
|
||||
msgctxt "#30101"
|
||||
msgid "Accounts"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30102"
|
||||
msgid "DigiOnline.ro"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30103"
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30104"
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30105"
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30106"
|
||||
msgid "Device Manufacturer"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30107"
|
||||
msgid "Device Model"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30108"
|
||||
msgid "Android Version"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30109"
|
||||
msgid "voyo.ro"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30110"
|
||||
msgid "General Settings"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30111"
|
||||
msgid "Display"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30112"
|
||||
msgid "Show program title in channel list"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30113"
|
||||
msgid "Enable debug logging"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30114"
|
||||
msgid "Simple PVR integration"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30115"
|
||||
msgid "Data file(s)"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30116"
|
||||
msgid "Update m3u file, daily at"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30117"
|
||||
msgid "Update XML file, daily at"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30118"
|
||||
msgid "Behave as"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30119"
|
||||
msgid "Device Model"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30120"
|
||||
msgid "Android Version"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30121"
|
||||
msgid "Platform"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30122"
|
||||
msgid "Phone App"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30123"
|
||||
msgid "TV App"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30124"
|
||||
msgid "tvrplus.ro"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "primaplay.ro"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "30125 Template String"
|
||||
msgstr ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
@ -0,0 +1,18 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
@ -0,0 +1,136 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
|
||||
import sys
|
||||
from urllib.parse import urlencode
|
||||
import os
|
||||
import logging
|
||||
import http.cookiejar
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import resources.lib.common.vars as common_vars
|
||||
|
||||
|
||||
def read_AddonSettings(__MyAddon__, NAME):
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Read the user preferences stored in the addon configuration
|
||||
|
||||
# Accounts
|
||||
# digionline.ro
|
||||
common_vars.__config_digionline_Enabled__ = __MyAddon__.getSetting('digionline_Enabled')
|
||||
common_vars.__logger__.debug('[ Addon settings ] digionline_Enabled = ' + str(common_vars.__config_digionline_Enabled__))
|
||||
|
||||
common_vars.__config_digionline_Username__ = __MyAddon__.getSetting('digionline_Username')
|
||||
common_vars.__config_digionline_Password__ = __MyAddon__.getSetting('digionline_Password')
|
||||
|
||||
common_vars.__config_digionline_BehaveAs__ = __MyAddon__.getSetting('digionline_BehaveAs')
|
||||
common_vars.__logger__.debug('[ Addon settings ] digionline_BehaveAs = ' + common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__])
|
||||
|
||||
common_vars.__config_digionline_PhoneDeviceManufacturer__ = __MyAddon__.getSetting('digionline_PhoneDeviceManufacturer')
|
||||
common_vars.__logger__.debug('[ Addon settings ] digionline_PhoneDeviceManufacturer = ' + str(common_vars.__config_digionline_PhoneDeviceManufacturer__))
|
||||
|
||||
common_vars.__config_digionline_PhoneDeviceModel__ = __MyAddon__.getSetting('digionline_PhoneDeviceModel')
|
||||
common_vars.__logger__.debug('[ Addon settings ] digionline_PhoneDeviceModel = ' + str(common_vars.__config_digionline_PhoneDeviceModel__))
|
||||
|
||||
common_vars.__config_digionline_PhoneAndroidVersion__ = __MyAddon__.getSetting('digionline_PhoneAndroidVersion')
|
||||
common_vars.__logger__.debug('[ Addon settings ] digionline_PhoneAndroidVersion = ' + str(common_vars.__config_digionline_PhoneAndroidVersion__))
|
||||
|
||||
common_vars.__config_digionline_TVDeviceModel__ = __MyAddon__.getSetting('digionline_TVDeviceModel')
|
||||
common_vars.__logger__.debug('[ Addon settings ] digionline_TVDeviceModel = ' + str(common_vars.__config_digionline_TVDeviceModel__))
|
||||
|
||||
common_vars.__config_digionline_TVAndroidVersion__ = __MyAddon__.getSetting('digionline_TVAndroidVersion')
|
||||
common_vars.__logger__.debug('[ Addon settings ] digionline_TVAndroidVersion = ' + str(common_vars.__config_digionline_TVAndroidVersion__))
|
||||
|
||||
common_vars.__config_digionline_TVPlatform__ = __MyAddon__.getSetting('digionline_TVPlatform')
|
||||
common_vars.__logger__.debug('[ Addon settings ] digionline_TVPlatform = ' + str(common_vars.__config_digionline_TVPlatform__))
|
||||
|
||||
|
||||
# voyo.ro
|
||||
common_vars.__config_voyo_Enabled__ = __MyAddon__.getSetting('voyo_Enabled')
|
||||
common_vars.__logger__.debug('[ Addon settings ] voyo_Enabled = ' + str(common_vars.__config_voyo_Enabled__))
|
||||
common_vars.__config_voyo_Username__ = __MyAddon__.getSetting('voyo_Username')
|
||||
common_vars.__config_voyo_Password__ = __MyAddon__.getSetting('voyo_Password')
|
||||
|
||||
# primaplay.ro
|
||||
common_vars.__config_primaplay_Enabled__ = __MyAddon__.getSetting('primaplay_Enabled')
|
||||
common_vars.__logger__.debug('[ Addon settings ] primaplay_Enabled = ' + str(common_vars.__config_primaplay_Enabled__))
|
||||
common_vars.__config_primaplay_Username__ = __MyAddon__.getSetting('primaplay_Username')
|
||||
common_vars.__config_primaplay_Password__ = __MyAddon__.getSetting('primaplay_Password')
|
||||
|
||||
# tvrplus.ro
|
||||
common_vars.__config_tvrplus_Enabled__ = __MyAddon__.getSetting('tvrplus_Enabled')
|
||||
common_vars.__logger__.debug('[ Addon settings ] tvrplus_Enabled = ' + str(common_vars.__config_tvrplus_Enabled__))
|
||||
|
||||
# General settings
|
||||
common_vars.__config_ShowTitleInChannelList__ = __MyAddon__.getSetting('ShowTitleInChannelList')
|
||||
common_vars.__logger__.debug('[ Addon settings ] ShowTitleInChannelList = ' + str(common_vars.__config_ShowTitleInChannelList__))
|
||||
common_vars.__config_DebugEnabled__ = __MyAddon__.getSetting('DebugEnabled')
|
||||
common_vars.__logger__.debug('[ Addon settings ] DebugEnabled = ' + str(common_vars.__config_DebugEnabled__))
|
||||
|
||||
## Simple PVR integration
|
||||
common_vars.__config_PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime__ = __MyAddon__.getSetting('PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime')
|
||||
common_vars.__logger__.debug('[ Addon settings ] PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime = ' + str(common_vars.__config_PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime__))
|
||||
common_vars.__config_PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime__ = __MyAddon__.getSetting('PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime')
|
||||
common_vars.__logger__.debug('[ Addon settings ] PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime = ' + str(common_vars.__config_PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime__))
|
||||
|
||||
|
||||
def get_url(**kwargs):
|
||||
####
|
||||
#
|
||||
# Create a URL for calling the plugin recursively from the given set of keyword arguments.
|
||||
#
|
||||
####
|
||||
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
common_vars.__logger__.debug('Called with parameters: ' + str(kwargs))
|
||||
|
||||
_call_url_ = '{0}?{1}'.format(common_vars.__plugin_url__, urlencode(kwargs))
|
||||
|
||||
common_vars.__logger__.debug('_call_url_: ' + str(_call_url_))
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
return _call_url_
|
||||
|
||||
|
||||
def has_accounts_enabled():
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
_answer_ = 'false'
|
||||
|
||||
if common_vars.__config_digionline_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('[ Addon settings ] digionline_Enabled = ' + str(common_vars.__config_digionline_Enabled__))
|
||||
_answer_ = 'true'
|
||||
|
||||
if common_vars.__config_voyo_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('[ Addon settings ] voyo_Enabled = ' + str(common_vars.__config_voyo_Enabled__))
|
||||
_answer_ = 'true'
|
||||
|
||||
if common_vars.__config_primaplay_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('[ Addon settings ] primaplay_Enabled = ' + str(common_vars.__config_primaplay_Enabled__))
|
||||
_answer_ = 'true'
|
||||
|
||||
if common_vars.__config_tvrplus_Enabled__ == 'true':
|
||||
common_vars.__logger__.debug('[ Addon settings ] tvrplus_Enabled = ' + str(common_vars.__config_tvrplus_Enabled__))
|
||||
_answer_ = 'true'
|
||||
|
||||
return _answer_
|
||||
|
@ -0,0 +1,188 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
######## Variables for the user preferences stored in the addon configuration ########
|
||||
|
||||
# Accounts
|
||||
__config_digionline_Enabled__ = ''
|
||||
__config_digionline_Username__ = ''
|
||||
__config_digionline_Password__ = ''
|
||||
__config_digionline_PhoneDeviceManufacturer__ = ''
|
||||
__config_digionline_PhoneDeviceModel__ = ''
|
||||
__config_digionline_PhoneAndroidVersion__ = ''
|
||||
|
||||
__config_digionline_TVDeviceModel__ = ''
|
||||
__config_digionline_TVAndroidVersion__ = ''
|
||||
__config_digionline_TVPlatform__ = ''
|
||||
|
||||
__config_voyo_Enabled__ = ''
|
||||
__config_voyo_Username__ = ''
|
||||
__config_voyo_Password__ = ''
|
||||
|
||||
__config_primaplay_Enabled__ = ''
|
||||
__config_primaplay_Username__ = ''
|
||||
__config_primaplay_Password__ = ''
|
||||
|
||||
__config_tvrplus_Enabled__ = ''
|
||||
|
||||
# General settings
|
||||
__config_ShowTitleInChannelList__ = ''
|
||||
__config_DebugEnabled__ = ''
|
||||
|
||||
# Cached data
|
||||
__config_categoriesCachedDataRetentionInterval__ = ''
|
||||
__config_channelsCachedDataRetentionInterval__ = ''
|
||||
__config_EPGDataCachedDataRetentionInterval__ = ''
|
||||
|
||||
## Simple PVR integration
|
||||
__config_PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime__ = ''
|
||||
__config_PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime__ = ''
|
||||
|
||||
|
||||
########
|
||||
|
||||
|
||||
|
||||
# UserAgent exposed by this add-on
|
||||
__digionline_API_userAgent__ = 'okhttp/4.8.1'
|
||||
__digionline_userAgent__ = 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0'
|
||||
__voyo_userAgent__ = 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0'
|
||||
__primaplay_userAgent__ = 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0'
|
||||
__tvrplus_userAgent__ = 'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0'
|
||||
|
||||
# The IDs used by add-on
|
||||
__AddonID__ = 'plugin.video.TVOnline.ro'
|
||||
__ServiceID__ = 'service.plugin.video.TVOnline.ro'
|
||||
|
||||
# File names for the files where the add-on and the service will write the log entries
|
||||
__AddonLogFilename__ = __AddonID__ + '.log'
|
||||
__ServiceLogFilename__ = __ServiceID__ + '.log'
|
||||
|
||||
# The cookiejars used by add-on
|
||||
__digionline_CookiesFilename__ = 'digionline.ro_cookies.txt'
|
||||
__digionline_CookieJar__ = ''
|
||||
|
||||
__voyo_CookiesFilename__ = 'voyo.ro_cookies.txt'
|
||||
__voyo_CookieJar__ = ''
|
||||
|
||||
__primaplay_CookiesFilename__ = 'primaplay.ro_cookies.txt'
|
||||
__primaplay_CookieJar__ = ''
|
||||
|
||||
__tvrplus_CookiesFilename__ = 'tvrplus.ro_cookies.txt'
|
||||
__tvrplus_CookieJar__ = ''
|
||||
|
||||
# Legacy file name for storing the state data
|
||||
__digionline_LegacyStateFilename__ = 'digionline.ro_state.txt'
|
||||
|
||||
# File name for storing the state data when behaving as Android phone
|
||||
__digionline_PhoneStateFilename__ = 'digionline.ro_PhoneState.txt'
|
||||
|
||||
# File name for storing the state data when behaving as Android TV
|
||||
__digionline_TVStateFilename__ = 'digionline.ro_TVState.txt'
|
||||
|
||||
|
||||
# The sessions used by add-on
|
||||
__digionline_Session__ = ''
|
||||
__voyo_Session__ = ''
|
||||
__primaplay_Session__ = ''
|
||||
__tvrplus_Session__ = ''
|
||||
|
||||
__digionline_ServiceSession__ = ''
|
||||
__voyo_ServiceSession__ = ''
|
||||
__primaplay_ServiceSession__ = ''
|
||||
__tvrplus_ServiceSession__ = ''
|
||||
|
||||
# The plugin url in plugin:// notation.
|
||||
__plugin_url__ = ''
|
||||
|
||||
# The plugin handle
|
||||
__handle__ = ''
|
||||
|
||||
__logger__ = ''
|
||||
|
||||
|
||||
|
||||
#### Constants
|
||||
__behave_map__ = {}
|
||||
__behave_map__['0'] = "Phone"
|
||||
__behave_map__['1'] = "TV"
|
||||
|
||||
__minute__ = (1 * 60)
|
||||
__day__ = (24 * 60 * 60)
|
||||
|
||||
# Max interval between authentications
|
||||
__digionline_AuthInterval__ = (1 * __day__)
|
||||
|
||||
# Directory holding the cached data.
|
||||
__digionline_cache_dir__ = 'cached_data/digionline.ro'
|
||||
__voyo_cache_dir__ = 'cached_data/voyo.ro'
|
||||
__primaplay_cache_dir__ = 'cached_data/primaplay.ro'
|
||||
__tvrplus_cache_dir__ = 'cached_data/tvrplus.ro'
|
||||
|
||||
# File names for the raw data
|
||||
__PVRIPTVSimpleClientIntegration_digionline_raw_m3u_FileName__ = __AddonID__ + 'digionline.m3u.raw'
|
||||
__PVRIPTVSimpleClientIntegration__digionline_raw_EPG_FileName__ = __AddonID__ + 'digionline.xml.raw'
|
||||
|
||||
__PVRIPTVSimpleClientIntegration_voyo_raw_m3u_FileName__ = __AddonID__ + 'voyo.m3u.raw'
|
||||
__PVRIPTVSimpleClientIntegration__voyo_raw_EPG_FileName__ = __AddonID__ + 'voyo.xml.raw'
|
||||
|
||||
__PVRIPTVSimpleClientIntegration_primaplay_raw_m3u_FileName__ = __AddonID__ + 'primaplay.m3u.raw'
|
||||
__PVRIPTVSimpleClientIntegration__primaplay_raw_EPG_FileName__ = __AddonID__ + 'primaplay.xml.raw'
|
||||
|
||||
__PVRIPTVSimpleClientIntegration_tvrplus_raw_m3u_FileName__ = __AddonID__ + 'tvrplus.m3u.raw'
|
||||
__PVRIPTVSimpleClientIntegration__tvrplus_raw_EPG_FileName__ = __AddonID__ + 'tvrplus.xml.raw'
|
||||
|
||||
# File containing the local copy of the list of categories and channels read from source
|
||||
__digionline_PhoneCategoriesChannelsCachedDataFilename__ = 'Phone_CategoriesChannels.json'
|
||||
__digionline_TVCategoriesChannelsCachedDataFilename__ = 'TV_CategoriesChannels.json'
|
||||
|
||||
# File containing the local copy of the epg read from source
|
||||
__digionline_PhoneEPGCachedDataFilename__ = 'Phone_EPG.json'
|
||||
__digionline_TVEPGCachedDataFilename__ = 'TV_EPG.json'
|
||||
|
||||
# Some sane defaults before being overwritten by the user settings
|
||||
# How much time has to pass before reading again from source the list of categories.
|
||||
__CachedDataRetentionInterval__ = (1 * __day__)
|
||||
|
||||
|
||||
### Service variables
|
||||
|
||||
## PVR IPTV Simple Client integration
|
||||
# Directory where data files are stored
|
||||
__PVRIPTVSimpleClientIntegration_DataDir__ = 'PVRIPTVSimpleClientIntegration'
|
||||
|
||||
# File names for the data files
|
||||
__PVRIPTVSimpleClientIntegration_m3u_FileName__ = __AddonID__ + '.m3u'
|
||||
__PVRIPTVSimpleClientIntegration_EPG_FileName__ = __AddonID__ + '.xml'
|
||||
__PVRIPTVSimpleClientIntegration_versions_FileName__ = __AddonID__ + '.json'
|
||||
|
||||
# Time of day for refreshing the contents in the data files.
|
||||
__PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime__ = ''
|
||||
__PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime__ = ''
|
||||
|
||||
# Previous/Old time of day for refreshing the contents in the data files.
|
||||
__PVRIPTVSimpleClientIntegration_m3u_FileOldRefreshTime__ = ''
|
||||
__PVRIPTVSimpleClientIntegration_EPG_FileOldRefreshTime__ = ''
|
||||
|
||||
# Time since the last update of data files. If this time has passed, the data files will be updated.
|
||||
__PVRIPTVSimpleClientIntegration_m3u_FileMaxAge__ = (1 * __day__) + (1 * __minute__)
|
||||
__PVRIPTVSimpleClientIntegration_EPG_FileMaxAge__ = (1 * __day__) + (1 * __minute__)
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,18 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
@ -0,0 +1,588 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
|
||||
import sys
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmcvfs
|
||||
import os
|
||||
import logging
|
||||
import http.cookiejar
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import inputstreamhelper
|
||||
import resources.lib.common.vars as common_vars
|
||||
import resources.lib.common.functions as common_functions
|
||||
|
||||
|
||||
def init_AddonCookieJar(NAME, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Initialize the common_vars.__primaplay_CookieJar__ variable.
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: common_vars.__logger__ name to use for sending the log messages
|
||||
# DATA_DIR: The addon's 'userdata' directory.
|
||||
#
|
||||
####
|
||||
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# File containing the session cookies
|
||||
cookies_file = os.path.join(DATA_DIR, common_vars.__primaplay_CookiesFilename__)
|
||||
common_vars.__logger__.debug('[ Addon cookies file ] cookies_file = ' + str(cookies_file))
|
||||
common_vars.__primaplay_CookieJar__ = http.cookiejar.MozillaCookieJar(cookies_file)
|
||||
|
||||
# If it doesn't exist already, create a new file where the cookies should be saved
|
||||
if not os.path.exists(cookies_file):
|
||||
common_vars.__primaplay_CookieJar__.save()
|
||||
common_vars.__logger__.debug('[ Addon cookiefile ] Created cookiejar file: ' + str(cookies_file))
|
||||
|
||||
# Load any cookies saved from the last run
|
||||
common_vars.__primaplay_CookieJar__.load()
|
||||
common_vars.__logger__.debug('[ Addon cookiejar ] Loaded cookiejar from file: ' + str(cookies_file))
|
||||
|
||||
|
||||
|
||||
def do_login(NAME, COOKIEJAR, SESSION):
|
||||
####
|
||||
#
|
||||
# Login to primaplay.ro for the given session.
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: Logger name to use for sending the log messages
|
||||
# COOKIEJAR: The cookiejar to be used with the given session
|
||||
# SESSION: The session to be used for login
|
||||
#
|
||||
# Return: dict variable with authentication details
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
|
||||
MyHeaders = {
|
||||
'User-Agent': common_vars.__primaplay_userAgent__,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('URL: https://www.primaplay.ro/login')
|
||||
common_vars.__logger__.debug('Method: GET')
|
||||
|
||||
# Send the GET request
|
||||
_request_ = SESSION.get('https://www.primaplay.ro/login', headers=MyHeaders, allow_redirects=False)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
_has_token_ = re.search('_token', _request_.content.decode(), re.IGNORECASE|re.DOTALL)
|
||||
|
||||
if _has_token_:
|
||||
_token_ = re.findall('name="_token" value="(.+?)"', _request_.content.decode(), re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('Found _token_ = ' + str(_token_))
|
||||
else:
|
||||
_token_ = "NO_TOKEN"
|
||||
common_vars.__logger__.debug('Set _token_ = ' + str(_token_))
|
||||
|
||||
MyHeaders = {
|
||||
'User-Agent': common_vars.__primaplay_userAgent__,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
# Setup form data to be sent
|
||||
MyFormData = {
|
||||
'email': common_vars.__config_primaplay_Username__,
|
||||
'password': common_vars.__config_primaplay_Password__,
|
||||
'_token': _token_,
|
||||
}
|
||||
|
||||
MyFormData_logger = {
|
||||
'email': common_vars.__config_primaplay_Username__,
|
||||
'password': '****************',
|
||||
'_token': _token_,
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('MyFormData: ' + str(MyFormData_logger))
|
||||
common_vars.__logger__.debug('URL: https://www.primaplay.ro/login')
|
||||
common_vars.__logger__.debug('Method: POST')
|
||||
|
||||
# Send the POST request
|
||||
_request_ = SESSION.post('https://www.primaplay.ro/login', headers=MyHeaders, data=MyFormData, allow_redirects=True)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def play_video(CHANNEL_ENDPOINT, NAME, COOKIEJAR, SESSION, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Play a video by the provided path.
|
||||
#
|
||||
# Parameters:
|
||||
# path: Fully-qualified video URL
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
common_vars.__logger__.debug('Chanel endpoint = ' + CHANNEL_ENDPOINT)
|
||||
|
||||
_auth_ = do_login(NAME, COOKIEJAR, SESSION)
|
||||
common_vars.__logger__.debug('_auth_ = ' + str(_auth_))
|
||||
|
||||
# Get the URL for the stream metadata
|
||||
|
||||
# Setup headers for the request
|
||||
MyHeaders = {
|
||||
'User-Agent': common_vars.__primaplay_userAgent__,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0',
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('URL: ' + CHANNEL_ENDPOINT)
|
||||
common_vars.__logger__.debug('Method: GET')
|
||||
|
||||
# Send the GET request
|
||||
_request_ = SESSION.get(CHANNEL_ENDPOINT, headers=MyHeaders)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
_premium_content_ = re.search('class="premium-content"', _request_.content.decode(), re.IGNORECASE|re.DOTALL)
|
||||
common_vars.__logger__.debug('Found _premium_content_ = ' + str(_premium_content_))
|
||||
|
||||
if _premium_content_:
|
||||
_raw_message_ = re.findall('<div class="premium-content">(.+?)</div>', _request_.content.decode(), re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('Found _raw_message_ = ' + str(_raw_message_))
|
||||
|
||||
_title_ = ""
|
||||
_title_ = re.findall('<h2>(.+?)</h2>', _raw_message_, re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('Found _title_ = ' + str(_title_))
|
||||
|
||||
_has_message_ = re.search('<p>', _raw_message_, re.IGNORECASE|re.DOTALL)
|
||||
|
||||
if _has_message_:
|
||||
_message_ = re.findall('<p>(.+?)</br>', _raw_message_, re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('Found _message_ = ' + str(_message_))
|
||||
|
||||
common_vars.__logger__.info('[primaplus.ro] => ' + _title_ + ' => ' + _message_)
|
||||
xbmcgui.Dialog().ok('[primaplus.ro] => ' + _title_.strip(), _message_.strip())
|
||||
|
||||
else:
|
||||
common_vars.__logger__.info('[primaplus.ro] => Error => ' + _title_.strip())
|
||||
xbmcgui.Dialog().ok('[primaplus.ro] => Error ', _title_.strip())
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
else:
|
||||
|
||||
_raw_stream_manifest_url_ = re.search(r'(?<!=// )\w+',_request_.content.decode())
|
||||
common_vars.__logger__.debug('Found _raw_stream_manifest_url_ = ' + str(_raw_stream_manifest_url_.groups))
|
||||
|
||||
_stream_manifest_url_ = re.findall('var videoSrc = \'(.+?)\';',_request_.content.decode(), re.IGNORECASE|re.DOTALL)[1]
|
||||
common_vars.__logger__.debug('Found _stream_manifest_url_ = ' + str(_stream_manifest_url_))
|
||||
|
||||
# Set the headers to be used with imputstream.adaptive
|
||||
_headers_ = ''
|
||||
_headers_ = _headers_ + '&User-Agent=' + common_vars.__primaplay_userAgent__
|
||||
_headers_ = _headers_ + '&Host=clever-live1ro.deja.media'
|
||||
_headers_ = _headers_ + '&Origin=https://www.primaplay.ro'
|
||||
_headers_ = _headers_ + '&Referer=https://www.primaplay.ro/'
|
||||
_headers_ = _headers_ + '&Connection=keep-alive'
|
||||
_headers_ = _headers_ + '&Accept-Language=en-US'
|
||||
_headers_ = _headers_ + '&Accept=*/*'
|
||||
_headers_ = _headers_ + '&Accept-Encoding=identity'
|
||||
common_vars.__logger__.debug('Created: _headers_ = ' + _headers_)
|
||||
|
||||
# # Create a playable item with a path to play.
|
||||
# # See: https://github.com/peak3d/inputstream.adaptive/issues/131#issuecomment-375059796
|
||||
# is_helper = inputstreamhelper.Helper('hls')
|
||||
# if is_helper.check_inputstream():
|
||||
# play_item = xbmcgui.ListItem(path=_stream_manifest_url_ + '|' + _headers_)
|
||||
# play_item.setProperty('inputstream', 'inputstream.adaptive')
|
||||
# play_item.setProperty('inputstream.adaptive.stream_headers', _headers_)
|
||||
# play_item.setProperty('inputstream.adaptive.manifest_type', 'hls')
|
||||
# play_item.setMimeType('application/vnd.apple.mpegurl')
|
||||
# play_item.setContentLookup(False)
|
||||
#
|
||||
# # Pass the item to the Kodi player.
|
||||
# xbmcplugin.setResolvedUrl(int(common_vars.__handle__), True, listitem=play_item)
|
||||
|
||||
play_item = xbmcgui.ListItem(path=_stream_manifest_url_ + '|' + _headers_)
|
||||
xbmcplugin.setResolvedUrl(int(common_vars.__handle__), True, listitem=play_item)
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def list_categories(NAME, COOKIEJAR, SESSION, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Create the list of video categories in the Kodi interface.
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def list_channels(NAME, COOKIEJAR, SESSION, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Create the list of playable videos in the Kodi interface.
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: Logger name to use for sending the log messages.
|
||||
# COOKIEJAR: The cookiejar to be used with the given session.
|
||||
# SESSION: The session to be used for login this call
|
||||
# DATA_DIR: The addon's 'userdata' directory.
|
||||
#
|
||||
# Return: The list of cached video categories
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Get the list of videos in the category.
|
||||
channels = get_channels(NAME, COOKIEJAR, SESSION)
|
||||
common_vars.__logger__.debug('Received channels = ' + str(channels))
|
||||
|
||||
for channel in channels:
|
||||
common_vars.__logger__.debug('Channel data => ' + str(channel))
|
||||
common_vars.__logger__.debug('Channel name: ' + channel['name'])
|
||||
common_vars.__logger__.debug('Channel url: ' + channel['url'])
|
||||
common_vars.__logger__.debug('Channel logo: ' + channel['logo'])
|
||||
common_vars.__logger__.debug('Channel ID: ' + channel['id'])
|
||||
|
||||
# Create a list item with a text label and a thumbnail image.
|
||||
list_item = xbmcgui.ListItem(label=channel['name'])
|
||||
|
||||
# Set additional info for the list item.
|
||||
# For available properties see https://codedocs.xyz/xbmc/xbmc/group__python__xbmcgui__listitem.html#ga0b71166869bda87ad744942888fb5f14
|
||||
# 'mediatype' is needed for skin to display info for this ListItem correctly.
|
||||
list_item.setInfo('video', {'title': channel['name'],
|
||||
'genre': 'General',
|
||||
'mediatype': 'video'})
|
||||
|
||||
|
||||
# Set graphics (thumbnail, fanart, banner, poster, landscape etc.) for the list item.
|
||||
list_item.setArt({'thumb': channel['logo']})
|
||||
|
||||
# Set 'IsPlayable' property to 'true'.
|
||||
# This is mandatory for playable items!
|
||||
list_item.setProperty('IsPlayable', 'true')
|
||||
|
||||
# Create a URL for a plugin recursive call.
|
||||
# Example: plugin://plugin.video.example/?action=play&channel_endpoint=/filme/tnt&channel_metadata=...
|
||||
url = common_functions.get_url(action='play', account='primaplay.ro', channel_endpoint=channel['url'])
|
||||
common_vars.__logger__.debug('URL for plugin recursive call: ' + url)
|
||||
|
||||
# This means that this item won't open any sub-list.
|
||||
is_folder = False
|
||||
|
||||
# Add our item to the Kodi virtual folder listing.
|
||||
xbmcplugin.addDirectoryItem(int(common_vars.__handle__), url, list_item, is_folder)
|
||||
|
||||
# Add a sort method for the virtual folder items (alphabetically, ignore articles)
|
||||
xbmcplugin.addSortMethod(int(common_vars.__handle__), xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE)
|
||||
|
||||
# Finish creating a virtual folder.
|
||||
xbmcplugin.endOfDirectory(int(common_vars.__handle__))
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def get_channels(NAME, COOKIEJAR, SESSION):
|
||||
####
|
||||
#
|
||||
# Get from primaplay.ro the list of channels/streams.
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: Logger name to use for sending the log messages
|
||||
# COOKIEJAR: The cookiejar to be used with the given session
|
||||
# SESSION: The session to be used for this call
|
||||
# DATA_DIR: The addon's 'userdata' directory.
|
||||
#
|
||||
# Return: The list of channels/streams in the given category
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Setup headers for the request
|
||||
MyHeaders = {
|
||||
'User-Agent': common_vars.__primaplay_userAgent__,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('URL: https://www.primaplay.ro/live')
|
||||
common_vars.__logger__.debug('Method: GET')
|
||||
|
||||
# Send the GET request
|
||||
_request_ = SESSION.get('https://www.primaplay.ro/live', headers=MyHeaders)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
_raw_channels_ = re.findall('<div class="card card-responsive(.+?)</div>', _request_.content.decode(), re.IGNORECASE|re.DOTALL)
|
||||
common_vars.__logger__.debug('Found _raw_channels_ = ' + str(_raw_channels_))
|
||||
|
||||
# Initialize the list of channels
|
||||
_channels_ = []
|
||||
|
||||
for _raw_channel_ in _raw_channels_:
|
||||
common_vars.__logger__.debug('_raw_channel_ = ' + str(_raw_channel_))
|
||||
|
||||
_channel_record_ = {}
|
||||
|
||||
_channel_name_ = re.findall('<span>(.+?)</span>', _raw_channel_, re.IGNORECASE)[0]
|
||||
common_vars.__logger__.debug('_channel_name_ = ' + str(_channel_name_))
|
||||
|
||||
_channel_url_ = re.findall('<a class="card-content" href="(.+?)"', _raw_channel_, re.IGNORECASE)[0]
|
||||
common_vars.__logger__.debug('_channel_url_ = ' + str(_channel_url_))
|
||||
|
||||
_channel_logo_ = re.findall('image:url\(\'(.+?)\'', _raw_channel_, re.IGNORECASE)[0]
|
||||
common_vars.__logger__.debug('_channel_logo_ = ' + str(_channel_logo_))
|
||||
|
||||
_channel_id_ = re.findall('card-bg (.+?)"', _raw_channel_, re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('_channel_id_ = ' + str(_channel_id_))
|
||||
|
||||
_channel_record_["name"] = _channel_name_
|
||||
_channel_record_["url"] = _channel_url_
|
||||
_channel_record_["logo"] = _channel_logo_
|
||||
_channel_record_["id"] = _channel_id_
|
||||
|
||||
common_vars.__logger__.debug('Created: _channel_record_ = ' + str(_channel_record_))
|
||||
_channels_.append(_channel_record_)
|
||||
|
||||
common_vars.__logger__.debug('_channels_ = ' + str(_channels_))
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
return _channels_
|
||||
|
||||
|
||||
def PVRIPTVSimpleClientIntegration_update_m3u_file(M3U_FILE, START_NUMBER, NAME, COOKIEJAR, SESSION):
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
common_vars.__logger__.debug('M3U_FILE = ' + M3U_FILE)
|
||||
common_vars.__logger__.debug('START_NUMBER = ' + str(START_NUMBER))
|
||||
|
||||
# Get the list of channels in the category.
|
||||
channels = get_channels(NAME, COOKIEJAR, SESSION)
|
||||
common_vars.__logger__.debug('Received channels = ' + str(channels))
|
||||
|
||||
_CHNO_ = START_NUMBER
|
||||
_data_file_ = open(M3U_FILE, 'a', encoding='utf-8')
|
||||
|
||||
for channel in channels:
|
||||
_line_ = "#EXTINF:0 tvg-id=\"primaplay__" + str(channel['id']) + "\" tvg-name=\"" + channel['name'] + "\" tvg-logo=\"" + channel['logo'] + "\" tvg-chno=\"" + str(_CHNO_) + "\" group-title=\"Prima Play\"," + channel['name']
|
||||
|
||||
_url_ = common_functions.get_url(action='play', account='primaplay.ro', channel_endpoint=channel['url'])
|
||||
_play_url_ = "plugin://" + common_vars.__AddonID__ + "/" + _url_
|
||||
|
||||
_data_file_.write(_line_ + "\n")
|
||||
_data_file_.write(_play_url_ + "\n")
|
||||
|
||||
_CHNO_ = _CHNO_ + 1
|
||||
|
||||
_data_file_.close()
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
return _CHNO_
|
||||
|
||||
|
||||
#def PVRIPTVSimpleClientIntegration_update_EPG_file(XML_FILE, NAME, COOKIEJAR, SESSION):
|
||||
# common_vars.__logger__ = logging.getLogger(NAME)
|
||||
# common_vars.__logger__.debug('Enter function')
|
||||
#
|
||||
# common_vars.__logger__.debug('XML_FILE = ' + XML_FILE)
|
||||
#
|
||||
# # Get the list of channels in the category.
|
||||
# channels = voyo_functions.get_channels(NAME, COOKIEJAR, SESSION)
|
||||
# common_vars.__logger__.debug('Received channels = ' + str(channels))
|
||||
#
|
||||
# epg = voyo_functions.get_epg_data(NAME, COOKIEJAR, SESSION)
|
||||
# common_vars.__logger__.debug('Received epg = ' + str(epg))
|
||||
#
|
||||
# _data_file_ = open(XML_FILE, 'a', encoding='utf-8')
|
||||
#
|
||||
# for channel in channels:
|
||||
# #common_vars.__logger__.debug('Channel name = ' + channel['name'])
|
||||
# #common_vars.__logger__.debug('Channel url = ' + channel['url'])
|
||||
# #common_vars.__logger__.debug('Channel logo = ' + channel['logo'])
|
||||
# #common_vars.__logger__.debug('Channel id = ' + channel['id'])
|
||||
# #common_vars.__logger__.debug('Channel data-url = ' + channel['data-url'])
|
||||
#
|
||||
# _line_ = " <channel id=\"voyo__" + channel['id'] + "\">"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
# _line_ = " <display-name>" + channel['name'] + "</display-name>"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
# _line_ = " </channel>"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
#
|
||||
# for program in epg[channel['id']]:
|
||||
#
|
||||
# ###### Probably there is a better way to deal with this ######
|
||||
# program['start_at'] = re.sub('-', '', program['start_at'], flags=re.IGNORECASE)
|
||||
# program['start_at'] = re.sub('T', '', program['start_at'], flags=re.IGNORECASE)
|
||||
# program['start_at'] = re.sub(':', '', program['start_at'], flags=re.IGNORECASE)
|
||||
# program['start_at'] = re.sub('\+', ' +', program['start_at'], flags=re.IGNORECASE)
|
||||
#
|
||||
# program['end_at'] = re.sub('-', '', program['end_at'], flags=re.IGNORECASE)
|
||||
# program['end_at'] = re.sub('T', '', program['end_at'], flags=re.IGNORECASE)
|
||||
# program['end_at'] = re.sub(':', '', program['end_at'], flags=re.IGNORECASE)
|
||||
# program['end_at'] = re.sub('\+', ' +', program['end_at'], flags=re.IGNORECASE)
|
||||
# ###### Probably there is a better way to deal with this ######
|
||||
#
|
||||
# _line_ = " <programme start=\"" + program['start_at'] + "\" stop=\"" + program['end_at'] + "\" channel=\"voyo__" + channel['id'] + "\">"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
#
|
||||
# # Replace unwanted characters in the program name
|
||||
# program['title'] = re.sub(' ', ' ', program['title'], flags=re.IGNORECASE)
|
||||
# program['title'] = re.sub(''', '\'', program['title'], flags=re.IGNORECASE)
|
||||
# _line_ = " <title>" + program['title'] + "</title>"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
#
|
||||
# # Replace unwanted characters in the program description
|
||||
# program['short_description'] = re.sub(' ', ' ', program['short_description'], flags=re.IGNORECASE)
|
||||
# program['short_description'] = re.sub(''', '\'', program['short_description'], flags=re.IGNORECASE)
|
||||
# program['description'] = re.sub(' ', ' ', program['description'], flags=re.IGNORECASE)
|
||||
# program['description'] = re.sub(''', '\'', program['description'], flags=re.IGNORECASE)
|
||||
#
|
||||
# _line_ = " <desc>" + program['short_description'] + "\n\n " + program['description'] + "\n </desc>"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
#
|
||||
# _line_ = " </programme>"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
#
|
||||
# _data_file_.close
|
||||
#
|
||||
# common_vars.__logger__.debug('Exit function')
|
||||
#
|
||||
#
|
||||
#
|
||||
#def get_epg_data(NAME, COOKIEJAR, SESSION):
|
||||
# ####
|
||||
# #
|
||||
# # Get from voyo.protv.ro the EPG data.
|
||||
# #
|
||||
# # Parameters:
|
||||
# # NAME: Logger name to use for sending the log messages
|
||||
# # COOKIEJAR: The cookiejar to be used with the given session
|
||||
# # SESSION: The session to be used for this call
|
||||
# #
|
||||
# # Return: A dict object containing the EPG data
|
||||
# #
|
||||
# ####
|
||||
# common_vars.__logger__ = logging.getLogger(NAME)
|
||||
# common_vars.__logger__.debug('Enter function')
|
||||
#
|
||||
# # Setup headers for the request
|
||||
# MyHeaders = {
|
||||
# 'Host': 'protvplus.ro',
|
||||
# 'User-Agent': common_vars.__protvplus_userAgent__,
|
||||
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
# 'Accept-Language': 'en-US',
|
||||
# 'Accept-Encoding': 'identity',
|
||||
# 'Connection': 'keep-alive',
|
||||
# 'Upgrade-Insecure-Requests': '1',
|
||||
# 'Cache-Control': 'max-age=0'
|
||||
# }
|
||||
#
|
||||
# common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
# common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
# common_vars.__logger__.debug('URL: https://protvplus.ro/tv-program')
|
||||
# common_vars.__logger__.debug('Method: GET')
|
||||
#
|
||||
# # Send the GET request
|
||||
# _request_ = SESSION.get('https://protvplus.ro/tv-program', headers=MyHeaders)
|
||||
#
|
||||
# # Save cookies for later use.
|
||||
# COOKIEJAR.save(ignore_discard=True)
|
||||
#
|
||||
# common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
# common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
# common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
# common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
#
|
||||
# _raw_epg_data_ = re.findall('EPG_program = (.+?);\n', _request_.content.decode(), re.IGNORECASE|re.DOTALL)[0]
|
||||
# common_vars.__logger__.debug('Found _raw_epg_data_ = ' + str(_raw_epg_data_))
|
||||
#
|
||||
# _epg_data_ = json.loads(_raw_epg_data_)
|
||||
#
|
||||
# common_vars.__logger__.debug('Exit function')
|
||||
#
|
||||
# return _epg_data_
|
||||
|
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Daniel Bader (http://dbader.org)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -0,0 +1,839 @@
|
||||
"""
|
||||
Python job scheduling for humans.
|
||||
|
||||
github.com/dbader/schedule
|
||||
|
||||
An in-process scheduler for periodic jobs that uses the builder pattern
|
||||
for configuration. Schedule lets you run Python functions (or any other
|
||||
callable) periodically at pre-determined intervals using a simple,
|
||||
human-friendly syntax.
|
||||
|
||||
Inspired by Addam Wiggins' article "Rethinking Cron" [1] and the
|
||||
"clockwork" Ruby module [2][3].
|
||||
|
||||
Features:
|
||||
- A simple to use API for scheduling jobs.
|
||||
- Very lightweight and no external dependencies.
|
||||
- Excellent test coverage.
|
||||
- Tested on Python 3.6, 3.7, 3.8, 3.9
|
||||
|
||||
Usage:
|
||||
>>> import schedule
|
||||
>>> import time
|
||||
|
||||
>>> def job(message='stuff'):
|
||||
>>> print("I'm working on:", message)
|
||||
|
||||
>>> schedule.every(10).minutes.do(job)
|
||||
>>> schedule.every(5).to(10).days.do(job)
|
||||
>>> schedule.every().hour.do(job, message='things')
|
||||
>>> schedule.every().day.at("10:30").do(job)
|
||||
|
||||
>>> while True:
|
||||
>>> schedule.run_pending()
|
||||
>>> time.sleep(1)
|
||||
|
||||
[1] https://adam.herokuapp.com/past/2010/4/13/rethinking_cron/
|
||||
[2] https://github.com/Rykian/clockwork
|
||||
[3] https://adam.herokuapp.com/past/2010/6/30/replace_cron_with_clockwork/
|
||||
"""
|
||||
from collections.abc import Hashable
|
||||
import datetime
|
||||
import functools
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from typing import Set, List, Optional, Callable, Union
|
||||
|
||||
logger = logging.getLogger("schedule")
|
||||
|
||||
|
||||
class ScheduleError(Exception):
|
||||
"""Base schedule exception"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ScheduleValueError(ScheduleError):
|
||||
"""Base schedule value error"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class IntervalError(ScheduleValueError):
|
||||
"""An improper interval was used"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CancelJob(object):
|
||||
"""
|
||||
Can be returned from a job to unschedule itself.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Scheduler(object):
|
||||
"""
|
||||
Objects instantiated by the :class:`Scheduler <Scheduler>` are
|
||||
factories to create jobs, keep record of scheduled jobs and
|
||||
handle their execution.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.jobs: List[Job] = []
|
||||
|
||||
def run_pending(self) -> None:
|
||||
"""
|
||||
Run all jobs that are scheduled to run.
|
||||
|
||||
Please note that it is *intended behavior that run_pending()
|
||||
does not run missed jobs*. For example, if you've registered a job
|
||||
that should run every minute and you only call run_pending()
|
||||
in one hour increments then your job won't be run 60 times in
|
||||
between but only once.
|
||||
"""
|
||||
runnable_jobs = (job for job in self.jobs if job.should_run)
|
||||
for job in sorted(runnable_jobs):
|
||||
self._run_job(job)
|
||||
|
||||
def run_all(self, delay_seconds: int = 0) -> None:
|
||||
"""
|
||||
Run all jobs regardless if they are scheduled to run or not.
|
||||
|
||||
A delay of `delay` seconds is added between each job. This helps
|
||||
distribute system load generated by the jobs more evenly
|
||||
over time.
|
||||
|
||||
:param delay_seconds: A delay added between every executed job
|
||||
"""
|
||||
logger.debug(
|
||||
"Running *all* %i jobs with %is delay in between",
|
||||
len(self.jobs),
|
||||
delay_seconds,
|
||||
)
|
||||
for job in self.jobs[:]:
|
||||
self._run_job(job)
|
||||
time.sleep(delay_seconds)
|
||||
|
||||
def get_jobs(self, tag: Optional[Hashable] = None) -> List["Job"]:
|
||||
"""
|
||||
Gets scheduled jobs marked with the given tag, or all jobs
|
||||
if tag is omitted.
|
||||
|
||||
:param tag: An identifier used to identify a subset of
|
||||
jobs to retrieve
|
||||
"""
|
||||
if tag is None:
|
||||
return self.jobs[:]
|
||||
else:
|
||||
return [job for job in self.jobs if tag in job.tags]
|
||||
|
||||
def clear(self, tag: Optional[Hashable] = None) -> None:
|
||||
"""
|
||||
Deletes scheduled jobs marked with the given tag, or all jobs
|
||||
if tag is omitted.
|
||||
|
||||
:param tag: An identifier used to identify a subset of
|
||||
jobs to delete
|
||||
"""
|
||||
if tag is None:
|
||||
logger.debug("Deleting *all* jobs")
|
||||
del self.jobs[:]
|
||||
else:
|
||||
logger.debug('Deleting all jobs tagged "%s"', tag)
|
||||
self.jobs[:] = (job for job in self.jobs if tag not in job.tags)
|
||||
|
||||
def cancel_job(self, job: "Job") -> None:
|
||||
"""
|
||||
Delete a scheduled job.
|
||||
|
||||
:param job: The job to be unscheduled
|
||||
"""
|
||||
try:
|
||||
logger.debug('Cancelling job "%s"', str(job))
|
||||
self.jobs.remove(job)
|
||||
except ValueError:
|
||||
logger.debug('Cancelling not-scheduled job "%s"', str(job))
|
||||
|
||||
def every(self, interval: int = 1) -> "Job":
|
||||
"""
|
||||
Schedule a new periodic job.
|
||||
|
||||
:param interval: A quantity of a certain time unit
|
||||
:return: An unconfigured :class:`Job <Job>`
|
||||
"""
|
||||
job = Job(interval, self)
|
||||
return job
|
||||
|
||||
def _run_job(self, job: "Job") -> None:
|
||||
ret = job.run()
|
||||
if isinstance(ret, CancelJob) or ret is CancelJob:
|
||||
self.cancel_job(job)
|
||||
|
||||
@property
|
||||
def next_run(self) -> Optional[datetime.datetime]:
|
||||
"""
|
||||
Datetime when the next job should run.
|
||||
|
||||
:return: A :class:`~datetime.datetime` object
|
||||
or None if no jobs scheduled
|
||||
"""
|
||||
if not self.jobs:
|
||||
return None
|
||||
return min(self.jobs).next_run
|
||||
|
||||
@property
|
||||
def idle_seconds(self) -> Optional[float]:
|
||||
"""
|
||||
:return: Number of seconds until
|
||||
:meth:`next_run <Scheduler.next_run>`
|
||||
or None if no jobs are scheduled
|
||||
"""
|
||||
if not self.next_run:
|
||||
return None
|
||||
return (self.next_run - datetime.datetime.now()).total_seconds()
|
||||
|
||||
|
||||
class Job(object):
|
||||
"""
|
||||
A periodic job as used by :class:`Scheduler`.
|
||||
|
||||
:param interval: A quantity of a certain time unit
|
||||
:param scheduler: The :class:`Scheduler <Scheduler>` instance that
|
||||
this job will register itself with once it has
|
||||
been fully configured in :meth:`Job.do()`.
|
||||
|
||||
Every job runs at a given fixed time interval that is defined by:
|
||||
|
||||
* a :meth:`time unit <Job.second>`
|
||||
* a quantity of `time units` defined by `interval`
|
||||
|
||||
A job is usually created and returned by :meth:`Scheduler.every`
|
||||
method, which also defines its `interval`.
|
||||
"""
|
||||
|
||||
def __init__(self, interval: int, scheduler: Scheduler = None):
|
||||
self.interval: int = interval # pause interval * unit between runs
|
||||
self.latest: Optional[int] = None # upper limit to the interval
|
||||
self.job_func: Optional[functools.partial] = None # the job job_func to run
|
||||
|
||||
# time units, e.g. 'minutes', 'hours', ...
|
||||
self.unit: Optional[str] = None
|
||||
|
||||
# optional time at which this job runs
|
||||
self.at_time: Optional[datetime.time] = None
|
||||
|
||||
# datetime of the last run
|
||||
self.last_run: Optional[datetime.datetime] = None
|
||||
|
||||
# datetime of the next run
|
||||
self.next_run: Optional[datetime.datetime] = None
|
||||
|
||||
# timedelta between runs, only valid for
|
||||
self.period: Optional[datetime.timedelta] = None
|
||||
|
||||
# Specific day of the week to start on
|
||||
self.start_day: Optional[str] = None
|
||||
|
||||
# optional time of final run
|
||||
self.cancel_after: Optional[datetime.datetime] = None
|
||||
|
||||
self.tags: Set[Hashable] = set() # unique set of tags for the job
|
||||
self.scheduler: Optional[Scheduler] = scheduler # scheduler to register with
|
||||
|
||||
def __lt__(self, other) -> bool:
|
||||
"""
|
||||
PeriodicJobs are sortable based on the scheduled time they
|
||||
run next.
|
||||
"""
|
||||
return self.next_run < other.next_run
|
||||
|
||||
def __str__(self) -> str:
|
||||
if hasattr(self.job_func, "__name__"):
|
||||
job_func_name = self.job_func.__name__ # type: ignore
|
||||
else:
|
||||
job_func_name = repr(self.job_func)
|
||||
|
||||
return ("Job(interval={}, unit={}, do={}, args={}, kwargs={})").format(
|
||||
self.interval,
|
||||
self.unit,
|
||||
job_func_name,
|
||||
"()" if self.job_func is None else self.job_func.args,
|
||||
"{}" if self.job_func is None else self.job_func.keywords,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
def format_time(t):
|
||||
return t.strftime("%Y-%m-%d %H:%M:%S") if t else "[never]"
|
||||
|
||||
def is_repr(j):
|
||||
return not isinstance(j, Job)
|
||||
|
||||
timestats = "(last run: %s, next run: %s)" % (
|
||||
format_time(self.last_run),
|
||||
format_time(self.next_run),
|
||||
)
|
||||
|
||||
if hasattr(self.job_func, "__name__"):
|
||||
job_func_name = self.job_func.__name__
|
||||
else:
|
||||
job_func_name = repr(self.job_func)
|
||||
args = [repr(x) if is_repr(x) else str(x) for x in self.job_func.args]
|
||||
kwargs = ["%s=%s" % (k, repr(v)) for k, v in self.job_func.keywords.items()]
|
||||
call_repr = job_func_name + "(" + ", ".join(args + kwargs) + ")"
|
||||
|
||||
if self.at_time is not None:
|
||||
return "Every %s %s at %s do %s %s" % (
|
||||
self.interval,
|
||||
self.unit[:-1] if self.interval == 1 else self.unit,
|
||||
self.at_time,
|
||||
call_repr,
|
||||
timestats,
|
||||
)
|
||||
else:
|
||||
fmt = (
|
||||
"Every %(interval)s "
|
||||
+ ("to %(latest)s " if self.latest is not None else "")
|
||||
+ "%(unit)s do %(call_repr)s %(timestats)s"
|
||||
)
|
||||
|
||||
return fmt % dict(
|
||||
interval=self.interval,
|
||||
latest=self.latest,
|
||||
unit=(self.unit[:-1] if self.interval == 1 else self.unit),
|
||||
call_repr=call_repr,
|
||||
timestats=timestats,
|
||||
)
|
||||
|
||||
@property
|
||||
def second(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError("Use seconds instead of second")
|
||||
return self.seconds
|
||||
|
||||
@property
|
||||
def seconds(self):
|
||||
self.unit = "seconds"
|
||||
return self
|
||||
|
||||
@property
|
||||
def minute(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError("Use minutes instead of minute")
|
||||
return self.minutes
|
||||
|
||||
@property
|
||||
def minutes(self):
|
||||
self.unit = "minutes"
|
||||
return self
|
||||
|
||||
@property
|
||||
def hour(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError("Use hours instead of hour")
|
||||
return self.hours
|
||||
|
||||
@property
|
||||
def hours(self):
|
||||
self.unit = "hours"
|
||||
return self
|
||||
|
||||
@property
|
||||
def day(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError("Use days instead of day")
|
||||
return self.days
|
||||
|
||||
@property
|
||||
def days(self):
|
||||
self.unit = "days"
|
||||
return self
|
||||
|
||||
@property
|
||||
def week(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError("Use weeks instead of week")
|
||||
return self.weeks
|
||||
|
||||
@property
|
||||
def weeks(self):
|
||||
self.unit = "weeks"
|
||||
return self
|
||||
|
||||
@property
|
||||
def monday(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError(
|
||||
"Scheduling .monday() jobs is only allowed for weekly jobs. "
|
||||
"Using .monday() on a job scheduled to run every 2 or more weeks "
|
||||
"is not supported."
|
||||
)
|
||||
self.start_day = "monday"
|
||||
return self.weeks
|
||||
|
||||
@property
|
||||
def tuesday(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError(
|
||||
"Scheduling .tuesday() jobs is only allowed for weekly jobs. "
|
||||
"Using .tuesday() on a job scheduled to run every 2 or more weeks "
|
||||
"is not supported."
|
||||
)
|
||||
self.start_day = "tuesday"
|
||||
return self.weeks
|
||||
|
||||
@property
|
||||
def wednesday(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError(
|
||||
"Scheduling .wednesday() jobs is only allowed for weekly jobs. "
|
||||
"Using .wednesday() on a job scheduled to run every 2 or more weeks "
|
||||
"is not supported."
|
||||
)
|
||||
self.start_day = "wednesday"
|
||||
return self.weeks
|
||||
|
||||
@property
|
||||
def thursday(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError(
|
||||
"Scheduling .thursday() jobs is only allowed for weekly jobs. "
|
||||
"Using .thursday() on a job scheduled to run every 2 or more weeks "
|
||||
"is not supported."
|
||||
)
|
||||
self.start_day = "thursday"
|
||||
return self.weeks
|
||||
|
||||
@property
|
||||
def friday(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError(
|
||||
"Scheduling .friday() jobs is only allowed for weekly jobs. "
|
||||
"Using .friday() on a job scheduled to run every 2 or more weeks "
|
||||
"is not supported."
|
||||
)
|
||||
self.start_day = "friday"
|
||||
return self.weeks
|
||||
|
||||
@property
|
||||
def saturday(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError(
|
||||
"Scheduling .saturday() jobs is only allowed for weekly jobs. "
|
||||
"Using .saturday() on a job scheduled to run every 2 or more weeks "
|
||||
"is not supported."
|
||||
)
|
||||
self.start_day = "saturday"
|
||||
return self.weeks
|
||||
|
||||
@property
|
||||
def sunday(self):
|
||||
if self.interval != 1:
|
||||
raise IntervalError(
|
||||
"Scheduling .sunday() jobs is only allowed for weekly jobs. "
|
||||
"Using .sunday() on a job scheduled to run every 2 or more weeks "
|
||||
"is not supported."
|
||||
)
|
||||
self.start_day = "sunday"
|
||||
return self.weeks
|
||||
|
||||
def tag(self, *tags: Hashable):
|
||||
"""
|
||||
Tags the job with one or more unique identifiers.
|
||||
|
||||
Tags must be hashable. Duplicate tags are discarded.
|
||||
|
||||
:param tags: A unique list of ``Hashable`` tags.
|
||||
:return: The invoked job instance
|
||||
"""
|
||||
if not all(isinstance(tag, Hashable) for tag in tags):
|
||||
raise TypeError("Tags must be hashable")
|
||||
self.tags.update(tags)
|
||||
return self
|
||||
|
||||
def at(self, time_str):
|
||||
|
||||
"""
|
||||
Specify a particular time that the job should be run at.
|
||||
|
||||
:param time_str: A string in one of the following formats:
|
||||
|
||||
- For daily jobs -> `HH:MM:SS` or `HH:MM`
|
||||
- For hourly jobs -> `MM:SS` or `:MM`
|
||||
- For minute jobs -> `:SS`
|
||||
|
||||
The format must make sense given how often the job is
|
||||
repeating; for example, a job that repeats every minute
|
||||
should not be given a string in the form `HH:MM:SS`. The
|
||||
difference between `:MM` and :SS` is inferred from the
|
||||
selected time-unit (e.g. `every().hour.at(':30')` vs.
|
||||
`every().minute.at(':30')`).
|
||||
|
||||
:return: The invoked job instance
|
||||
"""
|
||||
if self.unit not in ("days", "hours", "minutes") and not self.start_day:
|
||||
raise ScheduleValueError(
|
||||
"Invalid unit (valid units are `days`, `hours`, and `minutes`)"
|
||||
)
|
||||
if not isinstance(time_str, str):
|
||||
raise TypeError("at() should be passed a string")
|
||||
if self.unit == "days" or self.start_day:
|
||||
if not re.match(r"^([0-2]\d:)?[0-5]\d:[0-5]\d$", time_str):
|
||||
raise ScheduleValueError(
|
||||
"Invalid time format for a daily job (valid format is HH:MM(:SS)?)"
|
||||
)
|
||||
if self.unit == "hours":
|
||||
if not re.match(r"^([0-5]\d)?:[0-5]\d$", time_str):
|
||||
raise ScheduleValueError(
|
||||
"Invalid time format for an hourly job (valid format is (MM)?:SS)"
|
||||
)
|
||||
|
||||
if self.unit == "minutes":
|
||||
if not re.match(r"^:[0-5]\d$", time_str):
|
||||
raise ScheduleValueError(
|
||||
"Invalid time format for a minutely job (valid format is :SS)"
|
||||
)
|
||||
time_values = time_str.split(":")
|
||||
hour: Union[str, int]
|
||||
minute: Union[str, int]
|
||||
second: Union[str, int]
|
||||
if len(time_values) == 3:
|
||||
hour, minute, second = time_values
|
||||
elif len(time_values) == 2 and self.unit == "minutes":
|
||||
hour = 0
|
||||
minute = 0
|
||||
_, second = time_values
|
||||
elif len(time_values) == 2 and self.unit == "hours" and len(time_values[0]):
|
||||
hour = 0
|
||||
minute, second = time_values
|
||||
else:
|
||||
hour, minute = time_values
|
||||
second = 0
|
||||
if self.unit == "days" or self.start_day:
|
||||
hour = int(hour)
|
||||
if not (0 <= hour <= 23):
|
||||
raise ScheduleValueError(
|
||||
"Invalid number of hours ({} is not between 0 and 23)"
|
||||
)
|
||||
elif self.unit == "hours":
|
||||
hour = 0
|
||||
elif self.unit == "minutes":
|
||||
hour = 0
|
||||
minute = 0
|
||||
minute = int(minute)
|
||||
second = int(second)
|
||||
self.at_time = datetime.time(hour, minute, second)
|
||||
return self
|
||||
|
||||
def to(self, latest: int):
|
||||
"""
|
||||
Schedule the job to run at an irregular (randomized) interval.
|
||||
|
||||
The job's interval will randomly vary from the value given
|
||||
to `every` to `latest`. The range defined is inclusive on
|
||||
both ends. For example, `every(A).to(B).seconds` executes
|
||||
the job function every N seconds such that A <= N <= B.
|
||||
|
||||
:param latest: Maximum interval between randomized job runs
|
||||
:return: The invoked job instance
|
||||
"""
|
||||
self.latest = latest
|
||||
return self
|
||||
|
||||
def until(
|
||||
self,
|
||||
until_time: Union[datetime.datetime, datetime.timedelta, datetime.time, str],
|
||||
):
|
||||
"""
|
||||
Schedule job to run until the specified moment.
|
||||
|
||||
The job is canceled whenever the next run is calculated and it turns out the
|
||||
next run is after the until_time. The job is also canceled right before it runs,
|
||||
if the current time is after until_time. This latter case can happen when the
|
||||
the job was scheduled to run before until_time, but runs after until_time.
|
||||
|
||||
If until_time is a moment in the past, ScheduleValueError is thrown.
|
||||
|
||||
:param until_time: A moment in the future representing the latest time a job can
|
||||
be run. If only a time is supplied, the date is set to today.
|
||||
The following formats are accepted:
|
||||
|
||||
- datetime.datetime
|
||||
- datetime.timedelta
|
||||
- datetime.time
|
||||
- String in one of the following formats: "%Y-%m-%d %H:%M:%S",
|
||||
"%Y-%m-%d %H:%M", "%Y-%m-%d", "%H:%M:%S", "%H:%M"
|
||||
as defined by strptime() behaviour. If an invalid string format is passed,
|
||||
ScheduleValueError is thrown.
|
||||
|
||||
:return: The invoked job instance
|
||||
"""
|
||||
|
||||
if isinstance(until_time, datetime.datetime):
|
||||
self.cancel_after = until_time
|
||||
elif isinstance(until_time, datetime.timedelta):
|
||||
self.cancel_after = datetime.datetime.now() + until_time
|
||||
elif isinstance(until_time, datetime.time):
|
||||
self.cancel_after = datetime.datetime.combine(
|
||||
datetime.datetime.now(), until_time
|
||||
)
|
||||
elif isinstance(until_time, str):
|
||||
cancel_after = self._decode_datetimestr(
|
||||
until_time,
|
||||
[
|
||||
"%Y-%m-%d %H:%M:%S",
|
||||
"%Y-%m-%d %H:%M",
|
||||
"%Y-%m-%d",
|
||||
"%H:%M:%S",
|
||||
"%H:%M",
|
||||
],
|
||||
)
|
||||
if cancel_after is None:
|
||||
raise ScheduleValueError("Invalid string format for until()")
|
||||
if "-" not in until_time:
|
||||
# the until_time is a time-only format. Set the date to today
|
||||
now = datetime.datetime.now()
|
||||
cancel_after = cancel_after.replace(
|
||||
year=now.year, month=now.month, day=now.day
|
||||
)
|
||||
self.cancel_after = cancel_after
|
||||
else:
|
||||
raise TypeError(
|
||||
"until() takes a string, datetime.datetime, datetime.timedelta, "
|
||||
"datetime.time parameter"
|
||||
)
|
||||
if self.cancel_after < datetime.datetime.now():
|
||||
raise ScheduleValueError(
|
||||
"Cannot schedule a job to run until a time in the past"
|
||||
)
|
||||
return self
|
||||
|
||||
def do(self, job_func: Callable, *args, **kwargs):
|
||||
"""
|
||||
Specifies the job_func that should be called every time the
|
||||
job runs.
|
||||
|
||||
Any additional arguments are passed on to job_func when
|
||||
the job runs.
|
||||
|
||||
:param job_func: The function to be scheduled
|
||||
:return: The invoked job instance
|
||||
"""
|
||||
self.job_func = functools.partial(job_func, *args, **kwargs)
|
||||
functools.update_wrapper(self.job_func, job_func)
|
||||
self._schedule_next_run()
|
||||
if self.scheduler is None:
|
||||
raise ScheduleError(
|
||||
"Unable to a add job to schedule. "
|
||||
"Job is not associated with an scheduler"
|
||||
)
|
||||
self.scheduler.jobs.append(self)
|
||||
return self
|
||||
|
||||
@property
|
||||
def should_run(self) -> bool:
|
||||
"""
|
||||
:return: ``True`` if the job should be run now.
|
||||
"""
|
||||
assert self.next_run is not None, "must run _schedule_next_run before"
|
||||
return datetime.datetime.now() >= self.next_run
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Run the job and immediately reschedule it.
|
||||
If the job's deadline is reached (configured using .until()), the job is not
|
||||
run and CancelJob is returned immediately. If the next scheduled run exceeds
|
||||
the job's deadline, CancelJob is returned after the execution. In this latter
|
||||
case CancelJob takes priority over any other returned value.
|
||||
|
||||
:return: The return value returned by the `job_func`, or CancelJob if the job's
|
||||
deadline is reached.
|
||||
|
||||
"""
|
||||
if self._is_overdue(datetime.datetime.now()):
|
||||
logger.debug("Cancelling job %s", self)
|
||||
return CancelJob
|
||||
|
||||
logger.debug("Running job %s", self)
|
||||
ret = self.job_func()
|
||||
self.last_run = datetime.datetime.now()
|
||||
self._schedule_next_run()
|
||||
|
||||
if self._is_overdue(self.next_run):
|
||||
logger.debug("Cancelling job %s", self)
|
||||
return CancelJob
|
||||
return ret
|
||||
|
||||
def _schedule_next_run(self) -> None:
|
||||
"""
|
||||
Compute the instant when this job should run next.
|
||||
"""
|
||||
if self.unit not in ("seconds", "minutes", "hours", "days", "weeks"):
|
||||
raise ScheduleValueError(
|
||||
"Invalid unit (valid units are `seconds`, `minutes`, `hours`, "
|
||||
"`days`, and `weeks`)"
|
||||
)
|
||||
|
||||
if self.latest is not None:
|
||||
if not (self.latest >= self.interval):
|
||||
raise ScheduleError("`latest` is greater than `interval`")
|
||||
interval = random.randint(self.interval, self.latest)
|
||||
else:
|
||||
interval = self.interval
|
||||
|
||||
self.period = datetime.timedelta(**{self.unit: interval})
|
||||
self.next_run = datetime.datetime.now() + self.period
|
||||
if self.start_day is not None:
|
||||
if self.unit != "weeks":
|
||||
raise ScheduleValueError("`unit` should be 'weeks'")
|
||||
weekdays = (
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
)
|
||||
if self.start_day not in weekdays:
|
||||
raise ScheduleValueError(
|
||||
"Invalid start day (valid start days are {})".format(weekdays)
|
||||
)
|
||||
weekday = weekdays.index(self.start_day)
|
||||
days_ahead = weekday - self.next_run.weekday()
|
||||
if days_ahead <= 0: # Target day already happened this week
|
||||
days_ahead += 7
|
||||
self.next_run += datetime.timedelta(days_ahead) - self.period
|
||||
if self.at_time is not None:
|
||||
if self.unit not in ("days", "hours", "minutes") and self.start_day is None:
|
||||
raise ScheduleValueError("Invalid unit without specifying start day")
|
||||
kwargs = {"second": self.at_time.second, "microsecond": 0}
|
||||
if self.unit == "days" or self.start_day is not None:
|
||||
kwargs["hour"] = self.at_time.hour
|
||||
if self.unit in ["days", "hours"] or self.start_day is not None:
|
||||
kwargs["minute"] = self.at_time.minute
|
||||
self.next_run = self.next_run.replace(**kwargs) # type: ignore
|
||||
# Make sure we run at the specified time *today* (or *this hour*)
|
||||
# as well. This accounts for when a job takes so long it finished
|
||||
# in the next period.
|
||||
if not self.last_run or (self.next_run - self.last_run) > self.period:
|
||||
now = datetime.datetime.now()
|
||||
if (
|
||||
self.unit == "days"
|
||||
and self.at_time > now.time()
|
||||
and self.interval == 1
|
||||
):
|
||||
self.next_run = self.next_run - datetime.timedelta(days=1)
|
||||
elif self.unit == "hours" and (
|
||||
self.at_time.minute > now.minute
|
||||
or (
|
||||
self.at_time.minute == now.minute
|
||||
and self.at_time.second > now.second
|
||||
)
|
||||
):
|
||||
self.next_run = self.next_run - datetime.timedelta(hours=1)
|
||||
elif self.unit == "minutes" and self.at_time.second > now.second:
|
||||
self.next_run = self.next_run - datetime.timedelta(minutes=1)
|
||||
if self.start_day is not None and self.at_time is not None:
|
||||
# Let's see if we will still make that time we specified today
|
||||
if (self.next_run - datetime.datetime.now()).days >= 7:
|
||||
self.next_run -= self.period
|
||||
|
||||
def _is_overdue(self, when: datetime.datetime):
|
||||
return self.cancel_after is not None and when > self.cancel_after
|
||||
|
||||
def _decode_datetimestr(
|
||||
self, datetime_str: str, formats: List[str]
|
||||
) -> Optional[datetime.datetime]:
|
||||
for f in formats:
|
||||
try:
|
||||
return datetime.datetime.strptime(datetime_str, f)
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# The following methods are shortcuts for not having to
|
||||
# create a Scheduler instance:
|
||||
|
||||
#: Default :class:`Scheduler <Scheduler>` object
|
||||
default_scheduler = Scheduler()
|
||||
|
||||
#: Default :class:`Jobs <Job>` list
|
||||
jobs = default_scheduler.jobs # todo: should this be a copy, e.g. jobs()?
|
||||
|
||||
|
||||
def every(interval: int = 1) -> Job:
|
||||
"""Calls :meth:`every <Scheduler.every>` on the
|
||||
:data:`default scheduler instance <default_scheduler>`.
|
||||
"""
|
||||
return default_scheduler.every(interval)
|
||||
|
||||
|
||||
def run_pending() -> None:
|
||||
"""Calls :meth:`run_pending <Scheduler.run_pending>` on the
|
||||
:data:`default scheduler instance <default_scheduler>`.
|
||||
"""
|
||||
default_scheduler.run_pending()
|
||||
|
||||
|
||||
def run_all(delay_seconds: int = 0) -> None:
|
||||
"""Calls :meth:`run_all <Scheduler.run_all>` on the
|
||||
:data:`default scheduler instance <default_scheduler>`.
|
||||
"""
|
||||
default_scheduler.run_all(delay_seconds=delay_seconds)
|
||||
|
||||
|
||||
def get_jobs(tag: Optional[Hashable] = None) -> List[Job]:
|
||||
"""Calls :meth:`get_jobs <Scheduler.get_jobs>` on the
|
||||
:data:`default scheduler instance <default_scheduler>`.
|
||||
"""
|
||||
return default_scheduler.get_jobs(tag)
|
||||
|
||||
|
||||
def clear(tag: Optional[Hashable] = None) -> None:
|
||||
"""Calls :meth:`clear <Scheduler.clear>` on the
|
||||
:data:`default scheduler instance <default_scheduler>`.
|
||||
"""
|
||||
default_scheduler.clear(tag)
|
||||
|
||||
|
||||
def cancel_job(job: Job) -> None:
|
||||
"""Calls :meth:`cancel_job <Scheduler.cancel_job>` on the
|
||||
:data:`default scheduler instance <default_scheduler>`.
|
||||
"""
|
||||
default_scheduler.cancel_job(job)
|
||||
|
||||
|
||||
def next_run() -> Optional[datetime.datetime]:
|
||||
"""Calls :meth:`next_run <Scheduler.next_run>` on the
|
||||
:data:`default scheduler instance <default_scheduler>`.
|
||||
"""
|
||||
return default_scheduler.next_run
|
||||
|
||||
|
||||
def idle_seconds() -> Optional[float]:
|
||||
"""Calls :meth:`idle_seconds <Scheduler.idle_seconds>` on the
|
||||
:data:`default scheduler instance <default_scheduler>`.
|
||||
"""
|
||||
return default_scheduler.idle_seconds
|
||||
|
||||
|
||||
def repeat(job, *args, **kwargs):
|
||||
"""
|
||||
Decorator to schedule a new periodic job.
|
||||
|
||||
Any additional arguments are passed on to the decorated function
|
||||
when the job runs.
|
||||
|
||||
:param job: a :class:`Jobs <Job>`
|
||||
"""
|
||||
|
||||
def _schedule_decorator(decorated_function):
|
||||
job.do(decorated_function, *args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
return _schedule_decorator
|
@ -0,0 +1,18 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
@ -0,0 +1,338 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
|
||||
import sys
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmcvfs
|
||||
import os
|
||||
import logging
|
||||
import http.cookiejar
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import inputstreamhelper
|
||||
import resources.lib.common.vars as common_vars
|
||||
import resources.lib.common.functions as common_functions
|
||||
|
||||
|
||||
def init_AddonCookieJar(NAME, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Initialize the common_vars.__tvrplus_CookieJar__ variable.
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: common_vars.__logger__ name to use for sending the log messages
|
||||
# DATA_DIR: The addon's 'userdata' directory.
|
||||
#
|
||||
####
|
||||
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# File containing the session cookies
|
||||
cookies_file = os.path.join(DATA_DIR, common_vars.__tvrplus_CookiesFilename__)
|
||||
common_vars.__logger__.debug('[ Addon cookies file ] cookies_file = ' + str(cookies_file))
|
||||
common_vars.__tvrplus_CookieJar__ = http.cookiejar.MozillaCookieJar(cookies_file)
|
||||
|
||||
# If it doesn't exist already, create a new file where the cookies should be saved
|
||||
if not os.path.exists(cookies_file):
|
||||
common_vars.__tvrplus_CookieJar__.save()
|
||||
common_vars.__logger__.debug('[ Addon cookiefile ] Created cookiejar file: ' + str(cookies_file))
|
||||
|
||||
# Load any cookies saved from the last run
|
||||
common_vars.__tvrplus_CookieJar__.load()
|
||||
common_vars.__logger__.debug('[ Addon cookiejar ] Loaded cookiejar from file: ' + str(cookies_file))
|
||||
|
||||
|
||||
def list_categories(NAME, COOKIEJAR, SESSION, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Create the list of video categories in the Kodi interface.
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def list_channels(NAME, COOKIEJAR, SESSION, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Create the list of playable videos in the Kodi interface.
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: Logger name to use for sending the log messages.
|
||||
# COOKIEJAR: The cookiejar to be used with the given session.
|
||||
# SESSION: The session to be used for login this call
|
||||
# DATA_DIR: The addon's 'userdata' directory.
|
||||
#
|
||||
# Return: The list of cached video categories
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Get the list of videos in the category.
|
||||
channels = get_channels(NAME, COOKIEJAR, SESSION)
|
||||
common_vars.__logger__.debug('Received channels = ' + str(channels))
|
||||
|
||||
for channel in channels:
|
||||
common_vars.__logger__.debug('Channel data => ' + str(channel))
|
||||
common_vars.__logger__.debug('Channel name: ' + channel['name'])
|
||||
common_vars.__logger__.debug('Channel url: ' + channel['url'])
|
||||
common_vars.__logger__.debug('Channel logo: ' + channel['logo'])
|
||||
common_vars.__logger__.debug('Channel ID: ' + channel['id'])
|
||||
|
||||
# Create a list item with a text label and a thumbnail image.
|
||||
list_item = xbmcgui.ListItem(label=channel['name'])
|
||||
|
||||
# Set additional info for the list item.
|
||||
# For available properties see https://codedocs.xyz/xbmc/xbmc/group__python__xbmcgui__listitem.html#ga0b71166869bda87ad744942888fb5f14
|
||||
# 'mediatype' is needed for skin to display info for this ListItem correctly.
|
||||
list_item.setInfo('video', {'title': channel['name'],
|
||||
'genre': 'General',
|
||||
'mediatype': 'video'})
|
||||
|
||||
|
||||
# Set graphics (thumbnail, fanart, banner, poster, landscape etc.) for the list item.
|
||||
list_item.setArt({'thumb': channel['logo']})
|
||||
|
||||
# Set 'IsPlayable' property to 'true'.
|
||||
# This is mandatory for playable items!
|
||||
list_item.setProperty('IsPlayable', 'true')
|
||||
|
||||
# Create a URL for a plugin recursive call.
|
||||
# Example: plugin://plugin.video.example/?action=play&channel_endpoint=/filme/tnt&channel_metadata=...
|
||||
url = common_functions.get_url(action='play', account='tvrplus.ro', channel_endpoint=channel['url'])
|
||||
common_vars.__logger__.debug('URL for plugin recursive call: ' + url)
|
||||
|
||||
# This means that this item won't open any sub-list.
|
||||
is_folder = False
|
||||
|
||||
# Add our item to the Kodi virtual folder listing.
|
||||
xbmcplugin.addDirectoryItem(int(common_vars.__handle__), url, list_item, is_folder)
|
||||
|
||||
# Add a sort method for the virtual folder items (alphabetically, ignore articles)
|
||||
xbmcplugin.addSortMethod(int(common_vars.__handle__), xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE)
|
||||
|
||||
# Finish creating a virtual folder.
|
||||
xbmcplugin.endOfDirectory(int(common_vars.__handle__))
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def get_channels(NAME, COOKIEJAR, SESSION):
|
||||
####
|
||||
#
|
||||
# Get from tvrplus.ro the list of channels/streams.
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: Logger name to use for sending the log messages
|
||||
# COOKIEJAR: The cookiejar to be used with the given session
|
||||
# SESSION: The session to be used for this call
|
||||
# DATA_DIR: The addon's 'userdata' directory.
|
||||
#
|
||||
# Return: The list of channels/streams in the given category
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
MyAddon = xbmcaddon.Addon(id=common_vars.__AddonID__)
|
||||
__tvrplus_cacert__ = MyAddon.getAddonInfo('path') + 'resources/lib/tvrplus/tvrplus-ro-chain.pem'
|
||||
|
||||
# Setup headers for the request
|
||||
MyHeaders = {
|
||||
'Host': 'www.tvrplus.ro',
|
||||
'User-Agent': common_vars.__tvrplus_userAgent__,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('URL: https://www.tvrplus.ro')
|
||||
common_vars.__logger__.debug('Method: GET')
|
||||
|
||||
# Send the GET request
|
||||
_request_ = SESSION.get('https://www.tvrplus.ro', headers=MyHeaders, verify=__tvrplus_cacert__)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
_raw_channels_ = re.findall('<div class="big-gallery">(.+?)</div>', _request_.content.decode(), re.IGNORECASE|re.DOTALL)
|
||||
common_vars.__logger__.debug('Found _raw_channels_ = ' + str(_raw_channels_))
|
||||
|
||||
# Initialize the list of channels
|
||||
_channels_ = []
|
||||
|
||||
for _raw_channel_ in _raw_channels_:
|
||||
common_vars.__logger__.debug('_raw_channel_ = ' + str(_raw_channel_))
|
||||
|
||||
_channel_record_ = {}
|
||||
|
||||
_channel_name_ = re.findall('alt="(.+?)"', _raw_channel_, re.IGNORECASE)[0]
|
||||
common_vars.__logger__.debug('_channel_name_ = ' + str(_channel_name_))
|
||||
|
||||
_channel_url_ = re.findall('<a href="(.+?)"', _raw_channel_, re.IGNORECASE)[0]
|
||||
common_vars.__logger__.debug('_channel_url_ = ' + str(_channel_url_))
|
||||
|
||||
_channel_logo_ = re.findall('<img src="(.+?)"', _raw_channel_, re.IGNORECASE)[0]
|
||||
common_vars.__logger__.debug('_channel_logo_ = ' + str(_channel_logo_))
|
||||
|
||||
_channel_id_ = re.findall('live/(.+?)"', _raw_channel_, re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('_channel_id_ = ' + str(_channel_id_))
|
||||
|
||||
_channel_record_["name"] = _channel_name_
|
||||
_channel_record_["url"] = _channel_url_
|
||||
_channel_record_["logo"] = _channel_logo_
|
||||
_channel_record_["id"] = _channel_id_
|
||||
|
||||
common_vars.__logger__.debug('Created: _channel_record_ = ' + str(_channel_record_))
|
||||
_channels_.append(_channel_record_)
|
||||
|
||||
common_vars.__logger__.debug('_channels_ = ' + str(_channels_))
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
return _channels_
|
||||
|
||||
|
||||
|
||||
|
||||
def play_video(CHANNEL_ENDPOINT, NAME, COOKIEJAR, SESSION, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Play a video by the provided path.
|
||||
#
|
||||
# Parameters:
|
||||
# path: Fully-qualified video URL
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
common_vars.__logger__.debug('Chanel endpoint = ' + CHANNEL_ENDPOINT)
|
||||
|
||||
MyAddon = xbmcaddon.Addon(id=common_vars.__AddonID__)
|
||||
__tvrplus_cacert__ = MyAddon.getAddonInfo('path') + 'resources/lib/tvrplus/tvrplus-ro-chain.pem'
|
||||
|
||||
# Get the URL for the stream m3u8 file
|
||||
|
||||
# Setup headers for the request
|
||||
MyHeaders = {
|
||||
'Host': 'www.tvrplus.ro',
|
||||
'User-Agent': common_vars.__tvrplus_userAgent__,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('URL: ' + CHANNEL_ENDPOINT)
|
||||
common_vars.__logger__.debug('Method: GET')
|
||||
|
||||
# Send the GET request
|
||||
_request_ = SESSION.get(CHANNEL_ENDPOINT, headers=MyHeaders, verify=__tvrplus_cacert__)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
_stream_manifest_url_ = re.findall('src: \'(.+?)\'', _request_.content.decode(), re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('Found _stream_manifest_url_ = ' + _stream_manifest_url_)
|
||||
|
||||
# # Set the headers to be used with imputstream.adaptive
|
||||
# _headers_ = ''
|
||||
# _headers_ = _headers_ + '&User-Agent=' + common_vars.__digionline_userAgent__
|
||||
|
||||
# common_vars.__logger__.debug('Created: _headers_ = ' + _headers_)
|
||||
|
||||
# # Create a playable item with a path to play.
|
||||
# # See: https://github.com/peak3d/inputstream.adaptive/issues/131#issuecomment-375059796
|
||||
# is_helper = inputstreamhelper.Helper('hls')
|
||||
# if is_helper.check_inputstream():
|
||||
# play_item = xbmcgui.ListItem(path=_stream_manifest_url_ + '|' + _headers_)
|
||||
# play_item.setProperty('inputstream', 'inputstream.adaptive')
|
||||
# play_item.setProperty('inputstream.adaptive.stream_headers', _headers_)
|
||||
# play_item.setProperty('inputstream.adaptive.manifest_type', 'hls')
|
||||
# play_item.setMimeType('application/vnd.apple.mpegurl')
|
||||
# play_item.setContentLookup(False)
|
||||
#
|
||||
# # Pass the item to the Kodi player.
|
||||
# xbmcplugin.setResolvedUrl(int(common_vars.__handle__), True, listitem=play_item)
|
||||
|
||||
play_item = xbmcgui.ListItem(path=_stream_manifest_url_)
|
||||
|
||||
# Pass the item to the Kodi player.
|
||||
xbmcplugin.setResolvedUrl(int(common_vars.__handle__), True, listitem=play_item)
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def PVRIPTVSimpleClientIntegration_update_m3u_file(M3U_FILE, START_NUMBER, NAME, COOKIEJAR, SESSION):
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
common_vars.__logger__.debug('M3U_FILE = ' + M3U_FILE)
|
||||
common_vars.__logger__.debug('START_NUMBER = ' + str(START_NUMBER))
|
||||
|
||||
# Get the list of channels in the category.
|
||||
channels = get_channels(NAME, COOKIEJAR, SESSION)
|
||||
common_vars.__logger__.debug('Received channels = ' + str(channels))
|
||||
|
||||
_CHNO_ = START_NUMBER
|
||||
_data_file_ = open(M3U_FILE, 'a', encoding='utf-8')
|
||||
|
||||
for channel in channels:
|
||||
_line_ = "#EXTINF:0 tvg-id=\"tvrplus__" + str(channel['id']) + "\" tvg-name=\"" + channel['name'] + "\" tvg-logo=\"" + channel['logo'] + "\" tvg-chno=\"" + str(_CHNO_) + "\" group-title=\"TVR Plus\"," + channel['name']
|
||||
|
||||
_url_ = common_functions.get_url(action='play', account='tvrplus.ro', channel_endpoint=channel['url'])
|
||||
_play_url_ = "plugin://" + common_vars.__AddonID__ + "/" + _url_
|
||||
|
||||
_data_file_.write(_line_ + "\n")
|
||||
_data_file_.write(_play_url_ + "\n")
|
||||
|
||||
_CHNO_ = _CHNO_ + 1
|
||||
|
||||
_data_file_.close()
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
return _CHNO_
|
||||
|
@ -0,0 +1,55 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFyzCCBLOgAwIBAgIQCgWbJfVLPYeUzGYxR3U4ozANBgkqhkiG9w0BAQsFADBh
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
|
||||
QTAeFw0yMjA1MDQwMDAwMDBaFw0zMTExMDkyMzU5NTlaMFwxCzAJBgNVBAYTAlVT
|
||||
MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE0MDIGA1UEAxMrUmFwaWRTU0wgR2xv
|
||||
YmFsIFRMUyBSU0E0MDk2IFNIQTI1NiAyMDIyIENBMTCCAiIwDQYJKoZIhvcNAQEB
|
||||
BQADggIPADCCAgoCggIBAKY5PJhwCX2UyBb1nelu9APen53D5+C40T+BOZfSFaB0
|
||||
v0WJM3BGMsuiHZX2IHtwnjUhLL25d8tgLASaUNHCBNKKUlUGRXGztuDIeXb48d64
|
||||
k7Gk7u7mMRSrj+yuLSWOKnK6OGKe9+s6oaVIjHXY+QX8p2I2S3uew0bW3BFpkeAr
|
||||
LBCU25iqeaoLEOGIa09DVojd3qc/RKqr4P11173R+7Ub05YYhuIcSv8e0d7qN1sO
|
||||
1+lfoNMVfV9WcqPABmOasNJ+ol0hAC2PTgRLy/VZo1L0HRMr6j8cbR7q0nKwdbn4
|
||||
Ar+ZMgCgCcG9zCMFsuXYl/rqobiyV+8U37dDScAebZTIF/xPEvHcmGi3xxH6g+dT
|
||||
CjetOjJx8sdXUHKXGXC9ka33q7EzQIYlZISF7EkbT5dZHsO2DOMVLBdP1N1oUp0/
|
||||
1f6fc8uTDduELoKBRzTTZ6OOBVHeZyFZMMdi6tA5s/jxmb74lqH1+jQ6nTU2/Mma
|
||||
hGNxUuJpyhUHezgBA6sto5lNeyqc+3Cr5ehFQzUuwNsJaWbDdQk1v7lqRaqOlYjn
|
||||
iomOl36J5txTs0wL7etCeMRfyPsmc+8HmH77IYVMUOcPJb+0gNuSmAkvf5QXbgPI
|
||||
Zursn/UYnP9obhNbHc/9LYdQkB7CXyX9mPexnDNO7pggNA2jpbEarLmZGi4grMmf
|
||||
AgMBAAGjggGCMIIBfjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBTwnIX9
|
||||
op99j8lou9XUiU0dvtOQ/zAfBgNVHSMEGDAWgBQD3lA1VtFMu2bwo+IbG8OXsj3R
|
||||
VTAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
|
||||
MHYGCCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNl
|
||||
cnQuY29tMEAGCCsGAQUFBzAChjRodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20v
|
||||
RGlnaUNlcnRHbG9iYWxSb290Q0EuY3J0MEIGA1UdHwQ7MDkwN6A1oDOGMWh0dHA6
|
||||
Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwPQYD
|
||||
VR0gBDYwNDALBglghkgBhv1sAgEwBwYFZ4EMAQEwCAYGZ4EMAQIBMAgGBmeBDAEC
|
||||
AjAIBgZngQwBAgMwDQYJKoZIhvcNAQELBQADggEBAAfjh/s1f5dDdfm0sNm74/dW
|
||||
MbbsxfYV1LoTpFt+3MSUWvSbiPQfUkoV57b5rutRJvnPP9mSlpFwcZ3e1nSUbi2o
|
||||
ITGA7RCOj23I1F4zk0YJm42qAwJIqOVenR3XtyQ2VR82qhC6xslxtNf7f2Ndx2G7
|
||||
Mem4wpFhyPDT2P6UJ2MnrD+FC//ZKH5/ERo96ghz8VqNlmL5RXo8Ks9rMr/Ad9xw
|
||||
Y4hyRvAz5920myUffwdUqc0SvPlFnahsZg15uT5HkK48tHR0TLuLH8aRpzh4KJ/Y
|
||||
p0sARNb+9i1R4Fg5zPNvHs2BbIve0vkwxAy+R4727qYzl3027w9jEFC6HMXRaDc=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
|
||||
QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
|
||||
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
|
||||
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
|
||||
CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
|
||||
nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
|
||||
43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
|
||||
T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
|
||||
gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
|
||||
BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
|
||||
TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
|
||||
DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
|
||||
hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
|
||||
06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
|
||||
PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
|
||||
YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
|
||||
CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
|
||||
-----END CERTIFICATE-----
|
@ -0,0 +1,18 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
@ -0,0 +1,755 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
|
||||
import sys
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
import xbmcvfs
|
||||
import os
|
||||
import logging
|
||||
import http.cookiejar
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import inputstreamhelper
|
||||
import resources.lib.common.vars as common_vars
|
||||
import resources.lib.common.functions as common_functions
|
||||
|
||||
|
||||
def init_AddonCookieJar(NAME, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Initialize the common_vars.__voyo_CookieJar__ variable.
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: common_vars.__logger__ name to use for sending the log messages
|
||||
# DATA_DIR: The addon's 'userdata' directory.
|
||||
#
|
||||
####
|
||||
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# File containing the session cookies
|
||||
cookies_file = os.path.join(DATA_DIR, common_vars.__voyo_CookiesFilename__)
|
||||
common_vars.__logger__.debug('[ Addon cookies file ] cookies_file = ' + str(cookies_file))
|
||||
common_vars.__voyo_CookieJar__ = http.cookiejar.MozillaCookieJar(cookies_file)
|
||||
|
||||
# If it doesn't exist already, create a new file where the cookies should be saved
|
||||
if not os.path.exists(cookies_file):
|
||||
common_vars.__voyo_CookieJar__.save()
|
||||
common_vars.__logger__.debug('[ Addon cookiefile ] Created cookiejar file: ' + str(cookies_file))
|
||||
|
||||
# Load any cookies saved from the last run
|
||||
common_vars.__voyo_CookieJar__.load()
|
||||
common_vars.__logger__.debug('[ Addon cookiejar ] Loaded cookiejar from file: ' + str(cookies_file))
|
||||
|
||||
|
||||
|
||||
def do_auth_check(NAME, COOKIEJAR, SESSION):
|
||||
####
|
||||
#
|
||||
# Check if the user is logged-in to voyo.ro
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: Logger name to use for sending the log messages
|
||||
# COOKIEJAR: The cookiejar to be used with the given session
|
||||
# SESSION: The session to be used for login
|
||||
#
|
||||
# Return: dict variable with authentication details
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Setup headers for the request
|
||||
MyHeaders = {
|
||||
'Host': 'crm.cms.protv.ro',
|
||||
'Origin': 'https://voyo.protv.ro',
|
||||
'Referer': 'https://voyo.protv.ro',
|
||||
'User-Agent': common_vars.__voyo_userAgent__,
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('URL: https://crm.cms.protv.ro/api/v1/users/login-check')
|
||||
common_vars.__logger__.debug('Method: GET')
|
||||
|
||||
# Send the GET request
|
||||
_request_ = SESSION.get('https://crm.cms.protv.ro/api/v1/users/login-check', headers=MyHeaders)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
if _request_.status_code == 401:
|
||||
_api_response_ = json.loads(_request_.content.decode())
|
||||
common_vars.__logger__.debug('_api_response_ = ' + str(_api_response_))
|
||||
|
||||
_return_data_ = _api_response_
|
||||
|
||||
else:
|
||||
_return_data_ = {}
|
||||
_api_response_ = json.loads(_request_.content.decode())
|
||||
common_vars.__logger__.debug('_api_response_ = ' + str(_api_response_))
|
||||
|
||||
_return_data_['status'] = 'authorized'
|
||||
_return_data_['data'] = _api_response_['data']
|
||||
common_vars.__logger__.debug('Created: _return_data_ = ' + str(_return_data_))
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
return _return_data_
|
||||
|
||||
def do_login(NAME, COOKIEJAR, SESSION):
|
||||
####
|
||||
#
|
||||
# Login to voyo.ro for the given session.
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: Logger name to use for sending the log messages
|
||||
# COOKIEJAR: The cookiejar to be used with the given session
|
||||
# SESSION: The session to be used for login
|
||||
#
|
||||
# Return: dict variable with authentication details
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
_auth_check_1_ = do_auth_check(NAME, COOKIEJAR, SESSION)
|
||||
common_vars.__logger__.debug('_auth_check_1_ = ' + str(_auth_check_1_))
|
||||
|
||||
if _auth_check_1_['status'] == "unauthorized":
|
||||
common_vars.__logger__.info('Not authenticated.')
|
||||
|
||||
MyHeaders = {
|
||||
'User-Agent': common_vars.__voyo_userAgent__,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('URL: https://voyo.protv.ro/login')
|
||||
common_vars.__logger__.debug('Method: GET')
|
||||
|
||||
# Send the GET request
|
||||
_request_ = SESSION.get('https://voyo.protv.ro/login', headers=MyHeaders, allow_redirects=False)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
|
||||
MyHeaders = {
|
||||
'Host': 'voyo.protv.ro',
|
||||
'Origin': 'https://voyo.protvplus.ro',
|
||||
'User-Agent': common_vars.__voyo_userAgent__,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
# Setup form data to be sent
|
||||
MyFormData = {
|
||||
'email': common_vars.__config_voyo_Username__,
|
||||
'password': common_vars.__config_voyo_Password__,
|
||||
'login': 'Autentificare',
|
||||
'_do': 'content167-loginForm-form-submit'
|
||||
}
|
||||
|
||||
MyFormData_logger = {
|
||||
'email': common_vars.__config_voyo_Username__,
|
||||
'password': '****************',
|
||||
'login': 'Autentificare',
|
||||
'_do': 'content167-loginForm-form-submit'
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('MyFormData: ' + str(MyFormData_logger))
|
||||
common_vars.__logger__.debug('URL: https://voyo.protv.ro/login')
|
||||
common_vars.__logger__.debug('Method: POST')
|
||||
|
||||
# Send the POST request
|
||||
_request_ = SESSION.post('https://voyo.protv.ro/login', headers=MyHeaders, data=MyFormData, allow_redirects=False)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
_auth_check_2_ = do_auth_check(NAME, COOKIEJAR, SESSION)
|
||||
common_vars.__logger__.debug('_auth_check_2_ = ' + str(_auth_check_2_))
|
||||
|
||||
if _auth_check_2_['status'] == "unauthorized":
|
||||
common_vars.__logger__.info('Not authorized.')
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
return _auth_check_2_
|
||||
|
||||
else:
|
||||
common_vars.__logger__.info('Authorized.')
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
return _auth_check_2_
|
||||
|
||||
else:
|
||||
common_vars.__logger__.info('Already authorized.')
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
return _auth_check_1_
|
||||
|
||||
|
||||
def play_video(CHANNEL_ENDPOINT, NAME, COOKIEJAR, SESSION, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Play a video by the provided path.
|
||||
#
|
||||
# Parameters:
|
||||
# path: Fully-qualified video URL
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
common_vars.__logger__.debug('Chanel endpoint = ' + CHANNEL_ENDPOINT)
|
||||
|
||||
_auth_ = do_login(NAME, COOKIEJAR, SESSION)
|
||||
common_vars.__logger__.debug('_auth_ = ' + str(_auth_))
|
||||
|
||||
if _auth_['status'] == "unauthorized":
|
||||
common_vars.__logger__.info('[voyo.ro] => Authentication error => Invalid username or password.')
|
||||
xbmcgui.Dialog().ok('[voyo.ro] => Authentication error', 'Invalid username or password')
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
else:
|
||||
|
||||
__no_valid_subscription__ = False
|
||||
|
||||
if _auth_['data']['is_subscribed']:
|
||||
common_vars.__logger__.info('_auth_[data][is_subscribed] = ' + str(_auth_['data']['is_subscribed']))
|
||||
common_vars.__logger__.info(' __no_valid_subscription__ = ' + str(__no_valid_subscription__))
|
||||
else:
|
||||
__no_valid_subscription__ = True
|
||||
common_vars.__logger__.info('_auth_[data][is_subscribed] = ' + str(_auth_['data']['is_subscribed']))
|
||||
common_vars.__logger__.info(' __no_valid_subscription__ = ' + str(__no_valid_subscription__))
|
||||
|
||||
if __no_valid_subscription__:
|
||||
common_vars.__logger__.info('[voyo.ro] => No valid subscription => Pentru a putea vizualiza acest canal trebuie să ai abonament VOYO.')
|
||||
xbmcgui.Dialog().ok('[voyo.ro] => No valid subscription', 'Pentru a putea vizualiza acest canal trebuie să ai abonament VOYO.')
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
else:
|
||||
# Get the URL for the stream metadata
|
||||
|
||||
# Setup headers for the request
|
||||
MyHeaders = {
|
||||
# 'Host': 'protvplus.ro',
|
||||
'Referer': 'https://voyo.protv.ro',
|
||||
'User-Agent': common_vars.__voyo_userAgent__,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0',
|
||||
#'Authorization': 'Bearer ' + _auth_['data']['bearer']
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('URL: ' + CHANNEL_ENDPOINT)
|
||||
common_vars.__logger__.debug('Method: GET')
|
||||
|
||||
# Send the GET request
|
||||
_request_ = SESSION.get(CHANNEL_ENDPOINT, headers=MyHeaders)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
# _metadata_url_ = re.findall('src="(.+?)\?autoplay', _request_.content.decode(), re.IGNORECASE|re.DOTALL)[0]
|
||||
# common_vars.__logger__.debug('Found _metadata_url_ = ' + str(_metadata_url_))
|
||||
#
|
||||
# _stream_data__host_ = re.findall('//(.+?)/', _metadata_url_, re.IGNORECASE|re.DOTALL)[0]
|
||||
# common_vars.__logger__.debug('Found _stream_data__host_ = ' + str(_stream_data__host_))
|
||||
#
|
||||
# # Get the stream data
|
||||
# # Setup headers for the request
|
||||
# MyHeaders = {
|
||||
# 'Host': _stream_data__host_,
|
||||
# 'Referer': 'https://protvplus.ro',
|
||||
# 'User-Agent': common_vars.__protvplus_userAgent__,
|
||||
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
# 'Accept-Language': 'en-US',
|
||||
# 'Accept-Encoding': 'identity',
|
||||
# 'Connection': 'keep-alive',
|
||||
# 'Upgrade-Insecure-Requests': '1',
|
||||
# 'Cache-Control': 'max-age=0',
|
||||
# #'Authorization': 'Bearer ' + _auth_['data']['bearer']
|
||||
# }
|
||||
#
|
||||
# common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
# common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
# common_vars.__logger__.debug('URL: ' + _metadata_url_)
|
||||
# common_vars.__logger__.debug('Method: GET')
|
||||
#
|
||||
# # Send the GET request
|
||||
# _request_ = SESSION.get(_metadata_url_, headers=MyHeaders)
|
||||
#
|
||||
# # Save cookies for later use.
|
||||
# COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
# common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
# common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
# common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
# common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
_metadata_url_ = CHANNEL_ENDPOINT
|
||||
common_vars.__logger__.debug('_metadata_url_ = ' + str(_metadata_url_))
|
||||
|
||||
_stream_data__host_ = re.findall('//(.+?)/', _metadata_url_, re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('Found _stream_data__host_ = ' + str(_stream_data__host_))
|
||||
|
||||
_raw_stream_data_ = re.findall('\'player-1\', processAdTagModifier\((.+?)\), {"video"', _request_.content.decode(), re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('Found _raw_stream_data_ = ' + str(_raw_stream_data_))
|
||||
|
||||
_stream_data_ = json.loads(_raw_stream_data_)
|
||||
common_vars.__logger__.debug('_stream_data_ = ' + str(_stream_data_))
|
||||
|
||||
_stream_manifest_url_ = _stream_data_['tracks']['HLS'][0]['src']
|
||||
common_vars.__logger__.debug('Found _stream_manifest_url_ = ' + _stream_manifest_url_)
|
||||
|
||||
_stream_manifest_host_ = re.findall('//(.+?)/', _stream_manifest_url_, re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('Found _stream_manifest_host_ = ' + _stream_manifest_host_)
|
||||
|
||||
# Set the headers to be used with imputstream.adaptive
|
||||
_headers_ = ''
|
||||
_headers_ = _headers_ + '&Host=' + _stream_manifest_host_
|
||||
_headers_ = _headers_ + '&Origin=https://' + _stream_data__host_
|
||||
_headers_ = _headers_ + '&Referer=https://' + _stream_data__host_ + '/'
|
||||
_headers_ = _headers_ + '&User-Agent=' + common_vars.__voyo_userAgent__
|
||||
_headers_ = _headers_ + '&Connection=keep-alive'
|
||||
_headers_ = _headers_ + '&Accept-Language=en-US'
|
||||
_headers_ = _headers_ + '&Accept=*/*'
|
||||
_headers_ = _headers_ + '&Accept-Encoding=identity'
|
||||
common_vars.__logger__.debug('Created: _headers_ = ' + _headers_)
|
||||
|
||||
# Create a playable item with a path to play.
|
||||
# See: https://github.com/peak3d/inputstream.adaptive/issues/131#issuecomment-375059796
|
||||
is_helper = inputstreamhelper.Helper('hls')
|
||||
if is_helper.check_inputstream():
|
||||
play_item = xbmcgui.ListItem(path=_stream_manifest_url_ + '|' + _headers_)
|
||||
play_item.setProperty('inputstream', 'inputstream.adaptive')
|
||||
play_item.setProperty('inputstream.adaptive.stream_headers', _headers_)
|
||||
play_item.setProperty('inputstream.adaptive.manifest_type', 'hls')
|
||||
play_item.setMimeType('application/vnd.apple.mpegurl')
|
||||
play_item.setContentLookup(False)
|
||||
|
||||
# Pass the item to the Kodi player.
|
||||
xbmcplugin.setResolvedUrl(int(common_vars.__handle__), True, listitem=play_item)
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def list_categories(NAME, COOKIEJAR, SESSION, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Create the list of video categories in the Kodi interface.
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def list_channels(NAME, COOKIEJAR, SESSION, DATA_DIR):
|
||||
####
|
||||
#
|
||||
# Create the list of playable videos in the Kodi interface.
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: Logger name to use for sending the log messages.
|
||||
# COOKIEJAR: The cookiejar to be used with the given session.
|
||||
# SESSION: The session to be used for login this call
|
||||
# DATA_DIR: The addon's 'userdata' directory.
|
||||
#
|
||||
# Return: The list of cached video categories
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Get the list of videos in the category.
|
||||
channels = get_channels(NAME, COOKIEJAR, SESSION)
|
||||
common_vars.__logger__.debug('Received channels = ' + str(channels))
|
||||
|
||||
for channel in channels:
|
||||
common_vars.__logger__.debug('Channel data => ' + str(channel))
|
||||
common_vars.__logger__.debug('Channel name: ' + channel['name'])
|
||||
common_vars.__logger__.debug('Channel url: ' + channel['url'])
|
||||
common_vars.__logger__.debug('Channel logo: ' + channel['logo'])
|
||||
common_vars.__logger__.debug('Channel ID: ' + channel['id'])
|
||||
common_vars.__logger__.debug('Channel data-url: ' + channel['data-url'])
|
||||
|
||||
# Create a list item with a text label and a thumbnail image.
|
||||
list_item = xbmcgui.ListItem(label=channel['name'])
|
||||
|
||||
# Set additional info for the list item.
|
||||
# For available properties see https://codedocs.xyz/xbmc/xbmc/group__python__xbmcgui__listitem.html#ga0b71166869bda87ad744942888fb5f14
|
||||
# 'mediatype' is needed for skin to display info for this ListItem correctly.
|
||||
list_item.setInfo('video', {'title': channel['name'],
|
||||
'genre': 'General',
|
||||
'mediatype': 'video'})
|
||||
|
||||
|
||||
# Set graphics (thumbnail, fanart, banner, poster, landscape etc.) for the list item.
|
||||
list_item.setArt({'thumb': channel['logo']})
|
||||
|
||||
# Set 'IsPlayable' property to 'true'.
|
||||
# This is mandatory for playable items!
|
||||
list_item.setProperty('IsPlayable', 'true')
|
||||
|
||||
# Create a URL for a plugin recursive call.
|
||||
# Example: plugin://plugin.video.example/?action=play&channel_endpoint=/filme/tnt&channel_metadata=...
|
||||
url = common_functions.get_url(action='play', account='voyo.ro', channel_endpoint=channel['data-url'])
|
||||
common_vars.__logger__.debug('URL for plugin recursive call: ' + url)
|
||||
|
||||
# This means that this item won't open any sub-list.
|
||||
is_folder = False
|
||||
|
||||
# Add our item to the Kodi virtual folder listing.
|
||||
xbmcplugin.addDirectoryItem(int(common_vars.__handle__), url, list_item, is_folder)
|
||||
|
||||
# Add a sort method for the virtual folder items (alphabetically, ignore articles)
|
||||
xbmcplugin.addSortMethod(int(common_vars.__handle__), xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE)
|
||||
|
||||
# Finish creating a virtual folder.
|
||||
xbmcplugin.endOfDirectory(int(common_vars.__handle__))
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def get_channels(NAME, COOKIEJAR, SESSION):
|
||||
####
|
||||
#
|
||||
# Get from voyo.ro the list of channels/streams.
|
||||
#
|
||||
# Parameters:
|
||||
# NAME: Logger name to use for sending the log messages
|
||||
# COOKIEJAR: The cookiejar to be used with the given session
|
||||
# SESSION: The session to be used for this call
|
||||
# DATA_DIR: The addon's 'userdata' directory.
|
||||
#
|
||||
# Return: The list of channels/streams in the given category
|
||||
#
|
||||
####
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Setup headers for the request
|
||||
MyHeaders = {
|
||||
'Host': 'voyo.protv.ro',
|
||||
'User-Agent': common_vars.__voyo_userAgent__,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('URL: https://voyo.protv.ro/live')
|
||||
common_vars.__logger__.debug('Method: GET')
|
||||
|
||||
# Send the GET request
|
||||
_request_ = SESSION.get('https://voyo.protv.ro/live', headers=MyHeaders)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
_raw_channel_data_ = re.findall('<section class="c-section">(.+?)</section>', _request_.content.decode(), re.IGNORECASE|re.DOTALL)
|
||||
common_vars.__logger__.debug('Found _raw_channel_data_ = ' + str(_raw_channel_data_))
|
||||
|
||||
_raw_channels_ = re.findall('<div class="col-sm-6 col-lg-4 col-xl-3 i">(.+?)</a>', str(_raw_channel_data_), re.IGNORECASE|re.DOTALL)
|
||||
common_vars.__logger__.debug('Found _raw_channels_ = ' + str(_raw_channels_))
|
||||
|
||||
# Initialize the list of channels
|
||||
_channels_ = []
|
||||
|
||||
for _raw_channel_ in _raw_channels_:
|
||||
common_vars.__logger__.debug('_raw_channel_ = ' + str(_raw_channel_))
|
||||
|
||||
_channel_record_ = {}
|
||||
|
||||
_channel_name_ = re.findall('title="(.+?)"', _raw_channel_, re.IGNORECASE)[0]
|
||||
common_vars.__logger__.debug('_channel_name_ = ' + str(_channel_name_))
|
||||
|
||||
_channel_url_ = re.findall('<a href="(.+?)"', _raw_channel_, re.IGNORECASE)[0]
|
||||
common_vars.__logger__.debug('_channel_url_ = ' + str(_channel_url_))
|
||||
|
||||
_channel_logo_ = re.findall('<img src="(.+?)"', _raw_channel_, re.IGNORECASE)[0]
|
||||
common_vars.__logger__.debug('_channel_logo_ = ' + str(_channel_logo_))
|
||||
|
||||
_channel_id_ = re.findall('data-channel-id="(.+?)"', _raw_channel_, re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('_channel_id_ = ' + str(_channel_id_))
|
||||
|
||||
MyHeaders = {
|
||||
'Host': 'voyo.protv.ro',
|
||||
'User-Agent': common_vars.__voyo_userAgent__,
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US',
|
||||
'Accept-Encoding': 'identity',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Cache-Control': 'max-age=0'
|
||||
}
|
||||
|
||||
common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
common_vars.__logger__.debug('URL: ' + str(_channel_url_))
|
||||
common_vars.__logger__.debug('Method: GET')
|
||||
|
||||
# Send the GET request
|
||||
_request_ = SESSION.get(str(_channel_url_), headers=MyHeaders)
|
||||
|
||||
# Save cookies for later use.
|
||||
COOKIEJAR.save(ignore_discard=True)
|
||||
|
||||
common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
|
||||
_raw_channel_data_url_ = re.findall('<div class="iframe-wrap">(.+?)</iframe>', _request_.content.decode(), re.IGNORECASE|re.DOTALL)
|
||||
common_vars.__logger__.debug('Found _raw_channel_data_url_ = ' + str(_raw_channel_data_url_))
|
||||
|
||||
_channel_data_url_ = re.findall('src="(.+?)\?autoplay', str(_raw_channel_data_url_), re.IGNORECASE|re.DOTALL)[0]
|
||||
common_vars.__logger__.debug('Found _channel_data_url_ = ' + str(_channel_data_url_))
|
||||
|
||||
_channel_record_["name"] = _channel_name_
|
||||
_channel_record_["url"] = _channel_url_
|
||||
_channel_record_["logo"] = _channel_logo_
|
||||
_channel_record_["id"] = _channel_id_
|
||||
_channel_record_["data-url"] = _channel_data_url_
|
||||
|
||||
common_vars.__logger__.debug('Created: _channel_record_ = ' + str(_channel_record_))
|
||||
_channels_.append(_channel_record_)
|
||||
|
||||
common_vars.__logger__.debug('_channels_ = ' + str(_channels_))
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
return _channels_
|
||||
|
||||
|
||||
def PVRIPTVSimpleClientIntegration_update_m3u_file(M3U_FILE, START_NUMBER, NAME, COOKIEJAR, SESSION):
|
||||
common_vars.__logger__ = logging.getLogger(NAME)
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
common_vars.__logger__.debug('M3U_FILE = ' + M3U_FILE)
|
||||
common_vars.__logger__.debug('START_NUMBER = ' + str(START_NUMBER))
|
||||
|
||||
# Get the list of channels in the category.
|
||||
channels = get_channels(NAME, COOKIEJAR, SESSION)
|
||||
common_vars.__logger__.debug('Received channels = ' + str(channels))
|
||||
|
||||
_CHNO_ = START_NUMBER
|
||||
_data_file_ = open(M3U_FILE, 'a', encoding='utf-8')
|
||||
|
||||
for channel in channels:
|
||||
_line_ = "#EXTINF:0 tvg-id=\"voyo__" + str(channel['id']) + "\" tvg-name=\"" + channel['name'] + "\" tvg-logo=\"" + channel['logo'] + "\" tvg-chno=\"" + str(_CHNO_) + "\" group-title=\"Voyo\"," + channel['name']
|
||||
|
||||
_url_ = common_functions.get_url(action='play', account='voyo.ro', channel_endpoint=channel['data-url'])
|
||||
_play_url_ = "plugin://" + common_vars.__AddonID__ + "/" + _url_
|
||||
|
||||
_data_file_.write(_line_ + "\n")
|
||||
_data_file_.write(_play_url_ + "\n")
|
||||
|
||||
_CHNO_ = _CHNO_ + 1
|
||||
|
||||
_data_file_.close()
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
return _CHNO_
|
||||
|
||||
|
||||
#def PVRIPTVSimpleClientIntegration_update_EPG_file(XML_FILE, NAME, COOKIEJAR, SESSION):
|
||||
# common_vars.__logger__ = logging.getLogger(NAME)
|
||||
# common_vars.__logger__.debug('Enter function')
|
||||
#
|
||||
# common_vars.__logger__.debug('XML_FILE = ' + XML_FILE)
|
||||
#
|
||||
# # Get the list of channels in the category.
|
||||
# channels = get_channels(NAME, COOKIEJAR, SESSION)
|
||||
# common_vars.__logger__.debug('Received channels = ' + str(channels))
|
||||
#
|
||||
# epg = get_epg_data(NAME, COOKIEJAR, SESSION)
|
||||
# common_vars.__logger__.debug('Received epg = ' + str(epg))
|
||||
#
|
||||
# _data_file_ = open(XML_FILE, 'a', encoding='utf-8')
|
||||
#
|
||||
# for channel in channels:
|
||||
# #common_vars.__logger__.debug('Channel name = ' + channel['name'])
|
||||
# #common_vars.__logger__.debug('Channel url = ' + channel['url'])
|
||||
# #common_vars.__logger__.debug('Channel logo = ' + channel['logo'])
|
||||
# #common_vars.__logger__.debug('Channel id = ' + channel['id'])
|
||||
# #common_vars.__logger__.debug('Channel data-url = ' + channel['data-url'])
|
||||
#
|
||||
# _line_ = " <channel id=\"voyo__" + channel['id'] + "\">"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
# _line_ = " <display-name>" + channel['name'] + "</display-name>"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
# _line_ = " </channel>"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
#
|
||||
# for program in epg[channel['id']]:
|
||||
#
|
||||
# ###### Probably there is a better way to deal with this ######
|
||||
# program['start_at'] = re.sub('-', '', program['start_at'], flags=re.IGNORECASE)
|
||||
# program['start_at'] = re.sub('T', '', program['start_at'], flags=re.IGNORECASE)
|
||||
# program['start_at'] = re.sub(':', '', program['start_at'], flags=re.IGNORECASE)
|
||||
# program['start_at'] = re.sub('\+', ' +', program['start_at'], flags=re.IGNORECASE)
|
||||
#
|
||||
# program['end_at'] = re.sub('-', '', program['end_at'], flags=re.IGNORECASE)
|
||||
# program['end_at'] = re.sub('T', '', program['end_at'], flags=re.IGNORECASE)
|
||||
# program['end_at'] = re.sub(':', '', program['end_at'], flags=re.IGNORECASE)
|
||||
# program['end_at'] = re.sub('\+', ' +', program['end_at'], flags=re.IGNORECASE)
|
||||
# ###### Probably there is a better way to deal with this ######
|
||||
#
|
||||
# _line_ = " <programme start=\"" + program['start_at'] + "\" stop=\"" + program['end_at'] + "\" channel=\"voyo__" + channel['id'] + "\">"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
#
|
||||
# # Replace unwanted characters in the program name
|
||||
# program['title'] = re.sub(' ', ' ', program['title'], flags=re.IGNORECASE)
|
||||
# program['title'] = re.sub(''', '\'', program['title'], flags=re.IGNORECASE)
|
||||
# _line_ = " <title>" + program['title'] + "</title>"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
#
|
||||
# # Replace unwanted characters in the program description
|
||||
# program['short_description'] = re.sub(' ', ' ', program['short_description'], flags=re.IGNORECASE)
|
||||
# program['short_description'] = re.sub(''', '\'', program['short_description'], flags=re.IGNORECASE)
|
||||
# program['description'] = re.sub(' ', ' ', program['description'], flags=re.IGNORECASE)
|
||||
# program['description'] = re.sub(''', '\'', program['description'], flags=re.IGNORECASE)
|
||||
#
|
||||
# _line_ = " <desc>" + program['short_description'] + "\n\n " + program['description'] + "\n </desc>"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
#
|
||||
# _line_ = " </programme>"
|
||||
# _data_file_.write(_line_ + "\n")
|
||||
#
|
||||
# _data_file_.close
|
||||
#
|
||||
# common_vars.__logger__.debug('Exit function')
|
||||
#
|
||||
#
|
||||
#
|
||||
#def get_epg_data(NAME, COOKIEJAR, SESSION):
|
||||
# ####
|
||||
# #
|
||||
# # Get from voyo.protv.ro the EPG data.
|
||||
# #
|
||||
# # Parameters:
|
||||
# # NAME: Logger name to use for sending the log messages
|
||||
# # COOKIEJAR: The cookiejar to be used with the given session
|
||||
# # SESSION: The session to be used for this call
|
||||
# #
|
||||
# # Return: A dict object containing the EPG data
|
||||
# #
|
||||
# ####
|
||||
# common_vars.__logger__ = logging.getLogger(NAME)
|
||||
# common_vars.__logger__.debug('Enter function')
|
||||
#
|
||||
# # Setup headers for the request
|
||||
# MyHeaders = {
|
||||
# 'Host': 'protvplus.ro',
|
||||
# 'User-Agent': common_vars.__protvplus_userAgent__,
|
||||
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
# 'Accept-Language': 'en-US',
|
||||
# 'Accept-Encoding': 'identity',
|
||||
# 'Connection': 'keep-alive',
|
||||
# 'Upgrade-Insecure-Requests': '1',
|
||||
# 'Cache-Control': 'max-age=0'
|
||||
# }
|
||||
#
|
||||
# common_vars.__logger__.debug('Cookies: ' + str(list(COOKIEJAR)))
|
||||
# common_vars.__logger__.debug('Headers: ' + str(MyHeaders))
|
||||
# common_vars.__logger__.debug('URL: https://protvplus.ro/tv-program')
|
||||
# common_vars.__logger__.debug('Method: GET')
|
||||
#
|
||||
# # Send the GET request
|
||||
# _request_ = SESSION.get('https://protvplus.ro/tv-program', headers=MyHeaders)
|
||||
#
|
||||
# # Save cookies for later use.
|
||||
# COOKIEJAR.save(ignore_discard=True)
|
||||
#
|
||||
# common_vars.__logger__.debug('Received status code: ' + str(_request_.status_code))
|
||||
# common_vars.__logger__.debug('Received cookies: ' + str(list(COOKIEJAR)))
|
||||
# common_vars.__logger__.debug('Received headers: ' + str(_request_.headers))
|
||||
# common_vars.__logger__.debug('Received data: ' + _request_.content.decode())
|
||||
#
|
||||
# _raw_epg_data_ = re.findall('EPG_program = (.+?);\n', _request_.content.decode(), re.IGNORECASE|re.DOTALL)[0]
|
||||
# common_vars.__logger__.debug('Found _raw_epg_data_ = ' + str(_raw_epg_data_))
|
||||
#
|
||||
# _epg_data_ = json.loads(_raw_epg_data_)
|
||||
#
|
||||
# common_vars.__logger__.debug('Exit function')
|
||||
#
|
||||
# return _epg_data_
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 290 KiB |
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
@ -0,0 +1,291 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
|
||||
<!--
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
-->
|
||||
|
||||
<settings version="1">
|
||||
<section id="plugin.video.TVOnline.ro">
|
||||
<category id="Accounts" label="30101" help="">
|
||||
<group id="DigiOnline.ro" label="30102">
|
||||
<setting id="digionline_Enabled" type="boolean" label="30103" help="">
|
||||
<level>0</level>
|
||||
<default>false</default>
|
||||
<control type="toggle"/>
|
||||
</setting>
|
||||
<setting id="digionline_Username" type="string" label="30104" help="" parent="digionline_Enabled">
|
||||
<level>0</level>
|
||||
<default>__DEFAULT_USER__</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30104</heading>
|
||||
</control>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="digionline_Enabled">true</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
<setting id="digionline_Password" type="string" label="30105" help="" parent="digionline_Enabled">
|
||||
<level>0</level>
|
||||
<default>__DEFAULT_PASSWORD__</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30105</heading>
|
||||
<hidden>true</hidden>
|
||||
</control>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="digionline_Enabled">true</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
<setting id="digionline_BehaveAs" type="integer" label="30118" help="" parent="digionline_Enabled">
|
||||
<level>0</level>
|
||||
<default>0</default>
|
||||
<constraints>
|
||||
<options>
|
||||
<option label="30122">0</option>
|
||||
<option label="30123">1</option>
|
||||
</options>
|
||||
</constraints>
|
||||
<control type="spinner" format="string"/>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="digionline_Enabled">true</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
<setting id="digionline_PhoneDeviceManufacturer" type="string" label="30106" help="" parent="digionline_BehaveAs">
|
||||
<level>0</level>
|
||||
<default>__DEFAULT_DEVICE_MAUFACTURER__</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30106</heading>
|
||||
</control>
|
||||
<dependencies>
|
||||
<dependency type="visible">
|
||||
<and>
|
||||
<condition setting="digionline_Enabled">true</condition>
|
||||
<condition setting="digionline_BehaveAs">0</condition>
|
||||
</and>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
<setting id="digionline_PhoneDeviceModel" type="string" label="30107" help="" parent="digionline_BehaveAs">
|
||||
<level>0</level>
|
||||
<default>__DEFAULT_DEVICE_MODEL__</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30107</heading>
|
||||
</control>
|
||||
<dependencies>
|
||||
<dependency type="visible">
|
||||
<and>
|
||||
<condition setting="digionline_Enabled">true</condition>
|
||||
<condition setting="digionline_BehaveAs">0</condition>
|
||||
</and>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
<setting id="digionline_PhoneAndroidVersion" type="string" label="30108" help="" parent="digionline_BehaveAs">
|
||||
<level>0</level>
|
||||
<default>__DEFAULT_ANDROID_VERSION__</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30108</heading>
|
||||
</control>
|
||||
<dependencies>
|
||||
<dependency type="visible">
|
||||
<and>
|
||||
<condition setting="digionline_Enabled">true</condition>
|
||||
<condition setting="digionline_BehaveAs">0</condition>
|
||||
</and>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
<setting id="digionline_TVDeviceModel" type="string" label="30119" help="" parent="digionline_BehaveAs">
|
||||
<level>0</level>
|
||||
<default>__DEFAULT_DEVICE_MODEL__</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30119</heading>
|
||||
</control>
|
||||
<dependencies>
|
||||
<dependency type="visible">
|
||||
<and>
|
||||
<condition setting="digionline_Enabled">true</condition>
|
||||
<condition setting="digionline_BehaveAs">1</condition>
|
||||
</and>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
<setting id="digionline_TVAndroidVersion" type="string" label="30120" help="" parent="digionline_BehaveAs">
|
||||
<level>0</level>
|
||||
<default>__DEFAULT_ANDROID_VERSION__</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30120</heading>
|
||||
</control>
|
||||
<dependencies>
|
||||
<dependency type="visible">
|
||||
<and>
|
||||
<condition setting="digionline_Enabled">true</condition>
|
||||
<condition setting="digionline_BehaveAs">1</condition>
|
||||
</and>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
|
||||
<setting id="digionline_TVPlatform" type="string" label="30121" help="" parent="digionline_BehaveAs">
|
||||
<level>0</level>
|
||||
<default>Android</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30121</heading>
|
||||
</control>
|
||||
<visible>false</visible>
|
||||
</setting>
|
||||
|
||||
</group>
|
||||
<group id="voyo.ro" label="30109">
|
||||
<setting id="voyo_Enabled" type="boolean" label="30103" help="">
|
||||
<level>0</level>
|
||||
<default>false</default>
|
||||
<control type="toggle"/>
|
||||
</setting>
|
||||
<setting id="voyo_Username" type="string" label="30104" help="" parent="voyo_Enabled">
|
||||
<level>0</level>
|
||||
<default>__DEFAULT_USER__</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30104</heading>
|
||||
</control>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="voyo_Enabled">true</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
<setting id="voyo_Password" type="string" label="30105" help="" parent="voyo_Enabled">
|
||||
<level>0</level>
|
||||
<default>__DEFAULT_PASSWORD__</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30105</heading>
|
||||
<hidden>true</hidden>
|
||||
</control>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="voyo_Enabled">true</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
</group>
|
||||
|
||||
<group id="tvrplus.ro" label="30124">
|
||||
<setting id="tvrplus_Enabled" type="boolean" label="30103" help="">
|
||||
<level>0</level>
|
||||
<default>false</default>
|
||||
<control type="toggle"/>
|
||||
</setting>
|
||||
</group>
|
||||
|
||||
<group id="primaplay.ro" label="30125">
|
||||
<setting id="primaplay_Enabled" type="boolean" label="30103" help="">
|
||||
<level>0</level>
|
||||
<default>false</default>
|
||||
<control type="toggle"/>
|
||||
</setting>
|
||||
<setting id="primaplay_Username" type="string" label="30104" help="" parent="primaplay_Enabled">
|
||||
<level>0</level>
|
||||
<default>__DEFAULT_USER__</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30104</heading>
|
||||
</control>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="primaplay_Enabled">true</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
<setting id="primaplay_Password" type="string" label="30105" help="" parent="primaplay_Enabled">
|
||||
<level>0</level>
|
||||
<default>__DEFAULT_PASSWORD__</default>
|
||||
<constraints>
|
||||
<allowempty>false</allowempty>
|
||||
</constraints>
|
||||
<control type="edit" format="string">
|
||||
<heading>30105</heading>
|
||||
<hidden>true</hidden>
|
||||
</control>
|
||||
<dependencies>
|
||||
<dependency type="visible" setting="primaplay_Enabled">true</dependency>
|
||||
</dependencies>
|
||||
</setting>
|
||||
</group>
|
||||
|
||||
</category>
|
||||
<category id="GeneralSettings" label="30110" help="">
|
||||
<group id="Display" label="30111">
|
||||
<setting id="ShowTitleInChannelList" type="boolean" label="30112" help="">
|
||||
<level>0</level>
|
||||
<default>false</default>
|
||||
<control type="toggle"/>
|
||||
</setting>
|
||||
<setting id="DebugEnabled" type="boolean" label="30113" help="">
|
||||
<level>0</level>
|
||||
<default>false</default>
|
||||
<control type="toggle"/>
|
||||
</setting>
|
||||
</group>
|
||||
</category>
|
||||
<category id="SimplePVRIntegration" label="30114" help="">
|
||||
<group id="DataFiles" label="30115">
|
||||
<setting id="PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime" type="time" label="30116" help="">
|
||||
<level>0</level>
|
||||
<default>00:05</default>
|
||||
<control type="button" format="time">
|
||||
<heading>30116</heading>
|
||||
</control>
|
||||
</setting>
|
||||
<setting id="PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime" type="time" label="30117" help="">
|
||||
<level>0</level>
|
||||
<default>00:10</default>
|
||||
<control type="button" format="time">
|
||||
<heading>30117</heading>
|
||||
</control>
|
||||
</setting>
|
||||
</group>
|
||||
</category>
|
||||
</section>
|
||||
</settings>
|
||||
|
@ -0,0 +1,464 @@
|
||||
#
|
||||
#
|
||||
# Copyright (C) 2021 Alin Cretu
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
|
||||
import os
|
||||
import xbmcaddon
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
from urllib.parse import urlencode
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
import logging.handlers
|
||||
#from datetime import datetime
|
||||
#from datetime import timedelta
|
||||
import time
|
||||
import resources.lib.common.vars as common_vars
|
||||
import resources.lib.common.functions as common_functions
|
||||
import resources.lib.digionline.functions as digionline_functions
|
||||
import resources.lib.voyo.functions as voyo_functions
|
||||
import resources.lib.primaplay.functions as primaplay_functions
|
||||
import resources.lib.tvrplus.functions as tvrplus_functions
|
||||
import resources.lib.schedule as schedule
|
||||
import re
|
||||
|
||||
__SystemBuildVersion__ = xbmc.getInfoLabel('System.BuildVersion')
|
||||
__SystemBuildDate__ = xbmc.getInfoLabel('System.BuildDate')
|
||||
|
||||
# Kodi uses the following sys.argv arguments:
|
||||
# [0] - The base URL for this add-on, e.g. 'plugin://plugin.video.demo1/'.
|
||||
# [1] - The process handle for this add-on, as a numeric string.
|
||||
# [2] - The query string passed to this add-on, e.g. '?foo=bar&baz=quux'.
|
||||
|
||||
# Get the plugin url in plugin:// notation.
|
||||
common_vars.__plugin_url__ = sys.argv[0]
|
||||
|
||||
# Get the plugin handle as an integer number.
|
||||
#_handle = int(sys.argv[1])
|
||||
|
||||
MyServiceAddon = xbmcaddon.Addon(id=common_vars.__AddonID__)
|
||||
|
||||
# The version of the runing Addon
|
||||
__AddonVersion__ = MyServiceAddon.getAddonInfo('version')
|
||||
|
||||
# Initialize the Addon data directory
|
||||
MyServiceAddon_DataDir = xbmcvfs.translatePath(MyServiceAddon.getAddonInfo('profile'))
|
||||
if not os.path.exists(MyServiceAddon_DataDir):
|
||||
os.makedirs(MyServiceAddon_DataDir)
|
||||
|
||||
# Read the user preferences stored in the addon configuration
|
||||
common_functions.read_AddonSettings(MyServiceAddon, common_vars.__ServiceID__)
|
||||
|
||||
# Log file name
|
||||
service_logfile_name = os.path.join(MyServiceAddon_DataDir, common_vars.__ServiceLogFilename__)
|
||||
|
||||
# Configure logging
|
||||
if common_vars.__config_DebugEnabled__ == 'true':
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
else:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
common_vars.__logger__ = logging.getLogger(common_vars.__ServiceID__)
|
||||
common_vars.__logger__.propagate = False
|
||||
|
||||
# Create a rotating file handler
|
||||
# TODO: Extend the settings.xml to allow the user to choose the values for maxBytes and backupCount
|
||||
# TODO: Set the values for maxBytes and backupCount to values defined in the addon settings
|
||||
handler = logging.handlers.RotatingFileHandler(service_logfile_name, mode='a', maxBytes=104857600, backupCount=2, encoding='utf-8', delay=False)
|
||||
if common_vars.__config_DebugEnabled__ == 'true':
|
||||
handler.setLevel(logging.DEBUG)
|
||||
else:
|
||||
handler.setLevel(logging.INFO)
|
||||
|
||||
# Create a logging format to be used
|
||||
formatter = logging.Formatter('%(asctime)s %(funcName)s %(levelname)s: %(message)s', datefmt='%Y%m%d_%H%M%S')
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
# add the file handler to the common_vars.__logger__
|
||||
common_vars.__logger__.addHandler(handler)
|
||||
|
||||
|
||||
# Initialize the __AddonCookieJar__ variable
|
||||
digionline_functions.init_AddonCookieJar(common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
voyo_functions.init_AddonCookieJar(common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
primaplay_functions.init_AddonCookieJar(common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
tvrplus_functions.init_AddonCookieJar(common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
|
||||
# Start a new requests session and initialize the cookiejar
|
||||
common_vars.__digionline_ServiceSession__ = requests.Session()
|
||||
common_vars.__voyo_ServiceSession__ = requests.Session()
|
||||
common_vars.__primaplay_ServiceSession__ = requests.Session()
|
||||
common_vars.__tvrplus_ServiceSession__ = requests.Session()
|
||||
|
||||
# Put all session cookeis in the cookiejar
|
||||
common_vars.__digionline_ServiceSession__.cookies = common_vars.__digionline_CookieJar__
|
||||
common_vars.__voyo_ServiceSession__.cookies = common_vars.__digionline_CookieJar__
|
||||
common_vars.__primaplay_ServiceSession__.cookies = common_vars.__digionline_CookieJar__
|
||||
common_vars.__tvrplus_ServiceSession__.cookies = common_vars.__digionline_CookieJar__
|
||||
|
||||
|
||||
def schedule_jobs():
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Read the user preferences stored in the addon configuration
|
||||
common_functions.read_AddonSettings(MyServiceAddon, common_vars.__ServiceID__)
|
||||
|
||||
if common_vars.__PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime__ != common_vars.__PVRIPTVSimpleClientIntegration_m3u_FileOldRefreshTime__ or common_vars.__PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime__ != common_vars.__PVRIPTVSimpleClientIntegration_EPG_FileOldRefreshTime__:
|
||||
common_vars.__logger__.debug('__PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime__ = ' + common_vars.__PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime__)
|
||||
common_vars.__logger__.debug('__PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime__ = ' + common_vars.__PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime__)
|
||||
|
||||
schedule.clear('m3u')
|
||||
schedule.every().day.at(common_vars.__PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime__).do(PVRIPTVSimpleClientIntegration_update_m3u_file).tag('m3u')
|
||||
|
||||
schedule.clear('EPG')
|
||||
schedule.every().day.at(common_vars.__PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime__).do(PVRIPTVSimpleClientIntegration_update_EPG_file).tag('EPG')
|
||||
|
||||
# Record the new values
|
||||
common_vars.__PVRIPTVSimpleClientIntegration_m3u_FileOldRefreshTime__ = common_vars.__PVRIPTVSimpleClientIntegration_m3u_FileRefreshTime__
|
||||
common_vars.__PVRIPTVSimpleClientIntegration_EPG_FileOldRefreshTime__ = common_vars.__PVRIPTVSimpleClientIntegration_EPG_FileRefreshTime__
|
||||
|
||||
else:
|
||||
common_vars.__logger__.debug('No re-scheduling required !')
|
||||
|
||||
# (re)Initialize the files for PVR IPTV Simple Client
|
||||
PVRIPTVSimpleClientIntegration_init_m3u_file()
|
||||
PVRIPTVSimpleClientIntegration_init_EPG_file()
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def PVRIPTVSimpleClientIntegration_check_data_file(DATAFILE):
|
||||
####
|
||||
#
|
||||
# Check the status of data file.
|
||||
#
|
||||
# Parameters:
|
||||
# DATAFILE: File (full path) to be checked
|
||||
#
|
||||
# Return:
|
||||
# 0 - update of DATAFILE is not required
|
||||
# 1 - update of DATAFILE is required
|
||||
#
|
||||
####
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
_return_code_ = 0
|
||||
common_vars.__logger__.debug('Data file ==> ' + DATAFILE)
|
||||
|
||||
if os.path.exists(DATAFILE):
|
||||
# The DATAFILE exists.
|
||||
common_vars.__logger__.debug('\'' + DATAFILE + '\' exists.')
|
||||
|
||||
if os.path.getsize(DATAFILE) != 0:
|
||||
# The DATAFILE is not empty.
|
||||
common_vars.__logger__.debug('\'' + DATAFILE + '\' is not empty.')
|
||||
else:
|
||||
# The DATAFILE is empty.
|
||||
common_vars.__logger__.debug('\'' + DATAFILE + '\' is empty.')
|
||||
_return_code_ = 1
|
||||
|
||||
# Get the value (seconds since epoch) of the last modification time.
|
||||
_last_update_ = os.path.getmtime(DATAFILE)
|
||||
|
||||
if _last_update_ > time.time() - (1 * common_vars.__day__):
|
||||
# File was updated less than 24 hours ago, nothing to do
|
||||
common_vars.__logger__.debug('\'' + DATAFILE + '\' last update: ' + time.strftime("%Y%m%d_%H%M%S", time.localtime(_last_update_)))
|
||||
else:
|
||||
# File was updated 24 hours (or more) ago
|
||||
common_vars.__logger__.debug('\'' + DATAFILE + '\' last update: ' + time.strftime("%Y%m%d_%H%M%S", time.localtime(_last_update_)))
|
||||
_return_code_ = 1
|
||||
|
||||
else:
|
||||
# The DATAFILE does not exist.
|
||||
common_vars.__logger__.debug('\'' + DATAFILE + '\' does not exist.')
|
||||
_return_code_ = 1
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
return _return_code_
|
||||
|
||||
|
||||
def PVRIPTVSimpleClientIntegration_init_m3u_file():
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Read the user preferences stored in the addon configuration
|
||||
common_functions.read_AddonSettings(MyServiceAddon, common_vars.__ServiceID__)
|
||||
|
||||
if not os.path.exists(MyServiceAddon_DataDir + '/' + common_vars.__PVRIPTVSimpleClientIntegration_DataDir__ ):
|
||||
os.makedirs(MyServiceAddon_DataDir + '/' + common_vars.__PVRIPTVSimpleClientIntegration_DataDir__)
|
||||
|
||||
_m3u_file_ = os.path.join(MyServiceAddon_DataDir, common_vars.__PVRIPTVSimpleClientIntegration_DataDir__, common_vars.__PVRIPTVSimpleClientIntegration_m3u_FileName__)
|
||||
common_vars.__logger__.debug('m3u file: ' + _m3u_file_)
|
||||
|
||||
_update_required_ = PVRIPTVSimpleClientIntegration_check_data_file(_m3u_file_)
|
||||
common_vars.__logger__.debug('_update_required_ ==> ' + str(_update_required_))
|
||||
|
||||
# Check if m3u file is created with the correct behavior URLs
|
||||
_versions_file_ = os.path.join(MyServiceAddon_DataDir, common_vars.__PVRIPTVSimpleClientIntegration_DataDir__, common_vars.__PVRIPTVSimpleClientIntegration_versions_FileName__)
|
||||
common_vars.__logger__.debug('_versions_file_ = ' + _versions_file_)
|
||||
|
||||
__rsd__ = digionline_functions.digionline__read_PVRIPTVSimpleClientIntegration_FileVersionsData(common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
common_vars.__logger__.debug('Received data: ' + str(__rsd__))
|
||||
|
||||
if __rsd__['exit_status'] != 0:
|
||||
# Initialize the state data
|
||||
common_vars.__logger__.debug('Initialize state data')
|
||||
__metadata__ = {}
|
||||
__metadata__['version'] = "1"
|
||||
|
||||
__data__ = {}
|
||||
__data__['m3u_file'] = ""
|
||||
__data__['xml_file'] = ""
|
||||
|
||||
__state_data__ = {}
|
||||
__state_data__['metadata'] = __metadata__
|
||||
__state_data__['data'] = __data__
|
||||
|
||||
digionline_functions.digionline__write_PVRIPTVSimpleClientIntegration_FileVersionsData(__state_data__, common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
|
||||
_update_required_ = 1
|
||||
common_vars.__logger__.debug('_update_required_ ==> ' + str(_update_required_))
|
||||
|
||||
else:
|
||||
if __rsd__['state_data']['data']['m3u_file'] == common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__]:
|
||||
# m3u file is created with the correct behavior URLs
|
||||
common_vars.__logger__.debug('\'' + __rsd__['state_data']['data']['m3u_file'] + '\' = \'' + common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__] + '\'')
|
||||
else:
|
||||
# m3u file is NOT created with the correct behavior URLs
|
||||
common_vars.__logger__.debug('\'' + __rsd__['state_data']['data']['m3u_file'] + '\' != \'' + common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__] + '\'')
|
||||
_update_required_ = 1
|
||||
common_vars.__logger__.debug('_update_required_ ==> ' + str(_update_required_))
|
||||
|
||||
if _update_required_ == 1:
|
||||
PVRIPTVSimpleClientIntegration_update_m3u_file()
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
def PVRIPTVSimpleClientIntegration_update_m3u_file():
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Read the user preferences stored in the addon configuration
|
||||
common_functions.read_AddonSettings(MyServiceAddon, common_vars.__ServiceID__)
|
||||
|
||||
_current_channel_number_ = 1
|
||||
|
||||
if not os.path.exists(MyServiceAddon_DataDir + '/' + common_vars.__PVRIPTVSimpleClientIntegration_DataDir__ ):
|
||||
os.makedirs(MyServiceAddon_DataDir + '/' + common_vars.__PVRIPTVSimpleClientIntegration_DataDir__)
|
||||
|
||||
_m3u_file_ = os.path.join(MyServiceAddon_DataDir, common_vars.__PVRIPTVSimpleClientIntegration_DataDir__, common_vars.__PVRIPTVSimpleClientIntegration_m3u_FileName__)
|
||||
common_vars.__logger__.debug('_m3u_file_ = ' + _m3u_file_)
|
||||
|
||||
_tmp_m3u_file_ = os.path.join(MyServiceAddon_DataDir, common_vars.__PVRIPTVSimpleClientIntegration_DataDir__, common_vars.__PVRIPTVSimpleClientIntegration_m3u_FileName__ + '.tmp')
|
||||
common_vars.__logger__.debug('_tmp_m3u_file_ = ' + _tmp_m3u_file_)
|
||||
|
||||
if common_functions.has_accounts_enabled() == 'true':
|
||||
common_vars.__logger__.debug('Addon has at least one account enabled')
|
||||
|
||||
_data_file_ = open(_tmp_m3u_file_, 'w', encoding='utf-8')
|
||||
_data_file_.write("#EXTM3U tvg-shift=0" + "\n")
|
||||
_data_file_.close()
|
||||
|
||||
# digionline.ro
|
||||
if common_vars.__config_digionline_Enabled__ == 'true':
|
||||
|
||||
if common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__] == "Phone":
|
||||
_current_channel_number_ = digionline_functions.digionline__phone_updateM3Ufile(_tmp_m3u_file_, _current_channel_number_, common_vars.__ServiceID__, common_vars.__digionline_ServiceSession__, MyServiceAddon_DataDir)
|
||||
|
||||
if common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__] == "TV":
|
||||
_current_channel_number_ = digionline_functions.digionline__tv_updateM3Ufile(_tmp_m3u_file_, _current_channel_number_, common_vars.__ServiceID__, common_vars.__digionline_ServiceSession__, MyServiceAddon_DataDir)
|
||||
|
||||
common_vars.__logger__.debug('Update state data after updating m3u file.')
|
||||
__rsd__ = digionline_functions.digionline__read_PVRIPTVSimpleClientIntegration_FileVersionsData(common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
__rsd__['state_data']['data']['m3u_file'] = common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__]
|
||||
common_vars.__logger__.debug('__rsd__[\'state_data\'] = ' + str(__rsd__['state_data']))
|
||||
digionline_functions.digionline__write_PVRIPTVSimpleClientIntegration_FileVersionsData(__rsd__['state_data'], common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
|
||||
common_vars.__logger__.debug('_current_channel_number_ = ' + str(_current_channel_number_))
|
||||
|
||||
# voyo.ro
|
||||
if common_vars.__config_voyo_Enabled__ == 'true':
|
||||
_current_channel_number_ = voyo_functions.PVRIPTVSimpleClientIntegration_update_m3u_file(_tmp_m3u_file_, _current_channel_number_, common_vars.__ServiceID__, common_vars.__voyo_CookieJar__, common_vars.__voyo_ServiceSession__)
|
||||
|
||||
common_vars.__logger__.debug('_current_channel_number_ = ' + str(_current_channel_number_))
|
||||
|
||||
# primaplay.ro
|
||||
if common_vars.__config_primaplay_Enabled__ == 'true':
|
||||
_current_channel_number_ = primaplay_functions.PVRIPTVSimpleClientIntegration_update_m3u_file(_tmp_m3u_file_, _current_channel_number_, common_vars.__ServiceID__, common_vars.__primaplay_CookieJar__, common_vars.__primaplay_ServiceSession__)
|
||||
|
||||
common_vars.__logger__.debug('_current_channel_number_ = ' + str(_current_channel_number_))
|
||||
|
||||
# tvrplus.ro
|
||||
if common_vars.__config_tvrplus_Enabled__ == 'true':
|
||||
_current_channel_number_ = tvrplus_functions.PVRIPTVSimpleClientIntegration_update_m3u_file(_tmp_m3u_file_, _current_channel_number_, common_vars.__ServiceID__, common_vars.__tvrplus_CookieJar__, common_vars.__tvrplus_ServiceSession__)
|
||||
|
||||
common_vars.__logger__.debug('_current_channel_number_ = ' + str(_current_channel_number_))
|
||||
|
||||
|
||||
os.replace(_tmp_m3u_file_, _m3u_file_)
|
||||
|
||||
else:
|
||||
common_vars.__logger__.debug('Addon has no accounts enabled')
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
|
||||
def PVRIPTVSimpleClientIntegration_init_EPG_file():
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Read the user preferences stored in the addon configuration
|
||||
common_functions.read_AddonSettings(MyServiceAddon, common_vars.__ServiceID__)
|
||||
|
||||
if not os.path.exists(MyServiceAddon_DataDir + '/' + common_vars.__PVRIPTVSimpleClientIntegration_DataDir__ ):
|
||||
os.makedirs(MyServiceAddon_DataDir + '/' + common_vars.__PVRIPTVSimpleClientIntegration_DataDir__)
|
||||
|
||||
_epg_file_ = os.path.join(MyServiceAddon_DataDir, common_vars.__PVRIPTVSimpleClientIntegration_DataDir__, common_vars.__PVRIPTVSimpleClientIntegration_EPG_FileName__)
|
||||
common_vars.__logger__.debug('epg file: ' + _epg_file_)
|
||||
|
||||
_update_required_ = PVRIPTVSimpleClientIntegration_check_data_file(_epg_file_)
|
||||
common_vars.__logger__.debug('_update_required_ ==> ' + str(_update_required_))
|
||||
|
||||
# Check if xml file is created with the correct behavior URLs
|
||||
_versions_file_ = os.path.join(MyServiceAddon_DataDir, common_vars.__PVRIPTVSimpleClientIntegration_DataDir__, common_vars.__PVRIPTVSimpleClientIntegration_versions_FileName__)
|
||||
common_vars.__logger__.debug('_versions_file_ = ' + _versions_file_)
|
||||
|
||||
__rsd__ = digionline_functions.digionline__read_PVRIPTVSimpleClientIntegration_FileVersionsData(common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
common_vars.__logger__.debug('Received data: ' + str(__rsd__))
|
||||
|
||||
if __rsd__['exit_status'] != 0:
|
||||
# Initialize the state data
|
||||
common_vars.__logger__.debug('Initialize state data')
|
||||
__metadata__ = {}
|
||||
__metadata__['version'] = "1"
|
||||
|
||||
__data__ = {}
|
||||
__data__['m3u_file'] = ""
|
||||
__data__['xml_file'] = ""
|
||||
|
||||
__state_data__ = {}
|
||||
__state_data__['metadata'] = __metadata__
|
||||
__state_data__['data'] = __data__
|
||||
|
||||
digionline_functions.digionline__write_PVRIPTVSimpleClientIntegration_FileVersionsData(__state_data__, common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
|
||||
_update_required_ = 1
|
||||
common_vars.__logger__.debug('_update_required_ ==> ' + str(_update_required_))
|
||||
|
||||
else:
|
||||
if __rsd__['state_data']['data']['xml_file'] == common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__]:
|
||||
# xml file is created with the correct behavior URLs
|
||||
common_vars.__logger__.debug('\'' + __rsd__['state_data']['data']['xml_file'] + '\' = \'' + common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__] + '\'')
|
||||
else:
|
||||
# xml file is NOT created with the correct behavior URLs
|
||||
common_vars.__logger__.debug('\'' + __rsd__['state_data']['data']['xml_file'] + '\' != \'' + common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__] + '\'')
|
||||
_update_required_ = 1
|
||||
common_vars.__logger__.debug('_update_required_ ==> ' + str(_update_required_))
|
||||
|
||||
if _update_required_ == 1:
|
||||
PVRIPTVSimpleClientIntegration_update_EPG_file()
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
|
||||
def PVRIPTVSimpleClientIntegration_update_EPG_file():
|
||||
common_vars.__logger__.debug('Enter function')
|
||||
|
||||
# Read the user preferences stored in the addon configuration
|
||||
common_functions.read_AddonSettings(MyServiceAddon, common_vars.__ServiceID__)
|
||||
|
||||
if not os.path.exists(MyServiceAddon_DataDir + '/' + common_vars.__PVRIPTVSimpleClientIntegration_DataDir__ ):
|
||||
os.makedirs(MyServiceAddon_DataDir + '/' + common_vars.__PVRIPTVSimpleClientIntegration_DataDir__)
|
||||
|
||||
_epg_file_ = os.path.join(MyServiceAddon_DataDir, common_vars.__PVRIPTVSimpleClientIntegration_DataDir__, common_vars.__PVRIPTVSimpleClientIntegration_EPG_FileName__)
|
||||
common_vars.__logger__.debug('_epg_file_ = ' + _epg_file_)
|
||||
|
||||
_tmp_epg_file_ = os.path.join(MyServiceAddon_DataDir, common_vars.__PVRIPTVSimpleClientIntegration_DataDir__, common_vars.__PVRIPTVSimpleClientIntegration_EPG_FileName__ + '.tmp')
|
||||
|
||||
common_vars.__logger__.debug('_tmp_epg_file_ = ' + _tmp_epg_file_)
|
||||
|
||||
if common_functions.has_accounts_enabled() == 'true':
|
||||
common_vars.__logger__.debug('Addon has at least one account enabled')
|
||||
|
||||
_data_file_ = open(_tmp_epg_file_, 'w', encoding='utf-8')
|
||||
_data_file_.write("<?xml version=\"1.0\" encoding=\"utf-8\" ?>" + "\n")
|
||||
_data_file_.write("<tv>" + "\n")
|
||||
_data_file_.close()
|
||||
|
||||
if common_vars.__config_digionline_Enabled__ == 'true':
|
||||
|
||||
if common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__] == "Phone":
|
||||
digionline_functions.digionline__phone_updateEPGfile(_tmp_epg_file_, common_vars.__ServiceID__, common_vars.__digionline_ServiceSession__, MyServiceAddon_DataDir)
|
||||
|
||||
if common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__] == "TV":
|
||||
digionline_functions.digionline__tv_updateEPGfile(_tmp_epg_file_, common_vars.__ServiceID__, common_vars.__digionline_ServiceSession__, MyServiceAddon_DataDir)
|
||||
|
||||
common_vars.__logger__.debug('Update state data after updating xml file.')
|
||||
__rsd__ = digionline_functions.digionline__read_PVRIPTVSimpleClientIntegration_FileVersionsData(common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
__rsd__['state_data']['data']['xml_file'] = common_vars.__behave_map__[common_vars.__config_digionline_BehaveAs__]
|
||||
common_vars.__logger__.debug('__rsd__[\'state_data\'] = ' + str(__rsd__['state_data']))
|
||||
digionline_functions.digionline__write_PVRIPTVSimpleClientIntegration_FileVersionsData(__rsd__['state_data'], common_vars.__ServiceID__, MyServiceAddon_DataDir)
|
||||
|
||||
# if common_vars.__config_voyo_Enabled__ == 'true':
|
||||
# voyo_functions.PVRIPTVSimpleClientIntegration_update_EPG_file(_tmp_epg_file_, common_vars.__ServiceID__, common_vars.__voyo_CookieJar__, common_vars.__voyo_ServiceSession__)
|
||||
|
||||
# if common_vars.__config_primaplay_Enabled__ == 'true':
|
||||
# primaplay_functions.PVRIPTVSimpleClientIntegration_update_EPG_file(_tmp_epg_file_, common_vars.__ServiceID__, common_vars.__primaplay_CookieJar__, common_vars.__primaplay_ServiceSession__)
|
||||
|
||||
# if common_vars.__config_tvrplus_Enabled__ == 'true':
|
||||
# tvrplus_functions.PVRIPTVSimpleClientIntegration_update_EPG_file(_tmp_epg_file_, common_vars.__ServiceID__, common_vars.__tvrplus_CookieJar__, common_vars.__tvrplus_ServiceSession__)
|
||||
|
||||
_data_file_ = open(_tmp_epg_file_, 'a', encoding='utf-8')
|
||||
_data_file_.write("</tv>" + "\n")
|
||||
_data_file_.close()
|
||||
os.replace(_tmp_epg_file_, _epg_file_)
|
||||
|
||||
else:
|
||||
common_vars.__logger__.debug('Addon has no accounts enabled')
|
||||
|
||||
common_vars.__logger__.debug('Exit function')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
common_vars.__logger__.debug('Enter __main__ ')
|
||||
common_vars.__logger__.debug('=== SYSINFO === Addon version: ' + str(__AddonVersion__))
|
||||
common_vars.__logger__.debug('=== SYSINFO === System.BuildVersion: ' + str(__SystemBuildVersion__))
|
||||
common_vars.__logger__.debug('=== SYSINFO === System.BuildDate: ' + str(__SystemBuildDate__))
|
||||
|
||||
# Read the user preferences stored in the addon configuration
|
||||
common_functions.read_AddonSettings(MyServiceAddon, common_vars.__ServiceID__)
|
||||
|
||||
common_vars.__logger__.debug('Waiting 15 seconds for network to stabilize')
|
||||
time.sleep(15)
|
||||
common_vars.__logger__.debug('Done waiting 15 seconds for network to stabilize')
|
||||
|
||||
schedule_jobs()
|
||||
schedule.every().minute.at(":05").do(schedule_jobs)
|
||||
|
||||
common_vars.__logger__.debug('Finished scheduling jobs')
|
||||
|
||||
monitor = xbmc.Monitor()
|
||||
while not monitor.abortRequested():
|
||||
# Sleep/wait for abort for 300 seconds
|
||||
if monitor.waitForAbort(1):
|
||||
# Abort was requested while waiting. We should exit
|
||||
common_vars.__logger__.debug('Abort was requested while waiting.')
|
||||
break
|
||||
schedule.run_pending()
|
||||
common_vars.__logger__.debug('Exit __main__ ')
|
||||
|
Loading…
Reference in New Issue