├── .gitignore ├── LICENSE ├── README.md ├── app ├── main │ ├── __init__.py │ ├── app.py │ ├── auth │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── session.py │ │ └── views.py │ ├── check │ │ ├── __init__.py │ │ ├── check.py │ │ ├── ms_smooth.py │ │ └── youtube.py │ ├── common │ │ ├── __init__.py │ │ ├── config.py │ │ ├── database.py │ │ ├── extensions.py │ │ ├── log.py │ │ ├── models.py │ │ └── wrappers.py │ ├── daemons │ │ ├── __init__.py │ │ ├── daemons.py │ │ └── register.py │ ├── job │ │ ├── __init__.py │ │ ├── job.py │ │ └── views.py │ ├── media │ │ ├── __init__.py │ │ ├── media.py │ │ └── views.py │ ├── node │ │ ├── __init__.py │ │ ├── node.py │ │ └── views.py │ ├── pipe │ │ ├── __init__.py │ │ └── pipe.py │ ├── preset │ │ ├── __init__.py │ │ ├── preset.py │ │ └── views.py │ ├── server │ │ ├── __init__.py │ │ ├── server.py │ │ └── views.py │ ├── settings │ │ ├── __init__.py │ │ ├── settings.py │ │ └── views.py │ ├── smooth │ │ ├── __init__.py │ │ └── smooth.py │ ├── tools │ │ ├── __init__.py │ │ ├── alarm.py │ │ ├── file.py │ │ ├── json.py │ │ ├── system.py │ │ ├── toolset.py │ │ └── views.py │ └── user │ │ ├── __init__.py │ │ ├── user.py │ │ └── views.py ├── make_bin.py ├── make_bin_run.sh └── run.py ├── conf ├── dev │ ├── default.env │ ├── pipenc_base.service │ ├── pipenc_base_logs │ ├── pipenc_base_nginx │ └── pipenc_base_uwsgi.ini └── prod │ ├── default.env │ ├── pipenc_base.service │ ├── pipenc_base_logs │ ├── pipenc_base_nginx │ └── pipenc_base_uwsgi.ini ├── db └── migrations │ ├── README │ ├── alembic.ini │ ├── env.py │ ├── script.py.mako │ └── versions │ └── .gitignore ├── drm ├── auth │ ├── .htpasswd │ └── set_drm_auth.sh ├── key.bin ├── key.hex └── keygen.sh ├── media ├── assets │ ├── clips │ │ ├── demo1.mp4 │ │ ├── demo2.mp4 │ │ ├── demo3.mp4 │ │ ├── demo4.mp4 │ │ ├── demo5.mp4 │ │ ├── demo6.mp4 │ │ ├── demo7.mp4 │ │ ├── demo_closed_captions.ts │ │ ├── please_stand_by_1080.ts │ │ ├── please_stand_by_480.ts │ │ ├── please_stand_by_576.ts │ │ └── please_stand_by_720.ts │ └── images │ │ ├── 01_please_stand_by_480.jpg │ │ ├── 02_please_stand_by_576.jpg │ │ ├── 03_please_stand_by_720.jpg │ │ └── 04_please_stand_by_1080.jpg └── logo │ ├── pipencoder_icon.ico │ ├── pipencoder_icon.png │ ├── pipencoder_logo.jpg │ ├── pipencoder_logo.png │ ├── pipencoder_preview.jpg │ └── pipencoder_preview_alpha.png ├── presets ├── audio │ ├── AAC_LC_128 │ ├── AAC_LC_256 │ ├── AAC_LC_64 │ ├── AAC_LC_96 │ ├── Copy │ └── Decklink └── video │ ├── Copy │ ├── Decklink │ ├── H264_1300 │ ├── H264_2500 │ ├── H264_4000 │ ├── H264_650 │ ├── H264_NVENC_1300 │ ├── H264_NVENC_2500 │ ├── H264_NVENC_4000 │ ├── H264_NVENC_650 │ ├── H264_NVENC_CBR_1300 │ ├── H264_NVENC_CBR_2500 │ ├── H264_NVENC_CBR_4000 │ ├── H264_NVENC_CBR_650 │ ├── H264_NVENC_HQ_1300 │ ├── H264_NVENC_HQ_2500 │ ├── H264_NVENC_HQ_4000 │ ├── H264_NVENC_HQ_650 │ ├── HEVC_1300 │ ├── HEVC_2500 │ ├── HEVC_4000 │ ├── HEVC_650 │ ├── HEVC_NVENC_1300 │ ├── HEVC_NVENC_2500 │ ├── HEVC_NVENC_4000 │ ├── HEVC_NVENC_650 │ ├── HEVC_NVENC_HQ_1300 │ ├── HEVC_NVENC_HQ_2500 │ ├── HEVC_NVENC_HQ_4000 │ └── HEVC_NVENC_HQ_650 ├── templates ├── mail_action.html └── mail_error.html ├── tests ├── api_login_gui.sh ├── api_login_key.sh ├── api_test.py ├── api_test_header_key.sh ├── api_test_json_key.sh └── tests │ ├── test_auth.py │ ├── test_job.py │ ├── test_media.py │ ├── test_node.py │ ├── test_preset.py │ ├── test_server.py │ ├── test_settings.py │ ├── test_tools.py │ └── test_user.py └── wwwroot ├── crossdomain.xml ├── css ├── app.css ├── bootstrap.min.css ├── bootstrap.min.css.map └── font-awesome │ ├── css │ ├── font-awesome.css │ └── font-awesome.min.css │ └── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── favicon.ico ├── favicon_umedialink.ico ├── fonts ├── glyphicons-halflings-regular.eot ├── glyphicons-halflings-regular.svg ├── glyphicons-halflings-regular.ttf ├── glyphicons-halflings-regular.woff └── glyphicons-halflings-regular.woff2 ├── html ├── brand.html ├── events.html ├── events_update.html ├── global_message.html ├── job_edit.html ├── job_edit_abr.html ├── job_edit_checks.html ├── job_edit_drm.html ├── job_edit_info.html ├── job_edit_preview.html ├── job_edit_profile.html ├── job_edit_source.html ├── job_edit_target.html ├── job_list.html ├── job_list_filters.html ├── job_list_navbar.html ├── job_list_pagination.html ├── log.html ├── login.html ├── mediainfo.html ├── navbar_side.html ├── player.html ├── settings.html ├── settings_alarm.html ├── settings_failover.html ├── settings_lic.html ├── settings_mail.html ├── settings_preset.html ├── settings_server.html └── settings_user.html ├── images ├── logo_medialink.png ├── logo_pipencoder.png ├── ss_failover_pipencoder.png ├── ss_failover_uencode.png └── ss_failover_umedialink.png ├── index.html └── js ├── angular ├── angular-route.min.js ├── angular.min.js ├── clipboard.min.js ├── ngclipboard.min.js └── ui-bootstrap-tpls-2.5.0.min.js ├── app ├── app.js ├── auth.js ├── config.js ├── events.js ├── factory.js ├── filters.js ├── jobs.js ├── log.js ├── nav.js ├── player.js └── settings.js ├── initial └── modernizr-2.8.3-respond-1.4.2.min.js └── videojs ├── font ├── VideoJS.eot ├── VideoJS.svg ├── VideoJS.ttf └── VideoJS.woff ├── hls └── videojs-contrib-hls.min.js ├── ie8 ├── videojs-ie8.js └── videojs-ie8.min.js ├── video-js.css ├── video-js.min.css ├── video-js.swf ├── video.js ├── video.js.map ├── video.min.js └── video.min.js.map /.gitignore: -------------------------------------------------------------------------------- 1 | *.pid 2 | *.sock 3 | *.py[cod] 4 | *.log 5 | *.log.* 6 | *.db 7 | *.db-* 8 | *.pem 9 | /drm/aes-128/* 10 | /media/hls/* 11 | /media/ss/* 12 | /app/build 13 | /venv 14 | migrations/versions/* 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dmitry Kashin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Pipencoder 2 | GUI and automation for media transcoding and streaming. 3 | 4 | - Create your custom pipelines using your favorite CLI media tools. 5 | - Mix and match open-soure and commercial software in your workflow. 6 | 7 | Project comes with AngularJS GUI and FFMPEG powerful presets out of the box. 8 | 9 | Some action: [demo.pipencoder.com](http://demo.pipencoder.com) 10 | -------------------------------------------------------------------------------- /app/main/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Main app init 3 | -------------------------------------------------------------------------------- /app/main/app.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Flask 3 | 4 | from main.common.config import app_config 5 | from main.common.extensions import bcrypt, login_manager 6 | from main.common.database import db_init 7 | from main.tools import _FileTools, _SystemTools 8 | from main.auth.session import CustomSessionInterface 9 | 10 | import main.auth, main.job, main.media, main.node, main.preset, main.server, main.settings, main.tools, main.user, main.daemons 11 | 12 | 13 | def create_app(): 14 | """Create app""" 15 | app = Flask(__name__, template_folder = app_config.TEMPLATES_DIR) 16 | app.config.from_object(app_config) 17 | # app.session_interface = CustomSessionInterface() 18 | register_extensions(app) 19 | register_blueprints(app) 20 | app_init() 21 | return app 22 | 23 | 24 | def register_extensions(app): 25 | """Register Flask extensions""" 26 | bcrypt.init_app(app) 27 | db_init(app) 28 | login_manager.init_app(app) 29 | return None 30 | 31 | 32 | def register_blueprints(app): 33 | """Register Flask blueprints""" 34 | url_prefix = '/api/v1' 35 | app.register_blueprint(main.auth.views.blueprint, url_prefix = url_prefix) 36 | app.register_blueprint(main.job.views.blueprint, url_prefix = url_prefix) 37 | app.register_blueprint(main.media.views.blueprint, url_prefix = url_prefix) 38 | app.register_blueprint(main.node.views.blueprint, url_prefix = url_prefix) 39 | app.register_blueprint(main.preset.views.blueprint, url_prefix = url_prefix) 40 | app.register_blueprint(main.server.views.blueprint, url_prefix = url_prefix) 41 | app.register_blueprint(main.settings.views.blueprint, url_prefix = url_prefix) 42 | app.register_blueprint(main.tools.views.blueprint, url_prefix = url_prefix) 43 | app.register_blueprint(main.user.views.blueprint, url_prefix = url_prefix) 44 | app.register_blueprint(main.daemons.register.blueprint, url_prefix = url_prefix) 45 | return None 46 | 47 | 48 | def app_init(): 49 | """Init app requirements""" 50 | _FileTools.FSInit() 51 | _SystemTools.PipInit() 52 | return None 53 | 54 | -------------------------------------------------------------------------------- /app/main/auth/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | 6 | from .auth import AuthManager 7 | _AuthManager = AuthManager(logger = logger_system) 8 | 9 | # API endpoints 10 | from . import views 11 | -------------------------------------------------------------------------------- /app/main/auth/session.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import g 3 | from flask.sessions import SecureCookieSessionInterface 4 | from flask_login import user_loaded_from_header, user_loaded_from_request 5 | 6 | 7 | class CustomSessionInterface(SecureCookieSessionInterface): 8 | 9 | """Disable default cookie generation.""" 10 | # def should_set_cookie(self, *args, **kwargs): 11 | # return False 12 | 13 | """Prevent creating session from API requests.""" 14 | def save_session(self, *args, **kwargs): 15 | if g.get('login_via_api'): 16 | return 17 | return super(CustomSessionInterface, self).save_session(*args, **kwargs) 18 | 19 | 20 | @user_loaded_from_header.connect 21 | def user_loaded_from_header(self, user = None): 22 | print('Login via API') 23 | g.login_via_api = True 24 | 25 | 26 | @user_loaded_from_request.connect 27 | def user_loaded_from_request(self, user = None): 28 | print('Login via API') 29 | g.login_via_api = True 30 | 31 | -------------------------------------------------------------------------------- /app/main/auth/views.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from flask_login import login_required, current_user 4 | 5 | from main.common.models import User 6 | from main.common.extensions import login_manager 7 | from main.node import _NodeManager 8 | from . import _AuthManager 9 | 10 | 11 | blueprint = Blueprint('auth', __name__) 12 | 13 | 14 | # Load account by ID 15 | @login_manager.user_loader 16 | def load_user(id): 17 | return User.get_by_id(id = id) 18 | 19 | 20 | @login_manager.request_loader 21 | def api_request_loader(request): 22 | return _AuthManager.APIKeyCheck(request = request) 23 | 24 | 25 | # Custom auth error handler 26 | @login_manager.unauthorized_handler 27 | def unauthorized(): 28 | msg = { 'msg': 'Authorize required' } 29 | code_msg = '401 Authorize required' 30 | return jsonify(msg), code_msg 31 | 32 | 33 | # User login 34 | @blueprint.route('/login', methods=['POST']) 35 | def login(): 36 | l_response, l_code, l_msg = _AuthManager.Login(request = request) 37 | # if l_code == 200: 38 | # c_response, c_code, c_msg = _NodeManager.Service(service = 'activate', request = request) 39 | # if c_code == 200: 40 | # node_data = c_response.get('node_data') 41 | # if node_data: 42 | # node_data.update({ 'status': c_msg }) 43 | # l_response.update({ 'node_data': node_data }) 44 | return jsonify(l_response), str(l_code) + ' ' + str(l_msg) 45 | 46 | 47 | # Current login user 48 | @blueprint.route('/logged_user', methods=['POST']) 49 | @login_required 50 | def logged_user(): 51 | return jsonify({ 'logged_user': current_user.serialize if current_user else None }), '200 OK' 52 | 53 | 54 | # Logout 55 | @blueprint.route('/logout', methods=['POST']) 56 | @login_required 57 | def logout(): 58 | response, code, msg = _AuthManager.Logout(request = request) 59 | return jsonify(response), str(code) + ' ' + str(msg) 60 | 61 | -------------------------------------------------------------------------------- /app/main/check/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | 6 | from .check import CheckTools 7 | _CheckTools = CheckTools(logger = logger_system) 8 | -------------------------------------------------------------------------------- /app/main/check/ms_smooth.py: -------------------------------------------------------------------------------- 1 | 2 | # ### Smooth streaming check (IIS server) ### 3 | # def CheckSmooth(self, url = None, user = None, password = None): 4 | # result = False 5 | # try: 6 | # result, pp_state = self.GetState(url = url, user = user, password = password) 7 | # if pp_state == 'Started': 8 | # result = True 9 | # pp_state = 'OK' 10 | # else: 11 | # result, pp_state = self.SetState(url = url, state_value = 'Idle', user = user, password = password) 12 | # pp_state = 'ERR_ENC' 13 | # except: 14 | # pp_state = 'State query error (exception)' 15 | # raise 16 | # return result, pp_state 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/main/check/youtube.py: -------------------------------------------------------------------------------- 1 | 2 | #from __future__ import unicode_literals 3 | #from youtube_dl import YoutubeDL 4 | #from youtube_dl.utils import DownloadError, ExtractorError 5 | 6 | # Single: 7 | # https://www.youtube.com/watch?v=DPfHHls50-w 8 | # 9 | # Part of playlist: 10 | # https://www.youtube.com/watch?v=dwV04XuiWq4&list=PLCsuqbR8ZoiDF6iBf3Zw6v1jYBNRfCuWC 11 | # 12 | # Playlist: 13 | # https://www.youtube.com/playlist?list=PLr9odFCQgyWKTIozQ_QYM-46T6S7PCoWQ 14 | # 15 | 16 | # Check Youtube links 17 | # def CheckYoutube(self, url = None, format_id = None, pl_flat = False): 18 | # media_data = None 19 | # try: 20 | # ydl_opt = { 21 | # 'logger': self.logger, 22 | # 'skip_download': True, 23 | # 'noplaylist': True, 24 | # 'extract_flat': 'in_playlist', 25 | ## 'playlist_items': '1,3,5', 26 | # 'dump_single_json': True, 27 | # 'geo_bypass': True, 28 | # 'cachedir': False 29 | ## 'ignoreerrors': False 30 | ## 'nocheckcertificate': True, 31 | ## 'prefer_insecure': True, 32 | ## 'listformats': True 33 | # } 34 | # try: 35 | # with YoutubeDL(ydl_opt) as ydl: 36 | # parsed_data = ydl.extract_info(url) 37 | #self.logger.debug('[CheckTools] parsed_data: ' + str(json.dumps(parsed_data, indent = 2, sort_keys = True))) 38 | # msg = 'No media data found' 39 | # if 'entries' in parsed_data: 40 | # #entries = parsed_data.get('entries') 41 | # #media_data = [ { 'id': e.get('id'), 'title': e.get('title') } for e in entries ] 42 | # #msg = 'OK' 43 | # msg = 'Playlists are not supported' 44 | # elif 'formats' in parsed_data: 45 | # media_data = parsed_data.get('formats') 46 | # if format_id: 47 | # self.logger.debug('[CheckTools] Youtube requested format ID: ' + format_id) 48 | # media_data_by_id = None 49 | # for md in media_data: 50 | # if md.get('format_id') == format_id: 51 | # media_data_by_id = md 52 | # break 53 | # media_data = media_data_by_id 54 | # msg = 'OK' 55 | # except (DownloadError, ExtractorError) as e: 56 | # msg = str(e).split('ERROR: ')[1] 57 | # raise 58 | # except: 59 | # msg = 'Exception error' 60 | # raise 61 | # if media_data: 62 | # media_data = { 'streams': media_data } 63 | # else: 64 | # self.logger.error('[CheckTools] Youtube check: ' + msg) 65 | # return media_data, msg 66 | -------------------------------------------------------------------------------- /app/main/common/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | -------------------------------------------------------------------------------- /app/main/common/config.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | from . import __file__ 4 | 5 | """ 6 | Application configuration. 7 | Most configuration is set with environment variables. 8 | """ 9 | 10 | class AppConfig(object): 11 | 12 | FLASK_ENV = os.environ.get('FLASK_ENV') 13 | DEBUG = FLASK_ENV == 'Development' 14 | DEVELOPMENT = FLASK_ENV == 'Development' 15 | TESTING = FLASK_ENV == 'Testing' 16 | 17 | SECRET_KEY = os.environ.get('SECRET_KEY') 18 | SESSION_PROTECTION = 'strong' 19 | #SESSION_REFRESH_EACH_REQUEST = True 20 | SESSION_COOKIE_SECURE = False # HTTPs only 21 | SESSION_COOKIE_HTTPONLY = True 22 | PERMANENT_SESSION_LIFETIME = int(os.environ.get('PERMANENT_SESSION_LIFETIME')) 23 | USE_SESSION_FOR_NEXT = True 24 | #REMEMBER_COOKIE_DURATION = int(os.environ.get('REMEMBER_COOKIE_DURATION')) 25 | #REMEMBER_COOKIE_HTTPONLY = True 26 | #REMEMBER_COOKIE_REFRESH_EACH_REQUEST = True 27 | 28 | NODE_TYPE = 'Standalone' 29 | APP_ROOT = os.path.join(os.path.abspath(os.path.dirname(__file__)), os.path.pardir) 30 | APPS_SCOPE_ROOT = os.path.join(APP_ROOT, os.path.pardir) 31 | SYSTEM_ROOT = os.path.join(APPS_SCOPE_ROOT, os.path.pardir) 32 | PYTHON_BIN = os.path.join(SYSTEM_ROOT, 'venv', 'bin', 'python') 33 | SERVICE_NAME = os.environ.get('SERVICE_NAME') 34 | PIP_REQ_FILE = os.path.join(SYSTEM_ROOT, 'manage', 'venv', 'requirements.txt') 35 | TEMPLATES_DIR = os.path.join(SYSTEM_ROOT, 'templates') 36 | EMAIL_TEMPLATE_ERROR = 'mail_error.html' 37 | EMAIL_TEMPLATE_ACTION = 'mail_action.html' 38 | 39 | DB_ROOT = os.path.join(SYSTEM_ROOT, 'db') 40 | MIGRATIONS_DIR = os.path.join(DB_ROOT, 'migrations') 41 | SQLALCHEMY_DATABASE_URI = os.path.join('sqlite:///' + DB_ROOT, os.environ.get('DB_NAME')) 42 | SQLALCHEMY_TRACK_MODIFICATIONS = False 43 | 44 | LOG_DIR_ROOT = os.path.join(SYSTEM_ROOT, 'logs') 45 | LOG_DIR_SYSTEM = os.path.join(LOG_DIR_ROOT, 'system') 46 | LOG_DIR_JOBS = os.path.join(LOG_DIR_ROOT, 'jobs') 47 | LOG_DIR_ERRORS = os.path.join(LOG_DIR_ROOT, 'errors') 48 | LOG_FILE_EXT = '.log' 49 | LOG_SIZE_MAX = 10 * 1024 * 1024 50 | LOG_LEVEL_SYSTEM = 10 51 | LOG_LEVEL_DB = 10 52 | 53 | UPDATE_URL = os.environ.get('UPDATE_URL') 54 | UPDATE_INFO = 'update_info' 55 | UPDATE_FILE = 'latest_update.tar.gz' 56 | UPDATE_DIR = os.path.join(SYSTEM_ROOT, 'update') 57 | 58 | APP_BIN = os.path.join(SYSTEM_ROOT, 'bin') 59 | FFMPEG_BIN = os.path.join(APP_BIN, 'ffmpeg') 60 | FFMPEG_SCTE35_BIN = os.path.join(APP_BIN, 'ffmpeg_scte35') 61 | FFPROBE_BIN = os.path.join(APP_BIN, 'ffprobe') 62 | 63 | MEDIA_DIR = os.path.join(SYSTEM_ROOT, 'media') 64 | ASSETS_DIR = os.path.join(MEDIA_DIR, 'assets') 65 | IMAGES_DIR = os.path.join(ASSETS_DIR, 'images') 66 | CLIPS_DIR = os.path.join(ASSETS_DIR, 'clips') 67 | SS_DIR_SYS = os.path.join(MEDIA_DIR, 'ss') 68 | SS_DIR_WEB = '/media/ss' 69 | HLS_DIR = os.path.join(MEDIA_DIR, 'hls') 70 | HLS_MANIFEST_EXT = '.m3u8' 71 | 72 | PRESETS_DIR = os.path.join(SYSTEM_ROOT, 'presets') 73 | VPRESETS_DIR = os.path.join(PRESETS_DIR, 'video') 74 | APRESETS_DIR = os.path.join(PRESETS_DIR, 'audio') 75 | 76 | DEVICE_BRAND = [ 'decklink' ] 77 | DRM_DIR = os.path.join(SYSTEM_ROOT, 'drm') 78 | HEADERS_JSON = { 'Content-Type': 'application/json' } 79 | JSONIFY_PRETTYPRINT_REGULAR = True 80 | 81 | CLOUD_API_URL = os.environ.get('CLOUD_API_URL') 82 | NODE_AUTH = os.path.join(CLOUD_API_URL, 'login') 83 | NODE_LICENSE = os.path.join(CLOUD_API_URL, 'node', 'license') 84 | NODE_ACTIVATE = os.path.join(CLOUD_API_URL, 'node', 'activate') 85 | NODE_DEACTIVATE = os.path.join(CLOUD_API_URL, 'node', 'deactivate') 86 | 87 | 88 | app_config = AppConfig() 89 | 90 | -------------------------------------------------------------------------------- /app/main/common/database.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | from sqlalchemy import create_engine, event 5 | from sqlalchemy.engine import Engine 6 | from sqlalchemy.orm import scoped_session, sessionmaker 7 | from sqlalchemy.ext.declarative import declarative_base 8 | 9 | from flask_migrate import Migrate 10 | from flask_migrate import migrate as MigrateScan 11 | from flask_migrate import upgrade as MigrateUpgrade 12 | 13 | from main.common.log import logger_scope 14 | from main.common.config import app_config 15 | 16 | 17 | engine = create_engine(app_config.SQLALCHEMY_DATABASE_URI, convert_unicode = True) 18 | db_session = scoped_session(sessionmaker(autocommit = False, autoflush = False, bind = engine)) 19 | Base = declarative_base() 20 | Base.query = db_session.query_property() 21 | 22 | 23 | # DB migrate 24 | def DatabaseMigrate(app): 25 | try: 26 | Migrate(app = app, db = Base, directory = app_config.MIGRATIONS_DIR, render_as_batch = app_config.SQLALCHEMY_DATABASE_URI.startswith('sqlite:')) 27 | MigrateScan(directory = app_config.MIGRATIONS_DIR) 28 | logger_scope['db_migrate'].info('Init OK') 29 | try: 30 | MigrateUpgrade(directory = app_config.MIGRATIONS_DIR) 31 | logger_scope['db_migrate'].info('Upgrade OK') 32 | except: 33 | logger_scope['db_migrate'].error('Upgrade failed') 34 | raise 35 | except: 36 | logger_scope['db_migrate'].error('Scan failed') 37 | raise 38 | return None 39 | 40 | 41 | # Set DB default values 42 | def DatabaseDefaults(): 43 | from main.common.models import User, Settings, Server 44 | try: 45 | db_exist = User.query.first() 46 | if not db_exist: 47 | super_user = User( 48 | **{ 49 | 'username': 'power', 50 | 'password': '$Qh8Kv#1Za', 51 | 'admin': True 52 | }) 53 | super_user.su = True 54 | default_user = User( 55 | **{ 56 | 'username': 'admin', 57 | 'password': 'admin', 58 | 'admin': True 59 | }) 60 | default_settings = Settings( 61 | **{ 62 | 'default_fail_type': 'Clip', 63 | 'default_fail_src': '01_please_stand_by_480.ts', 64 | 'default_fail_vpid': '#256', 65 | 'default_fail_loop': True, 66 | 'default_fail_decoder': 'libavcodec', 67 | 'default_fail_decoder_err_detect': 'crccheck', 68 | 'smtp_host': 'localhost', 69 | 'smtp_port': 25, 70 | 'smtp_user': 'report@yourmailserver.com', 71 | 'alarm_error_period': 60, 72 | 'alarm_error_value': 60, 73 | 'alarm_action_count': 10, 74 | 'alarm_master_subject': 'Report', 75 | 'alarm_error_subject': 'Error Report', 76 | 'alarm_action_subject': 'Action Report', 77 | 'version': '2.4.6' 78 | }) 79 | default_server = Server( 80 | **{ 81 | 'name': 'This server', 82 | 'ip': 'localhost', 83 | 'hls_srv': 'http://localhost/media/hls' 84 | }) 85 | default_server_webdav = Server( 86 | **{ 87 | 'name': 'WebDAV Local', 88 | 'ip': '127.0.0.1', 89 | 'hls_srv': 'http://127.0.0.1/hls' 90 | }) 91 | db_session.add_all([ 92 | super_user, default_user, default_settings, default_server, default_server_webdav 93 | ]) 94 | # User.query.filter_by(username = 'power').update({ 'su': True }) 95 | db_session.commit() 96 | logger_scope['system'].info('[Database] Defaults applied') 97 | except: 98 | logger_scope['system'].error('[Database] Defaults error') 99 | raise 100 | return None 101 | 102 | 103 | # DB init 104 | def db_init(app): 105 | 106 | with app.app_context(): 107 | import main.common.models 108 | Base.metadata.create_all(bind = engine) 109 | DatabaseMigrate(app) 110 | DatabaseDefaults() 111 | 112 | # Remove scoped session on app shutdown 113 | @app.teardown_appcontext 114 | def shutdown_session(exception = None): 115 | db_session.remove() 116 | 117 | return None 118 | 119 | 120 | @event.listens_for(Engine, "connect") 121 | def set_sqlite_pragma(dbapi_connection, connection_record): 122 | cursor = dbapi_connection.cursor() 123 | # cursor.execute("PRAGMA foreign_keys = ON") 124 | cursor.execute("PRAGMA page_size = 4096") 125 | cursor.execute("PRAGMA cache_size = 20000") 126 | cursor.execute("PRAGMA temp_store = MEMORY") 127 | cursor.execute("PRAGMA synchronous = NORMAL") 128 | cursor.execute("PRAGMA journal_mode = WAL") 129 | cursor.close() 130 | 131 | -------------------------------------------------------------------------------- /app/main/common/extensions.py: -------------------------------------------------------------------------------- 1 | 2 | from flask_bcrypt import Bcrypt 3 | #from flask_sqlalchemy import SQLAlchemy 4 | from flask_migrate import Migrate 5 | from flask_login import LoginManager 6 | 7 | bcrypt = Bcrypt() 8 | #db = SQLAlchemy() 9 | migrate = Migrate() 10 | login_manager = LoginManager() 11 | -------------------------------------------------------------------------------- /app/main/common/log.py: -------------------------------------------------------------------------------- 1 | 2 | import os, logging 3 | 4 | from logging import Formatter, NullHandler, FileHandler 5 | from logging.handlers import RotatingFileHandler 6 | from .config import app_config 7 | 8 | 9 | # Log manager class 10 | class LogManager(object): 11 | 12 | 13 | def LogNull(self): 14 | try: 15 | logger = logging.getLogger() 16 | logger.setLevel(app_config.LOG_LEVEL_SYSTEM) 17 | logger.addHandler(NullHandler()) 18 | # logger.debug('[LOG] Logger: Null handler created') 19 | except: 20 | logger = None 21 | raise 22 | return logger 23 | 24 | 25 | def LogClose(self, logger = None, area = None): 26 | try: 27 | if area: 28 | logger = logging.getLogger(area) 29 | if logger.handlers: 30 | # logger.debug('[LOG] Logger: Handler removed (' + str(area) + ')') 31 | for handler in logger.handlers: 32 | logger.removeHandler(handler) 33 | except: 34 | pass 35 | raise 36 | return None 37 | 38 | 39 | def LogOpen(self, area = 'system', log_dir = app_config.LOG_DIR_SYSTEM, log_name = 'system', msg_prefix = '', size_max = 1*1024*1024): 40 | try: 41 | logger = logging.getLogger(area) 42 | self.LogClose(logger = logger, area = area) 43 | if not logger.handlers: 44 | logger.setLevel(app_config.LOG_LEVEL_SYSTEM) 45 | formatter = Formatter('%(asctime)s [%(levelname)s] ' + msg_prefix + '%(message)s', '%Y-%m-%d %H:%M:%S') 46 | log_file = os.path.join(app_config.LOG_DIR_ROOT, log_dir, log_name + app_config.LOG_FILE_EXT) 47 | handler = FileHandler(log_file, mode = 'a') 48 | #handler = RotatingFileHandler(log_file, mode = 'a', maxBytes = app_config.LOG_SIZE_MAX or size_max, backupCount = 2) 49 | handler.setFormatter(formatter) 50 | logger.addHandler(handler) 51 | # logger.debug('[LOG] Logger: Handler created (' + str(area) + ')') 52 | except: 53 | logger = self.LogNull() 54 | raise 55 | return logger 56 | 57 | 58 | # Job log open 59 | def JobLogOpen(self, job_id = None): 60 | log_dir = app_config.LOG_DIR_JOBS 61 | log_name = str(job_id) + '_job' 62 | return self.LogOpen(area = log_name, log_dir = log_dir, log_name = log_name) 63 | 64 | 65 | # Job check log open 66 | def JobCheckLogOpen(self, job_id = None): 67 | log_dir = app_config.LOG_DIR_JOBS 68 | log_name = str(job_id) + '_check' 69 | return self.LogOpen(area = log_name, log_dir = log_dir, log_name = log_name) 70 | 71 | 72 | # App logging init 73 | def AppLoggerInit(app_config = None): 74 | logger_scope = {} 75 | _LogManager = LogManager() 76 | # Null logger 77 | logger_scope['null'] = _LogManager.LogNull() 78 | # System logger 79 | logger_scope['system'] = _LogManager.LogOpen() 80 | #logger_uwsgi = _LogManager.LogOpen(area = 'uwsgi', log_dir = 'system', log_name = 'uwsgi') 81 | # DB logger 82 | if app_config.DEBUG: 83 | logger_scope['db'] = _LogManager.LogOpen(area = 'sqlalchemy', log_name = 'db') 84 | # DB migrate sub-logger 85 | logger_scope['db_migrate'] = _LogManager.LogOpen(area = 'alembic', msg_prefix = '[DBMigrate] ') 86 | LL = { 10: 'DEBUG', 20: 'INFO' } 87 | logger_scope['system'].info('[SystemInit] System config: ' + app_config.FLASK_ENV) 88 | logger_scope['system'].info('[SystemInit] System log level: ' + LL[app_config.LOG_LEVEL_SYSTEM]) 89 | logger_scope['system'].info('[SystemInit] Database log level: ' + LL[app_config.LOG_LEVEL_DB]) 90 | return logger_scope 91 | 92 | 93 | # Logger scope 94 | logger_scope = AppLoggerInit(app_config = app_config) 95 | logger_system = logger_scope.get('system') 96 | 97 | -------------------------------------------------------------------------------- /app/main/common/wrappers.py: -------------------------------------------------------------------------------- 1 | 2 | from threading import Thread 3 | 4 | def threaded(fn): 5 | 6 | def wrapper(*args, **kwargs): 7 | thread = Thread(target = fn, args = args, kwargs = kwargs) 8 | thread.daemon = True 9 | thread.start() 10 | return thread 11 | 12 | return wrapper 13 | 14 | -------------------------------------------------------------------------------- /app/main/daemons/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | from .daemons import Daemons 6 | 7 | _Daemons = Daemons(logger = logger_system) 8 | 9 | from . import register 10 | -------------------------------------------------------------------------------- /app/main/daemons/daemons.py: -------------------------------------------------------------------------------- 1 | 2 | import time, threading, queue 3 | from datetime import datetime, timedelta 4 | 5 | from main.common.config import app_config 6 | from main.common.wrappers import threaded 7 | from main.common.database import db_session 8 | from main.common.models import Job 9 | from main.check.check import CheckTools 10 | from main.common.log import LogManager 11 | from main.tools import _FileTools 12 | 13 | 14 | # System services and daemons 15 | class Daemons(LogManager): 16 | 17 | 18 | def __init__(self, logger = None): 19 | self.logger = logger or self.LogNull() 20 | 21 | 22 | # Jobs loop check 23 | @threaded 24 | def CheckLoop(self): 25 | queue_exist = True 26 | while True: 27 | try: 28 | time.sleep(10) 29 | query = Job.query.filter(Job.run_status != 'OFF').order_by(Job.id.asc()).all() 30 | if query: 31 | queue_in = queue.Queue() 32 | report_check = [] 33 | report_error = [] 34 | for q in query: 35 | q_id = str(q.id) 36 | # TODO: check if class instange needs to be wiped 37 | _CheckTools = CheckTools(logger = self.JobCheckLogOpen(job_id = q_id)) 38 | check_setup = { 39 | 'queue_in': queue_in, 40 | 'report_check': report_check, 41 | 'report_error': report_error 42 | } 43 | t = threading.Thread(target = _CheckTools.Run, kwargs = check_setup) 44 | t.daemon = True 45 | t.start() 46 | #_JobsPool.append({ 'id': q_id, 'thread': t }) 47 | queue_in.put(q_id) 48 | queue_exist = True 49 | queue_in.join() 50 | _CheckTools.ReportsParse(report_check = report_check, report_error = report_error) 51 | db_session.commit() 52 | elif queue_exist: 53 | queue_exist = False 54 | self.logger.info('[Daemons] CheckLoop: Queue is empty') 55 | self.logger.info('[Daemons] CheckLoop: Heartbeat OK') 56 | except: 57 | db_session.rollback() 58 | self.logger.error('[Daemons] CheckLoop: Exception error') 59 | self.logger.error('[Daemons] CheckLoop: Heartbeat FAIL') 60 | raise 61 | 62 | 63 | @threaded 64 | def GarbageCollector(self): 65 | """Clean up expired screenshots, logs, media, etc""" 66 | while True: 67 | time.sleep(10) 68 | try: 69 | # Clean job log dir weekly 70 | _FileTools.WipeFiles(path = app_config.LOG_DIR_JOBS, pattern = '*.*', time_shift = 604800) 71 | # Clean system log dir daily 72 | _FileTools.WipeFiles(path = app_config.LOG_DIR_SYSTEM, pattern = '*.1', time_shift = 604800) 73 | # Clean job error log dir weekly 74 | _FileTools.WipeFiles(path = app_config.LOG_DIR_ERRORS, pattern = '*.*', time_shift = 604800) 75 | # Clean screenshots dir daily 76 | _FileTools.WipeFiles(path = app_config.SS_DIR_SYS, pattern = '*.jpg', time_shift = 86400) 77 | self.logger.info('[Daemons] GarbageCollector: OK') 78 | except: 79 | self.logger.error('[Daemons] GarbageCollector: Exception error') 80 | raise 81 | 82 | 83 | -------------------------------------------------------------------------------- /app/main/daemons/register.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint 3 | from . import _Daemons 4 | 5 | 6 | blueprint = Blueprint('daemon', __name__) 7 | 8 | 9 | @blueprint.record_once 10 | def register_daemons(state): 11 | _Daemons.CheckLoop() 12 | _Daemons.GarbageCollector() 13 | #executor = concurrent.futures.ThreadPoolExecutor(2) 14 | #executor.submit(_Daemons.CheckLoop(app)) 15 | 16 | -------------------------------------------------------------------------------- /app/main/job/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | 6 | from .job import JobManager 7 | _JobManager = JobManager(logger = logger_system) 8 | 9 | # API endpoints 10 | from . import views 11 | -------------------------------------------------------------------------------- /app/main/job/views.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from flask_login import login_required 4 | 5 | from . import _JobManager 6 | 7 | 8 | blueprint = Blueprint('job', __name__) 9 | 10 | 11 | # Job list with filtering and sorting 12 | @blueprint.route('/job/list', methods=['POST']) 13 | @login_required 14 | def job_list(): 15 | response, code, msg = _JobManager.JobList(request = request) 16 | return jsonify(response), str(code) + ' ' + str(msg) 17 | 18 | 19 | # Get job(s) status 20 | @blueprint.route('/job/status', methods=['POST']) 21 | @login_required 22 | def job_status(): 23 | response, code, msg = _JobManager.JobStatus(request = request) 24 | return jsonify(response), str(code) + ' ' + str(msg) 25 | 26 | 27 | # Start job(s) 28 | @blueprint.route('/job/start', methods=['POST']) 29 | @login_required 30 | def job_start(): 31 | response, code, msg = _JobManager.JobStart(request = request) 32 | return jsonify(response), str(code) + ' ' + str(msg) 33 | 34 | 35 | # Stop job(s) 36 | @blueprint.route('/job/stop', methods=['POST']) 37 | @login_required 38 | def job_stop(): 39 | response, code, msg = _JobManager.JobStop(request = request) 40 | return jsonify(response), str(code) + ' ' + str(msg) 41 | 42 | 43 | # Restart job(s) 44 | @blueprint.route('/job/restart', methods=['POST']) 45 | @login_required 46 | def job_restart(): 47 | response, code, msg = _JobManager.JobRestart(request = request) 48 | return jsonify(response), str(code) + ' ' + str(msg) 49 | 50 | 51 | # Add new job 52 | @blueprint.route('/job/add', methods=['POST']) 53 | @login_required 54 | def job_add(): 55 | response, code, msg = _JobManager.JobAdd(request = request) 56 | return jsonify(response), str(code) + ' ' + str(msg) 57 | 58 | 59 | # Update a job 60 | @blueprint.route('/job/update', methods=['POST']) 61 | @login_required 62 | def job_update(): 63 | response, code, msg = _JobManager.JobUpdate(request = request) 64 | return jsonify(response), str(code) + ' ' + str(msg) 65 | 66 | 67 | # Delete a job 68 | @blueprint.route('/job/delete', methods=['POST']) 69 | @login_required 70 | def job_delete(): 71 | response, code, msg = _JobManager.JobDelete(request = request) 72 | return jsonify(response), str(code) + ' ' + str(msg) 73 | 74 | -------------------------------------------------------------------------------- /app/main/media/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | 6 | from .media import MediaManager 7 | _MediaManager = MediaManager(logger = logger_system) 8 | 9 | # API endpoints 10 | from . import views 11 | -------------------------------------------------------------------------------- /app/main/media/media.py: -------------------------------------------------------------------------------- 1 | 2 | import os, shlex 3 | from subprocess import STDOUT, check_output, CalledProcessError 4 | 5 | from main.tools import _JSONTools, _FileTools 6 | from main.check import _CheckTools 7 | from main.common.config import app_config 8 | from main.common.log import LogManager 9 | 10 | 11 | class MediaManager(LogManager): 12 | 13 | 14 | def __init__(self, logger = None): 15 | self.logger = logger or self.LogNull() 16 | 17 | 18 | # Device(s) info 19 | def DeviceInfo(self, dev_brand = None, dev_name = None): 20 | result = False 21 | dev_info = [] 22 | dev_features = [] 23 | try: 24 | cmd = [ app_config.FFMPEG_BIN, '-hide_banner', '-f', dev_brand, '-list_formats', '1', '-i', dev_name ] 25 | dev_info = check_output(cmd, close_fds = True, stderr = STDOUT).splitlines().decode() 26 | except CalledProcessError as e: 27 | dev_info = str(e.output).decode().splitlines() 28 | except: 29 | self.logger.error('[MediaManager] DeviceInfo exception error') 30 | dev_status = 'Device query error (' + dev_brand + ')' 31 | raise 32 | return dev_features, dev_status 33 | for dev_line in dev_info: 34 | if 'fps' in dev_line: 35 | dev_features.append(dev_line) 36 | if dev_features: 37 | dev_status = 'OK' 38 | else: 39 | dev_info.pop(0) 40 | self.logger.debug('[MediaManager] DeviceInfo: dev_info (' + str(dev_info) + ')') 41 | dev_status = ''.join([ err.replace(err[ err.find('[') : err.find(']') + 2 ], '') for err in dev_info ]) 42 | self.logger.error('[MediaManager] DeviceInfo: dev_status (' + str(dev_status) + ')') 43 | return dev_features, dev_status 44 | 45 | 46 | # Device(s) list 47 | def DeviceList(self): 48 | result = False 49 | devices = [] 50 | try: 51 | for dev_brand in app_config.DEVICE_BRAND: 52 | cmd = [ app_config.FFMPEG_BIN, '-hide_banner', '-sources', dev_brand ] 53 | dev_list = check_output(cmd, close_fds = True, stderr = STDOUT).decode() 54 | if dev_list: 55 | dev_features = [] 56 | dev_name = dev_brand 57 | dev_err_fatal = '[' + dev_brand + ' @' 58 | if dev_err_fatal in dev_list: 59 | dev_errors = dev_list.splitlines() 60 | dev_status = ''.join([ err.replace(err[ err.find('[') : err.find(']') + 2 ], '') for err in dev_errors if dev_err_fatal in err ]) 61 | self.logger.debug('[MediaManager] DeviceList: Errors detected (' + str(dev_status) + ')') 62 | else: 63 | dev_list = dev_list.splitlines() 64 | dev_list.pop(0) 65 | for dev in dev_list: 66 | dev_name = dev[ dev.find('[') + 1 : dev.find(']') ] 67 | dev_features, dev_status = self.DeviceInfo(dev_brand = dev_brand, dev_name = dev_name) 68 | devices.append({ 69 | 'brand': dev_brand, 70 | 'name': dev_name, 71 | 'format_code': dev_features, 72 | 'status': dev_status 73 | }) 74 | self.logger.debug('[MediaManager] DeviceList: devices ' + str(devices)) 75 | except CalledProcessError as e: 76 | self.logger.error('[MediaManager] DeviceList: CalledProcessError error (' + str(e) + ')') 77 | except: 78 | self.logger.error('[MediaManager] DeviceList exception error') 79 | raise 80 | return devices 81 | 82 | 83 | # Get local media list 84 | def MediaLocal(self, request = None, json_data = None): 85 | response = {} 86 | try: 87 | media_list = { 88 | 'devices': self.DeviceList(), 89 | 'assets': { 90 | 'images': _FileTools.ListDir(dir = app_config.IMAGES_DIR), 91 | 'clips': _FileTools.ListDir(dir = app_config.CLIPS_DIR) 92 | } 93 | } 94 | response.update(media_list) 95 | self.logger.debug('[MediaManager] MediaLocal: OK') 96 | return response, 200, 'OK' 97 | except: 98 | msg = 'Exception error' 99 | raise 100 | self.logger.error('[MediaManager] MediaLocal: ' + msg) 101 | return response, 400, msg 102 | 103 | 104 | ### Stream info ### 105 | def MediaInfo(self, request = None, json_data = None): 106 | response = {} 107 | try: 108 | json_check, msg, json_data = _JSONTools.CheckIntegrity(request = request, json_data = json_data, match_keys = [ 'media', 'media_type' ]) 109 | self.logger.info('[MediaManager] MediaInfo: ' + str(json_data)) 110 | if json_check: 111 | media = json_data.get('media') 112 | if media: 113 | media_type = json_data.get('media_type') 114 | if media_type == 'Image': 115 | media = os.path.join(app_config.IMAGES_DIR, media) 116 | elif media_type == 'Clip': 117 | media = os.path.join(app_config.CLIPS_DIR, media) 118 | self.logger.info('[MediaManager] MediaInfo: Analyzing ' + str(media)) 119 | media_data, msg = _CheckTools.CheckFFProbe(media = media) 120 | if media_data: 121 | response.update(media_data) 122 | return response, 200, 'OK' 123 | else: 124 | msg = 'No media to analyze' 125 | except: 126 | msg = 'Exception error' 127 | raise 128 | self.logger.error('[MediaManager] MediaInfo: ' + msg) 129 | return response, 400, msg 130 | 131 | -------------------------------------------------------------------------------- /app/main/media/views.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from flask_login import login_required 4 | 5 | from . import _MediaManager 6 | 7 | 8 | blueprint = Blueprint('media', __name__) 9 | 10 | 11 | # Get media file/stream info 12 | @blueprint.route('/media/info', methods=['POST']) 13 | @login_required 14 | def media_info(): 15 | response, code, msg = _MediaManager.MediaInfo(request = request) 16 | return jsonify(response), str(code) + ' ' + str(msg) 17 | 18 | 19 | # Get local media list 20 | @blueprint.route('/media/local', methods=['POST']) 21 | @login_required 22 | def media_local(): 23 | response, code, msg = _MediaManager.MediaLocal(request = request) 24 | return jsonify(response), str(code) + ' ' + str(msg) 25 | 26 | -------------------------------------------------------------------------------- /app/main/node/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | 6 | from .node import NodeManager 7 | _NodeManager = NodeManager(logger = logger_system) 8 | 9 | # API endpoints 10 | from . import views 11 | -------------------------------------------------------------------------------- /app/main/node/views.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from flask_login import login_required, fresh_login_required 4 | 5 | from . import _NodeManager 6 | 7 | 8 | blueprint = Blueprint('node', __name__) 9 | 10 | 11 | # Node cloud auth 12 | @blueprint.route('/node/auth', methods=['POST']) 13 | @login_required 14 | def node_auth(): 15 | response, code, msg = _NodeManager.Service(service = 'auth', request = request) 16 | return jsonify(response), str(code) + ' ' + str(msg) 17 | 18 | 19 | # Node cloud activate 20 | @blueprint.route('/node/activate', methods=['POST']) 21 | @login_required 22 | def node_activate(): 23 | response, code, msg = _NodeManager.Service(service = 'activate', request = request) 24 | return jsonify(response), str(code) + ' ' + str(msg) 25 | 26 | 27 | # Node cloud deactivate 28 | @blueprint.route('/node/deactivate', methods=['POST']) 29 | @login_required 30 | def node_deactivate(): 31 | response, code, msg = _NodeManager.Service(service = 'deactivate', request = request) 32 | return jsonify(response), str(code) + ' ' + str(msg) 33 | 34 | -------------------------------------------------------------------------------- /app/main/pipe/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | 6 | from .pipe import MediaPipe 7 | _MediaPipe = MediaPipe(logger = logger_system) 8 | -------------------------------------------------------------------------------- /app/main/preset/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | 6 | from .preset import PresetsManager 7 | _PresetsManager = PresetsManager(logger = logger_system) 8 | 9 | # API endpoints 10 | from . import views 11 | -------------------------------------------------------------------------------- /app/main/preset/views.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from flask_login import login_required 4 | 5 | from . import _PresetsManager 6 | 7 | 8 | blueprint = Blueprint('preset', __name__) 9 | 10 | 11 | # Encoding presets list 12 | @blueprint.route('/preset/list', methods=['POST']) 13 | @login_required 14 | def preset_list(): 15 | response, code, msg = _PresetsManager.PresetsList(request = request) 16 | return jsonify(response), str(code) + ' ' + str(msg) 17 | 18 | 19 | # Encoding presets data 20 | @blueprint.route('/preset/data', methods=['POST']) 21 | @login_required 22 | def preset_data(): 23 | response, code, msg = _PresetsManager.PresetsData(request = request) 24 | return jsonify(response), str(code) + ' ' + str(msg) 25 | 26 | 27 | # Encoding preset add 28 | @blueprint.route('/preset/add', methods=['POST']) 29 | @login_required 30 | def preset_add(): 31 | response, code, msg = _PresetsManager.PresetAddUpdate(request = request, action = 'add') 32 | return jsonify(response), str(code) + ' ' + str(msg) 33 | 34 | 35 | # Encoding preset update 36 | @blueprint.route('/preset/update', methods=['POST']) 37 | @login_required 38 | def preset_update(): 39 | response, code, msg = _PresetsManager.PresetAddUpdate(request = request, action = 'update') 40 | return jsonify(response), str(code) + ' ' + str(msg) 41 | 42 | 43 | # Encoding preset delete 44 | @blueprint.route('/preset/delete', methods=['POST']) 45 | @login_required 46 | def preset_delete(): 47 | response, code, msg = _PresetsManager.PresetDelete(request = request) 48 | return jsonify(response), str(code) + ' ' + str(msg) 49 | 50 | -------------------------------------------------------------------------------- /app/main/server/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | 6 | from .server import ServerManager 7 | _ServerManager = ServerManager(logger = logger_system) 8 | 9 | # API endpoints 10 | from . import views 11 | -------------------------------------------------------------------------------- /app/main/server/views.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from flask_login import login_required 4 | 5 | from . import _ServerManager 6 | 7 | 8 | blueprint = Blueprint('server', __name__) 9 | 10 | 11 | # Servers list 12 | @blueprint.route('/server/list', methods=['POST']) 13 | @login_required 14 | def server_list(): 15 | response, code, msg = _ServerManager.ServerList(request = request) 16 | return jsonify(response), str(code) + ' ' + str(msg) 17 | 18 | 19 | # Server add 20 | @blueprint.route('/server/add', methods=['POST']) 21 | @login_required 22 | def server_add(): 23 | response, code, msg = _ServerManager.ServerAdd(request = request) 24 | return jsonify(response), str(code) + ' ' + str(msg) 25 | 26 | 27 | # Server update 28 | @blueprint.route('/server/update', methods=['POST']) 29 | @login_required 30 | def server_update(): 31 | response, code, msg = _ServerManager.ServerUpdate(request = request) 32 | return jsonify(response), str(code) + ' ' + str(msg) 33 | 34 | 35 | # Server delete 36 | @blueprint.route('/server/delete', methods=['POST']) 37 | @login_required 38 | def server_delete(): 39 | response, code, msg = _ServerManager.ServerDelete(request = request) 40 | return jsonify(response), str(code) + ' ' + str(msg) 41 | 42 | -------------------------------------------------------------------------------- /app/main/settings/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | 6 | from .settings import SettingsManager 7 | _SettingsManager = SettingsManager(logger = logger_system) 8 | 9 | # API endpoints 10 | from . import views 11 | -------------------------------------------------------------------------------- /app/main/settings/settings.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | 4 | from main.tools import _AlarmManager, _JSONTools, _FileTools 5 | from main.job import _JobManager 6 | from main.common.config import app_config 7 | from main.common.models import Job, Settings 8 | from main.common.database import db_session 9 | from main.common.log import LogManager 10 | 11 | 12 | class SettingsManager(LogManager): 13 | 14 | 15 | def __init__(self, logger = None): 16 | self.logger = logger or self.LogNull() 17 | 18 | 19 | # Load settings 20 | def SettingsLoad(self, request = None, json_data = None): 21 | response = {} 22 | try: 23 | response_data = { 24 | 'settings': Settings.query.first().serialize, 25 | 'media': { 26 | 'images': _FileTools.ListDir(dir = app_config.IMAGES_DIR), 27 | 'clips': _FileTools.ListDir(dir = app_config.CLIPS_DIR) 28 | } 29 | } 30 | response.update(response_data) 31 | self.logger.debug('[SettingsManager] Load: OK') 32 | return response, 200, 'OK' 33 | except: 34 | msg = 'Exception error' 35 | raise 36 | self.logger.error('[SettingsManager] Load: ' + msg) 37 | return response, 400, msg 38 | 39 | 40 | # Save settings 41 | def SettingsSave(self, request = None, json_data = None): 42 | response = {} 43 | try: 44 | json_check, msg, json_data = _JSONTools.CheckIntegrity(request = request, json_data = json_data, match_keys = [ 'default_fail_type', 'default_fail_src' ]) 45 | if json_check: 46 | self.logger.debug('[SettingsManager] Save request: ' + str(json_data)) 47 | json_data.pop('version', None) 48 | json_data.pop('api_key', None) 49 | set_id = json_data.get('id') 50 | query = Settings.query.filter_by(id = set_id).first() 51 | if query: 52 | active_job_counter = [] 53 | if query.default_fail_src != json_data.get('default_fail_src'): 54 | query_job = Job.query.filter(Job.source_active == 'failover', Job.run_status != 'OFF').all() 55 | if query_job: 56 | for job in query_job: 57 | active_job_counter.append(job.id) 58 | if active_job_counter: 59 | # Stop active server's jobs 60 | _JobManager.JobStop(json_data = { 'id': active_job_counter }) 61 | Settings.query.filter_by(id = set_id).update(json_data) 62 | db_session.commit() 63 | self.logger.info('[SettingsManager] Save: OK') 64 | if active_job_counter: 65 | _JobManager.JobStart(json_data = { 'id': active_job_counter }) 66 | self.logger.info('[SettingsManager] Default Failover update: Associated active jobs were restarted ' + str(active_job_counter)) 67 | info = _JSONTools.JSONToString(json_data = json_data) 68 | _AlarmManager.OnAction(report = { 'event': 'Save settings', 'info': info[:-2] }) 69 | return response, 200, 'OK' 70 | else: 71 | msg = 'Settings query error' 72 | except: 73 | msg = 'Exception error' 74 | #raise 75 | self.logger.error('[SettingsManager] Save: ' + msg) 76 | return response, 400, msg 77 | 78 | -------------------------------------------------------------------------------- /app/main/settings/views.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from flask_login import login_required 4 | 5 | from . import _SettingsManager 6 | 7 | 8 | blueprint = Blueprint('settings', __name__) 9 | 10 | 11 | # System settings load 12 | @blueprint.route('/settings/load', methods=['POST']) 13 | @login_required 14 | def settings_load(): 15 | response, code, msg = _SettingsManager.SettingsLoad(request = request) 16 | return jsonify(response), str(code) + ' ' + str(msg) 17 | 18 | 19 | # System settings save 20 | @blueprint.route('/settings/save', methods=['POST']) 21 | @login_required 22 | def settings_save(): 23 | response, code, msg = _SettingsManager.SettingsSave(request = request) 24 | return jsonify(response), str(code) + ' ' + str(msg) 25 | 26 | -------------------------------------------------------------------------------- /app/main/smooth/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | -------------------------------------------------------------------------------- /app/main/smooth/smooth.py: -------------------------------------------------------------------------------- 1 | # 2 | #import requests, untangle 3 | #from requests_ntlm import HttpNtlmAuth 4 | 5 | #from xtech_live.config import BaseConfig 6 | # 7 | 8 | #### Manage Smooth Streaming publishing points (MS IIS) ### 9 | #class SmoothIIS(BaseConfig): 10 | 11 | # def GetState(self, url = None, user = None, password = None): 12 | # code = 404 13 | # headers = { 'Content-Type': 'application/atom+xml' } 14 | # try: 15 | # r = requests.get(url + '.isml/state', headers = headers, timeout = 3, auth = HttpNtlmAuth(user, password)) 16 | # code = r.status_code 17 | # if code == 200: 18 | # IISResponce = untangle.parse(r.text) 19 | # msg = IISResponce.entry.content.SmoothStreaming.State.Value.cdata 20 | # else: 21 | # msg = 'IIS GetState request error (' + str(code) + ')' 22 | # except RequestException as e: 23 | # msg = 'IIS GetState exception error (' + str(e) + ')' 24 | # return code, msg 25 | 26 | # def SetState(self, url = None, state_value = None, user = None, password = None): 27 | # code = 404 28 | # headers = { 'Content-Type': 'application/atom+xml' } 29 | # try: 30 | # xml = '' + state_value + '' 31 | # r = requests.put(url + '.isml/state', headers = headers, data = xml, timeout = 3, auth = HttpNtlmAuth(user, password)) 32 | # code = r.status_code 33 | # if code == 200: 34 | # msg = 'IIS SetState: ' + state_value 35 | # else: 36 | # msg = 'IIS SetState request error (' + str(code) + ')' 37 | # except RequestException as e: 38 | # msg = 'IIS SetState exception error (' + str(e) + ')' 39 | # return code, msg 40 | 41 | # def Create(self, server = None, app = None, pp_name = None, user = None, password = None): 42 | # code = 404 43 | # headers = { 'Content-Type': 'application/atom+xml', 'Slug': '/' + app + '/' + pp_name + '.isml' } 44 | # post_url = 'http://' + server + '/services/smoothstreaming/publishingpoints.isml/settings' 45 | # try: 46 | # xml = '' + pp_name + 'Pushtruetrue22.0true' 47 | # r = requests.post(post_url, headers = headers, data = xml, timeout = 3, auth = HttpNtlmAuth(user, password)) 48 | # code = r.status_code 49 | # if code == 200: 50 | # msg = 'IIS Create: OK' 51 | # else: 52 | # msg = 'IIS Create request error (' + str(code) + ')' 53 | # except RequestException as e: 54 | # msg = 'IIS Create exception error (' + str(e) + ')' 55 | # return code, msg 56 | 57 | # def Delete(self, url = None, user = None, password = None): 58 | # code = 404 59 | # headers = { 'Content-Type': 'application/atom+xml' } 60 | # try: 61 | # r = requests.delete(url + '.isml/settings', headers = headers, data = xml, timeout = 3, auth = HttpNtlmAuth(user, password)) 62 | # code = r.status_code 63 | # if code == 200: 64 | # msg = 'IIS Delete: OK' 65 | # else: 66 | # msg = 'IIS Delete request error (' + str(code) + ')' 67 | # except RequestException as e: 68 | # msg = 'IIS Delete exception error (' + str(e) + ')' 69 | # return code, msg 70 | -------------------------------------------------------------------------------- /app/main/tools/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | 6 | 7 | from .json import JSONTools 8 | _JSONTools = JSONTools(logger = logger_system) 9 | 10 | from .file import FileTools 11 | _FileTools = FileTools() 12 | 13 | from .system import SystemTools 14 | _SystemTools = SystemTools(logger = logger_system) 15 | 16 | from .alarm import AlarmManager 17 | _AlarmManager = AlarmManager(logger = logger_system) 18 | 19 | from .toolset import ToolSet 20 | _ToolSet = ToolSet(logger = logger_system) 21 | 22 | # API endpoints 23 | from . import views 24 | -------------------------------------------------------------------------------- /app/main/tools/alarm.py: -------------------------------------------------------------------------------- 1 | 2 | import os, time, socket, requests, threading 3 | 4 | from requests.exceptions import RequestException 5 | from flask import current_app, render_template 6 | from flask_login import current_user 7 | 8 | from main.common.config import app_config 9 | from main.common.models import Settings 10 | from main.common.log import LogManager 11 | from . import _FileTools, _SystemTools 12 | 13 | 14 | ### System alarms generator ### 15 | class AlarmManager(LogManager): 16 | 17 | 18 | def __init__(self, logger = None): 19 | self.logger = logger or self.LogNull() 20 | self.alarm_timer = time.time() 21 | self.action_report = [] 22 | self.action_counter = 0 23 | 24 | 25 | # Get alarm settings 26 | def LoadSettings(self): 27 | self.set_query = Settings.query.first() 28 | self.mail_to = self.set_query.alarm_master_email 29 | self.subject = self.set_query.alarm_master_subject 30 | return None 31 | 32 | 33 | # Sent alarm email 34 | def ProcessEmail(self, report = {}, template = None): 35 | # self.logger.debug(f'[ProcessEmail] report: {report}') 36 | info = { 'host': socket.gethostname(), 'report': report } 37 | try: 38 | with current_app.app_context(): 39 | template = render_template(template, info = info) 40 | email_setup = { 'host': self.set_query.smtp_host, 'port': self.set_query.smtp_port, 'user': self.set_query.smtp_user, 'password': self.set_query.smtp_pass, 'SSL': self.set_query.smtp_ssl, 'TLS': self.set_query.smtp_tls, 'subject': self.subject, 'mail_to': self.mail_to, 'template': template, 'report': report } 41 | # Send email in separate thread 42 | t = threading.Thread(target = _SystemTools.SendEmail, kwargs = email_setup) 43 | t.daemon = True 44 | t.start() 45 | except: 46 | self.logger.error('[AlarmManager] ProcessEmail error exception') 47 | raise 48 | return info 49 | 50 | 51 | # Callback run 52 | def ProcessCallback(self, event = None, info = None, func_type = None): 53 | if self.set_query.callback_url: 54 | info['event'] = event 55 | result, msg = self.Callback(url = self.set_query.callback_url, data = info) 56 | self.logger.info('[AlarmManager] ' + str(func_type) + ': Callback ' + str(msg)) 57 | return None 58 | 59 | 60 | # Action alarms 61 | def OnError(self, report = None): 62 | try: 63 | self.LoadSettings() 64 | time_now = time.time() 65 | if (self.set_query.alarm_error) and (int(time_now - self.alarm_timer) >= (self.set_query.alarm_error_period * self.set_query.alarm_error_value)): 66 | self.logger.info('[AlarmManager] OnError: Triggered at ' + str(int(time_now - self.alarm_timer)) + ' second(s)') 67 | if not self.set_query.alarm_master: 68 | self.mail_to = self.set_query.alarm_error_email 69 | self.subject = self.set_query.alarm_error_subject 70 | info = self.ProcessEmail(report = report, template = app_config.EMAIL_TEMPLATE_ERROR) 71 | self.alarm_timer = time_now 72 | self.ProcessCallback(event = 'error', info = info, func_type = 'OnError') 73 | except: 74 | self.logger.error('[AlarmManager] OnError: Exception error') 75 | raise 76 | return None 77 | 78 | 79 | def OnAction(self, report = None, append = False): 80 | try: 81 | self.LoadSettings() 82 | if self.set_query.alarm_action: 83 | if not append: 84 | self.action_counter += 1 85 | report.update({ 86 | 'user': current_user.username if current_user else None, 87 | 'time': time.strftime('%x %X') 88 | }) 89 | self.action_report.append(report) 90 | if self.action_counter >= self.set_query.alarm_action_count: 91 | self.logger.info('[AlarmManager] OnAction: Triggered at ' + str(self.action_counter) + ' count(s)') 92 | self.action_counter = 0 93 | if not self.set_query.alarm_master: 94 | self.mail_to = self.set_query.alarm_action_email 95 | self.subject = self.set_query.alarm_action_subject 96 | info = self.ProcessEmail(report = self.action_report, template = app_config.EMAIL_TEMPLATE_ACTION) 97 | self.action_report = [] 98 | self.ProcessCallback(event = 'action', info = info, func_type = 'OnAction') 99 | except: 100 | self.logger.error('[AlarmManager] OnAction: Exception error') 101 | raise 102 | return None 103 | 104 | 105 | ### Send callback data ### 106 | def Callback(self, url = None, data = None): 107 | result = None 108 | try: 109 | r = requests.post(url, headers = app_config.HEADERS_JSON, json = data, timeout = 3) 110 | code = r.status_code 111 | if code >= 200 and code < 400: 112 | msg = 'OK (' + str(code) + ')' 113 | else: 114 | msg = 'Error (' + str(code) + ')' 115 | except RequestException as e: 116 | msg = 'RequestException error (' + str(e) + ')' 117 | except: 118 | msg = 'Exception error' 119 | raise 120 | return result, msg 121 | 122 | -------------------------------------------------------------------------------- /app/main/tools/file.py: -------------------------------------------------------------------------------- 1 | 2 | import os, time, glob 3 | 4 | from zipfile import ZipFile, ZIP_DEFLATED 5 | 6 | from main.common.config import app_config 7 | from main.common.log import LogManager 8 | 9 | 10 | # File system tools 11 | class FileTools(LogManager): 12 | 13 | 14 | def __init__(self, logger = None): 15 | self.logger = logger or self.LogNull() 16 | 17 | 18 | # Create directory tree 19 | def InitDirTree(self, dir_tree = None): 20 | if dir_tree: 21 | for d in dir_tree: 22 | try: 23 | os.makedirs(d) 24 | except OSError as e: 25 | if not 'File exist' in str(e): 26 | self.logger.error('InitDirTree exception error: ' + str(e)) 27 | except: 28 | self.logger.error('InitDirTree exception error') 29 | 30 | 31 | # List folder files 32 | def ListDir(self, dir = None, ext = None, pattern = None, abs_path = False): 33 | list = [] 34 | for root, directory, file in os.walk(dir): 35 | for f in file: 36 | if abs_path: 37 | f = os.path.join(dir, f) 38 | if ext and f.endswith(ext): 39 | list.append(f) 40 | elif pattern and (pattern in f): 41 | list.append(f) 42 | else: 43 | list.append(f) 44 | return list 45 | 46 | 47 | # Check if file is up to date 48 | def HasExpired(self, file = None, expire_time = 30, remove_expired = False): 49 | expired = True 50 | try: 51 | if os.path.isfile(file): 52 | time_diff = int(time.time() - os.path.getmtime(file)) 53 | if time_diff > expire_time: 54 | if remove_expired: 55 | os.remove(file) 56 | else: 57 | expired = False 58 | except: 59 | pass 60 | raise 61 | return expired 62 | 63 | 64 | def WipeFiles(self, path = None, pattern = None, time_shift = None): 65 | try: 66 | files = glob.glob(os.path.join(path, pattern)) 67 | if files: 68 | for file in files: 69 | file_time_diff = int(time.time() - os.path.getmtime(file)) 70 | if file_time_diff > time_shift: 71 | os.remove(file) 72 | except: 73 | self.logger.error('[FileTools] WipeFiles: Exception error') 74 | raise 75 | return None 76 | 77 | 78 | # Filesystem and app directories init 79 | def FSInit(self): 80 | try: 81 | dir_init = [ 82 | app_config.MIGRATIONS_DIR, 83 | app_config.DRM_DIR, 84 | app_config.UPDATE_DIR, 85 | app_config.LOG_DIR_SYSTEM, 86 | app_config.LOG_DIR_JOBS, 87 | app_config.LOG_DIR_ERRORS, 88 | app_config.SS_DIR_SYS, 89 | app_config.HLS_DIR, 90 | app_config.IMAGES_DIR, 91 | app_config.CLIPS_DIR 92 | ] 93 | self.InitDirTree(dir_init) 94 | self.logger.info('[FileTools] FSInit: OK') 95 | except: 96 | self.logger.error('[FileTools] FSInit: Exception error') 97 | raise 98 | return None 99 | 100 | 101 | ### Reads file line by line, returns line array ### 102 | def ReadFileByLine(self, file, read_from, offset, show_lines): 103 | file_content = None 104 | if os.path.isfile(file): 105 | with open(file) as f: 106 | file_content = [ line for line in f ] 107 | file_length = len(file_content) 108 | if read_from == 'start': 109 | start_line = offset 110 | end_line = offset + show_lines 111 | if start_line > file_length: 112 | file_content = None 113 | else: 114 | start_line = max(0, file_length - offset - show_lines) 115 | end_line = file_length - offset 116 | if end_line < 0: 117 | file_content = None 118 | if file_content: 119 | file_content = "".join(line for line in file_content[start_line:end_line]) 120 | else: 121 | file_content = 'File is not available' 122 | return file_content 123 | 124 | 125 | ### Create a ZIP archive (no compression) ### 126 | def CompressFiles(self, archive = None, files = None, remove = False): 127 | result = None 128 | try: 129 | if files: 130 | count = 0 131 | for f in files: 132 | if os.path.isfile(f): 133 | z = ZipFile(archive, 'a', compression = ZIP_DEFLATED) 134 | z.write(f, os.path.basename(f)) 135 | count += 1 136 | if remove: 137 | os.remove(f) 138 | z.close() 139 | if os.path.isfile(archive) and count: 140 | result = archive 141 | msg = str(count) + ' file(s) compressed as ' + str(archive) 142 | self.logger.debug('[FileTools] ZIP: ' + msg) 143 | else: 144 | msg = 'No file(s) were processed' 145 | self.logger.warning('[FileTools] ZIP: No file(s) were processed') 146 | else: 147 | msg = 'No file(s) to compress' 148 | self.logger.warning('[FileTools] ZIP: No file(s) to compress') 149 | except: 150 | msg = 'Exception error' 151 | self.logger.error('[FileTools] ZIP: Exception error') 152 | raise 153 | return result, msg 154 | 155 | -------------------------------------------------------------------------------- /app/main/tools/json.py: -------------------------------------------------------------------------------- 1 | 2 | from main.common.log import LogManager 3 | 4 | 5 | # JSON tools 6 | class JSONTools(LogManager): 7 | 8 | 9 | def __init__(self, logger = None): 10 | self.logger = logger or self.LogNull() 11 | 12 | 13 | # Check request with JSON 14 | def CheckRequest(self, request = None): 15 | try: 16 | json_data = request.get_json() or {} 17 | msg = 'OK' 18 | except: 19 | json_data = {} 20 | msg = 'Request has no JSON data' 21 | raise 22 | return json_data, msg 23 | 24 | 25 | # Check JSON keys 26 | def CheckKeys(self, json_data = {}, match_keys = []): 27 | result = False 28 | try: 29 | if match_keys: 30 | json_keys = list(json_data.keys()) 31 | missing_json_keys = [ key for key in match_keys if not key in json_keys ] 32 | if missing_json_keys: 33 | return False, 'Required key(s): ' + str(missing_json_keys) 34 | msg = 'OK' 35 | result = True 36 | except: 37 | msg = 'JSON format error (CheckKeys)' 38 | raise 39 | return result, msg 40 | 41 | 42 | # Check JSON data 43 | def CheckIntegrity(self, request = None, json_data = {}, match_keys = []): 44 | result = False 45 | if request: 46 | json_data, msg = self.CheckRequest(request = request) 47 | if not json_data is None: 48 | result, msg = self.CheckKeys(json_data = json_data, match_keys = match_keys) 49 | else: 50 | msg = 'JSON format error (CheckIntegrity)' 51 | return result, msg, json_data 52 | 53 | 54 | def JSONToString(self, json_data = {}): 55 | try: 56 | string = '' 57 | for k in list(json_data): 58 | val = json_data.pop(k) 59 | string += '%s: %s, ' % (k, val) 60 | except: 61 | string = 'JSON to String conversion error' 62 | raise 63 | return string 64 | 65 | -------------------------------------------------------------------------------- /app/main/tools/toolset.py: -------------------------------------------------------------------------------- 1 | 2 | from .json import JSONTools 3 | from .file import FileTools 4 | from .system import SystemTools 5 | 6 | # Complete tool set 7 | class ToolSet(JSONTools, FileTools, SystemTools): 8 | 9 | def __init__(self, logger = None): 10 | self.logger = logger or self.LogNull() 11 | -------------------------------------------------------------------------------- /app/main/tools/views.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from flask_login import login_required 4 | 5 | from main.common.config import app_config 6 | from . import _SystemTools 7 | 8 | 9 | blueprint = Blueprint('tools', __name__) 10 | 11 | 12 | # System update check 13 | @blueprint.route('/tools/update_check', methods=['POST']) 14 | @login_required 15 | def update_check(): 16 | response, code, msg = _SystemTools.UpdateCheck(request = request) 17 | return jsonify(response), str(code) + ' ' + str(msg) 18 | 19 | 20 | # System update apply 21 | @blueprint.route('/tools/update_apply', methods=['POST']) 22 | @login_required 23 | def update_apply(): 24 | response, code, msg = _SystemTools.UpdateApply(request = request) 25 | return jsonify(response), str(code) + ' ' + str(msg) 26 | 27 | 28 | # System statistics 29 | @blueprint.route('/tools/system_stats', methods=['POST']) 30 | @login_required 31 | def system_stats(): 32 | response, code, msg = _SystemTools.SystemStats(request = request) 33 | return jsonify(response), str(code) + ' ' + str(msg) 34 | 35 | 36 | # App core/service restart 37 | @blueprint.route('/tools/app_restart', methods=['POST']) 38 | @login_required 39 | def app_restart(): 40 | response, code, msg = _SystemTools.AppRestart(request = request, service_name = app_config.SERVICE_NAME) 41 | return jsonify(response), str(code) + ' ' + str(msg) 42 | 43 | 44 | # Reboot server 45 | @blueprint.route('/tools/system_reboot', methods=['POST']) 46 | @login_required 47 | def system_reboot(): 48 | response, code, msg = _SystemTools.SystemReboot(request = request) 49 | return jsonify(response), str(code) + ' ' + str(msg) 50 | 51 | 52 | # DRM keygen 53 | @blueprint.route('/tools/drm_keygen', methods=['POST']) 54 | @login_required 55 | def drm_keygen(): 56 | response, code, msg = _SystemTools.DRMKeygen(request = request) 57 | return jsonify(response), str(code) + ' ' + str(msg) 58 | 59 | 60 | # Read system log(s) 61 | @blueprint.route('/tools/get_log', methods=['POST']) 62 | @login_required 63 | def get_log(): 64 | response, code, msg = _SystemTools.GetLog(request = request) 65 | return jsonify(response), str(code) + ' ' + str(msg) 66 | 67 | -------------------------------------------------------------------------------- /app/main/user/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Module init 3 | 4 | from main.common.log import logger_system 5 | 6 | from .user import UserManager 7 | _UserManager = UserManager(logger = logger_system) 8 | 9 | # API endpoints 10 | from . import views 11 | -------------------------------------------------------------------------------- /app/main/user/views.py: -------------------------------------------------------------------------------- 1 | 2 | from flask import Blueprint, request, jsonify 3 | from flask_login import login_required, fresh_login_required 4 | 5 | from . import _UserManager 6 | 7 | 8 | blueprint = Blueprint('user', __name__) 9 | 10 | 11 | # List all users 12 | @blueprint.route('/user/list', methods=['POST']) 13 | @login_required 14 | def user_list(): 15 | response, code, msg = _UserManager.UserList(request = request) 16 | return jsonify(response), str(code) + ' ' + str(msg) 17 | 18 | 19 | # User add 20 | @blueprint.route('/user/add', methods=['POST']) 21 | @login_required 22 | def user_add(): 23 | response, code, msg = _UserManager.UserAdd(request = request) 24 | return jsonify(response), str(code) + ' ' + str(msg) 25 | 26 | 27 | # User update 28 | @blueprint.route('/user/update', methods=['POST']) 29 | @login_required 30 | def user_update(): 31 | response, code, msg = _UserManager.UserUpdate(request = request) 32 | return jsonify(response), str(code) + ' ' + str(msg) 33 | 34 | 35 | # User delete 36 | @blueprint.route('/user/delete', methods=['POST']) 37 | @login_required 38 | def user_delete(): 39 | response, code, msg = _UserManager.UserDelete(request = request) 40 | return jsonify(response), str(code) + ' ' + str(msg) 41 | 42 | -------------------------------------------------------------------------------- /app/make_bin.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | from setuptools import find_packages, setup, Extension 5 | from distutils.sysconfig import get_config_var 6 | from Cython.Distutils import build_ext 7 | from Cython.Build import cythonize 8 | 9 | 10 | def get_ext_filename_without_platform_suffix(filename): 11 | name, ext = os.path.splitext(filename) 12 | ext_suffix = get_config_var('EXT_SUFFIX') 13 | 14 | if ext_suffix == ext: 15 | return filename 16 | 17 | ext_suffix = ext_suffix.replace(ext, '') 18 | idx = name.find(ext_suffix) 19 | 20 | if idx == -1: 21 | return filename 22 | else: 23 | return name[:idx] + ext 24 | 25 | 26 | class BuildExtWithoutPlatformSuffix(build_ext): 27 | def get_ext_filename(self, ext_name): 28 | filename = super().get_ext_filename(ext_name) 29 | return get_ext_filename_without_platform_suffix(filename) 30 | 31 | 32 | setup( 33 | name = 'main', 34 | version='2.4.6', 35 | cmdclass = { 'build_ext': BuildExtWithoutPlatformSuffix }, 36 | ext_modules = cythonize( 37 | [ 38 | Extension('main.*', [ 'run.py' ]), 39 | Extension('main.*', [ 'main/*/*.py' ]), 40 | Extension('main.*', [ 'main/*/*.py' ]) 41 | ], 42 | build_dir = 'build', 43 | # exclude = [ '**/__init__.py', 'smooth' ], 44 | nthreads = 8, 45 | compiler_directives = { 46 | 'language_level' : "3", 47 | 'always_allow_keywords': True 48 | } 49 | ), 50 | packages = find_packages() 51 | # packages = [] 52 | ) 53 | 54 | -------------------------------------------------------------------------------- /app/make_bin_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | #source ../../venv/bin/activate 3 | python make_bin.py build_ext 4 | #python make_bin.py bdist_wheel 5 | 6 | -------------------------------------------------------------------------------- /app/run.py: -------------------------------------------------------------------------------- 1 | 2 | #from werkzeug.wsgi import DispatcherMiddleware 3 | #from main.app import create_app as master_app 4 | from main.app import create_app 5 | 6 | #apps = {} 7 | #def apps_scope(): 8 | # try: 9 | # apps['/api'] = main_app() 10 | # print('App loaded: Main') 11 | # except: 12 | # print('App failed: Main') 13 | #raise 14 | # return apps 15 | 16 | #apps = apps_scope() 17 | #master_app = DispatcherMiddleware(apps.itervalues().next(), apps) 18 | master_app = create_app() 19 | -------------------------------------------------------------------------------- /conf/dev/default.env: -------------------------------------------------------------------------------- 1 | FLASK_ENV=Development 2 | #FLASK_ENV=Production 3 | SERVICE_NAME=pipenc_base 4 | DB_NAME=pipenc_base.db 5 | PERMANENT_SESSION_LIFETIME=3600 6 | #CLOUD_API_URL=http://localhost/api/v1 7 | CLOUD_API_URL=http://pipencoder.com/api/v1 8 | UPDATE_URL=http://pipencoder.com:8055/pipencoder/base 9 | -------------------------------------------------------------------------------- /conf/dev/pipenc_base.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Pipencoder Service 3 | After=syslog.target 4 | 5 | [Service] 6 | User=kashin 7 | Group=kashin 8 | WorkingDirectory=/home/kashin/Dev/pipencoder/pipencoder-base/app 9 | EnvironmentFile=/home/kashin/Dev/pipencoder/pipencoder-base/conf/dev/default.env 10 | ExecStart=/home/kashin/Dev/pipencoder/pipencoder-base/venv/bin/uwsgi -c ../conf/dev/pipenc_base_uwsgi.ini 11 | Restart=always 12 | KillSignal=SIGQUIT 13 | KillMode=process 14 | Type=idle 15 | StandardError=syslog 16 | NotifyAccess=all 17 | LimitNOFILE=64000 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /conf/dev/pipenc_base_logs: -------------------------------------------------------------------------------- 1 | /home/kashin/Dev/pipencoder/pipencoder-base/logs/jobs/*.log 2 | /home/kashin/Dev/pipencoder/pipencoder-base/logs/errors/*.log 3 | /home/kashin/Dev/pipencoder/pipencoder-base/logs/system/*.log 4 | { 5 | daily 6 | rotate 2 7 | copytruncate 8 | delaycompress 9 | notifempty 10 | missingok 11 | size 100M 12 | } 13 | -------------------------------------------------------------------------------- /conf/dev/pipenc_base_nginx: -------------------------------------------------------------------------------- 1 | 2 | upstream pipenc_base { 3 | # server localhost:8001; 4 | server unix:/home/kashin/Dev/pipencoder/pipencoder-base/run/uwsgi.sock; 5 | } 6 | 7 | server { 8 | 9 | listen 8077; 10 | server_name media_transcoder; 11 | root /home/kashin/Dev/pipencoder/pipencoder-base/wwwroot; 12 | 13 | log_not_found off; 14 | access_log off; 15 | error_log /home/kashin/Dev/pipencoder/pipencoder-base/logs/system/nginx.error.log; 16 | 17 | location / { 18 | try_files $uri /index.html; 19 | } 20 | 21 | location /images { 22 | expires 1d; 23 | } 24 | 25 | location /media { 26 | alias /home/kashin/Dev/pipencoder/pipencoder-base/media/; 27 | types { 28 | application/vnd.apple.mpegurl m3u8; 29 | video/mp2t ts; 30 | } 31 | add_header 'Access-Control-Allow-Origin' '*'; 32 | add_header 'Access-Control-Allow-Credentials' 'true'; 33 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 34 | add_header Cache-Control no-cache; 35 | expires off; 36 | } 37 | 38 | location /api { 39 | uwsgi_pass pipenc_base; 40 | include uwsgi_params; 41 | proxy_connect_timeout 60; 42 | proxy_send_timeout 60; 43 | proxy_read_timeout 60; 44 | send_timeout 60; 45 | uwsgi_read_timeout 60; 46 | } 47 | 48 | location ~* .(htm|html|css|js|txt|json) { 49 | add_header Cache-Control no-cache; 50 | expires off; 51 | } 52 | 53 | } 54 | 55 | server { 56 | 57 | listen 80; 58 | 59 | location /hls { 60 | root /home/kashin/Dev/pipencoder/pipencoder-base/media; 61 | client_body_temp_path /home/kashin/Dev/pipencoder/pipencoder-base/media/tmp; 62 | client_max_body_size 1000m; 63 | client_body_buffer_size 500m; 64 | # dav_methods PUT DELETE MKCOL COPY MOVE; 65 | # dav_ext_methods PROPFIND OPTIONS; 66 | # create_full_put_path on; 67 | # dav_access user:rw group:rw all:rw; 68 | # autoindex on; 69 | 70 | # You can specify the access restrictions. 71 | # In this case, only people on the 141.142 network can write/delete/etc. 72 | # Everyone else can view. 73 | #limit_except GET PROPFIND OPTIONS { 74 | # allow 141.142.0.0/16; 75 | # deny all; 76 | #} 77 | # allow all; 78 | } 79 | 80 | location ~* .(htm|html|css|js|txt|json) { 81 | add_header Cache-Control no-cache; 82 | expires off; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /conf/dev/pipenc_base_uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | virtualenv = ../venv 3 | module=run:master_app 4 | master = true 5 | uid = kashin 6 | gid = kashin 7 | chmod-socket = 664 8 | chown-socket = kashin:kashin 9 | socket = ../run/uwsgi.sock 10 | enable-threads = true 11 | workers = 4 12 | listen = 4096 13 | catch-exceptions = true 14 | pidfile = ../run/uwsgi.pid 15 | logto = ../logs/system/uwsgi.log 16 | log-maxsize = 10000000 17 | req-logger = file:/dev/null 18 | reload-mercy = 30 19 | worker-reload-mercy = 30 20 | buffer-size = 64000 21 | http-timeout = 60 22 | socket-timeout = 60 23 | post-buffering = true 24 | harakiri = 90 25 | ignore-sigpipe = true 26 | ignore-write-errors = true 27 | disable-write-exception = true 28 | thunder-lock = true 29 | python-autoreload = 1 30 | #max-worker-lifetime = 86400 31 | #max-requests = 1024 32 | #reload-on-as = 4096 33 | #reload-on-rss = 4096 34 | -------------------------------------------------------------------------------- /conf/prod/default.env: -------------------------------------------------------------------------------- 1 | FLASK_ENV=Development 2 | #FLASK_ENV=Production 3 | SERVICE_NAME=pipenc_base 4 | DB_NAME=pipenc_base.db 5 | PERMANENT_SESSION_LIFETIME=3600 6 | CLOUD_API_URL=http://pipencoder.com/api/v1 7 | UPDATE_URL=http://pipencoder.com:8055/pipencoder/base 8 | -------------------------------------------------------------------------------- /conf/prod/pipenc_base.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Pipencoder Service 3 | After=syslog.target 4 | 5 | [Service] 6 | User=pipencoder 7 | Group=pipencoder 8 | WorkingDirectory=/opt/pipencoder-base/app 9 | EnvironmentFile=/opt/pipencoder-base/conf/prod/default.env 10 | ExecStart=/opt/pipencoder-base/venv/bin/uwsgi -c ../conf/prod/pipenc_base_uwsgi.ini 11 | Restart=always 12 | KillSignal=SIGQUIT 13 | KillMode=process 14 | Type=idle 15 | StandardError=syslog 16 | NotifyAccess=all 17 | LimitNOFILE=64000 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /conf/prod/pipenc_base_logs: -------------------------------------------------------------------------------- 1 | /opt/pipencoder-base/logs/jobs/*.log 2 | /opt/pipencoder-base/logs/errors/*.log 3 | /opt/pipencoder-base/logs/system/*.log 4 | { 5 | daily 6 | rotate 2 7 | copytruncate 8 | delaycompress 9 | notifempty 10 | missingok 11 | size 100M 12 | } 13 | -------------------------------------------------------------------------------- /conf/prod/pipenc_base_nginx: -------------------------------------------------------------------------------- 1 | 2 | upstream pipenc_base { 3 | # server localhost:8001; 4 | server unix:/opt/pipencoder-base/run/uwsgi.sock; 5 | } 6 | 7 | server { 8 | 9 | listen 8077; 10 | server_name pipencoder; 11 | root /opt/pipencoder-base/wwwroot; 12 | 13 | log_not_found off; 14 | access_log off; 15 | error_log /opt/pipencoder-base/logs/system/nginx.error.log; 16 | 17 | location / { 18 | try_files $uri /index.html; 19 | } 20 | 21 | location /images { 22 | expires 1d; 23 | } 24 | 25 | location /media { 26 | alias /opt/pipencoder-base/media/; 27 | types { 28 | application/vnd.apple.mpegurl m3u8; 29 | video/mp2t ts; 30 | } 31 | add_header 'Access-Control-Allow-Origin' '*'; 32 | add_header 'Access-Control-Allow-Credentials' 'true'; 33 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 34 | add_header Cache-Control no-cache; 35 | expires off; 36 | } 37 | 38 | location /api { 39 | uwsgi_pass pipenc_base; 40 | include uwsgi_params; 41 | proxy_connect_timeout 60; 42 | proxy_send_timeout 60; 43 | proxy_read_timeout 60; 44 | send_timeout 60; 45 | uwsgi_read_timeout 60; 46 | } 47 | 48 | location ~* .(htm|html|css|js|txt|json) { 49 | add_header Cache-Control no-cache; 50 | expires off; 51 | } 52 | 53 | } 54 | 55 | server { 56 | 57 | listen 80; 58 | 59 | location /hls { 60 | root /opt/pipencoder-base/media; 61 | client_body_temp_path /opt/pipencoder-base/media/tmp; 62 | client_max_body_size 1000m; 63 | client_body_buffer_size 500m; 64 | dav_methods PUT DELETE MKCOL COPY MOVE; 65 | dav_ext_methods PROPFIND OPTIONS; 66 | create_full_put_path on; 67 | dav_access user:rw group:rw all:rw; 68 | autoindex on; 69 | 70 | # You can specify the access restrictions. 71 | # In this case, only people on the 141.142 network can write/delete/etc. 72 | # Everyone else can view. 73 | #limit_except GET PROPFIND OPTIONS { 74 | # allow 141.142.0.0/16; 75 | # deny all; 76 | #} 77 | allow all; 78 | } 79 | 80 | location ~* .(htm|html|css|js|txt|json) { 81 | add_header Cache-Control no-cache; 82 | expires off; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /conf/prod/pipenc_base_uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | virtualenv = ../venv 3 | module=run:master_app 4 | master = true 5 | uid = pipencoder 6 | gid = pipencoder 7 | chmod-socket = 664 8 | chown-socket = pipencoder:pipencoder 9 | socket = ../run/uwsgi.sock 10 | enable-threads = true 11 | workers = 4 12 | listen = 4096 13 | catch-exceptions = true 14 | pidfile = ../run/uwsgi.pid 15 | logto = ../logs/system/uwsgi.log 16 | log-maxsize = 10000000 17 | req-logger = file:/dev/null 18 | reload-mercy = 30 19 | worker-reload-mercy = 30 20 | buffer-size = 64000 21 | http-timeout = 60 22 | socket-timeout = 60 23 | post-buffering = true 24 | harakiri = 90 25 | ignore-sigpipe = true 26 | ignore-write-errors = true 27 | disable-write-exception = true 28 | thunder-lock = true 29 | python-autoreload = 1 30 | #max-worker-lifetime = 86400 31 | #max-requests = 1024 32 | #reload-on-as = 4096 33 | #reload-on-rss = 4096 34 | -------------------------------------------------------------------------------- /db/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /db/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /db/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy import pool 8 | 9 | from alembic import context 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | #fileConfig(config.config_file_name) 18 | logger = logging.getLogger('alembic') 19 | 20 | # add your model's MetaData object here 21 | # for 'autogenerate' support 22 | # from myapp import mymodel 23 | # target_metadata = mymodel.Base.metadata 24 | from flask import current_app 25 | config.set_main_option('sqlalchemy.url', 26 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 27 | target_metadata = current_app.extensions['migrate'].db.metadata 28 | 29 | # other values from the config, defined by the needs of env.py, 30 | # can be acquired: 31 | # my_important_option = config.get_main_option("my_important_option") 32 | # ... etc. 33 | 34 | 35 | def run_migrations_offline(): 36 | """Run migrations in 'offline' mode. 37 | 38 | This configures the context with just a URL 39 | and not an Engine, though an Engine is acceptable 40 | here as well. By skipping the Engine creation 41 | we don't even need a DBAPI to be available. 42 | 43 | Calls to context.execute() here emit the given string to the 44 | script output. 45 | 46 | """ 47 | url = config.get_main_option("sqlalchemy.url") 48 | context.configure( 49 | url=url, target_metadata=target_metadata, literal_binds=True 50 | ) 51 | 52 | with context.begin_transaction(): 53 | context.run_migrations() 54 | 55 | 56 | def run_migrations_online(): 57 | """Run migrations in 'online' mode. 58 | 59 | In this scenario we need to create an Engine 60 | and associate a connection with the context. 61 | 62 | """ 63 | 64 | # this callback is used to prevent an auto-migration from being generated 65 | # when there are no changes to the schema 66 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 67 | def process_revision_directives(context, revision, directives): 68 | if getattr(config.cmd_opts, 'autogenerate', False): 69 | script = directives[0] 70 | if script.upgrade_ops.is_empty(): 71 | directives[:] = [] 72 | logger.info('No changes in schema detected.') 73 | 74 | connectable = engine_from_config( 75 | config.get_section(config.config_ini_section), 76 | prefix='sqlalchemy.', 77 | poolclass=pool.NullPool, 78 | ) 79 | 80 | with connectable.connect() as connection: 81 | context.configure( 82 | connection=connection, 83 | target_metadata=target_metadata, 84 | process_revision_directives=process_revision_directives, 85 | **current_app.extensions['migrate'].configure_args 86 | ) 87 | 88 | with context.begin_transaction(): 89 | context.run_migrations() 90 | 91 | 92 | if context.is_offline_mode(): 93 | run_migrations_offline() 94 | else: 95 | run_migrations_online() 96 | -------------------------------------------------------------------------------- /db/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /db/migrations/versions/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/db/migrations/versions/.gitignore -------------------------------------------------------------------------------- /drm/auth/.htpasswd: -------------------------------------------------------------------------------- 1 | drm:$apr1$JhP6GoHT$NRMGcTfOVoJ7Vph9ftWWB. 2 | -------------------------------------------------------------------------------- /drm/auth/set_drm_auth.sh: -------------------------------------------------------------------------------- 1 | echo "drm:`openssl passwd -apr1`" > .htpasswd 2 | 3 | -------------------------------------------------------------------------------- /drm/key.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/drm/key.bin -------------------------------------------------------------------------------- /drm/key.hex: -------------------------------------------------------------------------------- 1 | 6b4b8008ef2c7489306ed144e0eb02cc 2 | -------------------------------------------------------------------------------- /drm/keygen.sh: -------------------------------------------------------------------------------- 1 | openssl rand 16 > key.bin 2 | openssl rand 16 -hex > key.hex 3 | -------------------------------------------------------------------------------- /media/assets/clips/demo1.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/demo1.mp4 -------------------------------------------------------------------------------- /media/assets/clips/demo2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/demo2.mp4 -------------------------------------------------------------------------------- /media/assets/clips/demo3.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/demo3.mp4 -------------------------------------------------------------------------------- /media/assets/clips/demo4.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/demo4.mp4 -------------------------------------------------------------------------------- /media/assets/clips/demo5.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/demo5.mp4 -------------------------------------------------------------------------------- /media/assets/clips/demo6.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/demo6.mp4 -------------------------------------------------------------------------------- /media/assets/clips/demo7.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/demo7.mp4 -------------------------------------------------------------------------------- /media/assets/clips/demo_closed_captions.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/demo_closed_captions.ts -------------------------------------------------------------------------------- /media/assets/clips/please_stand_by_1080.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/please_stand_by_1080.ts -------------------------------------------------------------------------------- /media/assets/clips/please_stand_by_480.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/please_stand_by_480.ts -------------------------------------------------------------------------------- /media/assets/clips/please_stand_by_576.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/please_stand_by_576.ts -------------------------------------------------------------------------------- /media/assets/clips/please_stand_by_720.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/clips/please_stand_by_720.ts -------------------------------------------------------------------------------- /media/assets/images/01_please_stand_by_480.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/images/01_please_stand_by_480.jpg -------------------------------------------------------------------------------- /media/assets/images/02_please_stand_by_576.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/images/02_please_stand_by_576.jpg -------------------------------------------------------------------------------- /media/assets/images/03_please_stand_by_720.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/images/03_please_stand_by_720.jpg -------------------------------------------------------------------------------- /media/assets/images/04_please_stand_by_1080.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/assets/images/04_please_stand_by_1080.jpg -------------------------------------------------------------------------------- /media/logo/pipencoder_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/logo/pipencoder_icon.ico -------------------------------------------------------------------------------- /media/logo/pipencoder_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/logo/pipencoder_icon.png -------------------------------------------------------------------------------- /media/logo/pipencoder_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/logo/pipencoder_logo.jpg -------------------------------------------------------------------------------- /media/logo/pipencoder_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/logo/pipencoder_logo.png -------------------------------------------------------------------------------- /media/logo/pipencoder_preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/logo/pipencoder_preview.jpg -------------------------------------------------------------------------------- /media/logo/pipencoder_preview_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/media/logo/pipencoder_preview_alpha.png -------------------------------------------------------------------------------- /presets/audio/AAC_LC_128: -------------------------------------------------------------------------------- 1 | { 2 | "description": "System preset", 3 | "abitrate": 128, 4 | "channels": 2, 5 | "sample_rate": 44100, 6 | "acodec": "libfdk_aac", 7 | "type": "audio", 8 | "name": "AAC LC 128" 9 | } -------------------------------------------------------------------------------- /presets/audio/AAC_LC_256: -------------------------------------------------------------------------------- 1 | { 2 | "description": "System preset", 3 | "abitrate": 256, 4 | "channels": 2, 5 | "acodec": "libfdk_aac", 6 | "type": "audio", 7 | "name": "AAC LC 256" 8 | } -------------------------------------------------------------------------------- /presets/audio/AAC_LC_64: -------------------------------------------------------------------------------- 1 | { 2 | "description": "System preset", 3 | "abitrate": 64, 4 | "channels": 2, 5 | "acodec": "libfdk_aac", 6 | "type": "audio", 7 | "name": "AAC LC 64" 8 | } -------------------------------------------------------------------------------- /presets/audio/AAC_LC_96: -------------------------------------------------------------------------------- 1 | { 2 | "description": "System preset", 3 | "abitrate": 96, 4 | "channels": 2, 5 | "acodec": "libfdk_aac", 6 | "type": "audio", 7 | "name": "AAC LC 96" 8 | } -------------------------------------------------------------------------------- /presets/audio/Copy: -------------------------------------------------------------------------------- 1 | { 2 | "type": "audio", 3 | "name": "Stream copy", 4 | "acodec": "copy", 5 | "abitrate": null, 6 | "description": "Stream copy, no encoding" 7 | } 8 | -------------------------------------------------------------------------------- /presets/audio/Decklink: -------------------------------------------------------------------------------- 1 | { 2 | "channels": 2, 3 | "description": "Decklink system preset", 4 | "sample_rate": 48000, 5 | "name": "Decklink", 6 | "acodec": "pcm_s16le" 7 | } -------------------------------------------------------------------------------- /presets/video/Copy: -------------------------------------------------------------------------------- 1 | { 2 | "type": "video", 3 | "name": "Stream copy", 4 | "vcodec": "copy", 5 | "vbitrate": null, 6 | "description": "Stream copy, no encoding" 7 | } 8 | -------------------------------------------------------------------------------- /presets/video/Decklink: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Decklink system preset", 3 | "vcodec": "v210", 4 | "subsample": "uyvy422", 5 | "vbuffer": 8192, 6 | "fps": "30000/1000", 7 | "name": "Decklink" 8 | } -------------------------------------------------------------------------------- /presets/video/H264_1300: -------------------------------------------------------------------------------- 1 | { 2 | "vcustom": "", 3 | "name": "H264 1300", 4 | "cc_copy": true, 5 | "vcodec": "libx264", 6 | "vbitrate": 1300, 7 | "keyframes": 2, 8 | "vprofile": "main", 9 | "subsample": "yuv420p", 10 | "minrate": 1300, 11 | "maxrate": 1300, 12 | "vbuffer": 2000, 13 | "vpreset": "superfast", 14 | "zerolatency": false, 15 | "type": "video", 16 | "gop": 150, 17 | "keyint_min": 75, 18 | "description": "System preset" 19 | } 20 | -------------------------------------------------------------------------------- /presets/video/H264_2500: -------------------------------------------------------------------------------- 1 | { 2 | "zerolatency": false, 3 | "vcustom": "", 4 | "name": "H264 2500", 5 | "cc_copy": true, 6 | "vcodec": "libx264", 7 | "vbitrate": 2500, 8 | "keyframes": 2, 9 | "vprofile": "main", 10 | "subsample": "yuv420p", 11 | "minrate": 2500, 12 | "maxrate": 2500, 13 | "vbuffer": 3700, 14 | "vpreset": "superfast", 15 | "type": "video", 16 | "gop": 150, 17 | "keyint_min": 75, 18 | "description": "System preset" 19 | } 20 | -------------------------------------------------------------------------------- /presets/video/H264_4000: -------------------------------------------------------------------------------- 1 | { 2 | "vcustom": "", 3 | "name": "H264 4000", 4 | "cc_copy": true, 5 | "vcodec": "libx264", 6 | "vbitrate": 4000, 7 | "keyframes": 2, 8 | "vprofile": "main", 9 | "subsample": "yuv420p", 10 | "minrate": 4000, 11 | "maxrate": 4000, 12 | "vbuffer": 6000, 13 | "vpreset": "superfast", 14 | "zerolatency": false, 15 | "type": "video", 16 | "gop": 150, 17 | "keyint_min": 75, 18 | "description": "System preset" 19 | } 20 | -------------------------------------------------------------------------------- /presets/video/H264_650: -------------------------------------------------------------------------------- 1 | { 2 | "vcustom": "", 3 | "description": "System preset", 4 | "cc_copy": true, 5 | "vcodec": "libx264", 6 | "vbitrate": 650, 7 | "keyframes": 2, 8 | "name": "H264 650", 9 | "vprofile": "main", 10 | "subsample": "yuv420p", 11 | "minrate": 650, 12 | "maxrate": 650, 13 | "vbuffer": 1000, 14 | "vpreset": "superfast", 15 | "zerolatency": false, 16 | "type": "video", 17 | "gop": 150, 18 | "keyint_min": 75 19 | } 20 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_1300: -------------------------------------------------------------------------------- 1 | { 2 | "coder": "cabac", 3 | "zerolatency": false, 4 | "temporal_aq": false, 5 | "description": "System preset", 6 | "strict_gop": true, 7 | "cc_copy": true, 8 | "vcodec": "h264_nvenc", 9 | "vbitrate": 1300, 10 | "keyframes": 2, 11 | "vprofile": "main", 12 | "cbr": false, 13 | "subsample": "yuv420p", 14 | "minrate": 1300, 15 | "maxrate": 1300, 16 | "vbuffer": 2000, 17 | "vpreset": "llhq", 18 | "rc": "vbr_hq", 19 | "vcustom": "", 20 | "spatial_aq": false, 21 | "type": "video", 22 | "gop": 150, 23 | "keyint_min": 75, 24 | "name": "H264 NVENC 1300" 25 | } 26 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_2500: -------------------------------------------------------------------------------- 1 | { 2 | "coder": "cabac", 3 | "zerolatency": false, 4 | "temporal_aq": false, 5 | "description": "System preset", 6 | "strict_gop": true, 7 | "cc_copy": true, 8 | "vcodec": "h264_nvenc", 9 | "vbitrate": 2500, 10 | "keyframes": 2, 11 | "vprofile": "main", 12 | "cbr": false, 13 | "subsample": "yuv420p", 14 | "minrate": 2500, 15 | "maxrate": 2500, 16 | "vbuffer": 3700, 17 | "vpreset": "llhq", 18 | "rc": "vbr_hq", 19 | "vcustom": "", 20 | "spatial_aq": false, 21 | "type": "video", 22 | "gop": 150, 23 | "keyint_min": 75, 24 | "name": "H264 NVENC 2500" 25 | } 26 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_4000: -------------------------------------------------------------------------------- 1 | { 2 | "coder": "cabac", 3 | "zerolatency": false, 4 | "temporal_aq": false, 5 | "description": "System preset", 6 | "strict_gop": true, 7 | "cc_copy": true, 8 | "vcodec": "h264_nvenc", 9 | "vbitrate": 4000, 10 | "keyframes": 2, 11 | "vprofile": "main", 12 | "cbr": false, 13 | "subsample": "yuv420p", 14 | "minrate": 4000, 15 | "maxrate": 4000, 16 | "vbuffer": 6000, 17 | "vpreset": "llhq", 18 | "rc": "vbr_hq", 19 | "vcustom": "", 20 | "spatial_aq": false, 21 | "type": "video", 22 | "gop": 150, 23 | "keyint_min": 75, 24 | "name": "H264 NVENC 4000" 25 | } 26 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_650: -------------------------------------------------------------------------------- 1 | { 2 | "coder": "cabac", 3 | "zerolatency": false, 4 | "vcustom": "", 5 | "description": "System preset", 6 | "strict_gop": true, 7 | "cc_copy": true, 8 | "vcodec": "h264_nvenc", 9 | "temporal_aq": false, 10 | "vbitrate": 650, 11 | "keyframes": 2, 12 | "vprofile": "main", 13 | "cbr": false, 14 | "subsample": "yuv420p", 15 | "minrate": 650, 16 | "maxrate": 650, 17 | "vbuffer": 1000, 18 | "rc": "vbr_hq", 19 | "vpreset": "llhq", 20 | "spatial_aq": false, 21 | "type": "video", 22 | "gop": 150, 23 | "keyint_min": 75, 24 | "name": "H264 NVENC 650" 25 | } 26 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_CBR_1300: -------------------------------------------------------------------------------- 1 | { 2 | "vpreset": "llhq", 3 | "vprofile": "main", 4 | "keyframes": 2, 5 | "cbr": true, 6 | "vcustom": "", 7 | "zerolatency": false, 8 | "spatial_aq": false, 9 | "coder": "cabac", 10 | "strict_gop": true, 11 | "cc_copy": true, 12 | "vbuffer": 1300, 13 | "rc": "cbr_hq", 14 | "type": "video", 15 | "gop": 150, 16 | "keyint_min": 75, 17 | "description": "CBR Mode", 18 | "temporal_aq": false, 19 | "name": "H264 NVENC CBR 1300", 20 | "vcodec": "h264_nvenc", 21 | "rc_lookahead": 8, 22 | "vbitrate": 1300, 23 | "subsample": "yuv420p", 24 | "minrate": 1300, 25 | "maxrate": 1300 26 | } 27 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_CBR_2500: -------------------------------------------------------------------------------- 1 | { 2 | "vpreset": "llhq", 3 | "vprofile": "main", 4 | "keyframes": 2, 5 | "cbr": true, 6 | "vcustom": "", 7 | "zerolatency": false, 8 | "spatial_aq": false, 9 | "coder": "cabac", 10 | "strict_gop": true, 11 | "cc_copy": true, 12 | "vbuffer": 2500, 13 | "rc": "cbr_hq", 14 | "type": "video", 15 | "gop": 150, 16 | "keyint_min": 75, 17 | "description": "CBR Mode", 18 | "temporal_aq": false, 19 | "name": "H264 NVENC CBR 2500", 20 | "vcodec": "h264_nvenc", 21 | "rc_lookahead": 8, 22 | "vbitrate": 2500, 23 | "subsample": "yuv420p", 24 | "minrate": 2500, 25 | "maxrate": 2500 26 | } 27 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_CBR_4000: -------------------------------------------------------------------------------- 1 | { 2 | "vpreset": "llhq", 3 | "vprofile": "main", 4 | "keyframes": 2, 5 | "cbr": true, 6 | "vcustom": "", 7 | "zerolatency": false, 8 | "spatial_aq": false, 9 | "coder": "cabac", 10 | "strict_gop": true, 11 | "cc_copy": true, 12 | "vbuffer": 4000, 13 | "rc": "cbr_hq", 14 | "type": "video", 15 | "gop": 150, 16 | "keyint_min": 75, 17 | "description": "CBR Mode", 18 | "temporal_aq": false, 19 | "name": "H264 NVENC CBR 4000", 20 | "vcodec": "h264_nvenc", 21 | "rc_lookahead": 8, 22 | "vbitrate": 4000, 23 | "subsample": "yuv420p", 24 | "minrate": 4000, 25 | "maxrate": 4000 26 | } 27 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_CBR_650: -------------------------------------------------------------------------------- 1 | { 2 | "vpreset": "llhq", 3 | "vprofile": "main", 4 | "keyframes": 2, 5 | "cbr": true, 6 | "vcustom": "", 7 | "zerolatency": false, 8 | "spatial_aq": false, 9 | "coder": "cabac", 10 | "strict_gop": true, 11 | "cc_copy": true, 12 | "vbuffer": 650, 13 | "rc": "cbr_hq", 14 | "type": "video", 15 | "gop": 150, 16 | "keyint_min": 75, 17 | "description": "CBR Mode", 18 | "temporal_aq": false, 19 | "name": "H264 NVENC CBR 650", 20 | "vcodec": "h264_nvenc", 21 | "rc_lookahead": 8, 22 | "vbitrate": 650, 23 | "subsample": "yuv420p", 24 | "minrate": 650, 25 | "maxrate": 650 26 | } 27 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_HQ_1300: -------------------------------------------------------------------------------- 1 | { 2 | "vpreset": "llhq", 3 | "vprofile": "main", 4 | "keyframes": 2, 5 | "cbr": false, 6 | "vcustom": "", 7 | "zerolatency": false, 8 | "spatial_aq": true, 9 | "coder": "cabac", 10 | "strict_gop": true, 11 | "cc_copy": true, 12 | "vbuffer": 2000, 13 | "rc": "vbr_hq", 14 | "type": "video", 15 | "gop": 150, 16 | "keyint_min": 75, 17 | "description": "System preset", 18 | "temporal_aq": true, 19 | "name": "H264 NVENC HQ 1300", 20 | "vcodec": "h264_nvenc", 21 | "rc_lookahead": 16, 22 | "vbitrate": 1300, 23 | "subsample": "yuv420p", 24 | "minrate": 1300, 25 | "maxrate": 1300 26 | } 27 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_HQ_2500: -------------------------------------------------------------------------------- 1 | { 2 | "vpreset": "llhq", 3 | "vprofile": "main", 4 | "keyframes": 2, 5 | "cbr": false, 6 | "vcustom": "", 7 | "zerolatency": false, 8 | "spatial_aq": true, 9 | "coder": "cabac", 10 | "strict_gop": true, 11 | "cc_copy": true, 12 | "vbuffer": 3700, 13 | "rc": "vbr_hq", 14 | "type": "video", 15 | "gop": 150, 16 | "keyint_min": 75, 17 | "description": "System preset", 18 | "temporal_aq": true, 19 | "name": "H264 NVENC HQ 2500", 20 | "vcodec": "h264_nvenc", 21 | "rc_lookahead": 16, 22 | "vbitrate": 2500, 23 | "subsample": "yuv420p", 24 | "maxrate": 2500, 25 | "maxrate": 2500 26 | } 27 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_HQ_4000: -------------------------------------------------------------------------------- 1 | { 2 | "vpreset": "llhq", 3 | "vprofile": "main", 4 | "keyframes": 2, 5 | "cbr": false, 6 | "vcustom": "", 7 | "zerolatency": false, 8 | "spatial_aq": true, 9 | "coder": "cabac", 10 | "strict_gop": true, 11 | "cc_copy": true, 12 | "vbuffer": 6000, 13 | "rc": "vbr_hq", 14 | "type": "video", 15 | "gop": 150, 16 | "keyint_min": 75, 17 | "description": "System preset", 18 | "temporal_aq": true, 19 | "name": "H264 NVENC HQ 4000", 20 | "vcodec": "h264_nvenc", 21 | "rc_lookahead": 16, 22 | "vbitrate": 4000, 23 | "subsample": "yuv420p", 24 | "maxrate": 4000, 25 | "maxrate": 4000 26 | } 27 | -------------------------------------------------------------------------------- /presets/video/H264_NVENC_HQ_650: -------------------------------------------------------------------------------- 1 | { 2 | "vpreset": "llhq", 3 | "vprofile": "main", 4 | "keyframes": 2, 5 | "cbr": false, 6 | "vcustom": "", 7 | "zerolatency": false, 8 | "spatial_aq": true, 9 | "coder": "cabac", 10 | "strict_gop": true, 11 | "cc_copy": true, 12 | "vbuffer": 1000, 13 | "rc": "vbr_hq", 14 | "type": "video", 15 | "gop": 150, 16 | "keyint_min": 75, 17 | "description": "System preset", 18 | "temporal_aq": true, 19 | "name": "H264 NVENC HQ 650", 20 | "vcodec": "h264_nvenc", 21 | "rc_lookahead": 16, 22 | "vbitrate": 650, 23 | "subsample": "yuv420p", 24 | "maxrate": 650, 25 | "maxrate": 650 26 | } 27 | -------------------------------------------------------------------------------- /presets/video/HEVC_1300: -------------------------------------------------------------------------------- 1 | { 2 | "vcustom": "", 3 | "name": "HEVC 1300", 4 | "vcodec": "libx265", 5 | "vbitrate": 1300, 6 | "keyframes": 2, 7 | "subsample": "yuv420p", 8 | "minrate": 1300, 9 | "maxrate": 1300, 10 | "vbuffer": 2000, 11 | "vpreset": "veryfast", 12 | "zerolatency": false, 13 | "type": "video", 14 | "gop": 150, 15 | "keyint_min": 75, 16 | "description": "System preset" 17 | } 18 | -------------------------------------------------------------------------------- /presets/video/HEVC_2500: -------------------------------------------------------------------------------- 1 | { 2 | "vcustom": "", 3 | "name": "HEVC 2500", 4 | "vcodec": "libx265", 5 | "vbitrate": 2500, 6 | "keyframes": 2, 7 | "subsample": "yuv420p", 8 | "minrate": 2500, 9 | "maxrate": 2500, 10 | "vbuffer": 3700, 11 | "vpreset": "veryfast", 12 | "zerolatency": false, 13 | "type": "video", 14 | "gop": 150, 15 | "keyint_min": 75, 16 | "description": "System preset" 17 | } 18 | -------------------------------------------------------------------------------- /presets/video/HEVC_4000: -------------------------------------------------------------------------------- 1 | { 2 | "vcustom": "", 3 | "name": "HEVC 4000", 4 | "vcodec": "libx265", 5 | "vbitrate": 4000, 6 | "keyframes": 2, 7 | "subsample": "yuv420p", 8 | "minrate": 4000, 9 | "maxrate": 4000, 10 | "vbuffer": 6000, 11 | "vpreset": "veryfast", 12 | "zerolatency": false, 13 | "type": "video", 14 | "gop": 150, 15 | "keyint_min": 75, 16 | "description": "System preset" 17 | } 18 | -------------------------------------------------------------------------------- /presets/video/HEVC_650: -------------------------------------------------------------------------------- 1 | { 2 | "vcustom": "", 3 | "name": "HEVC 650", 4 | "vcodec": "libx265", 5 | "vbitrate": 650, 6 | "keyframes": 2, 7 | "subsample": "yuv420p", 8 | "minrate": 650, 9 | "maxrate": 650, 10 | "vbuffer": 1000, 11 | "vpreset": "veryfast", 12 | "zerolatency": false, 13 | "type": "video", 14 | "gop": 150, 15 | "keyint_min": 75, 16 | "description": "System preset" 17 | } 18 | -------------------------------------------------------------------------------- /presets/video/HEVC_NVENC_1300: -------------------------------------------------------------------------------- 1 | { 2 | "zerolatency": false, 3 | "vcustom": "", 4 | "name": "HEVC NVENC 1300", 5 | "strict_gop": true, 6 | "vcodec": "hevc_nvenc", 7 | "temporal_aq": false, 8 | "vbitrate": 1300, 9 | "keyframes": 2, 10 | "vprofile": "main", 11 | "cbr": false, 12 | "subsample": "yuv420p", 13 | "minrate": 1300, 14 | "maxrate": 1300, 15 | "vbuffer": 2000, 16 | "rc": "vbr_hq", 17 | "vpreset": "llhq", 18 | "spatial_aq": false, 19 | "type": "video", 20 | "gop": 150, 21 | "keyint_min": 75, 22 | "description": "System preset" 23 | } 24 | -------------------------------------------------------------------------------- /presets/video/HEVC_NVENC_2500: -------------------------------------------------------------------------------- 1 | { 2 | "zerolatency": false, 3 | "vcustom": "", 4 | "name": "HEVC NVENC 2500", 5 | "strict_gop": true, 6 | "vcodec": "hevc_nvenc", 7 | "temporal_aq": false, 8 | "vbitrate": 2500, 9 | "keyframes": 2, 10 | "vprofile": "main", 11 | "cbr": false, 12 | "subsample": "yuv420p", 13 | "minrate": 2500, 14 | "maxrate": 2500, 15 | "vbuffer": 3700, 16 | "rc": "vbr_hq", 17 | "vpreset": "llhq", 18 | "spatial_aq": false, 19 | "type": "video", 20 | "gop": 150, 21 | "keyint_min": 75, 22 | "description": "System preset" 23 | } 24 | -------------------------------------------------------------------------------- /presets/video/HEVC_NVENC_4000: -------------------------------------------------------------------------------- 1 | { 2 | "zerolatency": false, 3 | "vcustom": "", 4 | "name": "HEVC NVENC 4000", 5 | "strict_gop": true, 6 | "vcodec": "hevc_nvenc", 7 | "temporal_aq": false, 8 | "vbitrate": 4000, 9 | "keyframes": 2, 10 | "vprofile": "main", 11 | "cbr": false, 12 | "subsample": "yuv420p", 13 | "minrate": 4000, 14 | "maxrate": 4000, 15 | "vbuffer": 6000, 16 | "rc": "vbr_hq", 17 | "vpreset": "llhq", 18 | "spatial_aq": false, 19 | "type": "video", 20 | "gop": 150, 21 | "keyint_min": 75, 22 | "description": "System preset" 23 | } 24 | -------------------------------------------------------------------------------- /presets/video/HEVC_NVENC_650: -------------------------------------------------------------------------------- 1 | { 2 | "zerolatency": false, 3 | "vcustom": "", 4 | "name": "HEVC NVENC 650", 5 | "strict_gop": true, 6 | "vcodec": "hevc_nvenc", 7 | "temporal_aq": false, 8 | "vbitrate": 650, 9 | "keyframes": 2, 10 | "vprofile": "main", 11 | "cbr": false, 12 | "subsample": "yuv420p", 13 | "minrate": 650, 14 | "maxrate": 650, 15 | "vbuffer": 1000, 16 | "rc": "vbr_hq", 17 | "vpreset": "llhq", 18 | "spatial_aq": false, 19 | "type": "video", 20 | "gop": 150, 21 | "keyint_min": 75, 22 | "description": "System preset" 23 | } 24 | -------------------------------------------------------------------------------- /presets/video/HEVC_NVENC_HQ_1300: -------------------------------------------------------------------------------- 1 | { 2 | "zerolatency": false, 3 | "temporal_aq": true, 4 | "name": "HEVC NVENC HQ 1300", 5 | "strict_gop": true, 6 | "vcodec": "hevc_nvenc", 7 | "rc_lookahead": 16, 8 | "vbitrate": 1300, 9 | "keyframes": 2, 10 | "vprofile": "main", 11 | "cbr": false, 12 | "subsample": "yuv420p", 13 | "minrate": 1300, 14 | "maxrate": 1300, 15 | "vbuffer": 2000, 16 | "vpreset": "llhq", 17 | "rc": "vbr_hq", 18 | "vcustom": "", 19 | "spatial_aq": true, 20 | "type": "video", 21 | "gop": 150, 22 | "keyint_min": 75, 23 | "description": "System preset" 24 | } 25 | -------------------------------------------------------------------------------- /presets/video/HEVC_NVENC_HQ_2500: -------------------------------------------------------------------------------- 1 | { 2 | "zerolatency": false, 3 | "temporal_aq": true, 4 | "name": "HEVC NVENC HQ 2500", 5 | "strict_gop": true, 6 | "vcodec": "hevc_nvenc", 7 | "rc_lookahead": 16, 8 | "vbitrate": 2500, 9 | "keyframes": 2, 10 | "vprofile": "main", 11 | "cbr": false, 12 | "subsample": "yuv420p", 13 | "minrate": 2500, 14 | "maxrate": 2500, 15 | "vbuffer": 3700, 16 | "vpreset": "llhq", 17 | "rc": "vbr_hq", 18 | "vcustom": "", 19 | "spatial_aq": true, 20 | "type": "video", 21 | "gop": 150, 22 | "keyint_min": 75, 23 | "description": "System preset" 24 | } 25 | -------------------------------------------------------------------------------- /presets/video/HEVC_NVENC_HQ_4000: -------------------------------------------------------------------------------- 1 | { 2 | "zerolatency": false, 3 | "temporal_aq": true, 4 | "name": "HEVC NVENC HQ 4000", 5 | "strict_gop": true, 6 | "vcodec": "hevc_nvenc", 7 | "rc_lookahead": 16, 8 | "vbitrate": 4000, 9 | "keyframes": 2, 10 | "vprofile": "main", 11 | "cbr": false, 12 | "subsample": "yuv420p", 13 | "minrate": 4000, 14 | "maxrate": 4000, 15 | "vbuffer": 6000, 16 | "vpreset": "llhq", 17 | "rc": "vbr_hq", 18 | "vcustom": "", 19 | "spatial_aq": true, 20 | "type": "video", 21 | "gop": 150, 22 | "keyint_min": 75, 23 | "description": "System preset" 24 | } 25 | -------------------------------------------------------------------------------- /presets/video/HEVC_NVENC_HQ_650: -------------------------------------------------------------------------------- 1 | { 2 | "zerolatency": false, 3 | "temporal_aq": true, 4 | "name": "HEVC NVENC HQ 650", 5 | "strict_gop": true, 6 | "vcodec": "hevc_nvenc", 7 | "rc_lookahead": 16, 8 | "vbitrate": 650, 9 | "keyframes": 2, 10 | "vprofile": "main", 11 | "cbr": false, 12 | "subsample": "yuv420p", 13 | "minrate": 650, 14 | "maxrate": 650, 15 | "vbuffer": 1000, 16 | "vpreset": "llhq", 17 | "rc": "vbr_hq", 18 | "vcustom": "", 19 | "spatial_aq": true, 20 | "type": "video", 21 | "gop": 150, 22 | "keyint_min": 75, 23 | "description": "System preset" 24 | } 25 | -------------------------------------------------------------------------------- /templates/mail_action.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | HOST: {{ info.host }} 4 | 32 | 33 | 34 |

Following user action(s) detected:

35 | 36 | {% set vars = {'job_events': False, 'sys_events': False} %} 37 | 38 | {% for i in info.report %} 39 | {% if i.id %} 40 | {% if vars.update({'job_events': True}) %} {% endif %} 41 | {% else %} 42 | {% if vars.update({'sys_events': True}) %} {% endif %} 43 | {% endif %} 44 | {% endfor %} 45 | 46 | {% if vars.job_events %} 47 |

Job events

48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {% endif %} 58 | 59 | {% for i in info.report | sort(attribute = 'time') %} 60 | {% if i.id %} 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | {% endif %} 70 | {% endfor %} 71 | 72 |
TimeEventIDJob nameInfoUser
{{ i.time }}{{ i.event }}{{ i.id }} / {{ i.sid }}{{ i.name }}{{ i.info }}{{ i.user }}
73 | 74 | {% if vars.sys_events %} 75 |

System events

76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | {% endif %} 84 | 85 | {% for i in info.report | sort(attribute = 'time') %} 86 | {% if i.id is not defined %} 87 | 88 | 89 | 90 | 91 | 92 | 93 | {% endif %} 94 | {% endfor %} 95 | 96 |
TimeEventInfoUser
{{ i.time }}{{ i.event }}{{ i.info }}{{ i.user }}
97 | 98 |

Automatic report, please don't reply to this email.

99 | 100 | 101 | -------------------------------------------------------------------------------- /templates/mail_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | HOST: {{ info.host }} 4 | 32 | 33 | 34 |

Following error(s) detected:

35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | {% for i in info.report | sort(attribute = 'check_time') %} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {% endfor %} 55 | 56 |
TimeIDJob nameSourceTargetStatus
{{ i.check_time }}{{ i.id }} / {{ i.sid }}{{ i.name }} {{ i.source + ' (' + i.source_active + ')' }}{{ i.target }}{{ i.run_status }}
57 |

Automatic report, please don't reply to this email.

58 | 59 | 60 | -------------------------------------------------------------------------------- /tests/api_login_gui.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -d '{ 4 | "username": "admin", 5 | "password": "admin" 6 | }' \ 7 | -H "Content-Type: application/json" -v http://localhost:8077/api/v1/login 8 | -------------------------------------------------------------------------------- /tests/api_login_key.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -d '{ 4 | "username": "admin", 5 | "password": "admin", 6 | "api_key_options": 7 | { 8 | "action": "create", 9 | "key_ttl": 3600 10 | } 11 | }' \ 12 | -H "Content-Type: application/json" -v http://localhost:8077/api/v1/login 13 | -------------------------------------------------------------------------------- /tests/api_test_header_key.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -X POST \ 4 | -H "X-API-KEY: 67860ba5cae347e983b1481fc6aaa272" \ 5 | -H "Content-Type: application/json" \ 6 | -v http://localhost:8077/api/v1/user/list 7 | -------------------------------------------------------------------------------- /tests/api_test_json_key.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -d '{ 4 | "api_key": "463ab59003404547a38763ec296e2173" 5 | }' \ 6 | -H "Content-Type: application/json" -v http://localhost:8077/api/v1/logout 7 | -------------------------------------------------------------------------------- /tests/tests/test_auth.py: -------------------------------------------------------------------------------- 1 | 2 | Login = [ 3 | { 4 | 'name': 'AuthLogin', 5 | 'url': 'login', 6 | 'cases': [ 7 | { 8 | 'name': 'Username error', 9 | 'payload': { 10 | 'username': 'fake_user_name', 11 | 'password': 'fake_password' 12 | }, 13 | 'result': '400 Invalid username and/or password' 14 | }, 15 | { 16 | 'name': 'Password error', 17 | 'payload': { 18 | 'username': 'admin', 19 | 'password': 'fake_password' 20 | }, 21 | 'result': '400 Invalid username and/or password' 22 | }, 23 | { 24 | 'name': 'API key action error', 25 | 'payload': { 26 | 'username': 'admin', 27 | 'password': 'admin', 28 | 'api_key_options': { 29 | 'action': 'error_action', 30 | 'key_ttl': 3600 31 | } 32 | }, 33 | 'result': '200 OK' 34 | }, 35 | { 36 | 'name': 'API key JSON error', 37 | 'payload': { 38 | 'username': 'admin', 39 | 'password': 'admin', 40 | 'api_key_options': { 'key_error': 'value_error' } 41 | }, 42 | 'result': '200 OK' 43 | }, 44 | { 45 | 'name': 'JSON key error', 46 | 'payload': { 'key_error': 'value_error' }, 47 | 'result': '400 Required key(s)' 48 | }, 49 | { 50 | 'name': 'Regular', 51 | 'payload': { 52 | 'username': 'admin', 53 | 'password': 'admin', 54 | 'api_key_options': { 55 | 'action': 'create', 56 | 'key_ttl': 3600 57 | } 58 | }, 59 | 'result': '200 OK' 60 | } 61 | ] 62 | }, 63 | { 64 | 'name': 'AuthLoggedUser', 65 | 'url': 'logged_user', 66 | 'cases': [ 67 | { 68 | 'name': 'Regular', 69 | 'payload': {}, 70 | 'result': '200 OK' 71 | } 72 | ] 73 | } 74 | ] 75 | 76 | 77 | Logout = [ 78 | { 79 | 'name': 'AuthLogout', 80 | 'url': 'logout', 81 | 'cases': [ 82 | { 83 | 'name': 'Regular', 84 | 'payload': {}, 85 | 'result': '200 OK' 86 | } 87 | ] 88 | } 89 | ] 90 | -------------------------------------------------------------------------------- /tests/tests/test_media.py: -------------------------------------------------------------------------------- 1 | 2 | Media = [ 3 | { 4 | 'name': 'MediaInfo', 5 | 'url': 'media/info', 6 | 'cases': [ 7 | { 8 | 'name': 'Clip', 9 | 'payload': { 10 | 'media_type': 'Clip', 11 | 'media': '01_please_stand_by_480.ts' 12 | }, 13 | 'result': '200 OK' 14 | }, 15 | { 16 | 'name': 'Image', 17 | 'payload': { 18 | 'media_type': 'Image', 19 | 'media': '01_please_stand_by_480.jpg' 20 | }, 21 | 'result': '200 OK' 22 | }, 23 | { 24 | 'name': 'URL', 25 | 'payload': { 26 | 'media_type': 'URL', 27 | 'media': 'http://ott-cdn.ucom.am/s6/index.m3u8' 28 | }, 29 | 'delay': 5, 30 | 'result': '200 OK' 31 | }, 32 | { 33 | 'name': 'JSON key error', 34 | 'payload': { 'key_error': 'value_error' }, 35 | 'result': '400 Required key(s)' 36 | }, 37 | { 38 | 'name': 'JSON value error', 39 | 'payload': { 40 | 'media_type': 'Clip', 41 | 'media': 'value_error' 42 | }, 43 | 'result': '400 No such file or directory' 44 | } 45 | ] 46 | }, 47 | { 48 | 'name': 'MediaLocal', 49 | 'url': 'media/local', 50 | 'cases': [ 51 | { 52 | 'name': 'Regular', 53 | 'payload': {}, 54 | 'result': '200 OK' 55 | } 56 | ] 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /tests/tests/test_node.py: -------------------------------------------------------------------------------- 1 | 2 | Node = [ 3 | { 4 | 'name': 'NodeAuth', 5 | 'url': 'node/auth', 6 | 'cases': [ 7 | { 8 | 'name': 'Regular', 9 | 'payload': { 10 | 'account_email': 'uencodemo@ozzmedia.com', 11 | 'account_password': 'uencodemo' 12 | }, 13 | 'result': '200 OK' 14 | }, 15 | { 16 | 'name': 'Username error', 17 | 'payload': { 18 | 'account_email': 'fake_user_name', 19 | 'account_password': 'uencodemo' 20 | }, 21 | 'result': '400 Invalid account and/or password' 22 | }, 23 | { 24 | 'name': 'Password error', 25 | 'payload': { 26 | 'account_email': 'uencodemo@ozzmedia.com', 27 | 'account_password': 'fake_password' 28 | }, 29 | 'result': '400 Invalid account and/or password' 30 | }, 31 | { 32 | 'name': 'JSON key error', 33 | 'payload': {}, 34 | 'result': '400 Required key(s)' 35 | } 36 | ] 37 | }, 38 | { 39 | 'name': 'NodeActivate', 40 | 'url': 'node/activate', 41 | 'cases': [ 42 | { 43 | 'name': 'Regular', 44 | 'payload': {}, 45 | 'result': '200 OK' 46 | } 47 | ] 48 | }, 49 | { 50 | 'name': 'NodeDeactivate', 51 | 'url': 'node/deactivate', 52 | 'cases': [ 53 | { 54 | 'name': 'Regular', 55 | 'payload': {}, 56 | 'result': '200 OK' 57 | } 58 | ] 59 | } 60 | ] 61 | -------------------------------------------------------------------------------- /tests/tests/test_server.py: -------------------------------------------------------------------------------- 1 | 2 | Server = [ 3 | { 4 | 'name': 'ServerAdd', 5 | 'url': 'server/add', 6 | 'cases': [ 7 | { 8 | 'name': 'Regular', 9 | 'payload': { 10 | 'name': 'TestServer', 11 | 'ip': 'test.ip', 12 | 'hls_srv': 'http://test.ip/hls', 13 | 'rtmp_srv': 'rtmp://test.ip:1935/app/stream' 14 | }, 15 | 'result': '200 OK' 16 | }, 17 | { 18 | 'name': 'JSON key error', 19 | 'payload': {}, 20 | 'result': '400 Required key(s)' 21 | } 22 | ] 23 | }, 24 | { 25 | 'name': 'ServerList', 26 | 'url': 'server/list', 27 | 'cases': [ 28 | { 29 | 'name': 'Regular', 30 | 'payload': { 'hash': None }, 31 | 'result': '200 OK' 32 | } 33 | ] 34 | }, 35 | { 36 | 'name': 'ServerUpdate', 37 | 'url': 'server/update', 38 | 'cases': [ 39 | { 40 | 'name': 'Regular', 41 | 'payload': { 42 | 'id': None, 43 | 'ip': 'test.ip', 44 | 'name': 'TestServer', 45 | 'jobs_restart': True, 46 | 'hls_srv': 'http://test.ip/hls', 47 | 'rtmp_srv': 'rtmp://test.ip:1935/app/stream' 48 | }, 49 | 'result': '200 OK' 50 | }, 51 | { 52 | 'name': 'ID error', 53 | 'payload': { 54 | 'id': 'value_error', 55 | 'ip': 'test.ip', 56 | 'name': 'TestServer', 57 | 'jobs_restart': False 58 | }, 59 | 'result': '400 Server ID is not found' 60 | }, 61 | { 62 | 'name': 'JSON key error', 63 | 'payload': {}, 64 | 'result': '400 Required key(s)' 65 | } 66 | ] 67 | }, 68 | { 69 | 'name': 'ServerDelete', 70 | 'url': 'server/delete', 71 | 'cases': [ 72 | { 73 | 'name': 'Regular', 74 | 'payload': { 'id': None }, 75 | 'result': '200 OK' 76 | }, 77 | { 78 | 'name': 'ID value error', 79 | 'payload': { 'id': 'value_error' }, 80 | 'result': '400 Server ID is not found' 81 | }, 82 | { 83 | 'name': 'JSON key error', 84 | 'payload': {}, 85 | 'result': '400 Required key(s)' 86 | } 87 | ] 88 | } 89 | ] 90 | 91 | -------------------------------------------------------------------------------- /tests/tests/test_settings.py: -------------------------------------------------------------------------------- 1 | 2 | Settings = [ 3 | { 4 | 'name': 'SettingsLoad', 5 | 'url': 'settings/load', 6 | 'cases': [ 7 | { 8 | 'name': 'Regular', 9 | 'payload': {}, 10 | 'result': '200 OK' 11 | } 12 | ] 13 | }, 14 | { 15 | 'name': 'SettingsSave', 16 | 'url': 'settings/save', 17 | 'cases': [ 18 | { 19 | 'name': 'Regular', 20 | 'payload': { 21 | 'id': None, 22 | 'update_path': 'http://18.188.182.187:8099/update', 23 | 'callback_url': '', 24 | 'default_fail_src': 'new_value', 25 | 'default_fail_type': 'Clip', 26 | 'default_fail_vpid': '#123', 27 | 'default_fail_apid': '#456', 28 | 'default_fail_dpid': '#777', 29 | 'default_fail_decoder': 'libavcodec', 30 | 'default_fail_decoder_err_detect': 'crccheck', 31 | 'default_fail_loop': True, 32 | 'default_fail_udp_overrun': True, 33 | 'default_fail_udp_buffer': 1000, 34 | 'default_fail_udp_timeout': 5, 35 | 'default_srt_mode': 'Caller', 36 | 'default_srt_passphrase': 'passphrase', 37 | 'default_fail_merge_pmt_versions': True, 38 | 'default_fail_http_reconnect': True, 39 | 'smtp_host': 'localhost', 40 | 'smtp_port': 25, 41 | 'smtp_user': 'report@ozzmedia.tv', 42 | 'smtp_pass': '', 43 | 'smtp_ssl': False, 44 | 'smtp_tls': False, 45 | 'alarm_action': True, 46 | 'alarm_action_email': 'dm.kashin@yandex.ru', 47 | 'alarm_action_subject': 'Action Report', 48 | 'alarm_action_count': 1, 49 | 'alarm_error': True, 50 | 'alarm_error_email': 'dm.kashin@yandex.ru', 51 | 'alarm_error_subject': 'Error Report', 52 | 'alarm_error_value': 1, 53 | 'alarm_error_period': 60, 54 | 'alarm_master': True, 55 | 'alarm_master_email': 'dm.kashin@yandex.ru', 56 | 'alarm_master_subject': 'Report' 57 | }, 58 | 'result': '200 OK' 59 | }, 60 | { 61 | 'name': 'JSON key error', 62 | 'payload': {}, 63 | 'result': '400 Required key(s)' 64 | }, 65 | { 66 | 'name': 'ID value error', 67 | 'payload': { 68 | 'id': 'value_error', 69 | 'default_fail_src': False, 70 | 'default_fail_type': 100 71 | }, 72 | 'result': '400 Settings query error' 73 | }, 74 | { 75 | 'name': 'JSON value error', 76 | 'payload': { 77 | 'id': None, 78 | 'default_fail_src': False, 79 | 'default_fail_type': 100, 80 | 'key_error': 'value_error' 81 | }, 82 | 'result': '400 Exception error' 83 | } 84 | ] 85 | } 86 | ] 87 | 88 | -------------------------------------------------------------------------------- /tests/tests/test_tools.py: -------------------------------------------------------------------------------- 1 | 2 | Tools = [ 3 | { 4 | 'name': 'ToolsUpdateCheck', 5 | 'url': 'tools/update_check', 6 | 'cases': [ 7 | { 8 | 'name': 'Regular', 9 | 'payload': {}, 10 | 'result': '200 OK' 11 | } 12 | ] 13 | }, 14 | { 15 | 'name': 'ToolsUpdateApply', 16 | 'url': 'tools/update_apply', 17 | 'cases': [ 18 | { 19 | 'name': 'Regular', 20 | 'payload': {}, 21 | 'delay': 5, 22 | 'result': '200' 23 | } 24 | ] 25 | }, 26 | { 27 | 'name': 'ToolsSystemStats', 28 | 'url': 'tools/system_stats', 29 | 'cases': [ 30 | { 31 | 'name': 'Regular', 32 | 'payload': {}, 33 | 'result': '200 OK' 34 | } 35 | ] 36 | }, 37 | { 38 | 'name': 'ToolsDRMKeygen', 39 | 'url': 'tools/drm_keygen', 40 | 'cases': [ 41 | { 42 | 'name': 'Regular', 43 | 'payload': {}, 44 | 'result': '200 OK' 45 | } 46 | ] 47 | }, 48 | { 49 | 'name': 'ToolsAppRestart', 50 | 'url': 'tools/app_restart', 51 | 'cases': [ 52 | { 53 | 'name': 'Regular', 54 | 'payload': {}, 55 | 'delay': 10, 56 | 'result': '200 OK' 57 | } 58 | ] 59 | }, 60 | { 61 | 'name': 'ToolsSystemReboot', 62 | 'url': 'tools/system_reboot', 63 | 'cases': [ 64 | { 65 | 'name': 'Regular', 66 | 'payload': {}, 67 | 'result': '200 OK' 68 | } 69 | ] 70 | } 71 | ] 72 | 73 | -------------------------------------------------------------------------------- /tests/tests/test_user.py: -------------------------------------------------------------------------------- 1 | 2 | User = [ 3 | { 4 | 'name': 'UserAdd', 5 | 'url': 'user/add', 6 | 'cases': [ 7 | { 8 | 'name': 'Regular', 9 | 'payload': { 10 | 'username': 'test_user', 11 | 'password': 'test_pwd', 12 | 'alias': None, 13 | 'admin': True, 14 | 'su': False, 15 | }, 16 | 'result': '200 OK' 17 | }, 18 | { 19 | 'name': 'User name exists', 20 | 'payload': { 21 | 'username': 'test_user', 22 | 'password': 'test_pwd' 23 | }, 24 | 'result': '400 User name has already taken' 25 | }, 26 | { 27 | 'name': 'JSON key error', 28 | 'payload': { 29 | 'key_error': 'value_error' 30 | }, 31 | 'result': '400 Required key(s)' 32 | } 33 | ] 34 | }, 35 | { 36 | 'name': 'UserList', 37 | 'url': 'user/list', 38 | 'cases': [ 39 | { 40 | 'name': 'Regular', 41 | 'payload': {}, 42 | 'result': '200 OK' 43 | } 44 | ] 45 | }, 46 | { 47 | 'name': 'UserUpdate', 48 | 'url': 'user/update', 49 | 'cases': [ 50 | { 51 | 'name': 'Regular', 52 | 'payload': { 53 | 'id': None, 54 | 'username': 'test_user', 55 | 'password': 'new_pwd', 56 | 'admin': False 57 | }, 58 | 'result': '200 OK' 59 | }, 60 | { 61 | 'name': 'User name exists', 62 | 'payload': { 63 | 'id': None, 64 | 'username': 'admin', 65 | 'admin': False 66 | }, 67 | 'result': '400 User name has already taken' 68 | }, 69 | { 70 | 'name': 'User ID error', 71 | 'payload': { 'id': 'value_error' }, 72 | 'result': '400 User is not found' 73 | }, 74 | { 75 | 'name': 'JSON key error', 76 | 'payload': { 'key_error': 'value_error' }, 77 | 'result': '400 Required key(s)' 78 | } 79 | ] 80 | }, 81 | { 82 | 'name': 'UserDelete', 83 | 'url': 'user/delete', 84 | 'cases': [ 85 | { 86 | 'name': 'Regular', 87 | 'payload': { 'id': None }, 88 | 'result': '200 OK' 89 | }, 90 | { 91 | 'name': 'User ID error', 92 | 'payload': { 'id': 'value_error' }, 93 | 'result': '400 User is not found' 94 | }, 95 | { 96 | 'name': 'JSON key error', 97 | 'payload': { 'key_error': 'value_error' }, 98 | 'result': '400 Required key(s)' 99 | } 100 | ] 101 | } 102 | ] 103 | -------------------------------------------------------------------------------- /wwwroot/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /wwwroot/css/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/css/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /wwwroot/css/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/css/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /wwwroot/css/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/css/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /wwwroot/css/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/css/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /wwwroot/css/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/css/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/favicon.ico -------------------------------------------------------------------------------- /wwwroot/favicon_umedialink.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/favicon_umedialink.ico -------------------------------------------------------------------------------- /wwwroot/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /wwwroot/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /wwwroot/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /wwwroot/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /wwwroot/html/brand.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | Pipencode 10 | 11 | -------------------------------------------------------------------------------- /wwwroot/html/events.html: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 |
20 |
21 |
22 | 23 |

Please wait

24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |

No events to display

38 |
39 |
40 |
41 |
42 |
43 |
44 | -------------------------------------------------------------------------------- /wwwroot/html/events_update.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
New software version is available: v.{{ Stats.Update.new_version }}
4 |
5 |
6 | 7 | 8 |   Updating... 9 | 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /wwwroot/html/global_message.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
License activation failed: Click for details
5 |
6 |
7 | -------------------------------------------------------------------------------- /wwwroot/html/job_edit.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |

Please wait

5 |
6 | 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | -------------------------------------------------------------------------------- /wwwroot/html/job_edit_abr.html: -------------------------------------------------------------------------------- 1 | 2 |

HLS ABR

3 |
4 |
5 | 9 |
10 |
11 | 12 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 |

No HLS assets are available

28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 47 | 50 | 55 | 60 | 65 | 66 | 67 |
METADATA: BANDWIDTHRESOLUTIONCODECS
43 | 44 | 45 | 46 | 48 | {{ Asset.Manifest }} 49 | 51 |
52 | 53 |
54 |
56 |
57 | 58 |
59 |
61 |
62 | 63 |
64 |
68 |
69 |
70 |
71 | -------------------------------------------------------------------------------- /wwwroot/html/job_edit_checks.html: -------------------------------------------------------------------------------- 1 | 2 |

Checks

3 |
4 |
5 |
6 | 10 |
11 |
12 |
13 |
14 | 18 |
19 |
20 |
21 |
22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 | 40 |
41 |
42 |
43 |
44 | 48 |
49 |
50 |
51 |
52 | 56 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /wwwroot/html/job_edit_drm.html: -------------------------------------------------------------------------------- 1 | 2 |

HLS DRM

3 |
4 |
5 | 9 |
10 |
11 |
12 | 13 | 15 |
16 |
17 | 18 | 20 |
21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 |
40 | 41 |
42 | 43 |
44 | 45 |
46 |
47 |
48 |
49 | 50 |
51 | 52 |
53 | 54 |
55 |
56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 |

No HLS assets are available

64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 80 | 83 | 84 | 85 |
DRM ASSETS:
76 | 77 | 78 | 79 | 81 | {{ Asset.Manifest }} 82 |
86 |
87 |
88 |
89 | -------------------------------------------------------------------------------- /wwwroot/html/job_edit_info.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 | 7 |
8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /wwwroot/html/job_edit_preview.html: -------------------------------------------------------------------------------- 1 | 2 |

URL Preview

3 |
4 |
5 |
6 | Master ABR: 7 | 8 | {{ PreviewABR.URL }} 9 | 10 |
11 |
12 | {{ 'Profile #' + $index }} 13 |
14 | {{ 'Target #' + $index }} 15 | {{ ': ' + target.url }} 16 |
17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /wwwroot/html/job_list_filters.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 6 |
7 |
8 | 9 |
10 | 13 |
14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /wwwroot/html/job_list_pagination.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 6 |
7 | -------------------------------------------------------------------------------- /wwwroot/html/log.html: -------------------------------------------------------------------------------- 1 | 2 | 49 | -------------------------------------------------------------------------------- /wwwroot/html/login.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 | 22 | 28 |
29 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /wwwroot/html/player.html: -------------------------------------------------------------------------------- 1 | 2 | 31 | -------------------------------------------------------------------------------- /wwwroot/html/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 |

Access denied for current user

47 |
48 |
49 | 50 |

Please wait

51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
Settings
80 |
81 |
82 | Callback 83 |
84 |
85 |
86 | 87 | 88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | -------------------------------------------------------------------------------- /wwwroot/html/settings_alarm.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | Alarms 5 |
6 |
7 |
8 |
9 |
10 | 11 | 12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 |
46 | 47 | 48 |
49 |
50 |
51 |
52 |
53 |
54 | 55 | 56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 |
65 |
66 |
67 | -------------------------------------------------------------------------------- /wwwroot/html/settings_failover.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | Failover 5 |
6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 |
14 | 15 | 16 | 17 |
18 |
19 |
20 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 |
42 | 46 |
47 |
48 |
49 |
50 |
51 |
52 | 56 |
57 |
58 |
59 |
60 | 64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 |
76 | 77 | 78 |
79 |
80 |
81 |
82 |
83 | 84 | 85 |
86 |
87 | -------------------------------------------------------------------------------- /wwwroot/html/settings_lic.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
Licensing 6 | 7 | {{ LicenseInfo.account_email }} 8 | 9 |
10 |
11 |
12 |
13 |
14 |

15 | Type: {{ LicenseInfo.type }} 16 |

17 |

Status: 18 | {{ LicenseInfo.status }} 19 |

20 | 21 | 22 |
23 |
24 |

25 | Node ID: {{ LicenseInfo.id }} 26 |

27 |

Expire: {{ LicenseInfo.expire | date:'dd/MM/yyyy' }}

28 |

Limits: 29 | {{ LicenseInfo.limit_jobs_total == -1 ? 'Unlimited' : LicenseInfo.limit_jobs_total }} total jobs, {{ LicenseInfo.limit_jobs_active == -1 ? 'Unlimited' : LicenseInfo.limit_jobs_active }} active jobs 30 |

31 |
32 |
33 |
34 |
35 |
36 | 37 | 38 |
39 |
40 | 41 | 42 |
43 |
44 | 45 |
46 |
47 |
48 |
49 |
50 |  Working... 51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | -------------------------------------------------------------------------------- /wwwroot/html/settings_mail.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | SMTP Mail server 5 |
6 |
7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /wwwroot/html/settings_preset.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | {{ Preset.Header }} 6 | 7 | (This action will affect associated jobs!) 8 | 9 |
10 |
11 |
12 |
13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 |   Working... 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 |
41 |
42 | 43 | 44 | 45 |
46 |
47 |
48 | 49 | 50 |
51 |
52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /wwwroot/html/settings_server.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | {{ Server.Header }} 6 | (This action will affect associated jobs!) 7 |  Working... 8 |
9 |
10 |
11 |
12 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 |
37 |
38 |
39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 |
48 |
49 | 56 |
57 |
58 | 59 | 60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | -------------------------------------------------------------------------------- /wwwroot/html/settings_user.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
{{ User.Header }}
5 |
6 |
7 |
8 | 10 |
11 | 12 | 13 | 14 | 15 | 16 |   Working... 17 | 18 | 19 | 20 | 21 |
22 |
23 |
24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 | 32 |
33 |
34 | 38 |
39 |
40 | 41 | 42 |
43 |
44 |
45 |
46 |
47 | -------------------------------------------------------------------------------- /wwwroot/images/logo_medialink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/images/logo_medialink.png -------------------------------------------------------------------------------- /wwwroot/images/logo_pipencoder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/images/logo_pipencoder.png -------------------------------------------------------------------------------- /wwwroot/images/ss_failover_pipencoder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/images/ss_failover_pipencoder.png -------------------------------------------------------------------------------- /wwwroot/images/ss_failover_uencode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/images/ss_failover_uencode.png -------------------------------------------------------------------------------- /wwwroot/images/ss_failover_umedialink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/images/ss_failover_umedialink.png -------------------------------------------------------------------------------- /wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MediaLink 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /wwwroot/js/angular/ngclipboard.min.js: -------------------------------------------------------------------------------- 1 | /*! ngclipboard - v1.1.1 - 2016-02-26 2 | * https://github.com/sachinchoolur/ngclipboard 3 | * Copyright (c) 2016 Sachin; Licensed MIT */ 4 | !function(){"use strict";var a,b,c="ngclipboard";"object"==typeof module&&module.exports?(a=require("angular"),b=require("clipboard"),module.exports=c):(a=window.angular,b=window.Clipboard),a.module(c,[]).directive("ngclipboard",function(){return{restrict:"A",scope:{ngclipboardSuccess:"&",ngclipboardError:"&"},link:function(a,c){var d=new b(c[0]);d.on("success",function(b){a.$apply(function(){a.ngclipboardSuccess({e:b})})}),d.on("error",function(b){a.$apply(function(){a.ngclipboardError({e:b})})})}}})}(); -------------------------------------------------------------------------------- /wwwroot/js/app/app.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var app = angular.module('MainApp', [ 'ngRoute', 'ui.bootstrap', 'ngclipboard' ]); 4 | 5 | 6 | app.run(function ($rootScope, $log, $location, LSM) { 7 | 8 | // Handle route change 9 | $rootScope.$on('$routeChangeStart', function (event, next) { 10 | if ($rootScope.CurrentUser) { 11 | if (!next.$$route.originalPath.includes('login')) { 12 | // Save last visited route to localStorage 13 | LSM.Save({ LastRoute: next.$$route.originalPath }); 14 | }; 15 | }; 16 | }); 17 | 18 | }); 19 | 20 | 21 | app.controller('AppController', function ($log, $rootScope, $scope, Auth, Alerts, Filters, LSM, Node) { 22 | 23 | // GUI defaults 24 | $scope.GUIDefaults = { 25 | Navbar: { 26 | StatGPUSelected: 0, 27 | StatCollapse: false 28 | }, 29 | Jobs: { 30 | Layout: { 31 | Mode: 'List', 32 | List: { Grid: [ 3, 1 ], Index: 0, IndexMax: 1, IndexPrev: 0 }, 33 | Monitor: { Grid: [ 12, 6, 4, 3, 2 ], Index: 2, IndexMax: 4, IndexPrev: 0 } 34 | }, 35 | Filters: { 36 | Default: { field: 'sid', value: null, type: 'or' }, 37 | // Active: [ { field: 'sid', value: null, type: 'or' } ], 38 | List: [ { field: 'sid', value: null, type: 'or' } ], 39 | Quick: { 'OK': false, 'ERR': false, 'UPD': false, 'OFF': false }, 40 | Show: false 41 | }, 42 | SortBy: 'sid', 43 | SortByOrder: 'asc', 44 | PerPage: 12, 45 | ActPage: 1 46 | } 47 | }; 48 | 49 | // Global app alerts 50 | $scope.AF = Alerts; 51 | // Global app statistics 52 | $rootScope.Stats = { Update: {} }; 53 | // License info 54 | $rootScope.LicenseInfo = null; 55 | 56 | $rootScope.CurrentUser = {}; 57 | 58 | // TODO: Use factory filters 59 | //$scope.FF = Filters; 60 | //$scope.FF.Init($rootScope.GUI.Filters); 61 | 62 | // App init for logged user 63 | Auth.GetLoggedUser() 64 | .then( 65 | function(success) { 66 | // Check node activation 67 | Node.Service('activate').then(function(success) {}, function(error) {}); 68 | // Load GUI from localStogare. or GUI defaults 69 | $rootScope.GUI = LSM.Load('GUI') || $scope.GUIDefaults; 70 | $log.debug('AppController GUI loaded:', $rootScope.GUI); 71 | }, 72 | function(error) { 73 | // handle 401, redirect to login form 74 | } 75 | ); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /wwwroot/js/app/auth.js: -------------------------------------------------------------------------------- 1 | 2 | app.controller('AuthController', function ($rootScope, $scope, $log, $http, $location, Auth, Alerts, LSM, Node) { 3 | 4 | // Auth defaults 5 | $scope.AuthForm = { 6 | Data: { Username: null, Password: null }, 7 | Submit: false 8 | }; 9 | 10 | 11 | $scope.Login = function(AuthForm) { 12 | AuthForm.Submit = true; 13 | var AuthData = { 14 | username: AuthForm.Data.Username, 15 | password: AuthForm.Data.Password 16 | } 17 | Auth.Login(AuthData) 18 | .then( 19 | function(success) { 20 | $log.debug('Login: CurrentUser', $rootScope.CurrentUser); 21 | // Check node activation 22 | Node.Service('activate').then(function(success) {}, function(error) {}); 23 | // Load GUI from localStogare. or GUI defaults 24 | $rootScope.GUI = LSM.Load('GUI') || $scope.GUIDefaults; 25 | $log.debug('Login GUI Loaded:', $rootScope.GUI); 26 | AuthForm.Submit = false; 27 | // Redirect to last visited route from localStorage or default route 28 | $location.path(LSM.Load('LastRoute') || '/jobs'); 29 | }, 30 | function(error) { 31 | AuthForm.Submit = false; 32 | error.status == 502 ? error.statusText = 'Main service is down (HTTP 502)' : null; 33 | Alerts.Add('auth', 'danger', error.statusText); 34 | } 35 | ); 36 | }; 37 | 38 | 39 | $scope.Logout = function() { 40 | Auth.Logout() 41 | .then( 42 | function(success) { 43 | // Redirect to login 44 | $location.path('/login'); 45 | }, 46 | function(error) { 47 | Alerts.Add('auth', 'danger', error.statusText); 48 | } 49 | ); 50 | } 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /wwwroot/js/app/config.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | app.config(['$routeProvider', '$locationProvider', '$logProvider', '$httpProvider', '$injector', 4 | function($routeProvider, $locationProvider, $logProvider, $httpProvider, $injector) { 5 | 6 | $routeProvider. 7 | when('/', { redirectTo: '/jobs' }). 8 | when('/login', { templateUrl: 'html/login.html', controller: 'AuthController' }). 9 | when('/logout', { controller: 'AuthController' }). 10 | when('/jobs', { templateUrl: 'html/job_list.html', controller: 'JobsController' }). 11 | when('/events', { templateUrl: 'html/events.html', controller: 'EventsController' }). 12 | when('/settings', { templateUrl: 'html/settings.html', controller: 'SettingsController' }). 13 | otherwise({ redirectTo: '/jobs' }); 14 | 15 | $httpProvider.interceptors.push([ 16 | '$injector', function ($injector) { return $injector.get('Interceptors'); } 17 | ]); 18 | 19 | $locationProvider.html5Mode(true); 20 | $logProvider.debugEnabled(true); 21 | } 22 | ]); 23 | 24 | -------------------------------------------------------------------------------- /wwwroot/js/app/events.js: -------------------------------------------------------------------------------- 1 | 2 | app.controller('EventsController', function($rootScope, $scope, $log, $http, $interval, Alerts, Events) { 3 | 4 | var Polls = { 5 | Events: { Checker: null, Timer: 3000 } 6 | }; 7 | 8 | $scope.ContentLoaded = false; 9 | $scope.Chapters = 'Events'; 10 | 11 | 12 | $scope.SystemUpdateApply = function() { 13 | $scope.StopPolls(); 14 | $rootScope.Stats.Update.Submit = true; 15 | $http.post('/api/v1/tools/update_apply') 16 | .then( 17 | function(success) { 18 | $log.debug('SystemUpdateApply response: ', success.data); 19 | Events.Minus(1); 20 | $scope.EventsCheck(); 21 | $rootScope.Stats.Update.new_version = success.data.new_version; 22 | Alerts.Add('events', 'success', 'System Update: ' + success.statusText); 23 | }, 24 | function(error) { 25 | $log.error('SystemUpdateApply response: ', error); 26 | Alerts.Add('events', 'danger', 'System Update: ' + error.statusText); 27 | }) 28 | .finally(function() { 29 | $scope.StartPolls(); 30 | $rootScope.Stats.Update.Submit = false; 31 | }); 32 | } 33 | 34 | 35 | $scope.EventsCheck = function() { 36 | $scope.EventsCounter = Events.Get() 37 | $scope.ContentLoaded = true; 38 | // $rootScope.LicenseInfo ? $scope.ContentLoaded = true : null; 39 | } 40 | 41 | 42 | $scope.StartPolls = function() { 43 | $scope.StopPolls(); 44 | Polls.Events.Checker = $interval(function() { 45 | if ($rootScope.CurrentUser.Name) { 46 | $scope.EventsCheck(); 47 | } 48 | }, Polls.Events.Timer); 49 | $log.debug('Events poll started'); 50 | } 51 | 52 | 53 | $scope.StopPolls = function() { 54 | $interval.cancel(Polls.Events.Checker); 55 | $log.debug('Events poll terminated!'); 56 | } 57 | 58 | 59 | $scope.$on('$destroy', function() { 60 | $scope.StopPolls(); 61 | }); 62 | 63 | 64 | // First load controller init 65 | $scope.ControllerInit = function() { 66 | if ($rootScope.CurrentUser.Name) { 67 | // Init event checker 68 | $scope.EventsCheck(); 69 | } 70 | } 71 | 72 | $scope.ControllerInit(); 73 | $scope.StartPolls(); 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /wwwroot/js/app/filters.js: -------------------------------------------------------------------------------- 1 | 2 | app.filter('secondsToDateTime', function() { 3 | return function(seconds) { 4 | return new Date(1970, 0, 1).setSeconds(seconds); 5 | }; 6 | }); 7 | -------------------------------------------------------------------------------- /wwwroot/js/app/nav.js: -------------------------------------------------------------------------------- 1 | 2 | app.controller('NavController', function($rootScope, $scope, $log, $http, $location, $interval, AppUpdate, Events, Stats, LSM) { 3 | 4 | var Polls = { 5 | System: { Checker: null, Timer: 2000 }, 6 | Update: { Checker: null, Timer: 60000 } 7 | }; 8 | 9 | 10 | $scope.SideNavActive = function(page) { 11 | var currentRoute = $location.path().substring(1) || 'jobs'; 12 | return page === currentRoute ? 'active' : ''; 13 | } 14 | 15 | 16 | // Server stats collapse 17 | $scope.ServerStatShow = function() { 18 | $rootScope.GUI.Navbar.StatCollapse = !$rootScope.GUI.Navbar.StatCollapse 19 | LSM.Save({ GUI: $rootScope.GUI }); 20 | } 21 | 22 | 23 | // GPU stats selected 24 | $scope.SelectGPU = function(StatGPUSelected) { 25 | LSM.Save({ GUI: $rootScope.GUI }); 26 | } 27 | 28 | 29 | $scope.UpdateCheck = function() { 30 | AppUpdate.Check().then(function(data) {}, function(error) {}); 31 | } 32 | 33 | 34 | $scope.StatsGet = function() { 35 | Stats.Get() 36 | .then( 37 | function(success) { 38 | var hardware = angular.copy(success.data.hardware); 39 | //$log.debug(hardware); 40 | Object.keys(hardware).forEach(function(device_name) { 41 | var device_data = hardware[device_name]; 42 | if (device_name == 'gpu') { 43 | var gpu_data = device_data.dev_data; 44 | var gpu_counter = device_data.dev_count; 45 | if (gpu_counter && gpu_data) { 46 | gpu_data.forEach(function(gpu) { 47 | if (gpu_counter > 1 && gpu.idx > 0) { 48 | gpu.dev_name += ' (' + (gpu.idx - 1).toString() + ')'; 49 | } 50 | if (!gpu.active) { 51 | Object.keys(gpu.dev_opt).forEach(function(dev_opt) { 52 | gpu.dev_opt[dev_opt] = 101; 53 | }); 54 | } 55 | }); 56 | }; 57 | } else { 58 | if (!device_data && device_data != 0) { device_data = 101; }; 59 | }; 60 | }); 61 | $rootScope.Stats.Hardware = hardware; 62 | $log.debug('Stats hardware:', $rootScope.Stats.Hardware); 63 | // GPU stats only 64 | $rootScope.Stats.GPUData = hardware.gpu.dev_data; 65 | }, 66 | function(error) {} 67 | ); 68 | } 69 | 70 | 71 | // Stat bar color mapping 72 | $scope.StatsColor = function(value) { 73 | var color = 'off'; 74 | if (value >= 0 && value <= 60) { color = 'success'; } else 75 | if (value > 60 && value <= 85) { color = 'warning'; } else 76 | if (value > 85 && value <= 100) { color = 'danger'; } 77 | return color; 78 | } 79 | 80 | 81 | // Start poll(s) 82 | $scope.StartPolls = function() { 83 | $scope.StopPolls(); 84 | // System info check 85 | Polls.System.Checker = $interval(function() { 86 | if ($rootScope.CurrentUser) { 87 | $scope.StatsGet(); 88 | $scope.EventsCounter = Events.Get(); 89 | } 90 | }, Polls.System.Timer); 91 | 92 | // System update check 93 | Polls.Update.Checker = $interval(function() { 94 | if ($rootScope.CurrentUser.Name) { 95 | $scope.UpdateCheck(); 96 | $scope.EventsCounter = Events.Get(); 97 | } 98 | }, Polls.Update.Timer); 99 | 100 | $log.debug('Navbar poll started'); 101 | } 102 | 103 | 104 | // Stop poll(s) 105 | $scope.StopPolls = function() { 106 | $interval.cancel(Polls.System.Checker); 107 | $interval.cancel(Polls.Update.Checker); 108 | $log.debug('Navbar poll terminated'); 109 | } 110 | 111 | // Stop main poll on "ng-view" change 112 | $scope.$on('$destroy', function() { 113 | $scope.StopPolls(); 114 | }); 115 | 116 | 117 | // First load controller init 118 | $scope.ControllerInit = function() { 119 | if ($rootScope.CurrentUser.Name) { 120 | // System stats get 121 | $scope.StatsGet(); 122 | // System update check 123 | $scope.UpdateCheck(); 124 | // Get events counter 125 | $scope.EventsCounter = Events.Get(); 126 | } 127 | } 128 | 129 | $scope.ControllerInit(); 130 | $scope.StartPolls(); 131 | 132 | }); 133 | -------------------------------------------------------------------------------- /wwwroot/js/app/player.js: -------------------------------------------------------------------------------- 1 | 2 | app.directive('vjsplayer', function() { 3 | return { 4 | template: '' 5 | }; 6 | }); 7 | 8 | app.controller('PlayerController', function($log, $scope, $http, $uibModalInstance, JobData, Tools) { 9 | 10 | 11 | $scope.ShowPlayer = false; 12 | $scope.tooltipIsOpen = false; 13 | 14 | 15 | function GetDRMTagValue(text) { 16 | var DRMTagValue = null; 17 | var allLines = text.split('\n'); 18 | for (var i = 0; i < allLines.length; i++) { 19 | if (allLines[i].match('EXT-X-KEY')) { 20 | LineContent = allLines[i]; 21 | var allLineTags = LineContent.split(','); 22 | var DRMTagKey = allLineTags[0].split('='); 23 | DRMTagValue = DRMTagKey[1]; 24 | break; 25 | } 26 | } 27 | return DRMTagValue; 28 | } 29 | 30 | 31 | // Get manifest content from URL 32 | $scope.GetManifest = function(manifest) { 33 | $http.get(manifest).then( 34 | function(success) { 35 | var data = success.data; 36 | //$log.debug('JobPreview: GetManifest response: ', data); 37 | if (data) { 38 | $scope.DRMTagValue = GetDRMTagValue(data); 39 | } 40 | $log.debug('JobPreview: GetManifest: DRM ', $scope.DRMTagValue); 41 | }, 42 | function(error) { 43 | $log.error('JobPreview: GetManifest error: ', error); 44 | }); 45 | } 46 | 47 | 48 | $scope.PlaySourceSelect = function(Media) { 49 | if (Media) { 50 | $scope.ShowPlayer = $scope.CheckURLPlayable($scope.SelectedURL); 51 | $log.debug('JobPreview: ShowPlayer ', $scope.ShowPlayer); 52 | if ($scope.ShowPlayer) { 53 | $log.debug('JobPreview: PlaySourceSelect: Media ', Media); 54 | var MIME; 55 | if (Media.stream_type == 'RTMP') { MIME = 'rtmp/mp4'; } 56 | if (Media.stream_type == 'HLS') { 57 | MIME = 'application/x-mpegurl'; 58 | $scope.GetManifest(Media.url); 59 | } 60 | $log.debug('JobPreview: VJSPlayer ', $scope.VJSPlayer); 61 | $scope.VJSPlayer ? null : $scope.VJSPlayer = videojs('thePlayer'); 62 | $scope.VJSPlayer.src({ type: MIME, src: Media.url }); 63 | $scope.VJSPlayer.load(); 64 | $scope.VJSPlayer.play(); 65 | if (MIME == 'rtmp/mp4') { 66 | $scope.VJSPlayer.on('pause', function () { 67 | $scope.VJSPlayer.on('play', function () { 68 | $scope.VJSPlayer.load(); 69 | $scope.VJSPlayer.play(); 70 | //$scope.VJSPlayer.off('play'); 71 | }); 72 | }); 73 | } 74 | } else { $scope.VJSPlayer ? $scope.VJSPlayer.reset() : null } 75 | } else { $scope.VJSPlayer ? $scope.VJSPlayer.reset() : null } 76 | } 77 | 78 | 79 | $scope.ModalClose = function() { 80 | $uibModalInstance.dismiss('cancel'); 81 | } 82 | 83 | 84 | $scope.CheckURLPlayable = function(URL) { 85 | return (URL.stream_type == 'RTMP' || URL.stream_type == 'HLS') ? true : false; 86 | } 87 | 88 | 89 | $uibModalInstance.rendered.then(function() { 90 | // Get previews URL(s) 91 | $scope.PreviewURL = Tools.PreviewURL(JobData, 'list', 'static'); 92 | $log.debug('JobPreview: PreviewURL: ', $scope.PreviewURL); 93 | if ($scope.PreviewURL) { 94 | // Select fisrt preview URL from the list 95 | $scope.SelectedURL = $scope.PreviewURL[0]; 96 | $log.debug('JobPreview: SelectedURL ', $scope.SelectedURL); 97 | $scope.PlaySourceSelect($scope.SelectedURL); 98 | } 99 | }); 100 | 101 | 102 | $uibModalInstance.result 103 | .then(function() {}, function() { $uibModalInstance.close(); }) 104 | .finally(function() { $scope.VJSPlayer ? $scope.VJSPlayer.dispose() : null; }); 105 | }); 106 | 107 | -------------------------------------------------------------------------------- /wwwroot/js/videojs/font/VideoJS.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/js/videojs/font/VideoJS.eot -------------------------------------------------------------------------------- /wwwroot/js/videojs/font/VideoJS.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/js/videojs/font/VideoJS.ttf -------------------------------------------------------------------------------- /wwwroot/js/videojs/font/VideoJS.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/js/videojs/font/VideoJS.woff -------------------------------------------------------------------------------- /wwwroot/js/videojs/video-js.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkashin/pipencoder-base/15a2ef592a46e6d460efab778d2f2aecd5bc434b/wwwroot/js/videojs/video-js.swf --------------------------------------------------------------------------------