3 Commits a2f1e21b22 ... b0480867c7

Author SHA1 Message Date
  Matthias Ladkau b0480867c7 fix: First art display 4 years ago
  Matthias Ladkau 9f10af38cc chore: Remove trailing spaces 4 years ago
  Matthias Ladkau 6512fc229a fix: Adding webserver and proper playlist processing 4 years ago

+ 57 - 13
musikautomat/display.py

@@ -1,39 +1,44 @@
 #!./bin/python3
 
+import os
+
 import yaml
 import pygame # type: ignore
 from typing import Dict
+import random
 
 # Color constants
 
-BLACK = (0, 0, 0) 
-GREEN = (0, 255, 0) 
+BLACK = (0, 0, 0)
+GREEN = (0, 255, 0)
+WHITE = (255, 255, 255)
 
 class Display:
     '''
     Display object
     '''
     def __init__(self, config, pydsp):
-        
+
         # Set config values
-        
+
         self._dx = config.get("dimx", 128) # Drawing dimensions
         self._dy = config.get("dimy", 160)
         self._playlistFile = config.get("playlist", "music.yml") # Playlist data
         self._pydsp = pydsp # Pygame display object
         self._fontsize = config.get("fontsize", 12) # Fontsize to use
-        self._drawlines = int(self._dy / 12) - 1 # Max number of drawn lines
+        self._drawlines = int(self._dy / 12) - 5 # Max number of drawn lines
 
         # Set initialisation values
 
         self._selection_pointer = 0 # Current selected value
         self._line_offset = 0 # Drawing offset in playlist
         self._current_item = None
+        self._bgImg = None
 
 
-    def loadPlaylist(self) -> Dict:
+    def getPlaylist(self) -> Dict:
         '''
-        Load the playlist.
+        Return the playlist for this display.
         '''
         return yaml.safe_load(open(self._playlistFile)).get("playlist", [])
 
@@ -43,7 +48,7 @@ class Display:
         Load the data from the given playlist file and draw it according to the current state.
         '''
         self._pydsp.fill((0,0,0))
-        playlist = self.loadPlaylist()
+        playlist = self.getPlaylist()
 
         # Correct offset and selection pointer
 
@@ -55,7 +60,7 @@ class Display:
             self._selection_pointer = self._drawlines # Move the line offset up if possible
             if self._line_offset < len(playlist) - self._drawlines - 1:
                 self._line_offset+=1
-        
+
         # Draw lines
 
         font = pygame.font.Font('freesansbold.ttf', self._fontsize)
@@ -72,18 +77,57 @@ class Display:
 
             if drawline == self._selection_pointer:
                 text = font.render(item["name"], True, BLACK, GREEN)
+
+                pygame.draw.rect(self._pydsp, BLACK, (0,0,self._dx,50))
+                try:
+                    self.drawArt(item)
+                except Exception as e:
+                    print("Error while drawing art:", e)
+
                 self._current_item = item
             else:
                 text = font.render(item["name"], True, GREEN, BLACK)
-            
-            self._pydsp.blit(text, (0, drawline * self._fontsize))
-            
+
+            self._pydsp.blit(text, (0, drawline * self._fontsize+50))
+
             # Increase number of drawn lines - stop when the maximum is reached
 
             drawline += 1
             if drawline > self._drawlines:
                 break
 
+    def drawArt(self, item):
+        '''
+        Beautify the display.
+        '''
+        font = pygame.font.Font('freesansbold.ttf', self._fontsize)
+
+        # Draw a background image
+
+        if self._bgImg is None:
+            bg = os.listdir("web/background")
+            bg = bg[random.randint(0, len(bg)-1)]
+            self._bgImg = pygame.image.load(os.path.join("web/background", bg))
+            pygame.transform.scale(self._bgImg, (self._dx, 50))
+
+        self._pydsp.blit(self._bgImg, (0,0), (0, 0, self._dx, 50))
+
+        path = item.get("path")
+        title = item.get("name", "")
+        artist = item.get("artist", "")
+
+        # Try to extract meta data
+
+        if path is not None:
+            pass
+
+        s = title
+        if artist != "":
+            s = "%s (%s)" % (s, artist)
+
+        text = font.render(s, True, WHITE, BLACK)
+        self._pydsp.blit(text, (0, 38))
+
 
     def currentItem(self) -> Dict:
         '''
@@ -100,5 +144,5 @@ class Display:
             self._selection_pointer-=1
         elif action == "down":
             self._selection_pointer+=1
-        
+
         self.update()

+ 6 - 87
musikautomat/main.py

@@ -1,8 +1,8 @@
 #!./bin/python3
 
-import sys, os, time
-import traceback
+import os
 import yaml
+import threading
 
 # Include pygame without support prompt
 # Typing only available from version >= 2.0
@@ -10,8 +10,7 @@ import yaml
 os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
 import pygame # type: ignore
 
-from display import Display
-from player import Player
+from musikautomat import Musikautomat
 
 # Configuration
 
@@ -28,90 +27,10 @@ pygame.init()
 
 # Create the display
 
-pydsp = pygame.display.set_mode((dx, dy), 0, 32)
+pydsp = pygame.display.set_mode((dx, dy))
 pygame.display.set_caption("Musikautomat")
 pygame.mouse.set_visible(False)
 pygame.key.set_repeat(400, 100)
 
-# Initialise state objects
-
-display = Display(config, pydsp)
-display.update()
-eventSink = display
-player = Player(config, pydsp)
-clk = pygame.time.Clock()
-
-# Run the game loop
-
-while True:
-    clk.tick(10) # We only need 10 FPS
-
-    try:
-
-        for event in pygame.event.get():
-            
-            # Handle exit event
-            
-            if event.type == pygame.QUIT:
-                pygame.quit()
-                sys.exit()
-            
-            # Handle key events
-
-            if event.type == pygame.KEYDOWN:
-
-                if event.key == pygame.K_DOWN:
-                    eventSink.action("down")
-
-                elif event.key == pygame.K_UP:
-                    eventSink.action("up")
-
-                elif event.key == pygame.K_RIGHT:
-
-                    # Activate player
-
-                    item = display.currentItem()
-                    eventSink = player
-                    
-                    # TODO File selection depending on type
-                    path = item.get("path")
-                    files = os.listdir(path)
-
-                    player.setData([{ 
-                        "name" : f,
-                        "path" : os.path.join(path, f)
-                    } for f in files])
-
-                    player.update()
-
-                elif event.key == pygame.K_LEFT:
-
-                    # Get back to display
-
-                    eventSink = display
-                    display.update()
-
-                elif event.key == pygame.K_RETURN:
-
-                    # Play / pause current item
-
-                    player.togglePlay()
-
-
-    except Exception as e:
-        
-        # Display error
-        traceback.print_exc()
-
-        pydsp.fill((0,0,0))
-        font = pygame.font.Font('freesansbold.ttf', 12)
-        
-        s = str(e)
-        n = int(dx / 6)
-        for i, start in enumerate(range(0, len(s), n)):
-            text = font.render(s[start:start+n], True, (255, 0, 0) , (0, 0, 0))
-            pydsp.blit(text, (0, i * 12))
-        
-        eventSink = display
-
-    pygame.display.update()
+ma = Musikautomat(pydsp, config)
+ma.runDisplay()

+ 3 - 0
musikautomat/music.yml

@@ -8,6 +8,9 @@ playlist:
   - name: ACDC
     type: dir
     path: /home/ml/Music/acdc
+  - name: TesT
+    type: dir
+    path: /home/ml/Music/test
   - name: Pantera1
     type: dir
     path: foo

+ 160 - 0
musikautomat/musikautomat.py

@@ -0,0 +1,160 @@
+#!./bin/python3
+
+import os
+import sys
+import threading
+import time
+import traceback
+from typing import Dict
+
+import pygame # type: ignore
+import tornado.ioloop, tornado.web
+import asyncio
+
+from display import Display
+from player import Player
+
+class WebHandler(tornado.web.RequestHandler):
+
+    def initialize(self, musikautomat):
+        self.musikautomat = musikautomat
+
+    def get(self, a):
+        if a.startswith("play"):
+
+            if a == "play/dir":
+                path = self.get_argument('path')
+                self.musikautomat.play({
+                    "type" : "dir",
+                    "name" : self.get_argument('name', path),
+                    "path" : path
+                }, start=True)
+
+class Musikautomat:
+
+    def __init__(self, pydsp, config: Dict):
+
+        # Initialise state objects
+
+        self._pydsp = pydsp
+        self._display = Display(config, self._pydsp)
+        self._display.update()
+        self._eventSink = self._display
+        self._player = Player(config, self._pydsp)
+        self._dx = config.get("dimx", 128)
+
+        threading.Thread(target=self.runWebServer, daemon=True).start()
+
+
+    def runWebServer(self):
+        '''
+        Run event loop for web interface.
+        '''
+        urls = [
+            (r"/api/(.*)", WebHandler, dict(musikautomat=self)),
+            (r"/(.*)", tornado.web.StaticFileHandler, {"path" : "./web",
+            "default_filename" : "index.html" }),
+        ]
+
+        asyncio.set_event_loop(asyncio.new_event_loop())
+        app = tornado.web.Application(urls, debug=False)
+        app.listen(8080)
+        tornado.ioloop.IOLoop.instance().start()
+
+
+    def runDisplay(self):
+        '''
+        Run event loop for display and buttons.
+        '''
+        clk = pygame.time.Clock()
+        while True:
+            clk.tick(10) # We only need 10 FPS
+
+            try:
+                for event in pygame.event.get():
+
+                    # Handle exit event
+
+                    if event.type == pygame.QUIT:
+                        pygame.quit()
+                        sys.exit()
+
+                    # Handle key events
+
+                    if event.type == pygame.KEYDOWN:
+
+                        if event.key == pygame.K_DOWN:
+                            self._eventSink.action("down")
+
+                        elif event.key == pygame.K_UP:
+                            self._eventSink.action("up")
+
+                        elif event.key == pygame.K_RIGHT:
+
+                            if self._eventSink == self._player:
+                                self._player.togglePlay()
+                            else:
+                                self.play(self._display.currentItem())
+
+                        elif event.key == pygame.K_LEFT:
+
+                            # Get back to display
+
+                            self._eventSink = self._display
+                            self._display.update()
+
+            except Exception as e:
+
+                # Display error
+                traceback.print_exc()
+
+                self._pydsp.fill((0,0,0))
+                font = pygame.font.Font('freesansbold.ttf', 12)
+
+                s = str(e)
+                n = int(self._dx / 6)
+
+                for i, start in enumerate(range(0, len(s), n)):
+                    text = font.render(s[start:start+n], True, (255, 0, 0) , (0, 0, 0))
+                    self._pydsp.blit(text, (0, i * 12))
+
+                self._eventSink = self._display
+
+            pygame.display.update()
+
+
+    def play(self, item, start=False):
+        '''
+        Play an item.
+        '''
+        print("Player data:", item)
+
+        self._eventSink = self._player
+
+        t = item.get("type")
+        if t == "dir":
+            self.playDir(item)
+        else:
+            raise Exception("Unknown type: %s" % t)
+
+        if start:
+            self._player.togglePlay()
+
+
+    def playDir(self, item):
+        '''
+        Play all files in a directory.
+        '''
+        path = item.get("path")
+        files = os.listdir(path)
+
+        def toDisplayName(s):
+            s = os.path.splitext(s)[0]
+            return s.replace("_", " ").title()
+
+        self._player.setPlaylist(sorted([{
+            "name" : toDisplayName(f),
+            "path" : os.path.join(path, f)
+        } for f in files], key=lambda i: i["name"]))
+
+        self._player.update()

+ 97 - 9
musikautomat/player.py

@@ -1,12 +1,16 @@
 #!./bin/python3
 
+import time
+import threading
+
+import pygame # type: ignore
 from display import Display
 from typing import Dict
 from pymplb import MPlayer
 
 class Player(Display):
     '''
-    Player object
+    Player object which interacts with mplayer.
     '''
     def __init__(self, config, pydsp):
         super().__init__({
@@ -16,21 +20,105 @@ class Player(Display):
             "fontsize" : config.get("fontsize"),
         }, pydsp)
 
-        self._player = MPlayer()
+        self.playing = False # Flag if the player is playing
+        self._playingItem = None
+        self._playingPlaylist = None # Current playing order
+
+        self._data = [] # Current playlist
+
+        self._player = MPlayer() # Reference to mplayer control object
+
+        threading.Thread(target=self._updateFromPlayer, daemon=True).start()
+
+    def _updateFromPlayer(self):
+        '''
+        Thread to update the player display from the state of mplayer.
+        '''
+        font = pygame.font.Font('freesansbold.ttf', int(self._fontsize))
+
+        while True:
+
+            try:
+                time.sleep(0.5)
+
+                if self.playing:
+                    player_playing = self._player.p_path
+
+                    # Update playlist display if the current song has changed
+                    # and the item can be found in the current playlist
+
+                    if player_playing != self._playingItem.get("path"):
+
+                        for i, item in enumerate(self._data):
+                            if item.get("path") == player_playing:
+                                self._selection_pointer = i
+                                self._current_item = item
+                                self.update()
+
+                    # Start the playlist again if the player ran out of items
+
+                    if player_playing == "(null)":
+                        for item in self._playingPlaylist:
+                            self._player.loadfile(item["path"], 1)
+
+            except Exception as e:
+                print("Error update from player:", e)
 
-    def setData(self, data: Dict):
+
+    def setPlaylist(self, data: Dict):
+        '''
+        Set the playlist for this player.
+        '''
+        self._bgImg = None
+        self._selection_pointer = 0
         self._data = data
 
-    def loadPlaylist(self) -> Dict:
+    def getPlaylist(self) -> Dict:
+        '''
+        Return the playlist for this display.
+        '''
         return self._data
-    
+
     def togglePlay(self):
+        '''
+        Toggle playing the selected item.
+        '''
         if not self._current_item:
             return
-        
+
         if self._player.p_path == self._current_item["path"]:
-            print("stop")
+
+            # Stop what is currently playing
+
+            self.playing = False
             self._player.stop()
+
         else:
-            print("Playing:", self._current_item)
-            self._player.loadfile(self._current_item["path"])
+            ci = self._current_item["path"]
+
+            self._playingPlaylist = []
+
+            # Build up the playlist in mplayer in the correct order
+
+            add = 0
+            for item in self._data:
+
+                if item == self._current_item:
+                    self.playing = True
+                    self._playingItem = item
+                    self._player.loadfile(item["path"])
+                    self._playingPlaylist.append(item)
+                    add = 1
+
+                elif add == 1:
+                    self._player.loadfile(item["path"], 1)
+                    self._playingPlaylist.append(item)
+
+            for item in self._data:
+
+                if item != self._current_item:
+                    self._player.loadfile(item["path"], 1)
+                    self._playingPlaylist.append(item)
+
+                else:
+                    break

+ 1 - 0
musikautomat/requirements.txt

@@ -3,5 +3,6 @@ mypy-extensions==0.4.3
 pkg-resources==0.0.0
 pygame==2.0.0.dev6
 PyYAML==5.1.2
+tornado==6.0.3
 typed-ast==1.4.0
 typing-extensions==3.7.4.1

BIN
musikautomat/web/background/purple.jpg


+ 1 - 0
musikautomat/web/index.html

@@ -0,0 +1 @@
+<HTML>foo</HTML>