From be87dcb9a8ffea4d8b8de89a62b9ff6e5d01395d Mon Sep 17 00:00:00 2001 From: deathbybandaid Date: Thu, 4 Feb 2021 10:58:50 -0500 Subject: [PATCH] Initial commit --- LICENSE | 13 ++++ README.md | 22 ++++++ plugin.json | 5 ++ requirements.txt | 0 stream/__init__.py | 173 +++++++++++++++++++++++++++++++++++++++++++ stream/plugin.json | 3 + web/__init__.py | 13 ++++ web/plugin.json | 3 + web/webwatch.html | 11 +++ web/webwatch_api.py | 124 +++++++++++++++++++++++++++++++ web/webwatch_html.py | 30 ++++++++ webwatch_conf.json | 9 +++ 12 files changed, 406 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 plugin.json create mode 100644 requirements.txt create mode 100644 stream/__init__.py create mode 100644 stream/plugin.json create mode 100644 web/__init__.py create mode 100644 web/plugin.json create mode 100644 web/webwatch.html create mode 100644 web/webwatch_api.py create mode 100644 web/webwatch_html.py create mode 100644 webwatch_conf.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..130e443 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + +Copyright (C) 2017 Sam Zick + +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document, and changing it is allowed as long +as the name is changed. + +DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2943782 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +

fHDHR web Plugin Watch Logo

+ + +Welcome to the world of streaming content as a DVR device! We use some fancy python here to achieve a system of: + +**f**un +**H**ome +**D**istribution +**H**iatus +**R**ecreation + +fHDHR is labeled as beta until we reach v1.0.0 + +Join us in `#fHDHR `_ on Freenode. + +# Installation + +1) Review Installation guide located at [Docs](https://github.com/fHDHR/fHDHR/blob/main/docs/README.md) + +2) Insert this plugin into the `plugins` directory of fHDHR using `git clone` or downloading a release zip file. + +3) Adjust your configuration file with the below settings: diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..a92efbf --- /dev/null +++ b/plugin.json @@ -0,0 +1,5 @@ +{ + "name":"webwatch", + "version":"v0.6.0-beta", + "type":"web" +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/stream/__init__.py b/stream/__init__.py new file mode 100644 index 0000000..dc90f1d --- /dev/null +++ b/stream/__init__.py @@ -0,0 +1,173 @@ +import os +import sys +import subprocess + +from fHDHR.exceptions import TunerError + + +def setup(plugin): + + # Check config for ffmpeg path + ffmpeg_path = None + if plugin.config.dict["webwatch"]["ffmpeg_path"]: + # verify path is valid + if os.path.isfile(plugin.config.dict["webwatch"]["ffmpeg_path"]): + ffmpeg_path = plugin.config.dict["webwatch"]["ffmpeg_path"] + else: + plugin.logger.warning("Failed to find ffmpeg at %s." % plugin.config.dict["webwatch"]["ffmpeg_path"]) + + if not ffmpeg_path: + plugin.logger.info("Attempting to find ffmpeg in PATH.") + if plugin.config.internal["versions"]["Operating System"]["version"] in ["Linux", "Darwin"]: + find_ffmpeg_command = ["which", "ffmpeg"] + elif plugin.config.internal["versions"]["Operating System"]["version"] in ["Windows"]: + find_ffmpeg_command = ["where", "ffmpeg"] + + ffmpeg_proc = subprocess.Popen(find_ffmpeg_command, stdout=subprocess.PIPE) + ffmpeg_path = ffmpeg_proc.stdout.read().decode().strip("\n") + ffmpeg_proc.terminate() + ffmpeg_proc.communicate() + ffmpeg_proc.kill() + if not ffmpeg_path: + ffmpeg_path = None + elif ffmpeg_path.isspace(): + ffmpeg_path = None + + if ffmpeg_path: + plugin.config.dict["webwatch"]["ffmpeg_path"] = ffmpeg_path + + if ffmpeg_path: + ffmpeg_command = [ffmpeg_path, "-version", "pipe:stdout"] + try: + ffmpeg_proc = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE) + ffmpeg_version = ffmpeg_proc.stdout.read().decode().split("version ")[1].split(" ")[0] + except FileNotFoundError: + ffmpeg_version = None + except PermissionError: + ffmpeg_version = None + finally: + ffmpeg_proc.terminate() + ffmpeg_proc.communicate() + ffmpeg_proc.kill() + + if not ffmpeg_version: + ffmpeg_version = "Missing" + plugin.logger.warning("Failed to find ffmpeg.") + + plugin.config.register_version("ffmpeg", ffmpeg_version, "env") + + +class Plugin_OBJ(): + + def __init__(self, fhdhr, plugin_utils, stream_args, tuner): + self.fhdhr = fhdhr + self.plugin_utils = plugin_utils + self.stream_args = stream_args + self.tuner = tuner + + if self.plugin_utils.config.internal["versions"]["ffmpeg"]["version"] == "Missing": + raise TunerError("806 - Tune Failed: FFMPEG Missing") + + self.bytes_per_read = 1024 + self.ffmpeg_command = self.ffmpeg_command_assemble(stream_args) + + def get(self): + + ffmpeg_proc = subprocess.Popen(self.ffmpeg_command, stdout=subprocess.PIPE) + + def generate(): + try: + while self.tuner.tuner_lock.locked(): + + chunk = ffmpeg_proc.stdout.read(self.bytes_per_read) + if not chunk: + break + # raise TunerError("807 - No Video Data") + yield chunk + chunk_size = int(sys.getsizeof(chunk)) + self.tuner.add_downloaded_size(chunk_size) + self.plugin_utils.logger.info("Connection Closed: Tuner Lock Removed") + + except GeneratorExit: + self.plugin_utils.logger.info("Connection Closed.") + except Exception as e: + self.plugin_utils.logger.info("Connection Closed: %s" % e) + finally: + ffmpeg_proc.terminate() + ffmpeg_proc.communicate() + ffmpeg_proc.kill() + self.plugin_utils.logger.info("Connection Closed: Tuner Lock Removed") + if hasattr(self.fhdhr.origins.origins_dict[self.tuner.origin], "close_stream"): + self.fhdhr.origins.origins_dict[self.tuner.origin].close_stream(self.tuner.number, self.stream_args) + self.tuner.close() + # raise TunerError("806 - Tune Failed") + + return generate() + + def ffmpeg_command_assemble(self, stream_args): + ffmpeg_command = [ + self.plugin_utils.config.dict["webwatch"]["ffmpeg_path"], + "-i", stream_args["stream_info"]["url"], + ] + ffmpeg_command.extend(self.ffmpeg_headers(stream_args)) + ffmpeg_command.extend(self.ffmpeg_duration(stream_args)) + ffmpeg_command.extend(self.transcode_profiles(stream_args)) + ffmpeg_command.extend(self.ffmpeg_loglevel()) + ffmpeg_command.extend(["pipe:stdout"]) + return ffmpeg_command + + def ffmpeg_headers(self, stream_args): + ffmpeg_command = [] + if stream_args["stream_info"]["headers"]: + headers_string = "" + if len(list(stream_args["stream_info"]["headers"].keys())) > 1: + for x in list(stream_args["stream_info"]["headers"].keys()): + headers_string += "%s: %s\r\n" % (x, stream_args["stream_info"]["headers"][x]) + else: + for x in list(stream_args["stream_info"]["headers"].keys()): + headers_string += "%s: %s" % (x, stream_args["stream_info"]["headers"][x]) + ffmpeg_command.extend(["-headers", '\"%s\"' % headers_string]) + return ffmpeg_command + + def ffmpeg_duration(self, stream_args): + ffmpeg_command = [] + if stream_args["duration"]: + ffmpeg_command.extend(["-t", str(stream_args["duration"])]) + else: + ffmpeg_command.extend( + [ + "-reconnect", "1", + "-reconnect_at_eof", "1", + "-reconnect_streamed", "1", + "-reconnect_delay_max", "2", + ] + ) + + return ffmpeg_command + + def ffmpeg_loglevel(self): + ffmpeg_command = [] + log_level = self.plugin_utils.config.dict["logging"]["level"].lower() + + loglevel_dict = { + "debug": "debug", + "info": "info", + "error": "error", + "warning": "warning", + "critical": "fatal", + } + if log_level not in ["info", "debug"]: + ffmpeg_command.extend(["-nostats", "-hide_banner"]) + ffmpeg_command.extend(["-loglevel", loglevel_dict[log_level]]) + return ffmpeg_command + + def transcode_profiles(self, stream_args): + + ffmpeg_command = [] + ffmpeg_command.extend([ + "-c:v", "libvpx", + "-c:a", "libvorbis", + "-f", "webm" + ]) + + return ffmpeg_command diff --git a/stream/plugin.json b/stream/plugin.json new file mode 100644 index 0000000..cc7518d --- /dev/null +++ b/stream/plugin.json @@ -0,0 +1,3 @@ +{ + "type":"alt_stream" +} diff --git a/web/__init__.py b/web/__init__.py new file mode 100644 index 0000000..de27672 --- /dev/null +++ b/web/__init__.py @@ -0,0 +1,13 @@ + +from .webwatch_html import Watch_HTML +from .webwatch_api import WebWatch_Tuner + + +class Plugin_OBJ(): + + def __init__(self, fhdhr, plugin_utils): + self.fhdhr = fhdhr + self.plugin_utils = plugin_utils + + self.webwatch_html = Watch_HTML(fhdhr, plugin_utils) + self.webwatch_api = WebWatch_Tuner(fhdhr, plugin_utils) diff --git a/web/plugin.json b/web/plugin.json new file mode 100644 index 0000000..0a5a6e1 --- /dev/null +++ b/web/plugin.json @@ -0,0 +1,3 @@ +{ + "type":"web" +} diff --git a/web/webwatch.html b/web/webwatch.html new file mode 100644 index 0000000..53e77a2 --- /dev/null +++ b/web/webwatch.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block content %} + +

fHDHR Channel Watch

+ + + +{% endblock %} diff --git a/web/webwatch_api.py b/web/webwatch_api.py new file mode 100644 index 0000000..7c08493 --- /dev/null +++ b/web/webwatch_api.py @@ -0,0 +1,124 @@ +from flask import Response, request, redirect, abort, session +import urllib.parse + +from fHDHR.exceptions import TunerError + + +class WebWatch_Tuner(): + endpoints = ["/api/webwatch"] + endpoint_name = "api_webwatch" + endpoint_methods = ["GET", "POST"] + + def __init__(self, fhdhr, plugin_utils): + self.fhdhr = fhdhr + + def __call__(self, *args): + return self.get(*args) + + def get(self, *args): + + client_address = request.remote_addr + + accessed_url = request.args.get('accessed', default=request.url, type=str) + + method = request.args.get('method', default="stream", type=str) + + redirect_url = request.args.get('redirect', default=None, type=str) + + origin_methods = self.fhdhr.origins.valid_origins + origin = request.args.get('origin', default=None, type=str) + if origin and origin not in origin_methods: + return "%s Invalid channels origin" % origin + + if method == "stream": + + channel_number = request.args.get('channel', None, type=str) + if not channel_number: + return "Missing Channel" + + if origin: + + if str(channel_number) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("number", origin)]: + chan_obj = self.fhdhr.device.channels.get_channel_obj("number", channel_number, origin) + elif str(channel_number) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("id", origin)]: + chan_obj = self.fhdhr.device.channels.get_channel_obj("id", channel_number, origin) + else: + response = Response("Not Found", status=404) + response.headers["X-fHDHR-Error"] = "801 - Unknown Channel" + self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) + abort(response) + + else: + + if str(channel_number) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("id")]: + chan_obj = self.fhdhr.device.channels.get_channel_obj("id", channel_number) + else: + response = Response("Not Found", status=404) + response.headers["X-fHDHR-Error"] = "801 - Unknown Channel" + self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) + abort(response) + + if not chan_obj.dict["enabled"]: + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = str("806 - Tune Failed") + self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) + abort(response) + + origin = chan_obj.origin + channel_number = chan_obj.number + + duration = request.args.get('duration', default=0, type=int) + + stream_args = { + "channel": channel_number, + "origin": origin, + "method": "webwatch", + "duration": duration, + "origin_quality": self.fhdhr.config.dict["streaming"]["origin_quality"], + "transcode_quality": self.fhdhr.config.dict["streaming"]["transcode_quality"], + "accessed": accessed_url, + "client": client_address, + "client_id": session["session_id"] + } + + try: + tunernum = self.fhdhr.device.tuners.first_available(origin, channel_number) + except TunerError as e: + self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s" + % (stream_args["method"], str(stream_args["channel"]), str(e))) + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = str(e) + self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) + abort(response) + + tuner = self.fhdhr.device.tuners.tuners[origin][str(tunernum)] + + try: + stream_args = self.fhdhr.device.tuners.get_stream_info(stream_args) + except TunerError as e: + self.fhdhr.logger.info("A %s stream request for %s channel %s was rejected due to %s" + % (origin, stream_args["method"], str(stream_args["channel"]), str(e))) + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = str(e) + self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) + tuner.close() + abort(response) + + self.fhdhr.logger.info("%s Tuner #%s to be used for stream." % (origin, tunernum)) + tuner.set_status(stream_args) + session["tuner_used"] = tunernum + + try: + stream = tuner.get_stream(stream_args, tuner) + except TunerError as e: + response.headers["X-fHDHR-Error"] = str(e) + self.fhdhr.logger.error(response.headers["X-fHDHR-Error"]) + tuner.close() + abort(response) + + return Response(stream.get()) + + if redirect_url: + return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Success" % method))) + else: + return "%s Success" % method diff --git a/web/webwatch_html.py b/web/webwatch_html.py new file mode 100644 index 0000000..f293176 --- /dev/null +++ b/web/webwatch_html.py @@ -0,0 +1,30 @@ +from flask import request, render_template_string, session +import pathlib +from io import StringIO + + +class Watch_HTML(): + endpoints = ["/webwatch", "/webwatch.html"] + endpoint_name = "page_webwatch_html" + endpoint_access_level = 0 + pretty_name = "Watch" + endpoint_category = "pages" + + def __init__(self, fhdhr, plugin_utils): + self.fhdhr = fhdhr + + self.template_file = pathlib.Path(plugin_utils.config.dict["plugin_web_paths"][plugin_utils.namespace]["path"]).joinpath('webwatch.html') + self.template = StringIO() + self.template.write(open(self.template_file).read()) + + def __call__(self, *args): + return self.get(*args) + + def get(self, *args): + + origin = self.fhdhr.origins.valid_origins[0] + channel_id = [x["id"] for x in self.fhdhr.device.channels.get_channels(origin)][0] + + watch_url = '/api/webwatch?method=stream&channel=%s&origin=%s' % (channel_id, origin) + + return render_template_string(self.template.getvalue(), request=request, session=session, fhdhr=self.fhdhr, watch_url=watch_url) diff --git a/webwatch_conf.json b/webwatch_conf.json new file mode 100644 index 0000000..dc9ea1c --- /dev/null +++ b/webwatch_conf.json @@ -0,0 +1,9 @@ +{ + "webwatch":{ + "ffmpeg_path":{ + "value": "none", + "config_file": true, + "config_web": true + } + } +}