#!/usr/bin/python3
#
# Linux Python3 Streamlink Daemon
#
# Copyright (c) 2017 - 2021 Billy2011 @vuplus-support.org
# Copyright (c) 2021-2024 jbleyel (python3 mod)
# Copyright (c) 2025 ©Dorik1972 aka Pepsik
# Copyright (c) 2026 OpenPLi team
#

import os
import re
import sys
import time
import errno
import json
import atexit
import socketserver
import http.server as SimpleHTTPServer
from streamlink import logger
from streamlink import Streamlink
from streamlink.options import Options
from streamlink.utils.parse import parse_qsd
from streamlink._version import __version__ as streamlink_ver
from socket import socket, AF_INET, SOCK_DGRAM
from urllib.parse import unquote, urlparse, urlencode
from signal import signal, SIGTERM
from copy import deepcopy
from itertools import pairwise
from contextlib import suppress

__copyright__ = '©Dorik1972 aka Pepsik; OpenPLi Team'
__version__ = '1.8.5'
__updated__ = '2026'
__description__ = 'Streamlink proxy daemon for Enigma2'

srvname = 'streamlinksrv'
srvport = 8088
timeout = 30

logger.root.name = srvname
logger.root.setLevel(logger.INFO)
logger.basicConfig(stream=sys.stdout)

sort_streams = lambda s: [int(t) if t.isdigit() else t.lower() for t in re.split(r'(\d+)(?:p|i)', s)]
_streams = lambda s: dict(sorted(s.items(), key=lambda x: sort_streams(x[0])))


class RequestHandler(SimpleHTTPServer.BaseHTTPRequestHandler):
	server_version = f'{srvname[:10].capitalize()} {srvname[10:]}'
	protocol_version = 'HTTP/1.1'

	streamlink: Streamlink = Streamlink({
		"stream-timeout": timeout,
		"stream-segment-timeout": 12.0,
		"stream-segment-attempts": 10,
		"stream-segment-threads": 10,
		"hls-segment-stream-data": True,
		"hls-audio-select": "*",
		"ringbuffer-size": 1024 * 1024 * 32,
		"ffmpeg-no-validation": True,
		"http-stream-timeout": 30.0,
	}, plugins_builtin=True)

	for k, v in filter(lambda x: x[0].startswith('-'), pairwise(sys.argv[2:])):
		match k:
			case "--plugin-dir":
				streamlink.plugins.load_path(v)
			case "--locale":
				streamlink.options.set('locale', v)
			case k if k in ("-l", "--loglevel"):
				logger.root.setLevel({v: k for k, v in logger._levelToNames.items()}.get(v, logger.INFO))
			case _:
				continue

	default_options = deepcopy(streamlink.options)

	def setup(self):
		SimpleHTTPServer.BaseHTTPRequestHandler.setup(self)
		self.request.settimeout(timeout)

	def log_message(self, format, *args):
		pass

	def log_request(self, code='-', size='-'):
		logger.root.info(f'Request from: {self.address_string()} - {unquote(self.requestline)} {code} {size}')
		logger.root.trace(f'Request headers:\n{json.dumps(dict(self.headers), indent=2)}')

	def do_GET(self):
		self.send_response(200)
		self.send_header("Content-type", "video/mp2t")
		self.send_header("Transfer-Encoding", "chunked")
		self.send_header("Connection", "keep-alive")
		self.send_header("Keep-Alive", "timeout=%s, max=100" % timeout)
		self.send_header("Accept-Ranges", "none")
		self.end_headers()

		self.streamlink.options = deepcopy(self.default_options)

		try:
			url = unquote(self.path[1:])
			parsed_url = urlparse(url)
			query = parse_qsd(parsed_url.query)
			default_stream = query.pop('default-stream', 'best')

			plugin_options = Options()
			pluginname = None
			pluginclass = None
			resolved_url = url

			try:
				pluginname, pluginclass, resolved_url = self.streamlink.resolve_url(url)
			except Exception as e:
				logger.root.debug(f"Plugin detection failed: {e}")

			# YouTube: prefer VP9/AV1 and high resolution
			if "youtube.com" in url.lower() or "youtu.be" in url.lower():
				logger.root.info("YouTube URL detected - optimize resolution selection")
				# Priority: 2160p VP9/AV1 > 1440p > 1080p60 > best high bitrate > fallback
				self.streamlink.set_option(
					"youtube-format",
					"best[height<=?2160][vcodec^=vp9]/best[height<=?2160][vcodec^=av1]/"
					"best[height<=?2160]/best[height<=?1440]/best[height<=?1080][fps=60]/"
					"best[height<=?1080]/best"
				)
				self.streamlink.set_option("hls-live-edge", 4)
				self.streamlink.set_option("stream-segment-timeout", 12.0)
				# attempt to prevent throttling
				self.streamlink.set_option("http-headers", {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"})

			for k in [*query]:
				if k in self.streamlink.options:
					self.streamlink.set_option(k, query.pop(k))
				elif pluginname and k.startswith(pluginname):
					plugin_options.set(k.replace(f'{pluginname}-', ''), query.pop(k))

			url = parsed_url._replace(query=urlencode(query)).geturl()

			if pluginclass:
				try:
					plugin_instance = pluginclass(self.streamlink, url, plugin_options)
					raw_streams = plugin_instance.streams()
					streams = _streams({
						k[:k.rfind('_')] if "_" in k else k: v
						for k, v in raw_streams.items()
						if (k[:1].isdigit() or 'live' in k.lower() or 'best' in k.lower())
						   and v.__class__.__name__ != 'MuxedStream'
					})
					if streams:
						logger.root.debug(f"{(pluginname or 'STREAM').upper()} plugin streams: {list(streams.keys())}")
				except Exception as e:
					logger.root.debug(f"Plugin retrieve streams failed: {e}")
					streams = None
			else:
				streams = None

			if not streams:
				logger.root.warning("No streams via plugin, retry")
				try:
					streams = _streams(self.streamlink.streams(url))
				except Exception as e:
					logger.root.debug(f"Generic stream retrieval failed: {e}")

			if not streams:
				raise ValueError("No playable streams found")

			logger.root.debug(f"Available streams: {list(streams.keys())}")

		except Exception as err:
			msg = f"Error processing stream: {err}"
			logger.root.error(msg)

			url = 'https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8'
			streams = _streams({
				k: v for k, v in self.streamlink.streams(url).items()
				if k[:1].isdigit() or 'live' in k
			})

		finally:
			with suppress(BrokenPipeError, ConnectionResetError, OSError):
				selected = None

				if default_stream in streams:
					selected = streams[default_stream]
					logger.root.info(f"Selected quality: {default_stream}")

				elif 'best' in streams:
					selected = streams['best']
					logger.root.info("Selected quality: best")

				elif streams:
					sorted_keys = sorted(streams.keys(), key=lambda k: sort_streams(k), reverse=True)
					selected = streams[sorted_keys[0]]
					logger.root.info(f"Choosen quality: {sorted_keys[0]}")

				if selected is None:
					logger.root.error("No available streams found to play")
					return

				logger.root.info(f"Start playing stream: {default_stream or 'highest available'}")

				with selected.open() as fd:
					fsrc_read = fd.read
					fdst_write = self.wfile.write
					COPY_BUFSIZE = 128 * 1024

					while chunk := fsrc_read(COPY_BUFSIZE):
						fdst_write(b'%X\r\n%s\r\n' % (len(chunk), chunk))

class StreamlinkDaemon:
	"""
	A generic daemon class.
	"""
	def __init__(self, pidfile: str) -> None:
		self.pidfile = pidfile

	def _daemonize(self) -> None:
		if (pid := os.fork()) > 0:
			sys.exit(0)
		os.setsid()
		os.umask(0)
		if (pid := os.fork()) > 0:
			with open(self.pidfile, "w+") as fd:
				fd.write(f"{pid}\n")
			sys.exit(0)

		atexit.register(self._cleanup)
		signal(SIGTERM, self._cleanup)

		for fd in range(3):
			with suppress(OSError):
				os.close(fd)

		devnull = os.devnull if hasattr(os, "devnull") else "/dev/null"
		devnull_fd = os.open(devnull, os.O_RDWR)
		for i in range(3):
			os.dup2(devnull_fd, i)
		os.close(devnull_fd)

	def _cleanup(self) -> None:
		if os.path.isfile(self.pidfile):
			os.unlink(self.pidfile)

	def _pid_running(self, pid: str) -> bool:
		with suppress(OSError):
			os.kill(pid, 0)
			return True

	def get_pid(self) -> int or None:
		with suppress(ValueError, FileNotFoundError), open(self.pidfile, "r") as pf:
			return int(pf.readline().strip())

	def start(self, daemonize: bool = True) -> None:
		if self.get_pid():
			logger.root.warning(f"pidfile {self.pidfile} already exist. '{srvname.capitalize()}' daemon already running?")
			sys.exit(1)

		if daemonize:
			self._daemonize()

		try:
			ip = [(s.connect(('1.1.1.1', 0)), s.getsockname()[0], s.close()) for s in [socket(AF_INET, SOCK_DGRAM)]][0][1]
		except:
			ip = 'localhost'

		socketserver.ForkingTCPServer.allow_reuse_address = True
		with socketserver.ForkingTCPServer(("", srvport), RequestHandler) as server:
			logger.root.info(f"{srvname.capitalize()} version {__version__} started - {ip}:{server.server_address[1]}")
			logger.root.debug(f'Streamlink version: {streamlink_ver}')
			try:
				server.serve_forever()
			except KeyboardInterrupt:
				logger.root.info(f"{srvname.capitalize()} stopped")
			except Exception as err:
				logger.root.error(f"Unexpected error: {err}")

	def stop(self) -> None:
		if pid := self.get_pid():
			os.kill(pid, SIGTERM)
			for _ in range(25):
				time.sleep(0.2)
				if not self._pid_running(pid):
					break
			else:
				logger.root.warning(f"Unable to stop '{srvname}'. Still in use?")
		else:
			logger.root.error(f"pidfile {self.pidfile} not found. Daemon not running?")

	def restart(self, daemonize: bool = True) -> None:
		if self.get_pid():
			self.stop()
		self.start(daemonize)


if __name__ == "__main__":
	daemon = StreamlinkDaemon("/var/run/%s.pid" % srvname)
	match sys.argv[:2]:
		case x, y if y in ("-V", "--version"):
			logger.root.info(f'{srvname.capitalize()} version: {__version__}')
			logger.root.info(f'Streamlink version: {streamlink_ver}')
		case [*_, "start"]:
			daemon.start()
		case [*_, "stop"]:
			daemon.stop()
		case [*_, "restart"]:
			daemon.restart()
		case [*_, "manualstart"]:
			daemon.restart(False)
		case _:
			print(f"usage: {os.path.basename(sys.argv[0])} start|stop|restart|manualstart [--loglevel debug|info|trace]")
			sys.exit(2)
	sys.exit(0)
