2021-04-25 23:18:50 +00:00
|
|
|
from helpers.state_manager import StateManager
|
2021-05-11 22:52:34 +00:00
|
|
|
from helpers.os_environment import isWindows, resolve_external_file_path
|
2021-04-25 22:05:31 +00:00
|
|
|
from typing import List
|
|
|
|
from setproctitle import setproctitle
|
|
|
|
from multiprocessing import current_process, Queue
|
|
|
|
from time import sleep
|
|
|
|
import os
|
2021-04-25 23:18:50 +00:00
|
|
|
import json
|
|
|
|
from syncer import sync
|
2021-04-25 22:05:31 +00:00
|
|
|
|
|
|
|
from helpers.logging_manager import LoggingManager
|
|
|
|
from helpers.the_terminator import Terminator
|
2021-04-25 23:18:50 +00:00
|
|
|
from helpers.myradio_api import MyRadioAPI
|
2021-08-16 22:43:09 +00:00
|
|
|
from helpers.normalisation import generate_normalised_file
|
2021-04-25 23:18:50 +00:00
|
|
|
from baps_types.plan import PlanItem
|
2021-04-25 22:05:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
class FileManager:
|
|
|
|
logger: LoggingManager
|
2021-04-25 23:18:50 +00:00
|
|
|
api: MyRadioAPI
|
2021-04-25 22:05:31 +00:00
|
|
|
|
2021-04-25 23:18:50 +00:00
|
|
|
def __init__(self, channel_from_q: List[Queue], server_config: StateManager):
|
2021-04-25 22:05:31 +00:00
|
|
|
|
|
|
|
self.logger = LoggingManager("FileManager")
|
2021-04-25 23:18:50 +00:00
|
|
|
self.api = MyRadioAPI(self.logger, server_config)
|
|
|
|
|
2021-04-25 22:05:31 +00:00
|
|
|
process_title = "File Manager"
|
|
|
|
setproctitle(process_title)
|
|
|
|
current_process().name = process_title
|
|
|
|
|
|
|
|
terminator = Terminator()
|
2021-08-16 22:43:09 +00:00
|
|
|
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.last_known_item_ids = [[]]*self.channel_count
|
2021-04-25 22:05:31 +00:00
|
|
|
try:
|
|
|
|
|
|
|
|
while not terminator.terminate:
|
|
|
|
# If all channels have received the delete command, reset for the next one.
|
2021-08-16 22:43:09 +00:00
|
|
|
if (self.channel_received == None or self.channel_received == [True]*self.channel_count):
|
|
|
|
self.channel_received = [False]*self.channel_count
|
2021-04-25 22:05:31 +00:00
|
|
|
|
2021-08-16 22:43:09 +00:00
|
|
|
for channel in range(self.channel_count):
|
2021-04-25 22:05:31 +00:00
|
|
|
try:
|
|
|
|
message = channel_from_q[channel].get_nowait()
|
2021-05-11 22:52:34 +00:00
|
|
|
except Exception:
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
2021-04-25 22:05:31 +00:00
|
|
|
#source = message.split(":")[0]
|
2021-04-25 23:18:50 +00:00
|
|
|
command = message.split(":",2)[1]
|
|
|
|
|
|
|
|
# If we have requested a new show plan, empty the music-tmp directory for the previous show.
|
2021-04-25 22:05:31 +00:00
|
|
|
if command == "GET_PLAN":
|
|
|
|
|
2021-08-16 22:43:09 +00:00
|
|
|
if self.channel_received != [False]*self.channel_count and self.channel_received[channel] != True:
|
2021-04-25 22:05:31 +00:00
|
|
|
# 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.
|
|
|
|
|
2021-08-16 22:43:09 +00:00
|
|
|
self.channel_received[channel] = True
|
2021-04-25 22:05:31 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
# Delete the previous show files!
|
|
|
|
# Note: The players load into RAM. If something is playing over the load, the source file can still be deleted.
|
|
|
|
path: str = resolve_external_file_path("/music-tmp/")
|
|
|
|
|
|
|
|
if not os.path.isdir(path):
|
|
|
|
self.logger.log.warning("Music-tmp folder is missing, not handling.")
|
|
|
|
continue
|
|
|
|
|
|
|
|
files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
|
|
|
|
for file in files:
|
2021-05-11 22:52:34 +00:00
|
|
|
if isWindows():
|
|
|
|
filepath = path+"\\"+file
|
|
|
|
else:
|
|
|
|
filepath = path+"/"+file
|
2021-04-27 20:55:32 +00:00
|
|
|
self.logger.log.info("Removing file {} on new show load.".format(filepath))
|
2021-05-11 23:14:35 +00:00
|
|
|
try:
|
|
|
|
os.remove(filepath)
|
|
|
|
except Exception:
|
|
|
|
self.logger.log.warning("Failed to remove, skipping. Likely file is still in use.")
|
|
|
|
continue
|
2021-08-16 22:43:09 +00:00
|
|
|
self.channel_received[channel] = True
|
2021-04-25 22:05:31 +00:00
|
|
|
|
2021-04-25 23:18:50 +00:00
|
|
|
# If we receive a new status message, let's check for files which have not been pre-loaded.
|
|
|
|
if command == "STATUS":
|
|
|
|
extra = message.split(":",3)
|
|
|
|
if extra[2] != "OKAY":
|
|
|
|
continue
|
|
|
|
|
|
|
|
status = json.loads(extra[3])
|
|
|
|
show_plan = status["show_plan"]
|
|
|
|
item_ids = []
|
|
|
|
for item in show_plan:
|
|
|
|
item_ids += item["timeslotitemid"]
|
|
|
|
|
|
|
|
# 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.
|
2021-08-16 22:43:09 +00:00
|
|
|
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
|
2021-04-25 22:05:31 +00:00
|
|
|
|
|
|
|
except Exception:
|
2021-05-11 22:52:34 +00:00
|
|
|
self.logger.log.exception("Failed to handle message {} on channel {}.".format(message, channel))
|
2021-04-25 22:05:31 +00:00
|
|
|
|
2021-08-16 22:43:09 +00:00
|
|
|
# Let's try preload / normalise some files now we're free of messages.
|
|
|
|
preloaded = self.do_preload()
|
|
|
|
normalised = self.do_normalise()
|
2021-04-25 23:18:50 +00:00
|
|
|
|
2021-08-16 22:43:09 +00:00
|
|
|
if (not preloaded and not normalised):
|
|
|
|
# We didn't do any hard work, let's sleep.
|
|
|
|
sleep(0.5)
|
2021-04-25 23:18:50 +00:00
|
|
|
|
2021-04-25 22:05:31 +00:00
|
|
|
except Exception as e:
|
|
|
|
self.logger.log.exception(
|
|
|
|
"Received unexpected exception: {}".format(e))
|
|
|
|
del self.logger
|
2021-08-16 22:43:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
# Attempt to preload a file onto disk.
|
|
|
|
def do_preload(self):
|
|
|
|
channel = self.next_channel_preload
|
|
|
|
# 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
|
|
|
|
# Given we probably took some time to download, let's not sleep in the loop.
|
|
|
|
if not downloaded_something:
|
|
|
|
# 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] = True
|
|
|
|
|
|
|
|
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
|
|
|
|
# TODO: quit early if all channels are normalised already.
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
if "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.next_channel_preload += 1
|
|
|
|
if self.next_channel_preload >= self.channel_count:
|
|
|
|
self.next_channel_preload = 0
|
|
|
|
|
|
|
|
return normalised_something
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|