Merge branch 'dev' into faderama
This commit is contained in:
commit
9e6db5bbf0
38 changed files with 509 additions and 97 deletions
|
@ -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
|
||||
|
|
168
file_manager.py
168
file_manager.py
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
49
helpers/normalisation.py
Normal 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
|
||||
|
11
player.py
11
player.py
|
@ -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
93
ui-static/vendor/fonts/OFL.txt
vendored
Normal 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.
|
BIN
ui-static/vendor/fonts/nunito-v16-latin-200.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-200.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-200.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-200.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-200italic.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-200italic.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-200italic.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-200italic.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-300.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-300.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-300.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-300.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-300italic.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-300italic.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-300italic.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-300italic.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-600.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-600.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-600.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-600.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-600italic.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-600italic.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-600italic.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-600italic.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-700.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-700.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-700.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-700.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-700italic.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-700italic.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-700italic.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-700italic.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-800.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-800.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-800.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-800.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-800italic.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-800italic.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-800italic.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-800italic.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-900.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-900.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-900.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-900.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-900italic.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-900italic.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-900italic.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-900italic.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-italic.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-italic.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-italic.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-italic.woff2
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-regular.woff
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-regular.woff
vendored
Normal file
Binary file not shown.
BIN
ui-static/vendor/fonts/nunito-v16-latin-regular.woff2
vendored
Normal file
BIN
ui-static/vendor/fonts/nunito-v16-latin-regular.woff2
vendored
Normal file
Binary file not shown.
126
ui-static/vendor/fonts/nunito.css
vendored
Normal file
126
ui-static/vendor/fonts/nunito.css
vendored
Normal 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+ */
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue