]> git.0d.be Git - botaradio.git/blob - media/playlist.py
dc489feed40497afc99601bd3c0d22ad40bfed70
[botaradio.git] / media / playlist.py
1 import json
2 import random
3 import threading
4 import logging
5 import random
6
7 import variables as var
8 from media.file import FileItem
9 from media.url import URLItem
10 from media.url_from_playlist import PlaylistURLItem
11 from media.radio import RadioItem
12 from database import MusicDatabase
13 from media.library import MusicLibrary
14
15 class PlaylistItemWrapper:
16     def __init__(self, lib, id, type, user):
17         self.lib = lib
18         self.id = id
19         self.user = user
20         self.type = type
21         self.log = logging.getLogger("bot")
22         self.version = 0
23
24     def item(self):
25         return self.lib[self.id]
26
27     def to_dict(self):
28         dict = self.item().to_dict()
29         dict['user'] = self.user
30         return dict
31
32     def validate(self):
33         ret = self.item().validate()
34         if ret and self.item().version > self.version:
35             self.version = self.item().version
36             self.lib.save(self.id)
37         return ret
38
39     def prepare(self):
40         ret = self.item().prepare()
41         if ret and self.item().version > self.version:
42             self.version = self.item().version
43             self.lib.save(self.id)
44         return ret
45
46     def async_prepare(self):
47         th = threading.Thread(
48             target=self.prepare, name="Prepare-" + self.id[:7])
49         self.log.info(
50             "%s: start preparing item in thread: " % self.item().type + self.format_debug_string())
51         th.daemon = True
52         th.start()
53         return th
54
55     def uri(self):
56         return self.item().uri()
57
58     def add_tags(self, tags):
59         self.item().add_tags(tags)
60         if self.item().version > self.version:
61             self.version = self.item().version
62             self.lib.save(self.id)
63
64     def remove_tags(self, tags):
65         self.item().remove_tags(tags)
66         if self.item().version > self.version:
67             self.version = self.item().version
68             self.lib.save(self.id)
69
70     def clear_tags(self):
71         self.item().clear_tags()
72         if self.item().version > self.version:
73             self.version = self.item().version
74             self.lib.save(self.id)
75
76     def is_ready(self):
77         return self.item().is_ready()
78
79     def is_failed(self):
80         return self.item().is_failed()
81
82     def format_current_playing(self):
83         return self.item().format_current_playing(self.user)
84
85     def format_song_string(self):
86         return self.item().format_song_string(self.user)
87
88     def format_short_string(self):
89         return self.item().format_short_string()
90
91     def format_debug_string(self):
92         return self.item().format_debug_string()
93
94     def display_type(self):
95         return self.item().display_type()
96
97
98 def get_item_wrapper(bot, **kwargs):
99     item = var.library.get_item(bot, **kwargs)
100     if 'user' not in kwargs:
101         raise KeyError("Which user added this song?")
102     return PlaylistItemWrapper(var.library, item.id, kwargs['type'], kwargs['user'])
103
104 def get_item_wrapper_by_id(bot, id, user):
105     item = var.library.get_item_by_id(bot, id)
106     if item:
107         return PlaylistItemWrapper(var.library, item.id, item.type, user)
108     else:
109         return None
110
111 def get_item_wrappers_by_tags(bot, tags, user):
112     items = var.library.get_items_by_tags(bot, tags)
113     ret = []
114     for item in items:
115         ret.append(PlaylistItemWrapper(var.library, item.id, item.type, user))
116     return ret
117
118 def get_playlist(mode, _list=None, index=None):
119     if _list and index is None:
120         index = _list.current_index
121
122     if _list is None:
123         if mode == "one-shot":
124             return OneshotPlaylist()
125         elif mode == "repeat":
126             return RepeatPlaylist()
127         elif mode == "random":
128             return RandomPlaylist()
129         elif mode == "autoplay":
130             return AutoPlaylist()
131     else:
132         if mode == "one-shot":
133             return OneshotPlaylist().from_list(_list, index)
134         elif mode == "repeat":
135             return RepeatPlaylist().from_list(_list, index)
136         elif mode == "random":
137             return RandomPlaylist().from_list(_list, index)
138         elif mode == "autoplay":
139             return AutoPlaylist().from_list(_list, index)
140     raise
141
142 class BasePlaylist(list):
143     def __init__(self):
144         super().__init__()
145         self.current_index = -1
146         self.version = 0  # increase by one after each change
147         self.mode = "base"  # "repeat", "random"
148         self.pending_items = []
149         self.log = logging.getLogger("bot")
150         self.validating_thread_lock = threading.Lock()
151
152     def is_empty(self):
153         return True if len(self) == 0 else False
154
155     def from_list(self, _list, current_index):
156         self.version += 1
157         super().clear()
158         self.extend(_list)
159         self.current_index = current_index
160
161         return self
162
163     def append(self, item: PlaylistItemWrapper):
164         self.version += 1
165         super().append(item)
166         self.pending_items.append(item)
167         self.start_async_validating()
168
169         return item
170
171     def insert(self, index, item):
172         self.version += 1
173
174         if index == -1:
175             index = self.current_index
176
177         super().insert(index, item)
178
179         if index <= self.current_index:
180             self.current_index += 1
181
182         self.pending_items.append(item)
183         self.start_async_validating()
184
185         return item
186
187     def extend(self, items):
188         self.version += 1
189         super().extend(items)
190         self.pending_items.extend(items)
191         self.start_async_validating()
192         return items
193
194     def next(self):
195         if len(self) == 0:
196             return False
197
198         self.version += 1
199
200         if self.current_index < len(self) - 1:
201             self.current_index += 1
202             return self[self.current_index]
203         else:
204             return False
205
206     def point_to(self, index):
207         self.version += 1
208         if -1 <= index < len(self):
209             self.current_index = index
210
211     def find(self, id):
212         for index, wrapper in enumerate(self):
213             if wrapper.item.id == id:
214                 return index
215         return None
216
217     def __delitem__(self, key):
218         return self.remove(key)
219
220     def remove(self, index):
221         self.version += 1
222         if index > len(self) - 1:
223             return False
224
225         removed = self[index]
226         super().__delitem__(index)
227
228         if self.current_index > index:
229             self.current_index -= 1
230
231         # reference counter
232         counter = 0
233         for wrapper in self:
234             if wrapper.id == removed.id:
235                 counter += 1
236
237         if counter == 0:
238             var.library.free(removed.id)
239         return removed
240
241     def remove_by_id(self, id):
242         self.version += 1
243         to_be_removed = []
244         for index, wrapper in enumerate(self):
245             if wrapper.id == id:
246                 to_be_removed.append(index)
247
248         for index in to_be_removed:
249             self.remove(index)
250
251     def current_item(self):
252         if len(self) == 0:
253             return False
254
255         return self[self.current_index]
256
257     def next_index(self):
258         if self.current_index < len(self) - 1:
259             return self.current_index + 1
260         else:
261             return False
262
263     def next_item(self):
264         if self.current_index < len(self) - 1:
265             return self[self.current_index + 1]
266         else:
267             return False
268
269     def randomize(self):
270         # current_index will lose track after shuffling, thus we take current music out before shuffling
271         #current = self.current_item()
272         #del self[self.current_index]
273
274         random.shuffle(self)
275
276         #self.insert(0, current)
277         self.current_index = -1
278         self.version += 1
279
280     def clear(self):
281         self.version += 1
282         self.current_index = -1
283         var.library.free_all()
284         super().clear()
285
286     def save(self):
287         var.db.remove_section("playlist_item")
288         assert self.current_index is not None
289         var.db.set("playlist", "current_index", self.current_index)
290
291         for index, music in enumerate(self):
292             var.db.set("playlist_item", str(index), json.dumps({'id': music.id, 'user': music.user }))
293
294     def load(self):
295         current_index = var.db.getint("playlist", "current_index", fallback=-1)
296         if current_index == -1:
297             return
298
299         items = var.db.items("playlist_item")
300         if items:
301             music_wrappers = []
302             items.sort(key=lambda v: int(v[0]))
303             for item in items:
304                 item = json.loads(item[1])
305                 music_wrapper = get_item_wrapper_by_id(var.bot, item['id'], item['user'])
306                 if music_wrapper:
307                     music_wrappers.append(music_wrapper)
308             self.from_list(music_wrappers, current_index)
309
310     def _debug_print(self):
311         print("===== Playlist(%d)=====" % self.current_index)
312         for index, item_wrapper in enumerate(self):
313             if index == self.current_index:
314                 print("-> %d %s" % (index, item_wrapper.format_debug_string()))
315             else:
316                 print("%d %s" % (index, item_wrapper.format_debug_string()))
317         print("=====     End     =====")
318
319     def start_async_validating(self):
320         if not self.validating_thread_lock.locked():
321             th = threading.Thread(target=self._check_valid, name="Validating")
322             th.daemon = True
323             th.start()
324
325     def _check_valid(self):
326         self.log.debug("playlist: start validating...")
327         self.validating_thread_lock.acquire()
328         while len(self.pending_items) > 0:
329             item = self.pending_items.pop()
330             self.log.debug("playlist: validating %s" % item.format_debug_string())
331             if not item.validate() or item.is_failed():
332                 self.log.debug("playlist: validating failed.")
333                 var.library.delete(item.id)
334                 self.remove_by_id(item.id)
335
336         self.log.debug("playlist: validating finished.")
337         self.validating_thread_lock.release()
338
339
340 class OneshotPlaylist(BasePlaylist):
341     def __init__(self):
342         super().__init__()
343         self.mode = "one-shot"
344         self.current_index = -1
345
346     def from_list(self, _list, current_index):
347         if len(_list) > 0:
348             if current_index > -1:
349                 for i in range(current_index):
350                     _list.pop(0)
351                 return super().from_list(_list, 0)
352             return super().from_list(_list, -1)
353         return self
354
355     def next(self):
356         if len(self) == 0:
357             return False
358
359         self.version += 1
360
361         if len(self) > 0:
362             if self.current_index != -1:
363                 super().__delitem__(self.current_index)
364                 if len(self) == 0:
365                     return False
366             else:
367                 self.current_index = 0
368             return self[0]
369
370         else:
371             self.clear()
372             return False
373
374     def next_index(self):
375         if len(self) > 1:
376             return 1
377         else:
378             return False
379
380     def next_item(self):
381         if len(self) > 1:
382             return self[1]
383         else:
384             return False
385
386     def point_to(self, index):
387         self.version += 1
388         self.current_index = -1
389         for i in range(index + 1):
390             super().__delitem__(0)
391
392
393 class RepeatPlaylist(BasePlaylist):
394     def __init__(self):
395         super().__init__()
396         self.mode = "repeat"
397
398     def next(self):
399         if len(self) == 0:
400             return False
401
402         self.version += 1
403
404         if self.current_index < len(self) - 1:
405             self.current_index += 1
406             return self[self.current_index]
407         else:
408             self.current_index = 0
409             return self[0]
410
411     def next_index(self):
412         if self.current_index < len(self) - 1:
413             return self.current_index + 1
414         else:
415             return 0
416
417     def next_item(self):
418         return self[self.next_index()]
419
420
421 class RandomPlaylist(BasePlaylist):
422     def __init__(self):
423         super().__init__()
424         self.mode = "random"
425
426     def from_list(self, _list, current_index):
427         self.version += 1
428         random.shuffle(_list)
429         return super().from_list(_list, -1)
430
431     def next(self):
432         if len(self) == 0:
433             return False
434
435         self.version += 1
436
437         if self.current_index < len(self) - 1:
438             self.current_index += 1
439             return self[self.current_index]
440         else:
441             self.randomize()
442             self.current_index = 0
443             return self[0]
444
445
446 class AutoPlaylist(BasePlaylist):
447     def __init__(self):
448         super().__init__()
449         self.mode = "autoplay"
450
451     def refresh(self):
452         _list = []
453         ids = var.music_db.query_all_ids()
454         for _ in range(20):
455             _list.append(get_item_wrapper_by_id(var.bot, ids[random.randint(0, len(ids)-1)], 'AutoPlay'))
456         self.from_list(_list, -1)
457
458     # def from_list(self, _list, current_index):
459     #     self.version += 1
460     #     self.refresh()
461     #     return self
462
463     def clear(self):
464         super().clear()
465         self.refresh()
466
467     def next(self):
468         if len(self) == 0:
469             self.refresh()
470             return False
471
472         self.version += 1
473
474         if self.current_index < len(self) - 1:
475             self.current_index += 1
476             return self[self.current_index]
477         else:
478             self.refresh()
479             self.current_index = 0
480             return self[0]