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
 #!./bin/python3
 
 
+import os
+
 import yaml
 import yaml
 import pygame # type: ignore
 import pygame # type: ignore
 from typing import Dict
 from typing import Dict
+import random
 
 
 # Color constants
 # Color constants
 
 
-BLACK = (0, 0, 0) 
-GREEN = (0, 255, 0) 
+BLACK = (0, 0, 0)
+GREEN = (0, 255, 0)
+WHITE = (255, 255, 255)
 
 
 class Display:
 class Display:
     '''
     '''
     Display object
     Display object
     '''
     '''
     def __init__(self, config, pydsp):
     def __init__(self, config, pydsp):
-        
+
         # Set config values
         # Set config values
-        
+
         self._dx = config.get("dimx", 128) # Drawing dimensions
         self._dx = config.get("dimx", 128) # Drawing dimensions
         self._dy = config.get("dimy", 160)
         self._dy = config.get("dimy", 160)
         self._playlistFile = config.get("playlist", "music.yml") # Playlist data
         self._playlistFile = config.get("playlist", "music.yml") # Playlist data
         self._pydsp = pydsp # Pygame display object
         self._pydsp = pydsp # Pygame display object
         self._fontsize = config.get("fontsize", 12) # Fontsize to use
         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
         # Set initialisation values
 
 
         self._selection_pointer = 0 # Current selected value
         self._selection_pointer = 0 # Current selected value
         self._line_offset = 0 # Drawing offset in playlist
         self._line_offset = 0 # Drawing offset in playlist
         self._current_item = None
         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", [])
         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.
         Load the data from the given playlist file and draw it according to the current state.
         '''
         '''
         self._pydsp.fill((0,0,0))
         self._pydsp.fill((0,0,0))
-        playlist = self.loadPlaylist()
+        playlist = self.getPlaylist()
 
 
         # Correct offset and selection pointer
         # Correct offset and selection pointer
 
 
@@ -55,7 +60,7 @@ class Display:
             self._selection_pointer = self._drawlines # Move the line offset up if possible
             self._selection_pointer = self._drawlines # Move the line offset up if possible
             if self._line_offset < len(playlist) - self._drawlines - 1:
             if self._line_offset < len(playlist) - self._drawlines - 1:
                 self._line_offset+=1
                 self._line_offset+=1
-        
+
         # Draw lines
         # Draw lines
 
 
         font = pygame.font.Font('freesansbold.ttf', self._fontsize)
         font = pygame.font.Font('freesansbold.ttf', self._fontsize)
@@ -72,18 +77,57 @@ class Display:
 
 
             if drawline == self._selection_pointer:
             if drawline == self._selection_pointer:
                 text = font.render(item["name"], True, BLACK, GREEN)
                 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
                 self._current_item = item
             else:
             else:
                 text = font.render(item["name"], True, GREEN, BLACK)
                 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
             # Increase number of drawn lines - stop when the maximum is reached
 
 
             drawline += 1
             drawline += 1
             if drawline > self._drawlines:
             if drawline > self._drawlines:
                 break
                 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:
     def currentItem(self) -> Dict:
         '''
         '''
@@ -100,5 +144,5 @@ class Display:
             self._selection_pointer-=1
             self._selection_pointer-=1
         elif action == "down":
         elif action == "down":
             self._selection_pointer+=1
             self._selection_pointer+=1
-        
+
         self.update()
         self.update()

+ 6 - 87
musikautomat/main.py

@@ -1,8 +1,8 @@
 #!./bin/python3
 #!./bin/python3
 
 
-import sys, os, time
-import traceback
+import os
 import yaml
 import yaml
+import threading
 
 
 # Include pygame without support prompt
 # Include pygame without support prompt
 # Typing only available from version >= 2.0
 # Typing only available from version >= 2.0
@@ -10,8 +10,7 @@ import yaml
 os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
 os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
 import pygame # type: ignore
 import pygame # type: ignore
 
 
-from display import Display
-from player import Player
+from musikautomat import Musikautomat
 
 
 # Configuration
 # Configuration
 
 
@@ -28,90 +27,10 @@ pygame.init()
 
 
 # Create the display
 # 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.display.set_caption("Musikautomat")
 pygame.mouse.set_visible(False)
 pygame.mouse.set_visible(False)
 pygame.key.set_repeat(400, 100)
 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
   - name: ACDC
     type: dir
     type: dir
     path: /home/ml/Music/acdc
     path: /home/ml/Music/acdc
+  - name: TesT
+    type: dir
+    path: /home/ml/Music/test
   - name: Pantera1
   - name: Pantera1
     type: dir
     type: dir
     path: foo
     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
 #!./bin/python3
 
 
+import time
+import threading
+
+import pygame # type: ignore
 from display import Display
 from display import Display
 from typing import Dict
 from typing import Dict
 from pymplb import MPlayer
 from pymplb import MPlayer
 
 
 class Player(Display):
 class Player(Display):
     '''
     '''
-    Player object
+    Player object which interacts with mplayer.
     '''
     '''
     def __init__(self, config, pydsp):
     def __init__(self, config, pydsp):
         super().__init__({
         super().__init__({
@@ -16,21 +20,105 @@ class Player(Display):
             "fontsize" : config.get("fontsize"),
             "fontsize" : config.get("fontsize"),
         }, pydsp)
         }, 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
         self._data = data
 
 
-    def loadPlaylist(self) -> Dict:
+    def getPlaylist(self) -> Dict:
+        '''
+        Return the playlist for this display.
+        '''
         return self._data
         return self._data
-    
+
     def togglePlay(self):
     def togglePlay(self):
+        '''
+        Toggle playing the selected item.
+        '''
         if not self._current_item:
         if not self._current_item:
             return
             return
-        
+
         if self._player.p_path == self._current_item["path"]:
         if self._player.p_path == self._current_item["path"]:
-            print("stop")
+
+            # Stop what is currently playing
+
+            self.playing = False
             self._player.stop()
             self._player.stop()
+
         else:
         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
 pkg-resources==0.0.0
 pygame==2.0.0.dev6
 pygame==2.0.0.dev6
 PyYAML==5.1.2
 PyYAML==5.1.2
+tornado==6.0.3
 typed-ast==1.4.0
 typed-ast==1.4.0
 typing-extensions==3.7.4.1
 typing-extensions==3.7.4.1

BIN
musikautomat/web/background/purple.jpg


+ 1 - 0
musikautomat/web/index.html

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