Horrible hack for subsonic support
This commit is contained in:
parent
3718d3b90c
commit
3aedfcf139
|
@ -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',
|
||||
|
|
|
@ -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()]))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue