forked from UniversityRadioYork/BAPSicle
-
Notifications
You must be signed in to change notification settings - Fork 0
/
file_manager.py
277 lines (229 loc) · 11.8 KB
/
file_manager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
from helpers.state_manager import StateManager
from helpers.os_environment import isWindows, resolve_external_file_path
from typing import List
from setproctitle import setproctitle
from multiprocessing import current_process, Queue
from time import sleep
import os
import json
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
class FileManager:
logger: LoggingManager
api: MyRadioAPI
def __init__(self, channel_from_q: List[Queue], server_config: StateManager):
self.logger = LoggingManager("FileManager")
self.api = MyRadioAPI(self.logger, server_config)
process_title = "File Manager"
setproctitle(process_title)
current_process().name = process_title
terminator = Terminator()
self.normalisation_mode = server_config.get()["normalisation_mode"]
if self.normalisation_mode != "on":
self.logger.log.info("Normalisation is disabled.")
else:
self.logger.log.info("Normalisation is enabled.")
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 (
self.channel_received is None
or self.channel_received == [True] * self.channel_count
):
self.channel_received = [False] * self.channel_count
for channel in range(self.channel_count):
try:
message = channel_from_q[channel].get_nowait()
except Exception:
continue
try:
# source = message.split(":")[0]
command = message.split(":", 2)[1]
# If we have requested a new show plan, empty the music-tmp directory for the previous show.
if command == "GETPLAN":
if (
self.channel_received != [
False] * self.channel_count
and self.channel_received[channel] is False
):
# 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.
self.channel_received[channel] = True
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:
if isWindows():
filepath = path + "\\" + file
else:
filepath = path + "/" + file
self.logger.log.info(
"Removing file {} on new show load.".format(
filepath
)
)
try:
os.remove(filepath)
except Exception:
self.logger.log.warning(
"Failed to remove, skipping. Likely file is still in use."
)
continue
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":
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.
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()
if not preloaded and not normalised:
# We didn't do any hard work, let's sleep.
sleep(0.2)
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):
if self.normalisation_mode != "on":
return False
# 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