Browse Source

refactor out library class

master
dave 2 years ago
parent
commit
c910de0eb0
  1. 84
      pysonic/api.py
  2. 8
      pysonic/daemon.py
  3. 42
      pysonic/database.py
  4. 98
      pysonic/library.py
  5. 14
      pysonic/scanner.py

84
pysonic/api.py

@ -2,7 +2,7 @@ import logging
import subprocess
from time import time
from threading import Thread
from pysonic.library import LETTER_GROUPS
from pysonic.database import LETTER_GROUPS
from pysonic.types import MUSIC_TYPES
from pysonic.apilib import formatresponse, ApiResponse
import cherrypy
@ -11,16 +11,15 @@ logging = logging.getLogger("api")
class PysonicSubsonicApi(object):
def __init__(self, db, library, options):
def __init__(self, db, options):
self.db = db
self.library = library
self.options = options
@cherrypy.expose
@formatresponse
def index(self):
response = ApiResponse()
response.add_child("totals", **self.library.db.get_stats())
response.add_child("totals", **self.db.get_stats())
return response
@cherrypy.expose
@ -46,7 +45,7 @@ class PysonicSubsonicApi(object):
def getMusicFolders_view(self, **kwargs):
response = ApiResponse()
response.add_child("musicFolders")
for folder in self.library.get_libraries():
for folder in self.db.get_libraries():
response.add_child("musicFolder", _parent="musicFolders", id=folder["id"], name=folder["name"])
return response
@ -58,7 +57,7 @@ class PysonicSubsonicApi(object):
# TODO real lastmodified date
# TODO deal with ignoredArticles
response.add_child("indexes", lastModified="1502310831000", ignoredArticles="The El La Los Las Le Les")
artists = self.library.get_artists(sortby="name", order="asc")
artists = self.db.get_artists(sortby="name", order="asc")
for letter in LETTER_GROUPS:
index = response.add_child("index", _parent="indexes", name=letter.upper())
for artist in artists:
@ -83,7 +82,7 @@ class PysonicSubsonicApi(object):
qargs.update(limit=(offset, size))
albums = self.library.get_albums(**qargs)
albums = self.db.get_albums(**qargs)
response = ApiResponse()
@ -111,7 +110,7 @@ class PysonicSubsonicApi(object):
List either and artist or album dir
"""
dir_id = int(id)
dirtype, dirinfo, entity = self.library.db.get_subsonic_musicdir(dirid=dir_id)
dirtype, dirinfo, entity = self.db.get_subsonic_musicdir(dirid=dir_id)
response = ApiResponse()
@ -171,7 +170,7 @@ class PysonicSubsonicApi(object):
def stream_view(self, id, maxBitRate="256", **kwargs):
maxBitRate = int(maxBitRate)
assert maxBitRate >= 32 and maxBitRate <= 320
song = self.library.get_song(int(id))
song = self.db.get_songs(id=int(id))[0]
fpath = song["_fullpath"]
media_bitrate = song.get("bitrate") / 1024 if song.get("bitrate") else 320
to_bitrate = min(maxBitRate,
@ -219,7 +218,7 @@ class PysonicSubsonicApi(object):
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)
# self.db.report_transcode(id, to_bitrate, length)
else:
logging.error("transcode of {} exited with code {} after {}s".format(id, proc.returncode,
int(time() - start)))
@ -248,7 +247,7 @@ class PysonicSubsonicApi(object):
"""
if id.startswith("pl-"): # get art from first track in playlist
playlist_id = int(id[len("pl-"):])
_, songs = self.library.get_playlist(playlist_id)
songs = self.db.get_playlist_songs(playlist_id)
for song in songs:
if song["albumcoverid"]:
id = song["albumcoverid"]
@ -262,8 +261,7 @@ class PysonicSubsonicApi(object):
else:
id = int(id)
cover = self.library.get_cover(id)
fpath = cover["_fullpath"]
fpath = self.db.get_cover_path(id)
type2ct = {
'jpg': 'image/jpeg',
'png': 'image/png',
@ -280,14 +278,14 @@ class PysonicSubsonicApi(object):
break
total += len(data)
yield data
logging.info("\nSent {} bytes for {}".format(total, fpath))
logging.info("sent {} bytes for {}".format(total, fpath))
return content()
getCoverArt_view._cp_config = {'response.stream': True}
@cherrypy.expose
@formatresponse
def getArtistInfo_view(self, id, includeNotPresent="true", **kwargs):
info = self.library.get_artist_info(id)
info = self.db.get_artist_info(id)
response = ApiResponse()
response.add_child("artistInfo")
response.set_attrs("artistInfo", **info)
@ -296,7 +294,7 @@ class PysonicSubsonicApi(object):
@cherrypy.expose
@formatresponse
def getUser_view(self, username, **kwargs):
user = {} if self.options.disable_auth else self.library.db.get_user(cherrypy.request.login)
user = {} if self.options.disable_auth else self.db.get_user(cherrypy.request.login)
response = ApiResponse()
response.add_child("user",
username=user["username"],
@ -321,19 +319,19 @@ class PysonicSubsonicApi(object):
@cherrypy.expose
@formatresponse
def star_view(self, id, **kwargs):
self.library.set_starred(cherrypy.request.login, int(id), starred=True)
self.db.set_starred(cherrypy.request.login, int(id), starred=True)
return ApiResponse()
@cherrypy.expose
@formatresponse
def unstar_view(self, id, **kwargs):
self.library.set_starred(cherrypy.request.login, int(id), starred=False)
self.db.set_starred(cherrypy.request.login, int(id), starred=False)
return ApiResponse()
@cherrypy.expose
@formatresponse
def getStarred_view(self, **kwargs):
children = self.library.get_starred(cherrypy.request.login)
children = self.db.get_starred(cherrypy.request.login)
response = ApiResponse()
response.add_child("starred")
for item in children:
@ -355,7 +353,7 @@ class PysonicSubsonicApi(object):
"""
response = ApiResponse()
response.add_child("randomSongs")
children = self.library.db.get_songs(limit=size, sortby="random")
children = self.db.get_songs(limit=size, sortby="random")
for song in children:
moreargs = {}
if song["format"]:
@ -390,7 +388,7 @@ class PysonicSubsonicApi(object):
def getGenres_view(self, **kwargs):
response = ApiResponse()
response.add_child("genres")
for row in self.library.db.get_genres():
for row in self.db.get_genres():
response.add_child("genre", _parent="genres", value=row["name"], songCount=420, albumCount=69)
return response
@ -418,7 +416,7 @@ class PysonicSubsonicApi(object):
query = query.replace("*", "") # TODO handle this
artists = 0
for item in self.library.get_artists(name_contains=query):
for item in self.db.get_artists(name_contains=query):
response.add_child("artist", _parent="searchResult2", id=item["id"], name=item["name"])
artists += 1
if artists >= artistCount:
@ -426,7 +424,7 @@ class PysonicSubsonicApi(object):
# TODO make this more efficient
albums = 0
for album in self.library.get_albums(name_contains=query):
for album in self.db.get_albums(name_contains=query):
response.add_child("album", _parent="searchResult2",
id=album["dir"],
parent=album["artistdir"],
@ -445,7 +443,7 @@ class PysonicSubsonicApi(object):
# TODO make this more efficient
songs = 0
for song in self.library.db.get_songs(title_contains=query):
for song in self.db.get_songs(title_contains=query):
response.add_child("song", _parent="searchResult2",
id=song["id"],
parent=song["albumdir"],
@ -485,11 +483,11 @@ class PysonicSubsonicApi(object):
def savePlayQueue_view(self, id, current, position, **kwargs):
print("TODO save playqueue with items {} current {} position {}".format(id, repr(current), repr(position)))
current = int(current)
song = self.library.get_song(current)
self.library.db.update_album_played(song['albumid'], time())
self.library.db.increment_album_plays(song['albumid'])
song = self.db.get_songs(id=current)[0]
self.db.update_album_played(song['albumid'], time())
self.db.increment_album_plays(song['albumid'])
if int(position) == 0:
self.library.db.increment_track_plays(current)
self.db.increment_track_plays(current)
# TODO save playlist with items ['378', '386', '384', '380', '383'] current 383 position 4471
# id entries are strings!
@ -498,19 +496,19 @@ class PysonicSubsonicApi(object):
def createPlaylist_view(self, name, songId, **kwargs):
if type(songId) != list:
songId = [songId]
user = self.library.db.get_user(cherrypy.request.login)
self.library.db.add_playlist(user["id"], name, songId)
user = self.db.get_user(cherrypy.request.login)
self.db.add_playlist(user["id"], name, songId)
return ApiResponse()
#TODO the response should be the new playlist, check the cap
@cherrypy.expose
@formatresponse
def getPlaylists_view(self, **kwargs):
user = self.library.db.get_user(cherrypy.request.login)
user = self.db.get_user(cherrypy.request.login)
response = ApiResponse()
response.add_child("playlists")
for playlist in self.library.db.get_playlists(user["id"]):
for playlist in self.db.get_playlists(user["id"]):
response.add_child("playlist",
_parent="playlists",
id=playlist["id"],
@ -529,9 +527,10 @@ class PysonicSubsonicApi(object):
@cherrypy.expose
@formatresponse
def getPlaylist_view(self, id, **kwargs):
user = self.library.db.get_user(cherrypy.request.login)
plinfo, songs = self.library.get_playlist(int(id))
id = int(id)
user = self.db.get_user(cherrypy.request.login)
plinfo = self.db.get_playlist(id)
songs = self.db.get_playlist_songs(id)
response = ApiResponse()
response.add_child("playlist",
id=plinfo["id"],
@ -569,15 +568,16 @@ class PysonicSubsonicApi(object):
@cherrypy.expose
@formatresponse
def updatePlaylist_view(self, playlistId, songIndexToRemove=None, songIdToAdd=None, **kwargs):
user = self.library.db.get_user(cherrypy.request.login)
plinfo, songs = self.library.get_playlist(int(playlistId))
playlistId = int(playlistId)
user = self.db.get_user(cherrypy.request.login)
plinfo = self.db.get_playlist(playlistId)
assert plinfo["ownerid"] == user["id"]
if songIndexToRemove:
self.library.db.remove_index_from_playlist(playlistId, songIndexToRemove)
self.db.remove_index_from_playlist(playlistId, songIndexToRemove)
elif songIdToAdd:
self.library.db.add_to_playlist(playlistId, songIdToAdd)
self.db.add_to_playlist(playlistId, songIdToAdd)
#TODO there are more modification methods
return ApiResponse()
@ -585,9 +585,9 @@ class PysonicSubsonicApi(object):
@cherrypy.expose
@formatresponse
def deletePlaylist_view(self, id, **kwargs):
user = self.library.db.get_user(cherrypy.request.login)
plinfo, _ = self.library.get_playlist(int(id))
user = self.db.get_user(cherrypy.request.login)
plinfo = self.db.get_playlist(int(id))
assert plinfo["ownerid"] == user["id"]
self.library.delete_playlist(plinfo["id"])
self.db.delete_playlist(plinfo["id"])
return ApiResponse()

8
pysonic/daemon.py

@ -3,7 +3,6 @@ import logging
import cherrypy
from sqlite3 import DatabaseError
from pysonic.api import PysonicSubsonicApi
from pysonic.library import PysonicLibrary
from pysonic.database import PysonicDatabase, DuplicateRootException
@ -35,15 +34,14 @@ def main():
format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s")
db = PysonicDatabase(path=args.database_path)
library = PysonicLibrary(db)
for dirname in args.dirs:
dirname = os.path.abspath(dirname)
assert os.path.exists(dirname), "--dirs must be paths that exist"
try:
library.add_root_dir(dirname)
db.add_root(dirname)
except DuplicateRootException:
pass
library.update()
db.update()
for username, password in args.user:
try:
@ -55,7 +53,7 @@ def main():
# logging.warning("Artists: {}".format([i["name"] for i in library.get_artists()]))
# logging.warning("Albums: {}".format(len(library.get_albums())))
api = PysonicSubsonicApi(db, library, args)
api = PysonicSubsonicApi(db, args)
api_config = {}
if args.disable_auth:
logging.warning("starting up with auth disabled")

42
pysonic/database.py

@ -1,3 +1,4 @@
import os
import sqlite3
import logging
from hashlib import sha512
@ -5,7 +6,17 @@ from time import time
from contextlib import closing
from collections import Iterable
from pysonic.scanner import PysonicFilesystemScanner
logging = logging.getLogger("database")
LETTER_GROUPS = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
"u", "v", "w", "xyz", "0123456789"]
keys_in_table = ["title", "album", "artist", "type", "size"]
@ -49,11 +60,18 @@ class PysonicDatabase(object):
self.db = None
self.open()
self.migrate()
self.scanner = PysonicFilesystemScanner(self)
def open(self):
self.db = sqlite3.connect(self.path, **self.sqlite_opts)
self.db.row_factory = dict_factory
def update(self):
"""
Start the library media scanner ands
"""
self.scanner.init_scan()
def migrate(self):
# Create db
queries = ["""CREATE TABLE 'libraries' (
@ -150,6 +168,16 @@ class PysonicDatabase(object):
# logging.warning("db schema is version {}".format(version))
pass
def get_artist_info(self, item_id):
#TODO
return {"biography": "placeholder biography",
"musicBrainzId": "playerholder",
"lastFmUrl": "https://www.last.fm/music/Placeholder",
"smallImageUrl": "",
"mediumImageUrl": "",
"largeImageUrl": "",
"similarArtists": []}
@cursor
def get_stats(self, c):
songs = c.execute("SELECT COUNT(*) as cnt FROM songs").fetchone()['cnt']
@ -167,7 +195,7 @@ class PysonicDatabase(object):
:return: int
:raises: sqlite3.IntegrityError
"""
assert path.startswith("/")
path = os.path.abspath(os.path.normpath(path))
try:
c.execute("INSERT INTO libraries ('name', 'path') VALUES (?, ?)", (name, path, ))
c.execute("COMMIT")
@ -348,11 +376,16 @@ class PysonicDatabase(object):
return genres
@cursor
def get_cover(self, c, coverid):
def get_cover(self, c, cover_id):
cover = None
for cover in c.execute("SELECT * FROM covers WHERE id = ?", (coverid, )):
for cover in c.execute("SELECT * FROM covers WHERE id = ?", (cover_id, )):
return cover
def get_cover_path(self, cover_id):
cover = self.get_cover(cover_id)
library = self.get_libraries(cover["library"])[0]
return os.path.join(library["path"], cover["path"])
@cursor
def get_subsonic_musicdir(self, c, dirid):
"""
@ -469,12 +502,13 @@ class PysonicDatabase(object):
@cursor
def empty_playlist(self, c, playlist_id):
#TODO combine with ??
#TODO combine with delete_playlist
c.execute("DELETE FROM playlist_entries WHERE playlistid=?", (playlist_id, ))
c.execute("COMMIT")
@cursor
def delete_playlist(self, c, playlist_id):
c.execute("DELETE FROM playlist_entries WHERE playlistid=?", (playlist_id, ))
c.execute("DELETE FROM playlists WHERE id=?", (playlist_id, ))
c.execute("COMMIT")

98
pysonic/library.py

@ -1,98 +0,0 @@
import os
import logging
from pysonic.scanner import PysonicFilesystemScanner
from pysonic.types import MUSIC_TYPES
LETTER_GROUPS = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
"u", "v", "w", "xyz", "0123456789"]
logging = logging.getLogger("library")
def memoize(function):
memo = {}
def wrapper(*args):
if args in memo:
return memo[args]
else:
rv = function(*args)
memo[args] = rv
return rv
return wrapper
class NoDataException(Exception):
pass
class PysonicLibrary(object):
def __init__(self, database):
self.db = database
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")
def update(self):
"""
Start the library media scanner ands
"""
self.scanner.init_scan()
def add_root_dir(self, path):
"""
The music library consists of a number of root dirs. This adds a new root
"""
path = os.path.abspath(os.path.normpath(path))
self.db.add_root(path)
# def get_artists(self, *args, **kwargs):
# artists = self.db.get_artists(*args, **kwargs)
# for item in artists:
# item["parent"] = item["libraryid"]
# return artists
# def get_albums(self, *args, **kwargs):
# albums = self.db.get_albums(*args, **kwargs)
# for item in albums:
# item["parent"] = item["artistid"]
# return albums
def get_artist_info(self, item_id):
#TODO
return {"biography": "placeholder biography",
"musicBrainzId": "playerholder",
"lastFmUrl": "https://www.last.fm/music/Placeholder",
"smallImageUrl": "",
"mediumImageUrl": "",
"largeImageUrl": "",
"similarArtists": []}
def get_cover(self, cover_id):
cover = self.db.get_cover(cover_id)
library = self.db.get_libraries(cover["library"])[0]
cover['_fullpath'] = os.path.join(library["path"], cover["path"])
return cover
def get_song(self, song_id):
song = self.db.get_songs(id=song_id)[0]
library = self.db.get_libraries(song["library"])[0]
song['_fullpath'] = os.path.join(library["path"], song["file"])
return song
def get_playlist(self, playlist_id):
playlist_info = self.db.get_playlist(playlist_id)
songs = self.db.get_playlist_songs(playlist_id)
return (playlist_info, songs)
def delete_playlist(self, playlist_id):
self.db.empty_playlist(playlist_id)
self.db.delete_playlist(playlist_id)

14
pysonic/scanner.py

@ -18,8 +18,8 @@ RE_NUMBERS = re.compile(r'^([0-9]+)')
class PysonicFilesystemScanner(object):
def __init__(self, library):
self.library = library
def __init__(self, db):
self.db = db
def init_scan(self):
self.scanner = Thread(target=self.rescan, daemon=True)
@ -31,7 +31,7 @@ class PysonicFilesystemScanner(object):
"""
start = time()
logging.warning("Beginning library rescan")
for parent in self.library.db.get_libraries():
for parent in self.db.get_libraries():
logging.info("Scanning {}".format(parent["path"]))
self.scan_root(parent["id"], parent["path"])
logging.warning("Rescan complete in %ss", round(time() - start, 3))
@ -63,7 +63,7 @@ class PysonicFilesystemScanner(object):
:type path list
"""
assert path
# with closing(self.library.db.db.cursor()) as cursor:
# with closing(self.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)
@ -109,7 +109,7 @@ class PysonicFilesystemScanner(object):
if len(path) > 1:
album = path[-1]
with closing(self.library.db.db.cursor()) as cursor:
with closing(self.db.db.cursor()) as cursor:
artist_id, artist_dirid = self.create_or_get_artist(cursor, pid, path[0])
album_id = None
@ -226,8 +226,8 @@ class PysonicFilesystemScanner(object):
q += "ORDER BY albumid"
#TODO scraping ID3 etc from the media files can be parallelized
with closing(self.library.db.db.cursor()) as reader, \
closing(self.library.db.db.cursor()) as writer:
with closing(self.db.db.cursor()) as reader, \
closing(self.db.db.cursor()) as writer:
processed = 0 # commit batching counter
for row in reader.execute(q):
# Find meta, bail if the file was unreadable

Loading…
Cancel
Save