diff --git a/pysonic/api.py b/pysonic/api.py index 211c694..14cb9ed 100644 --- a/pysonic/api.py +++ b/pysonic/api.py @@ -198,7 +198,7 @@ class PysonicApi(object): index = response.add_child("index", _parent="indexes", name=letter.upper()) for artist in artists: if artist["name"][0].lower() in letter: - response.add_child("artist", _real_parent=index, id=artist["id"], name=artist["name"]) + response.add_child("artist", _real_parent=index, id=artist["dir"], name=artist["name"]) return response @cherrypy.expose @@ -245,33 +245,54 @@ class PysonicApi(object): """ List an artist dir """ - artist_id = int(id) + dir_id = int(id) cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' response = ApiResponse() response.add_child("directory") - artist = self.library.get_artists(id=artist_id)[0] - children = self.library.get_albums(artist=artist_id) - response.set_attrs(_path="directory", name=artist['name'], id=artist['id'], - parent=artist['libraryid'], playCount=10) + dirtype, dirinfo, entity = self.library.db.get_musicdir(dirid=dir_id) - for item in children: + from pprint import pprint + pprint(dirinfo) + pprint(entity) + + response.set_attrs(_path="directory", name=entity['name'], id=entity['id'], + parent=dirinfo['parent'], playCount=420) + + for childtype, child in entity["children"]: # omit not dirs and media in browser # if not item["isdir"] and item["type"] not in MUSIC_TYPES: # continue # item_meta = item['metadata'] + moreargs = {} + if childtype == "album": + moreargs.update(name=child["name"], + isDir="true", # TODO song files in artist dir + parent=entity["id"], + coverArt=child["coverid"], + id=child["dir"]) + # album=item["name"], + # title=item["name"], # TODO dupe? + # artist=artist["name"], + # coverArt=item["coverid"], + elif childtype == "song": + moreargs.update(name=child["title"], + artist=child["_artist"]["name"], + contentType=child["format"], + coverArt=entity["coverid"], + id=child["id"], # this is probably fucked ? + duration=child["length"], + isDir="false", + parent=entity["dir"], + # title=xxx + ) + # duration="230" size="8409237" suffix="mp3" track="2" year="2005"/> response.add_child("child", _parent="directory", - album=item["name"], - title=item["name"], # TODO dupe? - artist=artist["name"], - coverArt=item["coverid"], - id=item["id"], - isDir="false", # TODO song files in artist dir - parent=artist["id"], size="4096", - type="music") + type="music", + **moreargs) return response @@ -284,11 +305,7 @@ class PysonicApi(object): :param directory: :param dir_meta: """ - print("\n\n\n") - print(item) - print(item_meta) - print(directory) - print(dir_meta) + raise Exception("stop using this") child = dict(id=item["id"], parent=item["id"], isDir="true" if "file" not in item else "false", @@ -328,15 +345,19 @@ class PysonicApi(object): def stream_view(self, id, maxBitRate="256", **kwargs): maxBitRate = int(maxBitRate) assert maxBitRate >= 32 and maxBitRate <= 320 - fpath = self.library.get_filepath(id) - meta = self.library.get_file_metadata(id) - to_bitrate = min(maxBitRate, self.options.max_bitrate, meta.get("media_kbitrate", 320)) + song = self.library.get_song(id) + fpath = "library/" + song["file"] + # import pdb + # from pprint import pprint + # pdb.set_trace() + # meta = self.library.get_file_metadata(id) + to_bitrate = min(maxBitRate, self.options.max_bitrate, song.get("bitrate", 320 * 1024) / 1024) cherrypy.response.headers['Content-Type'] = 'audio/mpeg' - if "media_length" in meta: - cherrypy.response.headers['X-Content-Duration'] = str(int(meta['media_length'])) + #if "media_length" in meta: + # cherrypy.response.headers['X-Content-Duration'] = str(int(meta['media_length'])) cherrypy.response.headers['X-Content-Kbitrate'] = str(to_bitrate) - if (self.options.skip_transcode or meta.get("media_kbitrate", -1) == to_bitrate) \ - and meta["type"] == "audio/mpeg": + if (self.options.skip_transcode or song.get("bitrate", -1024) / 1024 == to_bitrate) \ + and format["type"] == "audio/mpeg": def content(): with open(fpath, "rb") as f: while True: @@ -346,10 +367,10 @@ class PysonicApi(object): yield data return content() else: - transcode_meta = "transcoded_{}_size".format(to_bitrate) - if transcode_meta in meta: - cherrypy.response.headers['Content-Length'] = str(int(meta[transcode_meta])) - + # transcode_meta = "transcoded_{}_size".format(to_bitrate) + # if transcode_meta in meta: + # cherrypy.response.headers['Content-Length'] = str(int(meta[transcode_meta])) + print(fpath) transcode_args = ["ffmpeg", "-i", fpath, "-map", "0:0", "-b:a", "{}k".format(to_bitrate), "-v", "0", "-f", "mp3", "-"] @@ -359,13 +380,13 @@ class PysonicApi(object): def content(proc): length = 0 - completed = False + # completed = False start = time() try: while True: data = proc.stdout.read(16 * 1024) if not data: - completed = True + # completed = True break yield data length += len(data) @@ -373,8 +394,8 @@ class PysonicApi(object): proc.poll() if proc.returncode is None or proc.returncode == 0: logging.warning("transcoded {} in {}s".format(id, int(time() - start))) - if completed: - self.library.report_transcode(id, to_bitrate, length) + # if completed: + # self.library.report_transcode(id, to_bitrate, length) else: logging.error("transcode of {} exited with code {} after {}s".format(id, proc.returncode, int(time() - start))) @@ -394,7 +415,8 @@ class PysonicApi(object): @cherrypy.expose def getCoverArt_view(self, id, **kwargs): - fpath = self.library.get_filepath(id) + cover = self.library.get_cover(id) + fpath = "library/" + cover["path"] type2ct = { 'jpg': 'image/jpeg', 'png': 'image/png', diff --git a/pysonic/daemon.py b/pysonic/daemon.py index c86802a..c45b7f1 100644 --- a/pysonic/daemon.py +++ b/pysonic/daemon.py @@ -1,7 +1,7 @@ import os import logging import cherrypy -from sqlite3 import IntegrityError +from sqlite3 import DatabaseError from pysonic.api import PysonicApi from pysonic.library import PysonicLibrary from pysonic.database import PysonicDatabase, DuplicateRootException @@ -47,7 +47,7 @@ def main(): for username, password in args.user: try: db.add_user(username, password) - except IntegrityError: + except DatabaseError: db.update_user(username, password) # logging.warning("Libraries: {}".format([i["name"] for i in library.get_libraries()])) diff --git a/pysonic/database.py b/pysonic/database.py index cbb401b..c633459 100644 --- a/pysonic/database.py +++ b/pysonic/database.py @@ -54,16 +54,23 @@ class PysonicDatabase(object): 'id' INTEGER PRIMARY KEY AUTOINCREMENT, 'name' TEXT, 'path' TEXT UNIQUE);""", + """CREATE TABLE 'dirs' ( + 'id' INTEGER PRIMARY KEY AUTOINCREMENT, + 'library' INTEGER, + 'parent' INTEGER, + 'name' TEXT, + UNIQUE(parent, name) + )""", """CREATE TABLE 'artists' ( 'id' INTEGER PRIMARY KEY AUTOINCREMENT, 'libraryid' INTEGER, - 'dir' TEXT UNIQUE, + 'dir' INTEGER UNIQUE, 'name' TEXT)""", """CREATE TABLE 'albums' ( 'id' INTEGER PRIMARY KEY AUTOINCREMENT, 'artistid' INTEGER, 'coverid' INTEGER, - 'dir' TEXT, + 'dir' INTEGER, 'name' TEXT, UNIQUE (artistid, dir));""", """CREATE TABLE 'songs' ( @@ -141,16 +148,24 @@ class PysonicDatabase(object): return libs @readcursor - def get_artists(self, cursor, id=None, sortby=None, order=None): + def get_artists(self, cursor, id=None, dirid=None, sortby=None, order=None): assert order in ["asc", "desc", None] artists = [] q = "SELECT * FROM artists" params = [] + conditions = [] if id: - q += " WHERE id = ?" + conditions.append("id = ?") params.append(id) + if dirid: + conditions.append("dir = ?") + params.append(dirid) + if conditions: + q += " WHERE " + " AND ".join(conditions) if sortby: q += " ORDER BY {} {}".format(sortby, order.upper() if order else "ASC") + print(q) + print(params) cursor.execute(q, params) for row in cursor: artists.append(row) @@ -181,10 +196,86 @@ class PysonicDatabase(object): albums.append(row) return albums + @readcursor + def get_song(self, cursor, songid): + for item in cursor.execute("SELECT * FROM songs WHERE id=?", (songid, )): + return item + return None + # @readcursor + # def get_artist_by_dir(self, cursor, dirid): + # for row in cursor.execute(""" + # SELECT artists.* + # FROM dirs + # INNER JOIN artists + # ON artists.dir = dirs.id + # WHERE dirs.id=?""", (dirid, )): + # return [row] + # return [] + + @readcursor + def get_cover(self, cursor, coverid): + cover = None + for cover in cursor.execute("SELECT * FROM covers WHERE id = ?", (coverid, )): + return cover + + @readcursor + def get_musicdir(self, cursor, dirid): + """ + The world is a harsh place. + Again, this bullshit exists only to serve subsonic clients. Given a directory ID it returns a dict containing: + - the directory itself + - its parent + - its child dirs + - its child media + + that's a lie, it's a tuple and it's full of BS. read the code + """ + # find directory + dirinfo = None + for dirinfo in cursor.execute("SELECT * FROM dirs WHERE id = ?", (dirid, )): + pass + assert dirinfo + + ret = None + + # see if it matches the artists or albums table + artist = None + for artist in cursor.execute("SELECT * FROM artists WHERE dir = ?", (dirid, )): + pass + + # if artist: + # get child albums + if artist: + ret = ("artist", dirinfo, artist) + children = [] + for album in cursor.execute("SELECT * FROM albums WHERE artistid = ?", (artist["id"], )): + children.append(("album", album)) + ret[2]['children'] = children + return ret + + # else if album: + # get child tracks + album = None + for album in cursor.execute("SELECT * FROM albums WHERE dir = ?", (dirid, )): + pass + if album: + ret = ("album", dirinfo, album) + + artist_info = cursor.execute("SELECT * FROM artists WHERE id = ?", (album["artistid"], )).fetchall()[0] + + children = [] + for song in cursor.execute("SELECT * FROM songs WHERE albumid = ?", (album["id"], )): + song["_artist"] = artist_info + children.append(("song", song)) + ret[2]['children'] = children + return ret + + + diff --git a/pysonic/library.py b/pysonic/library.py index 090c70a..57c9e3b 100644 --- a/pysonic/library.py +++ b/pysonic/library.py @@ -35,6 +35,8 @@ class PysonicLibrary(object): self.get_libraries = self.db.get_libraries self.get_artists = self.db.get_artists self.get_albums = self.db.get_albums + self.get_song = self.db.get_song + self.get_cover = self.db.get_cover self.scanner = PysonicFilesystemScanner(self) logging.info("library ready") @@ -74,6 +76,9 @@ class PysonicLibrary(object): "largeImageUrl": "", "similarArtists": []} + # def get_cover(self, cover_id): + # cover = self.db.get_cover(cover_id) + diff --git a/pysonic/scanner.py b/pysonic/scanner.py index 992727f..a0139e8 100644 --- a/pysonic/scanner.py +++ b/pysonic/scanner.py @@ -46,6 +46,7 @@ class PysonicFilesystemScanner(object): root_depth = len(self.split_path(root)) for path, dirs, files in os.walk(root): child = self.split_path(path)[root_depth:] + # dirid = self.create_or_get_dbdir_tree(pid, child) # dumb table for Subsonic self.scan_dir(pid, root, child, dirs, files) logging.warning("Beginning metadata scan for library %s", pid) @@ -53,6 +54,28 @@ class PysonicFilesystemScanner(object): logging.warning("Finished scan for library %s", pid) + def create_or_get_dbdir_tree(self, cursor, pid, path): + """ + Return the ID of the directory specified by `path`. The path will be created as necessary. This bullshit exists + only to serve Subsonic, and can easily be lopped off. + :param pid: root parent the path resides in + :param path: single-file tree as a list of dir names under the root parent + :type path list + """ + assert path + # with closing(self.library.db.db.cursor()) as cursor: + parent_id = 0 # 0 indicates a top level item in the library + for name in path: + parent_id = self.create_or_get_dbdir(cursor, pid, parent_id, name) + return parent_id + + def create_or_get_dbdir(self, cursor, pid, parent_id, name): + for row in cursor.execute("SELECT * FROM dirs WHERE library=? and parent=? and name=?", + (pid, parent_id, name, )): + return row['id'] + cursor.execute("INSERT INTO dirs (library, parent, name) VALUES (?, ?, ?)", (pid, parent_id, name)) + return cursor.lastrowid + def scan_dir(self, pid, root, path, dirs, files): """ Scan a single directory in the library. @@ -79,26 +102,29 @@ class PysonicFilesystemScanner(object): with closing(self.library.db.db.cursor()) as cursor: # Create artist entry - cursor.execute("SELECT * FROM artists WHERE dir = ?", (artist, )) + artist_dirid = self.create_or_get_dbdir_tree(cursor, pid, [path[0]]) + cursor.execute("SELECT * FROM artists WHERE dir = ?", (artist_dirid, )) row = cursor.fetchone() + artist_id = None if row: artist_id = row['id'] else: cursor.execute("INSERT INTO artists (libraryid, dir, name) VALUES (?, ?, ?)", - (pid, artist, artist)) + (pid, artist_dirid, artist)) artist_id = cursor.lastrowid # Create album entry album_id = None + album_dirid = self.create_or_get_dbdir_tree(cursor, pid, path) libpath = os.path.join(*path) if album: - cursor.execute("SELECT * FROM albums WHERE artistid = ? AND dir = ?", (artist_id, libpath, )) + cursor.execute("SELECT * FROM albums WHERE artistid = ? AND dir = ?", (artist_id, album_dirid, )) row = cursor.fetchone() if row: album_id = row['id'] else: cursor.execute("INSERT INTO albums (artistid, dir, name) VALUES (?, ?, ?)", - (artist_id, libpath, path[-1])) + (artist_id, album_dirid, path[-1])) album_id = cursor.lastrowid new_files = False