Initial commit

This commit is contained in:
deathbybandaid 2021-02-04 10:58:50 -05:00
commit be87dcb9a8
12 changed files with 406 additions and 0 deletions

13
LICENSE Normal file
View File

@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2017 Sam Zick <Sam@deathbybandaid.net>
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.

22
README.md Normal file
View File

@ -0,0 +1,22 @@
<p align="center">fHDHR web Plugin Watch <img src="docs/images/logo.ico" alt="Logo"/></p>
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 <irc://irc.freenode.net/#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:

5
plugin.json Normal file
View File

@ -0,0 +1,5 @@
{
"name":"webwatch",
"version":"v0.6.0-beta",
"type":"web"
}

0
requirements.txt Normal file
View File

173
stream/__init__.py Normal file
View File

@ -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

3
stream/plugin.json Normal file
View File

@ -0,0 +1,3 @@
{
"type":"alt_stream"
}

13
web/__init__.py Normal file
View File

@ -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)

3
web/plugin.json Normal file
View File

@ -0,0 +1,3 @@
{
"type":"web"
}

11
web/webwatch.html Normal file
View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block content %}
<h4 style="text-align: center;">fHDHR Channel Watch</h4>
<video width='50%' height='40%' autoplay controls>
<source src="{{ watch_url }}"> Your browser does not support the video tag.
</video>
{% endblock %}

124
web/webwatch_api.py Normal file
View File

@ -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

30
web/webwatch_html.py Normal file
View File

@ -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)

9
webwatch_conf.json Normal file
View File

@ -0,0 +1,9 @@
{
"webwatch":{
"ffmpeg_path":{
"value": "none",
"config_file": true,
"config_web": true
}
}
}