Browse Source

Python server added

Yorick Rommers 10 years ago
parent
commit
dc7f8905ae

+ 16 - 0
yjmpd-c#/config.cfg

@@ -0,0 +1,16 @@
+[HTTP]
+port = 8585
+
+[Daemon]
+port = 17170
+
+[Library]
+musicdir = /media/USBHDD/shares/Music/English
+jancodir = /mnt/Muziek/
+
+[Database]
+username = yjmpd
+password = qLjStxr6xncrfcna
+host = imegumii.nl
+database = yjmpd
+port = 3306

+ 3 - 0
yjmpd-c#/requirements.txt

@@ -0,0 +1,3 @@
+configparser
+mutagen
+PyMySQL

+ 72 - 0
yjmpd-c#/scan.py

@@ -0,0 +1,72 @@
+import sys
+import os
+import configparser
+
+from yjdaemon.yjmpd import YJMPD
+from yjdaemon.HTTPServer import HTTPServerThread
+from yjdaemon.Database import Database
+from yjdaemon.libraryscanner import LibraryScanner
+
+debug = True
+
+config = configparser.ConfigParser()
+try:
+    config.read("config.cfg")
+    HTTP_PORT = int(config.get("HTTP", "port"))
+    DAEMON_PORT = int(config.get("Daemon","port"))
+    MUSIC_DIR = str(config.get("Library", "musicdir"))
+    DB_USERNAME = config.get("Database", "username")
+    DB_PASSWORD = config.get("Database", "password")
+    DB_HOST     = config.get("Database", "host")
+    DB_DATABASE = config.get("Database", "database")
+    DB_PORT     = config.getint("Database", "port")
+except Exception as e:
+    print(e.with_traceback())
+    sys.exit(1)
+
+class MainDaemon(YJMPD):
+    def run(self):
+        HTTP_thread = HTTPServerThread(HTTP_PORT)
+        HTTP_thread.start()
+
+
+def Test():
+    print("Test")
+
+    # socket_thread = ServiceSocket(DAEMON_PORT)
+    # socket_thread.start()
+    #HTTP_thread = HTTPServerThread(HTTP_PORT)
+    #HTTP_thread.start()
+    db = Database(DB_USERNAME, DB_PASSWORD, DB_HOST, DB_PORT, DB_DATABASE)
+    LibraryScanner(db, MUSIC_DIR)
+
+
+
+
+
+if debug:
+    Test()
+else:
+    if __name__ == "__main__":
+        username = os.getenv('USER')
+        if None == username:
+            dir = "/tmp/.pydaemon.pid"
+        else:
+            dir = "/home/" + username + "/.pydaemon.pid"
+        daemon = MainDaemon(dir, MUSIC_DIR)
+        if len(sys.argv) == 2:
+            if 'start' == sys.argv[1]:
+                daemon.start()
+            elif 'stop' == sys.argv[1]:
+                daemon.stop()
+            elif 'restart' == sys.argv[1]:
+                daemon.restart()
+            elif 'status' == sys.argv[1]:
+                daemon.status()
+            else:
+                print("Unknown command")
+                sys.exit(2)
+            sys.exit(0)
+        else:
+            print("usage: %s start|stop|status|restart" % sys.argv[0])
+            sys.exit(2)

+ 72 - 0
yjmpd-c#/test.py

@@ -0,0 +1,72 @@
+import sys
+import os
+import configparser
+
+from yjdaemon.yjmpd import YJMPD
+from yjdaemon.HTTPServer import HTTPServerThread
+from yjdaemon.Database import Database
+from yjdaemon.libraryscanner import LibraryScanner
+
+debug = True
+
+config = configparser.ConfigParser()
+try:
+    config.read("config.cfg")
+    HTTP_PORT = int(config.get("HTTP", "port"))
+    DAEMON_PORT = int(config.get("Daemon","port"))
+    MUSIC_DIR = str(config.get("Library", "musicdir"))
+    DB_USERNAME = config.get("Database", "username")
+    DB_PASSWORD = config.get("Database", "password")
+    DB_HOST     = config.get("Database", "host")
+    DB_DATABASE = config.get("Database", "database")
+    DB_PORT     = config.getint("Database", "port")
+except Exception as e:
+    print(e.with_traceback())
+    sys.exit(1)
+
+class MainDaemon(YJMPD):
+    def run(self):
+        HTTP_thread = HTTPServerThread(HTTP_PORT)
+        HTTP_thread.start()
+
+
+def Test():
+    print("Test")
+
+    # socket_thread = ServiceSocket(DAEMON_PORT)
+    # socket_thread.start()
+    HTTP_thread = HTTPServerThread(HTTP_PORT)
+    HTTP_thread.start()
+    #db = Database(DB_USERNAME, DB_PASSWORD, DB_HOST, DB_PORT, DB_DATABASE)
+    #LibraryScanner(db, MUSIC_DIR)
+
+
+
+
+
+if debug:
+    Test()
+else:
+    if __name__ == "__main__":
+        username = os.getenv('USER')
+        if None == username:
+            dir = "/tmp/.pydaemon.pid"
+        else:
+            dir = "/home/" + username + "/.pydaemon.pid"
+        daemon = MainDaemon(dir, MUSIC_DIR)
+        if len(sys.argv) == 2:
+            if 'start' == sys.argv[1]:
+                daemon.start()
+            elif 'stop' == sys.argv[1]:
+                daemon.stop()
+            elif 'restart' == sys.argv[1]:
+                daemon.restart()
+            elif 'status' == sys.argv[1]:
+                daemon.status()
+            else:
+                print("Unknown command")
+                sys.exit(2)
+            sys.exit(0)
+        else:
+            print("usage: %s start|stop|status|restart" % sys.argv[0])
+            sys.exit(2)

+ 169 - 0
yjmpd-c#/yjdaemon/API.py

@@ -0,0 +1,169 @@
+import json
+
+from yjdaemon.Database import Database as db
+import configparser
+"""
+Add a key to the validAPIcalls dictionary, with a corresponding function
+Function should return jsonified data, so that it can then be passed on to the client.
+example:
+
+Add this to the dictionary
+"getsongs": calls.getsongs
+
+then implement this function
+@staticmethod
+    def getsongs():
+        return calls.jsonify({"song": song})
+
+And the jsonified data will be returned to the client.
+
+Every function MUST return jsonified data!
+
+"""
+
+
+class calls:
+    @staticmethod
+    def APIcall(sanitizedpath):
+        if sanitizedpath in validAPIcalls:
+            return validAPIcalls[sanitizedpath]
+        else:
+            return None
+
+    @staticmethod
+    def getfromrawjson(data, param):
+        return calls.dejsonify(data)[param]
+
+    @staticmethod
+    def jsonify(data):
+        return json.dumps(data, sort_keys=True, indent=4).encode("utf-8")
+
+    @staticmethod
+    def dejsonify(rawdata):
+        return json.loads(rawdata.decode("utf-8"))
+
+    @staticmethod
+    def getallsongs(args):
+        return calls.jsonify({"songs": db.executequerystatic("SELECT * FROM tracks;"), "args": args , "result": "OK"})
+
+    @staticmethod
+    def getartists(args):
+        return calls.jsonify({"artists": db.executequerystatic("SELECT artistName FROM tracks GROUP BY artistName;"), "args": args, "result" : "OK"})
+
+    @staticmethod
+    def getalbums(args):
+        return calls.jsonify({"albums": db.executequerystatic("SELECT albumName FROM tracks GROUP BY albumName;"), "args": args, "result":"OK"})
+
+    @staticmethod
+    def getgenres(args):
+        return calls.jsonify({"genres": db.executequerystatic("SELECT genre FROM tracks GROUP BY genre;"), "args": args, "result":"OK"})
+
+    @staticmethod
+    def getyears(args):
+        return calls.jsonify({"years": db.executequerystatic("SELECT year FROM tracks GROUP BY year;"), "args": args, "result":"OK"})
+
+    @staticmethod
+    def getalbumnames(args):
+        return calls.jsonify({"albumnames": db.executequerystatic("SELECT albumName FROM tracks GROUP BY albumName;"), "args": args})
+
+    @staticmethod
+    def search(args):
+        """search?
+        q(gneeral search term)=iets&
+        artist=bleh
+        &genre=neeee"""
+        data = args.split("&")
+        songnames = """
+        SELECT * FROM tracks WHERE LCASE(trackName) LIKE LCASE(\"%{}%\") GROUP BY trackName;
+        """
+        artists = """
+        SELECT artistName FROM tracks WHERE LCASE(artistName) LIKE LCASE(\"%{}%\") GROUP BY artistName;
+        """
+        genres = """
+        SELECT genre FROM tracks WHERE LCASE(genre) LIKE LCASE(\"%{}%\") GROUP BY genre;
+        """
+        albums = """
+        SELECT albumName FROM tracks WHERE LCASE(albumName) LIKE LCASE(\"%{}%\") GROUP BY albumName;
+        """
+        genquery="""
+        SELECT * FROM tracks WHERE LCASE(genre) LIKE LCASE(\"%{}%\") OR LCASE(albumName) LIKE LCASE(\"%{}%\") OR LCASE(trackName) LIKE LCASE(\"%{}%\") OR LCASE(artistName) LIKE LCASE(\"%{}%\");
+        """
+        genqueryres = ""
+        artistsres = ""
+        genresres = ""
+        songnamesres = ""
+        albumsres = ""
+        for entry in data:
+            values = entry.split("=")
+            if values[0] == "q":
+                genqueryres = genquery.format( values[1], values[1], values[1], values[1])
+            elif values[0] == "artist":
+                artistsres = artists.format(values[1])
+            elif values[0] == "genre":
+                genresres = genres.format(values[1])
+            elif values[0] == "songname":
+                songnamesres = songnames.format(values[1])
+            elif values[0] == "album":
+                albumsres = albums.format(values[1])
+        return calls.jsonify({"result":"OK", "genres": db.executequerystatic(genresres), "songnames": db.executequerystatic(songnamesres), "artists": db.executequerystatic(artistsres), "albums": db.executequerystatic(albumsres), "query" : db.executequerystatic(genqueryres)})
+        return calls.jsonify({"result":"OK"})
+
+    @staticmethod
+    def getsongs(args):
+        """Get song by album, genre, year, artist"""
+        data = args.partition("?")
+        splitstring = data[0].partition("=")
+        name= splitstring[0]
+        value = splitstring[2].replace("%20"," ")
+        if name == "album":
+            songs = db.executequerystatic("SELECT * FROM tracks WHERE albumName = \"" + value + "\"")
+        elif name == "genre":
+            songs = db.executequerystatic("SELECT * FROM tracks WHERE genre = \"" + value + "\"")
+        elif name == "year":
+            songs = db.executequerystatic("SELECT * FROM tracks WHERE year = \"" + value + "\"")
+        elif name == "artist":
+            songs = db.executequerystatic("SELECT * FROM tracks WHERE artistName = \"" + value + "\"")
+        elif name == "search":
+            songs = db.executequerystatic("SELECT * FROM tracks WHERE LCASE(trackName) LIKE LCASE(\"%"+ value + "%\") GROUP BY trackName")
+        else:
+            return calls.jsonify({"result":"NOK", "errormsg": "Not a valid argument"})
+        return calls.jsonify({"result":"OK","songs":songs})
+
+    @staticmethod
+    def getsongbyid(args):
+        config = configparser.ConfigParser()
+        try:
+            config.read("config.cfg")
+            musicdir = config.get("Library","musicdir")
+            port = config.get("HTTP","port")
+        except:
+            return calls.jsonify({"result" : "NOK" , "errormsg" : "I/O error while reading config."})
+        data = args.split("&")
+        splitsting = data[0].split("=")
+        id = splitsting[1]
+        file = db.executequerystatic(
+            "SELECT SUBSTRING_INDEX(trackUrl,'" + musicdir + "',-1) as filedir FROM `tracks` WHERE id = " + id)
+        try:
+            url = str(file[0][0])
+        except:
+            return calls.jsonify({"result": "NOK", "errormsg" : "Song ID does not exist in database."})
+        return calls.jsonify({"result": "OK", "songurl": "http://imegumii.nl:"+ "/music/English"+ url})
+
+    @staticmethod
+    def setsong(args, songname):
+        global song
+        song = songname
+        return calls.jsonify({"result": "OK", "args": args})
+
+
+validAPIcalls = {"getallsongs": calls.getallsongs,
+                 "setsong": calls.setsong,
+                 "search": calls.search,
+                 "getsongs": calls.getsongs,
+                 "getsongbyid": calls.getsongbyid,
+                 "getartists": calls.getartists,
+                 "getalbums": calls.getalbums,
+                 "getgenres": calls.getgenres,
+                 "getyears": calls.getyears,
+                 "getalbumnames": calls.getalbumnames
+                 }

+ 81 - 0
yjmpd-c#/yjdaemon/Database.py

@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import pymysql
+import configparser
+from threading import RLock as Lock
+from warnings import filterwarnings
+
+filterwarnings('ignore', category = pymysql.Warning)
+class Database:
+
+    buffer = []
+    cout = 0
+    lock = Lock()
+    def __init__(self, DB_USERNAME, DB_PASSWORD, DB_HOST, DB_PORT, DB_DATABASE):
+        """Init class """
+        self.cnx = pymysql.connect(user=DB_USERNAME, password=DB_PASSWORD, host=DB_HOST,  database=DB_DATABASE, port=DB_PORT, charset='utf8')
+        self.cursor = self.cnx.cursor()
+
+    def executeQuery(self, query):
+        try:
+            self.cursor.execute(query)
+            self.cnx.commit()
+            return self.cursor.fetchall()
+        except pymysql.ProgrammingError as e:
+            print(e)
+            print(query)
+
+    @staticmethod
+    def executequerystatic(query):
+        config = configparser.ConfigParser()
+        try:
+            config.read("config.cfg")
+            DB_USERNAME = config.get("Database", "username")
+            DB_PASSWORD = config.get("Database", "password")
+            DB_HOST     = config.get("Database", "host")
+            DB_DATABASE = config.get("Database", "database")
+            DB_PORT     = config.getint("Database", "port")
+        except Exception as e:
+            print(e)
+
+        cnx = pymysql.connect(user=DB_USERNAME, password=DB_PASSWORD, host=DB_HOST,  database=DB_DATABASE, port=DB_PORT)
+        cursor = cnx.cursor()
+        if query != "":
+            cursor.execute(query)
+            cnx.commit()
+            return cursor.fetchall()
+        else:
+            return []
+
+
+    def turnoffautocommit(self):
+        self.cursor.execute("SET autocommit=0;")
+        self.cnx.commit()
+
+    def removeSong(self, path):
+         self.db.executeQuery("DELETE FROM `tracks` WHERE trackUrl = " + path.replace("'", '\\\''))
+
+    def updateInsertSong(self, genre, trackname, artistname, albumname, albumartist, tracknumber, year, duration, path): #notworking
+        self.db.executeQuery("INSERT INTO `tracks` (`genre`, `trackUrl`, `trackName`, `artistName`, `albumName`, `albumArtist`, `trackNumber`, `year`, `duration`) VALUES ('" + genre + b"','" + path.replace("'", '\\\'') + "','" + trackname + "','" + artistname + "','" + albumname + "','" + albumartist + "','" + tracknumber + "','" + year + "','0') " +
+                             b" ON DUPLICATE KEY UPDATE `genre`=VALUES(`genre`) , `trackName` = VALUES(`trackName`) , `artistName` = VALUES(`artistName`) ,`albumName` = VALUES(`albumName`) , `albumArtist` = VALUES(`albumArtist`) , `trackNumber` = VALUES(`trackNumber`) , `year` = VALUES(`year`) , `duration` = VALUES(`duration`)")
+
+    def insertMultipleSongs(self, genre, trackname, artistname, albumname, albumartist, tracknumber, year, duration, path):
+        self.buffer.append([genre, trackname, artistname, albumname, albumartist, tracknumber, year, duration, path])
+        # Database.lock.acquire()
+        if len(self.buffer) > 50:
+            self.pushbuffer()
+        # Database.lock.release()
+
+    def pushbuffer(self):
+        query = ('INSERT INTO `tracks` (`genre`, `trackUrl`, `trackName`, `artistName`, `albumName`, `albumArtist`, `trackNumber`, `year`, `duration`) VALUES ')
+        for song in self.buffer:
+            query += "("
+            for field in song:
+                query += "'" + field + "',"
+            query = query[:-1]
+            query += "),"
+        query = query[:-1]
+        query += " ON DUPLICATE KEY UPDATE `genre`=VALUES(`genre`) , `trackName` = VALUES(`trackName`) , `artistName` = VALUES(`artistName`) ,`albumName` = VALUES(`albumName`) , `albumArtist` = VALUES(`albumArtist`) , `trackNumber` = VALUES(`trackNumber`) , `year` = VALUES(`year`) , `duration` = VALUES(`duration`);"
+        self.executeQuery(query)
+        del self.buffer[:]
+

+ 82 - 0
yjmpd-c#/yjdaemon/HTTPServer.py

@@ -0,0 +1,82 @@
+#!/usr/bin/env python3
+
+
+import http.server
+import socketserver
+import threading
+import html
+
+import yjdaemon.API as API
+
+"""
+HTTP request handler.
+"""
+
+
+class HTTPServerThread(threading.Thread):
+    def __init__(self, PORT):
+        threading.Thread.__init__(self)
+        self.PORT = PORT
+
+    def run(self):
+        Handler = HTTPHandler
+        socketserver.TCPServer.allow_reuse_address = True
+        httpd = socketserver.TCPServer(("", self.PORT), Handler)
+        httpd.serve_forever()
+
+
+"""
+REST API
+"""
+
+
+class HTTPHandler(http.server.SimpleHTTPRequestHandler):
+    def send_message(self, status, content_type, data):
+        self.send_response(status)
+        self.send_header("Content-type:", content_type)
+        self.end_headers()
+        self.wfile.write(data)
+        self.wfile.write("\n".encode("utf-8"))
+
+    def do_GET(self):
+        """ Serve a GET request. """
+        path = html.unescape(self.path)
+        path = str(path).lstrip("/").split("?")[0]
+        retval = API.calls.APIcall(path)
+        if retval is not None:  # if call is valid API function
+            try:
+                args = str(html.unescape(self.path)).lstrip("/").split("?")[1]
+            except IndexError as e:
+                print(e)
+                self.send_message(403, "application/json", API.calls.jsonify({"error": "Missing parameters."}))
+                return
+            self.send_message(200, "application/json", retval(args))
+        else:  # else parse as normal HTTP request
+            f = self.send_head()
+            if f:
+                try:
+                    self.copyfile(f, self.wfile)
+                finally:
+                    f.close()
+
+    def do_POST(self):
+        """ Serve a POST request. """
+        path = html.unescape(self.path)
+        path = str(path).lstrip("/").split("?")[0]
+        retval = API.calls.APIcall(path)
+        if retval is not None:  # if call is valid API function
+            try:
+                args = str(html.unescape(self.path)).lstrip("/").split("?")[1] #if no args
+            except IndexError as e:
+                print(e)
+                self.send_message(403, "application/json", API.calls.jsonify({"error": "Missing parameters."}))
+                return
+            if self.headers["Content-Type"] == 'application/json':  # if data is json data
+                length = int(self.headers["Content-Length"])
+                rawdata = self.rfile.read(length)
+                result = retval(args, API.calls.getfromrawjson(rawdata, "data"))
+                self.send_message(200, "application/json", result)
+            else:  # if not json data
+                self.send_message(422, "application/json", API.calls.jsonify({"error": "Not JSON data."}))
+        else:  # if not API function
+            self.send_message(403, "application/json", API.calls.jsonify({"error": "Not an API function"}))

+ 36 - 0
yjmpd-c#/yjdaemon/ServiceSocket.py

@@ -0,0 +1,36 @@
+import socket
+import threading
+
+endline = "\r\n"
+
+class ServiceSocket(threading.Thread):
+    def __init__(self, PORT):
+        threading.Thread.__init__(self)
+        self.PORT = PORT
+
+    def run(self):
+        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        self.s.bind(("", self.PORT))
+        self.s.listen(1)
+        while 1:
+            conn, addr = self.s.accept()
+            ClientHandler(conn).start()
+
+
+
+class ClientHandler(threading.Thread):
+    def __init__(self,conn):
+        threading.Thread.__init__(self)
+        self.conn = conn
+    def run(self):
+        while 1:
+            data = self.conn.recv(1024)
+            if not data:
+                break
+            else:
+                datastring = data.decode('utf-8', "ignore").rstrip(endline).lower()
+                print(datastring)
+
+
+

+ 0 - 0
yjmpd-c#/yjdaemon/__init__.py


BIN
yjmpd-c#/yjdaemon/__pycache__/API.cpython-34.pyc


BIN
yjmpd-c#/yjdaemon/__pycache__/API.cpython-35.pyc


BIN
yjmpd-c#/yjdaemon/__pycache__/Database.cpython-34.pyc


BIN
yjmpd-c#/yjdaemon/__pycache__/Database.cpython-35.pyc


BIN
yjmpd-c#/yjdaemon/__pycache__/HTTPServer.cpython-34.pyc


BIN
yjmpd-c#/yjdaemon/__pycache__/HTTPServer.cpython-35.pyc


BIN
yjmpd-c#/yjdaemon/__pycache__/__init__.cpython-34.pyc


BIN
yjmpd-c#/yjdaemon/__pycache__/__init__.cpython-35.pyc


BIN
yjmpd-c#/yjdaemon/__pycache__/libraryscanner.cpython-34.pyc


BIN
yjmpd-c#/yjdaemon/__pycache__/libraryscanner.cpython-35.pyc


BIN
yjmpd-c#/yjdaemon/__pycache__/yjmpd.cpython-34.pyc


BIN
yjmpd-c#/yjdaemon/__pycache__/yjmpd.cpython-35.pyc


+ 78 - 0
yjmpd-c#/yjdaemon/libraryscanner.py

@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from mutagen.easyid3 import EasyID3
+from mutagen.mp3 import MP3
+import mutagen._util
+import os
+from watchdog.observers import Observer
+from watchdog.events import FileSystemEventHandler
+
+class LibraryScanner:
+
+    def __init__(self, Database, librarypath):
+        """Init class """
+        self.url = librarypath
+        self.db = Database
+        self.scanRecursif()
+        ob = Observer()
+        ob.schedule(Filehandler(self), self.url, recursive=True)
+        ob.start()
+
+
+    def scanRecursif(self):
+        print("Scanning library "+self.url+" recursively...")
+        # musicdirs = [os.path.join(self.url,o) for o in os.listdir(self.url) if os.path.isdir(os.path.join(self.url,o))]
+        self.db.turnoffautocommit()
+        for root, directories, filenames in os.walk(self.url):
+            self.scandir(filenames,root)
+
+    def scandir(self,filenames, root):
+        for filename in filenames:
+            if filename.lower().endswith(('.mp3')):
+                path = os.path.join(root,filename)
+                try:
+                    print(path, end='\r')
+                    id3 = EasyID3(path)
+                    audio = MP3(path)
+                    print(audio.info.length)
+                    self.db.insertMultipleSongs(self.getValue(id3, "genre"),path.replace("'", '\\\''),self.getValue(id3, "title"),self.getValue(id3, "artist"),self.getValue(id3, "album"),self.getValue(id3, "performer"),self.getValue(id3, "tracknumber"),self.getValue(id3, "date"),str(audio.info.length))
+                except (mutagen.id3._util.ID3NoHeaderError):
+                    print("Error reading ID3 tag",  end='\r')
+
+
+    def insertSong(self, path):
+        try:
+            id3 = EasyID3(path)
+            self.db.executeQuery(b"INSERT INTO `tracks` (`genre`, `trackUrl`, `trackName`, `artistName`, `albumName`, `albumArtist`, `trackNumber`, `year`, `duration`) VALUES ('" + self.getValue(id3,"genre") + b"'," + b"'" + path.replace("'", '\\\'').encode('utf8') + b"'," + b"'" + self.getValue(id3, "title") + b"'," + b"'" + self.getValue(id3, "artist") + b"'," + b"'" +  self.getValue(id3, "album") + b"'," + b"'" +  self.getValue(id3, "performer") + b"'," + b"'" + self.getValue(id3, "tracknumber") + b"'," + b"'" + self.getValue(id3, "date") + b"'," + b"'0') "  +
+                                 b" ON DUPLICATE KEY UPDATE `genre`=VALUES(`genre`) , `trackName` = VALUES(`trackName`) , `artistName` = VALUES(`artistName`) ,`albumName` = VALUES(`albumName`) , `albumArtist` = VALUES(`albumArtist`) , `trackNumber` = VALUES(`trackNumber`) , `year` = VALUES(`year`) , `duration` = VALUES(`duration`)")
+
+        except (mutagen.id3._util.ID3NoHeaderError):
+            pass
+
+    def removeSong(self,path):
+        self.db.removeSong(path)
+
+    def getValue(self, id3, value):
+        try:
+            return id3[value][0].replace("'", "\\'")
+        except (KeyError,  IndexError, ValueError):
+            print("Error reading value of ID3 tag",  end='\r')
+        return ""
+
+
+class Filehandler(FileSystemEventHandler):
+    def __init__(self, LibraryScanner):
+        self.libscanner = LibraryScanner
+
+    def process(self, event):
+        if not(event.is_directory):
+            if os.path.isfile(event.src_path) and event.src_path.lower().endswith(('.mp3','.flac', 'm4a')):
+                self.libscanner.insertSong(event.src_path)
+            else:
+                self.libscanner.removeSong(event.src_path)
+
+    def on_modified(self, event):
+        self.process(event)
+
+    def on_created(self, event):
+        self.process(event)

+ 60 - 0
yjmpd-c#/yjdaemon/maindaemon.py

@@ -0,0 +1,60 @@
+import sys
+import os
+import configparser
+from yjdaemon.Database import Database
+from yjdaemon.libraryscanner import LibraryScanner
+from yjdaemon.yjmpd import YJMPD
+from yjdaemon.HTTPServer import HTTPServerThread
+
+debug = False
+
+config = configparser.ConfigParser()
+try:
+    config.read("../config.cfg")
+    HTTP_PORT = int(config.get("HTTP", "port"))
+    DAEMON_PORT = int(config.get("Daemon", "port"))
+    MUSIC_DIR = str(config.get("Library", "jancodir"))
+
+    DB_USERNAME = config.get("Database", "username")
+    DB_PASSWORD = config.get("Database", "password")
+    DB_HOST     = config.get("Database", "host")
+    DB_DATABASE = config.get("Database", "database")
+    DB_PORT     = config.getint("Database", "port")
+
+except Exception as e:
+    print(e.with_traceback())
+    sys.exit(1)
+
+
+class MainDaemon(YJMPD):
+    def run(self):
+        HTTP_thread = HTTPServerThread(HTTP_PORT)
+        HTTP_thread.start()
+        db = Database(DB_USERNAME, DB_PASSWORD, DB_HOST, DB_PORT, DB_DATABASE)
+        LibraryScanner(db, MUSIC_DIR)
+
+
+
+if __name__ == "__main__":
+    username = os.getenv('USER')
+    if None == username:
+        dir = "/tmp/.pydaemon.pid"
+    else:
+        dir = "/home/" + username + "/.pydaemon.pid"
+    daemon = MainDaemon(dir, MUSIC_DIR)
+    if len(sys.argv) == 2:
+        if 'start' == sys.argv[1]:
+            daemon.start()
+        elif 'stop' == sys.argv[1]:
+            daemon.stop()
+        elif 'restart' == sys.argv[1]:
+            daemon.restart()
+        elif 'status' == sys.argv[1]:
+            daemon.status()
+        else:
+            print("Unknown command")
+            sys.exit(2)
+        sys.exit(0)
+    else:
+        print("usage: %s start|stop|status|restart" % sys.argv[0])
+        sys.exit(2)

+ 135 - 0
yjmpd-c#/yjdaemon/yjmpd.py

@@ -0,0 +1,135 @@
+import sys
+import os
+import time
+import atexit
+import signal
+
+
+class YJMPD:
+    """A generic yjdaemon class.
+
+    Usage: subclass the yjdaemon class and override the run() method."""
+
+    def __init__(self, pidfile, root):
+        self.pidfile = pidfile
+        self.root = root
+
+    def daemonize(self):
+        """Deamonize class. UNIX double fork mechanism."""
+
+        try:
+            pid = os.fork()
+            if pid > 0:
+                # exit first parent
+                sys.exit(0)
+        except OSError as err:
+            sys.stderr.write('fork #1 failed: {0}\n'.format(err))
+            sys.exit(1)
+
+        # decouple from parent environment
+        os.chdir(self.root)
+        os.setsid()
+        os.umask(0)
+
+        # do second fork
+        try:
+            pid = os.fork()
+            if pid > 0:
+                # exit from second parent
+                sys.exit(0)
+        except OSError as err:
+            sys.stderr.write('fork #2 failed: {0}\n'.format(err))
+            sys.exit(1)
+
+        # redirect standard file descriptors
+        sys.stdout.flush()
+        sys.stderr.flush()
+        si = open(os.devnull, 'r')
+        so = open(os.devnull, 'a+')
+        se = open(os.devnull, 'a+')
+
+        os.dup2(si.fileno(), sys.stdin.fileno())
+        os.dup2(so.fileno(), sys.stdout.fileno())
+        os.dup2(se.fileno(), sys.stderr.fileno())
+
+        # write pidfile
+        atexit.register(self.delpid)
+
+        pid = str(os.getpid())
+        with open(self.pidfile, 'w+') as f:
+            f.write(pid + '\n')
+
+    def delpid(self):
+        os.remove(self.pidfile)
+
+    def start(self):
+        """Start the yjdaemon."""
+
+        # Check for a pidfile to see if the yjdaemon already runs
+        try:
+            with open(self.pidfile, 'r') as pf:
+
+                pid = int(pf.read().strip())
+        except IOError:
+            pid = None
+
+        if pid:
+            message = "pidfile {0} already exist. " + \
+                      "Daemon already running?\n"
+            sys.stderr.write(message.format(self.pidfile))
+            sys.exit(1)
+
+        # Start the yjdaemon
+        self.daemonize()
+        self.run()
+
+    def stop(self):
+        """Stop the yjdaemon."""
+
+        # Get the pid from the pidfile
+        try:
+            with open(self.pidfile, 'r') as pf:
+                pid = int(pf.read().strip())
+        except IOError:
+            pid = None
+
+        if not pid:
+            message = "pidfile {0} does not exist. " + \
+                      "Daemon not running?\n"
+            sys.stderr.write(message.format(self.pidfile))
+            return  # not an error in a restart
+
+        # Try killing the yjdaemon process
+        try:
+            while 1:
+                os.kill(pid, signal.SIGTERM)
+                time.sleep(0.1)
+        except OSError as err:
+            e = str(err.args)
+            if e.find("No such process") > 0:
+                if os.path.exists(self.pidfile):
+                    os.remove(self.pidfile)
+            else:
+                print(str(err.args))
+                sys.exit(1)
+
+    def restart(self):
+        """Restart the yjdaemon."""
+        self.stop()
+        self.start()
+
+
+
+    def status(self):
+        """Print out status."""
+        if os.path.exists(self.pidfile):
+            message = "Daemon running.\n"
+        else:
+            message = "Daemon not running.\n"
+        sys.stdout.write(message)
+
+    def run(self):
+        """You should override this method when you subclass Daemon.
+
+        It will be called after the process has been daemonized by
+        start() or restart()."""

+ 64 - 0
yjmpd-c#/yjmpd.sql

@@ -0,0 +1,64 @@
+-- phpMyAdmin SQL Dump
+-- version 4.3.8deb0.1
+-- http://www.phpmyadmin.net
+--
+-- Host: localhost
+-- Gegenereerd op: 27 sep 2015 om 22:19
+-- Serverversie: 5.6.19-0ubuntu0.14.04.1
+-- PHP-versie: 5.5.9-1ubuntu4.11
+
+SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
+SET time_zone = "+00:00";
+
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8 */;
+
+--
+-- Database: `yjmpd`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Tabelstructuur voor tabel `tracks`
+--
+
+CREATE TABLE IF NOT EXISTS `tracks` (
+  `id` int(11) NOT NULL,
+  `genre` varchar(100) DEFAULT NULL,
+  `trackUrl` varchar(200) NOT NULL,
+  `trackName` varchar(100) DEFAULT NULL,
+  `artistName` varchar(100) DEFAULT NULL,
+  `albumName` varchar(100) DEFAULT NULL,
+  `albumArtist` varchar(100) DEFAULT NULL,
+  `trackNumber` int(11) DEFAULT NULL,
+  `year` int(11) DEFAULT NULL,
+  `duration` time DEFAULT NULL,
+  `playCount` int(11) NOT NULL DEFAULT '0'
+) ENGINE=InnoDB AUTO_INCREMENT=8135 DEFAULT CHARSET=latin1;
+
+--
+-- Indexen voor geëxporteerde tabellen
+--
+
+--
+-- Indexen voor tabel `tracks`
+--
+ALTER TABLE `tracks`
+  ADD PRIMARY KEY (`id`), ADD UNIQUE KEY `trackUrl` (`trackUrl`);
+
+--
+-- AUTO_INCREMENT voor geëxporteerde tabellen
+--
+
+--
+-- AUTO_INCREMENT voor een tabel `tracks`
+--
+ALTER TABLE `tracks`
+  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=8135;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;