1
0
Fork 0
mirror of https://github.com/fastogt/fastocloud_admin.git synced 2025-03-09 23:38:52 +00:00

Subscribers (#4)

* Subscribers start impl

* Send subscribers to service

* Add subscribers

* Review

* User agent
This commit is contained in:
Alexandr Topilski 2019-06-11 07:16:46 +03:00 committed by GitHub
parent 197173dd66
commit df32bb51a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 388 additions and 28 deletions

View file

@ -1,3 +1,8 @@
1.2.0 /
[Alexandr Topilski]
- User agents
- Subscribers
1.1.0 / June 6, 2019
[Alexandr Topilski]
- User type

View file

@ -44,6 +44,7 @@ class Client:
VODS_IN_DIRECTORY = 'vods_in_directory'
VODS_DIRECTORY = 'vods_directory'
STREAMS = 'streams'
SUBSCRIBERS = 'subscribers'
STREAM_ID = 'id'
LICENSE_KEY = 'license_key'
PATH = 'path'
@ -126,8 +127,8 @@ class Client:
self._send_request(command_id, Commands.PREPARE_SERVICE_COMMAND, command_args)
@is_active_decorator
def sync_service(self, command_id: int, streams: list):
command_args = {Client.STREAMS: streams}
def sync_service(self, command_id: int, streams: list, subscribers: list):
command_args = {Client.STREAMS: streams, Client.SUBSCRIBERS: subscribers}
self._send_request(command_id, Commands.SYNC_SERVICE_COMMAND, command_args)
@is_active_decorator

View file

@ -19,7 +19,7 @@ class Url(EmbeddedDocument):
class InputUrl(Url):
pass
user_agent = IntField(default=constants.UserAgent.GSTREAMER, required=True)
class OutputUrl(Url):

View file

@ -1,6 +1,6 @@
from wtforms import Form
from flask_babel import lazy_gettext
from wtforms.fields import StringField, FieldList, IntegerField, FormField, FloatField
from wtforms.fields import StringField, FieldList, IntegerField, FormField, FloatField, SelectField
from wtforms.validators import InputRequired, Length, NumberRange
import app.constants as constants
@ -16,7 +16,9 @@ class UrlForm(Form):
class InputUrlForm(UrlForm):
pass
user_agent = SelectField(lazy_gettext(u'User agent:'),
validators=[InputRequired()],
choices=constants.AVAILABLE_USER_AGENTS, coerce=constants.UserAgent.coerce)
class InputUrlsForm(Form):
@ -25,7 +27,7 @@ class InputUrlsForm(Form):
def get_data(self) -> InputUrls:
urls = InputUrls()
for url in self.data['urls']:
urls.urls.append(InputUrl(url['id'], url['uri']))
urls.urls.append(InputUrl(url['id'], url['uri'], url['user_agent']))
return urls

View file

@ -3,4 +3,4 @@ PUBLIC_CONFIG = {'site': {'title': 'FastoCloud', 'keywords': 'video,cloud,iptv,s
'support': {'contact_email': 'support@fastogt.com',
'contact_address': 'Republic of Belarus, Minsk, Stadionnay str. 5',
'community_channel': 'https://discord.gg/cnUXsws'},
'project': {'version': '1.1.0', 'version_type': 'release'}}
'project': {'version': '1.2.0', 'version_type': 'release'}}

View file

@ -195,3 +195,22 @@ class Roles(IntEnum):
def __str__(self):
return str(self.value)
class UserAgent(IntEnum):
GSTREAMER = 0
VLC = 1
@classmethod
def choices(cls):
return [(choice, choice.name) for choice in cls]
@classmethod
def coerce(cls, item):
return cls(int(item)) if not isinstance(item, cls) else item
def __str__(self):
return str(self.value)
AVAILABLE_USER_AGENTS = [(UserAgent.GSTREAMER, 'GStreamer'), (UserAgent.VLC, 'VLC'), ]

View file

@ -13,6 +13,8 @@ class ServiceSettingsForm(FlaskForm):
host = FormField(HostAndPortForm, lazy_gettext(u'Host:'), validators=[])
http_host = FormField(HostAndPortForm, lazy_gettext(u'Http host:'), validators=[])
vods_host = FormField(HostAndPortForm, lazy_gettext(u'Vods host:'), validators=[])
subscribers_host = FormField(HostAndPortForm, lazy_gettext(u'Subscribers host:'), validators=[])
bandwidth_host = FormField(HostAndPortForm, lazy_gettext(u'Bandwidth host:'), validators=[])
feedback_directory = StringField(lazy_gettext(u'Feedback directory:'), validators=[InputRequired()])
timeshifts_directory = StringField(lazy_gettext(u'Timeshifts directory:'), validators=[InputRequired()])
@ -32,6 +34,8 @@ class ServiceSettingsForm(FlaskForm):
settings.host = self.host.get_data()
settings.http_host = self.http_host.get_data()
settings.vods_host = self.vods_host.get_data()
settings.subscribers_host = self.subscribers_host.get_data()
settings.bandwidth_host = self.bandwidth_host.get_data()
settings.feedback_directory = self.feedback_directory.data
settings.timeshifts_directory = self.timeshifts_directory.data

View file

@ -24,6 +24,10 @@ class ServerSettings:
DEFAULT_SERVICE_HTTP_PORT = 8000
DEFAULT_SERVICE_VODS_HOST = 'localhost'
DEFAULT_SERVICE_VODS_PORT = 7000
DEFAULT_SERVICE_SUBSCRIBERS_HOST = 'localhost'
DEFAULT_SERVICE_SUBSCRIBERS_PORT = 6000
DEFAULT_SERVICE_BANDWIDTH_HOST = 'localhost'
DEFAULT_SERVICE_BANDWIDTH_PORT = 5000
name = StringField(unique=True, default=DEFAULT_SERVICE_NAME, max_length=MAX_SERVICE_NAME_LENGTH,
min_length=MIN_SERVICE_NAME_LENGTH)
@ -32,6 +36,10 @@ class ServerSettings:
port=DEFAULT_SERVICE_HTTP_PORT))
vods_host = EmbeddedDocumentField(HostAndPort, default=HostAndPort(host=DEFAULT_SERVICE_VODS_HOST,
port=DEFAULT_SERVICE_VODS_PORT))
subscribers_host = EmbeddedDocumentField(HostAndPort, default=HostAndPort(host=DEFAULT_SERVICE_SUBSCRIBERS_HOST,
port=DEFAULT_SERVICE_SUBSCRIBERS_PORT))
bandwidth_host = EmbeddedDocumentField(HostAndPort, default=HostAndPort(host=DEFAULT_SERVICE_BANDWIDTH_HOST,
port=DEFAULT_SERVICE_BANDWIDTH_PORT))
feedback_directory = StringField(default=DEFAULT_FEEDBACK_DIR_PATH)
timeshifts_directory = StringField(default=DEFAULT_TIMESHIFTS_DIR_PATH)

View file

@ -54,7 +54,7 @@ class Service(IStreamHandler):
self._settings = settings
self.__reload_from_db()
# other fields
self._client = ServiceClient(self, settings)
self._client = ServiceClient(settings.id, self, settings)
self._host = host
self._port = port
self._socketio = socketio
@ -69,7 +69,7 @@ class Service(IStreamHandler):
return self._client.stop_service(delay)
def get_log_service(self):
return self._client.get_log_service(self._host, self._port, self.id)
return self._client.get_log_service(self._host, self._port)
def ping(self):
return self._client.ping_service()

View file

@ -1,3 +1,5 @@
from bson.objectid import ObjectId
from app.client.client import Client
from app.client.client_handler import IClientHandler
from app.client.json_rpc import Request, Response
@ -11,6 +13,8 @@ import app.constants as constants
class ServiceClient(IClientHandler):
HTTP_HOST = 'http_host'
VODS_HOST = 'vods_host'
SUBSCRIBERS_HOST = 'subscribers_host'
BANDWIDTH_HOST = 'bandwidth_host'
VERSION = 'version'
@staticmethod
@ -25,7 +29,8 @@ class ServiceClient(IClientHandler):
def get_pipeline_stream_path(host: str, port: int, stream_id: str):
return constants.DEFAULT_STREAM_PIPELINE_PATH_TEMPLATE_3SIS.format(host, port, stream_id)
def __init__(self, handler: IStreamHandler, settings: ServiceSettings):
def __init__(self, sid: ObjectId, handler: IStreamHandler, settings: ServiceSettings):
self.id = sid
self._request_id = 0
self._handler = handler
self._service_settings = settings
@ -50,8 +55,9 @@ class ServiceClient(IClientHandler):
def stop_service(self, delay: int):
return self._client.stop_service(self._gen_request_id(), delay)
def get_log_service(self, host: str, port: int, sid: str):
return self._client.get_log_service(self._gen_request_id(), ServiceClient.get_log_service_path(host, port, sid))
def get_log_service(self, host: str, port: int):
return self._client.get_log_service(self._gen_request_id(),
ServiceClient.get_log_service_path(host, port, str(self.id)))
def start_stream(self, config: dict):
return self._client.start_stream(self._gen_request_id(), config)
@ -74,7 +80,12 @@ class ServiceClient(IClientHandler):
streams = []
for stream in self._service_settings.streams:
streams.append(stream.config())
return self._client.sync_service(self._gen_request_id(), streams)
subscribers = []
for subs in self._service_settings.subscribers:
subscribers.append(subs.to_service(self.id))
return self._client.sync_service(self._gen_request_id(), streams, subscribers)
def get_http_host(self) -> str:
return self._http_host
@ -82,6 +93,12 @@ class ServiceClient(IClientHandler):
def get_vods_host(self) -> str:
return self._vods_host
def get_subscribers_host(self) -> str:
return self._subscribers_host
def get_bandwidth_host(self) -> str:
return self._bandwidth_host
def get_vods_in(self) -> list:
return self._vods_in
@ -98,7 +115,9 @@ class ServiceClient(IClientHandler):
self.sync_service()
if self._handler:
self._set_runtime_fields(resp.result[ServiceClient.HTTP_HOST],
resp.result[ServiceClient.VODS_HOST], resp.result[ServiceClient.VERSION])
resp.result[ServiceClient.VODS_HOST], resp.result[ServiceClient.VODS_HOST],
resp.result[ServiceClient.SUBSCRIBERS_HOST],
resp.result[ServiceClient.BANDWIDTH_HOST])
self._handler.on_service_statistic_received(resp.result)
if req.method == Commands.PREPARE_SERVICE_COMMAND and resp.is_message():
@ -127,9 +146,13 @@ class ServiceClient(IClientHandler):
self._handler.on_client_state_changed(status)
# private
def _set_runtime_fields(self, http_host=None, vods_host=None, version=None, vods_in=None):
def _set_runtime_fields(self, http_host=None, vods_host=None, subscribers_host=None, bandwidth_host=None,
version=None,
vods_in=None):
self._http_host = http_host
self._vods_host = vods_host
self._subscribers_host = subscribers_host
self._bandwidth_host = bandwidth_host
self._version = version
self._vods_in = vods_in

View file

@ -20,6 +20,7 @@ class ServiceSettings(Document, ServerSettings):
streams = ListField(EmbeddedDocumentField(Stream), default=[])
users = ListField(EmbeddedDocumentField(UserPair), default=[])
subscribers = ListField(ReferenceField('Subscriber'), default=[])
def generate_playlist(self) -> str:
result = '#EXTM3U\n'
@ -47,4 +48,23 @@ class ServiceSettings(Document, ServerSettings):
for user in self.users:
if user.id == uid:
self.users.remove(user)
break
self.save()
def add_subscriber(self, sid):
self.subscribers.append(sid)
self.save()
def remove_subscriber(self, sid):
for subscriber in self.subscribers:
if subscriber == sid:
self.subscribers.remove(subscriber)
break
self.save()
def find_stream_settings_by_id(self, sid):
for stream in self.streams:
if stream.id == sid:
return stream
return None

View file

@ -6,9 +6,11 @@ from flask_login import login_required, current_user
from app import get_runtime_folder
from app.service.forms import ServiceSettingsForm, ActivateForm, UploadM3uForm, UserServerForm
from app.subscriber.forms import SubscriberForm
from app.service.service_entry import ServiceSettings, UserPair
from app.utils.m3u_parser import M3uParser
from app.home.user_loging_manager import User
from app.subscriber.subscriber_entry import Subscriber
import app.constants as constants
@ -157,11 +159,25 @@ class ServiceView(FlaskView):
return render_template('service/user/add.html', form=form)
@login_required
@route('/subscriber/add/<sid>', methods=['GET', 'POST'])
def subscriber_add(self, sid):
form = SubscriberForm(obj=Subscriber())
if request.method == 'POST' and form.validate_on_submit():
server = ServiceSettings.objects(id=sid).first()
if server:
new_entry = form.make_entry()
new_entry.add_server(server)
server.add_subscriber(new_entry)
return jsonify(status='ok'), 200
return render_template('service/subscriber/add.html', form=form)
@login_required
@route('/add', methods=['GET', 'POST'])
def add(self):
model = ServiceSettings()
form = ServiceSettingsForm(obj=model)
form = ServiceSettingsForm(obj=ServiceSettings())
if request.method == 'POST' and form.validate_on_submit():
new_entry = form.make_entry()
admin = UserPair(current_user.id, constants.Roles.ADMIN)

View file

@ -65,6 +65,46 @@ class StreamFields:
TIMESTAMP = 'timestamp'
class EpgInfo:
ID_FIELD = 'id'
URL_FIELD = 'url'
TITLE_FIELD = 'display_name'
ICON_FIELD = 'icon'
PROGRAMS_FIELD = 'programs'
id = str
url = str
title = str
icon = str
programs = []
def __init__(self, eid: str, url: str, title: str, icon: str, programs=list()):
self.id = eid
self.url = url
self.title = title
self.icon = icon
self.programs = programs
def to_dict(self) -> dict:
return {EpgInfo.ID_FIELD: self.id, EpgInfo.URL_FIELD: self.url, EpgInfo.TITLE_FIELD: self.title,
EpgInfo.ICON_FIELD: self.icon, EpgInfo.PROGRAMS_FIELD: self.programs}
class ChannelInfo:
EPG_FIELD = 'epg'
VIDEO_ENABLE_FIELD = 'video'
AUDIO_ENABLE_FIELD = 'audio'
def __init__(self, epg: EpgInfo, have_video=True, have_audio=True):
self.have_video = have_video
self.have_audio = have_audio
self.epg = epg
def to_dict(self) -> dict:
return {ChannelInfo.EPG_FIELD: self.epg.to_dict(), ChannelInfo.VIDEO_ENABLE_FIELD: self.have_video,
ChannelInfo.AUDIO_ENABLE_FIELD: self.have_audio}
class Stream(EmbeddedDocument):
meta = {'allow_inheritance': True, 'auto_create_index': True}
@ -159,6 +199,13 @@ class Stream(EmbeddedDocument):
conf[AUDIO_SELECT_FIELD] = audio_select
return conf
def to_channel_info(self) -> [ChannelInfo]:
ch = []
for out in self.output.urls:
epg = EpgInfo(self.get_id(), out.uri, self.name, self.icon)
ch.append(ChannelInfo(epg))
return ch
def generate_feedback_dir(self):
return '{0}/{1}/{2}'.format(self._settings.feedback_directory, self.get_type(), self.get_id())

26
app/subscriber/forms.py Normal file
View file

@ -0,0 +1,26 @@
from flask_wtf import FlaskForm
from flask_babel import lazy_gettext
from wtforms.fields import StringField, PasswordField, SubmitField, SelectField
from wtforms.validators import InputRequired, Length, Email
from app.subscriber.subscriber_entry import Subscriber
class SubscriberForm(FlaskForm):
AVAILABLE_COUNTRIES = [('UK', 'United kingdom'), ('RU', 'Russian'), ('BY', 'Belarus')]
email = StringField(lazy_gettext(u'Email:'),
validators=[InputRequired(), Email(message=lazy_gettext(u'Invalid email')), Length(max=30)])
password = PasswordField(lazy_gettext(u'Password:'), validators=[InputRequired(), Length(min=4, max=80)])
country = SelectField(lazy_gettext(u'Locale:'), coerce=str, validators=[InputRequired()],
choices=AVAILABLE_COUNTRIES)
apply = SubmitField(lazy_gettext(u'Apply'))
def make_entry(self):
return self.update_entry(Subscriber())
def update_entry(self, subscriber: Subscriber):
subscriber.email = self.email.data
subscriber.password = Subscriber.make_md5_hash_from_password(self.password.data)
subscriber.country = self.country.data
return subscriber

View file

@ -0,0 +1,105 @@
from datetime import datetime
from hashlib import md5
from bson.objectid import ObjectId
from enum import IntEnum
from mongoengine import Document, EmbeddedDocument, StringField, DateTimeField, IntField, ListField, ReferenceField, \
PULL, ObjectIdField, EmbeddedDocumentField
from app.service.service_entry import ServiceSettings
class Device(EmbeddedDocument):
DEFAULT_DEVICE_NAME = 'Device'
MIN_DEVICE_NAME_LENGTH = 3
MAX_DEVICE_NAME_LENGTH = 32
meta = {'allow_inheritance': False, 'auto_create_index': True}
id = ObjectIdField(required=True, default=ObjectId, unique=True, primary_key=True)
created_date = DateTimeField(default=datetime.now)
name = StringField(default=DEFAULT_DEVICE_NAME, min_length=MIN_DEVICE_NAME_LENGTH,
max_length=MAX_DEVICE_NAME_LENGTH,
required=True)
class Subscriber(Document):
ID_FIELD = "id"
EMAIL_FIELD = "login"
PASSWORD_FIELD = "password"
STATUS_FIELD = "status"
DEVICES_FIELD = "devices"
STREAMS_FIELD = "channels"
class Status(IntEnum):
NO_ACTIVE = 0
ACTIVE = 1
BANNED = 2
class Type(IntEnum):
USER = 0,
SUPPORT = 1
SUBSCRIBER_HASH_LENGHT = 32
meta = {'collection': 'subscribers', 'auto_create_index': False}
email = StringField(max_length=30, required=True)
password = StringField(min_length=SUBSCRIBER_HASH_LENGHT, max_length=SUBSCRIBER_HASH_LENGHT, required=True)
created_date = DateTimeField(default=datetime.now)
status = IntField(default=Status.ACTIVE)
type = IntField(default=Type.USER)
country = StringField(min_length=2, max_length=3, required=True)
servers = ListField(ReferenceField(ServiceSettings, reverse_delete_rule=PULL), default=[])
devices = ListField(EmbeddedDocumentField(Device), default=[])
streams = ListField(ObjectIdField(), default=[])
def add_server(self, server: ServiceSettings):
self.servers.append(server)
self.save()
def add_stream(self, stream):
self.streams.append(stream.id)
self.save()
def remove_stream(self, sid: ObjectId):
for stream in self.streams:
if stream == sid:
self.streams.remove(stream)
break
self.save()
def to_service(self, sid: ObjectId) -> dict:
for serv in self.servers:
if serv.id == sid:
devices = []
for dev in self.devices:
devices.append(str(dev.id))
streams = []
for stream in self.streams:
founded_stream = serv.find_stream_settings_by_id(stream)
if founded_stream:
channels = founded_stream.to_channel_info()
for ch in channels:
streams.append(ch.to_dict())
conf = {
Subscriber.ID_FIELD: str(self.id), Subscriber.EMAIL_FIELD: self.email,
Subscriber.PASSWORD_FIELD: self.password, Subscriber.STATUS_FIELD: self.status,
Subscriber.DEVICES_FIELD: devices, Subscriber.STREAMS_FIELD: streams}
return conf
return {}
@staticmethod
def make_md5_hash_from_password(password: str) -> str:
m = md5()
m.update(password.encode())
return m.hexdigest()
@classmethod
def md5_user(cls, email: str, password: str, country: str):
return cls(email=email, password=Subscriber.make_md5_hash_from_password(password), country=country)
Subscriber.register_delete_rule(ServiceSettings, "subscribers", PULL)

View file

@ -38,6 +38,10 @@
<br>
{{ render_bootstrap_form(form.vods_host) }}
<br>
{{ render_bootstrap_form(form.subscribers_host) }}
<br>
{{ render_bootstrap_form(form.bandwidth_host) }}
<br>
{{ render_bootstrap_field(form.feedback_directory) }}
<br>
{{ render_bootstrap_field(form.timeshifts_directory) }}

View file

@ -0,0 +1,4 @@
{% extends 'service/subscriber/base.html' %}
{% block title %}
Add subscriber to server
{% endblock %}

View file

@ -0,0 +1,45 @@
{% from 'bootstrap/wtf.html' import form_field %}
{% macro render_bootstrap_field(field) %}
<div class="row">
<label class="col-md-4">{{ field.label }}</label>
<div class="col-md-8">
{{ field(class='form-control')|safe }}
</div>
</div>
{% endmacro %}
{% macro render_bootstrap_form(form) %}
<div class="row">
<label class="col-md-4">{{ form.label }}</label>
<div class="col-md-8">
{{ form() }}
</div>
</div>
{% endmacro %}
<form id="subscriber_entry_form" name="subscriber_entry_form" class="form" method="post">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">
{% block title %}
{% endblock %}
</h4>
</div>
<div class="modal-body">
{{ form.hidden_tag() }}
<br>
{{ render_bootstrap_field(form.email) }}
<br>
{{ render_bootstrap_field(form.password) }}
<br>
{{ render_bootstrap_field(form.country) }}
</div>
<div class="modal-footer">
{% block footer %}
<button type="button" class="btn btn-danger" data-dismiss="modal">{% trans %}Cancel{% endtrans %}</button>
{{ form_field(form.apply, class="btn btn-success") }}
{% endblock %}
</div>
</form>

View file

@ -0,0 +1,4 @@
{% extends 'service/subscriber/base.html' %}
{% block title %}
Edit subscriber
{% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'service/base.html' %}
{% extends 'service/user/base.html' %}
{% block title %}
Edit service
{% endblock %}

View file

@ -588,11 +588,12 @@ Dashboard | {{ config['PUBLIC_CONFIG'].site.title }}
}
function edit_stream_entry(url) {
var data_ser = $('#stream_entry_form').serialize();
$.ajax({
url: url,
type: "POST",
dataType: 'json',
data: $('#stream_entry_form').serialize(),
data: data_ser,
success: function (response) {
console.log(response);
$('#stream_dialog').modal('hide');
@ -808,12 +809,5 @@ Dashboard | {{ config['PUBLIC_CONFIG'].site.title }}
}
});
}
</script>
{% endblock %}

View file

@ -74,6 +74,10 @@ Settings | {{ config['PUBLIC_CONFIG'].site.title }}
onclick="add_user_to_server('{{ server.id }}')">
{% trans %}Add user{% endtrans %}
</button>
<button type="submit" class="btn btn-success btn-xs"
onclick="add_subscriber_to_server('{{ server.id }}')">
{% trans %}Add subscriber{% endtrans %}
</button>
<button type="submit" class="btn btn-danger btn-xs"
onclick="remove_server('{{ server.id }}')">
{% trans %}Remove{% endtrans %}
@ -155,6 +159,37 @@ Settings | {{ config['PUBLIC_CONFIG'].site.title }}
}
function add_subscriber_to_server_entry(url) {
$.ajax({
url: url,
type: "POST",
dataType: 'json',
data: $('#subscriber_entry_form').serialize(),
success: function (response) {
console.log(response);
$('#service_dialog').modal('hide');
window.location.reload();
},
error: function (error) {
console.error(error);
$('#service_dialog .modal-content').html(data);
}
});
}
function add_subscriber_to_server(sid) {
var url = "/service/subscriber/add/" + sid;
$.get(url, function(data) {
$('#service_dialog .modal-content').html(data);
$('#service_dialog').modal();
$('#apply').click(function(event) {
event.preventDefault();
add_subscriber_to_server_entry(url);
})
});
}
function add_server() {
var url = "{{ url_for('ServiceView:add') }}";
$.get(url, function(data) {
@ -215,7 +250,5 @@ Settings | {{ config['PUBLIC_CONFIG'].site.title }}
});
}
</script>
{% endblock %}