Horrible hack for subsonic support

This commit is contained in:
dave 2018-04-03 23:33:43 -07:00
parent 3718d3b90c
commit 3aedfcf139
5 changed files with 190 additions and 46 deletions

View File

@ -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',

View File

@ -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()]))

View File

@ -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

View File

@ -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)

View File

@ -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