diff --git a/src/core/__builtins__.py b/src/core/__builtins__.py
index 19bc464..e524e13 100644
--- a/src/core/__builtins__.py
+++ b/src/core/__builtins__.py
@@ -4,6 +4,7 @@ import builtins
import threading
import re
import secrets
+import subprocess
# Lib imports
from flask import session
@@ -14,6 +15,9 @@ from core.utils import Logger
from core.utils import MessageHandler # Get simple message processor
+class BuiltinsException(Exception):
+ ...
+
# NOTE: Threads WILL NOT die with parent's destruction.
def threaded_wrapper(fn):
@@ -42,22 +46,54 @@ def _get_file_size(file):
# NOTE: Just reminding myself we can add to builtins two different ways...
# __builtins__.update({"event_system": Builtins()})
-builtins.app_name = "WebFM"
-builtins.threaded = threaded_wrapper
-builtins.daemon_threaded = daemon_threaded_wrapper
-builtins.sizeof_fmt = sizeof_fmt_def
-builtins.get_file_size = _get_file_size
-builtins.ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__))
-builtins.BG_IMGS_PATH = ROOT_FILE_PTH + "/static/imgs/backgrounds/"
-builtins.BG_FILE_TYPE = (".webm", ".mp4", ".gif", ".jpg", ".png", ".webp")
-builtins.valid_fname_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]{4,20}")
-builtins.logger = Logger().get_logger()
-builtins.json_message = MessageHandler()
+builtins.app_name = "WebFM"
+builtins.threaded = threaded_wrapper
+builtins.daemon_threaded = daemon_threaded_wrapper
+builtins.sizeof_fmt = sizeof_fmt_def
+builtins.get_file_size = _get_file_size
+builtins.ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__))
+builtins.BG_IMGS_PATH = ROOT_FILE_PTH + "/static/imgs/backgrounds/"
+builtins.BG_FILE_TYPE = (".webm", ".mp4", ".gif", ".jpg", ".png", ".webp")
+builtins.valid_fname_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]{4,20}")
+builtins.logger = Logger().get_logger()
+builtins.json_message = MessageHandler()
+
+
+
+# NOTE: Need threads defined before instantiating
+
+def _start_rtsp_server():
+ PATH = f"{ROOT_FILE_PTH}/utils/rtsp-server"
+ RAMFS = "/dev/shm/webfm"
+ SYMLINK = f"{PATH}/m3u8_stream_files"
+
+ if not os.path.exists(PATH) or not os.path.exists(f"{PATH}/rtsp-simple-server"):
+ msg = f"\n\nAlert: Reference --> https://github.com/aler9/rtsp-simple-server/releases" + \
+ f"\nPlease insure {PATH} exists and rtsp-simple-server binary is there.\n\n"
+ raise BuiltinsException(msg)
+
+ if not os.path.exists(RAMFS):
+ os.mkdir(RAMFS)
+ if not os.path.exists(f"{PATH}/m3u8_stream_files"):
+ os.symlink(RAMFS, SYMLINK)
+
+ @daemon_threaded
+ def _start_rtsp_server_threaded():
+ os.chdir(PATH)
+ command = ["./rtsp-simple-server", "./rtsp-simple-server.yml"]
+ process = subprocess.Popen(command)
+ process.wait()
+
+ _start_rtsp_server_threaded()
+
+_start_rtsp_server()
+
-# NOTE: Need threads defined befor instantiating
from core.utils.shellfm.windows.controller import WindowController # Get file manager controller
window_controllers = {}
+processes = {}
+
def _get_view():
controller = None
try:
@@ -85,11 +121,64 @@ def _get_view():
view.logger = logger
session['win_controller_id'] = id
- window_controllers.update( {id: controller } )
+ window_controllers.update( {id: controller} )
controller = window_controllers[ session["win_controller_id"] ].get_window_by_index(0).get_tab_by_index(0)
return controller
+def _get_stream(video_path=None, stream_target=None):
+ process = None
+ try:
+ window = window_controllers[ session["win_controller_id"] ].get_window_by_index(0)
+ tab = window.get_tab_by_index(0)
+ id = f"{window.get_id()}{tab.get_id()}"
+ process = processes[id]
+ except Exception as e:
+ if video_path and stream_target:
+ # NOTE: Yes, technically we should check if cuda is supported.
+ # Yes, the process probably should give us info we can process when failure occures.
+ command = [
+ "ffmpeg", "-nostdin", "-fflags", "+genpts", "-hwaccel", "cuda",
+ "-stream_loop", "1", "-i", video_path, "-strict", "experimental",
+ "-vcodec", "copy", "-acodec", "copy", "-f", "rtsp",
+ "-rtsp_transport", "udp", stream_target
+ ]
-builtins.get_view = _get_view
+ # command = [
+ # "ffmpeg", "-nostdin", "-fflags", "+genpts", "-i", video_path,
+ # "-strict", "experimental", "-f",
+ # "rtsp", "-rtsp_transport", "udp", stream_target
+ # ]
+
+ proc = subprocess.Popen(command, shell=False, stdin=None, stdout=None, stderr=None)
+ window = window_controllers[ session["win_controller_id"] ].get_window_by_index(0)
+ tab = window.get_tab_by_index(0)
+ id = f"{window.get_id()}{tab.get_id()}"
+
+ processes.update( {id: proc} )
+ process = processes[id]
+
+ return process
+
+def _kill_stream(process):
+ try:
+ if process.poll() == None:
+ process.terminate()
+ while process.poll() == None:
+ ...
+
+ window = window_controllers[ session["win_controller_id"] ].get_window_by_index(0)
+ tab = window.get_tab_by_index(0)
+ id = f"{window.get_id()}{tab.get_id()}"
+ del processes[id]
+ except Exception as e:
+ return False
+
+ return True
+
+
+
+builtins.get_view = _get_view
+builtins.get_stream = _get_stream
+builtins.kill_stream = _kill_stream
diff --git a/src/core/__init__.py b/src/core/__init__.py
index cafddee..aacb05e 100644
--- a/src/core/__init__.py
+++ b/src/core/__init__.py
@@ -1,4 +1,6 @@
# Python imports
+import os
+import subprocess
# Lib imports
from flask import Flask
@@ -11,7 +13,6 @@ from flask_login import login_user
from flask_login import logout_user
from flask_login import LoginManager
-
app = Flask(__name__)
app.config.from_object("core.config.ProductionConfig")
# app.config.from_object("core.config.DevelopmentConfig")
@@ -40,6 +41,7 @@ app.jinja_env.globals['oidc_isAdmin'] = oidc_isAdmin
app.jinja_env.globals['TITLE'] = app.config["TITLE"]
+
from core.models import db
from core.models import User
from core.models import Favorites
diff --git a/src/core/routes/CRUD.py b/src/core/routes/CRUD.py
index c3770c6..8f4a193 100644
--- a/src/core/routes/CRUD.py
+++ b/src/core/routes/CRUD.py
@@ -41,6 +41,8 @@ def delete_item(_hash = None):
msg = "[Error] Unable to delete the file/folder...."
return json_message.create("danger", msg)
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
@app.route('/api/create/<_type>', methods=['GET', 'POST'])
@@ -77,9 +79,9 @@ def create_item(_type = None):
msg = "[Success] created the file/dir..."
return json_message.create("success", msg)
- else:
- msg = "Can't manage the request type..."
- return json_message.create("danger", msg)
+
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
@app.route('/api/upload', methods=['GET', 'POST'])
@@ -109,6 +111,6 @@ def upload():
msg = "[Success] Uploaded file(s)..."
return json_message.create("success", msg)
- else:
- msg = "Can't manage the request type..."
- return json_message.create("danger", msg)
+
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
diff --git a/src/core/routes/Favorites.py b/src/core/routes/Favorites.py
index b449e21..06f451f 100644
--- a/src/core/routes/Favorites.py
+++ b/src/core/routes/Favorites.py
@@ -11,7 +11,7 @@ from core import Favorites # Get from __init__
@app.route('/api/list-favorites', methods=['GET', 'POST'])
-def listFavorites():
+def list_favorites():
if request.method == 'POST':
list = db.session.query(Favorites).all()
faves = []
@@ -19,12 +19,12 @@ def listFavorites():
faves.append([fave.link, fave.id])
return json_message.faves_list(faves)
- else:
- msg = "Can't manage the request type..."
- return json_message.create("danger", msg)
+
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
@app.route('/api/load-favorite/<_id>', methods=['GET', 'POST'])
-def loadFavorite(_id):
+def load_favorite(_id):
if request.method == 'POST':
try:
ID = int(_id)
@@ -36,13 +36,13 @@ def loadFavorite(_id):
print(repr(e))
msg = "Incorrect Favorites ID..."
return json_message.create("danger", msg)
- else:
- msg = "Can't manage the request type..."
- return json_message.create("danger", msg)
+
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
@app.route('/api/manage-favorites/<_action>', methods=['GET', 'POST'])
-def manageFavorites(_action):
+def manage_favorites(_action):
if request.method == 'POST':
ACTION = _action.strip()
view = get_view()
@@ -62,6 +62,6 @@ def manageFavorites(_action):
db.session.commit()
return json_message.create("success", msg)
- else:
- msg = "Can't manage the request type..."
- return json_message.create("danger", msg)
+
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
diff --git a/src/core/routes/Images.py b/src/core/routes/Images.py
index ea94ade..321dfdd 100644
--- a/src/core/routes/Images.py
+++ b/src/core/routes/Images.py
@@ -18,7 +18,7 @@ tmdb = scraper.get_tmdb_scraper()
@app.route('/api/get-background-poster-trailer', methods=['GET', 'POST'])
-def getPosterTrailer():
+def get_poster_trailer():
if request.method == 'GET':
info = {}
view = get_view()
@@ -69,6 +69,8 @@ def getPosterTrailer():
return info
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
@app.route('/backgrounds', methods=['GET', 'POST'])
def backgrounds():
@@ -81,10 +83,10 @@ def backgrounds():
return json_message.backgrounds(files)
@app.route('/api/get-thumbnails', methods=['GET', 'POST'])
-def getThumbnails():
+def get_thumbnails():
if request.method == 'GET':
view = get_view()
return json_message.thumbnails( view.get_video_icons() )
- else:
- msg = "Can't manage the request type..."
- return json_message.create("danger", msg)
+
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
diff --git a/src/core/routes/Routes.py b/src/core/routes/Routes.py
index 7fd581c..43a8d43 100644
--- a/src/core/routes/Routes.py
+++ b/src/core/routes/Routes.py
@@ -1,5 +1,7 @@
# Python imports
import os
+# import subprocess
+import uuid
# Lib imports
from flask import redirect
@@ -30,7 +32,7 @@ def home():
@app.route('/api/list-files/<_hash>', methods=['GET', 'POST'])
-def listFiles(_hash = None):
+def list_files(_hash = None):
if request.method == 'POST':
view = get_view()
dot_dots = view.get_dot_dots()
@@ -64,13 +66,13 @@ def listFiles(_hash = None):
in_fave = "true" if fave else "false"
files.update({'in_fave': in_fave})
return files
- else:
- msg = "Can't manage the request type..."
- return json_message.create("danger", msg)
+
+ msg = "Can't manage the request type..."
+ return json_message.create("danger", msg)
@app.route('/api/file-manager-action/<_type>/<_hash>', methods=['GET', 'POST'])
-def fileManagerAction(_type, _hash = None):
+def file_manager_action(_type, _hash = None):
view = get_view()
if _type == "reset-path" and _hash == "None":
@@ -86,20 +88,39 @@ def fileManagerAction(_type, _hash = None):
if _type == "files":
logger.debug(f"Downloading:\n\tDirectory: {folder}\n\tFile: {file}")
return send_from_directory(directory=folder, filename=file)
+
if _type == "remux":
# NOTE: Need to actually implimint a websocket to communicate back to client that remux has completed.
# As is, the remux thread hangs until completion and client tries waiting until server reaches connection timeout.
# I.E....this is stupid but for now works better than nothing
good_result = view.remux_video(_hash, fpath)
- if good_result:
- return '{"path":"static/remuxs/' + _hash + '.mp4"}'
- else:
+ if not good_result:
msg = "Remuxing: Remux failed or took too long; please, refresh the page and try again..."
- return json_message.create("success", msg)
+ return json_message.create("warning", msg)
- if _type == "remux":
- stream_target = view.remux_video(_hash, fpath)
+ return '{"path":"static/remuxs/' + _hash + '.mp4"}'
+ if _type == "stream":
+ process = get_stream()
+ if process:
+ if not kill_stream(process):
+ msg = "Couldn't stop an existing stream!"
+ return json_message.create("danger", msg)
+
+ _sub_uuid = uuid.uuid4().hex
+ _video_path = fpath
+ _stub = f"{_hash}{_sub_uuid}"
+ _rtsp_path = f"rtsp://www.{app_name.lower()}.com:8554/{_stub}"
+ _webrtc_path = f"http://www.{app_name.lower()}.com:8889/{_stub}/"
+ _stream_target = _rtsp_path
+
+ stream = get_stream(_video_path, _stream_target)
+ if stream.poll():
+ msg = "Streaming: Setting up stream failed! Please try again..."
+ return json_message.create("danger", msg)
+
+ _stream_target = _webrtc_path
+ return {"stream": _stream_target}
# NOTE: Positionally protecting actions further down that are privlidged
# Be aware of ordering!
@@ -116,3 +137,20 @@ def fileManagerAction(_type, _hash = None):
msg = "Opened media..."
view.open_file_locally(fpath)
return json_message.create("success", msg)
+
+
+@app.route('/api/stop-current-stream', methods=['GET', 'POST'])
+def stop_current_stream():
+ type = "success"
+ msg = "Stopped found stream process..."
+ process = get_stream()
+
+ if process:
+ if not kill_stream(process):
+ type = "danger"
+ msg = "Couldn't stop an existing stream!"
+ else:
+ type = "warning"
+ msg = "No stream process found. Nothing to stop..."
+
+ return json_message.create(type, msg)
diff --git a/src/core/static/css/context-menu.css b/src/core/static/css/context-menu.css
index 781dc3a..dcc3832 100644
--- a/src/core/static/css/context-menu.css
+++ b/src/core/static/css/context-menu.css
@@ -1,3 +1,8 @@
+#ctxDownload,
+#img2Tab {
+ display: none;
+}
+
.menu {
width: 165px;
z-index: 999;
@@ -7,15 +12,17 @@
display: none;
transition: 0.2s display ease-in;
}
+
.menu .menu-options {
list-style: none;
- padding: 10px 0;
z-index: 1;
+ padding: unset !important;
+ margin: unset !important;
}
+
.menu .menu-options .menu-option {
font-weight: 500;
- z-index: 1;
- padding: 10px 40px 10px 20px;
+ padding: 10px 20px 10px 20px;
cursor: pointer;
}
@@ -28,9 +35,11 @@ button {
background: grey;
border: none;
}
+
button .next {
color: green;
}
+
button[disabled="false"]:hover .next {
color: red;
animation: move 0.5s;
diff --git a/src/core/static/js/webfm/backgrounds-manager.js b/src/core/static/js/webfm/backgrounds-manager.js
index cea809c..c7fcb19 100644
--- a/src/core/static/js/webfm/backgrounds-manager.js
+++ b/src/core/static/js/webfm/backgrounds-manager.js
@@ -29,6 +29,8 @@ const loadBackground = () => {
setCookie('bgSlug', path);
setBackgroundElement(bgElm, path);
} else {
+ // NOTE: Probably in IFRAME and unloaded the background...
+ if (!bgElm) return ;
setBackgroundElement(bgElm, bgPath);
}
}
diff --git a/src/core/static/js/webfm/context-menu.js b/src/core/static/js/webfm/context-menu.js
index 518e067..90fa7bf 100644
--- a/src/core/static/js/webfm/context-menu.js
+++ b/src/core/static/js/webfm/context-menu.js
@@ -1,6 +1,16 @@
-const menu = document.querySelector(".menu");
-let menuVisible = false;
-let active_card = null;
+const img2TabElm = document.getElementById("img2Tab");
+const ctxDownloadElm = document.getElementById("ctxDownload");
+const menu = document.querySelector(".menu");
+
+let menuVisible = false;
+let img2TabSrc = null;
+let active_card = null;
+
+const img2Tab = () => {
+ if (img2TabSrc !== null) {
+ window.open(img2TabSrc,'_blank');
+ }
+};
const toggleMenu = command => {
menu.style.display = command === "show" ? "block" : "none";
@@ -24,6 +34,16 @@ document.body.addEventListener("contextmenu", e => {
let target = e.target;
let elm = target;
+
+ ctxDownloadElm.style.display = (elm.nodeName === "IMG" ||
+ (elm.hasAttribute("ftype") &&
+ (elm.getAttribute("ftype") === "file" ||
+ elm.getAttribute("ftype") === "video" ||
+ elm.getAttribute("ftype") === "image")
+ )) ? "block" : "none";
+ img2TabElm.style.display = (elm.nodeName === "IMG") ? "block" : "none";
+ img2TabSrc = (elm.nodeName === "IMG") ? elm.src : null;
+
while (elm.nodeName != "BODY") {
if (!elm.classList.contains("card")) {
elm = elm.parentElement;
diff --git a/src/core/static/js/webfm/events.js b/src/core/static/js/webfm/events.js
index bd99dcb..f840284 100644
--- a/src/core/static/js/webfm/events.js
+++ b/src/core/static/js/webfm/events.js
@@ -41,10 +41,10 @@ const openFile = (eve) => {
if (ftype === "dir") {
listFilesAjax(hash);
- } else if (ftype === "video") {
- showFile(title, hash, extension, "video", target);
+ } else if (ftype === "video" || ftype === "stream") {
+ showFile(title, hash, extension, ftype, target);
} else {
- showFile(title, hash, extension, "file");
+ showFile(title, hash, extension, ftype);
}
}
diff --git a/src/core/static/js/webfm/post-ajax.js b/src/core/static/js/webfm/post-ajax.js
index aa6cf74..f319790 100644
--- a/src/core/static/js/webfm/post-ajax.js
+++ b/src/core/static/js/webfm/post-ajax.js
@@ -63,11 +63,13 @@ const updateHTMLDirList = async (data) => {
// Set faves state
let tggl_faves_btn = document.getElementById("tggl-faves-btn");
- if (isInFaves == "true")
- tggl_faves_btn.classList.add("btn-info");
- else
- tggl_faves_btn.classList.remove("btn-info");
-
+ if (isInFaves == "true") {
+ tggl_faves_btn.classList.remove("btn-secondary");
+ tggl_faves_btn.classList.add("btn-warning");
+ } else {
+ tggl_faves_btn.classList.add("btn-secondary");
+ tggl_faves_btn.classList.remove("btn-warning");
+ }
renderFilesList(data.list);
loadBackgroundPoster();
diff --git a/src/core/static/js/webfm/react-ui-logic.js b/src/core/static/js/webfm/react-ui-logic.js
index 750150b..e62e0f9 100644
--- a/src/core/static/js/webfm/react-ui-logic.js
+++ b/src/core/static/js/webfm/react-ui-logic.js
@@ -55,14 +55,16 @@ class FilesList extends React.Component {
const name = file[0];
if (name == "000.jpg") { continue }
- const hash = file[1];
- const fsize = file[2];
- let extension = re.exec( name.toLowerCase() )[1] ? name : "file.dir";
- let data = setFileIconType(extension);
- let icon = data[0];
- let filetype = data[1];
+ const hash = file[1];
+ const fsize = file[2];
+ let extension = re.exec( name.toLowerCase() )[1] ? name : "file.dir";
+ let data = setFileIconType(extension);
+ let icon = data[0];
+ let filetype = data[1];
let card_header = null;
let card_body = null;
+ let download_button = null;
+ let stream_button = null;
if (filetype === "video") {
card_header = name;
@@ -70,6 +72,10 @@ class FilesList extends React.Component {
{fsize}
;
+ stream_button =