Browse Source

Refactor and misc bugfix

v2
Dave Pedu 4 years ago
parent
commit
33e501928e
  1. 47
      pysonic/api.py
  2. 121
      pysonic/scanner.py

47
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

121
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']

Loading…
Cancel
Save