├── .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 | Time |
51 | Event |
52 | ID |
53 | Job name |
54 | Info |
55 | User |
56 |
57 | {% endif %}
58 |
59 | {% for i in info.report | sort(attribute = 'time') %}
60 | {% if i.id %}
61 |
62 | {{ i.time }} |
63 | {{ i.event }} |
64 | {{ i.id }} / {{ i.sid }} |
65 | {{ i.name }} |
66 | {{ i.info }} |
67 | {{ i.user }} |
68 |
69 | {% endif %}
70 | {% endfor %}
71 |
72 |
73 |
74 | {% if vars.sys_events %}
75 | System events
76 |
77 |
78 | Time |
79 | Event |
80 | Info |
81 | User |
82 |
83 | {% endif %}
84 |
85 | {% for i in info.report | sort(attribute = 'time') %}
86 | {% if i.id is not defined %}
87 |
88 | {{ i.time }} |
89 | {{ i.event }} |
90 | {{ i.info }} |
91 | {{ i.user }} |
92 |
93 | {% endif %}
94 | {% endfor %}
95 |
96 |
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 | Time |
38 | ID |
39 | Job name |
40 | Source |
41 | Target |
42 | Status |
43 |
44 |
45 | {% for i in info.report | sort(attribute = 'check_time') %}
46 |
47 | {{ i.check_time }} |
48 | {{ i.id }} / {{ i.sid }} |
49 | {{ i.name }} |
50 | {{ i.source + ' (' + i.source_active + ')' }} |
51 | {{ i.target }} |
52 | {{ i.run_status }} |
53 |
54 | {% endfor %}
55 |
56 |
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 |
10 |
11 |
--------------------------------------------------------------------------------
/wwwroot/html/events.html:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
21 |
22 |
23 |
Please wait
24 |
25 |
26 |
27 |
28 |
29 |
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 |
7 |
--------------------------------------------------------------------------------
/wwwroot/html/job_edit.html:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 |
38 |
39 |
40 |
41 |
47 |
48 |
49 |
50 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
No HLS assets are available
64 |
65 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/wwwroot/html/job_edit_info.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
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 |
7 |
--------------------------------------------------------------------------------
/wwwroot/html/log.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Events:
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
43 |
44 |
45 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/wwwroot/html/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |

5 |
11 |
12 |
29 |
{{ Alert.msg }}
30 |
31 |
32 |
--------------------------------------------------------------------------------
/wwwroot/html/player.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
Selected media has no preview feature
13 |
14 |
15 |
30 |
31 |
--------------------------------------------------------------------------------
/wwwroot/html/settings.html:
--------------------------------------------------------------------------------
1 |
2 |
39 |
40 |
41 |
42 |
Loading content
43 |
Working...
44 |
45 |
46 |
Access denied for current user
47 |
48 |
49 |
50 |
Please wait
51 |
52 |
53 |
54 |
55 |
60 |
65 |
70 |
75 |
98 |
99 |
--------------------------------------------------------------------------------
/wwwroot/html/settings_alarm.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
31 |
51 |
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 |
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 |
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 |
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 |
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 |
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 |
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
--------------------------------------------------------------------------------