diff --git a/yjmpd-c#/config.cfg b/yjmpd-c#/config.cfg new file mode 100644 index 0000000..d4b4f20 --- /dev/null +++ b/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 diff --git a/yjmpd-c#/requirements.txt b/yjmpd-c#/requirements.txt new file mode 100644 index 0000000..96bc99c --- /dev/null +++ b/yjmpd-c#/requirements.txt @@ -0,0 +1,3 @@ +configparser +mutagen +PyMySQL \ No newline at end of file diff --git a/yjmpd-c#/scan.py b/yjmpd-c#/scan.py new file mode 100644 index 0000000..778d0dd --- /dev/null +++ b/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) diff --git a/yjmpd-c#/test.py b/yjmpd-c#/test.py new file mode 100644 index 0000000..446975e --- /dev/null +++ b/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) diff --git a/yjmpd-c#/yjdaemon/API.py b/yjmpd-c#/yjdaemon/API.py new file mode 100644 index 0000000..1c6d5f1 --- /dev/null +++ b/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 + } diff --git a/yjmpd-c#/yjdaemon/Database.py b/yjmpd-c#/yjdaemon/Database.py new file mode 100644 index 0000000..69234a5 --- /dev/null +++ b/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[:] + diff --git a/yjmpd-c#/yjdaemon/HTTPServer.py b/yjmpd-c#/yjdaemon/HTTPServer.py new file mode 100644 index 0000000..8fa28a2 --- /dev/null +++ b/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"})) diff --git a/yjmpd-c#/yjdaemon/ServiceSocket.py b/yjmpd-c#/yjdaemon/ServiceSocket.py new file mode 100644 index 0000000..ee848b0 --- /dev/null +++ b/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) + + + diff --git a/yjmpd-c#/yjdaemon/__init__.py b/yjmpd-c#/yjdaemon/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yjmpd-c#/yjdaemon/__pycache__/API.cpython-34.pyc b/yjmpd-c#/yjdaemon/__pycache__/API.cpython-34.pyc new file mode 100644 index 0000000..c6ce791 Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/API.cpython-34.pyc differ diff --git a/yjmpd-c#/yjdaemon/__pycache__/API.cpython-35.pyc b/yjmpd-c#/yjdaemon/__pycache__/API.cpython-35.pyc new file mode 100644 index 0000000..0c87cbe Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/API.cpython-35.pyc differ diff --git a/yjmpd-c#/yjdaemon/__pycache__/Database.cpython-34.pyc b/yjmpd-c#/yjdaemon/__pycache__/Database.cpython-34.pyc new file mode 100644 index 0000000..82b5180 Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/Database.cpython-34.pyc differ diff --git a/yjmpd-c#/yjdaemon/__pycache__/Database.cpython-35.pyc b/yjmpd-c#/yjdaemon/__pycache__/Database.cpython-35.pyc new file mode 100644 index 0000000..ea5bc60 Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/Database.cpython-35.pyc differ diff --git a/yjmpd-c#/yjdaemon/__pycache__/HTTPServer.cpython-34.pyc b/yjmpd-c#/yjdaemon/__pycache__/HTTPServer.cpython-34.pyc new file mode 100644 index 0000000..402bbf8 Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/HTTPServer.cpython-34.pyc differ diff --git a/yjmpd-c#/yjdaemon/__pycache__/HTTPServer.cpython-35.pyc b/yjmpd-c#/yjdaemon/__pycache__/HTTPServer.cpython-35.pyc new file mode 100644 index 0000000..85640e5 Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/HTTPServer.cpython-35.pyc differ diff --git a/yjmpd-c#/yjdaemon/__pycache__/__init__.cpython-34.pyc b/yjmpd-c#/yjdaemon/__pycache__/__init__.cpython-34.pyc new file mode 100644 index 0000000..a64ad48 Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/__init__.cpython-34.pyc differ diff --git a/yjmpd-c#/yjdaemon/__pycache__/__init__.cpython-35.pyc b/yjmpd-c#/yjdaemon/__pycache__/__init__.cpython-35.pyc new file mode 100644 index 0000000..15eb217 Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/__init__.cpython-35.pyc differ diff --git a/yjmpd-c#/yjdaemon/__pycache__/libraryscanner.cpython-34.pyc b/yjmpd-c#/yjdaemon/__pycache__/libraryscanner.cpython-34.pyc new file mode 100644 index 0000000..e7689ef Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/libraryscanner.cpython-34.pyc differ diff --git a/yjmpd-c#/yjdaemon/__pycache__/libraryscanner.cpython-35.pyc b/yjmpd-c#/yjdaemon/__pycache__/libraryscanner.cpython-35.pyc new file mode 100644 index 0000000..c8eccc5 Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/libraryscanner.cpython-35.pyc differ diff --git a/yjmpd-c#/yjdaemon/__pycache__/yjmpd.cpython-34.pyc b/yjmpd-c#/yjdaemon/__pycache__/yjmpd.cpython-34.pyc new file mode 100644 index 0000000..1fc75f0 Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/yjmpd.cpython-34.pyc differ diff --git a/yjmpd-c#/yjdaemon/__pycache__/yjmpd.cpython-35.pyc b/yjmpd-c#/yjdaemon/__pycache__/yjmpd.cpython-35.pyc new file mode 100644 index 0000000..dd11cff Binary files /dev/null and b/yjmpd-c#/yjdaemon/__pycache__/yjmpd.cpython-35.pyc differ diff --git a/yjmpd-c#/yjdaemon/libraryscanner.py b/yjmpd-c#/yjdaemon/libraryscanner.py new file mode 100755 index 0000000..2d66232 --- /dev/null +++ b/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) diff --git a/yjmpd-c#/yjdaemon/maindaemon.py b/yjmpd-c#/yjdaemon/maindaemon.py new file mode 100644 index 0000000..408f931 --- /dev/null +++ b/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) diff --git a/yjmpd-c#/yjdaemon/yjmpd.py b/yjmpd-c#/yjdaemon/yjmpd.py new file mode 100644 index 0000000..1cdd2b6 --- /dev/null +++ b/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().""" diff --git a/yjmpd-c#/yjmpd.sql b/yjmpd-c#/yjmpd.sql new file mode 100644 index 0000000..e0e60be --- /dev/null +++ b/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 */;