Merge branch 'dev' into faderama

This commit is contained in:
Matthew Stratford 2021-09-01 21:01:19 +01:00
commit 9e6db5bbf0
38 changed files with 509 additions and 97 deletions

View file

@ -1,16 +1,15 @@
autopep8
pygame==2.0.1
sanic
sanic-cors
syncer
aiohttp
mutagen
sounddevice
autopep8
setproctitle
pyttsx3
websockets
typing_extensions
pyserial
requests
jinja2
sanic==21.3.4
sanic-Cors==1.0.0
syncer==1.3.0
aiohttp==3.7.4.post0
mutagen==1.45.1
sounddevice==0.4.2
setproctitle==1.2.2
pyttsx3==2.90
websockets==8.1
typing_extensions==3.10.0.0
pyserial==3.5
requests==2.26.0
Jinja2==3.0.1

View file

@ -11,6 +11,7 @@ from syncer import sync
from helpers.logging_manager import LoggingManager
from helpers.the_terminator import Terminator
from helpers.myradio_api import MyRadioAPI
from helpers.normalisation import generate_normalised_file
from baps_types.plan import PlanItem
@ -28,19 +29,21 @@ class FileManager:
current_process().name = process_title
terminator = Terminator()
channel_count = len(channel_from_q)
channel_received = None
last_known_show_plan = [[]]*channel_count
next_channel_preload = 0
last_known_item_ids = [[]]*channel_count
self.channel_count = len(channel_from_q)
self.channel_received = None
self.last_known_show_plan = [[]]*self.channel_count
self.next_channel_preload = 0
self.known_channels_preloaded = [False]*self.channel_count
self.known_channels_normalised = [False]*self.channel_count
self.last_known_item_ids = [[]]*self.channel_count
try:
while not terminator.terminate:
# If all channels have received the delete command, reset for the next one.
if (channel_received == None or channel_received == [True]*channel_count):
channel_received = [False]*channel_count
if (self.channel_received == None or self.channel_received == [True]*self.channel_count):
self.channel_received = [False]*self.channel_count
for channel in range(channel_count):
for channel in range(self.channel_count):
try:
message = channel_from_q[channel].get_nowait()
except Exception:
@ -53,11 +56,11 @@ class FileManager:
# If we have requested a new show plan, empty the music-tmp directory for the previous show.
if command == "GET_PLAN":
if channel_received != [False]*channel_count and channel_received[channel] != True:
if self.channel_received != [False]*self.channel_count and self.channel_received[channel] != True:
# We've already received a delete trigger on a channel, let's not delete the folder more than once.
# If the channel was already in the process of being deleted, the user has requested it again, so allow it.
channel_received[channel] = True
self.channel_received[channel] = True
continue
# Delete the previous show files!
@ -80,7 +83,9 @@ class FileManager:
except Exception:
self.logger.log.warning("Failed to remove, skipping. Likely file is still in use.")
continue
channel_received[channel] = True
self.channel_received[channel] = True
self.known_channels_preloaded = [False]*self.channel_count
self.known_channels_normalised = [False]*self.channel_count
# If we receive a new status message, let's check for files which have not been pre-loaded.
if command == "STATUS":
@ -96,44 +101,123 @@ class FileManager:
# If the new status update has a different order / list of items, let's update the show plan we know about
# This will trigger the chunk below to do the rounds again and preload any new files.
if item_ids != last_known_item_ids[channel]:
last_known_item_ids[channel] = item_ids
last_known_show_plan[channel] = show_plan
if item_ids != self.last_known_item_ids[channel]:
self.last_known_item_ids[channel] = item_ids
self.last_known_show_plan[channel] = show_plan
self.known_channels_preloaded[channel] = False
except Exception:
self.logger.log.exception("Failed to handle message {} on channel {}.".format(message, channel))
# Let's try preload / normalise some files now we're free of messages.
preloaded = self.do_preload()
normalised = self.do_normalise()
# Right, let's have a quick check in the status for shows without filenames, to preload them.
delay = True
for i in range(len(last_known_show_plan[next_channel_preload])):
if (not preloaded and not normalised):
# We didn't do any hard work, let's sleep.
sleep(0.2)
item_obj = PlanItem(last_known_show_plan[next_channel_preload][i])
if not item_obj.filename:
self.logger.log.info("Checking pre-load on channel {}, weight {}: {}".format(next_channel_preload, item_obj.weight, item_obj.name))
# Getting the file name will only pull the new file if the file doesn't already exist, so this is not too inefficient.
item_obj.filename,did_download = sync(self.api.get_filename(item_obj, True))
# Alright, we've done one, now let's give back control to process new statuses etc.
# Save back the resulting item back in regular dict form
last_known_show_plan[next_channel_preload][i] = item_obj.__dict__
if did_download:
# Given we probably took some time to download, let's not sleep in the loop.
delay = False
self.logger.log.info("File successfully preloaded: {}".format(item_obj.filename))
break
else:
# We didn't download anything this time, file was already loaded.
# Let's try the next one.
continue
next_channel_preload += 1
if next_channel_preload >= channel_count:
next_channel_preload = 0
if delay:
sleep(0.1)
except Exception as e:
self.logger.log.exception(
"Received unexpected exception: {}".format(e))
del self.logger
# Attempt to preload a file onto disk.
def do_preload(self):
channel = self.next_channel_preload
# All channels have preloaded all files, do nothing.
if (self.known_channels_preloaded == [True]*self.channel_count):
return False # Didn't preload anything
# Right, let's have a quick check in the status for shows without filenames, to preload them.
# Keep an eye on if we downloaded anything.
# If we didn't, we know that all items in this channel have been downloaded.
downloaded_something = False
for i in range(len(self.last_known_show_plan[channel])):
item_obj = PlanItem(self.last_known_show_plan[channel][i])
# We've not downloaded this file yet, let's do that.
if not item_obj.filename:
self.logger.log.info("Checking pre-load on channel {}, weight {}: {}".format(channel, item_obj.weight, item_obj.name))
# Getting the file name will only pull the new file if the file doesn't already exist, so this is not too inefficient.
item_obj.filename,did_download = sync(self.api.get_filename(item_obj, True))
# Alright, we've done one, now let's give back control to process new statuses etc.
# Save back the resulting item back in regular dict form
self.last_known_show_plan[channel][i] = item_obj.__dict__
if did_download:
downloaded_something = True
self.logger.log.info("File successfully preloaded: {}".format(item_obj.filename))
break
else:
# We didn't download anything this time, file was already loaded.
# Let's try the next one.
continue
# Tell the file manager that this channel is fully downloaded, this is so it can consider normalising once all channels have files.
self.known_channels_preloaded[channel] = not downloaded_something
self.next_channel_preload += 1
if self.next_channel_preload >= self.channel_count:
self.next_channel_preload = 0
return downloaded_something
# If we've preloaded everything, get to work normalising tracks before playback.
def do_normalise(self):
# Some channels still have files to preload, do nothing.
if (self.known_channels_preloaded != [True]*self.channel_count):
return False # Didn't normalise
# Quit early if all channels are normalised already.
if (self.known_channels_normalised == [True]*self.channel_count):
return False
channel = self.next_channel_preload
normalised_something = False
# Look through all the show plan files
for i in range(len(self.last_known_show_plan[channel])):
item_obj = PlanItem(self.last_known_show_plan[channel][i])
filename = item_obj.filename
if not filename:
self.logger.log.exception("Somehow got empty filename when all channels are preloaded.")
continue # Try next song.
elif (not os.path.isfile(filename)):
self.logger.log.exception("Filename for normalisation does not exist. This is bad.")
continue
elif "normalised" in filename:
continue
# Sweet, we now need to try generating a normalised version.
try:
self.logger.log.info("Normalising on channel {}: {}".format(channel,filename))
# This will return immediately if we already have a normalised file.
item_obj.filename = generate_normalised_file(filename)
# TODO Hacky
self.last_known_show_plan[channel][i] = item_obj.__dict__
normalised_something = True
break # Now go let another channel have a go.
except Exception as e:
self.logger.log.exception("Failed to generate normalised file.", str(e))
continue
self.known_channels_normalised[channel] = not normalised_something
self.next_channel_preload += 1
if self.next_channel_preload >= self.channel_count:
self.next_channel_preload = 0
return normalised_something

View file

@ -156,23 +156,22 @@ class MyRadioAPI:
payload = json.loads(await request)["payload"]
shows = []
if not payload["current"]:
self._logException("API did not return a current show.")
else:
shows.append(payload["current"])
if not payload["next"]:
self._logException("API did not return a list of next shows.")
else:
shows.extend(payload["next"])
shows = []
shows.append(payload["current"])
shows.extend(payload["next"])
timeslots = []
# Remove jukebox etc
for show in shows:
if not "timeslot_id" in show:
shows.remove(show)
# TODO filter out jukebox
return shows
async def get_showplan(self, timeslotid: int):

49
helpers/normalisation.py Normal file
View file

@ -0,0 +1,49 @@
import os
from helpers.os_environment import resolve_external_file_path
from pydub import AudioSegment, effects # Audio leveling!
# Stuff to help make BAPSicle play out leveled audio.
def match_target_amplitude(sound, target_dBFS):
change_in_dBFS = target_dBFS - sound.dBFS
return sound.apply_gain(change_in_dBFS)
# Takes filename in, normalialises it and returns a normalised file path.
def generate_normalised_file(filename: str):
if (not (isinstance(filename, str) and filename.endswith(".mp3"))):
raise ValueError("Invalid filename given.")
# Already normalised.
if filename.endswith("-normalised.mp3"):
return filename
normalised_filename = "{}-normalised.mp3".format(filename.rsplit(".",1)[0])
# The file already exists, short circuit.
if (os.path.exists(normalised_filename)):
return normalised_filename
sound = AudioSegment.from_file(filename, "mp3")
normalised_sound = effects.normalize(sound) #match_target_amplitude(sound, -10)
normalised_sound.export(normalised_filename, bitrate="320k", format="mp3")
return normalised_filename
# Returns either a normalised file path (based on filename), or the original if not available.
def get_normalised_filename_if_available(filename:str):
if (not (isinstance(filename, str) and filename.endswith(".mp3"))):
raise ValueError("Invalid filename given.")
# Already normalised.
if filename.endswith("-normalised.mp3"):
return filename
normalised_filename = "{}-normalised.mp3".format(filename.rstrip(".mp3"))
# normalised version exists
if (os.path.exists(normalised_filename)):
return normalised_filename
# Else we've not got a normalised verison, just take original.
return filename

View file

@ -35,6 +35,7 @@ from mutagen.mp3 import MP3
from syncer import sync
from threading import Timer
from helpers.normalisation import get_normalised_filename_if_available
from helpers.myradio_api import MyRadioAPI
from helpers.state_manager import StateManager
from helpers.logging_manager import LoggingManager
@ -428,6 +429,9 @@ class Player:
if not loaded_item.filename:
return False
# Swap with a normalised version if it's ready, else returns original.
loaded_item.filename = get_normalised_filename_if_available(loaded_item.filename)
self.state.update("loaded_item", loaded_item)
for i in range(len(showplan)):
@ -437,6 +441,10 @@ class Player:
# TODO: Update the show plan filenames???
load_attempt = 0
if not isinstance(loaded_item.filename, str):
return False
while load_attempt < 5:
load_attempt += 1
try:
@ -457,10 +465,11 @@ class Player:
continue # Try loading again.
try:
if ".mp3" in loaded_item.filename:
if loaded_item.filename.endswith(".mp3"):
song = MP3(loaded_item.filename)
self.state.update("length", song.info.length)
else:
# WARNING! Pygame / SDL can't seek .wav files :/
self.state.update(
"length", mixer.Sound(
loaded_item.filename).get_length() / 1000

93
ui-static/vendor/fonts/OFL.txt vendored Normal file
View file

@ -0,0 +1,93 @@
Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

126
ui-static/vendor/fonts/nunito.css vendored Normal file
View file

@ -0,0 +1,126 @@
/* nunito-200 - latin */
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 200;
src: local(''),
url('nunito-v16-latin-200.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-200.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-200italic - latin */
@font-face {
font-family: 'Nunito';
font-style: italic;
font-weight: 200;
src: local(''),
url('nunito-v16-latin-200italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-200italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-300 - latin */
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 300;
src: local(''),
url('nunito-v16-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-regular - latin */
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 400;
src: local(''),
url('nunito-v16-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-italic - latin */
@font-face {
font-family: 'Nunito';
font-style: italic;
font-weight: 400;
src: local(''),
url('nunito-v16-latin-italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-300italic - latin */
@font-face {
font-family: 'Nunito';
font-style: italic;
font-weight: 300;
src: local(''),
url('nunito-v16-latin-300italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-300italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-600 - latin */
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 600;
src: local(''),
url('nunito-v16-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-600italic - latin */
@font-face {
font-family: 'Nunito';
font-style: italic;
font-weight: 600;
src: local(''),
url('nunito-v16-latin-600italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-600italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-700 - latin */
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 700;
src: local(''),
url('nunito-v16-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-700italic - latin */
@font-face {
font-family: 'Nunito';
font-style: italic;
font-weight: 700;
src: local(''),
url('nunito-v16-latin-700italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-700italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-800 - latin */
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 800;
src: local(''),
url('nunito-v16-latin-800.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-800.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-800italic - latin */
@font-face {
font-family: 'Nunito';
font-style: italic;
font-weight: 800;
src: local(''),
url('nunito-v16-latin-800italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-800italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-900 - latin */
@font-face {
font-family: 'Nunito';
font-style: normal;
font-weight: 900;
src: local(''),
url('nunito-v16-latin-900.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-900.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}
/* nunito-900italic - latin */
@font-face {
font-family: 'Nunito';
font-style: italic;
font-weight: 900;
src: local(''),
url('nunito-v16-latin-900italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
url('nunito-v16-latin-900italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
}

View file

@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% block content_inner %}
<div class="text-center">
<div class="error-big mx-auto" data-text="404">404</div>
<div class="error error-big mx-auto" data-text="404">404</div>
<p class="lead text-gray-800 mb-5">Page Not Found</p>
<p class="text-gray-900 mb-0">Looks like you fell off the tip of the iceberg.</p>
<a href="/">← Escape Back Home</a>

View file

@ -14,7 +14,7 @@
<!-- Custom fonts for this template-->
<link href="/static/vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i" rel="stylesheet">
<link href="/static/vendor/fonts/nunito.css" rel="stylesheet">
<!-- Custom styles for this template-->
<link href="/static/css/sb-admin-2.css" rel="stylesheet">
@ -24,30 +24,27 @@
<body class="bg-gradient-primary">
<div class="container">
<div class="container mt-5">
<!-- Outer Row -->
<div class="row justify-content-center">
<div class="col-xl-10 col-lg-12 col-md-9">
<div class="mt-5">
<a href="/">
<h1 class="h1 text-light d-inline">BAPSicle</h1>
</a>
{% if data.ui_menu is undefined or data.ui_menu is true %}
<div class="d-inline float-right">
<a href="/status" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'status' %}active{% endif %}">
Status
</a>
<a href="/config/player" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'config' %}active{% endif %}">
Player Config
</a>
<a href="/config/server" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'server' %}active{% endif %}">
Server Config
</a>
<a href="/logs" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'logs' %}active{% endif %}">
Logs
<div class="d-flex justify-content-between">
<a href="/">
<h1 class="h1 text-light">BAPSicle</h1>
</a>
{% if data.ui_menu is undefined or data.ui_menu is true %}
<div>
<a href="/status" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'status' %}active{% endif %}">
Status
</a>
<a href="/config/player" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'config' %}active{% endif %}">
Player Config
</a>
<a href="/config/server" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'server' %}active{% endif %}">
Server Config
</a>
<a href="/logs" class="btn btn-user btn-outline-light btn-primary ml-4 {% if data.ui_page == 'logs' %}active{% endif %}">
Logs
</a>
</div>
</div>
{% endif %}
<div class="card o-hidden border-0 shadow-lg my-3">
@ -56,7 +53,7 @@
<div class="card-body p-0">
<!-- Nested Row within Card Body -->
<div class="row">
<div class="col-lg-12">
<div class="col">
<div class="px-4 pt-5 pb-3">
<div class="text-center">
<h2 class="h3 text-gray-900">{{ data.ui_title }}</h2>
@ -77,11 +74,8 @@
{% endblock %}
</div>
</div>
</div>
</div>
</div>
</body>

View file

@ -1,7 +1,8 @@
from helpers.normalisation import get_normalised_filename_if_available
from helpers.myradio_api import MyRadioAPI
from sanic import Sanic
from sanic import Sanic, log
from sanic.exceptions import NotFound, abort
from sanic.response import html, text, file, redirect
from sanic.response import html, file, redirect
from sanic.response import json as resp_json
from sanic_cors import CORS
from syncer import sync
@ -10,7 +11,6 @@ import asyncio
from jinja2 import Environment, FileSystemLoader
from urllib.parse import unquote
from setproctitle import setproctitle
import logging
from typing import Any, Optional, List
from multiprocessing.queues import Queue
from queue import Empty
@ -25,7 +25,57 @@ from helpers.state_manager import StateManager
from helpers.the_terminator import Terminator
env = Environment(loader=FileSystemLoader('%s/ui-templates/' % os.path.dirname(__file__)))
app = Sanic("BAPSicle Web Server")
# From Sanic's default, but set to log to file.
LOGGING_CONFIG = dict(
version=1,
disable_existing_loggers=False,
loggers={
"sanic.root": {"level": "INFO", "handlers": ["file"]},
"sanic.error": {
"level": "INFO",
"handlers": ["error_file"],
"propagate": True,
"qualname": "sanic.error",
},
"sanic.access": {
"level": "INFO",
"handlers": ["access_file"],
"propagate": True,
"qualname": "sanic.access",
},
},
handlers={
"file": {
"class": "logging.FileHandler",
"formatter": "generic",
"filename": "logs/WebServer.log"
},
"error_file": {
"class": "logging.FileHandler",
"formatter": "generic",
"filename": "logs/WebServer.log"
},
"access_file": {
"class": "logging.FileHandler",
"formatter": "access",
"filename": "logs/WebServer.log"
},
},
formatters={
"generic": {
"format": "%(asctime)s | [%(process)d] [%(levelname)s] %(message)s",
"class": "logging.Formatter",
},
"access": {
"format": "%(asctime)s | (%(name)s)[%(levelname)s][%(host)s]: "
+ "%(request)s %(message)s %(status)d %(byte)d",
"class": "logging.Formatter",
},
},
)
app = Sanic("BAPSicle Web Server", log_config=LOGGING_CONFIG)
def render_template(file, data, status=200):
@ -313,15 +363,24 @@ def json_status(request):
async def audio_file(request, type: str, id: int):
if type not in ["managed", "track"]:
abort(404)
return await file("music-tmp/" + type + "-" + str(id) + ".mp3")
filename = resolve_external_file_path("music-tmp/{}-{}.mp3".format(type,id))
# Swap with a normalised version if it's ready, else returns original.
filename = get_normalised_filename_if_available(filename)
# Send file or 404
return await file(filename)
# Static Files
app.static("/favicon.ico", resolve_local_file_path("ui-static/favicon.ico"), name="ui-favicon")
app.static("/static", resolve_local_file_path("ui-static"), name="ui-static")
dist_directory = resolve_local_file_path("presenter-build")
app.static('/presenter', dist_directory)
app.static("/presenter/", resolve_local_file_path("presenter-build/index.html"),
strict_slashes=True, name="presenter-index")
app.static("/presenter/", resolve_local_file_path("presenter-build"))
# Helper Functions