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()) index = response.add_child("index", _parent="indexes", name=letter.upper())
for artist in artists: for artist in artists:
if artist["name"][0].lower() in letter: 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 return response
@cherrypy.expose @cherrypy.expose
@ -245,33 +245,54 @@ class PysonicApi(object):
""" """
List an artist dir List an artist dir
""" """
artist_id = int(id) dir_id = int(id)
cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8' cherrypy.response.headers['Content-Type'] = 'text/xml; charset=utf-8'
response = ApiResponse() response = ApiResponse()
response.add_child("directory") response.add_child("directory")
artist = self.library.get_artists(id=artist_id)[0] dirtype, dirinfo, entity = self.library.db.get_musicdir(dirid=dir_id)
children = self.library.get_albums(artist=artist_id)
response.set_attrs(_path="directory", name=artist['name'], id=artist['id'],
parent=artist['libraryid'], playCount=10)
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 # omit not dirs and media in browser
# if not item["isdir"] and item["type"] not in MUSIC_TYPES: # if not item["isdir"] and item["type"] not in MUSIC_TYPES:
# continue # continue
# item_meta = item['metadata'] # 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", 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", size="4096",
type="music") type="music",
**moreargs)
return response return response
@ -284,11 +305,7 @@ class PysonicApi(object):
:param directory: :param directory:
:param dir_meta: :param dir_meta:
""" """
print("\n\n\n") raise Exception("stop using this")
print(item)
print(item_meta)
print(directory)
print(dir_meta)
child = dict(id=item["id"], child = dict(id=item["id"],
parent=item["id"], parent=item["id"],
isDir="true" if "file" not in item else "false", isDir="true" if "file" not in item else "false",
@ -328,15 +345,19 @@ class PysonicApi(object):
def stream_view(self, id, maxBitRate="256", **kwargs): def stream_view(self, id, maxBitRate="256", **kwargs):
maxBitRate = int(maxBitRate) maxBitRate = int(maxBitRate)
assert maxBitRate >= 32 and maxBitRate <= 320 assert maxBitRate >= 32 and maxBitRate <= 320
fpath = self.library.get_filepath(id) song = self.library.get_song(id)
meta = self.library.get_file_metadata(id) fpath = "library/" + song["file"]
to_bitrate = min(maxBitRate, self.options.max_bitrate, meta.get("media_kbitrate", 320)) # 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' cherrypy.response.headers['Content-Type'] = 'audio/mpeg'
if "media_length" in meta: #if "media_length" in meta:
cherrypy.response.headers['X-Content-Duration'] = str(int(meta['media_length'])) # cherrypy.response.headers['X-Content-Duration'] = str(int(meta['media_length']))
cherrypy.response.headers['X-Content-Kbitrate'] = str(to_bitrate) cherrypy.response.headers['X-Content-Kbitrate'] = str(to_bitrate)
if (self.options.skip_transcode or meta.get("media_kbitrate", -1) == to_bitrate) \ if (self.options.skip_transcode or song.get("bitrate", -1024) / 1024 == to_bitrate) \
and meta["type"] == "audio/mpeg": and format["type"] == "audio/mpeg":
def content(): def content():
with open(fpath, "rb") as f: with open(fpath, "rb") as f:
while True: while True:
@ -346,10 +367,10 @@ class PysonicApi(object):
yield data yield data
return content() return content()
else: else:
transcode_meta = "transcoded_{}_size".format(to_bitrate) # transcode_meta = "transcoded_{}_size".format(to_bitrate)
if transcode_meta in meta: # if transcode_meta in meta:
cherrypy.response.headers['Content-Length'] = str(int(meta[transcode_meta])) # cherrypy.response.headers['Content-Length'] = str(int(meta[transcode_meta]))
print(fpath)
transcode_args = ["ffmpeg", "-i", fpath, "-map", "0:0", "-b:a", transcode_args = ["ffmpeg", "-i", fpath, "-map", "0:0", "-b:a",
"{}k".format(to_bitrate), "{}k".format(to_bitrate),
"-v", "0", "-f", "mp3", "-"] "-v", "0", "-f", "mp3", "-"]
@ -359,13 +380,13 @@ class PysonicApi(object):
def content(proc): def content(proc):
length = 0 length = 0
completed = False # completed = False
start = time() start = time()
try: try:
while True: while True:
data = proc.stdout.read(16 * 1024) data = proc.stdout.read(16 * 1024)
if not data: if not data:
completed = True # completed = True
break break
yield data yield data
length += len(data) length += len(data)
@ -373,8 +394,8 @@ class PysonicApi(object):
proc.poll() proc.poll()
if proc.returncode is None or proc.returncode == 0: if proc.returncode is None or proc.returncode == 0:
logging.warning("transcoded {} in {}s".format(id, int(time() - start))) logging.warning("transcoded {} in {}s".format(id, int(time() - start)))
if completed: # if completed:
self.library.report_transcode(id, to_bitrate, length) # self.library.report_transcode(id, to_bitrate, length)
else: else:
logging.error("transcode of {} exited with code {} after {}s".format(id, proc.returncode, logging.error("transcode of {} exited with code {} after {}s".format(id, proc.returncode,
int(time() - start))) int(time() - start)))
@ -394,7 +415,8 @@ class PysonicApi(object):
@cherrypy.expose @cherrypy.expose
def getCoverArt_view(self, id, **kwargs): def getCoverArt_view(self, id, **kwargs):
fpath = self.library.get_filepath(id) cover = self.library.get_cover(id)
fpath = "library/" + cover["path"]
type2ct = { type2ct = {
'jpg': 'image/jpeg', 'jpg': 'image/jpeg',
'png': 'image/png', 'png': 'image/png',

View File

@ -1,7 +1,7 @@
import os import os
import logging import logging
import cherrypy import cherrypy
from sqlite3 import IntegrityError from sqlite3 import DatabaseError
from pysonic.api import PysonicApi from pysonic.api import PysonicApi
from pysonic.library import PysonicLibrary from pysonic.library import PysonicLibrary
from pysonic.database import PysonicDatabase, DuplicateRootException from pysonic.database import PysonicDatabase, DuplicateRootException
@ -47,7 +47,7 @@ def main():
for username, password in args.user: for username, password in args.user:
try: try:
db.add_user(username, password) db.add_user(username, password)
except IntegrityError: except DatabaseError:
db.update_user(username, password) db.update_user(username, password)
# logging.warning("Libraries: {}".format([i["name"] for i in library.get_libraries()])) # 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, 'id' INTEGER PRIMARY KEY AUTOINCREMENT,
'name' TEXT, 'name' TEXT,
'path' TEXT UNIQUE);""", 'path' TEXT UNIQUE);""",
"""CREATE TABLE 'dirs' (
'id' INTEGER PRIMARY KEY AUTOINCREMENT,
'library' INTEGER,
'parent' INTEGER,
'name' TEXT,
UNIQUE(parent, name)
)""",
"""CREATE TABLE 'artists' ( """CREATE TABLE 'artists' (
'id' INTEGER PRIMARY KEY AUTOINCREMENT, 'id' INTEGER PRIMARY KEY AUTOINCREMENT,
'libraryid' INTEGER, 'libraryid' INTEGER,
'dir' TEXT UNIQUE, 'dir' INTEGER UNIQUE,
'name' TEXT)""", 'name' TEXT)""",
"""CREATE TABLE 'albums' ( """CREATE TABLE 'albums' (
'id' INTEGER PRIMARY KEY AUTOINCREMENT, 'id' INTEGER PRIMARY KEY AUTOINCREMENT,
'artistid' INTEGER, 'artistid' INTEGER,
'coverid' INTEGER, 'coverid' INTEGER,
'dir' TEXT, 'dir' INTEGER,
'name' TEXT, 'name' TEXT,
UNIQUE (artistid, dir));""", UNIQUE (artistid, dir));""",
"""CREATE TABLE 'songs' ( """CREATE TABLE 'songs' (
@ -141,16 +148,24 @@ class PysonicDatabase(object):
return libs return libs
@readcursor @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] assert order in ["asc", "desc", None]
artists = [] artists = []
q = "SELECT * FROM artists" q = "SELECT * FROM artists"
params = [] params = []
conditions = []
if id: if id:
q += " WHERE id = ?" conditions.append("id = ?")
params.append(id) params.append(id)
if dirid:
conditions.append("dir = ?")
params.append(dirid)
if conditions:
q += " WHERE " + " AND ".join(conditions)
if sortby: if sortby:
q += " ORDER BY {} {}".format(sortby, order.upper() if order else "ASC") q += " ORDER BY {} {}".format(sortby, order.upper() if order else "ASC")
print(q)
print(params)
cursor.execute(q, params) cursor.execute(q, params)
for row in cursor: for row in cursor:
artists.append(row) artists.append(row)
@ -181,10 +196,86 @@ class PysonicDatabase(object):
albums.append(row) albums.append(row)
return albums 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_libraries = self.db.get_libraries
self.get_artists = self.db.get_artists self.get_artists = self.db.get_artists
self.get_albums = self.db.get_albums 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) self.scanner = PysonicFilesystemScanner(self)
logging.info("library ready") logging.info("library ready")
@ -74,6 +76,9 @@ class PysonicLibrary(object):
"largeImageUrl": "", "largeImageUrl": "",
"similarArtists": []} "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)) root_depth = len(self.split_path(root))
for path, dirs, files in os.walk(root): for path, dirs, files in os.walk(root):
child = self.split_path(path)[root_depth:] 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) self.scan_dir(pid, root, child, dirs, files)
logging.warning("Beginning metadata scan for library %s", pid) logging.warning("Beginning metadata scan for library %s", pid)
@ -53,6 +54,28 @@ class PysonicFilesystemScanner(object):
logging.warning("Finished scan for library %s", pid) 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): def scan_dir(self, pid, root, path, dirs, files):
""" """
Scan a single directory in the library. Scan a single directory in the library.
@ -79,26 +102,29 @@ class PysonicFilesystemScanner(object):
with closing(self.library.db.db.cursor()) as cursor: with closing(self.library.db.db.cursor()) as cursor:
# Create artist entry # 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() row = cursor.fetchone()
artist_id = None
if row: if row:
artist_id = row['id'] artist_id = row['id']
else: else:
cursor.execute("INSERT INTO artists (libraryid, dir, name) VALUES (?, ?, ?)", cursor.execute("INSERT INTO artists (libraryid, dir, name) VALUES (?, ?, ?)",
(pid, artist, artist)) (pid, artist_dirid, artist))
artist_id = cursor.lastrowid artist_id = cursor.lastrowid
# Create album entry # Create album entry
album_id = None album_id = None
album_dirid = self.create_or_get_dbdir_tree(cursor, pid, path)
libpath = os.path.join(*path) libpath = os.path.join(*path)
if album: 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() row = cursor.fetchone()
if row: if row:
album_id = row['id'] album_id = row['id']
else: else:
cursor.execute("INSERT INTO albums (artistid, dir, name) VALUES (?, ?, ?)", cursor.execute("INSERT INTO albums (artistid, dir, name) VALUES (?, ?, ?)",
(artist_id, libpath, path[-1])) (artist_id, album_dirid, path[-1]))
album_id = cursor.lastrowid album_id = cursor.lastrowid
new_files = False new_files = False