Browse Source

feat: Support for m3u playlists

Matthias Ladkau 1 year ago
parent
commit
1b0758a072

+ 7 - 0
musikautomat/handler/__init__.py

@@ -1,5 +1,7 @@
 #!./bin/python3
 
+import os
+
 EVENT_UP = "event_up"
 EVENT_DOWN = "event_down"
 EVENT_LEFT = "event_left"
@@ -43,3 +45,8 @@ class BaseHandler:
         if event == EVENT_LEFT:
             self.stop()
             raise HandlerExit
+
+
+    def toDisplayName(self, s):
+        s = os.path.splitext(s)[0]
+        return s.replace("_", " ").title()

+ 77 - 0
musikautomat/handler/listhandler.py

@@ -0,0 +1,77 @@
+#!./bin/python3
+
+import os
+
+from handler import BaseHandler, EVENT_UP, EVENT_RIGHT, EVENT_DOWN
+from handler.lib.pymplb import MPlayer
+
+class BaseListHandler(BaseHandler):
+
+    def __init__(self, config, display):
+        super().__init__(config, display)
+        self.items = []
+
+
+    def setItems(self, items):
+        self.items = items
+        self.item_pointer = 0
+
+
+    def update(self):
+        '''
+        Called when the handler should update the display.
+        '''
+
+        # Show the slice of the main menu which is relevant
+
+        playlist_display = []
+
+        maxlines = self.display.drawlines
+
+        for i, item in enumerate(self.items):
+            if len(playlist_display) <= maxlines:
+                if i >= self.item_pointer - maxlines and i <= self.item_pointer + maxlines:
+                    playlist_display.append(item.get("name", "<unknown>"))
+            else:
+                break
+
+        self.display.update(playlist_display, min(self.item_pointer, maxlines))
+
+
+    def emitEvent(self, event):
+        '''
+        Called when an event has happened.
+        '''
+        super().emitEvent(event)
+
+        if event == EVENT_RIGHT:
+            item = self.items[self.item_pointer]
+            self.itemSelected(item)
+
+        if event == EVENT_UP:
+            self.item_pointer -=1
+
+        if event == EVENT_DOWN:
+            self.item_pointer +=1
+
+        if self.item_pointer >= len(self.items):
+            self.item_pointer = 0
+
+        if self.item_pointer < 0:
+            self.item_pointer = len(self.items) - 1
+
+
+    def nextItem(self):
+        self.item_pointer +=1
+        if self.item_pointer >= len(self.items):
+            self.item_pointer = 0
+        item = self.items[self.item_pointer]
+        self.itemSelected(item)
+        self.update()
+
+
+    def itemSelected(self, item):
+        '''
+        Called when an item has been selected by the user.
+        '''
+        pass

+ 105 - 0
musikautomat/handler/m3u.py

@@ -0,0 +1,105 @@
+#!./bin/python3
+
+import os
+import threading
+import time
+
+from handler import EVENT_LEFT
+from handler.listhandler import BaseListHandler
+from handler.lib.pymplb import MPlayer
+
+class M3UHandler(BaseListHandler):
+
+    def __init__(self, config, display):
+        super().__init__(config, display)
+        self._mplayer = None
+        self._mplayer_watcher = None
+        self.is_playing = False
+        self._last_event = time.time()
+
+    def setPlaylistItem(self, item):
+        '''
+        Called with the selected item.
+        '''
+        self.item = item
+
+        path = item["path"]
+        items = []
+
+        with open(path) as f:
+            for line in f:
+                item_path = str(line).strip()
+                if not item_path.startswith("#"):
+                    items.append({
+                        "name" : self.toDisplayName(os.path.basename(item_path)),
+                        "path" : os.path.join(os.path.dirname(path), item_path)
+                    })
+
+        if self._mplayer is None:
+            self._mplayer = MPlayer()
+            self.setItems(items)
+            self._mplayer_watcher = threading.Thread(target=self._watch_mplayer, daemon=True)
+            self._mplayer_watcher.start()
+
+        self.is_playing = False
+
+
+    def itemSelected(self, item):
+        self._mplayer.loadfile(item["path"])
+        self.is_playing = True
+
+
+    def stop(self):
+        '''
+        Called when the handler should stop.
+        '''
+        if self._mplayer is not None:
+            self._mplayer.quit()
+            self._mplayer = None
+            self._mplayer_watcher = None
+
+
+    def emitEvent(self, event):
+        '''
+        Called when an event has happened.
+        '''
+
+        # Clock the time so the thread can hold off if there
+        # was a recent user event
+
+        self._last_event = time.time()
+
+        if event == EVENT_LEFT and self.is_playing:
+            self._mplayer.stop()
+            self.is_playing = False
+            return
+
+        super().emitEvent(event)
+
+
+    def _watch_mplayer(self):
+        '''
+        Thread to watch mplayer and advance the playlist.
+        '''
+
+        while self._mplayer_watcher is not None:
+
+            try:
+                time.sleep(0.5)
+
+                if self._mplayer:
+                    playing_path = self._mplayer.p_path
+
+                    # Only do something in the thread if the last user
+                    # input is at least 3 seconds ago
+
+                    if time.time() - self._last_event > 3:
+
+                        # Advance to the next item if we are playing music
+
+                        if self.is_playing and playing_path == "(null)":
+                            self.nextItem()
+
+            except Exception as e:
+                print("Error watching player:", e)
+

+ 6 - 3
musikautomat/menu.py

@@ -9,7 +9,7 @@ import random
 import eyed3
 import tempfile
 import handler
-import handler.stream
+from handler import stream, m3u
 
 class Menu:
     '''
@@ -23,7 +23,8 @@ class Menu:
         # Register default handlers
 
         self.handler = {
-            "stream" : handler.stream.StreamHandler(config, display)
+            "stream" : stream.StreamHandler(config, display),
+            "m3u" : m3u.M3UHandler(config, display)
         }
 
         # Playlist data
@@ -68,7 +69,9 @@ class Menu:
                 self._current_item.emitEvent(event)
             except handler.HandlerExit as e:
                 self._current_item = None
-                self.update()
+
+            self.update()
+
             return
 
         if event == handler.EVENT_RIGHT: