Merge pull request #6853 from emveepee/addnextpvr

[le11] NextPVR: add new addon package
This commit is contained in:
CvH 2022-11-03 12:17:15 +01:00 committed by GitHub
commit ecfb0581f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 616 additions and 0 deletions

View File

@ -0,0 +1,2 @@
100
- Initial release

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,28 @@
# SPDX-License-Identifier: GPL-2.0-only
# Copyright (C) 2021-present Team LibreELEC (https://libreelec.tv)
PKG_NAME="nextpvr"
PKG_VERSION="6.1.0~Nexus"
PKG_ADDON_VERSION="6.1.0"
PKG_REV="100"
PKG_ARCH="any"
PKG_LICENSE="prop."
PKG_SITE="https://nextpvr.com"
PKG_DEPENDS_TARGET="toolchain"
PKG_SECTION="service"
PKG_SHORTDESC="NextPVR Server"
PKG_LONGDESC="NextPVR is a personal video recorder application. It allows to watch or record live TV, provides great features like series recordings and web scheduling."
PKG_TOOLCHAIN="manual"
PKG_IS_ADDON="yes"
PKG_ADDON_NAME="NextPVR Server"
PKG_ADDON_TYPE="xbmc.service.library"
PKG_ADDON_REQUIRES="tools.ffmpeg-tools:0.0.0 tools.dotnet-runtime:0.0.0 script.module.requests:0.0.0"
addon() {
:
}
post_install_addon() {
sed -e "s/@NEXTPVR_VERSION@/${PKG_ADDON_VERSION}/g" -i "${INSTALL}/bin/nextpvr-downloader"
}

View File

@ -0,0 +1,232 @@
# SPDX-License-Identifier: GPL-2.0-only
# Copyright (C) 2022-present Team LibreELEC (https://libreelec.tv)
import urllib.request, urllib.parse, urllib.error, os, zipfile
from urllib.error import URLError
import urllib.parse as urlparse
import requests
import json
import subprocess
from urllib.parse import parse_qs
import xbmc, xbmcvfs, xbmcgui, xbmcaddon
import shutil
import sys
import xml.etree.ElementTree as ET
temp = xbmcvfs.translatePath('special://temp')
ADDON_NAME = xbmcaddon.Addon().getAddonInfo('name')
LS = xbmcaddon.Addon().getLocalizedString
# Ignore isbn tables
SCANTABLES = ['atsc', 'dvb-c', 'dvb-s', 'dvb-t']
GENERIC_URL = 'https://nextpvr.com/stable/linux/NPVR.zip'
class Controller():
def __init__(self):
pass
def downloadScanTable(self):
# Taken from TVHeadend Addon
try:
url = 'https://github.com/tvheadend/dtv-scan-tables/archive/tvheadend.zip'
archive = os.path.join(temp, 'dtv_scantables.zip')
temp_folder = os.path.join(temp, 'dtv-scan-tables-tvheadend')
dest_folder = os.path.join(xbmcvfs.translatePath(xbmcaddon.Addon().getAddonInfo('path')), 'dtv-scan-tables')
xbmcgui.Dialog().notification(ADDON_NAME, LS(30042), xbmcgui.NOTIFICATION_INFO)
urllib.request.urlretrieve(url, archive)
zip = zipfile.ZipFile(archive)
if zip.testzip() is not None: raise zipfile.BadZipfile
if os.path.exists(temp_folder): shutil.rmtree(temp_folder)
if os.path.exists(dest_folder): shutil.rmtree(dest_folder)
xbmcgui.Dialog().notification(ADDON_NAME, LS(30043), xbmcgui.NOTIFICATION_INFO)
for idx, folder in enumerate(SCANTABLES):
for z in zip.filelist:
if folder in z.filename: zip.extract(z.filename, temp)
for folder in SCANTABLES:
shutil.copytree(os.path.join(temp_folder, folder), os.path.join(dest_folder, folder))
xbmcgui.Dialog().notification(ADDON_NAME, LS(30039), xbmcgui.NOTIFICATION_INFO)
except URLError as e:
xbmc.log('Could not download file: %s' % e.reason, xbmc.LOGERROR)
xbmcgui.Dialog().notification(ADDON_NAME, LS(30040), xbmcgui.NOTIFICATION_ERROR)
except zipfile.BadZipfile:
xbmc.log('Could not extract files from zip, bad zipfile', xbmc.LOGERROR)
xbmcgui.Dialog().notification(ADDON_NAME, LS(30041), xbmcgui.NOTIFICATION_ERROR)
def updateNextPVR(self):
try:
dest_folder = os.path.join(xbmcvfs.translatePath(xbmcaddon.Addon().getAddonInfo('path')), 'nextpvr-bin')
archive = os.path.join(temp, 'NPVR.zip')
xbmcgui.Dialog().notification(ADDON_NAME, LS(30011), xbmcgui.NOTIFICATION_INFO)
urllib.request.urlretrieve(GENERIC_URL, archive)
xbmcgui.Dialog().notification(ADDON_NAME, LS(30012), xbmcgui.NOTIFICATION_INFO)
zip = zipfile.ZipFile(archive)
if zip.testzip() is not None: raise zipfile.BadZipfile
zip.close()
command = 'unzip -o {0} -d {1} > /dev/null'.format(archive, dest_folder)
xbmc.log('Running: %s' % command, xbmc.LOGDEBUG)
os.system(command)
os.remove(archive)
xbmcgui.Dialog().notification(ADDON_NAME, LS(30039), xbmcgui.NOTIFICATION_INFO)
xbmc.log('NPVR.zip installed', xbmc.LOGDEBUG)
if xbmcgui.Dialog().yesno("NextPVR Server", LS(30020)):
self.id = xbmcaddon.Addon().getAddonInfo('id')
subprocess.call(['systemctl', 'restart', self.id])
except URLError as e:
xbmc.log('Could not download file: %s' % e.reason, xbmc.LOGERROR)
xbmcgui.Dialog().notification(ADDON_NAME, LS(30040), xbmcgui.NOTIFICATION_ERROR)
except zipfile.BadZipfile:
xbmc.log('Could not extract files from zip, bad zipfile', xbmc.LOGERROR)
xbmcgui.Dialog().notification(ADDON_NAME, LS(30041), xbmcgui.NOTIFICATION_ERROR)
def sessionLogin(self):
self.session = requests.session()
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0'
}
response = self.session.get(self.url, headers=headers)
parsed = urlparse.urlparse(response.url)
salt = parse_qs(parsed.query)['salt'][0]
if self.hashedPassword == None:
passwordHash = self.hashMe(self.password)
else:
passwordHash = self.hashedPassword
combined = self.hashMe(salt + ':' + self.username + ':' + passwordHash)
response = self.session.get(self.url + 'login.html?hash='+combined)
if response.status_code != 200 and response.status_code != 302 :
print(response.text, response.status_code)
sys.exit()
for cookie in self.session.cookies:
self.session.cookies[cookie.name] = cookie.value
def doSessionRequest5(self, method, isJSON = True):
xbmc.log(method, xbmc.LOGDEBUG)
retval = False
getResult = None
url = self.url + 'service?method=' + method
try:
request = self.session.get(url, headers={"Accept" : "application/json"})
getResult = json.loads(request.text)
if request.status_code == 200 :
if 'stat' in getResult:
retval = getResult['stat'] == 'ok'
else:
retval = True
else:
xbmc.log(getResult, xbmc.LOGERROR)
except Exception as e:
xbmc.log(str(e), xbmc.LOGERROR)
return retval, getResult
def hashMe (self, thedata):
import hashlib
h = hashlib.md5()
h.update(thedata.encode('utf-8'))
return h.hexdigest()
def loginNextPVR(self):
base = os.path.join(xbmcvfs.translatePath(xbmcaddon.Addon().getAddonInfo('profile')), 'config/config.xml')
tree = ET.parse(base)
root = tree.getroot()
child = root.find("WebServer")
self.port = child.find('Port').text
self.username = child.find('Username').text
self.hashedPIN = child.find('PinMD5').text
self.hashedPassword = child.find('Password').text.lower()
self.ip = '127.0.0.1'
self.url = 'http://{}:{}/'.format(self.ip, self.port)
self.sessionLogin()
def showMessage(self, message):
xbmc.log(message, xbmc.LOGDEBUG)
xbmcgui.Dialog().notification(ADDON_NAME, message, xbmcgui.NOTIFICATION_INFO)
def updateEpg(self):
self.loginNextPVR()
self.doSessionRequest5('system.epg.update')
self.doSessionRequest5('session.logout')
self.showMessage(LS(30015))
def updateM3u(self):
self.loginNextPVR()
self.doSessionRequest5('setting.m3u.update')
self.doSessionRequest5('session.logout')
self.showMessage(LS(30016))
def rescanDevices(self):
self.loginNextPVR()
self.doSessionRequest5('setting.devices&refresh=true')
self.doSessionRequest5('session.logout')
self.showMessage(LS(30017))
def transcodeHLS(self):
base = os.path.join(xbmcvfs.translatePath(xbmcaddon.Addon().getAddonInfo('profile')), 'config/config.xml')
tree = ET.parse(base)
parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
tree = ET.parse(base, parser=parser)
root = tree.getroot()
parent = root.find("WebServer")
child = parent.find('TranscodeHLS')
if child.text == 'default':
child.text = '-y [ANALYZE_DURATION] [SEEK] -i [SOURCE] -map_metadata -1 -threads [THREADS] -ignore_unknown -map 0:v:0? [PREFERRED_LANGUAGE] -map 0:a:[AUDIO_STREAM] -map -0:s -vcodec copy -acodec aac -ac 2 -c:s copy -hls_time [SEGMENT_DURATION] -start_number 0 -hls_list_size [SEGMENT_COUNT] -y [TARGET]'
else:
child.text = 'default'
tree.write(base, encoding='utf-8')
if child.text == 'default':
self.showMessage(LS(30018))
else:
self.showMessage(LS(30019))
def resetWebCredentials(self):
rewrite = False
base = os.path.join(xbmcvfs.translatePath(xbmcaddon.Addon().getAddonInfo('profile')), 'config/config.xml')
tree = ET.parse(base)
parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
tree = ET.parse(base, parser=parser)
root = tree.getroot()
parent = root.find("WebServer")
child = parent.find('Username')
if child.text != 'admin':
child.text = 'admin'
rewrite = True
child = parent.find('Password')
if child.text != '5f4dcc3b5aa765d61d8327deb882cf99':
child.text = '5f4dcc3b5aa765d61d8327deb882cf99'
rewrite = True
if rewrite:
tree.write(base, encoding='utf-8')
self.showMessage(LS(30046))
if __name__ == '__main__':
option = Controller()
try:
if sys.argv[1] == 'getscantables':
option.downloadScanTable()
elif sys.argv[1] == 'updategeneric':
option.updateNextPVR()
elif sys.argv[1] == 'updateepg':
option.updateEpg()
elif sys.argv[1] == 'transcode':
option.transcodeHLS()
elif sys.argv[1] == 'updatem3u':
option.updateM3u()
elif sys.argv[1] == 'rescan':
option.rescanDevices()
elif sys.argv[1] == 'defaults':
option.resetWebCredentials()
except IndexError:
pass

View File

@ -0,0 +1,76 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0-only
# Copyright (C) 2021-present Team LibreELEC (https://libreelec.tv)
. /etc/profile
oe_setup_addon service.nextpvr
ICON="${ADDON_DIR}/resources/icon.png"
CONTROL_FILE="/tmp/curl.nextpvr.done"
DATA_FILE="/tmp/curl.nextpvr.data"
NEXTPVR_FILE="NPVR-@NEXTPVR_VERSION@.zip"
# check for enough free disk space
if [ $(df . | awk 'END {print $4}') -lt 400000 ]; then
kodi-send --action="Notification(Not enough disk space, at least 400MB are required,30000,${ICON})" >/dev/null
exit 0
fi
# remove install status and folders
if [ -f ${ADDON_DIR}/extract.ok ]; then
rm ${ADDON_DIR}/extract.ok
fi
if [ -d ${ADDON_DIR}/nextpvr-bin ]; then
rm -rf ${ADDON_DIR}/nextpvr-bin
fi
if [ -d ${ADDON_DIR}/tmp_download ]; then
rm -rf ${ADDON_DIR}/tmp_download
fi
# create tmp download dir
mkdir -p ${ADDON_DIR}/tmp_download
cd ${ADDON_DIR}/tmp_download
echo "Downloading NextPVR"
# download NextPVR
rm -f ${CONTROL_FILE} ${DATA_FILE}
(
curl -# -O -C - https://nextpvr.com/stable/linux/${NEXTPVR_FILE} 2>${DATA_FILE}
touch ${CONTROL_FILE}
) |
while [ : ]; do
[ -f ${DATA_FILE} ] && prog="$(tr '\r' '\n' <${DATA_FILE} | tail -n 1 | sed -r 's/^[# ]+/#/;s/^[^0-9]*//g')" || prog=
kodi-send --action="Notification(Downloading NextPVR,\"${prog:-0.0%}\",3000,${ICON})" >/dev/null
[ -f ${CONTROL_FILE} ] && break
sleep 4
done
rm -f ${CONTROL_FILE} ${DATA_FILE}
# check for failed download
if [ ! -f ${NEXTPVR_FILE} ]; then
kodi-send --action="Notification(Download NextPVR failed,${ICON})"
exit 1
fi
# extract NextPVR
kodi-send --action="Notification(Extracting NextPVR,starting,1000,${ICON})" >/dev/null
mkdir -p ${ADDON_DIR}/nextpvr-bin
unzip ${NEXTPVR_FILE} -d ${ADDON_DIR}/nextpvr-bin >/dev/null
if [ "$(uname -m)" != "x86_64" ]; then
sed -i 's/<TranscodeHLS>default<\/TranscodeHLS>/<TranscodeHLS>-y [ANALYZE_DURATION] [SEEK] -i [SOURCE] -map_metadata -1 -threads [THREADS] -ignore_unknown -map 0:v:0? [PREFERRED_LANGUAGE] -map 0:a:[AUDIO_STREAM] -map -0:s -vcodec copy -acodec aac -ac 2 -c:s copy -hls_time [SEGMENT_DURATION] -start_number 0 -hls_list_size [SEGMENT_COUNT] -y [TARGET]<\/TranscodeHLS>/' ${ADDON_DIR}/nextpvr-bin/data/Config-master-dont-edit.xml
fi
sed -i 's/<RecordingDirectory>C:\\Users\\Public\\Videos\\<\/RecordingDirectory>/<RecordingDirectory>\/storage\/tvshows\/<\/RecordingDirectory>/' ${ADDON_DIR}/nextpvr-bin/data/Config-master-dont-edit.xml
sed -i 's/<LiveTVBufferDirectory>C:\\Users\\Public\\Videos\\<\/LiveTVBufferDirectory>/<LiveTVBufferDirectory>\/tmp\/<\/LiveTVBufferDirectory>/' ${ADDON_DIR}/nextpvr-bin/data/Config-master-dont-edit.xml
find ${ADDON_DIR}/nextpvr-bin/DeviceHost -name DeviceHostLinux -exec chmod 755 {} \;
# cleanup
cd ${ADDON_DIR}
rm -rf ${ADDON_DIR}/tmp_download
touch ${ADDON_DIR}/extract.ok
kodi-send --action="Notification(Extracting NextPVR,finished,1000,${ICON})" >/dev/null

View File

@ -0,0 +1,26 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# Copyright (C) 2021-present Team LibreELEC (https://libreelec.tv)
. /etc/profile
oe_setup_addon service.nextpvr
# check if nextpvr-server is already successful installed
if [ ! -f "${ADDON_DIR}/extract.ok" ]; then
cd ${ADDON_DIR}
nextpvr-downloader
fi
export NEXTPVR_DATADIR_USERDATA=${ADDON_HOME}/config/
export NEXTPVR_DVBDIR=${ADDON_DIR}/dtv-scan-tables/
export SATIP_RTSP_PORT=$satiprtsp
read -d. uptime < /proc/uptime
startdelay=$((waitfor-uptime))
if [ $startdelay -gt 0 ]; then
sleep $startdelay
fi
cd ${ADDON_DIR}/nextpvr-bin
exec dotnet ${ADDON_DIR}/nextpvr-bin/NextPVRServer.dll >/dev/null

View File

@ -0,0 +1,16 @@
# SPDX-License-Identifier: GPL-2.0-only
# Copyright (C) 2016-present Team LibreELEC (https://libreelec.tv)
import xbmc
import xbmcaddon
class Monitor(xbmc.Monitor):
def __init__(self, *args, **kwargs):
xbmc.Monitor.__init__(self)
def onSettingsChanged(self):
pass
if __name__ == "__main__":
Monitor().waitForAbort()

View File

@ -0,0 +1,142 @@
# Kodi Media Center language file
# Addon Name: nextpvr
# Addon id: service.nextpvr
# Addon Provider: Team LibreELEC
msgid ""
msgstr ""
msgctxt "#30001"
msgid "Download"
msgstr ""
msgctxt "#30002"
msgid "Download current Linux NPVR.zip"
msgstr ""
msgctxt "#30003"
msgid "Check the NextPVR forum before updating"
msgstr ""
msgctxt "#30004"
msgid "Manage Server"
msgstr ""
msgctxt "#30005"
msgid "Update guide"
msgstr ""
msgctxt "#30006"
msgid "Start an unscheduled EPG update"
msgstr ""
msgctxt "#30007"
msgid "Update IPTV m3u source"
msgstr ""
msgctxt "#30008"
msgid "Rescan m3u file(s) and update URLs"
msgstr ""
msgctxt "#30009"
msgid "Toggle custom HLS transcoding with default"
msgstr ""
msgctxt "#30010"
msgid "Change HLS transcoding mode, default will not work on all platforms"
msgstr ""
msgctxt "#30011"
msgid "Download NPVR.zip"
msgstr ""
msgctxt "#30012"
msgid "Extract NPVR.zip"
msgstr ""
msgctxt "#30013"
msgid "Rescan tuning devices"
msgstr ""
msgctxt "#30014"
msgid "Rescan for tuner changes after start-up"
msgstr ""
msgctxt "#30015"
msgid "Update EPG started"
msgstr ""
msgctxt "#30016"
msgid "Update m3u started"
msgstr ""
msgctxt "#30017"
msgid "Device refresh started"
msgstr ""
msgctxt "#30018"
msgid "Transcode set to default"
msgstr ""
msgctxt "#30019"
msgid "Transcode set to custom"
msgstr ""
msgctxt "#30020"
msgid "Restart server now"
msgstr ""
msgctxt "#30037"
msgid "Install the frequency scanning table for digital devices"
msgstr ""
msgctxt "#30038"
msgid "Download and install Scan-Tables"
msgstr ""
msgctxt "#30039"
msgid "Download completed and installed"
msgstr ""
msgctxt "#30040"
msgid "Could not download zip file"
msgstr ""
msgctxt "#30041"
msgid "Could not extract zip file"
msgstr ""
msgctxt "#30042"
msgid "Download Scan-Tables"
msgstr ""
msgctxt "#30043"
msgid "Extract Scan-Tables"
msgstr ""
msgctxt "#30044"
msgid "Reset web server credentials"
msgstr ""
msgctxt "#30045"
msgid "Reset to defaults Username: admin Password: password"
msgstr ""
msgctxt "#30046"
msgid "Set Username: admin Password: password"
msgstr ""
msgctxt "#30047"
msgid "SAT>IP RTSP port"
msgstr ""
msgctxt "#30048"
msgid "Default is 554 - TVHeadend uses 9983"
msgstr ""
msgctxt "#30049"
msgid "Startup uptime wait"
msgstr ""
msgctxt "#30050"
msgid "Delay service launch on boot to specified uptime (seconds)"
msgstr ""

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<settings version="1">
<section id="service.nextpvr">
<category id="download" label="30001">
<group id="0">
<setting id="scantable" type="action" label="30038" help="30037">
<level>0</level>
<data>RunScript(service.nextpvr, getscantables)</data>
<control type="button" format="action">
<close>false</close>
</control>
</setting>
<setting id="update" type="action" label="30002" help="30003">
<level>2</level>
<data>RunScript(service.nextpvr, updategeneric)</data>
<control type="button" format="action">
<close>false</close>
</control>
</setting>
</group>
</category>
<category id="control" label="30004">
<group id="0">
<setting id="waitfor" type="integer" label="30049" help="30050">
<level>1</level>
<default>5</default>
<constraints>
<minimum>5</minimum>
<step>1</step>
<maximum>60</maximum>
</constraints>
<control type="slider" format="integer">
<popup>false</popup>
</control>
</setting>
<setting id="updateepg" type="action" label="30005" help="30006">
<level>1</level>
<data>RunScript(service.nextpvr, updateepg)</data>
<control type="button" format="action">
<close>false</close>
</control>
</setting>
<setting id="updatem3u" type="action" label="30007" help="30008">
<level>3</level>
<data>RunScript(service.nextpvr, updatem3u)</data>
<control type="button" format="action">
<close>false</close>
</control>
</setting>
<setting id="rescan" type="action" label="30013" help="30014">
<level>2</level>
<data>RunScript(service.nextpvr, rescan)</data>
<control type="button" format="action">
<close>false</close>
</control>
</setting>
<setting id="transcode" type="action" label="30009" help="30010">
<level>3</level>
<data>RunScript(service.nextpvr, transcode)</data>
<control type="button" format="action">
<close>false</close>
</control>
</setting>
<setting id="defaults" type="action" label="30044" help="30045">
<level>3</level>
<data>RunScript(service.nextpvr, defaults)</data>
<control type="button" format="action">
<close>false</close>
</control>
</setting>
<setting id="satiprtsp" type="integer" label="30047" help="30048">
<level>1</level>
<default>554</default>
<control type="edit" format="integer">
<heading>30047</heading>
</control>
</setting>
</group>
</category>
</section>
</settings>

View File

@ -0,0 +1,13 @@
[Unit]
Description=NextPVR Server
Documentation=https://nextpvr.com
Wants=multi-user.target
After=multi-user.target
[Service]
SyslogIdentifier=%N
ExecStart=/bin/sh /storage/.kodi/addons/%N/bin/nextpvr.start
Restart=always
[Install]
WantedBy=multi-user.target