Commit of initial code
This commit is contained in:
commit
e33022dab6
302
app.py
Normal file
302
app.py
Normal file
@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Clear out old session lockfiles BEFORE importing cherry
|
||||
#import os
|
||||
#for f in os.listdir("sessions/"):
|
||||
# if "lock" in f:
|
||||
# os.remove("sessions/%s" % f)
|
||||
|
||||
# CREATE TABLE 'streams' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'user' INTEGER, 'name' TEXT, 'url' TEXT, 'directory' TEXT, 'status' INTEGER, 'message' TEXT);
|
||||
# CREATE TABLE 'times' ('id' INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 'streamid' INTEGER, 'su' BOOLEAN, 'm' BOOLEAN, 't' BOOLEAN, 'w' BOOLEAN, 'r' BOOLEAN, 'f' BOOLEAN, 'sa' BOOLEAN, 'starthour' INTEGER, 'startmin' INTEGER, 'endhour' INTEGER, 'endmin' INTEGER)
|
||||
# INSERT INTO "streams" ("id","user","name","url","directory","status","message") VALUES (NULL,NULL,'WCMF Breakroom','http://1681.live.streamtheworld.com/WCMFFMAAC','wcmf-breakroom','0','')
|
||||
# INSERT INTO "times" ("id","streamid","su","m","t","w","r","f","sa","starthour","startmin","endhour","endmin") VALUES (NULL,'1','0','1','1','1','1','1','0','2','0','7','15')
|
||||
|
||||
STREAM_STATUS_ACTIVE = 0
|
||||
STREAM_STATUS_PAUSED = 1
|
||||
STREAM_STATUS_ERROR = 2
|
||||
|
||||
import sys
|
||||
import os
|
||||
import os.path
|
||||
import cherrypy
|
||||
import json
|
||||
import signal
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from libs import database
|
||||
from libs import recordTick
|
||||
from feedgen.feed import FeedGenerator
|
||||
from datetime import datetime
|
||||
|
||||
if __name__ == '__main__' or 'uwsgi' in __name__:
|
||||
appdir = "/home/streamrecord/app"
|
||||
appconf = {
|
||||
'/': {
|
||||
#'tools.proxy.on':True,
|
||||
#'tools.proxy.base': conf["base"]["url"],
|
||||
'tools.sessions.on':True,
|
||||
'tools.sessions.storage_type':'file',
|
||||
'tools.sessions.storage_path':appdir+'/sessions/',
|
||||
'tools.sessions.timeout':525600,
|
||||
'request.show_tracebacks': True
|
||||
},
|
||||
'/media': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': appdir+"/static/"
|
||||
}
|
||||
}
|
||||
|
||||
cherrypy.config.update({
|
||||
'server.socket_port':3000,
|
||||
'server.thread_pool':1,
|
||||
'server.socket_host': '0.0.0.0',
|
||||
'sessionFilter.on':True,
|
||||
'server.show.tracebacks': True
|
||||
})
|
||||
|
||||
cherrypy.server.socket_timeout = 5
|
||||
|
||||
# env - jinja2 template renderer
|
||||
env = Environment(loader=FileSystemLoader("/home/streamrecord/app/templates"))
|
||||
# db - slightly custom sqlite3 object. rows = db.execute(query, args)
|
||||
db = database()
|
||||
# REC - recorder thread - see recordTick.py
|
||||
REC = recordTick(db)
|
||||
|
||||
def render(template, args):
|
||||
templatesCache = pysite.cacheTemplates()
|
||||
defaults = {"templates":templatesCache}
|
||||
for item in args:
|
||||
defaults[item] = args[item]
|
||||
return quickRender(template, defaults)
|
||||
|
||||
def quickRender(template, args):
|
||||
template = env.get_template(template)
|
||||
return template.render(args)
|
||||
|
||||
class siteRoot(object):
|
||||
def __init__(self):
|
||||
print("Siteroot init !")
|
||||
self.templateCache = self.cacheTemplates()
|
||||
|
||||
def cacheTemplates(self):
|
||||
templateFiles = os.listdir("jstemplates/")
|
||||
templateList = []
|
||||
nameList = []
|
||||
for item in templateFiles:
|
||||
name = item.split(".")
|
||||
templateList.append({"name":name[0],"content":open("jstemplates/"+item,"r").read().replace("\t", "").replace("\n","")})
|
||||
nameList.append(name[0])
|
||||
return quickRender("templates.html", {"names":json.dumps(nameList), "templates":templateList})
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return render("html.html", {})
|
||||
|
||||
@cherrypy.expose
|
||||
def htmltest(self):
|
||||
return render("html.tpl", {})
|
||||
#index.exposed = True
|
||||
|
||||
@cherrypy.expose
|
||||
def templates(self):
|
||||
return self.templateCache
|
||||
|
||||
class api(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
@cherrypy.expose
|
||||
def getStreams(self):
|
||||
streamList = db.execute('SELECT * FROM "streams"')
|
||||
|
||||
for stream in streamList:
|
||||
stream["time"] = db.execute('SELECT * FROM "times" WHERE streamid=?', [stream["id"]])[0]
|
||||
stream["files"] = self._getFiles(stream["id"])
|
||||
|
||||
return json.dumps(streamList)
|
||||
|
||||
def _getStream(self,id):
|
||||
streamList = db.execute('SELECT * FROM "streams" WHERE "id"=?', [int(id)])
|
||||
|
||||
for stream in streamList:
|
||||
stream["time"] = db.execute('SELECT * FROM "times" WHERE streamid=?', [stream["id"]])[0]
|
||||
stream["files"]=self._getFiles(id)
|
||||
return streamList[0]
|
||||
|
||||
@cherrypy.expose
|
||||
def getStream(self, id):
|
||||
return json.dumps(self._getStream(id))
|
||||
|
||||
@cherrypy.expose
|
||||
def changeTimeDay(self, streamid, day, value):
|
||||
streamid = int(streamid)
|
||||
value = value == "true"
|
||||
|
||||
col = ""
|
||||
if day == "daysu":
|
||||
col="su"
|
||||
elif day == "daym":
|
||||
col="m"
|
||||
elif day == "dayt":
|
||||
col="t"
|
||||
elif day == "dayw":
|
||||
col="w"
|
||||
elif day == "dayr":
|
||||
col="r"
|
||||
elif day == "dayf":
|
||||
col="f"
|
||||
elif day == "daysa":
|
||||
col="sa"
|
||||
else:
|
||||
raise cherrypy.HTTPError(500, message="Day not found")
|
||||
|
||||
db.execute('UPDATE "times" SET "'+col+'"=? WHERE "streamid"=? ;', [1 if value else 0,streamid])
|
||||
|
||||
return json.dumps({"result":True})
|
||||
|
||||
@cherrypy.expose
|
||||
def changeName(self, streamid, value):
|
||||
streamid = int(streamid)
|
||||
db.execute('UPDATE "streams" SET "name"=? WHERE "id"=?', [value,streamid])
|
||||
return json.dumps({"result":True})
|
||||
@cherrypy.expose
|
||||
def changeUrl(self, streamid, value):
|
||||
streamid = int(streamid)
|
||||
db.execute('UPDATE "streams" SET "url"=? WHERE "id"=?', [value,streamid])
|
||||
return json.dumps({"result":True})
|
||||
@cherrypy.expose
|
||||
def changeTime(self, streamid, startHour, startMin, endHour, endMin):
|
||||
startHour=int(startHour)
|
||||
assert startHour>=0 and startHour<=23
|
||||
startMin=int(startMin)
|
||||
assert startMin>=0 and startMin<=59
|
||||
endHour=int(endHour)
|
||||
assert endHour>=0 and endHour<=23
|
||||
endMin=int(endMin)
|
||||
assert endMin>=0 and endMin<=59
|
||||
|
||||
db.execute('UPDATE "times" SET "starthour"=?, "startmin"=?, "endhour"=?, "endmin"=? WHERE "streamid"=? ;', [startHour, startMin, endHour, endMin, streamid])
|
||||
return json.dumps({"result":True})
|
||||
|
||||
def _filterName(self, input):
|
||||
allowed="abcdefghijklmnopqrstuvwxyz123456789-"
|
||||
input = input.replace(" ", "-").lower()
|
||||
output=[]
|
||||
for i in range(0, len(allowed)):
|
||||
if input[i:i+1] in allowed:
|
||||
output.append(input[i:i+1])
|
||||
return ''.join(output)
|
||||
|
||||
@cherrypy.expose
|
||||
def createStream(self, data):
|
||||
data = json.loads(data)
|
||||
|
||||
assert not data["name"] == ""
|
||||
assert not data["url"] == ""
|
||||
assert data["time"]["su"] or data["time"]["m"] or data["time"]["t"] or data["time"]["w"] or data["time"]["r"] or data["time"]["f"] or data["time"]["sa"]
|
||||
|
||||
dirName = self._filterName(data["name"])
|
||||
|
||||
rowid = db.execute('INSERT INTO "streams" ("user", "name", "url", "directory", "status", "message") VALUES (?, ?, ?, ?, ?, ?);', [0, data["name"], data["url"], dirName, data["status"], ""])
|
||||
db.execute('INSERT INTO "times" ("streamid", "su", "m", "t", "w", "r", "f", "sa", "starthour", "startmin", "endhour", "endmin") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', [
|
||||
rowid,
|
||||
data["time"]["su"],
|
||||
data["time"]["m"],
|
||||
data["time"]["t"],
|
||||
data["time"]["w"],
|
||||
data["time"]["r"],
|
||||
data["time"]["f"],
|
||||
data["time"]["sa"],
|
||||
data["time"]["startHour"],
|
||||
data["time"]["startMin"],
|
||||
data["time"]["endHour"],
|
||||
data["time"]["endMin"]
|
||||
])
|
||||
|
||||
return json.dumps({"result":rowid})
|
||||
|
||||
def _getFiles(self, id):
|
||||
stream = db.execute('SELECT * FROM "streams" WHERE "id"=?', [int(id)])[0]
|
||||
recordingsDir = "files/output/"+stream["directory"]+"/"
|
||||
files = []
|
||||
if os.path.exists(recordingsDir):
|
||||
files = os.listdir(recordingsDir)
|
||||
files.sort()
|
||||
allFiles = []
|
||||
for i in range(0, len(files)):
|
||||
item = files[i]
|
||||
size = os.path.getsize(recordingsDir+item)
|
||||
allFiles.append({
|
||||
"filename":item,
|
||||
"directory":recordingsDir,
|
||||
"streamdir":stream["directory"],
|
||||
"filenum":i,
|
||||
"bytes":size,
|
||||
"mbytes":round(size/1024.0/1024.0, 2),
|
||||
"date":os.path.getmtime(recordingsDir+item)
|
||||
})
|
||||
return allFiles
|
||||
|
||||
@cherrypy.expose
|
||||
def getFiles(self, id):
|
||||
files = self._getFiles(id)
|
||||
return json.dumps({"data":files})
|
||||
|
||||
@cherrypy.expose
|
||||
def download(self, id, fn):
|
||||
files = self._getFiles(id)
|
||||
item = files[int(fn)]
|
||||
raise cherrypy.HTTPRedirect("/static/output/"+item["streamdir"]+"/"+item["filename"], 302)
|
||||
|
||||
@cherrypy.expose
|
||||
def getUrl(self, id, fn):
|
||||
files = self._getFiles(id)
|
||||
item = files[int(fn)]
|
||||
return json.dumps({"result":"/static/output/"+item["streamdir"]+"/"+item["filename"]})
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.response_headers(headers=[('Content-Type', 'application/rss+xml')])
|
||||
def getPodcast(self, id):
|
||||
"""fg = FeedGenerator()
|
||||
fg.load_extension('podcast')
|
||||
stream = self._getStream(id)
|
||||
fg.title("Radio Feed - %s" % stream["name"])
|
||||
fg.subtitle("Stream ID: %s"%stream["id"])
|
||||
fg.language('en')
|
||||
fg.link( href="http://192.168.1.200:3000/api/getPodcast?id=%s"%stream["id"], rel='self' )
|
||||
|
||||
for item in stream["files"]:
|
||||
fe = fg.add_entry()
|
||||
fe.id("http://192.168.1.200:3000/api/download?id=%s&fn=%s" % (stream["id"], item["filenum"]))
|
||||
fe.link(href="http://192.168.1.200:3000/api/download?id=%s&fn=%s" % (stream["id"], item["filenum"]), rel="alternate")
|
||||
fe.title(item["filename"])
|
||||
cherrypy.response.headers['Content-Type']= 'application/rss+xml'
|
||||
xml = fg.rss_str(pretty=True)
|
||||
|
||||
#return xml
|
||||
#return str.encode(open("extra.txt", "r").read())
|
||||
"""
|
||||
|
||||
stream = self._getStream(id)
|
||||
# Thu, 31 Jul 2014 07:13:48 +0000
|
||||
for f in stream["files"]:
|
||||
f["date"]=datetime.fromtimestamp(f["date"]).strftime("%a, %m %b %Y %H:%M:%S +%z")
|
||||
return str.encode(render("podcast.html", {
|
||||
"stream":stream,
|
||||
"builddate": datetime.now().strftime("%a, %m %b %Y %H:%M:%S +0100")#Thu, 31 Jul 2014 07:13:48 +0000
|
||||
}))
|
||||
|
||||
|
||||
|
||||
pysite = siteRoot()
|
||||
pysite.api = api()
|
||||
|
||||
print( "Ready to start application" )
|
||||
|
||||
if(len(sys.argv)>1 and sys.argv[1]=="test"):
|
||||
print("test!")
|
||||
application = cherrypy.quickstart(pysite, '/', appconf)
|
||||
else:
|
||||
sys.stdout = sys.stderr
|
||||
cherrypy.config.update({'environment': 'embedded'})
|
||||
application = cherrypy.tree.mount(pysite, "/", appconf)
|
||||
|
23
jstemplates/add.html
Normal file
23
jstemplates/add.html
Normal file
@ -0,0 +1,23 @@
|
||||
<form class="add-schedule" data-validation="addschedule">
|
||||
{{>stream}}
|
||||
<input type="submit" class="btn btn-success pull-right" value="Add" />
|
||||
|
||||
|
||||
<div class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
|
||||
<h4 class="modal-title">Errors</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-dismiss="modal">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
22
jstemplates/downloads.html
Normal file
22
jstemplates/downloads.html
Normal file
@ -0,0 +1,22 @@
|
||||
<div id="downloads">
|
||||
{{#each streams}}
|
||||
<div class="panel panel-default" data-streamid="{{ id }}">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><a href="#view/{{ id }}">{{ name }}</a></h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{>stream_downloads}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div id="playerdrop">
|
||||
|
||||
<div class="controls">
|
||||
|
||||
</div>
|
||||
<div class="time">
|
||||
<input id="player-slider" type="text" data-slider-min="0" data-slider-max="20" data-slider-step="1" data-slider-value="0"/>
|
||||
</div>
|
||||
<div id="player"></div>
|
||||
</div>
|
12
jstemplates/error.html
Normal file
12
jstemplates/error.html
Normal file
@ -0,0 +1,12 @@
|
||||
<div class="row-fluid clearfix errorpanel">
|
||||
<div class="col-sm 12">
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Error</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
5
jstemplates/list.html
Normal file
5
jstemplates/list.html
Normal file
@ -0,0 +1,5 @@
|
||||
<ul>
|
||||
{{#each messages}}
|
||||
<li>{{ this }}</li>
|
||||
{{/each}}
|
||||
</ul>
|
93
jstemplates/stream.html
Normal file
93
jstemplates/stream.html
Normal file
@ -0,0 +1,93 @@
|
||||
<div class="panel panel-default" data-streamid="{{ id }}">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title"><a href="#view/{{ id }}">{{ name }}</a></h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row-fluid clearfix">
|
||||
<div class="col-sm-2">
|
||||
<strong>Name</strong>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" name="stream-name" value="{{ name }}" />
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<strong>URL</strong>
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
<input type="text" class="form-control" name="stream-url" value="{{ url }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid clearfix">
|
||||
<div class="col-sm-12"><br /></div>
|
||||
</div>
|
||||
<div class="row-fluid clearfix schedule-row">
|
||||
<div class="col-sm-2">
|
||||
<strong>Time</strong>
|
||||
</div>
|
||||
<div class="col-sm-6 day-input clearfix">
|
||||
<div class="dropdown pull-left">
|
||||
<a class="dropdown-toggle btn btn-default" data-toggle="dropdown" href="#">
|
||||
Days
|
||||
<b class="caret"></b>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-form" role="menu">
|
||||
<li><label class="checkbox"><input type="checkbox" name="daysu"{{#if time.su}} checked="checked"{{/if}}>S</label></li>
|
||||
<li><label class="checkbox"><input type="checkbox" name="daym"{{#if time.m}} checked="checked"{{/if}}>M</label></li>
|
||||
<li><label class="checkbox"><input type="checkbox" name="dayt"{{#if time.t}} checked="checked"{{/if}}>T</label></li>
|
||||
<li><label class="checkbox"><input type="checkbox" name="dayw"{{#if time.w}} checked="checked"{{/if}}>W</label></li>
|
||||
<li><label class="checkbox"><input type="checkbox" name="dayr"{{#if time.r}} checked="checked"{{/if}}>R</label></li>
|
||||
<li><label class="checkbox"><input type="checkbox" name="dayf"{{#if time.f}} checked="checked"{{/if}}>F</label></li>
|
||||
<li><label class="checkbox"><input type="checkbox" name="daysa"{{#if time.sa}} checked="checked"{{/if}}>S</label></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pull-left time-input">
|
||||
<input type="text" class="form-control hours" name="stream-starthour" value="{{ time.starthour }}"/>
|
||||
<span> : </span>
|
||||
<input type="text" class="form-control minutes" name="stream-startmin" value="{{ time.startmin }}"/>
|
||||
<span> - </span>
|
||||
<input type="text" class="form-control hours" name="stream-endhour" value="{{ time.endhour }}"/>
|
||||
<span> : </span>
|
||||
<input type="text" class="form-control minutes" name="stream-endmin" value="{{ time.endmin }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<strong>Status</strong>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="btn-group btn-group-status">
|
||||
<button type="button" class="btn btn-default btn-status-ok{{#is status 0 }} active{{/is}}"><span class="glyphicon glyphicon-ok"></span></button>
|
||||
<button type="button" class="btn btn-default btn-status-stop{{#is status 1 }} active{{/is}}"><span class="glyphicon glyphicon-stop"></span></button>
|
||||
<button type="button" class="btn btn-default btn-status-alarm{{#is status 2 }} active{{/is}}"><span class="glyphicon glyphicon-bell"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if message }}
|
||||
<div class="row-fluid clearfix errorpanel">
|
||||
<div class="col-sm 12">
|
||||
<div class="panel panel-danger">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Error</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if files }}
|
||||
<div class="row-fluid clearfix ">
|
||||
<div class="col-sm 12">
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Files</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{>stream_downloads}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
14
jstemplates/stream_downloads.html
Normal file
14
jstemplates/stream_downloads.html
Normal file
@ -0,0 +1,14 @@
|
||||
{{#each files}}
|
||||
<div class="row-fluid clearfix">
|
||||
<div class="col-sm-6">
|
||||
<strong>{{filename}}</strong>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{{mbytes}} mb
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<a href="javascript:void(0);" class="playlink" data-url="/api/download?id={{ ../id }}&fn={{ filenum }}">Play</a> ·
|
||||
<a href="/api/download?id={{ ../id }}&fn={{ filenum }}">Download</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
7
jstemplates/streams.html
Normal file
7
jstemplates/streams.html
Normal file
@ -0,0 +1,7 @@
|
||||
<h1>{{#is single true}}Viewing: {{streams.[0].name}} {{else}}Scheduled Recordings{{/is}}</h1>
|
||||
<hr />
|
||||
<div class="streams">
|
||||
{{#each streams}}
|
||||
{{>stream}}
|
||||
{{/each}}
|
||||
</div>
|
4
libs/__init__.py
Normal file
4
libs/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from libs.recordTick import recordTick
|
||||
from libs.database import database
|
13
libs/config/app.ini
Normal file
13
libs/config/app.ini
Normal file
@ -0,0 +1,13 @@
|
||||
[uwsgi]
|
||||
uid = streamrecord
|
||||
pid = streamrecord
|
||||
plugins = python3
|
||||
touch-reload = /home/streamrecord/app/app.py
|
||||
chdir = /home/streamrecord/app/
|
||||
wsgi-file = /home/streamrecord/app/app.py
|
||||
callable = application
|
||||
master = true
|
||||
processes = 1
|
||||
socket = 0.0.0.0:3330
|
||||
enable-threads = true
|
||||
no-threads-wait = true
|
36
libs/database.py
Normal file
36
libs/database.py
Normal file
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
import sqlite3
|
||||
import threading
|
||||
|
||||
class database(threading.Thread):
|
||||
def __init__(self):
|
||||
threading.Thread.__init__(self)
|
||||
self.db = None
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
self.db = self.openDB()
|
||||
|
||||
def openDB(self):
|
||||
db = sqlite3.connect("db.sqlite", check_same_thread=False, cached_statements=0, isolation_level=None)
|
||||
db.row_factory = self.dict_factory
|
||||
return db
|
||||
|
||||
def dict_factory(self, cursor, row):
|
||||
d = {}
|
||||
for idx, col in enumerate(cursor.description):
|
||||
d[col[0]] = row[idx]
|
||||
return d
|
||||
|
||||
def execute(self, sql, params=None):
|
||||
db = self.db
|
||||
cursor = db.cursor()
|
||||
if params:
|
||||
cursor.execute(sql, params)
|
||||
else:
|
||||
cursor.execute(sql)
|
||||
data = cursor.fetchall()
|
||||
if not cursor.lastrowid==None:
|
||||
return cursor.lastrowid
|
||||
cursor.close()
|
||||
return data
|
215
libs/recordTick.py
Normal file
215
libs/recordTick.py
Normal file
@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from threading import Thread
|
||||
import time
|
||||
import datetime
|
||||
from sched import scheduler
|
||||
import cherrypy
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
import os.path
|
||||
|
||||
class recordTick(Thread):
|
||||
def __init__(self, database):
|
||||
Thread.__init__(self)
|
||||
# sqlite3 reference
|
||||
self.db = database
|
||||
# list of downloader threads
|
||||
self.threads = {}
|
||||
# tick timer
|
||||
self.timer = scheduler(time.time, time.sleep)
|
||||
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
time.sleep(3)
|
||||
|
||||
## TESTING CODE
|
||||
#now=datetime.datetime.now()
|
||||
#self.db.execute('UPDATE "times" SET "starthour"=?, "startmin"=?, "endhour"=?, "endmin"=? WHERE "streamid"=? ;', (now.hour, now.minute, now.hour, now.minute+1, 1))
|
||||
#self.tick()
|
||||
## END TESTING CODE
|
||||
|
||||
self.scheduleTick()
|
||||
|
||||
def tick(self):
|
||||
now=datetime.datetime.now()
|
||||
#print("Tick start: %s" % now)
|
||||
|
||||
# Look for starting times set to now
|
||||
days = ["m", "t", "w", "r", "f", "sa", "su"]
|
||||
day = days[datetime.datetime.now().weekday()]
|
||||
|
||||
startTimes = self.db.execute('SELECT * FROM "times" where "starthour"=? AND "startmin"=? AND "'+day+'"=1', (now.hour, now.minute))
|
||||
for startTime in startTimes:
|
||||
# Start each downloader
|
||||
self.startStream(startTime["streamid"])
|
||||
|
||||
# Look for end times set to now
|
||||
endTimes = self.db.execute('SELECT * FROM "times" where "endhour"=? AND "endmin"=?', (now.hour, now.minute))
|
||||
for endTime in endTimes:
|
||||
# terminate each downloader
|
||||
self.endStream(endTime["streamid"])
|
||||
|
||||
#print("Tick end: %s" % now)
|
||||
|
||||
def startStream(self, id):
|
||||
# Find stream information
|
||||
stream = self.db.execute('SELECT * FROM "streams" WHERE "id"=? ;', (id,))[0]
|
||||
|
||||
# if the downloader isnt running already:
|
||||
if not stream["id"] in self.threads:
|
||||
# Create the recording thread
|
||||
self.threads[stream["id"]] = recordThread(stream["url"], stream["directory"])
|
||||
|
||||
def endStream(self, id):
|
||||
if id in self.threads:
|
||||
# tell the downloader to finish
|
||||
self.threads[id].cancel()
|
||||
del self.threads[id]
|
||||
|
||||
def scheduleTick(self):
|
||||
# schedule tick in the next minute
|
||||
self.timer.enter(self.timeToNextMinute(), 1, self.tick)
|
||||
self.timer.run()
|
||||
# Schedule the next tick
|
||||
Thread(target=self.scheduleTick).start()
|
||||
|
||||
def timeToNextMinute(self):
|
||||
# calculate time to the milliscond until the next minute rolls over
|
||||
# Find the next minute
|
||||
then = datetime.datetime.now()+datetime.timedelta(minutes=1)
|
||||
# Drop the seconds
|
||||
then = then-datetime.timedelta(seconds=then.second,microseconds=then.microsecond)
|
||||
# calculate difference
|
||||
wait = then - datetime.datetime.now()
|
||||
waitMillis = wait.seconds + int(wait.microseconds/1000)/1000
|
||||
return waitMillis
|
||||
|
||||
class recordThread(Thread):
|
||||
def __init__(self, url, directory):
|
||||
Thread.__init__(self)
|
||||
# URL to download
|
||||
self.url = url
|
||||
# Directory name to use
|
||||
self.directory = directory
|
||||
# True means the downloader keeps alive on failure
|
||||
self.running = True
|
||||
# Start time of the recording
|
||||
self.startdate = None
|
||||
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
print("%s starting downloader for %s" % (datetime.datetime.now(), self.url))
|
||||
# Download the stream to temp file(s)
|
||||
self.downloadStream()
|
||||
# Combine files into 1 audio file
|
||||
self.mergeStream()
|
||||
# Encode to mp3
|
||||
self.transcodeStream()
|
||||
# Delete temp files, move recording to save directory
|
||||
self.cleanup()
|
||||
print("%s finished downloader for %s" % (datetime.datetime.now(), self.url))
|
||||
|
||||
def downloadStream(self):
|
||||
self.startdate = datetime.datetime.now()
|
||||
# As long as we're supposed to keep retrying
|
||||
while self.running:
|
||||
# Create the temp dir for this stream
|
||||
if not os.path.exists("files/temp/"+self.directory):
|
||||
os.mkdir("files/temp/"+self.directory)
|
||||
|
||||
# If there are already files, we're resuming. take the next available number
|
||||
recNum = 0
|
||||
while os.path.exists("files/temp/%s/recdate%s.mp3" % (self.directory, ".%s"%recNum)):
|
||||
recNum = recNum + 1
|
||||
# Filename is something like files/temp/stream-name/rec-y-m-d_h-m-s.0.mp3
|
||||
fileName = "files/temp/%s/recdate%s.mp3" % (self.directory, "" if recNum == None else ".%s"%recNum)
|
||||
|
||||
# Args if we download with curl (bad)
|
||||
args_curl = [
|
||||
'/usr/bin/curl',
|
||||
#'-s',
|
||||
'-A',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36',
|
||||
'--output', fileName, self.url]
|
||||
|
||||
# args if we download/transcode with avconv (HERO!)
|
||||
args_libav = [
|
||||
'/usr/bin/avconv',
|
||||
'-loglevel',
|
||||
'error',
|
||||
'-i',
|
||||
self.url,
|
||||
'-ab',
|
||||
'128k',
|
||||
fileName
|
||||
]
|
||||
self.proc = subprocess.Popen(args_libav, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
output = self.proc.communicate()
|
||||
print("LibAV output for %s:\n%s" % (self.url, output))
|
||||
self.proc = None
|
||||
|
||||
|
||||
def mergeStream(self):
|
||||
# Get an ordered list of the piece files
|
||||
files = os.listdir("files/temp/%s"%self.directory)
|
||||
files.sort()
|
||||
# merge audio tracks into a matroska audio file
|
||||
command = ['/usr/bin/mkvmerge', '-o', "files/temp/%s/temp.mka"%self.directory, "files/temp/%s/%s"%(self.directory,files.pop(0))]
|
||||
for fname in files:
|
||||
command.append("+files/temp/%s/%s"%(self.directory,fname))
|
||||
self.mergeproc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
# Wait for the merge to finish
|
||||
output = self.mergeproc.communicate()
|
||||
|
||||
def transcodeStream(self):
|
||||
# Delete the existing output file
|
||||
if os.path.exists("files/temp/%s/out.mp3"%self.directory):
|
||||
os.unlink("files/temp/%s/out.mp3"%self.directory)
|
||||
|
||||
# Convert the matroska file to mp3
|
||||
command = ['/usr/bin/avconv', '-i', "files/temp/%s/temp.mka"%self.directory, '-q:a', '0', '-ab', '128k', "files/temp/%s/out.mp3"%self.directory]
|
||||
self.transcodeproc = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
# wait for the trancode to finish
|
||||
output = self.transcodeproc.communicate()
|
||||
|
||||
def cleanup(self):
|
||||
# create a dated name for the file
|
||||
newname = self.startdate.strftime("%Y-%m-%d_%H-%M-%S")+".mp3"
|
||||
|
||||
# make it's finished storage location
|
||||
if not os.path.exists("files/output/"+self.directory):
|
||||
os.mkdir("files/output/"+self.directory)
|
||||
|
||||
# copy final recording to output dir
|
||||
os.rename("files/temp/%s/out.mp3"%(self.directory), "files/output/%s/%s"%(self.directory,newname))
|
||||
|
||||
# Delete temp files
|
||||
files = os.listdir("files/temp/%s"%self.directory)
|
||||
for f in files:
|
||||
os.unlink("files/temp/%s/%s"%(self.directory,f))
|
||||
|
||||
def cancel(self):
|
||||
# turn off keep-alive dow the downloader
|
||||
self.running = False
|
||||
# Kill the download process
|
||||
self.proc.terminate()
|
||||
Thread(target=self.kill).start()
|
||||
|
||||
def kill(self):
|
||||
print("Starting kill thread for %s" % self.url)
|
||||
time.sleep(3)
|
||||
# kill the thread
|
||||
if not self.proc == None:
|
||||
# One more chance to go quietly...
|
||||
self.proc.terminate()
|
||||
time.sleep(3)
|
||||
else:
|
||||
print("Nothing to kill for %s" % self.url)
|
||||
if not self.proc == None:
|
||||
# Kill it
|
||||
self.proc.kill()
|
||||
|
1
static/bootstrap/css/bootstrap-flat.min.css
vendored
Normal file
1
static/bootstrap/css/bootstrap-flat.min.css
vendored
Normal file
@ -0,0 +1 @@
|
||||
.img-thumbnail{border-radius:0}code{border-radius:0}pre{border-radius:0}.form-control{border-radius:0}.form-control:focus{border-color:#66afe9;outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;-webkit-box-shadow:none;box-shadow:none}.input-sm{border-radius:0}.input-lg{border-radius:0}.btn{border-radius:0}.btn-lg{border-radius:0}.btn-sm,.btn-xs{border-radius:0}.dropdown-menu{border-radius:0;-webkit-box-shadow:none;box-shadow:none}.btn-group-xs>.btn{border-radius:0}.btn-group-sm>.btn{border-radius:0}.btn-group-lg>.btn{border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{border-radius:0}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-lg>.input-group-btn>.btn{border-radius:0}.input-group-addon{border-radius:0}.input-group-addon.input-sm{border-radius:0}.input-group-addon.input-lg{border-radius:0}.nav-tabs>li>a{border-radius:0}.nav-pills>li>a{border-radius:0}@media(min-width:768px){.navbar{border-radius:0}}.navbar-toggle{border-radius:0}.navbar-toggle .icon-bar{border-radius:0}.breadcrumb{border-radius:0}.pagination{border-radius:0}.pagination>li:first-child>a,.pagination>li:first-child>span{border-bottom-left-radius:0;border-top-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:0;border-top-left-radius:0}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:0;border-top-left-radius:0}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:0;border-bottom-right-radius:0}.pager li>a,.pager li>span{border-radius:0}.label{border-radius:0}.badge{border-radius:0}.container .jumbotron{border-radius:0}.thumbnail{border-radius:0}.alert{border-radius:0}.progress{border-radius:0;-webkit-box-shadow:none;box-shadow:none}.progress-bar{-webkit-box-shadow:none;box-shadow:none}.list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.list-group-item:last-child{border-bottom-right-radius:0;border-bottom-left-radius:0}.panel{border-radius:0;-webkit-box-shadow:none;box-shadow:none}.panel-heading{border-top-right-radius:0;border-top-left-radius:0}.panel-footer{border-bottom-right-radius:0;border-bottom-left-radius:0}.panel-group .panel{border-radius:0}.well{border-radius:0;-webkit-box-shadow:none;box-shadow:none}.well-lg{border-radius:0}.well-sm{border-radius:0}.close{text-shadow:none}.modal-content{border-radius:0;-webkit-box-shadow:none;box-shadow:none}@media screen and (min-width:768px){.modal-content{-webkit-box-shadow:none;box-shadow:none}}.tooltip-inner{border-radius:0}.popover{border-radius:0;-webkit-box-shadow:none;box-shadow:none}.popover-title{border-radius:0}.carousel-control{text-shadow:none}.carousel-indicators li{border-radius:0}.carousel-caption{text-shadow:none}
|
442
static/bootstrap/css/bootstrap-theme.css
vendored
Normal file
442
static/bootstrap/css/bootstrap-theme.css
vendored
Normal file
@ -0,0 +1,442 @@
|
||||
/*!
|
||||
* Bootstrap v3.2.0 (http://getbootstrap.com)
|
||||
* Copyright 2011-2014 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
*/
|
||||
|
||||
.btn-default,
|
||||
.btn-primary,
|
||||
.btn-success,
|
||||
.btn-info,
|
||||
.btn-warning,
|
||||
.btn-danger {
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
|
||||
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
|
||||
}
|
||||
.btn-default:active,
|
||||
.btn-primary:active,
|
||||
.btn-success:active,
|
||||
.btn-info:active,
|
||||
.btn-warning:active,
|
||||
.btn-danger:active,
|
||||
.btn-default.active,
|
||||
.btn-primary.active,
|
||||
.btn-success.active,
|
||||
.btn-info.active,
|
||||
.btn-warning.active,
|
||||
.btn-danger.active {
|
||||
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
|
||||
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
|
||||
}
|
||||
.btn:active,
|
||||
.btn.active {
|
||||
background-image: none;
|
||||
}
|
||||
.btn-default {
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
|
||||
background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
|
||||
background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #dbdbdb;
|
||||
border-color: #ccc;
|
||||
}
|
||||
.btn-default:hover,
|
||||
.btn-default:focus {
|
||||
background-color: #e0e0e0;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-default:active,
|
||||
.btn-default.active {
|
||||
background-color: #e0e0e0;
|
||||
border-color: #dbdbdb;
|
||||
}
|
||||
.btn-default:disabled,
|
||||
.btn-default[disabled] {
|
||||
background-color: #e0e0e0;
|
||||
background-image: none;
|
||||
}
|
||||
.btn-primary {
|
||||
background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
|
||||
background-image: -o-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#2d6ca2));
|
||||
background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #2b669a;
|
||||
}
|
||||
.btn-primary:hover,
|
||||
.btn-primary:focus {
|
||||
background-color: #2d6ca2;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-primary:active,
|
||||
.btn-primary.active {
|
||||
background-color: #2d6ca2;
|
||||
border-color: #2b669a;
|
||||
}
|
||||
.btn-primary:disabled,
|
||||
.btn-primary[disabled] {
|
||||
background-color: #2d6ca2;
|
||||
background-image: none;
|
||||
}
|
||||
.btn-success {
|
||||
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
|
||||
background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
|
||||
background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #3e8f3e;
|
||||
}
|
||||
.btn-success:hover,
|
||||
.btn-success:focus {
|
||||
background-color: #419641;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-success:active,
|
||||
.btn-success.active {
|
||||
background-color: #419641;
|
||||
border-color: #3e8f3e;
|
||||
}
|
||||
.btn-success:disabled,
|
||||
.btn-success[disabled] {
|
||||
background-color: #419641;
|
||||
background-image: none;
|
||||
}
|
||||
.btn-info {
|
||||
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
|
||||
background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
|
||||
background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #28a4c9;
|
||||
}
|
||||
.btn-info:hover,
|
||||
.btn-info:focus {
|
||||
background-color: #2aabd2;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-info:active,
|
||||
.btn-info.active {
|
||||
background-color: #2aabd2;
|
||||
border-color: #28a4c9;
|
||||
}
|
||||
.btn-info:disabled,
|
||||
.btn-info[disabled] {
|
||||
background-color: #2aabd2;
|
||||
background-image: none;
|
||||
}
|
||||
.btn-warning {
|
||||
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
|
||||
background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
|
||||
background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #e38d13;
|
||||
}
|
||||
.btn-warning:hover,
|
||||
.btn-warning:focus {
|
||||
background-color: #eb9316;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-warning:active,
|
||||
.btn-warning.active {
|
||||
background-color: #eb9316;
|
||||
border-color: #e38d13;
|
||||
}
|
||||
.btn-warning:disabled,
|
||||
.btn-warning[disabled] {
|
||||
background-color: #eb9316;
|
||||
background-image: none;
|
||||
}
|
||||
.btn-danger {
|
||||
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
|
||||
background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
|
||||
background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #b92c28;
|
||||
}
|
||||
.btn-danger:hover,
|
||||
.btn-danger:focus {
|
||||
background-color: #c12e2a;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-danger:active,
|
||||
.btn-danger.active {
|
||||
background-color: #c12e2a;
|
||||
border-color: #b92c28;
|
||||
}
|
||||
.btn-danger:disabled,
|
||||
.btn-danger[disabled] {
|
||||
background-color: #c12e2a;
|
||||
background-image: none;
|
||||
}
|
||||
.thumbnail,
|
||||
.img-thumbnail {
|
||||
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
||||
}
|
||||
.dropdown-menu > li > a:hover,
|
||||
.dropdown-menu > li > a:focus {
|
||||
background-color: #e8e8e8;
|
||||
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
|
||||
background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
|
||||
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.dropdown-menu > .active > a,
|
||||
.dropdown-menu > .active > a:hover,
|
||||
.dropdown-menu > .active > a:focus {
|
||||
background-color: #357ebd;
|
||||
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
|
||||
background-image: -o-linear-gradient(top, #428bca 0%, #357ebd 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#357ebd));
|
||||
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.navbar-default {
|
||||
background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
|
||||
background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
|
||||
background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
|
||||
}
|
||||
.navbar-default .navbar-nav > .active > a {
|
||||
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
|
||||
background-image: -o-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f3f3f3));
|
||||
background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
|
||||
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
|
||||
}
|
||||
.navbar-brand,
|
||||
.navbar-nav > li > a {
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
|
||||
}
|
||||
.navbar-inverse {
|
||||
background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
|
||||
background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
|
||||
background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.navbar-inverse .navbar-nav > .active > a {
|
||||
background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%);
|
||||
background-image: -o-linear-gradient(top, #222 0%, #282828 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#222), to(#282828));
|
||||
background-image: linear-gradient(to bottom, #222 0%, #282828 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
|
||||
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
|
||||
}
|
||||
.navbar-inverse .navbar-brand,
|
||||
.navbar-inverse .navbar-nav > li > a {
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
|
||||
}
|
||||
.navbar-static-top,
|
||||
.navbar-fixed-top,
|
||||
.navbar-fixed-bottom {
|
||||
border-radius: 0;
|
||||
}
|
||||
.alert {
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
|
||||
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
|
||||
}
|
||||
.alert-success {
|
||||
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
|
||||
background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
|
||||
background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #b2dba1;
|
||||
}
|
||||
.alert-info {
|
||||
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
|
||||
background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
|
||||
background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #9acfea;
|
||||
}
|
||||
.alert-warning {
|
||||
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
|
||||
background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
|
||||
background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #f5e79e;
|
||||
}
|
||||
.alert-danger {
|
||||
background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
|
||||
background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
|
||||
background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #dca7a7;
|
||||
}
|
||||
.progress {
|
||||
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
|
||||
background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
|
||||
background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.progress-bar {
|
||||
background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
|
||||
background-image: -o-linear-gradient(top, #428bca 0%, #3071a9 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#3071a9));
|
||||
background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.progress-bar-success {
|
||||
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
|
||||
background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
|
||||
background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.progress-bar-info {
|
||||
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
|
||||
background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
|
||||
background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.progress-bar-warning {
|
||||
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
|
||||
background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
|
||||
background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.progress-bar-danger {
|
||||
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
|
||||
background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
|
||||
background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.progress-bar-striped {
|
||||
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
|
||||
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
|
||||
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
|
||||
}
|
||||
.list-group {
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
||||
}
|
||||
.list-group-item.active,
|
||||
.list-group-item.active:hover,
|
||||
.list-group-item.active:focus {
|
||||
text-shadow: 0 -1px 0 #3071a9;
|
||||
background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
|
||||
background-image: -o-linear-gradient(top, #428bca 0%, #3278b3 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#3278b3));
|
||||
background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #3278b3;
|
||||
}
|
||||
.panel {
|
||||
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
|
||||
}
|
||||
.panel-default > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
|
||||
background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
|
||||
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.panel-primary > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
|
||||
background-image: -o-linear-gradient(top, #428bca 0%, #357ebd 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#357ebd));
|
||||
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.panel-success > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
|
||||
background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
|
||||
background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.panel-info > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
|
||||
background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
|
||||
background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.panel-warning > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
|
||||
background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
|
||||
background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.panel-danger > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
|
||||
background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
|
||||
background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.well {
|
||||
background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
|
||||
background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
|
||||
background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
|
||||
background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #dcdcdc;
|
||||
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-theme.css.map */
|
1
static/bootstrap/css/bootstrap-theme.css.map
Normal file
1
static/bootstrap/css/bootstrap-theme.css.map
Normal file
File diff suppressed because one or more lines are too long
5
static/bootstrap/css/bootstrap-theme.min.css
vendored
Normal file
5
static/bootstrap/css/bootstrap-theme.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6203
static/bootstrap/css/bootstrap.css
vendored
Normal file
6203
static/bootstrap/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
static/bootstrap/css/bootstrap.css.map
Normal file
1
static/bootstrap/css/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
5
static/bootstrap/css/bootstrap.min.css
vendored
Normal file
5
static/bootstrap/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/bootstrap/fonts/glyphicons-halflings-regular.eot
Normal file
BIN
static/bootstrap/fonts/glyphicons-halflings-regular.eot
Normal file
Binary file not shown.
229
static/bootstrap/fonts/glyphicons-halflings-regular.svg
Normal file
229
static/bootstrap/fonts/glyphicons-halflings-regular.svg
Normal file
@ -0,0 +1,229 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata></metadata>
|
||||
<defs>
|
||||
<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
|
||||
<font-face units-per-em="1200" ascent="960" descent="-240" />
|
||||
<missing-glyph horiz-adv-x="500" />
|
||||
<glyph />
|
||||
<glyph />
|
||||
<glyph unicode="
" />
|
||||
<glyph unicode=" " />
|
||||
<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
|
||||
<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
|
||||
<glyph unicode=" " />
|
||||
<glyph unicode=" " horiz-adv-x="652" />
|
||||
<glyph unicode=" " horiz-adv-x="1304" />
|
||||
<glyph unicode=" " horiz-adv-x="652" />
|
||||
<glyph unicode=" " horiz-adv-x="1304" />
|
||||
<glyph unicode=" " horiz-adv-x="434" />
|
||||
<glyph unicode=" " horiz-adv-x="326" />
|
||||
<glyph unicode=" " horiz-adv-x="217" />
|
||||
<glyph unicode=" " horiz-adv-x="217" />
|
||||
<glyph unicode=" " horiz-adv-x="163" />
|
||||
<glyph unicode=" " horiz-adv-x="260" />
|
||||
< |