Browse Source

Merge branch 'feat/musikautomat-rewrite' of diy/Hackerradio into master

Matthias Ladkau 3 years ago
parent
commit
d28d286844

+ 3 - 0
.gitignore

@@ -7,3 +7,6 @@
 /musikautomat/.mypy_cache
 /musikautomat/__pycache__
 *.pyc
+/.mypy_cache
+/.vscode
+

musikautomat/README.md → musikautomat/Readme.md


+ 2 - 2
musikautomat/config.yml

@@ -6,10 +6,10 @@ dimx: 128
 dimy: 160
 
 # Config for main display
-playlist: music.yml
+playlists: music.yml
 
 # Fontsize for drawing
 fontsize: 12
 
 # Auto start the first item
-autostart: false
+autostart: true

+ 29 - 123
musikautomat/display.py

@@ -6,7 +6,6 @@ import yaml
 import pygame # type: ignore
 from typing import Dict
 import random
-import eyed3
 import tempfile
 
 # Color constants
@@ -17,153 +16,60 @@ WHITE = (255, 255, 255)
 
 class Display:
     '''
-    Display object
+    Display object which displays a list of strings. It can optionally highlight
+    a string in the list and display a picture.
     '''
+
     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) - 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
+        self.drawlines = int(self._dy / 12) - 5 # Max number of drawn lines
 
 
-    def getPlaylist(self) -> Dict:
-        '''
-        Return the playlist for this display.
+    def update(self, text=None, highlight=-1, title=None, img=None):
         '''
-        return yaml.safe_load(open(self._playlistFile)).get("playlist", [])
+        Update the display.
 
-
-    def update(self):
-        '''
-        Load the data from the given playlist file and draw it according to the current state.
+        text      - List of max self.drawlines items to display as list.
+        highlight - Text item to highlight.
+        title     - List of max 4 items for the title.
+        img       - Image to display in upper left corner.
         '''
         self._pydsp.fill((0,0,0))
-        playlist = self.getPlaylist()
-
-        # Correct offset and selection pointer
-
-        if self._selection_pointer < 0:
-            self._selection_pointer = 0 # Make sure the selection is at least at pos 0
-            if self._line_offset > 0: # Move the line offset down if possible
-                self._line_offset-=1
-        elif self._selection_pointer > self._drawlines: # Make sure sure selection does not exceed list
-            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)
-        drawline = 0 # Line currently drawm
-
-        for i, item in enumerate(playlist):
 
-            # Skip lines until we reached the offset
+        if not img:
+            img = os.path.join("img", "c64tetris.png")
 
-            if i < self._line_offset:
-                continue
+        i = pygame.transform.scale(pygame.image.load(img), (50, 50))
+        self._pydsp.blit(i, (0,0), (0, 0, self._dx, 50))
 
-            # Draw the line
+        if not title:
+            title = ["", "Hacker", "       Radio"]
 
-            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+50))
-
-            # Increase number of drawn lines - stop when the maximum is reached
-
-            drawline += 1
-            if drawline > self._drawlines:
+        for i, item in enumerate(title):
+            rendertext = font.render(item, True, WHITE, BLACK)
+            self._pydsp.blit(rendertext, (55, 2 + i*12))
+            if i == 3:
                 break
 
-    def drawArt(self, item):
-        '''
-        Beautify the display.
-        '''
-        return
-        font = pygame.font.Font('freesansbold.ttf', 10)
-
-        path = item.get("path")
-        title = item.get("name", "")
-        artist = item.get("artist", "")
-        album = item.get("album", "")
-        date = item.get("date", "")
+        if not text:
+            text = []
 
-        # Try to extract meta data
-
-        if path is not None and os.path.isfile(path):
-            f = eyed3.load(path)
-            title = f.tag.title
-            artist = f.tag.artist
-            album = f.tag.album
-            if f.tag.best_release_date:
-                date = str(f.tag.best_release_date)
-
-            # Load album art
-
-            for i in f.tag.images:
-                with tempfile.TemporaryFile() as f:
-                    f.write(i.image_data)
-                    f.seek(0)
-                    i = pygame.image.load(f)
+        for i, item in enumerate(text):
+            if i == highlight:
+                rendertext = font.render(item, True, BLACK, GREEN)
+            else:
+                rendertext = font.render(item, True, GREEN, BLACK)
 
-                i = pygame.transform.scale(i, (50, 50))
-                self._pydsp.blit(i, (0,0), (0, 0, self._dx, 50))
+            self._pydsp.blit(rendertext, (0, i * self._fontsize+50))
 
+            if i == self.drawlines:
                 break
-
-        text = font.render(title, True, WHITE, BLACK)
-        self._pydsp.blit(text, (50, 0))
-
-        if artist != "":
-            text = font.render(artist, True, WHITE, BLACK)
-            self._pydsp.blit(text, (50, 10))
-
-        if album != "":
-            text = font.render(album, True, WHITE, BLACK)
-            self._pydsp.blit(text, (50, 20))
-
-        if date != "":
-            text = font.render(date, True, WHITE, BLACK)
-            self._pydsp.blit(text, (50, 30))
-
-
-    def currentItem(self) -> Dict:
-        '''
-        Return the current selected item.
-        '''
-        return self._current_item
-
-
-    def action(self, action: str):
-        '''
-        Perform an action to this display.
-        '''
-        if action == "up":
-            self._selection_pointer-=1
-        elif action == "down":
-            self._selection_pointer+=1
-
-        self.update()

+ 52 - 0
musikautomat/handler/__init__.py

@@ -0,0 +1,52 @@
+#!./bin/python3
+
+import os
+
+EVENT_UP = "event_up"
+EVENT_DOWN = "event_down"
+EVENT_LEFT = "event_left"
+EVENT_RIGHT = "event_right"
+
+class HandlerExit(Exception):
+    pass
+
+class BaseHandler:
+
+    def __init__(self, config, display):
+        self.display = display
+        self.config = config
+
+
+    def setPlaylistItem(self, item):
+        '''
+        Called with the selected item.
+        '''
+        self.item = item
+
+
+    def update(self, text=None, highlight=-1, title=None, img=None):
+        '''
+        Called when the handler should update the display.
+        '''
+        self.display.update(text, highlight, title, img)
+
+
+    def stop(self):
+        '''
+        Called when the handler should stop.
+        '''
+        pass
+
+
+    def emitEvent(self, event):
+        '''
+        Called when an event has happened.
+        '''
+        if event == EVENT_LEFT:
+            self.stop()
+            raise HandlerExit
+
+
+    def toDisplayName(self, s):
+        s = os.path.splitext(s)[0]
+        return s.replace("_", " ").title()

+ 20 - 0
musikautomat/handler/dir.py

@@ -0,0 +1,20 @@
+#!./bin/python3
+
+import os
+
+from handler.filelist import FileListHandler
+
+class DirectoryHandler(FileListHandler):
+
+    def getPlaylistItems(self, item):
+        path = item["path"]
+        items = []
+
+        items = sorted([{
+            "name" : self.toDisplayName(f),
+            "path" : os.path.join(path, f)
+        } for f in os.listdir(path)], key=lambda i: i["name"])
+
+        print("GOT items:", items)
+
+        return items

+ 108 - 0
musikautomat/handler/filelist.py

@@ -0,0 +1,108 @@
+#!./bin/python3
+
+import threading
+import time
+
+from handler import EVENT_LEFT
+from handler.listhandler import BaseListHandler
+from handler.lib.pymplb import MPlayer
+
+class FileListHandler(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.
+        '''
+        super().setPlaylistItem(item)
+
+        if self._mplayer is None:
+            self._mplayer = MPlayer()
+            self.setItems(self.getPlaylistItems(item))
+            self._mplayer_watcher = threading.Thread(target=self._watch_mplayer, daemon=True)
+            self._mplayer_watcher.start()
+
+        self.is_playing = False
+
+
+    def getPlaylistItems(self, item):
+        raise NotImplementedError()
+
+
+    def update(self, text=None, highlight=-1, title=None, img=None):
+        '''
+        Called when the handler should update the display.
+        '''
+        super().update(title=[
+            "",
+            "Playlist:",
+            self.item["name"],
+            "[play]" if self.is_playing else "[stop]"
+        ], img=self.item.get("img"))
+
+
+    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)

+ 1 - 0
musikautomat/handler/lib/__init__.py

@@ -0,0 +1 @@
+#!./bin/python3

musikautomat/pymplb.py → musikautomat/handler/lib/pymplb.py


+ 82 - 0
musikautomat/handler/listhandler.py

@@ -0,0 +1,82 @@
+#!./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):
+        '''
+        Set the items which should be displayed.
+        '''
+        self.items = items
+        self.item_pointer = 0
+
+
+    def update(self, text=None, highlight=-1, title=None, img=None):
+        '''
+        Called when the handler should update the display.
+        '''
+
+        # Show the slice of the main menu which is relevant
+
+        playlist_display = text or []
+
+        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
+
+        super().update(playlist_display,
+            highlight if highlight != -1 else min(self.item_pointer, maxlines),
+            title, img)
+
+
+    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

+ 22 - 0
musikautomat/handler/m3u.py

@@ -0,0 +1,22 @@
+#!./bin/python3
+
+import os
+
+from handler.filelist import FileListHandler
+
+class M3UHandler(FileListHandler):
+
+    def getPlaylistItems(self, 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)
+                    })
+
+        return items

+ 48 - 0
musikautomat/handler/stream.py

@@ -0,0 +1,48 @@
+#!./bin/python3
+
+from handler import BaseHandler
+from handler.lib.pymplb import MPlayer
+
+class StreamHandler(BaseHandler):
+
+    def __init__(self, config, display):
+        super().__init__(config, display)
+        self._mplayer = None
+
+
+    def setPlaylistItem(self, item):
+        '''
+        Called with the selected item.
+        '''
+        super().setPlaylistItem(item)
+
+        if self._mplayer is None:
+            self._mplayer = MPlayer()
+            self._mplayer.loadfile(self.item["url"])
+
+
+    def update(self, text=None, highlight=-1, title=None, img=None):
+        '''
+        Called when the handler should update the display.
+        '''
+        super().update(title=[
+            "",
+            "Stream:",
+            self.item["name"]
+        ], img=self.item.get("img"))
+
+
+    def stop(self):
+        '''
+        Called when the handler should stop.
+        '''
+        if self._mplayer is not None:
+            self._mplayer.quit()
+            self._mplayer = None
+
+
+    def emitEvent(self, event):
+        '''
+        Called when an event has happened.
+        '''
+        super().emitEvent(event)

BIN
musikautomat/img/c64giana.png


BIN
musikautomat/img/c64tetris.png


+ 101 - 0
musikautomat/menu.py

@@ -0,0 +1,101 @@
+#!./bin/python3
+
+import os
+
+import yaml
+import pygame # type: ignore
+from typing import Dict
+import random
+import tempfile
+import handler
+from handler import stream, m3u, dir
+
+class Menu:
+    '''
+    Control object which holds the menu state.
+    '''
+
+    def __init__(self, config, display):
+        self._display = display # Display object
+        self._current_item = None # Current item with display control
+
+        # Register default handlers
+
+        self.handler = {
+            "stream" : stream.StreamHandler(config, display),
+            "m3u" : m3u.M3UHandler(config, display),
+            "dir" : dir.DirectoryHandler(config, display)
+        }
+
+        # Playlist data
+
+        playlistsFile = config.get("playlists", "music.yml")
+        self.playlists = yaml.safe_load(open(playlistsFile)).get("playlist", [])
+        self.playlists_pointer = 0
+
+        self.update()
+
+    def update(self):
+
+        # Delegate to the current item if there is one
+
+        if self._current_item is not None:
+            self._current_item.update()
+            return
+
+        # Show the slice of the main menu which is relevant
+
+        playlist_display = []
+
+        maxlines = self._display.drawlines
+
+        for i, item in enumerate(self.playlists):
+
+            if len(playlist_display) <= maxlines:
+                if i >= self.playlists_pointer - maxlines and i <= self.playlists_pointer + maxlines:
+                    playlist_display.append(item.get("name", "<unknown>"))
+            else:
+                break
+
+        playlist_pointer = self.playlists_pointer
+        self._display.update(playlist_display, min(playlist_pointer, maxlines))
+
+    def emitEvent(self, event):
+
+        # Delegate to the current item if there is one
+
+        if self._current_item is not None:
+            try:
+                self._current_item.emitEvent(event)
+            except handler.HandlerExit as e:
+                self._current_item = None
+
+            self.update()
+
+            return
+
+        if event == handler.EVENT_RIGHT:
+            self.selectPlaylistItem(self.playlists[self.playlists_pointer])
+
+        if event == handler.EVENT_UP:
+            self.playlists_pointer -=1
+
+        if event == handler.EVENT_DOWN:
+            self.playlists_pointer +=1
+
+        if self.playlists_pointer >= len(self.playlists):
+            self.playlists_pointer = 0
+
+        if self.playlists_pointer < 0:
+            self.playlists_pointer = len(self.playlists) - 1
+
+        self.update()
+
+    def selectPlaylistItem(self, item):
+        handler = self.handler.get(item["type"])
+
+        print("Selecting:", item)
+
+        if handler is not None:
+            handler.setPlaylistItem(item)
+            self._current_item = handler

+ 35 - 4
musikautomat/music.yml

@@ -1,10 +1,41 @@
 playlist:
+  - name: Awesome Mix
+    type: m3u
+    path: /media/media/audio/awesome_mix.m3u
   - name: Domemucke
     type: stream
     url: http://web:gemasuxx@devt.de:5051/pleasuredome
+    img: img/c64giana.png
+  - name: Domemucke1
+    type: stream
+    url: http://web:gemasuxx@devt.de:5051/pleasuredome
+  - name: Domemucke2
+    type: stream
+    url: http://web:gemasuxx@devt.de:5051/pleasuredome
+  - name: Domemucke3
+    type: stream
+    url: http://web:gemasuxx@devt.de:5051/pleasuredome
+  - name: Domemucke4
+    type: stream
+    url: http://web:gemasuxx@devt.de:5051/pleasuredome
+  - name: Domemucke5
+    type: stream
+    url: http://web:gemasuxx@devt.de:5051/pleasuredome
+  - name: Domemucke6
+    type: stream
+    url: http://web:gemasuxx@devt.de:5051/pleasuredome
+  - name: Domemucke7
+    type: stream
+    url: http://web:gemasuxx@devt.de:5051/pleasuredome
+  - name: Domemucke8
+    type: stream
+    url: http://web:gemasuxx@devt.de:5051/pleasuredome
+  - name: Domemucke9
+    type: stream
+    url: http://web:gemasuxx@devt.de:5051/pleasuredome
+  - name: Domemucke10
+    type: stream
+    url: http://web:gemasuxx@devt.de:5051/pleasuredome
   - name: Aerosmith
     type: dir
-    path: /home/ml/Music/aerosmith
-  - name: Good Stuff
-    type: m3u
-    path: /home/ml/Music/goodstuff.m3u
+    path: /media/media/audio/music/a/aerosmith

+ 15 - 129
musikautomat/musikautomat.py

@@ -10,31 +10,15 @@ from typing import Dict
 import pygame # type: ignore
 import tornado.ioloop, tornado.web
 import asyncio
+from display import Display
+import menu
+import handler
 
 try:
     import RPi.GPIO as GPIO # Support for Raspberry PI GPIO input
 except:
     import mock_rpi_gpio as GPIO
 
-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):
@@ -44,13 +28,9 @@ class Musikautomat:
         self._autostart = config.get("autostart")
         self._pydsp = pydsp
         self._display = Display(config, self._pydsp)
-        self._display.update()
-        self._eventSink = self._display
-        self._player = Player(config, self._pydsp)
+        self._menu = menu.Menu(config, self._display)
         self._dx = config.get("dimx", 128)
 
-        threading.Thread(target=self.runWebServer, daemon=True).start()
-
         GPIO.setmode(GPIO.BOARD) # Use physical pin numbering
         GPIO.setwarnings(False) # Disable warnings
         GPIO.setup(3, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
@@ -58,40 +38,6 @@ class Musikautomat:
         GPIO.setup(7, GPIO.IN, pull_up_down=GPIO.PUD_UP)
         GPIO.setup(11, GPIO.IN, pull_up_down=GPIO.PUD_UP)
 
-    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 eventUp(self):
-        self._eventSink.action("up")
-
-    def eventDown(self):
-        self._eventSink.action("down")
-
-    def eventLeft(self):
-
-        # Get back to display
-
-        self._eventSink = self._display
-        self._display.update()
-
-    def eventRight(self):
-        if self._eventSink == self._player:
-            self._player.togglePlay()
-        else:
-            self.play(self._display.currentItem())
-
     def runDisplay(self):
         '''
         Run event loop for display and buttons.
@@ -99,9 +45,9 @@ class Musikautomat:
         clk = pygame.time.Clock()
         gpio_event_timer = 0
 
-        if self._autostart:
-            self.eventRight()
-            self.eventRight()
+        #if self._autostart:
+        #    self.eventRight()
+        #    self.eventRight()
 
         while True:
             clk.tick(10) # We only need 10 FPS
@@ -162,74 +108,14 @@ class Musikautomat:
 
             pygame.display.update()
 
-    def toDisplayName(self, s):
-        s = os.path.splitext(s)[0]
-        return s.replace("_", " ").title()
-
-    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)
-        elif t == "m3u":
-            self.playM3U(item)
-        elif t == "stream":
-            self.playStream(item)
-        else:
-            raise Exception("Unknown type: %s" % t)
-
-        if start:
-            self._player.togglePlay()
-
-
-    def playStream(self, item):
-        '''
-        Play a stream.
-        '''
-        self._player.setPlaylist([{
-            "name" : item["name"],
-            "path" : item["url"]
-        }])
-
-        self._player.update()
-
-
-    def playDir(self, item):
-        '''
-        Play all files in a directory.
-        '''
-        path = item.get("path")
-        files = os.listdir(path)
-
-        self._player.setPlaylist(sorted([{
-            "name" : self.toDisplayName(f),
-            "path" : os.path.join(path, f)
-        } for f in files], key=lambda i: i["name"]))
+    def eventUp(self):
+        self._menu.emitEvent(handler.EVENT_UP)
 
-        self._player.update()
+    def eventDown(self):
+        self._menu.emitEvent(handler.EVENT_DOWN)
 
+    def eventLeft(self):
+        self._menu.emitEvent(handler.EVENT_LEFT)
 
-    def playM3U(self, item):
-        '''
-        Play all files of a M3U playlist.
-        '''
-        path = item.get("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)
-                    })
-
-        self._player.setPlaylist(items)
-        self._player.update()
+    def eventRight(self):
+        self._menu.emitEvent(handler.EVENT_RIGHT)

+ 31 - 0
old/musikautomat_v1/README.md

@@ -0,0 +1,31 @@
+Musikautomat
+--
+Simple graphical interface for mplayer.
+
+Setup:
+--
+Install:
+- mplayer
+- python3 (version >3.6)
+- python3-pip
+- python3-venv
+
+Setup virtual environment:
+```
+python3 -m venv .
+```
+
+Install dependencies:
+```
+./bin/pip3 install -r requirements.txt
+```
+
+Freeze dependencies:
+```
+./bin/pip3 freeze > requirements.txt
+```
+
+Type check:
+```
+./bin/mypy .
+```

+ 15 - 0
old/musikautomat_v1/config.yml

@@ -0,0 +1,15 @@
+# Frame buffer device to use for output
+fbdev: /dev/fb0
+
+# Display resolution
+dimx: 128
+dimy: 160
+
+# Config for main display
+playlist: music.yml
+
+# Fontsize for drawing
+fontsize: 12
+
+# Auto start the first item
+autostart: false

+ 169 - 0
old/musikautomat_v1/display.py

@@ -0,0 +1,169 @@
+#!./bin/python3
+
+import os
+
+import yaml
+import pygame # type: ignore
+from typing import Dict
+import random
+import eyed3
+import tempfile
+
+# Color constants
+
+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) - 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 getPlaylist(self) -> Dict:
+        '''
+        Return the playlist for this display.
+        '''
+        return yaml.safe_load(open(self._playlistFile)).get("playlist", [])
+
+
+    def update(self):
+        '''
+        Load the data from the given playlist file and draw it according to the current state.
+        '''
+        self._pydsp.fill((0,0,0))
+        playlist = self.getPlaylist()
+
+        # Correct offset and selection pointer
+
+        if self._selection_pointer < 0:
+            self._selection_pointer = 0 # Make sure the selection is at least at pos 0
+            if self._line_offset > 0: # Move the line offset down if possible
+                self._line_offset-=1
+        elif self._selection_pointer > self._drawlines: # Make sure sure selection does not exceed list
+            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)
+        drawline = 0 # Line currently drawm
+
+        for i, item in enumerate(playlist):
+
+            # Skip lines until we reached the offset
+
+            if i < self._line_offset:
+                continue
+
+            # Draw the line
+
+            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+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.
+        '''
+        return
+        font = pygame.font.Font('freesansbold.ttf', 10)
+
+        path = item.get("path")
+        title = item.get("name", "")
+        artist = item.get("artist", "")
+        album = item.get("album", "")
+        date = item.get("date", "")
+
+        # Try to extract meta data
+
+        if path is not None and os.path.isfile(path):
+            f = eyed3.load(path)
+            title = f.tag.title
+            artist = f.tag.artist
+            album = f.tag.album
+            if f.tag.best_release_date:
+                date = str(f.tag.best_release_date)
+
+            # Load album art
+
+            for i in f.tag.images:
+                with tempfile.TemporaryFile() as f:
+                    f.write(i.image_data)
+                    f.seek(0)
+                    i = pygame.image.load(f)
+
+                i = pygame.transform.scale(i, (50, 50))
+                self._pydsp.blit(i, (0,0), (0, 0, self._dx, 50))
+
+                break
+
+        text = font.render(title, True, WHITE, BLACK)
+        self._pydsp.blit(text, (50, 0))
+
+        if artist != "":
+            text = font.render(artist, True, WHITE, BLACK)
+            self._pydsp.blit(text, (50, 10))
+
+        if album != "":
+            text = font.render(album, True, WHITE, BLACK)
+            self._pydsp.blit(text, (50, 20))
+
+        if date != "":
+            text = font.render(date, True, WHITE, BLACK)
+            self._pydsp.blit(text, (50, 30))
+
+
+    def currentItem(self) -> Dict:
+        '''
+        Return the current selected item.
+        '''
+        return self._current_item
+
+
+    def action(self, action: str):
+        '''
+        Perform an action to this display.
+        '''
+        if action == "up":
+            self._selection_pointer-=1
+        elif action == "down":
+            self._selection_pointer+=1
+
+        self.update()

+ 36 - 0
old/musikautomat_v1/main.py

@@ -0,0 +1,36 @@
+#!./bin/python3
+
+import os
+import yaml
+import threading
+
+# Include pygame without support prompt
+# Typing only available from version >= 2.0
+
+os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
+import pygame # type: ignore
+
+from musikautomat import Musikautomat
+
+# Configuration
+
+config = yaml.safe_load(open("config.yml"))
+
+fbdev = config.get("fbdev", "/dev/fb0")
+dx = config.get("dimx", 128)
+dy = config.get("dimy", 160)
+
+# Initialise pygame
+
+os.environ["SDL_FBDEV"] = fbdev
+pygame.init()
+
+# Create the display
+print("Trying to open %s with %sx%s" % (fbdev, dx, dy))
+pydsp = pygame.display.set_mode((dx, dy))
+pygame.display.set_caption("Musikautomat")
+pygame.mouse.set_visible(False)
+pygame.key.set_repeat(400, 100)
+
+ma = Musikautomat(pydsp, config)
+ma.runDisplay()

+ 20 - 0
old/musikautomat_v1/mock_rpi_gpio.py

@@ -0,0 +1,20 @@
+#!./bin/python3
+
+# Mock GPIO module for non-Raspberry PI platforms
+
+BOARD=1
+IN=1
+PUD_DOWN=1
+PUD_UP=2
+
+def setmode(mode):
+    pass
+
+def setwarnings(mode):
+    pass
+
+def setup(pin, mode, pull_up_down=None):
+    pass
+
+def input(pin):
+    return 1

+ 10 - 0
old/musikautomat_v1/music.yml

@@ -0,0 +1,10 @@
+playlist:
+  - name: Domemucke
+    type: stream
+    url: http://web:gemasuxx@devt.de:5051/pleasuredome
+  - name: Aerosmith
+    type: dir
+    path: /home/ml/Music/aerosmith
+  - name: Good Stuff
+    type: m3u
+    path: /home/ml/Music/goodstuff.m3u

+ 235 - 0
old/musikautomat_v1/musikautomat.py

@@ -0,0 +1,235 @@
+#!./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
+
+try:
+    import RPi.GPIO as GPIO # Support for Raspberry PI GPIO input
+except:
+    import mock_rpi_gpio as GPIO
+
+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._autostart = config.get("autostart")
+        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()
+
+        GPIO.setmode(GPIO.BOARD) # Use physical pin numbering
+        GPIO.setwarnings(False) # Disable warnings
+        GPIO.setup(3, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
+        GPIO.setup(5, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
+        GPIO.setup(7, GPIO.IN, pull_up_down=GPIO.PUD_UP)
+        GPIO.setup(11, GPIO.IN, pull_up_down=GPIO.PUD_UP)
+
+    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 eventUp(self):
+        self._eventSink.action("up")
+
+    def eventDown(self):
+        self._eventSink.action("down")
+
+    def eventLeft(self):
+
+        # Get back to display
+
+        self._eventSink = self._display
+        self._display.update()
+
+    def eventRight(self):
+        if self._eventSink == self._player:
+            self._player.togglePlay()
+        else:
+            self.play(self._display.currentItem())
+
+    def runDisplay(self):
+        '''
+        Run event loop for display and buttons.
+        '''
+        clk = pygame.time.Clock()
+        gpio_event_timer = 0
+
+        if self._autostart:
+            self.eventRight()
+            self.eventRight()
+
+        while True:
+            clk.tick(10) # We only need 10 FPS
+
+            if gpio_event_timer > 1:
+                if GPIO.input(3) == 0: # Up
+                    gpio_event_timer=0
+                    self.eventUp()
+                elif GPIO.input(5) == 0: # LEFT
+                    gpio_event_timer=0
+                    self.eventLeft()
+                elif GPIO.input(7) == 0: # RIGHT
+                    gpio_event_timer=0
+                    self.eventRight()
+                elif GPIO.input(11) == 0: # DOWN
+                    gpio_event_timer=0
+                    self.eventDown()
+            else:
+                gpio_event_timer+=1
+
+            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_UP:
+                            self.eventUp()
+                        elif event.key == pygame.K_LEFT:
+                            self.eventLeft()
+                        elif event.key == pygame.K_RIGHT:
+                            self.eventRight()
+                        elif event.key == pygame.K_DOWN:
+                            self.eventDown()
+
+            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 toDisplayName(self, s):
+        s = os.path.splitext(s)[0]
+        return s.replace("_", " ").title()
+
+    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)
+        elif t == "m3u":
+            self.playM3U(item)
+        elif t == "stream":
+            self.playStream(item)
+        else:
+            raise Exception("Unknown type: %s" % t)
+
+        if start:
+            self._player.togglePlay()
+
+
+    def playStream(self, item):
+        '''
+        Play a stream.
+        '''
+        self._player.setPlaylist([{
+            "name" : item["name"],
+            "path" : item["url"]
+        }])
+
+        self._player.update()
+
+
+    def playDir(self, item):
+        '''
+        Play all files in a directory.
+        '''
+        path = item.get("path")
+        files = os.listdir(path)
+
+        self._player.setPlaylist(sorted([{
+            "name" : self.toDisplayName(f),
+            "path" : os.path.join(path, f)
+        } for f in files], key=lambda i: i["name"]))
+
+        self._player.update()
+
+
+    def playM3U(self, item):
+        '''
+        Play all files of a M3U playlist.
+        '''
+        path = item.get("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)
+                    })
+
+        self._player.setPlaylist(items)
+        self._player.update()

musikautomat/player.py → old/musikautomat_v1/player.py


+ 254 - 0
old/musikautomat_v1/pymplb.py

@@ -0,0 +1,254 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2010, Stefan Parviainen <pafcu@iki.fi>
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""
+pymplb (PYthonMPLayerBingings) is a library that can be used to play media using an external MPlayer process.
+The library runs the MPlayer binary in slave mode as a subprocess and then sends slave-mode commands to the process.
+Commands are mapped to class methods and properties to class properties (by default prefixed with 'p_').
+Commands are discovered at runtime and thus these bindings should automatically also support any new commands added to MPlayer in the future.
+An example:
+>>> import pymplb
+>>> player = pymplb.MPlayer()
+>>> player.loadfile('test.ogv')
+>>> player.p_filename
+'test.ogv'
+"""
+
+from functools import partial
+import subprocess
+import atexit
+
+class PlayerNotFoundException(Exception):
+	"""Exception which is raised when the external mplayer binary is not found."""
+	def __init__(self, player_path):
+		Exception.__init__(self, 'Player not found at %s'%player_path)
+
+def make_mplayer_class(mplayer_bin='mplayer', method_prefix='', property_prefix='p_'):
+	"""
+	Construct a MPlayer class which user mplayer_bin as the player binary and prepends the given prefixes to property and method names.
+	Prefixes are needed because some properties and methods have the same name.
+	You only need to construct a new class if the default values are not suitable (i.e. mplayer is not in your path, or some new commands have been introduced that conflict with the default prefixes.
+	"""
+
+	# Yes, I'm aware it's a bit messy to have a function in a function in a class in a function
+	# Decrease your indentation and bear with me here
+	class _MPlayer(object): #pylint: disable-msg=R0903
+		"""
+		This is the main class used to play audio and video files by launching mplayer as a subprocess in slave mode.
+		Slave mode methods can be called directly (e.g. x.loadfile("somefile)") while properties are prefixed to avoid
+		name conflicts between methods and properties (e.g. x.p_looping = False).
+		Available methods and properties are determined at runtime when the class is instantiated. All methods and properties are
+		type-safe and properties respect minimum and maximum values given by mplayer.
+		"""
+		_arg_types = {'Flag':type(False), 'String':type(''), 'Integer':type(0), 'Float':type(0.0), 'Position':type(0.0), 'Time':type(0.0)} # Mapping from mplayer -> Python types
+		_player_methods = {} # Need to keep track of methods because they must be modified after they have been added
+
+		def __init__(self, env=None, mplayer_args_d=None, **mplayer_args):
+			if mplayer_args_d: # Make pylint happy by not passing {} as an argument
+				mplayer_args.update(mplayer_args_d)
+			cmd_args = [mplayer_bin, '-slave', '-quiet', '-idle', '-msglevel', 'all=-1:global=4']
+			for (name, value) in mplayer_args.items():
+				cmd_args.append('-'+name)
+				if value != None and value != True:
+					cmd_args.append(str(value))
+
+			self.__player = _MPlayer._run_player(cmd_args, env=env)
+
+			# Partially apply methods to use the newly created player
+			for (name, func) in self._player_methods.items():
+				setattr(self, name, partial(func, self.__player))
+
+			atexit.register(self.close) # Make sure subprocess is killed
+
+		def close(self):
+			"""Method that kills the MPlayer subprocess"""
+			try:
+				self.__player.terminate()
+			except:
+				pass
+
+		@staticmethod
+		def _run_player(args, env=None):
+			"""Helper function that runs MPlayer with the given arguments"""
+			try:
+				player = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
+			except OSError as err:
+				if err.errno == 2:
+					raise PlayerNotFoundException(args[0])
+				else:
+					raise
+			return player
+
+		@classmethod
+		def _add_methods(cls, mplayer_bin):
+			"""Discover which commands MPlayer understands and add them as class methods"""
+			def cmd(name, argtypes, obligatory, player, *args, **kwargs):
+				"""Function which sends the given command to the MPlayer process"""
+				if len(args) < obligatory:
+					raise TypeError('TypeError: %s() takes at least %d arguments (%d given)'%(name, obligatory, len(args)))
+				if len(args) > len(argtypes):
+					raise TypeError('TypeError: %s() takes at most %d arguments (%d given)'%(name, len(argtypes), len(args)))
+				for i in range(len(args)):
+					if type(args[i]) != argtypes[i]:
+						raise TypeError('Argument %d of %s() has type %s, should be %s'%(i, name, type(args[i]).__name__, argtypes[i].__name__))
+
+				pausing = kwargs.get('pausing', '')
+				if pausing != '':
+					pausing = pausing + ' '
+
+				mplayer_command = '%s%s %s\n' % (pausing, name, ' '.join((str(x) for x in args)))
+				player.stdin.write(mplayer_command.encode('utf-8'))
+				player.stdin.flush()
+
+				# Read return value of commands that give one
+				# Hopefully this is smart enough ...
+				if name.startswith('get_'):
+					while True:
+						line = player.stdout.readline().decode('utf-8')
+						if line == '': # no more lines
+							return None
+
+						if not line[:3] == 'ANS':
+							continue
+
+						retval = line.split('=', 2)[1].rstrip()
+						if retval == 'PROPERTY_UNAVAILABLE':
+							return None
+
+						return retval
+
+			player = cls._run_player([mplayer_bin, '-input', 'cmdlist'])
+
+			# Add each command found
+			for line in player.stdout:
+				line = str(line.decode('utf-8'))
+				parts = line.strip().split()
+				name = parts[0]
+				args = parts[1:]
+				if len(parts) > 1:
+					obligatory = len([x for x in args if x[0] != '[']) # Number of obligatory args
+					try:
+						argtypes = [cls._arg_types[y] for y in [x.strip('[]') for x in args]]
+					except KeyError:  # Unknown argument type
+						continue # Some garbage on the output (version?)
+
+				method = partial(cmd, name, argtypes, obligatory)
+				if not args:
+					method.__doc__ = 'Method taking no arguments'
+				elif len(args) == 1:
+					method.__doc__ = 'Method taking argument of type %s' % args[0]
+				else:
+					method.__doc__ = 'Method taking arguments of types %s' % ' '.join(args)
+
+				cls._player_methods[cls._method_prefix+name] = method
+				setattr(cls, cls._method_prefix+name, method)
+
+		@classmethod
+		def _add_properties(cls, mplayer_bin):
+			"""Discover which properties MPlayer understands and add them as class properties"""
+			def get_prop(name, prop_type, islist, self):
+				"""Function which calls the get_property method to get the property value and does some type checking"""
+				# self argument is needed to be property at the end because of partial
+				retval = getattr(self, cls._method_prefix+'get_property')(name)
+				if islist and retval == '(null)':
+					return []
+				if retval != None:
+					if prop_type != type(False):
+						if islist:
+							retval = [prop_type(x) for x in retval.split(',')]
+						else:
+							retval = prop_type(retval)
+					else:
+						if islist:
+							retval = [x == 'yes' for x in retval.split(',')]
+						else:
+							retval = (retval == 'yes')
+				return retval
+
+			# Function for getting and setting properties
+			def set_prop(name, prop_type, islist, prop_min, prop_max, self, value):
+				"""Function which calls the set_property method to set the property value and does some type checking"""
+				if islist:
+					for elem in value:
+						if type(elem) != prop_type:
+							raise TypeError('TypeError: Element %s has wrong type %s, not %s'%(elem, type(elem).__name__, prop_type))
+						if prop_min != None and elem < prop_min:
+							raise ValueError('ValueError: Element %s must be at least %s'%(elem, prop_min))
+						if prop_max != None and elem > prop_max:
+							raise ValueError('ValueError: Element %s must be at most %s'%(elem, prop_max))
+					value = ','.join([str(elem) for elem in value])
+				else:
+					if type(value) != prop_type:
+						raise TypeError('TypeError: %s has type %s, not %s'%(name, prop_type.__name__, type(value).__name__))
+					if prop_min != None and value < prop_min:
+						raise ValueError('ValueError: %s must be at least %s (>%s)'%(name, prop_min, value))
+					if prop_max != None and value > prop_max:
+						raise ValueError('ValueError: %s must be at most %s (<%s)'%(name, prop_max, value))
+
+				getattr(self, cls._method_prefix+'set_property')(name, str(value))
+
+			player = cls._run_player([mplayer_bin, '-list-properties'])
+			# Add each property found
+			for line in player.stdout:
+				parts = line.strip().decode('utf-8').split()
+				if not (len(parts) == 4 or (len(parts) == 5 and parts[2] == 'list')):
+					continue
+				name = parts[0]
+				try:
+					prop_type = cls._arg_types[parts[1]]
+				except KeyError:
+					continue
+
+				if parts[2] == 'list': # Actually a list
+					prop_min = parts[3]
+					prop_max = parts[4]
+					islist = True
+				else:
+					prop_min = parts[2]
+					prop_max = parts[3]
+					islist = False
+
+				if prop_min == 'No':
+					prop_min = None
+				else:
+					prop_min = prop_type(prop_min)
+
+				if prop_max == 'No':
+					prop_max = None
+				else:
+					prop_max = prop_type(prop_max)
+
+
+				getter = partial(get_prop, name, prop_type, islist)
+				setter = partial(set_prop, name, prop_type, islist, prop_min, prop_max)
+				setattr(cls, cls._property_prefix+name, property(getter, setter, doc='Property of type %s in range [%s, %s].'%(prop_type.__name__, prop_min, prop_max)))
+	# end of _MPlayer
+
+	_MPlayer._method_prefix = method_prefix
+	_MPlayer._property_prefix = property_prefix
+
+	_MPlayer._add_methods(mplayer_bin)
+	_MPlayer._add_properties(mplayer_bin)
+	return _MPlayer
+
+try:
+	MPlayer = make_mplayer_class() # pylint: disable-msg=C0103
+except PlayerNotFoundException as e:
+	print("Fatal:", e)
+
+if __name__ == "__main__":
+	import doctest
+	doctest.testmod(optionflags=doctest.ELLIPSIS)

+ 12 - 0
old/musikautomat_v1/requirements.txt

@@ -0,0 +1,12 @@
+eyeD3==0.8.11
+mypy==0.740
+mypy-extensions==0.4.3
+pkg-resources==0.0.0
+pygame==2.0.0.dev6
+python-magic==0.4.15
+PyYAML==5.1.2
+RPi.GPIO==0.7.0; platform_machine == 'armv7l'
+six==1.13.0
+tornado==6.0.3
+typed-ast==1.4.0
+typing-extensions==3.7.4.1

musikautomat/web/index.html → old/musikautomat_v1/web/index.html