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 { {name} {fsize} ; + stream_button = + + ; + } else if (filetype === "image") { card_header = name; card_body = @@ -82,7 +88,11 @@ class FilesList extends React.Component { {name} {fsize} ; - + } + if (filetype !== "dir") { + download_button = + Download + ; } final.push( @@ -95,9 +105,10 @@ class FilesList extends React.Component { {card_body} diff --git a/src/core/static/js/webfm/ui-logic.js b/src/core/static/js/webfm/ui-logic.js index ba658cc..5bdba5b 100644 --- a/src/core/static/js/webfm/ui-logic.js +++ b/src/core/static/js/webfm/ui-logic.js @@ -49,7 +49,7 @@ const scrollFilesToTop = () => { } -const closeFile = () => { +const closeFile = async () => { const video = document.getElementById("video"); const trailerPlayer = document.getElementById("trailerPlayer") let title = document.getElementById("selectedFile"); @@ -65,6 +65,9 @@ const closeFile = () => { trailerPlayer.src = "#"; trailerPlayer.style.display = "none"; + + // FIXME: Yes, a wasted call every time there is no stream. + await fetchData("api/stop-current-stream"); clearSelectedActiveMedia(); clearModalFades(); } @@ -79,15 +82,16 @@ const showFile = async (title, hash, extension, type, target=null) => { let titleElm = document.getElementById("selectedFile"); titleElm.innerText = title; - if (type === "video") { - setupVideo(hash, extension); + // FIXME: Yes, a wasted call every time there is no stream. + await fetchData("api/stop-current-stream"); + if (type === "video" || type === "stream") { + isStream = (type === "stream") + setupVideo(hash, extension, isStream); setSelectedActiveMedia(target); - } - if (type === "file") { - setupFile(hash, extension); - } - if (type === "trailer") { + } else if (type === "trailer") { launchTrailer(hash); + } else { + setupFile(hash, extension); } } @@ -100,7 +104,7 @@ const launchTrailer = (link) => { modal.show(); } -const setupVideo = async (hash, extension) => { +const setupVideo = async (hash, extension, isStream=false) => { let modal = new bootstrap.Modal(document.getElementById('file-view-modal'), { keyboard: false }); let video = document.getElementById("video"); video.poster = "static/imgs/icons/loading.gif"; @@ -109,13 +113,23 @@ const setupVideo = async (hash, extension) => { video_path = "api/file-manager-action/files/" + hash; clearSelectedActiveMedia(); - try { + try { if ((/\.(avi|mkv|wmv|flv|f4v|mov|m4v|mpg|mpeg|mp4|webm|mp3|flac|ogg)$/i).test(extension)) { if ((/\.(avi|mkv|wmv|flv|f4v)$/i).test(extension)) { - data = await fetchData( "api/file-manager-action/remux/" + hash ); - if ( data.hasOwnProperty('path') ) { - video_path = data.path; + if (isStream) { + data = await fetchData( "api/file-manager-action/stream/" + hash ); + if (data.hasOwnProperty('stream')) { + video_path = data.stream; + } } else { + data = await fetchData( "api/file-manager-action/remux/" + hash ); + if (data.hasOwnProperty('path')) { + video_path = data.path; + } + } + + if (data.hasOwnProperty('path') === null && + data.hasOwnProperty('stream') === null) { displayMessage(data.message.text, data.message.type); return; } @@ -254,14 +268,16 @@ const updateBackground = (srcLink, isvideo = true) => { const manageFavorites = (elm) => { - const classType = "btn-info"; + const classType = "btn-warning"; const hasClass = elm.classList.contains(classType); if (hasClass) { - elm.classList.remove(classType); action = "delete"; + elm.classList.remove(classType); + elm.classList.add("btn-secondary"); } else { - elm.classList.add(classType); action = "add"; + elm.classList.add(classType); + elm.classList.remove("btn-secondary"); } manageFavoritesAjax(action); diff --git a/src/core/templates/body-footer.html b/src/core/templates/body-footer.html index fd45ca6..0700a7f 100644 --- a/src/core/templates/body-footer.html +++ b/src/core/templates/body-footer.html @@ -9,7 +9,7 @@