refactor out library class
This commit is contained in:
parent
bfcb528ddf
commit
c910de0eb0
|
@ -2,7 +2,7 @@ import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
from time import time
|
from time import time
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from pysonic.library import LETTER_GROUPS
|
from pysonic.database import LETTER_GROUPS
|
||||||
from pysonic.types import MUSIC_TYPES
|
from pysonic.types import MUSIC_TYPES
|
||||||
from pysonic.apilib import formatresponse, ApiResponse
|
from pysonic.apilib import formatresponse, ApiResponse
|
||||||
import cherrypy
|
import cherrypy
|
||||||
|
@ -11,16 +11,15 @@ logging = logging.getLogger("api")
|
||||||
|
|
||||||
|
|
||||||
class PysonicSubsonicApi(object):
|
class PysonicSubsonicApi(object):
|
||||||
def __init__(self, db, library, options):
|
def __init__(self, db, options):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.library = library
|
|
||||||
self.options = options
|
self.options = options
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@formatresponse
|
@formatresponse
|
||||||
def index(self):
|
def index(self):
|
||||||
response = ApiResponse()
|
response = ApiResponse()
|
||||||
response.add_child("totals", **self.library.db.get_stats())
|
response.add_child("totals", **self.db.get_stats())
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
|
@ -46,7 +45,7 @@ class PysonicSubsonicApi(object):
|
||||||
def getMusicFolders_view(self, **kwargs):
|
def getMusicFolders_view(self, **kwargs):
|
||||||
response = ApiResponse()
|
response = ApiResponse()
|
||||||
response.add_child("musicFolders")
|
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"])
|
response.add_child("musicFolder", _parent="musicFolders", id=folder["id"], name=folder["name"])
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -58,7 +57,7 @@ class PysonicSubsonicApi(object):
|
||||||
# TODO real lastmodified date
|
# TODO real lastmodified date
|
||||||
# TODO deal with ignoredArticles
|
# TODO deal with ignoredArticles
|
||||||
response.add_child("indexes", lastModified="1502310831000", ignoredArticles="The El La Los Las Le Les")
|
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:
|
for letter in LETTER_GROUPS:
|
||||||
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:
|
||||||
|
@ -83,7 +82,7 @@ class PysonicSubsonicApi(object):
|
||||||
|
|
||||||
qargs.update(limit=(offset, size))
|
qargs.update(limit=(offset, size))
|
||||||
|
|
||||||
albums = self.library.get_albums(**qargs)
|
albums = self.db.get_albums(**qargs)
|
||||||
|
|
||||||
response = ApiResponse()
|
response = ApiResponse()
|
||||||
|
|
||||||
|
@ -111,7 +110,7 @@ class PysonicSubsonicApi(object):
|
||||||
List either and artist or album dir
|
List either and artist or album dir
|
||||||
"""
|
"""
|
||||||
dir_id = int(id)
|
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()
|
response = ApiResponse()
|
||||||
|
|
||||||
|
@ -171,7 +170,7 @@ class PysonicSubsonicApi(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
|
||||||
song = self.library.get_song(int(id))
|
song = self.db.get_songs(id=int(id))[0]
|
||||||
fpath = song["_fullpath"]
|
fpath = song["_fullpath"]
|
||||||
media_bitrate = song.get("bitrate") / 1024 if song.get("bitrate") else 320
|
media_bitrate = song.get("bitrate") / 1024 if song.get("bitrate") else 320
|
||||||
to_bitrate = min(maxBitRate,
|
to_bitrate = min(maxBitRate,
|
||||||
|
@ -219,7 +218,7 @@ class PysonicSubsonicApi(object):
|
||||||
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.db.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)))
|
||||||
|
@ -248,7 +247,7 @@ class PysonicSubsonicApi(object):
|
||||||
"""
|
"""
|
||||||
if id.startswith("pl-"): # get art from first track in playlist
|
if id.startswith("pl-"): # get art from first track in playlist
|
||||||
playlist_id = int(id[len("pl-"):])
|
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:
|
for song in songs:
|
||||||
if song["albumcoverid"]:
|
if song["albumcoverid"]:
|
||||||
id = song["albumcoverid"]
|
id = song["albumcoverid"]
|
||||||
|
@ -262,8 +261,7 @@ class PysonicSubsonicApi(object):
|
||||||
else:
|
else:
|
||||||
id = int(id)
|
id = int(id)
|
||||||
|
|
||||||
cover = self.library.get_cover(id)
|
fpath = self.db.get_cover_path(id)
|
||||||
fpath = cover["_fullpath"]
|
|
||||||
type2ct = {
|
type2ct = {
|
||||||
'jpg': 'image/jpeg',
|
'jpg': 'image/jpeg',
|
||||||
'png': 'image/png',
|
'png': 'image/png',
|
||||||
|
@ -280,14 +278,14 @@ class PysonicSubsonicApi(object):
|
||||||
break
|
break
|
||||||
total += len(data)
|
total += len(data)
|
||||||
yield data
|
yield data
|
||||||
logging.info("\nSent {} bytes for {}".format(total, fpath))
|
logging.info("sent {} bytes for {}".format(total, fpath))
|
||||||
return content()
|
return content()
|
||||||
getCoverArt_view._cp_config = {'response.stream': True}
|
getCoverArt_view._cp_config = {'response.stream': True}
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@formatresponse
|
@formatresponse
|
||||||
def getArtistInfo_view(self, id, includeNotPresent="true", **kwargs):
|
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 = ApiResponse()
|
||||||
response.add_child("artistInfo")
|
response.add_child("artistInfo")
|
||||||
response.set_attrs("artistInfo", **info)
|
response.set_attrs("artistInfo", **info)
|
||||||
|
@ -296,7 +294,7 @@ class PysonicSubsonicApi(object):
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@formatresponse
|
@formatresponse
|
||||||
def getUser_view(self, username, **kwargs):
|
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 = ApiResponse()
|
||||||
response.add_child("user",
|
response.add_child("user",
|
||||||
username=user["username"],
|
username=user["username"],
|
||||||
|
@ -321,19 +319,19 @@ class PysonicSubsonicApi(object):
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@formatresponse
|
@formatresponse
|
||||||
def star_view(self, id, **kwargs):
|
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()
|
return ApiResponse()
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@formatresponse
|
@formatresponse
|
||||||
def unstar_view(self, id, **kwargs):
|
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()
|
return ApiResponse()
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@formatresponse
|
@formatresponse
|
||||||
def getStarred_view(self, **kwargs):
|
def getStarred_view(self, **kwargs):
|
||||||
children = self.library.get_starred(cherrypy.request.login)
|
children = self.db.get_starred(cherrypy.request.login)
|
||||||
response = ApiResponse()
|
response = ApiResponse()
|
||||||
response.add_child("starred")
|
response.add_child("starred")
|
||||||
for item in children:
|
for item in children:
|
||||||
|
@ -355,7 +353,7 @@ class PysonicSubsonicApi(object):
|
||||||
"""
|
"""
|
||||||
response = ApiResponse()
|
response = ApiResponse()
|
||||||
response.add_child("randomSongs")
|
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:
|
for song in children:
|
||||||
moreargs = {}
|
moreargs = {}
|
||||||
if song["format"]:
|
if song["format"]:
|
||||||
|
@ -390,7 +388,7 @@ class PysonicSubsonicApi(object):
|
||||||
def getGenres_view(self, **kwargs):
|
def getGenres_view(self, **kwargs):
|
||||||
response = ApiResponse()
|
response = ApiResponse()
|
||||||
response.add_child("genres")
|
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)
|
response.add_child("genre", _parent="genres", value=row["name"], songCount=420, albumCount=69)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -418,7 +416,7 @@ class PysonicSubsonicApi(object):
|
||||||
query = query.replace("*", "") # TODO handle this
|
query = query.replace("*", "") # TODO handle this
|
||||||
|
|
||||||
artists = 0
|
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"])
|
response.add_child("artist", _parent="searchResult2", id=item["id"], name=item["name"])
|
||||||
artists += 1
|
artists += 1
|
||||||
if artists >= artistCount:
|
if artists >= artistCount:
|
||||||
|
@ -426,7 +424,7 @@ class PysonicSubsonicApi(object):
|
||||||
|
|
||||||
# TODO make this more efficient
|
# TODO make this more efficient
|
||||||
albums = 0
|
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",
|
response.add_child("album", _parent="searchResult2",
|
||||||
id=album["dir"],
|
id=album["dir"],
|
||||||
parent=album["artistdir"],
|
parent=album["artistdir"],
|
||||||
|
@ -445,7 +443,7 @@ class PysonicSubsonicApi(object):
|
||||||
|
|
||||||
# TODO make this more efficient
|
# TODO make this more efficient
|
||||||
songs = 0
|
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",
|
response.add_child("song", _parent="searchResult2",
|
||||||
id=song["id"],
|
id=song["id"],
|
||||||
parent=song["albumdir"],
|
parent=song["albumdir"],
|
||||||
|
@ -485,11 +483,11 @@ class PysonicSubsonicApi(object):
|
||||||
def savePlayQueue_view(self, id, current, position, **kwargs):
|
def savePlayQueue_view(self, id, current, position, **kwargs):
|
||||||
print("TODO save playqueue with items {} current {} position {}".format(id, repr(current), repr(position)))
|
print("TODO save playqueue with items {} current {} position {}".format(id, repr(current), repr(position)))
|
||||||
current = int(current)
|
current = int(current)
|
||||||
song = self.library.get_song(current)
|
song = self.db.get_songs(id=current)[0]
|
||||||
self.library.db.update_album_played(song['albumid'], time())
|
self.db.update_album_played(song['albumid'], time())
|
||||||
self.library.db.increment_album_plays(song['albumid'])
|
self.db.increment_album_plays(song['albumid'])
|
||||||
if int(position) == 0:
|
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
|
# TODO save playlist with items ['378', '386', '384', '380', '383'] current 383 position 4471
|
||||||
# id entries are strings!
|
# id entries are strings!
|
||||||
|
|
||||||
|
@ -498,19 +496,19 @@ class PysonicSubsonicApi(object):
|
||||||
def createPlaylist_view(self, name, songId, **kwargs):
|
def createPlaylist_view(self, name, songId, **kwargs):
|
||||||
if type(songId) != list:
|
if type(songId) != list:
|
||||||
songId = [songId]
|
songId = [songId]
|
||||||
user = self.library.db.get_user(cherrypy.request.login)
|
user = self.db.get_user(cherrypy.request.login)
|
||||||
self.library.db.add_playlist(user["id"], name, songId)
|
self.db.add_playlist(user["id"], name, songId)
|
||||||
return ApiResponse()
|
return ApiResponse()
|
||||||
#TODO the response should be the new playlist, check the cap
|
#TODO the response should be the new playlist, check the cap
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@formatresponse
|
@formatresponse
|
||||||
def getPlaylists_view(self, **kwargs):
|
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 = ApiResponse()
|
||||||
response.add_child("playlists")
|
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",
|
response.add_child("playlist",
|
||||||
_parent="playlists",
|
_parent="playlists",
|
||||||
id=playlist["id"],
|
id=playlist["id"],
|
||||||
|
@ -529,9 +527,10 @@ class PysonicSubsonicApi(object):
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@formatresponse
|
@formatresponse
|
||||||
def getPlaylist_view(self, id, **kwargs):
|
def getPlaylist_view(self, id, **kwargs):
|
||||||
user = self.library.db.get_user(cherrypy.request.login)
|
id = int(id)
|
||||||
plinfo, songs = self.library.get_playlist(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 = ApiResponse()
|
||||||
response.add_child("playlist",
|
response.add_child("playlist",
|
||||||
id=plinfo["id"],
|
id=plinfo["id"],
|
||||||
|
@ -569,15 +568,16 @@ class PysonicSubsonicApi(object):
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@formatresponse
|
@formatresponse
|
||||||
def updatePlaylist_view(self, playlistId, songIndexToRemove=None, songIdToAdd=None, **kwargs):
|
def updatePlaylist_view(self, playlistId, songIndexToRemove=None, songIdToAdd=None, **kwargs):
|
||||||
user = self.library.db.get_user(cherrypy.request.login)
|
playlistId = int(playlistId)
|
||||||
plinfo, songs = self.library.get_playlist(int(playlistId))
|
user = self.db.get_user(cherrypy.request.login)
|
||||||
|
plinfo = self.db.get_playlist(playlistId)
|
||||||
|
|
||||||
assert plinfo["ownerid"] == user["id"]
|
assert plinfo["ownerid"] == user["id"]
|
||||||
|
|
||||||
if songIndexToRemove:
|
if songIndexToRemove:
|
||||||
self.library.db.remove_index_from_playlist(playlistId, songIndexToRemove)
|
self.db.remove_index_from_playlist(playlistId, songIndexToRemove)
|
||||||
elif songIdToAdd:
|
elif songIdToAdd:
|
||||||
self.library.db.add_to_playlist(playlistId, songIdToAdd)
|
self.db.add_to_playlist(playlistId, songIdToAdd)
|
||||||
#TODO there are more modification methods
|
#TODO there are more modification methods
|
||||||
|
|
||||||
return ApiResponse()
|
return ApiResponse()
|
||||||
|
@ -585,9 +585,9 @@ class PysonicSubsonicApi(object):
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
@formatresponse
|
@formatresponse
|
||||||
def deletePlaylist_view(self, id, **kwargs):
|
def deletePlaylist_view(self, id, **kwargs):
|
||||||
user = self.library.db.get_user(cherrypy.request.login)
|
user = self.db.get_user(cherrypy.request.login)
|
||||||
plinfo, _ = self.library.get_playlist(int(id))
|
plinfo = self.db.get_playlist(int(id))
|
||||||
assert plinfo["ownerid"] == user["id"]
|
assert plinfo["ownerid"] == user["id"]
|
||||||
|
|
||||||
self.library.delete_playlist(plinfo["id"])
|
self.db.delete_playlist(plinfo["id"])
|
||||||
return ApiResponse()
|
return ApiResponse()
|
||||||
|
|
|
@ -3,7 +3,6 @@ import logging
|
||||||
import cherrypy
|
import cherrypy
|
||||||
from sqlite3 import DatabaseError
|
from sqlite3 import DatabaseError
|
||||||
from pysonic.api import PysonicSubsonicApi
|
from pysonic.api import PysonicSubsonicApi
|
||||||
from pysonic.library import PysonicLibrary
|
|
||||||
from pysonic.database import PysonicDatabase, DuplicateRootException
|
from pysonic.database import PysonicDatabase, DuplicateRootException
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,15 +34,14 @@ def main():
|
||||||
format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s")
|
format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s")
|
||||||
|
|
||||||
db = PysonicDatabase(path=args.database_path)
|
db = PysonicDatabase(path=args.database_path)
|
||||||
library = PysonicLibrary(db)
|
|
||||||
for dirname in args.dirs:
|
for dirname in args.dirs:
|
||||||
dirname = os.path.abspath(dirname)
|
dirname = os.path.abspath(dirname)
|
||||||
assert os.path.exists(dirname), "--dirs must be paths that exist"
|
assert os.path.exists(dirname), "--dirs must be paths that exist"
|
||||||
try:
|
try:
|
||||||
library.add_root_dir(dirname)
|
db.add_root(dirname)
|
||||||
except DuplicateRootException:
|
except DuplicateRootException:
|
||||||
pass
|
pass
|
||||||
library.update()
|
db.update()
|
||||||
|
|
||||||
for username, password in args.user:
|
for username, password in args.user:
|
||||||
try:
|
try:
|
||||||
|
@ -55,7 +53,7 @@ def main():
|
||||||
# logging.warning("Artists: {}".format([i["name"] for i in library.get_artists()]))
|
# logging.warning("Artists: {}".format([i["name"] for i in library.get_artists()]))
|
||||||
# logging.warning("Albums: {}".format(len(library.get_albums())))
|
# logging.warning("Albums: {}".format(len(library.get_albums())))
|
||||||
|
|
||||||
api = PysonicSubsonicApi(db, library, args)
|
api = PysonicSubsonicApi(db, args)
|
||||||
api_config = {}
|
api_config = {}
|
||||||
if args.disable_auth:
|
if args.disable_auth:
|
||||||
logging.warning("starting up with auth disabled")
|
logging.warning("starting up with auth disabled")
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import logging
|
import logging
|
||||||
from hashlib import sha512
|
from hashlib import sha512
|
||||||
|
@ -5,7 +6,17 @@ from time import time
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from collections import Iterable
|
from collections import Iterable
|
||||||
|
|
||||||
|
|
||||||
|
from pysonic.scanner import PysonicFilesystemScanner
|
||||||
|
|
||||||
|
|
||||||
logging = logging.getLogger("database")
|
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"]
|
keys_in_table = ["title", "album", "artist", "type", "size"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,11 +60,18 @@ class PysonicDatabase(object):
|
||||||
self.db = None
|
self.db = None
|
||||||
self.open()
|
self.open()
|
||||||
self.migrate()
|
self.migrate()
|
||||||
|
self.scanner = PysonicFilesystemScanner(self)
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
self.db = sqlite3.connect(self.path, **self.sqlite_opts)
|
self.db = sqlite3.connect(self.path, **self.sqlite_opts)
|
||||||
self.db.row_factory = dict_factory
|
self.db.row_factory = dict_factory
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""
|
||||||
|
Start the library media scanner ands
|
||||||
|
"""
|
||||||
|
self.scanner.init_scan()
|
||||||
|
|
||||||
def migrate(self):
|
def migrate(self):
|
||||||
# Create db
|
# Create db
|
||||||
queries = ["""CREATE TABLE 'libraries' (
|
queries = ["""CREATE TABLE 'libraries' (
|
||||||
|
@ -150,6 +168,16 @@ class PysonicDatabase(object):
|
||||||
# logging.warning("db schema is version {}".format(version))
|
# logging.warning("db schema is version {}".format(version))
|
||||||
pass
|
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
|
@cursor
|
||||||
def get_stats(self, c):
|
def get_stats(self, c):
|
||||||
songs = c.execute("SELECT COUNT(*) as cnt FROM songs").fetchone()['cnt']
|
songs = c.execute("SELECT COUNT(*) as cnt FROM songs").fetchone()['cnt']
|
||||||
|
@ -167,7 +195,7 @@ class PysonicDatabase(object):
|
||||||
:return: int
|
:return: int
|
||||||
:raises: sqlite3.IntegrityError
|
:raises: sqlite3.IntegrityError
|
||||||
"""
|
"""
|
||||||
assert path.startswith("/")
|
path = os.path.abspath(os.path.normpath(path))
|
||||||
try:
|
try:
|
||||||
c.execute("INSERT INTO libraries ('name', 'path') VALUES (?, ?)", (name, path, ))
|
c.execute("INSERT INTO libraries ('name', 'path') VALUES (?, ?)", (name, path, ))
|
||||||
c.execute("COMMIT")
|
c.execute("COMMIT")
|
||||||
|
@ -348,11 +376,16 @@ class PysonicDatabase(object):
|
||||||
return genres
|
return genres
|
||||||
|
|
||||||
@cursor
|
@cursor
|
||||||
def get_cover(self, c, coverid):
|
def get_cover(self, c, cover_id):
|
||||||
cover = None
|
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
|
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
|
@cursor
|
||||||
def get_subsonic_musicdir(self, c, dirid):
|
def get_subsonic_musicdir(self, c, dirid):
|
||||||
"""
|
"""
|
||||||
|
@ -469,12 +502,13 @@ class PysonicDatabase(object):
|
||||||
|
|
||||||
@cursor
|
@cursor
|
||||||
def empty_playlist(self, c, playlist_id):
|
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("DELETE FROM playlist_entries WHERE playlistid=?", (playlist_id, ))
|
||||||
c.execute("COMMIT")
|
c.execute("COMMIT")
|
||||||
|
|
||||||
@cursor
|
@cursor
|
||||||
def delete_playlist(self, c, playlist_id):
|
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("DELETE FROM playlists WHERE id=?", (playlist_id, ))
|
||||||
c.execute("COMMIT")
|
c.execute("COMMIT")
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -18,8 +18,8 @@ RE_NUMBERS = re.compile(r'^([0-9]+)')
|
||||||
|
|
||||||
|
|
||||||
class PysonicFilesystemScanner(object):
|
class PysonicFilesystemScanner(object):
|
||||||
def __init__(self, library):
|
def __init__(self, db):
|
||||||
self.library = library
|
self.db = db
|
||||||
|
|
||||||
def init_scan(self):
|
def init_scan(self):
|
||||||
self.scanner = Thread(target=self.rescan, daemon=True)
|
self.scanner = Thread(target=self.rescan, daemon=True)
|
||||||
|
@ -31,7 +31,7 @@ class PysonicFilesystemScanner(object):
|
||||||
"""
|
"""
|
||||||
start = time()
|
start = time()
|
||||||
logging.warning("Beginning library rescan")
|
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"]))
|
logging.info("Scanning {}".format(parent["path"]))
|
||||||
self.scan_root(parent["id"], parent["path"])
|
self.scan_root(parent["id"], parent["path"])
|
||||||
logging.warning("Rescan complete in %ss", round(time() - start, 3))
|
logging.warning("Rescan complete in %ss", round(time() - start, 3))
|
||||||
|
@ -63,7 +63,7 @@ class PysonicFilesystemScanner(object):
|
||||||
:type path list
|
:type path list
|
||||||
"""
|
"""
|
||||||
assert path
|
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
|
parent_id = 0 # 0 indicates a top level item in the library
|
||||||
for name in path:
|
for name in path:
|
||||||
parent_id = self.create_or_get_dbdir(cursor, pid, parent_id, name)
|
parent_id = self.create_or_get_dbdir(cursor, pid, parent_id, name)
|
||||||
|
@ -109,7 +109,7 @@ class PysonicFilesystemScanner(object):
|
||||||
if len(path) > 1:
|
if len(path) > 1:
|
||||||
album = 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])
|
artist_id, artist_dirid = self.create_or_get_artist(cursor, pid, path[0])
|
||||||
|
|
||||||
album_id = None
|
album_id = None
|
||||||
|
@ -226,8 +226,8 @@ class PysonicFilesystemScanner(object):
|
||||||
q += "ORDER BY albumid"
|
q += "ORDER BY albumid"
|
||||||
|
|
||||||
#TODO scraping ID3 etc from the media files can be parallelized
|
#TODO scraping ID3 etc from the media files can be parallelized
|
||||||
with closing(self.library.db.db.cursor()) as reader, \
|
with closing(self.db.db.cursor()) as reader, \
|
||||||
closing(self.library.db.db.cursor()) as writer:
|
closing(self.db.db.cursor()) as writer:
|
||||||
processed = 0 # commit batching counter
|
processed = 0 # commit batching counter
|
||||||
for row in reader.execute(q):
|
for row in reader.execute(q):
|
||||||
# Find meta, bail if the file was unreadable
|
# Find meta, bail if the file was unreadable
|
||||||
|
|
Loading…
Reference in New Issue