out source the api to separate project
104
.gitignore
vendored
@ -1,16 +1,90 @@
|
||||
.ropeproject
|
||||
**temp
|
||||
*.log*
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE / Editor
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
|
||||
# Mac OSX
|
||||
.DS_Store
|
||||
__pycache__/
|
||||
venv/
|
||||
*-orig.*
|
||||
tests/
|
||||
**/ffplayout/static/
|
||||
._*
|
||||
._**
|
||||
*.sqlite3
|
||||
**/migrations/*
|
||||
!**/migrations/__init__.py
|
||||
**/ffplayout/apps/*
|
||||
!**/ffplayout/apps/api_player/
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
||||
|
19
README.md
@ -2,6 +2,9 @@ ffplayout-gui
|
||||
=====
|
||||
|
||||
This web GUI is for managing [ffplayout-engine](https://github.com/ffplayout/ffplayout-engine).
|
||||
|
||||
The frontend depend on [ffplayout-api](https://github.com/ffplayout/ffplayout-api).
|
||||
|
||||
For a better understanding about the functionality, take a look to the screenshots below.
|
||||
|
||||
You can install it on a fresh Debian/CentOS minimal like system with running `./install.sh` as root.
|
||||
@ -16,25 +19,25 @@ After installations you have to setup ssl for your **https** connections.
|
||||
|
||||
## some impressions:
|
||||
#### Login
|
||||

|
||||

|
||||
|
||||
#### Landing Page
|
||||

|
||||

|
||||
|
||||
#### Control Page
|
||||

|
||||

|
||||
|
||||
#### Media Page
|
||||

|
||||

|
||||
|
||||
#### Media Page / Upload
|
||||

|
||||

|
||||
|
||||
#### Message Page
|
||||

|
||||

|
||||
|
||||
#### Logging Page
|
||||

|
||||

|
||||
|
||||
#### Configuration Page / GUI
|
||||

|
||||

|
||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 378 KiB After Width: | Height: | Size: 378 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
@ -1,84 +0,0 @@
|
||||
[{
|
||||
"model": "api_player.guisettings",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"channel": "Channel 1",
|
||||
"player_url": "http://localhost/live/stream.m3u8",
|
||||
"playout_config": "/etc/ffplayout/ffplayout.yml",
|
||||
"net_interface": "lo",
|
||||
"media_disk": "/",
|
||||
"extra_extensions": ".jpg,.jpeg,.png"
|
||||
}
|
||||
}, {
|
||||
"model": "api_player.messengepresets",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "default",
|
||||
"message": "Wellcome to ffplayout!",
|
||||
"x": "(w-text_w)/2",
|
||||
"y": "(h-text_h)/2",
|
||||
"font_size": 24,
|
||||
"font_spacing": 4,
|
||||
"font_color": "#ffffff",
|
||||
"font_alpha": 1.0,
|
||||
"show_box": true,
|
||||
"box_color": "#000000",
|
||||
"box_alpha": 0.8,
|
||||
"border_width": 4,
|
||||
"overall_alpha": "1"
|
||||
}
|
||||
}, {
|
||||
"model": "api_player.messengepresets",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "empty-text",
|
||||
"message": null,
|
||||
"x": null,
|
||||
"y": null,
|
||||
"font_size": 24,
|
||||
"font_spacing": 4,
|
||||
"font_color": "#ffffff",
|
||||
"font_alpha": 1.0,
|
||||
"show_box": false,
|
||||
"box_color": "#000000",
|
||||
"box_alpha": 0.8,
|
||||
"border_width": 0,
|
||||
"overall_alpha": "0"
|
||||
}
|
||||
}, {
|
||||
"model": "api_player.messengepresets",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "bottom-text-fade-in",
|
||||
"message": "The upcoming event will be delayed by a few minutes.",
|
||||
"x": "(w-text_w)/2",
|
||||
"y": "(h-line_h)*0.9",
|
||||
"font_size": 24,
|
||||
"font_spacing": 4,
|
||||
"font_color": "#ffffff",
|
||||
"font_alpha": 1.0,
|
||||
"show_box": true,
|
||||
"box_color": "#000000",
|
||||
"box_alpha": 0.8,
|
||||
"border_width": 4,
|
||||
"overall_alpha": "ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),0,if(lt(t,ld(1)+2),(t-(ld(1)+1))/1,if(lt(t,ld(1)+8),1,if(lt(t,ld(1)+9),(1-(t-(ld(1)+8)))/1,0))))"
|
||||
}
|
||||
}, {
|
||||
"model": "api_player.messengepresets",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "scrolling-text",
|
||||
"message": "We have a very important announcement to make.",
|
||||
"x": "ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),w+4,w-w/12*mod(t-ld(1),12*(w+tw)/w))",
|
||||
"y": "(h-line_h)*0.9",
|
||||
"font_size": 24,
|
||||
"font_spacing": 4,
|
||||
"font_color": "#ffffff",
|
||||
"font_alpha": 1.0,
|
||||
"show_box": true,
|
||||
"box_color": "#000000",
|
||||
"box_alpha": 0.8,
|
||||
"border_width": 4,
|
||||
"overall_alpha": "1"
|
||||
}
|
||||
}]
|
@ -1,15 +0,0 @@
|
||||
### Backend Apps
|
||||
|
||||
If you planing to extend the backend with your own apps (api endpoints),
|
||||
just add your app in folder: **ffplayout/apps**.
|
||||
|
||||
If you planing to us a DB, put a **settings.py** file in your app. With this object:
|
||||
|
||||
```python
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db-name.sqlite3'),
|
||||
}
|
||||
}
|
||||
```
|
@ -1,14 +0,0 @@
|
||||
[Unit]
|
||||
Description=ffplayout api daemon
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/var/www/ffplayout/ffplayout
|
||||
Environment=DJANGO_SETTINGS_MODULE=ffplayout.settings.production
|
||||
ExecStart=/var/www/ffplayout/venv/bin/gunicorn --workers 4 --worker-class=gevent --timeout 10800 --log-level=info --log-file=- --access-logfile=- --bind 127.0.0.1:8001 ffplayout.wsgi:application
|
||||
KillMode=process
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
40
docs/ffplayout-frontend.conf
Normal file
@ -0,0 +1,40 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
server_name ffplayout-frontend.local;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain application/xml text/css application/javascript;
|
||||
gzip_min_length 1000;
|
||||
|
||||
charset utf-8;
|
||||
|
||||
client_max_body_size 7000M; # should be desirable value
|
||||
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
|
||||
location / {
|
||||
if ($http_origin ~ '^https?://(localhost|ffplayout-frontend\.local)') {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
|
||||
}
|
||||
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
root /var/www/ffplayout-frontend/dist/;
|
||||
|
||||
}
|
||||
|
||||
access_log /var/log/nginx/ffplayout-frontend_access.log;
|
||||
error_log /var/log/nginx/ffplayout-frontend_error.log warn;
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
server_name ffplayout.local;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain application/xml text/css application/javascript;
|
||||
gzip_min_length 1000;
|
||||
|
||||
charset utf-8;
|
||||
|
||||
client_max_body_size 7000M; # should be desirable value
|
||||
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
|
||||
location / {
|
||||
if ($http_origin ~ '^https?://(localhost|ffplayout\.local)') {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
|
||||
}
|
||||
|
||||
if ($request_method = OPTIONS ) {
|
||||
add_header 'Access-Control-Max-Age' 1728000;
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
add_header 'Content-Length' 0;
|
||||
return 204;
|
||||
}
|
||||
|
||||
root /var/www/ffplayout/ffplayout/frontend/dist/;
|
||||
|
||||
}
|
||||
|
||||
location ~ ^/(api|admin|auth|api-auth) {
|
||||
if ($http_origin ~ '^https?://(localhost|ffplayout\.local)') {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin" always;
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With' always;
|
||||
}
|
||||
|
||||
add_header Last-Modified $date_gmt;
|
||||
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
|
||||
if_modified_since off;
|
||||
expires off;
|
||||
etag off;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 36000s;
|
||||
proxy_connect_timeout 36000s;
|
||||
proxy_send_timeout 36000s;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
send_timeout 36000s;
|
||||
proxy_no_cache 1;
|
||||
proxy_pass http://127.0.0.1:8001;
|
||||
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
alias /var/www/ffplayout/ffplayout/static/;
|
||||
}
|
||||
|
||||
location /live/ {
|
||||
alias /var/www/srs/live/;
|
||||
}
|
||||
|
||||
access_log /var/log/nginx/ffplayout_access.log;
|
||||
error_log /var/log/nginx/ffplayout_error.log warn;
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
# Manuel Installation Guide
|
||||
|
||||
**We are assuming that the system user `www-data` will run all processes!**
|
||||
|
||||
### API Setup
|
||||
|
||||
##### Preparation
|
||||
- clone repo to `/var/www/ffplayout`
|
||||
- cd in root folder from repo
|
||||
- add virtual environment: `virtualenv -p python3 venv`
|
||||
- run `source ./venv/bin/activate`
|
||||
- install dependencies: `pip install -r requirements-base.txt`
|
||||
- cd in `ffplayout`
|
||||
- generate and copy secret: `python manage.py shell -c 'from django.core.management import utils; print(utils.get_random_secret_key())'`
|
||||
- open **ffplayout/settings/production.py**
|
||||
- past secret key in variable `SECRET_KEY`
|
||||
- set `ALLOWED_HOSTS` with correct URL
|
||||
- set URL in `CORS_ORIGIN_WHITELIST`
|
||||
- migrate database: `python manage.py makemigrations && python manage.py migrate`
|
||||
- collect static files: `python manage.py collectstatic`
|
||||
- add super user to db: `python manage.py createsuperuser`
|
||||
- populate some data to db: `python manage.py loaddata ../docs/db_data.json`
|
||||
- run: `chown www-data. -R /var/www/ffplayout`
|
||||
|
||||
##### System Setup
|
||||
- copy **docs/ffplayout-api.service** from root folder to **/etc/systemd/system/**
|
||||
- enable service and start it: `systemctl enable ffplayout-api.service && systemctl start ffplayout-api.service`
|
||||
- install **nginx**
|
||||
- edit **docs/ffplayout.conf**
|
||||
- set correct IP and `server_name`
|
||||
- add domain `http_origin` test value
|
||||
- add https redirection and SSL if is necessary
|
||||
- copy **docs/ffplayout.conf** to **/etc/nginx/sites-available/**
|
||||
- symlink config: `ln -s /etc/nginx/sites-available/ffplayout.conf /etc/nginx/sites-enabled/`
|
||||
- restart nginx
|
||||
- run `visudo` and add:
|
||||
```
|
||||
www-data ALL = NOPASSWD: /bin/systemctl start ffplayout-engine.service, /bin/systemctl stop ffplayout-engine.service, /bin/systemctl reload ffplayout-engine.service, /bin/systemctl restart ffplayout-engine.service, /bin/systemctl status ffplayout-engine.service, /bin/systemctl is-active ffplayout-engine.service, /bin/journalctl -n 1000 -u ffplayout-engine.service
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
**We need a recent version of npm**
|
||||
|
||||
- go to folder **/var/www/ffplayout/ffplayout/frontend**
|
||||
- install dependencies: `npm install`
|
||||
- create **.env** file:
|
||||
```
|
||||
BASE_URL='http://localhost:3000'
|
||||
API_URL='/'
|
||||
```
|
||||
- in dev mode `API_URL` should be: `http://localhost:8000`
|
||||
- for deactivating progress animation: `DEV=true`
|
||||
- create symlink for the media folder
|
||||
- when your media folder is a subfolder (for example `/opt/ffplayout/media`) create the same folder structure under **static**:
|
||||
- `mkdir -p /var/www/ffplayout/ffplayout/frontend/static/opt/ffplayout`
|
||||
- `ln -s /opt/ffplayout/media /var/www/ffplayout/ffplayout/frontend/static/opt/ffplayout/`
|
||||
- build app: `npm run build`
|
||||
|
||||
Your frontend should be now in **/var/www/ffplayout/ffplayout/frontend/dist** folder, which we are included already in the nginx config. You can serve now the GUI under your domain URL.
|
||||
|
||||
### OS Specific
|
||||
On debian 10 you need to install:
|
||||
|
||||
```
|
||||
apt install -y curl
|
||||
```
|
||||
|
||||
```
|
||||
curl -sL https://deb.nodesource.com/setup_12.x | bash -
|
||||
```
|
||||
|
||||
**For full installation (with ffmpeg/srs):**
|
||||
```
|
||||
apt install -y sudo net-tools git python3-dev build-essential python3-virtualenv nodejs nginx autoconf automake libtool pkg-config yasm cmake curl mercurial git wget gperf mediainfo
|
||||
```
|
@ -1,111 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import requests
|
||||
import urllib3
|
||||
from pymediainfo import MediaInfo
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# argument parsing
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
stdin_parser = ArgumentParser(
|
||||
description='Convert text file to playlist and send it to the API')
|
||||
|
||||
stdin_parser.add_argument(
|
||||
'-u', '--user', help='API user', required=True
|
||||
)
|
||||
|
||||
stdin_parser.add_argument(
|
||||
'-p', '--password', help='API password', required=True
|
||||
)
|
||||
|
||||
stdin_parser.add_argument(
|
||||
'-c', '--channel', help='Channel name'
|
||||
)
|
||||
|
||||
stdin_parser.add_argument(
|
||||
'--url', help='the url from the ffplayout API', required=True
|
||||
)
|
||||
|
||||
stdin_parser.add_argument(
|
||||
'-d', '--date', help='date from target playlist, in YYYY-MM-DD',
|
||||
required=True
|
||||
)
|
||||
|
||||
stdin_parser.add_argument(
|
||||
'-f', '--file', help=('text file with clips, '
|
||||
'paths must match the paths on ffplayout'),
|
||||
required=True
|
||||
)
|
||||
|
||||
stdin_args = stdin_parser.parse_args()
|
||||
|
||||
|
||||
def auth():
|
||||
login = {'username': stdin_args.user,
|
||||
'password': stdin_args.password}
|
||||
|
||||
req = requests.post(
|
||||
'{}/auth/token/'.format(stdin_args.url), data=login)
|
||||
token = req.json()
|
||||
return token['access']
|
||||
|
||||
|
||||
def get_video_duration(clip):
|
||||
"""
|
||||
return video duration from container
|
||||
"""
|
||||
media_info = MediaInfo.parse(clip)
|
||||
duration = 0
|
||||
for track in media_info.tracks:
|
||||
if track.track_type == 'General':
|
||||
try:
|
||||
duration = float(
|
||||
track.to_data()["duration"]) / 1000
|
||||
break
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return duration
|
||||
|
||||
|
||||
def gen_playlist():
|
||||
json_data = {
|
||||
'channel': stdin_args.channel if stdin_args.channel else 'Channel 1',
|
||||
'date': stdin_args.date,
|
||||
'program': []
|
||||
}
|
||||
|
||||
with open(stdin_args.file, 'r') as content:
|
||||
for line in content:
|
||||
src = line.strip().strip('"').strip("'")
|
||||
duration = get_video_duration(src)
|
||||
json_data['program'].append({
|
||||
'in': 0,
|
||||
'out': duration,
|
||||
'duration': duration,
|
||||
'source': src
|
||||
})
|
||||
|
||||
return json_data
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
playlist = gen_playlist()
|
||||
|
||||
req = requests.post(
|
||||
'{}/api/player/playlist/'.format(stdin_args.url),
|
||||
data=json.dumps({'data': playlist}),
|
||||
headers={'Authorization': 'Bearer {}'.format(auth()),
|
||||
'content-type': 'application/json'})
|
||||
|
||||
if req.status_code == 201:
|
||||
print('Save remote playlist from done...')
|
||||
else:
|
||||
print(req.json()['detail'])
|
@ -1,21 +0,0 @@
|
||||
from apps.api_player.models import GuiSettings, MessengePresets
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
class GuiSettingsAdmin(admin.ModelAdmin):
|
||||
|
||||
class Meta:
|
||||
model = GuiSettings
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class MessengePresetsAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
|
||||
class Meta:
|
||||
model = MessengePresets
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
admin.site.register(GuiSettings, GuiSettingsAdmin)
|
||||
admin.site.register(MessengePresets, MessengePresetsAdmin)
|
@ -1,5 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiPlayerConfig(AppConfig):
|
||||
name = 'api_player'
|
@ -1,71 +0,0 @@
|
||||
import psutil
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class GuiSettings(models.Model):
|
||||
"""
|
||||
Here we manage the settings for the web GUI:
|
||||
- Player URL
|
||||
- settings for the statistics
|
||||
"""
|
||||
|
||||
addrs = psutil.net_if_addrs()
|
||||
addrs = [(i, i) for i in addrs.keys()]
|
||||
|
||||
channel = models.CharField(max_length=255, blank=True,
|
||||
default='Channel 1', null=True)
|
||||
player_url = models.CharField(max_length=255, blank=True,
|
||||
default=None, null=True)
|
||||
playout_config = models.CharField(
|
||||
max_length=255,
|
||||
default='/etc/ffplayout/ffplayout.yml')
|
||||
net_interface = models.CharField(
|
||||
max_length=20,
|
||||
choices=addrs,
|
||||
blank=True, default=None, null=True,
|
||||
)
|
||||
media_disk = models.CharField(
|
||||
max_length=255,
|
||||
help_text="should be a mount point, for statistics",
|
||||
blank=True, default=None, null=True)
|
||||
extra_extensions = models.CharField(
|
||||
max_length=255,
|
||||
help_text="file extensions, that are only visible in GUI",
|
||||
blank=True, null=True, default='')
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "guisettings"
|
||||
|
||||
def __str__(self):
|
||||
return str(self.channel)
|
||||
|
||||
|
||||
class MessengePresets(models.Model):
|
||||
name = models.CharField(max_length=255, help_text="the preset name")
|
||||
|
||||
message = models.CharField(
|
||||
max_length=1024, blank=True, null=True, default='')
|
||||
|
||||
x = models.CharField(
|
||||
max_length=512, blank=True, null=True, default='')
|
||||
|
||||
y = models.CharField(
|
||||
max_length=512, blank=True, null=True, default='')
|
||||
|
||||
font_size = models.IntegerField(default=24)
|
||||
font_spacing = models.IntegerField(default=4)
|
||||
font_color = models.CharField(max_length=12, default='#ffffff')
|
||||
font_alpha = models.FloatField(default=1.0)
|
||||
show_box = models.BooleanField(default=True)
|
||||
box_color = models.CharField(max_length=12, default='#000000')
|
||||
box_alpha = models.FloatField(default=0.8)
|
||||
border_width = models.IntegerField(default=4)
|
||||
overall_alpha = models.CharField(
|
||||
max_length=255, blank=True, null=True, default='')
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "messengepresets"
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
@ -1,54 +0,0 @@
|
||||
from apps.api_player.models import GuiSettings, MessengePresets
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
new_password = serializers.CharField(write_only=True, required=False)
|
||||
old_password = serializers.CharField(write_only=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'old_password',
|
||||
'new_password', 'email']
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
print(validated_data)
|
||||
instance.password = validated_data.get('password', instance.password)
|
||||
|
||||
if 'new_password' in validated_data and \
|
||||
'old_password' in validated_data:
|
||||
if not validated_data['new_password']:
|
||||
raise serializers.ValidationError({'new_password': 'not found'})
|
||||
|
||||
if not validated_data['old_password']:
|
||||
raise serializers.ValidationError({'old_password': 'not found'})
|
||||
|
||||
if not instance.check_password(validated_data['old_password']):
|
||||
raise serializers.ValidationError(
|
||||
{'old_password': 'wrong password'})
|
||||
|
||||
if validated_data['new_password'] and \
|
||||
instance.check_password(validated_data['old_password']):
|
||||
# instance.password = validated_data['new_password']
|
||||
instance.set_password(validated_data['new_password'])
|
||||
instance.save()
|
||||
return instance
|
||||
elif 'email' in validated_data:
|
||||
instance.email = validated_data['email']
|
||||
instance.save()
|
||||
return instance
|
||||
return instance
|
||||
|
||||
|
||||
class GuiSettingsSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = GuiSettings
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class MessengerSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = MessengePresets
|
||||
fields = '__all__'
|
@ -1,10 +0,0 @@
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(os.path.join(__file__)))
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -1,26 +0,0 @@
|
||||
from django.urls import include, path, re_path
|
||||
from rest_framework import routers
|
||||
|
||||
from . import views
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'user/users', views.UserViewSet)
|
||||
router.register(r'guisettings', views.GuiSettingsViewSet, 'guisettings')
|
||||
router.register(r'messenger', views.MessengerViewSet, 'messenger')
|
||||
|
||||
app_name = 'api_player'
|
||||
|
||||
urlpatterns = [
|
||||
path('player/', include(router.urls)),
|
||||
path('player/config/', views.Config.as_view()),
|
||||
path('player/log/', views.LogReader.as_view()),
|
||||
path('player/media/', views.Media.as_view()),
|
||||
path('player/media/op/', views.FileOperations.as_view()),
|
||||
re_path(r'^player/media/upload/(?P<filename>[^/]+)$',
|
||||
views.FileUpload.as_view()),
|
||||
path('player/send/message/', views.MessageSender.as_view()),
|
||||
path('player/playlist/', views.Playlist.as_view()),
|
||||
path('player/stats/', views.Statistics.as_view()),
|
||||
path('player/user/current/', views.CurrentUserView.as_view()),
|
||||
path('player/system/', views.SystemCtl.as_view()),
|
||||
]
|
@ -1,332 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from platform import uname
|
||||
from subprocess import PIPE, STDOUT, run
|
||||
from time import sleep
|
||||
|
||||
from pymediainfo import MediaInfo
|
||||
|
||||
import psutil
|
||||
import yaml
|
||||
import zmq
|
||||
from apps.api_player.models import GuiSettings
|
||||
from django.conf import settings
|
||||
from natsort import natsorted
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
def read_yaml():
|
||||
setting = GuiSettings.objects.filter(id=1).values()
|
||||
if setting:
|
||||
config = setting[0]
|
||||
|
||||
if config and os.path.isfile(config['playout_config']):
|
||||
with open(config['playout_config'], 'r') as config_file:
|
||||
return yaml.safe_load(config_file)
|
||||
|
||||
|
||||
def write_yaml(data):
|
||||
config = GuiSettings.objects.filter(id=1).values()[0]
|
||||
|
||||
if os.path.isfile(config['playout_config']):
|
||||
with open(config['playout_config'], 'w') as outfile:
|
||||
yaml.dump(data, outfile, default_flow_style=False,
|
||||
sort_keys=False, indent=4)
|
||||
|
||||
|
||||
def read_json(date):
|
||||
config = read_yaml()['playlist']['path']
|
||||
y, m, d = date.split('-')
|
||||
input = os.path.join(config, y, m, '{}.json'.format(date))
|
||||
if os.path.isfile(input):
|
||||
with open(input, 'r') as playlist:
|
||||
return json.load(playlist)
|
||||
|
||||
|
||||
def write_json(data):
|
||||
config = read_yaml()['playlist']['path']
|
||||
y, m, d = data['date'].split('-')
|
||||
_path = os.path.join(config, y, m)
|
||||
|
||||
if not os.path.isdir(_path):
|
||||
os.makedirs(_path, exist_ok=True)
|
||||
|
||||
output = os.path.join(_path, '{}.json'.format(data['date']))
|
||||
|
||||
if os.path.isfile(output) and data == read_json(data['date']):
|
||||
return Response(
|
||||
{'detail': 'Playlist from {} already exists'.format(data['date'])})
|
||||
|
||||
|
||||
with open(output, "w") as outfile:
|
||||
json.dump(data, outfile, indent=4)
|
||||
|
||||
return Response({'detail': 'Playlist from {} saved'.format(data['date'])})
|
||||
|
||||
|
||||
def read_log(type, _date):
|
||||
config = read_yaml()
|
||||
log_path = config['logging']['log_path']
|
||||
|
||||
if _date == datetime.now().strftime('%Y-%m-%d'):
|
||||
log_file = os.path.join(log_path, '{}.log'.format(type))
|
||||
else:
|
||||
log_file = os.path.join(log_path, '{}.log.{}'.format(type, _date))
|
||||
|
||||
if os.path.isfile(log_file):
|
||||
with open(log_file, 'r') as log:
|
||||
return log.read().strip()
|
||||
|
||||
|
||||
def send_message(data):
|
||||
config = read_yaml()
|
||||
address, port = config['text']['bind_address'].split(':')
|
||||
|
||||
context = zmq.Context(1)
|
||||
client = context.socket(zmq.REQ)
|
||||
client.connect('tcp://{}:{}'.format(address, port))
|
||||
|
||||
poll = zmq.Poller()
|
||||
poll.register(client, zmq.POLLIN)
|
||||
|
||||
request = ''
|
||||
reply_msg = ''
|
||||
|
||||
for key, value in data.items():
|
||||
request += "{}='{}':".format(key, value)
|
||||
|
||||
request = "{} reinit {}".format(
|
||||
settings.DRAW_TEXT_NODE, request.rstrip(':'))
|
||||
|
||||
client.send_string(request)
|
||||
|
||||
socks = dict(poll.poll(settings.REQUEST_TIMEOUT))
|
||||
|
||||
if socks.get(client) == zmq.POLLIN:
|
||||
reply = client.recv()
|
||||
|
||||
if reply and reply.decode() == '0 Success':
|
||||
reply_msg = reply.decode()
|
||||
else:
|
||||
reply_msg = reply.decode()
|
||||
else:
|
||||
reply_msg = 'No response from server'
|
||||
|
||||
client.setsockopt(zmq.LINGER, 0)
|
||||
client.close()
|
||||
poll.unregister(client)
|
||||
|
||||
context.term()
|
||||
return {'Success': reply_msg}
|
||||
|
||||
|
||||
def sizeof_fmt(num, suffix='B'):
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
if abs(num) < 1024.0:
|
||||
return "%3.1f%s%s" % (num, unit, suffix)
|
||||
num /= 1024.0
|
||||
return "%.1f%s%s" % (num, 'Yi', suffix)
|
||||
|
||||
|
||||
class PlayoutService:
|
||||
def __init__(self):
|
||||
self.service = ['ffplayout-engine.service']
|
||||
self.cmd = ['sudo', '/bin/systemctl']
|
||||
self.proc = None
|
||||
|
||||
def run_cmd(self):
|
||||
self.proc = run(self.cmd + self.service, stdout=PIPE, stderr=STDOUT,
|
||||
encoding="utf-8").stdout
|
||||
|
||||
def start(self):
|
||||
self.cmd.append('start')
|
||||
self.run_cmd()
|
||||
|
||||
def stop(self):
|
||||
self.cmd.append('stop')
|
||||
self.run_cmd()
|
||||
|
||||
def reload(self):
|
||||
self.cmd.append('reload')
|
||||
self.run_cmd()
|
||||
|
||||
def restart(self):
|
||||
self.cmd.append('restart')
|
||||
self.run_cmd()
|
||||
|
||||
def status(self):
|
||||
self.cmd.append('is-active')
|
||||
self.run_cmd()
|
||||
|
||||
return self.proc.replace('\n', '')
|
||||
|
||||
def log(self):
|
||||
self.cmd = ['sudo', '/bin/journalctl', '-n', '1000', '-u']
|
||||
|
||||
self.run_cmd()
|
||||
|
||||
return self.proc
|
||||
|
||||
|
||||
class SystemStats:
|
||||
def __init__(self):
|
||||
settings = GuiSettings.objects.filter(id=1).values()
|
||||
self.config = settings[0] if settings else []
|
||||
|
||||
def all(self):
|
||||
if self.config:
|
||||
return {
|
||||
**self.system(),
|
||||
**self.cpu(), **self.ram(), **self.swap(),
|
||||
**self.disk(), **self.net(), **self.net_speed()
|
||||
}
|
||||
|
||||
def system(self):
|
||||
return {
|
||||
'system': uname().system,
|
||||
'node': uname().node,
|
||||
'machine': uname().machine
|
||||
}
|
||||
|
||||
def cpu(self):
|
||||
return {
|
||||
'cpu_usage': psutil.cpu_percent(interval=1),
|
||||
'cpu_load': list(psutil.getloadavg())
|
||||
}
|
||||
|
||||
def ram(self):
|
||||
mem = psutil.virtual_memory()
|
||||
return {
|
||||
'ram_total': [mem.total, sizeof_fmt(mem.total)],
|
||||
'ram_used': [mem.used, sizeof_fmt(mem.used)],
|
||||
'ram_free': [mem.free, sizeof_fmt(mem.free)],
|
||||
'ram_cached': [mem.cached, sizeof_fmt(mem.cached)]
|
||||
}
|
||||
|
||||
def swap(self):
|
||||
swap = psutil.swap_memory()
|
||||
return {
|
||||
'swap_total': [swap.total, sizeof_fmt(swap.total)],
|
||||
'swap_used': [swap.used, sizeof_fmt(swap.used)],
|
||||
'swap_free': [swap.free, sizeof_fmt(swap.free)]
|
||||
}
|
||||
|
||||
def disk(self):
|
||||
if 'media_disk' in self.config and self.config['media_disk']:
|
||||
root = psutil.disk_usage(self.config['media_disk'])
|
||||
return {
|
||||
'disk_total': [root.total, sizeof_fmt(root.total)],
|
||||
'disk_used': [root.used, sizeof_fmt(root.used)],
|
||||
'disk_free': [root.free, sizeof_fmt(root.free)]
|
||||
}
|
||||
|
||||
def net(self):
|
||||
net = psutil.net_io_counters()
|
||||
return {
|
||||
'net_send': [net.bytes_sent, sizeof_fmt(net.bytes_sent)],
|
||||
'net_recv': [net.bytes_recv, sizeof_fmt(net.bytes_recv)],
|
||||
'net_errin': net.errin,
|
||||
'net_errout': net.errout
|
||||
}
|
||||
|
||||
def net_speed(self):
|
||||
net = psutil.net_if_stats()
|
||||
|
||||
if 'net_interface' not in self.config or \
|
||||
not self.config['net_interface']:
|
||||
return
|
||||
|
||||
if self.config['net_interface'] not in net:
|
||||
return {
|
||||
'net_speed_send': 'no network interface set!',
|
||||
'net_speed_recv': 'no network interface set!'
|
||||
}
|
||||
|
||||
net = psutil.net_io_counters(pernic=True)[self.config['net_interface']]
|
||||
|
||||
send_start = net.bytes_sent
|
||||
recv_start = net.bytes_recv
|
||||
|
||||
sleep(1)
|
||||
|
||||
net = psutil.net_io_counters(pernic=True)[self.config['net_interface']]
|
||||
|
||||
send_end = net.bytes_sent
|
||||
recv_end = net.bytes_recv
|
||||
|
||||
send_sec = send_end - send_start
|
||||
recv_sec = recv_end - recv_start
|
||||
|
||||
return {
|
||||
'net_speed_send': [send_sec, sizeof_fmt(send_sec)],
|
||||
'net_speed_recv': [recv_sec, sizeof_fmt(recv_sec)]
|
||||
}
|
||||
|
||||
|
||||
def get_video_duration(clip):
|
||||
"""
|
||||
return video duration from container
|
||||
"""
|
||||
media_info = MediaInfo.parse(clip)
|
||||
duration = 0
|
||||
for track in media_info.tracks:
|
||||
if track.track_type == 'General':
|
||||
try:
|
||||
duration = float(
|
||||
track.to_data()["duration"]) / 1000
|
||||
break
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return duration
|
||||
|
||||
|
||||
def get_path(input):
|
||||
"""
|
||||
return path and prevent breaking out of media root
|
||||
"""
|
||||
config = read_yaml()
|
||||
media_root_list = config['storage']['path'].strip('/').split('/')
|
||||
media_root_list.pop()
|
||||
media_root = '/' + '/'.join(media_root_list)
|
||||
|
||||
if input:
|
||||
input = os.path.abspath(os.path.join(media_root, input.strip('/')))
|
||||
|
||||
if not input.startswith(config['storage']['path']):
|
||||
input = os.path.join(config['storage']['path'], input.strip('/'))
|
||||
|
||||
return media_root, input
|
||||
|
||||
|
||||
def get_media_path(extensions, _dir=''):
|
||||
config = read_yaml()
|
||||
media_folder = config['storage']['path']
|
||||
extensions = extensions.split(',')
|
||||
playout_extensions = config['storage']['extensions']
|
||||
gui_extensions = [x for x in extensions if x not in playout_extensions]
|
||||
media_root, search_dir = get_path(_dir)
|
||||
|
||||
for root, dirs, files in os.walk(search_dir, topdown=True):
|
||||
root = root.rstrip('/')
|
||||
media_files = []
|
||||
|
||||
for file in files:
|
||||
ext = os.path.splitext(file)[1]
|
||||
if ext in playout_extensions:
|
||||
duration = get_video_duration(os.path.join(root, file))
|
||||
media_files.append({'file': file, 'duration': duration})
|
||||
elif ext in gui_extensions:
|
||||
media_files.append({'file': file, 'duration': ''})
|
||||
|
||||
dirs = natsorted(dirs)
|
||||
|
||||
if root.strip('/') != media_folder.strip('/') or not dirs:
|
||||
dirs.insert(0, '..')
|
||||
|
||||
root = re.sub(r'^{}'.format(media_root), '', root).strip('/')
|
||||
|
||||
return [root, dirs, natsorted(media_files, key=lambda x: x['file'])]
|
@ -1,286 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
from urllib.parse import unquote
|
||||
|
||||
from apps.api_player.models import GuiSettings, MessengePresets
|
||||
from apps.api_player.serializers import (GuiSettingsSerializer,
|
||||
MessengerSerializer, UserSerializer)
|
||||
from django.contrib.auth.models import User
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.parsers import FileUploadParser, JSONParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .utils import (PlayoutService, SystemStats, get_media_path, read_json,
|
||||
read_log, read_yaml, send_message, write_json, write_yaml)
|
||||
|
||||
|
||||
class CurrentUserView(APIView):
|
||||
def get(self, request):
|
||||
serializer = UserSerializer(request.user)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class UserFilter(filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username']
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filterset_class = UserFilter
|
||||
|
||||
|
||||
class GuiSettingsViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint that allows media to be viewed.
|
||||
"""
|
||||
queryset = GuiSettings.objects.all()
|
||||
serializer_class = GuiSettingsSerializer
|
||||
|
||||
|
||||
class MessengerFilter(filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = MessengePresets
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class MessengerViewSet(viewsets.ModelViewSet):
|
||||
queryset = MessengePresets.objects.all()
|
||||
serializer_class = MessengerSerializer
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filterset_class = MessengerFilter
|
||||
|
||||
|
||||
class MessageSender(APIView):
|
||||
"""
|
||||
send messages with zmq to the playout engine
|
||||
"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'data' in request.data:
|
||||
response = send_message(request.data['data'])
|
||||
return Response({"success": True, 'status': response})
|
||||
|
||||
return Response({"success": False})
|
||||
|
||||
|
||||
class Config(APIView):
|
||||
"""
|
||||
read and write config from ffplayout engine
|
||||
for reading endpoint is: http://127.0.0.1:8000/api/player/config/?config
|
||||
"""
|
||||
parser_classes = [JSONParser]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'configPlayout' in request.GET.dict():
|
||||
yaml_input = read_yaml()
|
||||
|
||||
if yaml_input:
|
||||
return Response(yaml_input)
|
||||
else:
|
||||
return Response(status=204)
|
||||
else:
|
||||
return Response(status=404)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'data' in request.data:
|
||||
write_yaml(request.data['data'])
|
||||
return Response(status=200)
|
||||
|
||||
return Response(status=404)
|
||||
|
||||
|
||||
class SystemCtl(APIView):
|
||||
"""
|
||||
controlling the ffplayout-engine systemd services
|
||||
"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'run' in request.data:
|
||||
service = PlayoutService()
|
||||
|
||||
if request.data['run'] == 'start':
|
||||
service.start()
|
||||
return Response(status=200)
|
||||
elif request.data['run'] == 'stop':
|
||||
service.stop()
|
||||
return Response(status=200)
|
||||
elif request.data['run'] == 'reload':
|
||||
service.reload()
|
||||
return Response(status=200)
|
||||
elif request.data['run'] == 'restart':
|
||||
service.restart()
|
||||
return Response(status=200)
|
||||
elif request.data['run'] == 'status':
|
||||
status = service.status()
|
||||
return Response({"data": status})
|
||||
elif request.data['run'] == 'log':
|
||||
log = service.log()
|
||||
return Response({"data": log})
|
||||
else:
|
||||
return Response(status=400)
|
||||
|
||||
return Response(status=404)
|
||||
|
||||
|
||||
class LogReader(APIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'type' in request.GET.dict() and 'date' in request.GET.dict():
|
||||
type = request.GET.dict()['type']
|
||||
_date = request.GET.dict()['date']
|
||||
|
||||
log = read_log(type, _date)
|
||||
|
||||
if log:
|
||||
return Response({'log': log})
|
||||
else:
|
||||
return Response(status=204)
|
||||
else:
|
||||
return Response(status=404)
|
||||
|
||||
|
||||
class Playlist(APIView):
|
||||
"""
|
||||
read and write config from ffplayout engine
|
||||
for reading endpoint:
|
||||
http://127.0.0.1:8000/api/player/playlist/?date=2020-04-12
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'date' in request.GET.dict():
|
||||
date = request.GET.dict()['date']
|
||||
json_input = read_json(date)
|
||||
|
||||
if json_input:
|
||||
return Response(json_input)
|
||||
else:
|
||||
return Response({
|
||||
"success": False,
|
||||
"error": "Playlist from {} not found!".format(date)})
|
||||
else:
|
||||
return Response(status=400)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'data' in request.data:
|
||||
return write_json(request.data['data'])
|
||||
|
||||
return Response({'detail': 'Unspecified save error'})
|
||||
|
||||
|
||||
class Statistics(APIView):
|
||||
"""
|
||||
get system statistics: cpu, ram, etc.
|
||||
for reading, endpoint is: http://127.0.0.1:8000/api/player/stats/?stats=all
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
stats = SystemStats()
|
||||
if 'stats' in request.GET.dict() and request.GET.dict()['stats'] \
|
||||
and hasattr(stats, request.GET.dict()['stats']):
|
||||
return Response(
|
||||
getattr(stats, request.GET.dict()['stats'])())
|
||||
else:
|
||||
return Response(status=404)
|
||||
|
||||
|
||||
class Media(APIView):
|
||||
"""
|
||||
get folder/files tree, for building a file explorer
|
||||
for reading, endpoint is: http://127.0.0.1:8000/api/player/media/?path
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'extensions' in request.GET.dict():
|
||||
extensions = request.GET.dict()['extensions']
|
||||
|
||||
if 'path' in request.GET.dict() and request.GET.dict()['path']:
|
||||
return Response({'tree': get_media_path(
|
||||
extensions, request.GET.dict()['path']
|
||||
)})
|
||||
elif 'path' in request.GET.dict():
|
||||
return Response({'tree': get_media_path(extensions)})
|
||||
else:
|
||||
return Response(status=204)
|
||||
else:
|
||||
return Response(status=404)
|
||||
|
||||
|
||||
class FileUpload(APIView):
|
||||
parser_classes = [FileUploadParser]
|
||||
|
||||
def put(self, request, filename, format=None):
|
||||
root = read_yaml()['storage']['path']
|
||||
file_obj = request.data['file']
|
||||
filename = unquote(filename)
|
||||
path = unquote(request.query_params['path']).split('/')[1:]
|
||||
|
||||
with open(os.path.join(root, *path, filename), 'wb') as outfile:
|
||||
for chunk in file_obj.chunks():
|
||||
outfile.write(chunk)
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class FileOperations(APIView):
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
if 'file' in request.GET.dict() and 'path' in request.GET.dict():
|
||||
root = read_yaml()['storage']['path']
|
||||
_file = unquote(request.GET.dict()['file'])
|
||||
folder = unquote(request.GET.dict()['path']).lstrip('/')
|
||||
_path = os.path.join(*(folder.split(os.path.sep)[1:]))
|
||||
fullPath = os.path.join(root, _path)
|
||||
|
||||
if not _file or _file == 'null':
|
||||
if os.path.isdir(fullPath):
|
||||
shutil.rmtree(fullPath, ignore_errors=True)
|
||||
return Response(status=200)
|
||||
else:
|
||||
return Response(status=404)
|
||||
elif os.path.isfile(os.path.join(fullPath, _file)):
|
||||
os.remove(os.path.join(fullPath, _file))
|
||||
return Response(status=200)
|
||||
else:
|
||||
return Response(status=404)
|
||||
else:
|
||||
return Response(status=404)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'folder' in request.data and 'path' in request.data:
|
||||
root = read_yaml()['storage']['path']
|
||||
folder = request.data['folder']
|
||||
_path = request.data['path'].split(os.path.sep)
|
||||
_path = '' if len(_path) == 1 else os.path.join(*_path[1:])
|
||||
fullPath = os.path.join(root, _path, folder)
|
||||
|
||||
try:
|
||||
# TODO: check if folder exists
|
||||
os.mkdir(fullPath)
|
||||
return Response(status=200)
|
||||
except OSError:
|
||||
Response(status=500)
|
||||
else:
|
||||
return Response(status=404)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
if 'path' in request.data and 'oldname' in request.data \
|
||||
and 'newname' in request.data:
|
||||
root = read_yaml()['storage']['path']
|
||||
old_name = request.data['oldname']
|
||||
new_name = request.data['newname']
|
||||
_path = os.path.join(
|
||||
*(request.data['path'].split(os.path.sep)[2:]))
|
||||
old_file = os.path.join(root, _path, old_name)
|
||||
new_file = os.path.join(root, _path, new_name)
|
||||
|
||||
os.rename(old_file, new_file)
|
||||
|
||||
return Response(status=200)
|
||||
else:
|
||||
return Response(status=204)
|
@ -1,17 +0,0 @@
|
||||
"""
|
||||
ASGI config for ffplayout project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault(
|
||||
'DJANGO_SETTINGS_MODULE', 'ffplayout.settings.development')
|
||||
|
||||
application = get_asgi_application()
|
@ -1,141 +0,0 @@
|
||||
"""
|
||||
Django settings for ffplayout project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.0.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from pydoc import locate
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(
|
||||
os.path.dirname(os.path.abspath(os.path.join(__file__, '..'))))
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django_filters',
|
||||
'rest_framework',
|
||||
'corsheaders'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'ffplayout.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'ffplayout.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
}
|
||||
]
|
||||
|
||||
# simple JWT auth settings
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(hours=7),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
|
||||
}
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# dynamic app loader
|
||||
APPS_DIR = os.path.join(BASE_DIR, 'apps/')
|
||||
|
||||
for dir in os.listdir(APPS_DIR):
|
||||
if os.path.isdir(os.path.join(APPS_DIR, dir)):
|
||||
app_name = 'apps.{}'.format(dir)
|
||||
|
||||
if app_name not in INSTALLED_APPS:
|
||||
# add app to installed apps
|
||||
INSTALLED_APPS += (app_name, )
|
||||
|
||||
if os.path.isfile(os.path.join(APPS_DIR, dir, 'settings.py')):
|
||||
db = locate('{}.settings.DATABASES'.format(app_name))
|
||||
|
||||
for key in db:
|
||||
if key not in DATABASES:
|
||||
# add app db to DATABASES
|
||||
DATABASES.update({key: db[key]})
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
|
||||
|
||||
# ffmpeg filter node, needs to be edit only when the filter chain changes
|
||||
DRAW_TEXT_NODE = 'Parsed_drawtext_2'
|
||||
|
||||
# zmq settings
|
||||
REQUEST_TIMEOUT = 1000
|
@ -1,26 +0,0 @@
|
||||
from ffplayout.settings.common import *
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'dhgfk(gl&16krnt_7*dp(9b3w*ft%nbsg-h2)&ihbte4le#o4f'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
# REST API
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
)
|
||||
}
|
||||
|
||||
CORS_ORIGIN_WHITELIST = (
|
||||
'http://localhost:3000',
|
||||
'http://localhost:8000',
|
||||
'http://ffplayout.local'
|
||||
)
|
@ -1,25 +0,0 @@
|
||||
from ffplayout.settings.common import *
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '---a-very-important-secret-key:-generate-it-new---'
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = ['localhost']
|
||||
|
||||
# REST API
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'DEFAULT_RENDERER_CLASSES': (
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
)
|
||||
}
|
||||
|
||||
CORS_ORIGIN_WHITELIST = (
|
||||
'http://ffplayout.local',
|
||||
)
|
@ -1,44 +0,0 @@
|
||||
"""ffplayout URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
import os
|
||||
from django.contrib import admin
|
||||
from django.conf import settings
|
||||
from django.urls import include, path
|
||||
from rest_framework_simplejwt.views import (TokenObtainPairView,
|
||||
TokenRefreshView)
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api-auth/', include(
|
||||
'rest_framework.urls', namespace='rest_framework')),
|
||||
path('auth/token/', TokenObtainPairView.as_view(),
|
||||
name='token_obtain_pair'),
|
||||
path('auth/token/refresh/', TokenRefreshView.as_view(),
|
||||
name='token_refresh')
|
||||
]
|
||||
|
||||
|
||||
# dynamic url loader
|
||||
for dir in os.listdir(settings.APPS_DIR):
|
||||
if os.path.isdir(os.path.join(settings.APPS_DIR, dir)):
|
||||
app_name = 'apps.{}'.format(dir)
|
||||
|
||||
_path = path('api/', include(
|
||||
'{}.urls'.format(app_name),
|
||||
namespace='{}'.format(app_name.split('.')[1])))
|
||||
|
||||
if _path not in urlpatterns:
|
||||
urlpatterns += (_path, )
|
@ -1,17 +0,0 @@
|
||||
"""
|
||||
WSGI config for ffplayout project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault(
|
||||
'DJANGO_SETTINGS_MODULE', 'ffplayout.settings.development')
|
||||
|
||||
application = get_wsgi_application()
|
90
ffplayout/frontend/.gitignore
vendored
@ -1,90 +0,0 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE / Editor
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
|
||||
# Mac OSX
|
||||
.DS_Store
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE',
|
||||
'ffplayout.settings.development')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
89
install.sh
@ -82,11 +82,19 @@ runInstall() {
|
||||
|
||||
echo ""
|
||||
echo "------------------------------------------------------------------------------"
|
||||
echo "ffplayout gui domain name (like: example.org)"
|
||||
echo "ffplayout api domain name (like: api.example.org)"
|
||||
echo "------------------------------------------------------------------------------"
|
||||
echo ""
|
||||
|
||||
read -p "domain name :$ " domain
|
||||
read -p "api domain name :$ " domainApi
|
||||
|
||||
echo ""
|
||||
echo "------------------------------------------------------------------------------"
|
||||
echo "ffplayout frontend domain name (like: www.example.org)"
|
||||
echo "------------------------------------------------------------------------------"
|
||||
echo ""
|
||||
|
||||
read -p "frontend domain name :$ " domainFrontend
|
||||
fi
|
||||
|
||||
if [[ ! -d /usr/local/srs ]]; then
|
||||
@ -499,15 +507,15 @@ EOF
|
||||
deactivate
|
||||
fi
|
||||
|
||||
if [[ ! -d "/var/www/ffplayout" ]]; then
|
||||
if [[ ! -d "/var/www/ffplayout-api" ]]; then
|
||||
echo ""
|
||||
echo "------------------------------------------------------------------------------"
|
||||
echo "install ffplayout gui"
|
||||
echo "install ffplayout-api"
|
||||
echo "------------------------------------------------------------------------------"
|
||||
|
||||
cd /var/www
|
||||
git clone https://github.com/ffplayout/ffplayout-gui.git ffplayout
|
||||
cd ffplayout
|
||||
git clone https://github.com/ffplayout/ffplayout-api.git
|
||||
cd ffplayout-api
|
||||
|
||||
virtualenv -p python3 venv
|
||||
source ./venv/bin/activate
|
||||
@ -536,36 +544,60 @@ EOF
|
||||
sed -i "s/User=root/User=$serviceUser/g" /etc/systemd/system/ffplayout-api.service
|
||||
sed -i "s/Group=root/Group=$serviceUser/g" /etc/systemd/system/ffplayout-api.service
|
||||
|
||||
sed -i "s/'localhost'/'localhost', \'$domain\'/g" /var/www/ffplayout/ffplayout/ffplayout/settings/production.py
|
||||
sed -i "s/ffplayout\\.local/$domain\'\n \'https\\:\/\/$domain/g" /var/www/ffplayout/ffplayout/ffplayout/settings/production.py
|
||||
sed -i "s/'localhost'/'localhost', \'$domainApi\'/g" /var/www/ffplayout/ffplayout/ffplayout/settings/production.py
|
||||
sed -i "s/ffplayout\\.local/$domainApi\'\n \'https\\:\/\/$domainApi/g" /var/www/ffplayout/ffplayout/ffplayout/settings/production.py
|
||||
|
||||
systemctl enable ffplayout-api.service
|
||||
|
||||
if [[ $installNginx == 'y' ]]; then
|
||||
cp docs/ffplayout.conf "$nginxConfig/"
|
||||
cp docs/ffplayout-api.conf "$nginxConfig/"
|
||||
|
||||
origin=$(echo "$domain" | sed 's/\./\\\\./g')
|
||||
origin=$(echo "$domainApi" | sed 's/\./\\\\./g')
|
||||
|
||||
sed -i "s/ffplayout.local/$domain/g" $nginxConfig/ffplayout.conf
|
||||
sed -i "s/ffplayout\\\.local/$origin/g" $nginxConfig/ffplayout.conf
|
||||
sed -i "s/ffplayout-api.local/$domainApi/g" $nginxConfig/ffplayout-api.conf
|
||||
sed -i "s/ffplayout-api\\\.local/$origin/g" $nginxConfig/ffplayout-api.conf
|
||||
|
||||
if [[ "$(grep -Ei 'debian|buntu|mint' /etc/*release)" ]]; then
|
||||
ln -s $nginxConfig/ffplayout.conf /etc/nginx/sites-enabled/
|
||||
ln -s $nginxConfig/ffplayout-api.conf /etc/nginx/sites-enabled/
|
||||
fi
|
||||
fi
|
||||
|
||||
cd /var/www/ffplayout/ffplayout/frontend
|
||||
ln -s "$mediaPath" /var/www/ffplayout/ffplayout/frontend/static/
|
||||
systemctl start ffplayout-api.service
|
||||
fi
|
||||
|
||||
if [[ ! -d "/var/www/ffplayout-frontend" ]]; then
|
||||
echo ""
|
||||
echo "------------------------------------------------------------------------------"
|
||||
echo "install ffplayout-frontend"
|
||||
echo "------------------------------------------------------------------------------"
|
||||
|
||||
cd /var/www
|
||||
git clone https://github.com/ffplayout/ffplayout-frontend.git
|
||||
cd ffplayout-frontend
|
||||
|
||||
ln -s "$mediaPath" /var/www/ffplayout-frontend/static/
|
||||
|
||||
sudo -H -u $serviceUser bash -c 'npm install'
|
||||
|
||||
cat <<EOF > ".env"
|
||||
BASE_URL='http://$domain'
|
||||
BASE_URL='http://$domainFrontend'
|
||||
API_URL='/'
|
||||
EOF
|
||||
|
||||
sudo -H -u $serviceUser bash -c 'npm run build'
|
||||
systemctl start ffplayout-api.service
|
||||
|
||||
if [[ $installNginx == 'y' ]]; then
|
||||
cp docs/ffplayout-frontend.conf "$nginxConfig/"
|
||||
|
||||
origin=$(echo "$domainFrontend" | sed 's/\./\\\\./g')
|
||||
|
||||
sed -i "s/ffplayout-frontend.local/$domainFrontend/g" $nginxConfig/ffplayout-frontend.conf
|
||||
sed -i "s/ffplayout-frontend\\\.local/$origin/g" $nginxConfig/ffplayout-frontend.conf
|
||||
|
||||
if [[ "$(grep -Ei 'debian|buntu|mint' /etc/*release)" ]]; then
|
||||
ln -s $nginxConfig/ffplayout-frontend.conf /etc/nginx/sites-enabled/
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $installNginx == 'y' ]]; then
|
||||
@ -615,15 +647,28 @@ runUpdate() {
|
||||
echo "------------------------------------------------------------------------------"
|
||||
fi
|
||||
|
||||
if [[ -d "/var/www/ffplayout" ]]; then
|
||||
cd "/var/www/ffplayout"
|
||||
if [[ -d "/var/www/ffplayout-api" ]]; then
|
||||
cd "/var/www/ffplayout-api"
|
||||
git pull
|
||||
|
||||
source ./venv/bin/activate
|
||||
pip install --upgrade -r requirements-base.txt
|
||||
deactivate
|
||||
|
||||
cd "ffplayout/frontend"
|
||||
echo ""
|
||||
echo "------------------------------------------------------------------------------"
|
||||
echo "updating ffplayout-api done..."
|
||||
echo "------------------------------------------------------------------------------"
|
||||
else
|
||||
echo ""
|
||||
echo "------------------------------------------------------------------------------"
|
||||
echo "WARNING: no ffplayout-api found..."
|
||||
echo "------------------------------------------------------------------------------"
|
||||
fi
|
||||
|
||||
if [[ -d "/var/www/ffplayout-frontend" ]]; then
|
||||
cd "/var/www/ffplayout-frontend"
|
||||
git pull
|
||||
|
||||
rm -rf node_modules
|
||||
sudo -H -u $serviceUser bash -c 'npm install'
|
||||
@ -631,12 +676,12 @@ runUpdate() {
|
||||
|
||||
echo ""
|
||||
echo "------------------------------------------------------------------------------"
|
||||
echo "updating ffplayout-gui done..."
|
||||
echo "updating ffplayout-frontend done..."
|
||||
echo "------------------------------------------------------------------------------"
|
||||
else
|
||||
echo ""
|
||||
echo "------------------------------------------------------------------------------"
|
||||
echo "WARNING: no ffplayout-gui found..."
|
||||
echo "WARNING: no ffplayout-frontend found..."
|
||||
echo "------------------------------------------------------------------------------"
|
||||
fi
|
||||
|
||||
|
@ -1,12 +0,0 @@
|
||||
Django<=3.1
|
||||
django-filter
|
||||
django-cors-headers
|
||||
djangorestframework
|
||||
djangorestframework-simplejwt
|
||||
gevent
|
||||
gunicorn
|
||||
natsort
|
||||
psutil
|
||||
pymediainfo
|
||||
pyyaml
|
||||
zmq
|
@ -1,42 +0,0 @@
|
||||
asgiref==3.2.10
|
||||
astroid==2.4.2
|
||||
autopep8==1.5.3
|
||||
Django==3.1
|
||||
django-cors-headers==3.4.0
|
||||
django-filter==2.3.0
|
||||
djangorestframework==3.11.1
|
||||
djangorestframework-simplejwt==4.4.0
|
||||
flake8==3.8.3
|
||||
gevent==20.6.2
|
||||
greenlet==0.4.16
|
||||
gunicorn==20.0.4
|
||||
isort==4.3.21
|
||||
jedi==0.17.2
|
||||
lazy-object-proxy==1.4.3
|
||||
mccabe==0.6.1
|
||||
natsort==7.0.1
|
||||
parso==0.7.1
|
||||
pluggy==0.13.1
|
||||
psutil==5.7.2
|
||||
pycodestyle==2.6.0
|
||||
pydocstyle==5.0.2
|
||||
pyflakes==2.2.0
|
||||
PyJWT==1.7.1
|
||||
pylint==2.5.3
|
||||
pymediainfo==4.2.1
|
||||
python-jsonrpc-server==0.3.4
|
||||
python-language-server==0.34.1
|
||||
pytz==2020.1
|
||||
PyYAML==5.3.1
|
||||
pyzmq==19.0.1
|
||||
rope==0.17.0
|
||||
six==1.15.0
|
||||
snowballstemmer==2.0.0
|
||||
sqlparse==0.3.1
|
||||
toml==0.10.1
|
||||
ujson==1.35
|
||||
wrapt==1.12.1
|
||||
yapf==0.30.0
|
||||
zmq==0.0.0
|
||||
zope.event==4.4
|
||||
zope.interface==5.1.0
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |