From 33e501928e4b344b1f740205a220683b3f5f6818 Mon Sep 17 00:00:00 2001 From: Dave Pedu Date: Wed, 4 Apr 2018 16:44:21 -0700 Subject: [PATCH] Refactor and misc bugfix --- pysonic/api.py | 47 +++++++++++++----- pysonic/scanner.py | 121 +++++++++++++++++++++++++++++---------------- 2 files changed, 113 insertions(+), 55 deletions(-) diff --git a/pysonic/api.py b/pysonic/api.py index 937b09d..5348f27 100644 --- a/pysonic/api.py +++ b/pysonic/api.py @@ -62,6 +62,7 @@ class ApiResponse(object): self.data = defaultdict(lambda: list()) def add_child(self, _type, _parent="", _real_parent=None, **kwargs): + kwargs = {k: v for k, v in kwargs.items() if v or type(v) is int} # filter out empty keys (0 is ok) parent = _real_parent if _real_parent else self.get_child(_parent) m = defaultdict(lambda: list()) m.update(dict(kwargs)) @@ -349,13 +350,16 @@ class PysonicApi(object): assert maxBitRate >= 32 and maxBitRate <= 320 song = self.library.get_song(id) fpath = song["_fullpath"] - to_bitrate = min(maxBitRate, self.options.max_bitrate, song.get("bitrate", 320 * 1024) / 1024) + media_bitrate = song.get("bitrate") / 1024 if song.get("bitrate") else 320 + to_bitrate = min(maxBitRate, + self.options.max_bitrate, + media_bitrate) cherrypy.response.headers['Content-Type'] = 'audio/mpeg' #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 song.get("bitrate", -1024) / 1024 == to_bitrate) \ - and format["type"] == "audio/mpeg": + if (self.options.skip_transcode or (song.get("bitrate") and media_bitrate == to_bitrate)) \ + and song["format"] == "audio/mpeg": def content(): with open(fpath, "rb") as f: while True: @@ -507,15 +511,34 @@ class PysonicApi(object): """ response = ApiResponse() response.add_child("randomSongs") - 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" - response.add_child(itemtype, _parent="randomSongs", - **self.render_node(item, item_meta, {}, self.db.getnode(item["parent"])["metadata"])) + children = self.library.db.get_songs(limit=size, sortby="random") + for song in children: + moreargs = {} + if song["format"]: + moreargs.update(contentType=song["format"]) + if song["albumcoverid"]: + moreargs.update(coverArt=song["albumcoverid"]) + if song["length"]: + moreargs.update(duration=song["length"]) + if song["track"]: + moreargs.update(track=song["track"]) + if song["year"]: + moreargs.update(year=song["year"]) + + file_extension = song["file"].split(".")[-1] + + response.add_child("song", + _parent="randomSongs", + title=song["title"], + album=song["albumname"], + artist=song["artistname"], + id=song["id"], + isDir="false", + parent=song["albumid"], + size=song["size"], + suffix=file_extension, + type="music", + **moreargs) return response @cherrypy.expose diff --git a/pysonic/scanner.py b/pysonic/scanner.py index b38a9b4..08ab8df 100644 --- a/pysonic/scanner.py +++ b/pysonic/scanner.py @@ -78,72 +78,49 @@ class PysonicFilesystemScanner(object): def scan_dir(self, pid, root, path, dirs, files): """ - Scan a single directory in the library. + Scan a single directory in the library. Actually, this ignores all dirs that don't contain files. Dirs are + interpreted as follows: + - The library root is ignored + - Empty dirs are ignored + - Dirs containing files are assumed to be an album + - Top level dirs in the library are assumed to be artists + - Any dirs not following the above rules are transparently ignored + - Files placed in an artist dir is an unhandled edge case TODO + - Any files with an image extension in an album dir will be assumed to be the cover regardless of naming + - TODO ignore dotfiles/dirs :param pid: parent id :param root: library root path :param path: scan location path, as a list of subdirs within the root :param dirs: dirs in the current path :param files: files in the current path """ - # If there are no files then just bail - if not files: + # If this is the library root or an empty dir just bail + if not path or not files: return # If it is the library root just bail if len(path) == 0: return - # Guess an artist from the dir - artist = path[0] - # Guess an album from the dir, if possible album = None if len(path) > 1: album = path[-1] with closing(self.library.db.db.cursor()) as cursor: - # Create artist entry - 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_dirid, artist)) - artist_id = cursor.lastrowid + artist_id, artist_dirid = self.create_or_get_artist(cursor, pid, path[0]) - # Create album entry album_id = None - album_dirid = self.create_or_get_dbdir_tree(cursor, pid, path) - libpath = os.path.join(*path) + album_dirid = None if album: - 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, album_dirid, path[-1])) - album_id = cursor.lastrowid + album_id, album_dirid = self.create_or_get_album(cursor, pid, path, artist_id) + + libpath = os.path.join(*path) new_files = False - for file in files: - if not any([file.endswith(".{}".format(i)) for i in MUSIC_EXTENSIONS]): + for fname in files: + if not any([fname.endswith(".{}".format(i)) for i in MUSIC_EXTENSIONS]): continue - fpath = os.path.join(libpath, file) - cursor.execute("SELECT id FROM songs WHERE file=?", (fpath, )) - - if not cursor.fetchall(): - # We leave most fields blank now and return later - cursor.execute("INSERT INTO songs (library, albumid, file, size, title) " - "VALUES (?, ?, ?, ?, ?)", - (pid, - album_id, - fpath, - os.stat(os.path.join(root, fpath)).st_size, - file, )) - new_files = True + new_files = self.add_music_if_new(cursor, pid, root, album_id, libpath, fname) or new_files # Create cover entry TODO we can probably skip this if there were no new audio files? if album_id: @@ -161,6 +138,64 @@ class PysonicFilesystemScanner(object): if new_files: # Commit after each dir IF audio files were found. no audio == dump the artist cursor.execute("COMMIT") + def add_music_if_new(self, cursor, pid, root_dir, album_id, fdir, fname): + fpath = os.path.join(fdir, fname) + cursor.execute("SELECT id FROM songs WHERE file=?", (fpath, )) + if not cursor.fetchall(): + # We leave most fields blank now and return later + # TODO probably not here but track file sizes and mark them for rescan on change + cursor.execute("INSERT INTO songs (library, albumid, file, size, title) " + "VALUES (?, ?, ?, ?, ?)", + (pid, + album_id, + fpath, + os.stat(os.path.join(root_dir, fpath)).st_size, + fname, )) + return True + return False + + def create_or_get_artist(self, cursor, pid, dirname): + """ + Retrieve, creating if necessary, directory information about an artist. Return tuple contains the artist's ID + and the dir id associated with the artist. + :param cursor: sqlite cursor to use + :param pid: root parent id we're working int + :param dirname: name of the artist dir + :return tuple: + """ + artist_dirid = self.create_or_get_dbdir_tree(cursor, pid, [dirname]) + 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_dirid, dirname)) + artist_id = cursor.lastrowid + return artist_id, artist_dirid + + def create_or_get_album(self, cursor, pid, dirnames, artist_id): + """ + Retrieve, creating if necessary, directory information about an album. Return tuple contains the albums's ID + and the dir id associated with the album. + :param cursor: sqlite cursor to use + :param pid: root parent id we're working int + :param dirnames: list of directories from the root to the album dir + :param artist_id: id of the artist the album belongs to + :return tuple: + """ + album_dirid = self.create_or_get_dbdir_tree(cursor, pid, dirnames) + 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, album_dirid, dirnames[-1])) + album_id = cursor.lastrowid + return album_id, album_dirid + def split_path(self, path): """ Given a path like /foo/bar, return ['foo', 'bar']