Commit d06c71ac authored by Deamos's avatar Deamos

Merge branch 'development' into 'master'

Beta 1 Release

See merge request !45
parents 8221668c 0af92b11
......@@ -61,6 +61,8 @@ RUN mkdir /var/www && \
mkdir /var/www/live && \
mkdir /var/www/videos && \
mkdir /var/www/live-rec && \
mkdir /var/www/live-adapt && \
mkdir /var/www/stream-thumb && \
mkdir /var/www/images && \
mkdir /var/log/gunicorn && \
chown -R www-data:www-data /var/www && \
......
......@@ -6,7 +6,7 @@
OSP was designed a self-hosted alternative to services like Twitch.tv, Ustream.tv, and Youtube Live.
**OSP is still considered Alpha and is not complete**
**OSP is still considered Beta and is not complete**
## Features:
- RTMP Streaming from an input source like Open Broadcast Software (OBS).
......@@ -14,10 +14,15 @@ OSP was designed a self-hosted alternative to services like Twitch.tv, Ustream.t
- Video Stream Recording and On-Demand Playback. [![N|Solid](https://i.imgur.com/4RV5IXH.jpg)](https://i.imgur.com/4RV5IXH.jpg)
- Per Channel Real-Time Chat for Video Streams. [![N|Solid](https://i.imgur.com/c598KLa.jpg)](https://i.imgur.com/c598KLa.jpg)
- Real-Time Chat Moderation by Channel Owners (Banning/Unbanning)
- Admin Controlled Adaptive Streaming
- Protected Streams to allow access only to the audience you want.
- Live Channels - Keep chatting and hang out when a stream isn't on
- Webhooks - Connect OSP to other services via fully customizable HTTP requests which will pass information
- Embed your stream or video directly into another web page easily
- Share channels or videos via Facebook or Twitter quickly
## Planned Features:
- Subscribe to a Channel and Get Notified on When a New Stream Starts.
- Password Protected Channels & Live Streams
## Tech
......@@ -31,15 +36,25 @@ Open Streaming Platform uses a number of open source projects to work properly:
* [Flask Uploads] - Manage User Uploads, such as Pictures
* [Flask-RestPlus] - Handling and Documentation of the OSP API
* [Bootstrap] - For Building responsive, mobile-first projects on the web
* [Bootstrap-Toggle] - Used to Build Toggle Buttons with Bootstrap
* [NGINX] - Open-Source, high-performance HTTP server and reverse proxy
* [NGINX-RTMP-Module] - NGINX Module for RTMP/HLS/MPEG-DASH live streaming
* [Socket.io] - Real-Time Communications Engine Between Client and Server
* [Flask Socket.io] - Interface Socket.io with Flask
* [Video.js] - Handles the HTML5 Video Playback of HLS video streams and MP4 Files
* [Font Awesome] - Interface Icons and Such
* [[Animista](http://animista.net/)] - Awesome CSS Animation Generator
And OSP itself is open source with a [public repository](https://gitlab.com/Deamos/flask-nginx-rtmp-manager) on Gitlab.
## Git Branches
OSP's Git Branches are setup in the following configuration
* **master** - Current Release Branch
* **release/(Version)** - Previous Official Releases
* **development** - Current Nightly Branch for OSP vNext
* **feature/(Name)** - In-progress Feature Builds to be merged with the Development Branch
## Installation
### Standard Install
......@@ -136,10 +151,18 @@ sudo git pull
```
sudo chown -R www-data:www-data /opt/osp
```
* Restart the OSP Service
* Run the DB Upgrade Script to Ensure the Database Schema is up-to-date
```
sudo service osp restart
bash dbUpgrade.sh
```
### Upgrading from Alpha4 to Beta1
With the move to Beta1, to support channel protections it is recommended to replace your nginx.conf file with the new configuration to support this feature. To do so, run the beta1upgrade.sh script to ensure all settings and directories are created.
```
cd /opt/osp/setup/other
sudo bash alpha4tobeta1.sh
```
After completion, your original nginx.conf file will be renamed to nginx.conf.old and you make adjustments to the new file.
### Upgrading from Alpha3 to Alpha4
Due to the changes from Python 2 to Python 3, You need to run a script to remove Python 2.7 and its modules and replace them with Python 3
......@@ -166,6 +189,127 @@ sudo service osp restart
### Chat Comands
- /ban <username> - Bans a user from chatting in a chat room
- /unban <username> - Unbans a user who has been banned
- /mute - Places a Chat Channel on Mute
- /unmute - Removes a Mute Placed on a Chat Channel
### Webhooks
Webhooks allow you to send a notification out to other services using a GET/POST/PUT/DELETE request which can be defined on a per channel basis depending on various triggers.
OSP currently supports the following triggers:
- At the Start of a Live Stream
- At the End of a Live Stream
- On a New Viewer Joining a Live Stream
- On a Stream Upvote
- On a Stream Metadata Change (Name/Topic)
- On a Stream Chat Message
- On Posting of a New Video to a Channel
- On a New Video Comment
- On a Video Upvote
- On a Video Metadata Change (Name/Topic)
When defining your webhook payload, you can use variables which will be replaced with live data at the time the webhook is run. Webhook variables are defined as the following:
- %channelname%
- %channelurl%
- %channeltopic%
- %channelimage%
- %streamer%
- %channeldescription%
- %streamname%
- %streamurl%
- %streamtopic%
- %streamimage%
- %user%
- %userpicture%
- %videoname%
- %videodate%
- %videodescription%
- %videotopic%
- %videourl%
- %videothumbnail%
- %comment%
Example Webhook for Discord:
- Type: **POST**
- Trigger Event: **Stream Start**
Header
```json
{
"Content-Type": "application/json"
}
```
Payload
```json
{
"content": "%channelname% went live on OSP Test",
"username": "OSP Bot",
"embeds": [
{
"title": "%streamurl%",
"url": "https://%streamurl%",
"color": 6570404,
"image": {
"url": "https://%channelimage%"
},
"author": {
"name": "%streamer% is now streaming"
},
"fields": [
{
"name": "Channel",
"value": "%channelname%",
"inline": true
},
{
"name": "Topic",
"value": "%streamtopic%",
"inline": true
},
{
"name": "Stream Name",
"value": "%streamname%",
"inline": true
},
{
"name": "Description",
"value": "%channeldescription%",
"inline": true
}
]
}
]
}
```
### Adaptive Streaming
While OSP now supports the ability to transcode to an adaptive stream for lower bandwidth users, this feature will use considerable CPU processing poweer and may slow down your OSP instance. It is recommended only to use this feature when there is either few streams occurring or if your server has sufficient resources to handle the ability to transcode multiple streams.
By default, NGINX-RTMP has only been configured to transcode 1080p, 720p, 480p, & 360p. You can optimize how streams are transcoded by editing the /usr/local/nginx/conf/nginx.conf file and following the instructions at https://licson.net/post/setting-up-adaptive-streaming-with-nginx/
### Theming
OSP Supports Custom HTML and CSS theming via creation of another directory under the /opt/osp/templates/themes directory.
Custom CSS can be created under the /opt/osp/static/css directory under the name $ThemeName.css.
When theming, all html files must be used. Use the Default Theme as a template to build your own theme.
Themes also must contain a theme.json file to work properly with OSP.
theme.json:
```json
{
"Name": "Example",
"Maintainer": "Some User",
"Version": "1.0",
"Description": "Description of Theme"
}
```
Thanks
----
Special thanks to the folks of the [OSP Discord channel](https://discord.gg/Jp5rzbD) for their testing and suggestions!
License
----
......
......@@ -14,6 +14,7 @@ from classes import RecordedVideo
from classes import topics
from classes import upvotes
from classes import apikey
from classes import views
from classes.shared import db
......@@ -32,10 +33,12 @@ api = Api(api_v1, version='1.0', title='OSP API', description='OSP API for Users
channelParserPut = reqparse.RequestParser()
channelParserPut.add_argument('channelName', type=str)
channelParserPut.add_argument('description', type=str)
channelParserPut.add_argument('topicID', type=int)
channelParserPost = reqparse.RequestParser()
channelParserPost.add_argument('channelName', type=str, required=True)
channelParserPost.add_argument('description', type=str, required=True)
channelParserPost.add_argument('topicID', type=int, required=True)
channelParserPost.add_argument('recordEnabled', type=bool, required=True)
channelParserPost.add_argument('chatEnabled', type=bool, required=True)
......@@ -56,6 +59,7 @@ class api_1_ListChannels(Resource):
Gets a List of all Public Channels
"""
channelList = Channel.Channel.query.all()
db.session.commit()
return {'results': [ob.serialize() for ob in channelList]}
# Channel - Create Channel
@api.expect(channelParserPost)
......@@ -70,7 +74,7 @@ class api_1_ListChannels(Resource):
if requestAPIKey != None:
if requestAPIKey.isValid():
args = channelParserPost.parse_args()
newChannel = Channel.Channel(int(requestAPIKey.userID), str(uuid.uuid4()), args['channelName'], int(args['topicID']), args['recordEnabled'], args['chatEnabled'])
newChannel = Channel.Channel(int(requestAPIKey.userID), str(uuid.uuid4()), args['channelName'], int(args['topicID']), args['recordEnabled'], args['chatEnabled'],args['description'])
db.session.add(newChannel)
db.session.commit()
......@@ -85,6 +89,7 @@ class api_1_ListChannel(Resource):
Get Info for One Channel
"""
channelList = Channel.Channel.query.filter_by(channelLoc=channelEndpointID).all()
db.session.commit()
return {'results': [ob.serialize() for ob in channelList]}
# Channel - Change Channel Name or Topic ID
@api.expect(channelParserPut)
......@@ -104,6 +109,9 @@ class api_1_ListChannel(Resource):
if 'channelName' in args:
if args['channelName'] is not None:
channelQuery.channelName = args['channelName']
if 'description' in args:
if args['description'] is not None:
channelQuery.description = args['channelName']
if 'topicID' in args:
if args['topicID'] is not None:
possibleTopics = topics.topics.query.filter_by(id=int(args['topicID'])).first()
......@@ -129,6 +137,24 @@ class api_1_ListChannel(Resource):
if filePath != '/var/www/videos/':
shutil.rmtree(filePath, ignore_errors=True)
channelVid = channelQuery.recordedVideo
channelUpvotes = channelQuery.upvotes
channelStreams = channelQuery.stream
for entry in channelVid:
for upvote in entry.upvotes:
db.session.delete(upvote)
vidComments = entry.comments
for comment in vidComments:
db.session.delete(comment)
vidViews = views.views.query.filter_by(viewType=1, itemID=entry.id)
for view in vidViews:
db.session.delete(view)
db.session.delete(entry)
for entry in channelUpvotes:
db.session.delete(entry)
for entry in channelStreams:
db.session.delete(entry)
db.session.delete(channelQuery)
db.session.commit()
return {'results': {'message': 'Channel Deleted'}}, 200
......@@ -141,6 +167,7 @@ class api_1_ListStreams(Resource):
Returns a List of All Active Streams
"""
streamList = Stream.Stream.query.all()
db.session.commit()
return {'results': [ob.serialize() for ob in streamList]}
@api.route('/streams/<int:streamID>')
......@@ -151,6 +178,7 @@ class api_1_ListStream(Resource):
Returns Info on a Single Active Streams
"""
streamList = Stream.Stream.query.filter_by(id=streamID).all()
db.session.commit()
return {'results': [ob.serialize() for ob in streamList]}
# Channel - Change Channel Name or Topic ID
......@@ -178,7 +206,7 @@ class api_1_ListStream(Resource):
if possibleTopics != None:
streamQuery.topic = int(args['topicID'])
db.session.commit()
return {'results': {'message': 'Channel Updated'}}, 200
return {'results': {'message': 'Stream Updated'}}, 200
return {'results': {'message': 'Request Error'}}, 400
@api.route('/vids/')
......@@ -188,6 +216,7 @@ class api_1_ListVideos(Resource):
Returns a List of All Recorded Videos
"""
videoList = RecordedVideo.RecordedVideo.query.filter_by(pending=False).all()
db.session.commit()
return {'results': [ob.serialize() for ob in videoList]}
@api.route('/vids/<int:videoID>')
......@@ -198,6 +227,7 @@ class api_1_ListVideo(Resource):
Returns Info on a Single Recorded Video
"""
videoList = RecordedVideo.RecordedVideo.query.filter_by(id=videoID).all()
db.session.commit()
return {'results': [ob.serialize() for ob in videoList]}
@api.expect(videoParserPut)
@api.doc(security='apikey')
......@@ -250,9 +280,15 @@ class api_1_ListVideo(Resource):
upvoteQuery = upvotes.videoUpvotes.query.filter_by(videoID=videoQuery.id).all()
for vote in upvoteQuery:
db.session.delete(vote)
vidComments = videoQuery.comments
for comment in vidComments:
db.session.delete(comment)
vidViews = views.views.query.filter_by(viewType=1, itemID=videoQuery.id)
for view in vidViews:
db.session.delete(view)
db.session.delete(videoQuery)
db.session.commit()
return {'results': {'message': 'Channel Deleted'}}, 200
return {'results': {'message': 'Video Deleted'}}, 200
return {'results': {'message': 'Request Error'}}, 400
@api.route('/topics/')
......@@ -262,6 +298,7 @@ class api_1_ListTopics(Resource):
Returns a List of All Topics
"""
topicList = topics.topics.query.all()
db.session.commit()
return {'results': [ob.serialize() for ob in topicList]}
@api.route('/topics/<int:topicID>')
......@@ -272,4 +309,5 @@ class api_1_ListTopic(Resource):
Returns Info on a Single Topic
"""
topicList = topics.topics.query.filter_by(id=topicID).all()
db.session.commit()
return {'results': [ob.serialize() for ob in topicList]}
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -11,22 +11,42 @@ class Channel(db.Model):
channelLoc = db.Column(db.String(255), unique=True)
topic = db.Column(db.Integer)
views = db.Column(db.Integer)
currentViewers = db.Column(db.Integer)
record = db.Column(db.Boolean)
chatEnabled = db.Column(db.Boolean)
chatBG = db.Column(db.String(255))
chatTextColor = db.Column(db.String(10))
chatAnimation = db.Column(db.String(255))
imageLocation = db.Column(db.String(255))
offlineImageLocation = db.Column(db.String(255))
description = db.Column(db.String(2048))
allowComments = db.Column(db.Boolean)
protected = db.Column(db.Boolean)
channelMuted = db.Column(db.Boolean)
stream = db.relationship('Stream', backref='channel', lazy="joined")
recordedVideo = db.relationship('RecordedVideo', backref='channel', lazy="joined")
upvotes = db.relationship('channelUpvotes', backref='stream', lazy="joined")
inviteCodes = db.relationship('inviteCode', backref='channel', lazy="joined")
invitedViewers = db.relationship('invitedViewer', backref='channel', lazy="joined")
webhooks = db.relationship('webhook', backref='channel', lazy="joined")
def __init__(self, owningUser, streamKey, channelName, topic, record, chatEnabled):
def __init__(self, owningUser, streamKey, channelName, topic, record, chatEnabled, allowComments, description):
self.owningUser = owningUser
self.streamKey = streamKey
self.channelName = channelName
self.description = description
self.topic = topic
self.channelLoc = str(uuid.uuid4())
self.record = record
self.allowComments = allowComments
self.chatEnabled = chatEnabled
self.chatBG = "Standard"
self.chatTextColor = "#FFFFFF"
self.chatAnimation = "slide-in-left"
self.views = 0
self.currentViewers = 0
self.protected = False
self.channelMuted = False
def __repr__(self):
return '<id %r>' % self.id
......@@ -40,11 +60,16 @@ class Channel(db.Model):
'channelEndpointID': self.channelLoc,
'owningUser': self.owningUser,
'channelName': self.channelName,
'description': self.description,
'channelImage': "/images/" + str(self.imageLocation),
'offlineImageLocation': "/images/" + str(self.offlineImageLocation),
'topic': self.topic,
'views': self.views,
'currentViews': self.currentViewers,
'recordingEnabled': self.record,
'chatEnabled': self.chatEnabled,
'stream': [obj.id for obj in self.stream],
'recordedVideoIDs': [obj.id for obj in self.recordedVideo],
'upvotes': self.get_upvotes()
'upvotes': self.get_upvotes(),
'protected': self.protected
}
\ No newline at end of file
from .shared import db
import datetime
class RecordedVideo(db.Model):
__tablename__ = "RecordedVideo"
......@@ -8,23 +7,27 @@ class RecordedVideo(db.Model):
owningUser = db.Column(db.Integer,db.ForeignKey('user.id'))
channelName = db.Column(db.String(255))
channelID = db.Column(db.Integer,db.ForeignKey('Channel.id'))
description = db.Column(db.String(2048))
topic = db.Column(db.Integer)
views = db.Column(db.Integer)
length = db.Column(db.Float)
videoLocation = db.Column(db.String(255))
thumbnailLocation = db.Column(db.String(255))
pending = db.Column(db.Boolean)
allowComments = db.Column(db.Boolean)
upvotes = db.relationship('videoUpvotes', backref='recordedVideo', lazy="joined")
comments = db.relationship('videoComments', backref='recordedVideo', lazy="joined")
def __init__(self,owningUser,channelID,channelName,topic,views,videoLocation):
self.videoDate = datetime.datetime.now()
self.owningUser=owningUser
self.channelID=channelID
self.channelName=channelName
self.topic=topic
self.views=views
self.videoLocation=videoLocation
def __init__(self, owningUser, channelID, channelName, topic, views, videoLocation, videoDate, allowComments):
self.videoDate = videoDate
self.owningUser = owningUser
self.channelID = channelID
self.channelName = channelName
self.topic = topic
self.views = views
self.videoLocation = videoLocation
self.pending = True
self.allowComments = allowComments
def __repr__(self):
return '<id %r>' % self.id
......@@ -39,6 +42,7 @@ class RecordedVideo(db.Model):
'owningUser': self.owningUser,
'videoDate': str(self.videoDate),
'videoName': self.channelName,
'description': self.description,
'topic': self.topic,
'views': self.views,
'length': self.length,
......
......@@ -4,6 +4,19 @@ from .shared import db
class ExtendedRegisterForm(RegisterForm):
username = StringField('username', [Required()])
email = StringField('email', [Required()])
def validate(self):
success = True
if not super(ExtendedRegisterForm, self).validate():
success = False
if db.session.query(User).filter(User.username == self.username.data.strip()).first():
self.username.errors.append("Username already taken")
success = False
if db.session.query(User).filter(User.email == self.email.data.strip()).first():
self.email.errors.append("Email address already taken")
success = False
return success
class ExtendedConfirmRegisterForm(ConfirmRegisterForm):
username = StringField('username', [Required()])
......@@ -25,5 +38,5 @@ class User(db.Model, UserMixin):
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
pictureLocation = db.Column(db.String(255))
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
\ No newline at end of file
roles = db.relationship('Role', secondary=roles_users, backref=db.backref('users', lazy='dynamic'))
invites = db.relationship('invitedViewer', backref='user', lazy="joined")
\ No newline at end of file
from .shared import db
from .settings import settings
class Stream(db.Model):
__tablename__ = "Stream"
......@@ -18,6 +19,7 @@ class Stream(db.Model):
self.currentViewers = 0
self.totalViewers = 0
self.topic = topic
self.channelMuted = False
def __repr__(self):
return '<id %r>' % self.id
......@@ -34,31 +36,24 @@ class Stream(db.Model):
db.session.commit()
def serialize(self):
if self.channel.record == True:
return {
'id': self.id,
'channelID': self.linkedChannel,
'channelEndpointID': self.channel.channelLoc,
'owningUser': self.channel.owningUser,
'streamPage': '/view/' + self.channel.channelLoc + '/',
'streamURL': '/live-rec/' + self.channel.channelLoc + '/index.m3u8',
'streamName': self.streamName,
'topic': self.topic,
'currentViewers': self.currentViewers,
'totalViewers': self.currentViewers,
'upvotes': self.get_upvotes()
}
sysSettings = settings.query.first()
streamURL = ''
if sysSettings.adaptiveStreaming is True:
streamURL = '/streams/' + self.channel.channelLoc + '.m3u8'
elif self.channel.record is True:
streamURL = '/live-rec/' + self.channel.channelLoc + '/index.m3u8'
else:
return {
'id': self.id,
'channelID': self.linkedChannel,
'channelEndpointID': self.channel.channelLoc,
'owningUser': self.channel.owningUser,
'streamPage': '/view/' + self.channel.channelLoc +'/',
'streamURL': '/live/' + self.channel.channelLoc + '/index.m3u8',
'streamName': self.streamName,
'topic': self.topic,
'currentViewers': self.currentViewers,
'totalViewers': self.currentViewers,
'upvotes': self.get_upvotes()
}
\ No newline at end of file
streamURL = '/live/' + self.channel.channelLoc + '/index.m3u8'
return {
'id': self.id,
'channelID': self.linkedChannel,
'channelEndpointID': self.channel.channelLoc,
'owningUser': self.channel.owningUser,
'streamPage': '/view/' + self.channel.channelLoc + '/',
'streamURL': streamURL,
'streamName': self.streamName,
'topic': self.topic,
'currentViewers': self.currentViewers,
'totalViewers': self.currentViewers,
'upvotes': self.get_upvotes()
}
from .shared import db
from datetime import datetime
class videoComments(db.Model):
id = db.Column(db.Integer, primary_key=True)
userID = db.Column(db.Integer,db.ForeignKey('user.id'))
timestamp = db.Column(db.DateTime)
comment = db.Column(db.String(2048))
videoID = db.Column(db.Integer,db.ForeignKey('RecordedVideo.id'))
def __init__(self, userID, comment, videoID):
self.userID = userID
self.timestamp = datetime.now()
self.comment = comment
self.videoID = videoID
def __repr__(self):
return '<id %r>' % self.id
\ No newline at end of file
from .shared import db
import datetime
from binascii import hexlify
import os
def generateKey(length):
key = hexlify(os.urandom(length))
return key.decode()
class invitedViewer(db.Model):
__tablename__ = 'invitedViewer'
id = db.Column(db.Integer, primary_key=True)
userID = db.Column(db.Integer, db.ForeignKey('user.id'))
channelID = db.Column(db.Integer, db.ForeignKey('Channel.id'))
addedDate = db.Column(db.DateTime)
expiration = db.Column(db.DateTime)
inviteCode = db.Column(db.Integer, db.ForeignKey('inviteCode.id'))
def __init__(self, userID, channelID, expirationDays, inviteCode=None):
self.userID = userID
self.channelID = channelID
self.addedDate = datetime.datetime.now()
if inviteCode is not None:
self.inviteCode = inviteCode
if int(expirationDays) <= 0:
self.expiration = None
else:
self.expiration = datetime.datetime.now() + datetime.timedelta(days=int(expirationDays))
def __repr__(self):
return '<id %r>' % self.id
def isValid(self):
now = datetime.datetime.now()
if self.expiration is None:
return True
elif now < self.expiration:
return True
else:
return False
class inviteCode(db.Model):
__tablename__ = 'inviteCode'
id = db.Column(db.Integer, primary_key=True)
code = db.Column(db.String(255), unique=True)
expiration = db.Column(db.DateTime)
channelID = db.Column(db.Integer, db.ForeignKey('Channel.id'))
uses = db.Column(db.Integer)
viewers = db.relationship('invitedViewer', backref='usedCode', lazy="joined")
def __init__(self, expirationDays, channelID):
self.code = generateKey(12)
self.channelID = channelID
self.uses = 0
if int(expirationDays) <= 0:
self.expiration = None
else:
self.expiration = datetime.datetime.now() + datetime.timedelta(days=int(expirationDays))
def __repr__(self):
return '<id %r>' % self.id
def isValid(self):
now = datetime.datetime.now()
if self.expiration is None:
return True
elif now < self.expiration:
return True
else:
return False
......@@ -13,9 +13,13 @@ class settings(db.Model):
smtpSendAs = db.Column(db.String(255))
allowRegistration = db.Column(db.Boolean)
allowRecording = db.Column(db.Boolean)
adaptiveStreaming = db.Column(db.Boolean)
background = db.Column(db.String(255))
showEmptyTables = db.Column(db.Boolean)
allowComments = db.Column(db.Boolean)
systemTheme = db.Column(db.String(255))
def __init__(self, siteName, siteAddress, smtpAddress, smtpPort, smtpTLS, smtpSSL, smtpUsername, smtpPassword, smtpSendAs, allowRegistration, allowRecording):
def __init__(self, siteName, siteAddress, smtpAddress, smtpPort, smtpTLS, smtpSSL, smtpUsername, smtpPassword, smtpSendAs, allowRegistration, allowRecording, adaptiveStreaming, showEmptyTables, allowComments):
self.siteName = siteName
self.siteAddress = siteAddress
self.smtpAddress = smtpAddress
......@@ -27,7 +31,11 @@ class settings(db.Model):
self.smtpSendAs = smtpSendAs
self.allowRegistration = allowRegistration
self.allowRecording = allowRecording
self.adaptiveStreaming = adaptiveStreaming
self.showEmptyTables = showEmptyTables
self.allowComments = allowComments
self.background = "Ash"
self.systemTheme = "Default"
def __repr__(self):
return '<id %r>' % self.id
\ No newline at end of file
from .shared import db
import datetime
class views(db.Model):
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.DateTime)
viewType = db.Column(db.Integer) # View Type of 0 indicates Live Streams, 1 indicated Video View
itemID = db.Column(db.Integer) # If View Type is 0, this values will be the associated Channel.ID that was streaming, 1 is the RecordedVideo.id
def __init__(self, viewType, itemID):
self.viewType = viewType
self.itemID = itemID