From efcef1f5df162b7c54a89852b9aa9679aa4170bd Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 16 Aug 2017 00:05:26 -0700 Subject: [PATCH] Partial shuffle support --- pysonic/api.py | 35 +++++++++++++++-------- pysonic/database.py | 69 +++++++++++++++++++++++++++++++++++++-------- pysonic/library.py | 17 ++++++----- pysonic/scanner.py | 9 +++--- 4 files changed, 94 insertions(+), 36 deletions(-) diff --git a/pysonic/api.py b/pysonic/api.py index e848e63..0f38dfa 100644 --- a/pysonic/api.py +++ b/pysonic/api.py @@ -118,12 +118,12 @@ class PysonicApi(object): doc.append(albumlist) for album in albumset: - album_meta = self.library.db.decode_metadata(album['metadata']) + album_meta = album['metadata'] tag = doc.new_tag("album", id=album["id"], parent=album["parent"], isDir="true" if album['isdir'] else "false", - title=album_meta.get("id3_title", album["name"]), + title=album_meta.get("id3_title", album["name"]), #TODO these cant be blank or dsub gets mad album=album_meta.get("id3_album", album["album"]), artist=album_meta.get("id3_artist", album["artist"]), # X year="2014" @@ -151,7 +151,7 @@ class PysonicApi(object): dirtag = doc.new_tag("directory") directory = self.library.get_dir(dir_id) - dir_meta = self.db.decode_metadata(directory["metadata"]) + dir_meta = directory["metadata"] children = self.library.get_dir_children(dir_id) dirtag.attrs.update(name=directory['name'], id=directory['id'], parent=directory['parent'], playCount=10) @@ -161,7 +161,7 @@ class PysonicApi(object): # omit not dirs and media in browser if not item["isdir"] and item["type"] not in MUSIC_TYPES: continue - item_meta = self.db.decode_metadata(item['metadata']) + item_meta = item['metadata'] dirtag.append(self.render_node(doc, item, item_meta, directory, dir_meta)) yield doc.prettify() @@ -327,23 +327,34 @@ class PysonicApi(object): @cherrypy.expose def getStarred_view(self, **kwargs): - children = self.library.get_starred(cherrypy.request.login) - cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' doc, root = self.response() - tag = doc.new_tag("starred") - - #directory = self.library.get_dir(dir_id) - #dir_meta = self.db.decode_metadata(directory["metadata"]) - #children = self.library.get_dir_children(dir_id) root.append(tag) + children = self.library.get_starred(cherrypy.request.login) for item in children: # omit not dirs and media in browser if not item["isdir"] and item["type"] not in MUSIC_TYPES: continue - item_meta = self.db.decode_metadata(item['metadata']) + item_meta = item['metadata'] + itemtype = "song" if item["type"] in MUSIC_TYPES else "album" + tag.append(self.render_node(doc, item, item_meta, {}, {}, tagname=itemtype)) + yield doc.prettify() + + @cherrypy.expose + def getRandomSongs_view(self, size=50, genre=None, fromYear=0, toYear=0, **kwargs): + cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' + doc, root = self.response() + tag = doc.new_tag("randomSongs") + root.append(tag) + + children = self.library.get_songs(size, shuffle=True) + for item in children: + # omit not dirs and media in browser + if not item["isdir"] and item["type"] not in MUSIC_TYPES: + continue + item_meta = item['metadata'] itemtype = "song" if item["type"] in MUSIC_TYPES else "album" tag.append(self.render_node(doc, item, item_meta, {}, {}, tagname=itemtype)) yield doc.prettify() diff --git a/pysonic/database.py b/pysonic/database.py index 036c401..508b7b0 100644 --- a/pysonic/database.py +++ b/pysonic/database.py @@ -87,13 +87,56 @@ class PysonicDatabase(object): # Virtual file tree def getnode(self, node_id): - with closing(self.db.cursor()) as cursor: - return cursor.execute("SELECT * FROM nodes WHERE id=?;", (node_id, )).fetchone() + return self.getnodes(node_id=node_id)[0] + + def _populate_meta(self, node): + node['metadata'] = self.decode_metadata(node['metadata']) + return node + + def getnodes(self, *parent_ids, node_id=None, types=None, limit=None, order=None): + """ + Find nodes that match the passed paramters. + :param parent_ids: one or more parents to find children of + :type parent_ids: int + :param node_id: single node id to return + :type node_id: int + :param types: filter by type column + :type types: list + :param limit: number of records to limit to + :param order: one of ("rand") to select ordering mode + """ + query = "SELECT * FROM nodes WHERE " + qargs = [] + + def add_filter(name, values): + nonlocal query + nonlocal qargs + query += "{} in (".format(name) + for value in (values if type(values) in [list, tuple] else [values]): + query += "?, " + qargs += [value] + query = query.rstrip(", ") + query += ") AND" + + if node_id: + add_filter("id", node_id) + if parent_ids: + add_filter("parent", parent_ids) + if types: + add_filter("type", types) + + query = query.rstrip(" AND") + + if order: + query += "ORDER BY " + if order == "rand": + query += "RANDOM()" + + if limit: # TODO 2-item tuple limit + query += " limit {}".format(limit) - def getnodes(self, *parent_ids): with closing(self.db.cursor()) as cursor: - return list(chain(*[cursor.execute("SELECT * FROM nodes WHERE parent=?;", (parent_id, )).fetchall() - for parent_id in parent_ids])) + return list(map(self._populate_meta, cursor.execute(query, qargs).fetchall())) def addnode(self, parent_id, fspath, name): fullpath = os.path.join(fspath, name) @@ -132,9 +175,9 @@ class PysonicDatabase(object): def get_metadata(self, node_id): keys_in_table = ["title", "album", "artist", "type"] node = self.getnode(node_id) - metadata = self.decode_metadata(node["metadata"]) - metadata.update({item: node[item] for item in ["title", "album", "artist", "type"]}) - return metadata + meta = node["metadata"] + meta.update({item: node[item] for item in keys_in_table}) + return meta def decode_metadata(self, metadata): if metadata: @@ -169,7 +212,10 @@ class PysonicDatabase(object): query = "INSERT INTO stars (userid, nodeid) VALUES (?, ?);" else: query = "DELETE FROM stars WHERE userid=? and nodeid=?;" - cursor.execute(query, (user_id, node_id)) + try: + cursor.execute(query, (user_id, node_id)) + except sqlite3.IntegrityError: + pass def get_starred_items(self, for_user_id=None): with closing(self.db.cursor()) as cursor: @@ -178,6 +224,5 @@ class PysonicDatabase(object): if for_user_id: q += """ AND userid=?""" qargs += [int(for_user_id)] - print(q) - print(qargs) - return cursor.execute(q, qargs).fetchall() + return list(map(self._populate_meta, + cursor.execute(q, qargs).fetchall())) diff --git a/pysonic/library.py b/pysonic/library.py index f6fe2de..28045dd 100644 --- a/pysonic/library.py +++ b/pysonic/library.py @@ -1,7 +1,7 @@ import os -import json import logging from pysonic.scanner import PysonicFilesystemScanner +from pysonic.types import MUSIC_TYPES LETTER_GROUPS = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", @@ -43,21 +43,21 @@ class PysonicLibrary(object): def add_dir(self, dir_path): dir_path = os.path.abspath(os.path.normpath(dir_path)) - libraries = [self.db.decode_metadata(i['metadata'])['fspath'] for i in self.db.getnodes(-1)] + libraries = [i['metadata']['fspath'] for i in self.db.getnodes(-1)] if dir_path in libraries: raise DuplicateRootException("Dir already in library") else: new_root = self.db._addnode(-1, 'New Library', is_dir=True) self.db.update_metadata(new_root['id'], fspath=dir_path) - @memoize + #@memoize def get_libraries(self): """ Libraries are top-level nodes """ return self.db.getnodes(-1) - @memoize + #@memoize def get_artists(self): # Assume artists are second level dirs return self.db.getnodes(*[item["id"] for item in self.get_libraries()]) @@ -68,18 +68,18 @@ class PysonicLibrary(object): def get_dir_children(self, dirid): return self.db.getnodes(dirid) - @memoize + #@memoize def get_albums(self): return self.db.getnodes(*[item["id"] for item in self.get_artists()]) - @memoize + #@memoize def get_filepath(self, nodeid): parents = [self.db.getnode(nodeid)] while parents[-1]['parent'] != -1: parents.append(self.db.getnode(parents[-1]['parent'])) root = parents.pop() parents.reverse() - return os.path.join(json.loads(root['metadata'])['fspath'], *[i['name'] for i in parents]) + return os.path.join(root['metadata']['fspath'], *[i['name'] for i in parents]) def get_file_metadata(self, nodeid): return self.db.get_metadata(nodeid) @@ -105,3 +105,6 @@ class PysonicLibrary(object): def get_starred(self, username): return self.db.get_starred_items(self.db.get_user(username)["id"]) + + def get_songs(self, limit=50, shuffle=True): + return self.db.getnodes(types=MUSIC_TYPES, limit=limit, order="rand") diff --git a/pysonic/scanner.py b/pysonic/scanner.py index 724b3f2..c18c7b6 100644 --- a/pysonic/scanner.py +++ b/pysonic/scanner.py @@ -1,6 +1,5 @@ import os import re -import json import logging import mimetypes from time import time @@ -28,7 +27,7 @@ class PysonicFilesystemScanner(object): logging.warning("Beginning library rescan") start = time() for parent in self.library.get_libraries(): - meta = json.loads(parent["metadata"]) + meta = parent["metadata"] logging.info("Scanning {}".format(meta["fspath"])) def recurse_dir(path, parent): @@ -70,7 +69,7 @@ class PysonicFilesystemScanner(object): artist = artist_dir["name"] for album_dir in self.library.db.getnodes(artist_dir["id"]): album = album_dir["name"] - album_meta = self.library.db.get_metadata(album_dir["id"]) + album_meta = album_dir["metadata"] for track_file in self.library.db.getnodes(album_dir["id"]): title = track_file["name"] if not track_file["title"]: @@ -106,9 +105,9 @@ class PysonicFilesystemScanner(object): artist = artist_dir["name"] for album_dir in self.library.db.getnodes(artist_dir["id"]): album = album_dir["name"] - album_meta = self.library.db.get_metadata(album_dir["id"]) + album_meta = album_dir["metadata"] for track_file in self.library.db.getnodes(album_dir["id"]): - track_meta = self.library.db.decode_metadata(track_file['metadata']) + track_meta = track_file['metadata'] title = track_file["name"] fpath = self.library.get_filepath(track_file["id"]) if track_meta.get('id3_done', False) or track_file.get("type", "x") not in MUSIC_TYPES: