diff --git a/packages/addons/service/nextpvr/changelog.txt b/packages/addons/service/nextpvr/changelog.txt new file mode 100644 index 0000000000..fe01f1b4fe --- /dev/null +++ b/packages/addons/service/nextpvr/changelog.txt @@ -0,0 +1,2 @@ +100 +- Initial release diff --git a/packages/addons/service/nextpvr/icon/icon.png b/packages/addons/service/nextpvr/icon/icon.png new file mode 100644 index 0000000000..ebc1f8b521 Binary files /dev/null and b/packages/addons/service/nextpvr/icon/icon.png differ diff --git a/packages/addons/service/nextpvr/package.mk b/packages/addons/service/nextpvr/package.mk new file mode 100644 index 0000000000..c5efc7463f --- /dev/null +++ b/packages/addons/service/nextpvr/package.mk @@ -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" +} diff --git a/packages/addons/service/nextpvr/source/addon.py b/packages/addons/service/nextpvr/source/addon.py new file mode 100644 index 0000000000..b1eaaad437 --- /dev/null +++ b/packages/addons/service/nextpvr/source/addon.py @@ -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 diff --git a/packages/addons/service/nextpvr/source/bin/nextpvr-downloader b/packages/addons/service/nextpvr/source/bin/nextpvr-downloader new file mode 100644 index 0000000000..803538469f --- /dev/null +++ b/packages/addons/service/nextpvr/source/bin/nextpvr-downloader @@ -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/default<\/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/C:\\Users\\Public\\Videos\\<\/RecordingDirectory>/\/storage\/tvshows\/<\/RecordingDirectory>/' ${ADDON_DIR}/nextpvr-bin/data/Config-master-dont-edit.xml +sed -i 's/C:\\Users\\Public\\Videos\\<\/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 diff --git a/packages/addons/service/nextpvr/source/bin/nextpvr.start b/packages/addons/service/nextpvr/source/bin/nextpvr.start new file mode 100644 index 0000000000..ec63d336db --- /dev/null +++ b/packages/addons/service/nextpvr/source/bin/nextpvr.start @@ -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 diff --git a/packages/addons/service/nextpvr/source/default.py b/packages/addons/service/nextpvr/source/default.py new file mode 100644 index 0000000000..5119bb652c --- /dev/null +++ b/packages/addons/service/nextpvr/source/default.py @@ -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() diff --git a/packages/addons/service/nextpvr/source/resources/Language/English/strings.po b/packages/addons/service/nextpvr/source/resources/Language/English/strings.po new file mode 100644 index 0000000000..498927074f --- /dev/null +++ b/packages/addons/service/nextpvr/source/resources/Language/English/strings.po @@ -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 "" diff --git a/packages/addons/service/nextpvr/source/resources/settings.xml b/packages/addons/service/nextpvr/source/resources/settings.xml new file mode 100644 index 0000000000..03ad6c7d86 --- /dev/null +++ b/packages/addons/service/nextpvr/source/resources/settings.xml @@ -0,0 +1,81 @@ + + +
+ + + + 0 + RunScript(service.nextpvr, getscantables) + + false + + + + 2 + RunScript(service.nextpvr, updategeneric) + + false + + + + + + + + 1 + 5 + + 5 + 1 + 60 + + + false + + + + 1 + RunScript(service.nextpvr, updateepg) + + false + + + + 3 + RunScript(service.nextpvr, updatem3u) + + false + + + + 2 + RunScript(service.nextpvr, rescan) + + false + + + + 3 + RunScript(service.nextpvr, transcode) + + false + + + + 3 + RunScript(service.nextpvr, defaults) + + false + + + + 1 + 554 + + 30047 + + + + +
+
diff --git a/packages/addons/service/nextpvr/source/system.d/service.nextpvr.service b/packages/addons/service/nextpvr/source/system.d/service.nextpvr.service new file mode 100644 index 0000000000..3603ac3558 --- /dev/null +++ b/packages/addons/service/nextpvr/source/system.d/service.nextpvr.service @@ -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