1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-23 01:16:18 +00:00

Untabbify

This commit is contained in:
spl0k 2017-10-28 12:23:31 +02:00
parent fa4b1eca84
commit 7effd3aee5
17 changed files with 1804 additions and 1804 deletions

View File

@ -29,187 +29,187 @@ from supysonic.managers.user import UserManager
@app.before_request @app.before_request
def set_formatter(): def set_formatter():
if not request.path.startswith('/rest/'): if not request.path.startswith('/rest/'):
return return
"""Return a function to create the response.""" """Return a function to create the response."""
(f, callback) = map(request.values.get, ['f', 'callback']) (f, callback) = map(request.values.get, ['f', 'callback'])
if f == 'jsonp': if f == 'jsonp':
# Some clients (MiniSub, Perisonic) set f to jsonp without callback for streamed data # Some clients (MiniSub, Perisonic) set f to jsonp without callback for streamed data
if not callback and request.endpoint not in [ 'stream_media', 'cover_art' ]: if not callback and request.endpoint not in [ 'stream_media', 'cover_art' ]:
return ResponseHelper.responsize_json({ return ResponseHelper.responsize_json({
'error': { 'error': {
'code': 0, 'code': 0,
'message': 'Missing callback' 'message': 'Missing callback'
} }
}, error = True), 400 }, error = True), 400
request.formatter = lambda x, **kwargs: ResponseHelper.responsize_jsonp(x, callback, kwargs) request.formatter = lambda x, **kwargs: ResponseHelper.responsize_jsonp(x, callback, kwargs)
elif f == "json": elif f == "json":
request.formatter = ResponseHelper.responsize_json request.formatter = ResponseHelper.responsize_json
else: else:
request.formatter = ResponseHelper.responsize_xml request.formatter = ResponseHelper.responsize_xml
request.error_formatter = lambda code, msg: request.formatter({ 'error': { 'code': code, 'message': msg } }, error = True) request.error_formatter = lambda code, msg: request.formatter({ 'error': { 'code': code, 'message': msg } }, error = True)
@app.before_request @app.before_request
def authorize(): def authorize():
if not request.path.startswith('/rest/'): if not request.path.startswith('/rest/'):
return return
error = request.error_formatter(40, 'Unauthorized'), 401 error = request.error_formatter(40, 'Unauthorized'), 401
if request.authorization: if request.authorization:
status, user = UserManager.try_auth(store, request.authorization.username, request.authorization.password) status, user = UserManager.try_auth(store, request.authorization.username, request.authorization.password)
if status == UserManager.SUCCESS: if status == UserManager.SUCCESS:
request.username = request.authorization.username request.username = request.authorization.username
request.user = user request.user = user
return return
(username, password) = map(request.values.get, [ 'u', 'p' ]) (username, password) = map(request.values.get, [ 'u', 'p' ])
if not username or not password: if not username or not password:
return error return error
status, user = UserManager.try_auth(store, username, password) status, user = UserManager.try_auth(store, username, password)
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
return error return error
request.username = username request.username = username
request.user = user request.user = user
@app.before_request @app.before_request
def get_client_prefs(): def get_client_prefs():
if not request.path.startswith('/rest/'): if not request.path.startswith('/rest/'):
return return
if 'c' not in request.values: if 'c' not in request.values:
return request.error_formatter(10, 'Missing required parameter') return request.error_formatter(10, 'Missing required parameter')
client = request.values.get('c') client = request.values.get('c')
prefs = store.get(ClientPrefs, (request.user.id, client)) prefs = store.get(ClientPrefs, (request.user.id, client))
if not prefs: if not prefs:
prefs = ClientPrefs() prefs = ClientPrefs()
prefs.user_id = request.user.id prefs.user_id = request.user.id
prefs.client_name = client prefs.client_name = client
store.add(prefs) store.add(prefs)
store.commit() store.commit()
request.prefs = prefs request.prefs = prefs
@app.after_request @app.after_request
def set_headers(response): def set_headers(response):
if not request.path.startswith('/rest/'): if not request.path.startswith('/rest/'):
return response return response
if response.mimetype.startswith('text'): if response.mimetype.startswith('text'):
f = request.values.get('f') f = request.values.get('f')
response.headers['Content-Type'] = 'application/json' if f in [ 'jsonp', 'json' ] else 'text/xml' response.headers['Content-Type'] = 'application/json' if f in [ 'jsonp', 'json' ] else 'text/xml'
response.headers['Access-Control-Allow-Origin'] = '*' response.headers['Access-Control-Allow-Origin'] = '*'
return response return response
@app.errorhandler(404) @app.errorhandler(404)
def not_found(error): def not_found(error):
if not request.path.startswith('/rest/'): if not request.path.startswith('/rest/'):
return error return error
return request.error_formatter(0, 'Not implemented'), 501 return request.error_formatter(0, 'Not implemented'), 501
class ResponseHelper: class ResponseHelper:
@staticmethod @staticmethod
def responsize_json(ret, error = False, version = "1.8.0"): def responsize_json(ret, error = False, version = "1.8.0"):
def check_lists(d): def check_lists(d):
for key, value in d.items(): for key, value in d.items():
if isinstance(value, dict): if isinstance(value, dict):
d[key] = check_lists(value) d[key] = check_lists(value)
elif isinstance(value, list): elif isinstance(value, list):
if len(value) == 0: if len(value) == 0:
del d[key] del d[key]
else: else:
d[key] = [ check_lists(item) if isinstance(item, dict) else item for item in value ] d[key] = [ check_lists(item) if isinstance(item, dict) else item for item in value ]
return d return d
ret = check_lists(ret) ret = check_lists(ret)
# add headers to response # add headers to response
ret.update({ ret.update({
'status': 'failed' if error else 'ok', 'status': 'failed' if error else 'ok',
'version': version 'version': version
}) })
return simplejson.dumps({ 'subsonic-response': ret }, indent = True, encoding = 'utf-8') return simplejson.dumps({ 'subsonic-response': ret }, indent = True, encoding = 'utf-8')
@staticmethod @staticmethod
def responsize_jsonp(ret, callback, error = False, version = "1.8.0"): def responsize_jsonp(ret, callback, error = False, version = "1.8.0"):
return "%s(%s)" % (callback, ResponseHelper.responsize_json(ret, error, version)) return "%s(%s)" % (callback, ResponseHelper.responsize_json(ret, error, version))
@staticmethod @staticmethod
def responsize_xml(ret, error = False, version = "1.8.0"): def responsize_xml(ret, error = False, version = "1.8.0"):
"""Return an xml response from json and replace unsupported characters.""" """Return an xml response from json and replace unsupported characters."""
ret.update({ ret.update({
'status': 'failed' if error else 'ok', 'status': 'failed' if error else 'ok',
'version': version, 'version': version,
'xmlns': "http://subsonic.org/restapi" 'xmlns': "http://subsonic.org/restapi"
}) })
elem = ElementTree.Element('subsonic-response') elem = ElementTree.Element('subsonic-response')
ResponseHelper.dict2xml(elem, ret) ResponseHelper.dict2xml(elem, ret)
return minidom.parseString(ElementTree.tostring(elem)).toprettyxml(indent = ' ', encoding = 'UTF-8') return minidom.parseString(ElementTree.tostring(elem)).toprettyxml(indent = ' ', encoding = 'UTF-8')
@staticmethod @staticmethod
def dict2xml(elem, dictionary): def dict2xml(elem, dictionary):
"""Convert a json structure to xml. The game is trivial. Nesting uses the [] parenthesis. """Convert a json structure to xml. The game is trivial. Nesting uses the [] parenthesis.
ex. { 'musicFolder': {'id': 1234, 'name': "sss" } } ex. { 'musicFolder': {'id': 1234, 'name': "sss" } }
ex. { 'musicFolder': [{'id': 1234, 'name': "sss" }, {'id': 456, 'name': "aaa" }]} ex. { 'musicFolder': [{'id': 1234, 'name': "sss" }, {'id': 456, 'name': "aaa" }]}
ex. { 'musicFolders': {'musicFolder' : [{'id': 1234, 'name': "sss" }, {'id': 456, 'name': "aaa" }] } } ex. { 'musicFolders': {'musicFolder' : [{'id': 1234, 'name': "sss" }, {'id': 456, 'name': "aaa" }] } }
ex. { 'index': [{'name': 'A', 'artist': [{'id': '517674445', 'name': 'Antonello Venditti'}] }] } ex. { 'index': [{'name': 'A', 'artist': [{'id': '517674445', 'name': 'Antonello Venditti'}] }] }
ex. {"subsonic-response": { "musicFolders": {"musicFolder": [{ "id": 0,"name": "Music"}]}, ex. {"subsonic-response": { "musicFolders": {"musicFolder": [{ "id": 0,"name": "Music"}]},
"status": "ok","version": "1.7.0","xmlns": "http://subsonic.org/restapi"}} "status": "ok","version": "1.7.0","xmlns": "http://subsonic.org/restapi"}}
""" """
if not isinstance(dictionary, dict): if not isinstance(dictionary, dict):
raise TypeError('Expecting a dict') raise TypeError('Expecting a dict')
if not all(map(lambda x: isinstance(x, basestring), dictionary.keys())): if not all(map(lambda x: isinstance(x, basestring), dictionary.keys())):
raise TypeError('Dictionary keys must be strings') raise TypeError('Dictionary keys must be strings')
subelems = { k: v for k, v in dictionary.iteritems() if isinstance(v, dict) } subelems = { k: v for k, v in dictionary.iteritems() if isinstance(v, dict) }
sequences = { k: v for k, v in dictionary.iteritems() if isinstance(v, list) } sequences = { k: v for k, v in dictionary.iteritems() if isinstance(v, list) }
attributes = { k: v for k, v in dictionary.iteritems() if k != '_value_' and k not in subelems and k not in sequences } attributes = { k: v for k, v in dictionary.iteritems() if k != '_value_' and k not in subelems and k not in sequences }
if '_value_' in dictionary: if '_value_' in dictionary:
elem.text = ResponseHelper.value_tostring(dictionary['_value_']) elem.text = ResponseHelper.value_tostring(dictionary['_value_'])
for attr, value in attributes.iteritems(): for attr, value in attributes.iteritems():
elem.set(attr, ResponseHelper.value_tostring(value)) elem.set(attr, ResponseHelper.value_tostring(value))
for sub, subdict in subelems.iteritems(): for sub, subdict in subelems.iteritems():
subelem = ElementTree.SubElement(elem, sub) subelem = ElementTree.SubElement(elem, sub)
ResponseHelper.dict2xml(subelem, subdict) ResponseHelper.dict2xml(subelem, subdict)
for seq, dicts in sequences.iteritems(): for seq, dicts in sequences.iteritems():
for subdict in dicts: for subdict in dicts:
subelem = ElementTree.SubElement(elem, seq) subelem = ElementTree.SubElement(elem, seq)
ResponseHelper.dict2xml(subelem, subdict) ResponseHelper.dict2xml(subelem, subdict)
@staticmethod @staticmethod
def value_tostring(value): def value_tostring(value):
if isinstance(value, basestring): if isinstance(value, basestring):
return value return value
if isinstance(value, bool): if isinstance(value, bool):
return str(value).lower() return str(value).lower()
return str(value) return str(value)
def get_entity(req, ent, param = 'id'): def get_entity(req, ent, param = 'id'):
eid = req.values.get(param) eid = req.values.get(param)
if not eid: if not eid:
return False, req.error_formatter(10, 'Missing %s id' % ent.__name__) return False, req.error_formatter(10, 'Missing %s id' % ent.__name__)
try: try:
eid = uuid.UUID(eid) eid = uuid.UUID(eid)
except: except:
return False, req.error_formatter(0, 'Invalid %s id' % ent.__name__) return False, req.error_formatter(0, 'Invalid %s id' % ent.__name__)
entity = store.get(ent, eid) entity = store.get(ent, eid)
if not entity: if not entity:
return False, (req.error_formatter(70, '%s not found' % ent.__name__), 404) return False, (req.error_formatter(70, '%s not found' % ent.__name__), 404)
return True, entity return True, entity
from .system import * from .system import *
from .browse import * from .browse import *

View File

@ -31,171 +31,171 @@ from supysonic.db import now
@app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ])
def rand_songs(): def rand_songs():
size = request.values.get('size', '10') size = request.values.get('size', '10')
genre, fromYear, toYear, musicFolderId = map(request.values.get, [ 'genre', 'fromYear', 'toYear', 'musicFolderId' ]) genre, fromYear, toYear, musicFolderId = map(request.values.get, [ 'genre', 'fromYear', 'toYear', 'musicFolderId' ])
try: try:
size = int(size) if size else 10 size = int(size) if size else 10
fromYear = int(fromYear) if fromYear else None fromYear = int(fromYear) if fromYear else None
toYear = int(toYear) if toYear else None toYear = int(toYear) if toYear else None
fid = uuid.UUID(musicFolderId) if musicFolderId else None fid = uuid.UUID(musicFolderId) if musicFolderId else None
except: except:
return request.error_formatter(0, 'Invalid parameter format') return request.error_formatter(0, 'Invalid parameter format')
query = store.find(Track) query = store.find(Track)
if fromYear: if fromYear:
query = query.find(Track.year >= fromYear) query = query.find(Track.year >= fromYear)
if toYear: if toYear:
query = query.find(Track.year <= toYear) query = query.find(Track.year <= toYear)
if genre: if genre:
query = query.find(Track.genre == genre) query = query.find(Track.genre == genre)
if fid: if fid:
query = query.find(Track.root_folder_id == fid) query = query.find(Track.root_folder_id == fid)
count = query.count() count = query.count()
if not count: if not count:
return request.formatter({ 'randomSongs': {} }) return request.formatter({ 'randomSongs': {} })
tracks = [] tracks = []
for _ in xrange(size): for _ in xrange(size):
x = random.choice(xrange(count)) x = random.choice(xrange(count))
tracks.append(query[x]) tracks.append(query[x])
return request.formatter({ return request.formatter({
'randomSongs': { 'randomSongs': {
'song': [ t.as_subsonic_child(request.user, request.prefs) for t in tracks ] 'song': [ t.as_subsonic_child(request.user, request.prefs) for t in tracks ]
} }
}) })
@app.route('/rest/getAlbumList.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getAlbumList.view', methods = [ 'GET', 'POST' ])
def album_list(): def album_list():
ltype, size, offset = map(request.values.get, [ 'type', 'size', 'offset' ]) ltype, size, offset = map(request.values.get, [ 'type', 'size', 'offset' ])
try: try:
size = int(size) if size else 10 size = int(size) if size else 10
offset = int(offset) if offset else 0 offset = int(offset) if offset else 0
except: except:
return request.error_formatter(0, 'Invalid parameter format') return request.error_formatter(0, 'Invalid parameter format')
query = store.find(Folder, Track.folder_id == Folder.id) query = store.find(Folder, Track.folder_id == Folder.id)
if ltype == 'random': if ltype == 'random':
albums = [] albums = []
count = query.count() count = query.count()
if not count: if not count:
return request.formatter({ 'albumList': {} }) return request.formatter({ 'albumList': {} })
for _ in xrange(size): for _ in xrange(size):
x = random.choice(xrange(count)) x = random.choice(xrange(count))
albums.append(query[x]) albums.append(query[x])
return request.formatter({ return request.formatter({
'albumList': { 'albumList': {
'album': [ a.as_subsonic_child(request.user) for a in albums ] 'album': [ a.as_subsonic_child(request.user) for a in albums ]
} }
}) })
elif ltype == 'newest': elif ltype == 'newest':
query = query.order_by(Desc(Folder.created)).config(distinct = True) query = query.order_by(Desc(Folder.created)).config(distinct = True)
elif ltype == 'highest': elif ltype == 'highest':
query = query.find(RatingFolder.rated_id == Folder.id).group_by(Folder.id).order_by(Desc(Avg(RatingFolder.rating))) query = query.find(RatingFolder.rated_id == Folder.id).group_by(Folder.id).order_by(Desc(Avg(RatingFolder.rating)))
elif ltype == 'frequent': elif ltype == 'frequent':
query = query.group_by(Folder.id).order_by(Desc(Avg(Track.play_count))) query = query.group_by(Folder.id).order_by(Desc(Avg(Track.play_count)))
elif ltype == 'recent': elif ltype == 'recent':
query = query.group_by(Folder.id).order_by(Desc(Max(Track.last_play))) query = query.group_by(Folder.id).order_by(Desc(Max(Track.last_play)))
elif ltype == 'starred': elif ltype == 'starred':
query = query.find(StarredFolder.starred_id == Folder.id, User.id == StarredFolder.user_id, User.name == request.username) query = query.find(StarredFolder.starred_id == Folder.id, User.id == StarredFolder.user_id, User.name == request.username)
elif ltype == 'alphabeticalByName': elif ltype == 'alphabeticalByName':
query = query.order_by(Folder.name).config(distinct = True) query = query.order_by(Folder.name).config(distinct = True)
elif ltype == 'alphabeticalByArtist': elif ltype == 'alphabeticalByArtist':
parent = ClassAlias(Folder) parent = ClassAlias(Folder)
query = query.find(Folder.parent_id == parent.id).order_by(parent.name, Folder.name).config(distinct = True) query = query.find(Folder.parent_id == parent.id).order_by(parent.name, Folder.name).config(distinct = True)
else: else:
return request.error_formatter(0, 'Unknown search type') return request.error_formatter(0, 'Unknown search type')
return request.formatter({ return request.formatter({
'albumList': { 'albumList': {
'album': [ f.as_subsonic_child(request.user) for f in query[offset:offset+size] ] 'album': [ f.as_subsonic_child(request.user) for f in query[offset:offset+size] ]
} }
}) })
@app.route('/rest/getAlbumList2.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getAlbumList2.view', methods = [ 'GET', 'POST' ])
def album_list_id3(): def album_list_id3():
ltype, size, offset = map(request.values.get, [ 'type', 'size', 'offset' ]) ltype, size, offset = map(request.values.get, [ 'type', 'size', 'offset' ])
try: try:
size = int(size) if size else 10 size = int(size) if size else 10
offset = int(offset) if offset else 0 offset = int(offset) if offset else 0
except: except:
return request.error_formatter(0, 'Invalid parameter format') return request.error_formatter(0, 'Invalid parameter format')
query = store.find(Album) query = store.find(Album)
if ltype == 'random': if ltype == 'random':
albums = [] albums = []
count = query.count() count = query.count()
if not count: if not count:
return request.formatter({ 'albumList2': {} }) return request.formatter({ 'albumList2': {} })
for _ in xrange(size): for _ in xrange(size):
x = random.choice(xrange(count)) x = random.choice(xrange(count))
albums.append(query[x]) albums.append(query[x])
return request.formatter({ return request.formatter({
'albumList2': { 'albumList2': {
'album': [ a.as_subsonic_album(request.user) for a in albums ] 'album': [ a.as_subsonic_album(request.user) for a in albums ]
} }
}) })
elif ltype == 'newest': elif ltype == 'newest':
query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Min(Track.created))) query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Min(Track.created)))
elif ltype == 'frequent': elif ltype == 'frequent':
query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Avg(Track.play_count))) query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Avg(Track.play_count)))
elif ltype == 'recent': elif ltype == 'recent':
query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Max(Track.last_play))) query = query.find(Track.album_id == Album.id).group_by(Album.id).order_by(Desc(Max(Track.last_play)))
elif ltype == 'starred': elif ltype == 'starred':
query = query.find(StarredAlbum.starred_id == Album.id, User.id == StarredAlbum.user_id, User.name == request.username) query = query.find(StarredAlbum.starred_id == Album.id, User.id == StarredAlbum.user_id, User.name == request.username)
elif ltype == 'alphabeticalByName': elif ltype == 'alphabeticalByName':
query = query.order_by(Album.name) query = query.order_by(Album.name)
elif ltype == 'alphabeticalByArtist': elif ltype == 'alphabeticalByArtist':
query = query.find(Artist.id == Album.artist_id).order_by(Artist.name, Album.name) query = query.find(Artist.id == Album.artist_id).order_by(Artist.name, Album.name)
else: else:
return request.error_formatter(0, 'Unknown search type') return request.error_formatter(0, 'Unknown search type')
return request.formatter({ return request.formatter({
'albumList2': { 'albumList2': {
'album': [ f.as_subsonic_album(request.user) for f in query[offset:offset+size] ] 'album': [ f.as_subsonic_album(request.user) for f in query[offset:offset+size] ]
} }
}) })
@app.route('/rest/getNowPlaying.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getNowPlaying.view', methods = [ 'GET', 'POST' ])
def now_playing(): def now_playing():
query = store.find(User, Track.id == User.last_play_id) query = store.find(User, Track.id == User.last_play_id)
return request.formatter({ return request.formatter({
'nowPlaying': { 'nowPlaying': {
'entry': [ dict( 'entry': [ dict(
u.last_play.as_subsonic_child(request.user, request.prefs).items() + u.last_play.as_subsonic_child(request.user, request.prefs).items() +
{ 'username': u.name, 'minutesAgo': (now() - u.last_play_date).seconds / 60, 'playerId': 0 }.items() { 'username': u.name, 'minutesAgo': (now() - u.last_play_date).seconds / 60, 'playerId': 0 }.items()
) for u in query if u.last_play_date + timedelta(seconds = u.last_play.duration * 2) > now() ] ) for u in query if u.last_play_date + timedelta(seconds = u.last_play.duration * 2) > now() ]
} }
}) })
@app.route('/rest/getStarred.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getStarred.view', methods = [ 'GET', 'POST' ])
def get_starred(): def get_starred():
folders = store.find(StarredFolder, StarredFolder.user_id == User.id, User.name == request.username) folders = store.find(StarredFolder, StarredFolder.user_id == User.id, User.name == request.username)
return request.formatter({ return request.formatter({
'starred': { 'starred': {
'artist': [ { 'id': str(sf.starred_id), 'name': sf.starred.name } for sf in folders.find(Folder.parent_id == StarredFolder.starred_id, Track.folder_id == Folder.id).config(distinct = True) ], 'artist': [ { 'id': str(sf.starred_id), 'name': sf.starred.name } for sf in folders.find(Folder.parent_id == StarredFolder.starred_id, Track.folder_id == Folder.id).config(distinct = True) ],
'album': [ sf.starred.as_subsonic_child(request.user) for sf in folders.find(Track.folder_id == StarredFolder.starred_id).config(distinct = True) ], 'album': [ sf.starred.as_subsonic_child(request.user) for sf in folders.find(Track.folder_id == StarredFolder.starred_id).config(distinct = True) ],
'song': [ st.starred.as_subsonic_child(request.user, request.prefs) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ] 'song': [ st.starred.as_subsonic_child(request.user, request.prefs) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ]
} }
}) })
@app.route('/rest/getStarred2.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getStarred2.view', methods = [ 'GET', 'POST' ])
def get_starred_id3(): def get_starred_id3():
return request.formatter({ return request.formatter({
'starred2': { 'starred2': {
'artist': [ sa.starred.as_subsonic_artist(request.user) for sa in store.find(StarredArtist, StarredArtist.user_id == User.id, User.name == request.username) ], 'artist': [ sa.starred.as_subsonic_artist(request.user) for sa in store.find(StarredArtist, StarredArtist.user_id == User.id, User.name == request.username) ],
'album': [ sa.starred.as_subsonic_album(request.user) for sa in store.find(StarredAlbum, StarredAlbum.user_id == User.id, User.name == request.username) ], 'album': [ sa.starred.as_subsonic_album(request.user) for sa in store.find(StarredAlbum, StarredAlbum.user_id == User.id, User.name == request.username) ],
'song': [ st.starred.as_subsonic_child(request.user, request.prefs) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ] 'song': [ st.starred.as_subsonic_child(request.user, request.prefs) for st in store.find(StarredTrack, StarredTrack.user_id == User.id, User.name == request.username) ]
} }
}) })

View File

@ -30,145 +30,145 @@ from supysonic.db import RatingTrack, RatingFolder
@app.route('/rest/star.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/star.view', methods = [ 'GET', 'POST' ])
def star(): def star():
id, albumId, artistId = map(request.values.getlist, [ 'id', 'albumId', 'artistId' ]) id, albumId, artistId = map(request.values.getlist, [ 'id', 'albumId', 'artistId' ])
def try_star(ent, starred_ent, eid): def try_star(ent, starred_ent, eid):
try: try:
uid = uuid.UUID(eid) uid = uuid.UUID(eid)
except: except:
return 2, request.error_formatter(0, 'Invalid %s id' % ent.__name__) return 2, request.error_formatter(0, 'Invalid %s id' % ent.__name__)
if store.get(starred_ent, (request.user.id, uid)): if store.get(starred_ent, (request.user.id, uid)):
return 2, request.error_formatter(0, '%s already starred' % ent.__name__) return 2, request.error_formatter(0, '%s already starred' % ent.__name__)
e = store.get(ent, uid) e = store.get(ent, uid)
if e: if e:
starred = starred_ent() starred = starred_ent()
starred.user_id = request.user.id starred.user_id = request.user.id
starred.starred_id = uid starred.starred_id = uid
store.add(starred) store.add(starred)
else: else:
return 1, request.error_formatter(70, 'Unknown %s id' % ent.__name__) return 1, request.error_formatter(70, 'Unknown %s id' % ent.__name__)
return 0, None return 0, None
for eid in id: for eid in id:
err, ferror = try_star(Track, StarredTrack, eid) err, ferror = try_star(Track, StarredTrack, eid)
if err == 1: if err == 1:
err, ferror = try_star(Folder, StarredFolder, eid) err, ferror = try_star(Folder, StarredFolder, eid)
if err: if err:
return ferror return ferror
elif err == 2: elif err == 2:
return ferror return ferror
for alId in albumId: for alId in albumId:
err, ferror = try_star(Album, StarredAlbum, alId) err, ferror = try_star(Album, StarredAlbum, alId)
if err: if err:
return ferror return ferror
for arId in artistId: for arId in artistId:
err, ferror = try_star(Artist, StarredArtist, arId) err, ferror = try_star(Artist, StarredArtist, arId)
if err: if err:
return ferror return ferror
store.commit() store.commit()
return request.formatter({}) return request.formatter({})
@app.route('/rest/unstar.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/unstar.view', methods = [ 'GET', 'POST' ])
def unstar(): def unstar():
id, albumId, artistId = map(request.values.getlist, [ 'id', 'albumId', 'artistId' ]) id, albumId, artistId = map(request.values.getlist, [ 'id', 'albumId', 'artistId' ])
def try_unstar(ent, eid): def try_unstar(ent, eid):
try: try:
uid = uuid.UUID(eid) uid = uuid.UUID(eid)
except: except:
return request.error_formatter(0, 'Invalid id') return request.error_formatter(0, 'Invalid id')
store.find(ent, ent.user_id == request.user.id, ent.starred_id == uid).remove() store.find(ent, ent.user_id == request.user.id, ent.starred_id == uid).remove()
return None return None
for eid in id: for eid in id:
err = try_unstar(StarredTrack, eid) err = try_unstar(StarredTrack, eid)
if err: if err:
return err return err
err = try_unstar(StarredFolder, eid) err = try_unstar(StarredFolder, eid)
if err: if err:
return err return err
for alId in albumId: for alId in albumId:
err = try_unstar(StarredAlbum, alId) err = try_unstar(StarredAlbum, alId)
if err: if err:
return err return err
for arId in artistId: for arId in artistId:
err = try_unstar(StarredArtist, arId) err = try_unstar(StarredArtist, arId)
if err: if err:
return err return err
store.commit() store.commit()
return request.formatter({}) return request.formatter({})
@app.route('/rest/setRating.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/setRating.view', methods = [ 'GET', 'POST' ])
def rate(): def rate():
id, rating = map(request.values.get, [ 'id', 'rating' ]) id, rating = map(request.values.get, [ 'id', 'rating' ])
if not id or not rating: if not id or not rating:
return request.error_formatter(10, 'Missing parameter') return request.error_formatter(10, 'Missing parameter')
try: try:
uid = uuid.UUID(id) uid = uuid.UUID(id)
rating = int(rating) rating = int(rating)
except: except:
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
if not rating in xrange(6): if not rating in xrange(6):
return request.error_formatter(0, 'rating must be between 0 and 5 (inclusive)') return request.error_formatter(0, 'rating must be between 0 and 5 (inclusive)')
if rating == 0: if rating == 0:
store.find(RatingTrack, RatingTrack.user_id == request.user.id, RatingTrack.rated_id == uid).remove() store.find(RatingTrack, RatingTrack.user_id == request.user.id, RatingTrack.rated_id == uid).remove()
store.find(RatingFolder, RatingFolder.user_id == request.user.id, RatingFolder.rated_id == uid).remove() store.find(RatingFolder, RatingFolder.user_id == request.user.id, RatingFolder.rated_id == uid).remove()
else: else:
rated = store.get(Track, uid) rated = store.get(Track, uid)
rating_ent = RatingTrack rating_ent = RatingTrack
if not rated: if not rated:
rated = store.get(Folder, uid) rated = store.get(Folder, uid)
rating_ent = RatingFolder rating_ent = RatingFolder
if not rated: if not rated:
return request.error_formatter(70, 'Unknown id') return request.error_formatter(70, 'Unknown id')
rating_info = store.get(rating_ent, (request.user.id, uid)) rating_info = store.get(rating_ent, (request.user.id, uid))
if rating_info: if rating_info:
rating_info.rating = rating rating_info.rating = rating
else: else:
rating_info = rating_ent() rating_info = rating_ent()
rating_info.user_id = request.user.id rating_info.user_id = request.user.id
rating_info.rated_id = uid rating_info.rated_id = uid
rating_info.rating = rating rating_info.rating = rating
store.add(rating_info) store.add(rating_info)
store.commit() store.commit()
return request.formatter({}) return request.formatter({})
@app.route('/rest/scrobble.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/scrobble.view', methods = [ 'GET', 'POST' ])
def scrobble(): def scrobble():
status, res = get_entity(request, Track) status, res = get_entity(request, Track)
if not status: if not status:
return res return res
t, submission = map(request.values.get, [ 'time', 'submission' ]) t, submission = map(request.values.get, [ 'time', 'submission' ])
if t: if t:
try: try:
t = int(t) / 1000 t = int(t) / 1000
except: except:
return request.error_formatter(0, 'Invalid time value') return request.error_formatter(0, 'Invalid time value')
else: else:
t = int(time.time()) t = int(time.time())
lfm = LastFm(request.user, app.logger) lfm = LastFm(request.user, app.logger)
if submission in (None, '', True, 'true', 'True', 1, '1'): if submission in (None, '', True, 'true', 'True', 1, '1'):
lfm.scrobble(res, t) lfm.scrobble(res, t)
else: else:
lfm.now_playing(res) lfm.now_playing(res)
return request.formatter({}) return request.formatter({})

View File

@ -26,155 +26,155 @@ import uuid, string
@app.route('/rest/getMusicFolders.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getMusicFolders.view', methods = [ 'GET', 'POST' ])
def list_folders(): def list_folders():
return request.formatter({ return request.formatter({
'musicFolders': { 'musicFolders': {
'musicFolder': [ { 'musicFolder': [ {
'id': str(f.id), 'id': str(f.id),
'name': f.name 'name': f.name
} for f in store.find(Folder, Folder.root == True).order_by(Folder.name) ] } for f in store.find(Folder, Folder.root == True).order_by(Folder.name) ]
} }
}) })
@app.route('/rest/getIndexes.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getIndexes.view', methods = [ 'GET', 'POST' ])
def list_indexes(): def list_indexes():
musicFolderId = request.values.get('musicFolderId') musicFolderId = request.values.get('musicFolderId')
ifModifiedSince = request.values.get('ifModifiedSince') ifModifiedSince = request.values.get('ifModifiedSince')
if ifModifiedSince: if ifModifiedSince:
try: try:
ifModifiedSince = int(ifModifiedSince) / 1000 ifModifiedSince = int(ifModifiedSince) / 1000
except: except:
return request.error_formatter(0, 'Invalid timestamp') return request.error_formatter(0, 'Invalid timestamp')
if musicFolderId is None: if musicFolderId is None:
folder = store.find(Folder, Folder.root == True) folder = store.find(Folder, Folder.root == True)
else: else:
try: try:
mfid = uuid.UUID(musicFolderId) mfid = uuid.UUID(musicFolderId)
except: except:
return request.error_formatter(0, 'Invalid id') return request.error_formatter(0, 'Invalid id')
folder = store.get(Folder, mfid) folder = store.get(Folder, mfid)
if not folder or (type(folder) is Folder and not folder.root): if not folder or (type(folder) is Folder and not folder.root):
return request.error_formatter(70, 'Folder not found') return request.error_formatter(70, 'Folder not found')
last_modif = max(map(lambda f: f.last_scan, folder)) if type(folder) is not Folder else folder.last_scan last_modif = max(map(lambda f: f.last_scan, folder)) if type(folder) is not Folder else folder.last_scan
if (not ifModifiedSince is None) and last_modif < ifModifiedSince: if (not ifModifiedSince is None) and last_modif < ifModifiedSince:
return request.formatter({ 'indexes': { 'lastModified': last_modif * 1000 } }) return request.formatter({ 'indexes': { 'lastModified': last_modif * 1000 } })
# The XSD lies, we don't return artists but a directory structure # The XSD lies, we don't return artists but a directory structure
if type(folder) is not Folder: if type(folder) is not Folder:
artists = [] artists = []
childs = [] childs = []
for f in folder: for f in folder:
artists += f.children artists += f.children
childs += f.tracks childs += f.tracks
else: else:
artists = folder.children artists = folder.children
childs = folder.tracks childs = folder.tracks
indexes = {} indexes = {}
for artist in artists: for artist in artists:
index = artist.name[0].upper() index = artist.name[0].upper()
if index in map(str, xrange(10)): if index in map(str, xrange(10)):
index = '#' index = '#'
elif index not in string.letters: elif index not in string.letters:
index = '?' index = '?'
if index not in indexes: if index not in indexes:
indexes[index] = [] indexes[index] = []
indexes[index].append(artist) indexes[index].append(artist)
return request.formatter({ return request.formatter({
'indexes': { 'indexes': {
'lastModified': last_modif * 1000, 'lastModified': last_modif * 1000,
'index': [ { 'index': [ {
'name': k, 'name': k,
'artist': [ { 'artist': [ {
'id': str(a.id), 'id': str(a.id),
'name': a.name 'name': a.name
} for a in sorted(v, key = lambda a: a.name.lower()) ] } for a in sorted(v, key = lambda a: a.name.lower()) ]
} for k, v in sorted(indexes.iteritems()) ], } for k, v in sorted(indexes.iteritems()) ],
'child': [ c.as_subsonic_child(request.user, request.prefs) for c in sorted(childs, key = lambda t: t.sort_key()) ] 'child': [ c.as_subsonic_child(request.user, request.prefs) for c in sorted(childs, key = lambda t: t.sort_key()) ]
} }
}) })
@app.route('/rest/getMusicDirectory.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getMusicDirectory.view', methods = [ 'GET', 'POST' ])
def show_directory(): def show_directory():
status, res = get_entity(request, Folder) status, res = get_entity(request, Folder)
if not status: if not status:
return res return res
directory = { directory = {
'id': str(res.id), 'id': str(res.id),
'name': res.name, 'name': res.name,
'child': [ f.as_subsonic_child(request.user) for f in sorted(res.children, key = lambda c: c.name.lower()) ] + [ t.as_subsonic_child(request.user, request.prefs) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] 'child': [ f.as_subsonic_child(request.user) for f in sorted(res.children, key = lambda c: c.name.lower()) ] + [ t.as_subsonic_child(request.user, request.prefs) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ]
} }
if not res.root: if not res.root:
directory['parent'] = str(res.parent_id) directory['parent'] = str(res.parent_id)
return request.formatter({ 'directory': directory }) return request.formatter({ 'directory': directory })
@app.route('/rest/getArtists.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getArtists.view', methods = [ 'GET', 'POST' ])
def list_artists(): def list_artists():
# According to the API page, there are no parameters? # According to the API page, there are no parameters?
indexes = {} indexes = {}
for artist in store.find(Artist): for artist in store.find(Artist):
index = artist.name[0].upper() if artist.name else '?' index = artist.name[0].upper() if artist.name else '?'
if index in map(str, xrange(10)): if index in map(str, xrange(10)):
index = '#' index = '#'
elif index not in string.letters: elif index not in string.letters:
index = '?' index = '?'
if index not in indexes: if index not in indexes:
indexes[index] = [] indexes[index] = []
indexes[index].append(artist) indexes[index].append(artist)
return request.formatter({ return request.formatter({
'artists': { 'artists': {
'index': [ { 'index': [ {
'name': k, 'name': k,
'artist': [ a.as_subsonic_artist(request.user) for a in sorted(v, key = lambda a: a.name.lower()) ] 'artist': [ a.as_subsonic_artist(request.user) for a in sorted(v, key = lambda a: a.name.lower()) ]
} for k, v in sorted(indexes.iteritems()) ] } for k, v in sorted(indexes.iteritems()) ]
} }
}) })
@app.route('/rest/getArtist.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getArtist.view', methods = [ 'GET', 'POST' ])
def artist_info(): def artist_info():
status, res = get_entity(request, Artist) status, res = get_entity(request, Artist)
if not status: if not status:
return res return res
info = res.as_subsonic_artist(request.user) info = res.as_subsonic_artist(request.user)
albums = set(res.albums) albums = set(res.albums)
albums |= { t.album for t in res.tracks } albums |= { t.album for t in res.tracks }
info['album'] = [ a.as_subsonic_album(request.user) for a in sorted(albums, key = lambda a: a.sort_key()) ] info['album'] = [ a.as_subsonic_album(request.user) for a in sorted(albums, key = lambda a: a.sort_key()) ]
return request.formatter({ 'artist': info }) return request.formatter({ 'artist': info })
@app.route('/rest/getAlbum.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getAlbum.view', methods = [ 'GET', 'POST' ])
def album_info(): def album_info():
status, res = get_entity(request, Album) status, res = get_entity(request, Album)
if not status: if not status:
return res return res
info = res.as_subsonic_album(request.user) info = res.as_subsonic_album(request.user)
info['song'] = [ t.as_subsonic_child(request.user, request.prefs) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] info['song'] = [ t.as_subsonic_child(request.user, request.prefs) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ]
return request.formatter({ 'album': info }) return request.formatter({ 'album': info })
@app.route('/rest/getSong.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getSong.view', methods = [ 'GET', 'POST' ])
def track_info(): def track_info():
status, res = get_entity(request, Track) status, res = get_entity(request, Track)
if not status: if not status:
return res return res
return request.formatter({ 'song': res.as_subsonic_child(request.user, request.prefs) }) return request.formatter({ 'song': res.as_subsonic_child(request.user, request.prefs) })
@app.route('/rest/getVideos.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getVideos.view', methods = [ 'GET', 'POST' ])
def list_videos(): def list_videos():
return request.error_formatter(0, 'Video streaming not supported') return request.error_formatter(0, 'Video streaming not supported')

View File

@ -24,28 +24,28 @@ from supysonic.db import ChatMessage
@app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ])
def get_chat(): def get_chat():
since = request.values.get('since') since = request.values.get('since')
try: try:
since = int(since) / 1000 if since else None since = int(since) / 1000 if since else None
except: except:
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
query = store.find(ChatMessage).order_by(ChatMessage.time) query = store.find(ChatMessage).order_by(ChatMessage.time)
if since: if since:
query = query.find(ChatMessage.time > since) query = query.find(ChatMessage.time > since)
return request.formatter({ 'chatMessages': { 'chatMessage': [ msg.responsize() for msg in query ] }}) return request.formatter({ 'chatMessages': { 'chatMessage': [ msg.responsize() for msg in query ] }})
@app.route('/rest/addChatMessage.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/addChatMessage.view', methods = [ 'GET', 'POST' ])
def add_chat_message(): def add_chat_message():
msg = request.values.get('message') msg = request.values.get('message')
if not msg: if not msg:
return request.error_formatter(10, 'Missing message') return request.error_formatter(10, 'Missing message')
chat = ChatMessage() chat = ChatMessage()
chat.user_id = request.user.id chat.user_id = request.user.id
chat.message = msg chat.message = msg
store.add(chat) store.add(chat)
store.commit() store.commit()
return request.formatter({}) return request.formatter({})

View File

@ -32,197 +32,197 @@ from supysonic.db import Track, Album, Artist, Folder, User, ClientPrefs, now
from . import get_entity from . import get_entity
def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate): def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate):
if not base_cmdline: if not base_cmdline:
return None return None
ret = base_cmdline.split() ret = base_cmdline.split()
for i in xrange(len(ret)): for i in xrange(len(ret)):
ret[i] = ret[i].replace('%srcpath', input_file).replace('%srcfmt', input_format).replace('%outfmt', output_format).replace('%outrate', str(output_bitrate)) ret[i] = ret[i].replace('%srcpath', input_file).replace('%srcfmt', input_format).replace('%outfmt', output_format).replace('%outrate', str(output_bitrate))
return ret return ret
@app.route('/rest/stream.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/stream.view', methods = [ 'GET', 'POST' ])
def stream_media(): def stream_media():
status, res = get_entity(request, Track) status, res = get_entity(request, Track)
if not status: if not status:
return res return res
maxBitRate, format, timeOffset, size, estimateContentLength = map(request.values.get, [ 'maxBitRate', 'format', 'timeOffset', 'size', 'estimateContentLength' ]) maxBitRate, format, timeOffset, size, estimateContentLength = map(request.values.get, [ 'maxBitRate', 'format', 'timeOffset', 'size', 'estimateContentLength' ])
if format: if format:
format = format.lower() format = format.lower()
src_suffix = res.suffix() src_suffix = res.suffix()
dst_suffix = res.suffix() dst_suffix = res.suffix()
dst_bitrate = res.bitrate dst_bitrate = res.bitrate
dst_mimetype = res.content_type dst_mimetype = res.content_type
if request.prefs.format: if request.prefs.format:
dst_suffix = request.prefs.format dst_suffix = request.prefs.format
if request.prefs.bitrate and request.prefs.bitrate < dst_bitrate: if request.prefs.bitrate and request.prefs.bitrate < dst_bitrate:
dst_bitrate = request.prefs.bitrate dst_bitrate = request.prefs.bitrate
if maxBitRate: if maxBitRate:
try: try:
maxBitRate = int(maxBitRate) maxBitRate = int(maxBitRate)
except: except:
return request.error_formatter(0, 'Invalid bitrate value') return request.error_formatter(0, 'Invalid bitrate value')
if dst_bitrate > maxBitRate and maxBitRate != 0: if dst_bitrate > maxBitRate and maxBitRate != 0:
dst_bitrate = maxBitRate dst_bitrate = maxBitRate
if format and format != 'raw' and format != src_suffix: if format and format != 'raw' and format != src_suffix:
dst_suffix = format dst_suffix = format
dst_mimetype = config.get_mime(dst_suffix) dst_mimetype = config.get_mime(dst_suffix)
if format != 'raw' and (dst_suffix != src_suffix or dst_bitrate != res.bitrate): if format != 'raw' and (dst_suffix != src_suffix or dst_bitrate != res.bitrate):
transcoder = config.get('transcoding', 'transcoder_{}_{}'.format(src_suffix, dst_suffix)) transcoder = config.get('transcoding', 'transcoder_{}_{}'.format(src_suffix, dst_suffix))
decoder = config.get('transcoding', 'decoder_' + src_suffix) or config.get('transcoding', 'decoder') decoder = config.get('transcoding', 'decoder_' + src_suffix) or config.get('transcoding', 'decoder')
encoder = config.get('transcoding', 'encoder_' + dst_suffix) or config.get('transcoding', 'encoder') encoder = config.get('transcoding', 'encoder_' + dst_suffix) or config.get('transcoding', 'encoder')
if not transcoder and (not decoder or not encoder): if not transcoder and (not decoder or not encoder):
transcoder = config.get('transcoding', 'transcoder') transcoder = config.get('transcoding', 'transcoder')
if not transcoder: if not transcoder:
message = 'No way to transcode from {} to {}'.format(src_suffix, dst_suffix) message = 'No way to transcode from {} to {}'.format(src_suffix, dst_suffix)
app.logger.info(message) app.logger.info(message)
return request.error_formatter(0, message) return request.error_formatter(0, message)
transcoder, decoder, encoder = map(lambda x: prepare_transcoding_cmdline(x, res.path, src_suffix, dst_suffix, dst_bitrate), [ transcoder, decoder, encoder ]) transcoder, decoder, encoder = map(lambda x: prepare_transcoding_cmdline(x, res.path, src_suffix, dst_suffix, dst_bitrate), [ transcoder, decoder, encoder ])
try: try:
if transcoder: if transcoder:
dec_proc = None dec_proc = None
proc = subprocess.Popen(transcoder, stdout = subprocess.PIPE) proc = subprocess.Popen(transcoder, stdout = subprocess.PIPE)
else: else:
dec_proc = subprocess.Popen(decoder, stdout = subprocess.PIPE) dec_proc = subprocess.Popen(decoder, stdout = subprocess.PIPE)
proc = subprocess.Popen(encoder, stdin = dec_proc.stdout, stdout = subprocess.PIPE) proc = subprocess.Popen(encoder, stdin = dec_proc.stdout, stdout = subprocess.PIPE)
except: except:
return request.error_formatter(0, 'Error while running the transcoding process') return request.error_formatter(0, 'Error while running the transcoding process')
def transcode(): def transcode():
try: try:
while True: while True:
data = proc.stdout.read(8192) data = proc.stdout.read(8192)
if not data: if not data:
break break
yield data yield data
except: except:
if dec_proc != None: if dec_proc != None:
dec_proc.terminate() dec_proc.terminate()
proc.terminate() proc.terminate()
if dec_proc != None: if dec_proc != None:
dec_proc.wait() dec_proc.wait()
proc.wait() proc.wait()
app.logger.info('Transcoding track {0.id} for user {1.id}. Source: {2} at {0.bitrate}kbps. Dest: {3} at {4}kbps'.format(res, request.user, src_suffix, dst_suffix, dst_bitrate)) app.logger.info('Transcoding track {0.id} for user {1.id}. Source: {2} at {0.bitrate}kbps. Dest: {3} at {4}kbps'.format(res, request.user, src_suffix, dst_suffix, dst_bitrate))
response = Response(transcode(), mimetype = dst_mimetype) response = Response(transcode(), mimetype = dst_mimetype)
else: else:
response = send_file(res.path, mimetype = dst_mimetype, conditional=True) response = send_file(res.path, mimetype = dst_mimetype, conditional=True)
res.play_count = res.play_count + 1 res.play_count = res.play_count + 1
res.last_play = now() res.last_play = now()
request.user.last_play = res request.user.last_play = res
request.user.last_play_date = now() request.user.last_play_date = now()
store.commit() store.commit()
return response return response
@app.route('/rest/download.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/download.view', methods = [ 'GET', 'POST' ])
def download_media(): def download_media():
status, res = get_entity(request, Track) status, res = get_entity(request, Track)
if not status: if not status:
return res return res
return send_file(res.path, conditional=True) return send_file(res.path, conditional=True)
@app.route('/rest/getCoverArt.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getCoverArt.view', methods = [ 'GET', 'POST' ])
def cover_art(): def cover_art():
status, res = get_entity(request, Folder) status, res = get_entity(request, Folder)
if not status: if not status:
return res return res
if not res.has_cover_art or not os.path.isfile(os.path.join(res.path, 'cover.jpg')): if not res.has_cover_art or not os.path.isfile(os.path.join(res.path, 'cover.jpg')):
return request.error_formatter(70, 'Cover art not found') return request.error_formatter(70, 'Cover art not found')
size = request.values.get('size') size = request.values.get('size')
if size: if size:
try: try:
size = int(size) size = int(size)
except: except:
return request.error_formatter(0, 'Invalid size value') return request.error_formatter(0, 'Invalid size value')
else: else:
return send_file(os.path.join(res.path, 'cover.jpg')) return send_file(os.path.join(res.path, 'cover.jpg'))
im = Image.open(os.path.join(res.path, 'cover.jpg')) im = Image.open(os.path.join(res.path, 'cover.jpg'))
if size > im.size[0] and size > im.size[1]: if size > im.size[0] and size > im.size[1]:
return send_file(os.path.join(res.path, 'cover.jpg')) return send_file(os.path.join(res.path, 'cover.jpg'))
size_path = os.path.join(config.get('webapp', 'cache_dir'), str(size)) size_path = os.path.join(config.get('webapp', 'cache_dir'), str(size))
path = os.path.abspath(os.path.join(size_path, str(res.id))) path = os.path.abspath(os.path.join(size_path, str(res.id)))
if os.path.exists(path): if os.path.exists(path):
return send_file(path) return send_file(path)
if not os.path.exists(size_path): if not os.path.exists(size_path):
os.makedirs(size_path) os.makedirs(size_path)
im.thumbnail([size, size], Image.ANTIALIAS) im.thumbnail([size, size], Image.ANTIALIAS)
im.save(path, 'JPEG') im.save(path, 'JPEG')
return send_file(path) return send_file(path)
@app.route('/rest/getLyrics.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getLyrics.view', methods = [ 'GET', 'POST' ])
def lyrics(): def lyrics():
artist, title = map(request.values.get, [ 'artist', 'title' ]) artist, title = map(request.values.get, [ 'artist', 'title' ])
if not artist: if not artist:
return request.error_formatter(10, 'Missing artist parameter') return request.error_formatter(10, 'Missing artist parameter')
if not title: if not title:
return request.error_formatter(10, 'Missing title parameter') return request.error_formatter(10, 'Missing title parameter')
query = store.find(Track, Album.id == Track.album_id, Artist.id == Album.artist_id, Track.title.like(title), Artist.name.like(artist)) query = store.find(Track, Album.id == Track.album_id, Artist.id == Album.artist_id, Track.title.like(title), Artist.name.like(artist))
for track in query: for track in query:
lyrics_path = os.path.splitext(track.path)[0] + '.txt' lyrics_path = os.path.splitext(track.path)[0] + '.txt'
if os.path.exists(lyrics_path): if os.path.exists(lyrics_path):
app.logger.debug('Found lyrics file: ' + lyrics_path) app.logger.debug('Found lyrics file: ' + lyrics_path)
try: try:
lyrics = read_file_as_unicode(lyrics_path) lyrics = read_file_as_unicode(lyrics_path)
except UnicodeError: except UnicodeError:
# Lyrics file couldn't be decoded. Rather than displaying an error, try with the potential next files or # Lyrics file couldn't be decoded. Rather than displaying an error, try with the potential next files or
# return no lyrics. Log it anyway. # return no lyrics. Log it anyway.
app.logger.warn('Unsupported encoding for lyrics file ' + lyrics_path) app.logger.warn('Unsupported encoding for lyrics file ' + lyrics_path)
continue continue
return request.formatter({ 'lyrics': { return request.formatter({ 'lyrics': {
'artist': track.album.artist.name, 'artist': track.album.artist.name,
'title': track.title, 'title': track.title,
'_value_': lyrics '_value_': lyrics
} }) } })
try: try:
r = requests.get("http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect", r = requests.get("http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect",
params = { 'artist': artist, 'song': title }) params = { 'artist': artist, 'song': title })
root = ElementTree.fromstring(r.content) root = ElementTree.fromstring(r.content)
ns = { 'cl': 'http://api.chartlyrics.com/' } ns = { 'cl': 'http://api.chartlyrics.com/' }
return request.formatter({ 'lyrics': { return request.formatter({ 'lyrics': {
'artist': root.find('cl:LyricArtist', namespaces = ns).text, 'artist': root.find('cl:LyricArtist', namespaces = ns).text,
'title': root.find('cl:LyricSong', namespaces = ns).text, 'title': root.find('cl:LyricSong', namespaces = ns).text,
'_value_': root.find('cl:Lyric', namespaces = ns).text '_value_': root.find('cl:Lyric', namespaces = ns).text
} }) } })
except requests.exceptions.RequestException, e: except requests.exceptions.RequestException, e:
app.logger.warn('Error while requesting the ChartLyrics API: ' + str(e)) app.logger.warn('Error while requesting the ChartLyrics API: ' + str(e))
return request.formatter({ 'lyrics': {} }) return request.formatter({ 'lyrics': {} })
def read_file_as_unicode(path): def read_file_as_unicode(path):
""" Opens a file trying with different encodings and returns the contents as a unicode string """ """ Opens a file trying with different encodings and returns the contents as a unicode string """
encodings = [ 'utf-8', 'latin1' ] # Should be extended to support more encodings encodings = [ 'utf-8', 'latin1' ] # Should be extended to support more encodings
for enc in encodings: for enc in encodings:
try: try:
contents = codecs.open(path, 'r', encoding = enc).read() contents = codecs.open(path, 'r', encoding = enc).read()
app.logger.debug('Read file {} with {} encoding'.format(path, enc)) app.logger.debug('Read file {} with {} encoding'.format(path, enc))
# Maybe save the encoding somewhere to prevent going through this loop each time for the same file # Maybe save the encoding somewhere to prevent going through this loop each time for the same file
return contents return contents
except UnicodeError: except UnicodeError:
pass pass
# Fallback to ASCII # Fallback to ASCII
app.logger.debug('Reading file {} with ascii encoding'.format(path)) app.logger.debug('Reading file {} with ascii encoding'.format(path))
return unicode(open(path, 'r').read()) return unicode(open(path, 'r').read())

View File

@ -27,114 +27,114 @@ from . import get_entity
@app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ])
def list_playlists(): def list_playlists():
query = store.find(Playlist, Or(Playlist.user_id == request.user.id, Playlist.public == True)).order_by(Playlist.name) query = store.find(Playlist, Or(Playlist.user_id == request.user.id, Playlist.public == True)).order_by(Playlist.name)
username = request.values.get('username') username = request.values.get('username')
if username: if username:
if not request.user.admin: if not request.user.admin:
return request.error_formatter(50, 'Restricted to admins') return request.error_formatter(50, 'Restricted to admins')
query = store.find(Playlist, Playlist.user_id == User.id, User.name == username).order_by(Playlist.name) query = store.find(Playlist, Playlist.user_id == User.id, User.name == username).order_by(Playlist.name)
return request.formatter({ 'playlists': { 'playlist': [ p.as_subsonic_playlist(request.user) for p in query ] } }) return request.formatter({ 'playlists': { 'playlist': [ p.as_subsonic_playlist(request.user) for p in query ] } })
@app.route('/rest/getPlaylist.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getPlaylist.view', methods = [ 'GET', 'POST' ])
def show_playlist(): def show_playlist():
status, res = get_entity(request, Playlist) status, res = get_entity(request, Playlist)
if not status: if not status:
return res return res
info = res.as_subsonic_playlist(request.user) info = res.as_subsonic_playlist(request.user)
info['entry'] = [ t.as_subsonic_child(request.user, request.prefs) for t in res.get_tracks() ] info['entry'] = [ t.as_subsonic_child(request.user, request.prefs) for t in res.get_tracks() ]
return request.formatter({ 'playlist': info }) return request.formatter({ 'playlist': info })
@app.route('/rest/createPlaylist.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/createPlaylist.view', methods = [ 'GET', 'POST' ])
def create_playlist(): def create_playlist():
# Only(?) method where the android client uses form data rather than GET params # Only(?) method where the android client uses form data rather than GET params
playlist_id, name = map(request.values.get, [ 'playlistId', 'name' ]) playlist_id, name = map(request.values.get, [ 'playlistId', 'name' ])
# songId actually doesn't seem to be required # songId actually doesn't seem to be required
songs = request.values.getlist('songId') songs = request.values.getlist('songId')
try: try:
playlist_id = uuid.UUID(playlist_id) if playlist_id else None playlist_id = uuid.UUID(playlist_id) if playlist_id else None
songs = map(uuid.UUID, songs) songs = map(uuid.UUID, songs)
except: except:
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
if playlist_id: if playlist_id:
playlist = store.get(Playlist, playlist_id) playlist = store.get(Playlist, playlist_id)
if not playlist: if not playlist:
return request.error_formatter(70, 'Unknwon playlist') return request.error_formatter(70, 'Unknwon playlist')
if playlist.user_id != request.user.id and not request.user.admin: if playlist.user_id != request.user.id and not request.user.admin:
return request.error_formatter(50, "You're not allowed to modify a playlist that isn't yours") return request.error_formatter(50, "You're not allowed to modify a playlist that isn't yours")
playlist.clear() playlist.clear()
if name: if name:
playlist.name = name playlist.name = name
elif name: elif name:
playlist = Playlist() playlist = Playlist()
playlist.user_id = request.user.id playlist.user_id = request.user.id
playlist.name = name playlist.name = name
store.add(playlist) store.add(playlist)
else: else:
return request.error_formatter(10, 'Missing playlist id or name') return request.error_formatter(10, 'Missing playlist id or name')
for sid in songs: for sid in songs:
track = store.get(Track, sid) track = store.get(Track, sid)
if not track: if not track:
return request.error_formatter(70, 'Unknown song') return request.error_formatter(70, 'Unknown song')
playlist.add(track) playlist.add(track)
store.commit() store.commit()
return request.formatter({}) return request.formatter({})
@app.route('/rest/deletePlaylist.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/deletePlaylist.view', methods = [ 'GET', 'POST' ])
def delete_playlist(): def delete_playlist():
status, res = get_entity(request, Playlist) status, res = get_entity(request, Playlist)
if not status: if not status:
return res return res
if res.user_id != request.user.id and not request.user.admin: if res.user_id != request.user.id and not request.user.admin:
return request.error_formatter(50, "You're not allowed to delete a playlist that isn't yours") return request.error_formatter(50, "You're not allowed to delete a playlist that isn't yours")
store.remove(res) store.remove(res)
store.commit() store.commit()
return request.formatter({}) return request.formatter({})
@app.route('/rest/updatePlaylist.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/updatePlaylist.view', methods = [ 'GET', 'POST' ])
def update_playlist(): def update_playlist():
status, res = get_entity(request, Playlist, 'playlistId') status, res = get_entity(request, Playlist, 'playlistId')
if not status: if not status:
return res return res
if res.user_id != request.user.id and not request.user.admin: if res.user_id != request.user.id and not request.user.admin:
return request.error_formatter(50, "You're not allowed to delete a playlist that isn't yours") return request.error_formatter(50, "You're not allowed to delete a playlist that isn't yours")
playlist = res playlist = res
name, comment, public = map(request.values.get, [ 'name', 'comment', 'public' ]) name, comment, public = map(request.values.get, [ 'name', 'comment', 'public' ])
to_add, to_remove = map(request.values.getlist, [ 'songIdToAdd', 'songIndexToRemove' ]) to_add, to_remove = map(request.values.getlist, [ 'songIdToAdd', 'songIndexToRemove' ])
try: try:
to_add = map(uuid.UUID, to_add) to_add = map(uuid.UUID, to_add)
to_remove = map(int, to_remove) to_remove = map(int, to_remove)
except: except:
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
if name: if name:
playlist.name = name playlist.name = name
if comment: if comment:
playlist.comment = comment playlist.comment = comment
if public: if public:
playlist.public = public in (True, 'True', 'true', 1, '1') playlist.public = public in (True, 'True', 'true', 1, '1')
for sid in to_add: for sid in to_add:
track = store.get(Track, sid) track = store.get(Track, sid)
if not track: if not track:
return request.error_formatter(70, 'Unknown song') return request.error_formatter(70, 'Unknown song')
playlist.add(track) playlist.add(track)
playlist.remove_at_indexes(to_remove) playlist.remove_at_indexes(to_remove)
store.commit() store.commit()
return request.formatter({}) return request.formatter({})

View File

@ -25,98 +25,98 @@ from supysonic.db import Folder, Track, Artist, Album
@app.route('/rest/search.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/search.view', methods = [ 'GET', 'POST' ])
def old_search(): def old_search():
artist, album, title, anyf, count, offset, newer_than = map(request.values.get, [ 'artist', 'album', 'title', 'any', 'count', 'offset', 'newerThan' ]) artist, album, title, anyf, count, offset, newer_than = map(request.values.get, [ 'artist', 'album', 'title', 'any', 'count', 'offset', 'newerThan' ])
try: try:
count = int(count) if count else 20 count = int(count) if count else 20
offset = int(offset) if offset else 0 offset = int(offset) if offset else 0
newer_than = int(newer_than) if newer_than else 0 newer_than = int(newer_than) if newer_than else 0
except: except:
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
if artist: if artist:
parent = ClassAlias(Folder) parent = ClassAlias(Folder)
query = store.find(parent, Folder.parent_id == parent.id, Track.folder_id == Folder.id, parent.name.contains_string(artist)).config(distinct = True) query = store.find(parent, Folder.parent_id == parent.id, Track.folder_id == Folder.id, parent.name.contains_string(artist)).config(distinct = True)
elif album: elif album:
query = store.find(Folder, Track.folder_id == Folder.id, Folder.name.contains_string(album)).config(distinct = True) query = store.find(Folder, Track.folder_id == Folder.id, Folder.name.contains_string(album)).config(distinct = True)
elif title: elif title:
query = store.find(Track, Track.title.contains_string(title)) query = store.find(Track, Track.title.contains_string(title))
elif anyf: elif anyf:
folders = store.find(Folder, Folder.name.contains_string(anyf)) folders = store.find(Folder, Folder.name.contains_string(anyf))
tracks = store.find(Track, Track.title.contains_string(anyf)) tracks = store.find(Track, Track.title.contains_string(anyf))
res = list(folders[offset : offset + count]) res = list(folders[offset : offset + count])
if offset + count > folders.count(): if offset + count > folders.count():
toff = max(0, offset - folders.count()) toff = max(0, offset - folders.count())
tend = offset + count - folders.count() tend = offset + count - folders.count()
res += list(tracks[toff : tend]) res += list(tracks[toff : tend])
return request.formatter({ 'searchResult': { return request.formatter({ 'searchResult': {
'totalHits': folders.count() + tracks.count(), 'totalHits': folders.count() + tracks.count(),
'offset': offset, 'offset': offset,
'match': [ r.as_subsonic_child(request.user) if r is Folder else r.as_subsonic_child(request.user, request.prefs) for r in res ] 'match': [ r.as_subsonic_child(request.user) if r is Folder else r.as_subsonic_child(request.user, request.prefs) for r in res ]
}}) }})
else: else:
return request.error_formatter(10, 'Missing search parameter') return request.error_formatter(10, 'Missing search parameter')
return request.formatter({ 'searchResult': { return request.formatter({ 'searchResult': {
'totalHits': query.count(), 'totalHits': query.count(),
'offset': offset, 'offset': offset,
'match': [ r.as_subsonic_child(request.user) if r is Folder else r.as_subsonic_child(request.user, request.prefs) for r in query[offset : offset + count] ] 'match': [ r.as_subsonic_child(request.user) if r is Folder else r.as_subsonic_child(request.user, request.prefs) for r in query[offset : offset + count] ]
}}) }})
@app.route('/rest/search2.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/search2.view', methods = [ 'GET', 'POST' ])
def new_search(): def new_search():
query, artist_count, artist_offset, album_count, album_offset, song_count, song_offset = map( query, artist_count, artist_offset, album_count, album_offset, song_count, song_offset = map(
request.values.get, [ 'query', 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ]) request.values.get, [ 'query', 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ])
try: try:
artist_count = int(artist_count) if artist_count else 20 artist_count = int(artist_count) if artist_count else 20
artist_offset = int(artist_offset) if artist_offset else 0 artist_offset = int(artist_offset) if artist_offset else 0
album_count = int(album_count) if album_count else 20 album_count = int(album_count) if album_count else 20
album_offset = int(album_offset) if album_offset else 0 album_offset = int(album_offset) if album_offset else 0
song_count = int(song_count) if song_count else 20 song_count = int(song_count) if song_count else 20
song_offset = int(song_offset) if song_offset else 0 song_offset = int(song_offset) if song_offset else 0
except: except:
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
if not query: if not query:
return request.error_formatter(10, 'Missing query parameter') return request.error_formatter(10, 'Missing query parameter')
parent = ClassAlias(Folder) parent = ClassAlias(Folder)
artist_query = store.find(parent, Folder.parent_id == parent.id, Track.folder_id == Folder.id, parent.name.contains_string(query)).config(distinct = True, offset = artist_offset, limit = artist_count) artist_query = store.find(parent, Folder.parent_id == parent.id, Track.folder_id == Folder.id, parent.name.contains_string(query)).config(distinct = True, offset = artist_offset, limit = artist_count)
album_query = store.find(Folder, Track.folder_id == Folder.id, Folder.name.contains_string(query)).config(distinct = True, offset = album_offset, limit = album_count) album_query = store.find(Folder, Track.folder_id == Folder.id, Folder.name.contains_string(query)).config(distinct = True, offset = album_offset, limit = album_count)
song_query = store.find(Track, Track.title.contains_string(query))[song_offset : song_offset + song_count] song_query = store.find(Track, Track.title.contains_string(query))[song_offset : song_offset + song_count]
return request.formatter({ 'searchResult2': { return request.formatter({ 'searchResult2': {
'artist': [ { 'id': str(a.id), 'name': a.name } for a in artist_query ], 'artist': [ { 'id': str(a.id), 'name': a.name } for a in artist_query ],
'album': [ f.as_subsonic_child(request.user) for f in album_query ], 'album': [ f.as_subsonic_child(request.user) for f in album_query ],
'song': [ t.as_subsonic_child(request.user, request.prefs) for t in song_query ] 'song': [ t.as_subsonic_child(request.user, request.prefs) for t in song_query ]
}}) }})
@app.route('/rest/search3.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/search3.view', methods = [ 'GET', 'POST' ])
def search_id3(): def search_id3():
query, artist_count, artist_offset, album_count, album_offset, song_count, song_offset = map( query, artist_count, artist_offset, album_count, album_offset, song_count, song_offset = map(
request.values.get, [ 'query', 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ]) request.values.get, [ 'query', 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ])
try: try:
artist_count = int(artist_count) if artist_count else 20 artist_count = int(artist_count) if artist_count else 20
artist_offset = int(artist_offset) if artist_offset else 0 artist_offset = int(artist_offset) if artist_offset else 0
album_count = int(album_count) if album_count else 20 album_count = int(album_count) if album_count else 20
album_offset = int(album_offset) if album_offset else 0 album_offset = int(album_offset) if album_offset else 0
song_count = int(song_count) if song_count else 20 song_count = int(song_count) if song_count else 20
song_offset = int(song_offset) if song_offset else 0 song_offset = int(song_offset) if song_offset else 0
except: except:
return request.error_formatter(0, 'Invalid parameter') return request.error_formatter(0, 'Invalid parameter')
if not query: if not query:
return request.error_formatter(10, 'Missing query parameter') return request.error_formatter(10, 'Missing query parameter')
artist_query = store.find(Artist, Artist.name.contains_string(query))[artist_offset : artist_offset + artist_count] artist_query = store.find(Artist, Artist.name.contains_string(query))[artist_offset : artist_offset + artist_count]
album_query = store.find(Album, Album.name.contains_string(query))[album_offset : album_offset + album_count] album_query = store.find(Album, Album.name.contains_string(query))[album_offset : album_offset + album_count]
song_query = store.find(Track, Track.title.contains_string(query))[song_offset : song_offset + song_count] song_query = store.find(Track, Track.title.contains_string(query))[song_offset : song_offset + song_count]
return request.formatter({ 'searchResult3': { return request.formatter({ 'searchResult3': {
'artist': [ a.as_subsonic_artist(request.user) for a in artist_query ], 'artist': [ a.as_subsonic_artist(request.user) for a in artist_query ],
'album': [ a.as_subsonic_album(request.user) for a in album_query ], 'album': [ a.as_subsonic_album(request.user) for a in album_query ],
'song': [ t.as_subsonic_child(request.user, request.prefs) for t in song_query ] 'song': [ t.as_subsonic_child(request.user, request.prefs) for t in song_query ]
}}) }})

View File

@ -23,9 +23,9 @@ from supysonic.web import app
@app.route('/rest/ping.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/ping.view', methods = [ 'GET', 'POST' ])
def ping(): def ping():
return request.formatter({}) return request.formatter({})
@app.route('/rest/getLicense.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getLicense.view', methods = [ 'GET', 'POST' ])
def license(): def license():
return request.formatter({ 'license': { 'valid': True } }) return request.formatter({ 'license': { 'valid': True } })

View File

@ -25,70 +25,70 @@ from supysonic.managers.user import UserManager
@app.route('/rest/getUser.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getUser.view', methods = [ 'GET', 'POST' ])
def user_info(): def user_info():
username = request.values.get('username') username = request.values.get('username')
if username is None: if username is None:
return request.error_formatter(10, 'Missing username') return request.error_formatter(10, 'Missing username')
if username != request.username and not request.user.admin: if username != request.username and not request.user.admin:
return request.error_formatter(50, 'Admin restricted') return request.error_formatter(50, 'Admin restricted')
user = store.find(User, User.name == username).one() user = store.find(User, User.name == username).one()
if user is None: if user is None:
return request.error_formatter(0, 'Unknown user') return request.error_formatter(0, 'Unknown user')
return request.formatter({ 'user': user.as_subsonic_user() }) return request.formatter({ 'user': user.as_subsonic_user() })
@app.route('/rest/getUsers.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/getUsers.view', methods = [ 'GET', 'POST' ])
def users_info(): def users_info():
if not request.user.admin: if not request.user.admin:
return request.error_formatter(50, 'Admin restricted') return request.error_formatter(50, 'Admin restricted')
return request.formatter({ 'users': { 'user': [ u.as_subsonic_user() for u in store.find(User) ] } }) return request.formatter({ 'users': { 'user': [ u.as_subsonic_user() for u in store.find(User) ] } })
@app.route('/rest/createUser.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/createUser.view', methods = [ 'GET', 'POST' ])
def user_add(): def user_add():
if not request.user.admin: if not request.user.admin:
return request.error_formatter(50, 'Admin restricted') return request.error_formatter(50, 'Admin restricted')
username, password, email, admin = map(request.values.get, [ 'username', 'password', 'email', 'adminRole' ]) username, password, email, admin = map(request.values.get, [ 'username', 'password', 'email', 'adminRole' ])
if not username or not password or not email: if not username or not password or not email:
return request.error_formatter(10, 'Missing parameter') return request.error_formatter(10, 'Missing parameter')
admin = True if admin in (True, 'True', 'true', 1, '1') else False admin = True if admin in (True, 'True', 'true', 1, '1') else False
status = UserManager.add(store, username, password, email, admin) status = UserManager.add(store, username, password, email, admin)
if status == UserManager.NAME_EXISTS: if status == UserManager.NAME_EXISTS:
return request.error_formatter(0, 'There is already a user with that username') return request.error_formatter(0, 'There is already a user with that username')
return request.formatter({}) return request.formatter({})
@app.route('/rest/deleteUser.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/deleteUser.view', methods = [ 'GET', 'POST' ])
def user_del(): def user_del():
if not request.user.admin: if not request.user.admin:
return request.error_formatter(50, 'Admin restricted') return request.error_formatter(50, 'Admin restricted')
username = request.values.get('username') username = request.values.get('username')
user = store.find(User, User.name == username).one() user = store.find(User, User.name == username).one()
if not user: if not user:
return request.error_formatter(70, 'Unknown user') return request.error_formatter(70, 'Unknown user')
status = UserManager.delete(store, user.id) status = UserManager.delete(store, user.id)
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
return request.error_formatter(0, UserManager.error_str(status)) return request.error_formatter(0, UserManager.error_str(status))
return request.formatter({}) return request.formatter({})
@app.route('/rest/changePassword.view', methods = [ 'GET', 'POST' ]) @app.route('/rest/changePassword.view', methods = [ 'GET', 'POST' ])
def user_changepass(): def user_changepass():
username, password = map(request.values.get, [ 'username', 'password' ]) username, password = map(request.values.get, [ 'username', 'password' ])
if not username or not password: if not username or not password:
return request.error_formatter(10, 'Missing parameter') return request.error_formatter(10, 'Missing parameter')
if username != request.username and not request.user.admin: if username != request.username and not request.user.admin:
return request.error_formatter(50, 'Admin restricted') return request.error_formatter(50, 'Admin restricted')
status = UserManager.change_password2(store, username, password) status = UserManager.change_password2(store, username, password)
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
return request.error_formatter(0, UserManager.error_str(status)) return request.error_formatter(0, UserManager.error_str(status))
return request.formatter({}) return request.formatter({})

View File

@ -30,397 +30,397 @@ import os.path
from supysonic import config from supysonic import config
def now(): def now():
return datetime.datetime.now().replace(microsecond = 0) return datetime.datetime.now().replace(microsecond = 0)
class UnicodeOrStrVariable(Variable): class UnicodeOrStrVariable(Variable):
__slots__ = () __slots__ = ()
def parse_set(self, value, from_db): def parse_set(self, value, from_db):
if isinstance(value, unicode): if isinstance(value, unicode):
return value return value
elif isinstance(value, str): elif isinstance(value, str):
return unicode(value) return unicode(value)
raise TypeError("Expected unicode, found %r: %r" % (type(value), value)) raise TypeError("Expected unicode, found %r: %r" % (type(value), value))
Unicode.variable_class = UnicodeOrStrVariable Unicode.variable_class = UnicodeOrStrVariable
class Folder(object): class Folder(object):
__storm_table__ = 'folder' __storm_table__ = 'folder'
id = UUID(primary = True, default_factory = uuid.uuid4) id = UUID(primary = True, default_factory = uuid.uuid4)
root = Bool(default = False) root = Bool(default = False)
name = Unicode() name = Unicode()
path = Unicode() # unique path = Unicode() # unique
created = DateTime(default_factory = now) created = DateTime(default_factory = now)
has_cover_art = Bool(default = False) has_cover_art = Bool(default = False)
last_scan = Int(default = 0) last_scan = Int(default = 0)
parent_id = UUID() # nullable parent_id = UUID() # nullable
parent = Reference(parent_id, id) parent = Reference(parent_id, id)
children = ReferenceSet(id, parent_id) children = ReferenceSet(id, parent_id)
def as_subsonic_child(self, user): def as_subsonic_child(self, user):
info = { info = {
'id': str(self.id), 'id': str(self.id),
'isDir': True, 'isDir': True,
'title': self.name, 'title': self.name,
'album': self.name, 'album': self.name,
'created': self.created.isoformat() 'created': self.created.isoformat()
} }
if not self.root: if not self.root:
info['parent'] = str(self.parent_id) info['parent'] = str(self.parent_id)
info['artist'] = self.parent.name info['artist'] = self.parent.name
if self.has_cover_art: if self.has_cover_art:
info['coverArt'] = str(self.id) info['coverArt'] = str(self.id)
starred = Store.of(self).get(StarredFolder, (user.id, self.id)) starred = Store.of(self).get(StarredFolder, (user.id, self.id))
if starred: if starred:
info['starred'] = starred.date.isoformat() info['starred'] = starred.date.isoformat()
rating = Store.of(self).get(RatingFolder, (user.id, self.id)) rating = Store.of(self).get(RatingFolder, (user.id, self.id))
if rating: if rating:
info['userRating'] = rating.rating info['userRating'] = rating.rating
avgRating = Store.of(self).find(RatingFolder, RatingFolder.rated_id == self.id).avg(RatingFolder.rating) avgRating = Store.of(self).find(RatingFolder, RatingFolder.rated_id == self.id).avg(RatingFolder.rating)
if avgRating: if avgRating:
info['averageRating'] = avgRating info['averageRating'] = avgRating
return info return info
class Artist(object): class Artist(object):
__storm_table__ = 'artist' __storm_table__ = 'artist'
id = UUID(primary = True, default_factory = uuid.uuid4) id = UUID(primary = True, default_factory = uuid.uuid4)
name = Unicode() # unique name = Unicode() # unique
def as_subsonic_artist(self, user): def as_subsonic_artist(self, user):
info = { info = {
'id': str(self.id), 'id': str(self.id),
'name': self.name, 'name': self.name,
# coverArt # coverArt
'albumCount': self.albums.count() 'albumCount': self.albums.count()
} }
starred = Store.of(self).get(StarredArtist, (user.id, self.id)) starred = Store.of(self).get(StarredArtist, (user.id, self.id))
if starred: if starred:
info['starred'] = starred.date.isoformat() info['starred'] = starred.date.isoformat()
return info return info
class Album(object): class Album(object):
__storm_table__ = 'album' __storm_table__ = 'album'
id = UUID(primary = True, default_factory = uuid.uuid4) id = UUID(primary = True, default_factory = uuid.uuid4)
name = Unicode() name = Unicode()
artist_id = UUID() artist_id = UUID()
artist = Reference(artist_id, Artist.id) artist = Reference(artist_id, Artist.id)
def as_subsonic_album(self, user): def as_subsonic_album(self, user):
info = { info = {
'id': str(self.id), 'id': str(self.id),
'name': self.name, 'name': self.name,
'artist': self.artist.name, 'artist': self.artist.name,
'artistId': str(self.artist_id), 'artistId': str(self.artist_id),
'songCount': self.tracks.count(), 'songCount': self.tracks.count(),
'duration': sum(self.tracks.values(Track.duration)), 'duration': sum(self.tracks.values(Track.duration)),
'created': min(self.tracks.values(Track.created)).isoformat() 'created': min(self.tracks.values(Track.created)).isoformat()
} }
track_with_cover = self.tracks.find(Track.folder_id == Folder.id, Folder.has_cover_art).any() track_with_cover = self.tracks.find(Track.folder_id == Folder.id, Folder.has_cover_art).any()
if track_with_cover: if track_with_cover:
info['coverArt'] = str(track_with_cover.folder_id) info['coverArt'] = str(track_with_cover.folder_id)
starred = Store.of(self).get(StarredAlbum, (user.id, self.id)) starred = Store.of(self).get(StarredAlbum, (user.id, self.id))
if starred: if starred:
info['starred'] = starred.date.isoformat() info['starred'] = starred.date.isoformat()
return info return info
def sort_key(self): def sort_key(self):
year = min(map(lambda t: t.year if t.year else 9999, self.tracks)) year = min(map(lambda t: t.year if t.year else 9999, self.tracks))
return '%i%s' % (year, self.name.lower()) return '%i%s' % (year, self.name.lower())
Artist.albums = ReferenceSet(Artist.id, Album.artist_id) Artist.albums = ReferenceSet(Artist.id, Album.artist_id)
class Track(object): class Track(object):
__storm_table__ = 'track' __storm_table__ = 'track'
id = UUID(primary = True, default_factory = uuid.uuid4) id = UUID(primary = True, default_factory = uuid.uuid4)
disc = Int() disc = Int()
number = Int() number = Int()
title = Unicode() title = Unicode()
year = Int() # nullable year = Int() # nullable
genre = Unicode() # nullable genre = Unicode() # nullable
duration = Int() duration = Int()
album_id = UUID() album_id = UUID()
album = Reference(album_id, Album.id) album = Reference(album_id, Album.id)
artist_id = UUID() artist_id = UUID()
artist = Reference(artist_id, Artist.id) artist = Reference(artist_id, Artist.id)
bitrate = Int() bitrate = Int()
path = Unicode() # unique path = Unicode() # unique
content_type = Unicode() content_type = Unicode()
created = DateTime(default_factory = now) created = DateTime(default_factory = now)
last_modification = Int() last_modification = Int()
play_count = Int(default = 0) play_count = Int(default = 0)
last_play = DateTime() # nullable last_play = DateTime() # nullable
root_folder_id = UUID() root_folder_id = UUID()
root_folder = Reference(root_folder_id, Folder.id) root_folder = Reference(root_folder_id, Folder.id)
folder_id = UUID() folder_id = UUID()
folder = Reference(folder_id, Folder.id) folder = Reference(folder_id, Folder.id)
def as_subsonic_child(self, user, prefs): def as_subsonic_child(self, user, prefs):
info = { info = {
'id': str(self.id), 'id': str(self.id),
'parent': str(self.folder_id), 'parent': str(self.folder_id),
'isDir': False, 'isDir': False,
'title': self.title, 'title': self.title,
'album': self.album.name, 'album': self.album.name,
'artist': self.artist.name, 'artist': self.artist.name,
'track': self.number, 'track': self.number,
'size': os.path.getsize(self.path), 'size': os.path.getsize(self.path),
'contentType': self.content_type, 'contentType': self.content_type,
'suffix': self.suffix(), 'suffix': self.suffix(),
'duration': self.duration, 'duration': self.duration,
'bitRate': self.bitrate, 'bitRate': self.bitrate,
'path': self.path[len(self.root_folder.path) + 1:], 'path': self.path[len(self.root_folder.path) + 1:],
'isVideo': False, 'isVideo': False,
'discNumber': self.disc, 'discNumber': self.disc,
'created': self.created.isoformat(), 'created': self.created.isoformat(),
'albumId': str(self.album_id), 'albumId': str(self.album_id),
'artistId': str(self.artist_id), 'artistId': str(self.artist_id),
'type': 'music' 'type': 'music'
} }
if self.year: if self.year:
info['year'] = self.year info['year'] = self.year
if self.genre: if self.genre:
info['genre'] = self.genre info['genre'] = self.genre
if self.folder.has_cover_art: if self.folder.has_cover_art:
info['coverArt'] = str(self.folder_id) info['coverArt'] = str(self.folder_id)
starred = Store.of(self).get(StarredTrack, (user.id, self.id)) starred = Store.of(self).get(StarredTrack, (user.id, self.id))
if starred: if starred:
info['starred'] = starred.date.isoformat() info['starred'] = starred.date.isoformat()
rating = Store.of(self).get(RatingTrack, (user.id, self.id)) rating = Store.of(self).get(RatingTrack, (user.id, self.id))
if rating: if rating:
info['userRating'] = rating.rating info['userRating'] = rating.rating
avgRating = Store.of(self).find(RatingTrack, RatingTrack.rated_id == self.id).avg(RatingTrack.rating) avgRating = Store.of(self).find(RatingTrack, RatingTrack.rated_id == self.id).avg(RatingTrack.rating)
if avgRating: if avgRating:
info['averageRating'] = avgRating info['averageRating'] = avgRating
if prefs and prefs.format and prefs.format != self.suffix(): if prefs and prefs.format and prefs.format != self.suffix():
info['transcodedSuffix'] = prefs.format info['transcodedSuffix'] = prefs.format
info['transcodedContentType'] = config.get_mime(prefs.format) info['transcodedContentType'] = config.get_mime(prefs.format)
return info return info
def duration_str(self): def duration_str(self):
ret = '%02i:%02i' % ((self.duration % 3600) / 60, self.duration % 60) ret = '%02i:%02i' % ((self.duration % 3600) / 60, self.duration % 60)
if self.duration >= 3600: if self.duration >= 3600:
ret = '%02i:%s' % (self.duration / 3600, ret) ret = '%02i:%s' % (self.duration / 3600, ret)
return ret return ret
def suffix(self): def suffix(self):
return os.path.splitext(self.path)[1][1:].lower() return os.path.splitext(self.path)[1][1:].lower()
def sort_key(self): def sort_key(self):
return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower() return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower()
Folder.tracks = ReferenceSet(Folder.id, Track.folder_id) Folder.tracks = ReferenceSet(Folder.id, Track.folder_id)
Album.tracks = ReferenceSet(Album.id, Track.album_id) Album.tracks = ReferenceSet(Album.id, Track.album_id)
Artist.tracks = ReferenceSet(Artist.id, Track.artist_id) Artist.tracks = ReferenceSet(Artist.id, Track.artist_id)
class User(object): class User(object):
__storm_table__ = 'user' __storm_table__ = 'user'
id = UUID(primary = True, default_factory = uuid.uuid4) id = UUID(primary = True, default_factory = uuid.uuid4)
name = Unicode() # unique name = Unicode() # unique
mail = Unicode() mail = Unicode()
password = Unicode() password = Unicode()
salt = Unicode() salt = Unicode()
admin = Bool(default = False) admin = Bool(default = False)
lastfm_session = Unicode() # nullable lastfm_session = Unicode() # nullable
lastfm_status = Bool(default = True) # True: ok/unlinked, False: invalid session lastfm_status = Bool(default = True) # True: ok/unlinked, False: invalid session
last_play_id = UUID() # nullable last_play_id = UUID() # nullable
last_play = Reference(last_play_id, Track.id) last_play = Reference(last_play_id, Track.id)
last_play_date = DateTime() # nullable last_play_date = DateTime() # nullable
def as_subsonic_user(self): def as_subsonic_user(self):
return { return {
'username': self.name, 'username': self.name,
'email': self.mail, 'email': self.mail,
'scrobblingEnabled': self.lastfm_session is not None and self.lastfm_status, 'scrobblingEnabled': self.lastfm_session is not None and self.lastfm_status,
'adminRole': self.admin, 'adminRole': self.admin,
'settingsRole': True, 'settingsRole': True,
'downloadRole': True, 'downloadRole': True,
'uploadRole': False, 'uploadRole': False,
'playlistRole': True, 'playlistRole': True,
'coverArtRole': False, 'coverArtRole': False,
'commentRole': False, 'commentRole': False,
'podcastRole': False, 'podcastRole': False,
'streamRole': True, 'streamRole': True,
'jukeboxRole': False, 'jukeboxRole': False,
'shareRole': False 'shareRole': False
} }
class ClientPrefs(object): class ClientPrefs(object):
__storm_table__ = 'client_prefs' __storm_table__ = 'client_prefs'
__storm_primary__ = 'user_id', 'client_name' __storm_primary__ = 'user_id', 'client_name'
user_id = UUID() user_id = UUID()
client_name = Unicode() client_name = Unicode()
format = Unicode() # nullable format = Unicode() # nullable
bitrate = Int() # nullable bitrate = Int() # nullable
class BaseStarred(object): class BaseStarred(object):
__storm_primary__ = 'user_id', 'starred_id' __storm_primary__ = 'user_id', 'starred_id'
user_id = UUID() user_id = UUID()
starred_id = UUID() starred_id = UUID()
date = DateTime(default_factory = now) date = DateTime(default_factory = now)
user = Reference(user_id, User.id) user = Reference(user_id, User.id)
class StarredFolder(BaseStarred): class StarredFolder(BaseStarred):
__storm_table__ = 'starred_folder' __storm_table__ = 'starred_folder'
starred = Reference(BaseStarred.starred_id, Folder.id) starred = Reference(BaseStarred.starred_id, Folder.id)
class StarredArtist(BaseStarred): class StarredArtist(BaseStarred):
__storm_table__ = 'starred_artist' __storm_table__ = 'starred_artist'
starred = Reference(BaseStarred.starred_id, Artist.id) starred = Reference(BaseStarred.starred_id, Artist.id)
class StarredAlbum(BaseStarred): class StarredAlbum(BaseStarred):
__storm_table__ = 'starred_album' __storm_table__ = 'starred_album'
starred = Reference(BaseStarred.starred_id, Album.id) starred = Reference(BaseStarred.starred_id, Album.id)
class StarredTrack(BaseStarred): class StarredTrack(BaseStarred):
__storm_table__ = 'starred_track' __storm_table__ = 'starred_track'
starred = Reference(BaseStarred.starred_id, Track.id) starred = Reference(BaseStarred.starred_id, Track.id)
class BaseRating(object): class BaseRating(object):
__storm_primary__ = 'user_id', 'rated_id' __storm_primary__ = 'user_id', 'rated_id'
user_id = UUID() user_id = UUID()
rated_id = UUID() rated_id = UUID()
rating = Int() rating = Int()
user = Reference(user_id, User.id) user = Reference(user_id, User.id)
class RatingFolder(BaseRating): class RatingFolder(BaseRating):
__storm_table__ = 'rating_folder' __storm_table__ = 'rating_folder'
rated = Reference(BaseRating.rated_id, Folder.id) rated = Reference(BaseRating.rated_id, Folder.id)
class RatingTrack(BaseRating): class RatingTrack(BaseRating):
__storm_table__ = 'rating_track' __storm_table__ = 'rating_track'
rated = Reference(BaseRating.rated_id, Track.id) rated = Reference(BaseRating.rated_id, Track.id)
class ChatMessage(object): class ChatMessage(object):
__storm_table__ = 'chat_message' __storm_table__ = 'chat_message'
id = UUID(primary = True, default_factory = uuid.uuid4) id = UUID(primary = True, default_factory = uuid.uuid4)
user_id = UUID() user_id = UUID()
time = Int(default_factory = lambda: int(time.time())) time = Int(default_factory = lambda: int(time.time()))
message = Unicode() message = Unicode()
user = Reference(user_id, User.id) user = Reference(user_id, User.id)
def responsize(self): def responsize(self):
return { return {
'username': self.user.name, 'username': self.user.name,
'time': self.time * 1000, 'time': self.time * 1000,
'message': self.message 'message': self.message
} }
class Playlist(object): class Playlist(object):
__storm_table__ = 'playlist' __storm_table__ = 'playlist'
id = UUID(primary = True, default_factory = uuid.uuid4) id = UUID(primary = True, default_factory = uuid.uuid4)
user_id = UUID() user_id = UUID()
name = Unicode() name = Unicode()
comment = Unicode() # nullable comment = Unicode() # nullable
public = Bool(default = False) public = Bool(default = False)
created = DateTime(default_factory = now) created = DateTime(default_factory = now)
tracks = Unicode() tracks = Unicode()
user = Reference(user_id, User.id) user = Reference(user_id, User.id)
def as_subsonic_playlist(self, user): def as_subsonic_playlist(self, user):
tracks = self.get_tracks() tracks = self.get_tracks()
info = { info = {
'id': str(self.id), 'id': str(self.id),
'name': self.name if self.user_id == user.id else '[%s] %s' % (self.user.name, self.name), 'name': self.name if self.user_id == user.id else '[%s] %s' % (self.user.name, self.name),
'owner': self.user.name, 'owner': self.user.name,
'public': self.public, 'public': self.public,
'songCount': len(tracks), 'songCount': len(tracks),
'duration': sum(map(lambda t: t.duration, tracks)), 'duration': sum(map(lambda t: t.duration, tracks)),
'created': self.created.isoformat() 'created': self.created.isoformat()
} }
if self.comment: if self.comment:
info['comment'] = self.comment info['comment'] = self.comment
return info return info
def get_tracks(self): def get_tracks(self):
if not self.tracks: if not self.tracks:
return [] return []
tracks = [] tracks = []
should_fix = False should_fix = False
store = Store.of(self) store = Store.of(self)
for t in self.tracks.split(','): for t in self.tracks.split(','):
try: try:
tid = uuid.UUID(t) tid = uuid.UUID(t)
track = store.get(Track, tid) track = store.get(Track, tid)
if track: if track:
tracks.append(track) tracks.append(track)
else: else:
should_fix = True should_fix = True
except: except:
should_fix = True should_fix = True
if should_fix: if should_fix:
self.tracks = ','.join(map(lambda t: str(t.id), tracks)) self.tracks = ','.join(map(lambda t: str(t.id), tracks))
store.commit() store.commit()
return tracks return tracks
def clear(self): def clear(self):
self.tracks = "" self.tracks = ""
def add(self, track): def add(self, track):
if isinstance(track, uuid.UUID): if isinstance(track, uuid.UUID):
tid = track tid = track
elif isinstance(track, Track): elif isinstance(track, Track):
tid = track.id tid = track.id
elif isinstance(track, basestring): elif isinstance(track, basestring):
tid = uuid.UUID(track) tid = uuid.UUID(track)
if self.tracks and len(self.tracks) > 0: if self.tracks and len(self.tracks) > 0:
self.tracks = "{},{}".format(self.tracks, tid) self.tracks = "{},{}".format(self.tracks, tid)
else: else:
self.tracks = str(tid) self.tracks = str(tid)
def remove_at_indexes(self, indexes): def remove_at_indexes(self, indexes):
tracks = self.tracks.split(',') tracks = self.tracks.split(',')
for i in indexes: for i in indexes:
if i < 0 or i >= len(tracks): if i < 0 or i >= len(tracks):
continue continue
tracks[i] = None tracks[i] = None
self.tracks = ','.join(t for t in tracks if t) self.tracks = ','.join(t for t in tracks if t)
def get_store(database_uri): def get_store(database_uri):
database = create_database(database_uri) database = create_database(database_uri)
store = Store(database) store = Store(database)
return store return store

View File

@ -30,76 +30,76 @@ from supysonic.managers.folder import FolderManager
@app.before_request @app.before_request
def check_admin(): def check_admin():
if not request.path.startswith('/folder'): if not request.path.startswith('/folder'):
return return
if not UserManager.get(store, session.get('userid'))[1].admin: if not UserManager.get(store, session.get('userid'))[1].admin:
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/folder') @app.route('/folder')
def folder_index(): def folder_index():
return render_template('folders.html', folders = store.find(Folder, Folder.root == True), admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('folders.html', folders = store.find(Folder, Folder.root == True), admin = UserManager.get(store, session.get('userid'))[1].admin)
@app.route('/folder/add', methods = [ 'GET', 'POST' ]) @app.route('/folder/add', methods = [ 'GET', 'POST' ])
def add_folder(): def add_folder():
if request.method == 'GET': if request.method == 'GET':
return render_template('addfolder.html', admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('addfolder.html', admin = UserManager.get(store, session.get('userid'))[1].admin)
error = False error = False
(name, path) = map(request.form.get, [ 'name', 'path' ]) (name, path) = map(request.form.get, [ 'name', 'path' ])
if name in (None, ''): if name in (None, ''):
flash('The name is required.') flash('The name is required.')
error = True error = True
if path in (None, ''): if path in (None, ''):
flash('The path is required.') flash('The path is required.')
error = True error = True
if error: if error:
return render_template('addfolder.html', admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('addfolder.html', admin = UserManager.get(store, session.get('userid'))[1].admin)
ret = FolderManager.add(store, name, path) ret = FolderManager.add(store, name, path)
if ret != FolderManager.SUCCESS: if ret != FolderManager.SUCCESS:
flash(FolderManager.error_str(ret)) flash(FolderManager.error_str(ret))
return render_template('addfolder.html', admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('addfolder.html', admin = UserManager.get(store, session.get('userid'))[1].admin)
flash("Folder '%s' created. You should now run a scan" % name) flash("Folder '%s' created. You should now run a scan" % name)
return redirect(url_for('folder_index')) return redirect(url_for('folder_index'))
@app.route('/folder/del/<id>') @app.route('/folder/del/<id>')
def del_folder(id): def del_folder(id):
try: try:
idid = uuid.UUID(id) idid = uuid.UUID(id)
except ValueError: except ValueError:
flash('Invalid folder id') flash('Invalid folder id')
return redirect(url_for('folder_index')) return redirect(url_for('folder_index'))
ret = FolderManager.delete(store, idid) ret = FolderManager.delete(store, idid)
if ret != FolderManager.SUCCESS: if ret != FolderManager.SUCCESS:
flash(FolderManager.error_str(ret)) flash(FolderManager.error_str(ret))
else: else:
flash('Deleted folder') flash('Deleted folder')
return redirect(url_for('folder_index')) return redirect(url_for('folder_index'))
@app.route('/folder/scan') @app.route('/folder/scan')
@app.route('/folder/scan/<id>') @app.route('/folder/scan/<id>')
def scan_folder(id = None): def scan_folder(id = None):
scanner = Scanner(store) scanner = Scanner(store)
if id is None: if id is None:
for folder in store.find(Folder, Folder.root == True): for folder in store.find(Folder, Folder.root == True):
scanner.scan(folder) scanner.scan(folder)
else: else:
status, folder = FolderManager.get(store, id) status, folder = FolderManager.get(store, id)
if status != FolderManager.SUCCESS: if status != FolderManager.SUCCESS:
flash(FolderManager.error_str(status)) flash(FolderManager.error_str(status))
return redirect(url_for('folder_index')) return redirect(url_for('folder_index'))
scanner.scan(folder) scanner.scan(folder)
scanner.finish() scanner.finish()
added, deleted = scanner.stats() added, deleted = scanner.stats()
store.commit() store.commit()
flash('Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2])) flash('Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2]))
flash('Deleted: %i artists, %i albums, %i tracks' % (deleted[0], deleted[1], deleted[2])) flash('Deleted: %i artists, %i albums, %i tracks' % (deleted[0], deleted[1], deleted[2]))
return redirect(url_for('folder_index')) return redirect(url_for('folder_index'))

View File

@ -26,67 +26,67 @@ from supysonic.managers.user import UserManager
@app.route('/playlist') @app.route('/playlist')
def playlist_index(): def playlist_index():
return render_template('playlists.html', mine = store.find(Playlist, Playlist.user_id == uuid.UUID(session.get('userid'))), return render_template('playlists.html', mine = store.find(Playlist, Playlist.user_id == uuid.UUID(session.get('userid'))),
others = store.find(Playlist, Playlist.user_id != uuid.UUID(session.get('userid')), Playlist.public == True), others = store.find(Playlist, Playlist.user_id != uuid.UUID(session.get('userid')), Playlist.public == True),
admin = UserManager.get(store, session.get('userid'))[1].admin) admin = UserManager.get(store, session.get('userid'))[1].admin)
@app.route('/playlist/<uid>') @app.route('/playlist/<uid>')
def playlist_details(uid): def playlist_details(uid):
try: try:
uid = uuid.UUID(uid) if type(uid) in (str, unicode) else uid uid = uuid.UUID(uid) if type(uid) in (str, unicode) else uid
except: except:
flash('Invalid playlist id') flash('Invalid playlist id')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))
playlist = store.get(Playlist, uid) playlist = store.get(Playlist, uid)
if not playlist: if not playlist:
flash('Unknown playlist') flash('Unknown playlist')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))
return render_template('playlist.html', playlist = playlist, admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('playlist.html', playlist = playlist, admin = UserManager.get(store, session.get('userid'))[1].admin)
@app.route('/playlist/<uid>', methods = [ 'POST' ]) @app.route('/playlist/<uid>', methods = [ 'POST' ])
def playlist_update(uid): def playlist_update(uid):
try: try:
uid = uuid.UUID(uid) uid = uuid.UUID(uid)
except: except:
flash('Invalid playlist id') flash('Invalid playlist id')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))
playlist = store.get(Playlist, uid) playlist = store.get(Playlist, uid)
if not playlist: if not playlist:
flash('Unknown playlist') flash('Unknown playlist')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))
if str(playlist.user_id) != session.get('userid'): if str(playlist.user_id) != session.get('userid'):
flash("You're not allowed to edit this playlist") flash("You're not allowed to edit this playlist")
elif not request.form.get('name'): elif not request.form.get('name'):
flash('Missing playlist name') flash('Missing playlist name')
else: else:
playlist.name = request.form.get('name') playlist.name = request.form.get('name')
playlist.public = request.form.get('public') in (True, 'True', 1, '1', 'on', 'checked') playlist.public = request.form.get('public') in (True, 'True', 1, '1', 'on', 'checked')
store.commit() store.commit()
flash('Playlist updated.') flash('Playlist updated.')
return playlist_details(uid) return playlist_details(uid)
@app.route('/playlist/del/<uid>') @app.route('/playlist/del/<uid>')
def playlist_delete(uid): def playlist_delete(uid):
try: try:
uid = uuid.UUID(uid) uid = uuid.UUID(uid)
except: except:
flash('Invalid playlist id') flash('Invalid playlist id')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))
playlist = store.get(Playlist, uid) playlist = store.get(Playlist, uid)
if not playlist: if not playlist:
flash('Unknown playlist') flash('Unknown playlist')
elif str(playlist.user_id) != session.get('userid'): elif str(playlist.user_id) != session.get('userid'):
flash("You're not allowed to delete this playlist") flash("You're not allowed to delete this playlist")
else: else:
store.remove(playlist) store.remove(playlist)
store.commit() store.commit()
flash('Playlist deleted') flash('Playlist deleted')
return redirect(url_for('playlist_index')) return redirect(url_for('playlist_index'))

View File

@ -29,53 +29,53 @@ from supysonic.lastfm import LastFm
@app.before_request @app.before_request
def check_admin(): def check_admin():
if not request.path.startswith('/user'): if not request.path.startswith('/user'):
return return
if request.endpoint in ('user_index', 'add_user', 'del_user', 'export_users', 'import_users', 'do_user_import') and not UserManager.get(store, session.get('userid'))[1].admin: if request.endpoint in ('user_index', 'add_user', 'del_user', 'export_users', 'import_users', 'do_user_import') and not UserManager.get(store, session.get('userid'))[1].admin:
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/user') @app.route('/user')
def user_index(): def user_index():
return render_template('users.html', users = store.find(User), admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('users.html', users = store.find(User), admin = UserManager.get(store, session.get('userid'))[1].admin)
@app.route('/user/<uid>') @app.route('/user/<uid>')
def user_profile(uid): def user_profile(uid):
if uid == 'me': if uid == 'me':
prefs = store.find(ClientPrefs, ClientPrefs.user_id == uuid.UUID(session.get('userid'))) prefs = store.find(ClientPrefs, ClientPrefs.user_id == uuid.UUID(session.get('userid')))
return render_template('profile.html', user = UserManager.get(store, session.get('userid'))[1], api_key = config.get('lastfm', 'api_key'), clients = prefs, admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('profile.html', user = UserManager.get(store, session.get('userid'))[1], api_key = config.get('lastfm', 'api_key'), clients = prefs, admin = UserManager.get(store, session.get('userid'))[1].admin)
else: else:
if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS: if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS:
return redirect(url_for('index')) return redirect(url_for('index'))
prefs = store.find(ClientPrefs, ClientPrefs.user_id == uuid.UUID(uid)) prefs = store.find(ClientPrefs, ClientPrefs.user_id == uuid.UUID(uid))
return render_template('profile.html', user = UserManager.get(store, uid)[1], api_key = config.get('lastfm', 'api_key'), clients = prefs, admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('profile.html', user = UserManager.get(store, uid)[1], api_key = config.get('lastfm', 'api_key'), clients = prefs, admin = UserManager.get(store, session.get('userid'))[1].admin)
@app.route('/user/<uid>', methods = [ 'POST' ]) @app.route('/user/<uid>', methods = [ 'POST' ])
def update_clients(uid): def update_clients(uid):
clients_opts = {} clients_opts = {}
for client in set(map(lambda k: k.rsplit('_', 1)[0], request.form.keys())): for client in set(map(lambda k: k.rsplit('_', 1)[0], request.form.keys())):
clients_opts[client] = { k.rsplit('_', 1)[1]: v for k, v in filter(lambda (k, v): k.startswith(client), request.form.iteritems()) } clients_opts[client] = { k.rsplit('_', 1)[1]: v for k, v in filter(lambda (k, v): k.startswith(client), request.form.iteritems()) }
app.logger.debug(clients_opts) app.logger.debug(clients_opts)
if uid == 'me': if uid == 'me':
userid = uuid.UUID(session.get('userid')) userid = uuid.UUID(session.get('userid'))
else: else:
if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS: if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS:
return redirect(url_for('index')) return redirect(url_for('index'))
userid = uuid.UUID(uid) userid = uuid.UUID(uid)
for client, opts in clients_opts.iteritems(): for client, opts in clients_opts.iteritems():
prefs = store.get(ClientPrefs, (userid, client)) prefs = store.get(ClientPrefs, (userid, client))
if 'delete' in opts and opts['delete'] in [ 'on', 'true', 'checked', 'selected', '1' ]: if 'delete' in opts and opts['delete'] in [ 'on', 'true', 'checked', 'selected', '1' ]:
store.remove(prefs) store.remove(prefs)
continue continue
prefs.format = opts['format'] if 'format' in opts and opts['format'] else None prefs.format = opts['format'] if 'format' in opts and opts['format'] else None
prefs.bitrate = int(opts['bitrate']) if 'bitrate' in opts and opts['bitrate'] else None prefs.bitrate = int(opts['bitrate']) if 'bitrate' in opts and opts['bitrate'] else None
store.commit() store.commit()
flash('Clients preferences updated.') flash('Clients preferences updated.')
return user_profile(uid) return user_profile(uid)
@app.route('/user/<uid>/changeusername', methods = [ 'GET', 'POST' ]) @app.route('/user/<uid>/changeusername', methods = [ 'GET', 'POST' ])
def change_username(uid): def change_username(uid):
@ -106,209 +106,209 @@ def change_username(uid):
@app.route('/user/<uid>/changemail', methods = [ 'GET', 'POST' ]) @app.route('/user/<uid>/changemail', methods = [ 'GET', 'POST' ])
def change_mail(uid): def change_mail(uid):
if uid == 'me': if uid == 'me':
user = UserManager.get(store, session.get('userid'))[1] user = UserManager.get(store, session.get('userid'))[1]
else: else:
if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS: if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS:
return redirect(url_for('index')) return redirect(url_for('index'))
user = UserManager.get(store, uid)[1] user = UserManager.get(store, uid)[1]
if request.method == 'POST': if request.method == 'POST':
mail = request.form.get('mail') mail = request.form.get('mail')
# No validation, lol. # No validation, lol.
user.mail = mail user.mail = mail
store.commit() store.commit()
return redirect(url_for('user_profile', uid = uid)) return redirect(url_for('user_profile', uid = uid))
return render_template('change_mail.html', user = user, admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('change_mail.html', user = user, admin = UserManager.get(store, session.get('userid'))[1].admin)
@app.route('/user/<uid>/changepass', methods = [ 'GET', 'POST' ]) @app.route('/user/<uid>/changepass', methods = [ 'GET', 'POST' ])
def change_password(uid): def change_password(uid):
if uid == 'me': if uid == 'me':
user = UserManager.get(store, session.get('userid'))[1].name user = UserManager.get(store, session.get('userid'))[1].name
else: else:
if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS: if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS:
return redirect(url_for('index')) return redirect(url_for('index'))
user = UserManager.get(store, uid)[1].name user = UserManager.get(store, uid)[1].name
if request.method == 'POST': if request.method == 'POST':
error = False error = False
if uid == 'me' or uid == session.get('userid'): if uid == 'me' or uid == session.get('userid'):
current, new, confirm = map(request.form.get, [ 'current', 'new', 'confirm' ]) current, new, confirm = map(request.form.get, [ 'current', 'new', 'confirm' ])
if current in ('', None): if current in ('', None):
flash('The current password is required') flash('The current password is required')
error = True error = True
else: else:
new, confirm = map(request.form.get, [ 'new', 'confirm' ]) new, confirm = map(request.form.get, [ 'new', 'confirm' ])
if new in ('', None): if new in ('', None):
flash('The new password is required') flash('The new password is required')
error = True error = True
if new != confirm: if new != confirm:
flash("The new password and its confirmation don't match") flash("The new password and its confirmation don't match")
error = True error = True
if not error: if not error:
if uid == 'me' or uid == session.get('userid'): if uid == 'me' or uid == session.get('userid'):
status = UserManager.change_password(store, session.get('userid'), current, new) status = UserManager.change_password(store, session.get('userid'), current, new)
else: else:
status = UserManager.change_password2(store, UserManager.get(store, uid)[1].name, new) status = UserManager.change_password2(store, UserManager.get(store, uid)[1].name, new)
if status != UserManager.SUCCESS: if status != UserManager.SUCCESS:
flash(UserManager.error_str(status)) flash(UserManager.error_str(status))
else: else:
flash('Password changed') flash('Password changed')
return redirect(url_for('user_profile', uid = uid)) return redirect(url_for('user_profile', uid = uid))
return render_template('change_pass.html', user = user, admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('change_pass.html', user = user, admin = UserManager.get(store, session.get('userid'))[1].admin)
@app.route('/user/add', methods = [ 'GET', 'POST' ]) @app.route('/user/add', methods = [ 'GET', 'POST' ])
def add_user(): def add_user():
if request.method == 'GET': if request.method == 'GET':
return render_template('adduser.html', admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('adduser.html', admin = UserManager.get(store, session.get('userid'))[1].admin)
error = False error = False
(name, passwd, passwd_confirm, mail, admin) = map(request.form.get, [ 'user', 'passwd', 'passwd_confirm', 'mail', 'admin' ]) (name, passwd, passwd_confirm, mail, admin) = map(request.form.get, [ 'user', 'passwd', 'passwd_confirm', 'mail', 'admin' ])
if name in (None, ''): if name in (None, ''):
flash('The name is required.') flash('The name is required.')
error = True error = True
if passwd in (None, ''): if passwd in (None, ''):
flash('Please provide a password.') flash('Please provide a password.')
error = True error = True
elif passwd != passwd_confirm: elif passwd != passwd_confirm:
flash("The passwords don't match.") flash("The passwords don't match.")
error = True error = True
if admin is None: if admin is None:
admin = True if store.find(User, User.admin == True).count() == 0 else False admin = True if store.find(User, User.admin == True).count() == 0 else False
else: else:
admin = True admin = True
if not error: if not error:
status = UserManager.add(store, name, passwd, mail, admin) status = UserManager.add(store, name, passwd, mail, admin)
if status == UserManager.SUCCESS: if status == UserManager.SUCCESS:
flash("User '%s' successfully added" % name) flash("User '%s' successfully added" % name)
return redirect(url_for('user_index')) return redirect(url_for('user_index'))
else: else:
flash(UserManager.error_str(status)) flash(UserManager.error_str(status))
return render_template('adduser.html', admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('adduser.html', admin = UserManager.get(store, session.get('userid'))[1].admin)
@app.route('/user/del/<uid>') @app.route('/user/del/<uid>')
def del_user(uid): def del_user(uid):
status = UserManager.delete(store, uid) status = UserManager.delete(store, uid)
if status == UserManager.SUCCESS: if status == UserManager.SUCCESS:
flash('Deleted user') flash('Deleted user')
else: else:
flash(UserManager.error_str(status)) flash(UserManager.error_str(status))
return redirect(url_for('user_index')) return redirect(url_for('user_index'))
@app.route('/user/export') @app.route('/user/export')
def export_users(): def export_users():
resp = make_response('\n'.join([ '%s,%s,%s,%s,"%s",%s,%s,%s' % (u.id, u.name, u.mail, u.password, u.salt, u.admin, u.lastfm_session, u.lastfm_status) resp = make_response('\n'.join([ '%s,%s,%s,%s,"%s",%s,%s,%s' % (u.id, u.name, u.mail, u.password, u.salt, u.admin, u.lastfm_session, u.lastfm_status)
for u in store.find(User) ])) for u in store.find(User) ]))
resp.headers['Content-disposition'] = 'attachment;filename=users.csv' resp.headers['Content-disposition'] = 'attachment;filename=users.csv'
resp.headers['Content-type'] = 'text/csv' resp.headers['Content-type'] = 'text/csv'
return resp return resp
@app.route('/user/import') @app.route('/user/import')
def import_users(): def import_users():
return render_template('importusers.html', admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('importusers.html', admin = UserManager.get(store, session.get('userid'))[1].admin)
@app.route('/user/import', methods = [ 'POST' ]) @app.route('/user/import', methods = [ 'POST' ])
def do_user_import(): def do_user_import():
if not request.files['file']: if not request.files['file']:
return render_template('importusers.html', admin = UserManager.get(store, session.get('userid'))[1].admin) return render_template('importusers.html', admin = UserManager.get(store, session.get('userid'))[1].admin)
users = [] users = []
reader = csv.reader(request.files['file']) reader = csv.reader(request.files['file'])
for id, name, mail, password, salt, admin, lfmsess, lfmstatus in reader: for id, name, mail, password, salt, admin, lfmsess, lfmstatus in reader:
mail = None if mail == 'None' else mail mail = None if mail == 'None' else mail
admin = admin == 'True' admin = admin == 'True'
lfmsess = None if lfmsess == 'None' else lfmsess lfmsess = None if lfmsess == 'None' else lfmsess
lfmstatus = lfmstatus == 'True' lfmstatus = lfmstatus == 'True'
user = User() user = User()
user.id = uuid.UUID(id) user.id = uuid.UUID(id)
user.name = name user.name = name
user.password = password user.password = password
user.salt = salt user.salt = salt
user.admin = admin user.admin = admin
user.lastfm_session = lfmsess user.lastfm_session = lfmsess
user.lastfm_status = lfmstatus user.lastfm_status = lfmstatus
users.append(user) users.append(user)
store.find(User).remove() store.find(User).remove()
for u in users: for u in users:
store.add(u) store.add(u)
store.commit() store.commit()
return redirect(url_for('user_index')) return redirect(url_for('user_index'))
@app.route('/user/<uid>/lastfm/link') @app.route('/user/<uid>/lastfm/link')
def lastfm_reg(uid): def lastfm_reg(uid):
token = request.args.get('token') token = request.args.get('token')
if token in ('', None): if token in ('', None):
flash('Missing LastFM auth token') flash('Missing LastFM auth token')
return redirect(url_for('user_profile', uid = uid)) return redirect(url_for('user_profile', uid = uid))
if uid == 'me': if uid == 'me':
lfm = LastFm(UserManager.get(store, session.get('userid'))[1], app.logger) lfm = LastFm(UserManager.get(store, session.get('userid'))[1], app.logger)
else: else:
if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS: if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS:
return redirect(url_for('index')) return redirect(url_for('index'))
lfm = LastFm(UserManager.get(store, uid)[1], app.logger) lfm = LastFm(UserManager.get(store, uid)[1], app.logger)
status, error = lfm.link_account(token) status, error = lfm.link_account(token)
store.commit() store.commit()
flash(error if not status else 'Successfully linked LastFM account') flash(error if not status else 'Successfully linked LastFM account')
return redirect(url_for('user_profile', uid = uid)) return redirect(url_for('user_profile', uid = uid))
@app.route('/user/<uid>/lastfm/unlink') @app.route('/user/<uid>/lastfm/unlink')
def lastfm_unreg(uid): def lastfm_unreg(uid):
if uid == 'me': if uid == 'me':
lfm = LastFm(UserManager.get(store, session.get('userid'))[1], app.logger) lfm = LastFm(UserManager.get(store, session.get('userid'))[1], app.logger)
else: else:
if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS: if not UserManager.get(store, session.get('userid'))[1].admin or not UserManager.get(store, uid)[0] is UserManager.SUCCESS:
return redirect(url_for('index')) return redirect(url_for('index'))
lfm = LastFm(UserManager.get(store, uid)[1], app.logger) lfm = LastFm(UserManager.get(store, uid)[1], app.logger)
lfm.unlink_account() lfm.unlink_account()
store.commit() store.commit()
flash('Unliked LastFM account') flash('Unliked LastFM account')
return redirect(url_for('user_profile', uid = uid)) return redirect(url_for('user_profile', uid = uid))
@app.route('/user/login', methods = [ 'GET', 'POST']) @app.route('/user/login', methods = [ 'GET', 'POST'])
def login(): def login():
return_url = request.args.get('returnUrl') or url_for('index') return_url = request.args.get('returnUrl') or url_for('index')
if session.get('userid'): if session.get('userid'):
flash('Already logged in') flash('Already logged in')
return redirect(return_url) return redirect(return_url)
if request.method == 'GET': if request.method == 'GET':
return render_template('login.html') return render_template('login.html')
name, password = map(request.form.get, [ 'user', 'password' ]) name, password = map(request.form.get, [ 'user', 'password' ])
error = False error = False
if name in ('', None): if name in ('', None):
flash('Missing user name') flash('Missing user name')
error = True error = True
if password in ('', None): if password in ('', None):
flash('Missing password') flash('Missing password')
error = True error = True
if not error: if not error:
status, user = UserManager.try_auth(store, name, password) status, user = UserManager.try_auth(store, name, password)
if status == UserManager.SUCCESS: if status == UserManager.SUCCESS:
session['userid'] = str(user.id) session['userid'] = str(user.id)
session['username'] = user.name session['username'] = user.name
flash('Logged in!') flash('Logged in!')
return redirect(return_url) return redirect(return_url)
else: else:
flash(UserManager.error_str(status)) flash(UserManager.error_str(status))
return render_template('login.html') return render_template('login.html')
@app.route('/user/logout') @app.route('/user/logout')
def logout(): def logout():
session.clear() session.clear()
flash('Logged out!') flash('Logged out!')
return redirect(url_for('login')) return redirect(url_for('login'))

View File

@ -22,81 +22,81 @@ import requests, hashlib
from supysonic import config from supysonic import config
class LastFm: class LastFm:
def __init__(self, user, logger): def __init__(self, user, logger):
self.__user = user self.__user = user
self.__api_key = config.get('lastfm', 'api_key') self.__api_key = config.get('lastfm', 'api_key')
self.__api_secret = config.get('lastfm', 'secret') self.__api_secret = config.get('lastfm', 'secret')
self.__enabled = self.__api_key is not None and self.__api_secret is not None self.__enabled = self.__api_key is not None and self.__api_secret is not None
self.__logger = logger self.__logger = logger
def link_account(self, token): def link_account(self, token):
if not self.__enabled: if not self.__enabled:
return False, 'No API key set' return False, 'No API key set'
res = self.__api_request(False, method = 'auth.getSession', token = token) res = self.__api_request(False, method = 'auth.getSession', token = token)
if not res: if not res:
return False, 'Error connecting to LastFM' return False, 'Error connecting to LastFM'
elif 'error' in res: elif 'error' in res:
return False, 'Error %i: %s' % (res['error'], res['message']) return False, 'Error %i: %s' % (res['error'], res['message'])
else: else:
self.__user.lastfm_session = res['session']['key'] self.__user.lastfm_session = res['session']['key']
self.__user.lastfm_status = True self.__user.lastfm_status = True
return True, 'OK' return True, 'OK'
def unlink_account(self): def unlink_account(self):
self.__user.lastfm_session = None self.__user.lastfm_session = None
self.__user.lastfm_status = True self.__user.lastfm_status = True
def now_playing(self, track): def now_playing(self, track):
if not self.__enabled: if not self.__enabled:
return return
self.__api_request(True, method = 'track.updateNowPlaying', artist = track.album.artist.name, track = track.title, album = track.album.name, self.__api_request(True, method = 'track.updateNowPlaying', artist = track.album.artist.name, track = track.title, album = track.album.name,
trackNumber = track.number, duration = track.duration) trackNumber = track.number, duration = track.duration)
def scrobble(self, track, ts): def scrobble(self, track, ts):
if not self.__enabled: if not self.__enabled:
return return
self.__api_request(True, method = 'track.scrobble', artist = track.album.artist.name, track = track.title, album = track.album.name, self.__api_request(True, method = 'track.scrobble', artist = track.album.artist.name, track = track.title, album = track.album.name,
timestamp = ts, trackNumber = track.number, duration = track.duration) timestamp = ts, trackNumber = track.number, duration = track.duration)
def __api_request(self, write, **kwargs): def __api_request(self, write, **kwargs):
if not self.__enabled: if not self.__enabled:
return return
if write: if write:
if not self.__user.lastfm_session or not self.__user.lastfm_status: if not self.__user.lastfm_session or not self.__user.lastfm_status:
return return
kwargs['sk'] = self.__user.lastfm_session kwargs['sk'] = self.__user.lastfm_session
kwargs['api_key'] = self.__api_key kwargs['api_key'] = self.__api_key
sig_str = '' sig_str = ''
for k, v in sorted(kwargs.iteritems()): for k, v in sorted(kwargs.iteritems()):
if type(v) is unicode: if type(v) is unicode:
sig_str += k + v.encode('utf-8') sig_str += k + v.encode('utf-8')
else: else:
sig_str += k + str(v) sig_str += k + str(v)
sig = hashlib.md5(sig_str + self.__api_secret).hexdigest() sig = hashlib.md5(sig_str + self.__api_secret).hexdigest()
kwargs['api_sig'] = sig kwargs['api_sig'] = sig
kwargs['format'] = 'json' kwargs['format'] = 'json'
try: try:
if write: if write:
r = requests.post('http://ws.audioscrobbler.com/2.0/', data = kwargs) r = requests.post('http://ws.audioscrobbler.com/2.0/', data = kwargs)
else: else:
r = requests.get('http://ws.audioscrobbler.com/2.0/', params = kwargs) r = requests.get('http://ws.audioscrobbler.com/2.0/', params = kwargs)
except requests.exceptions.RequestException, e: except requests.exceptions.RequestException, e:
self.__logger.warn('Error while connecting to LastFM: ' + str(e)) self.__logger.warn('Error while connecting to LastFM: ' + str(e))
return None return None
json = r.json() json = r.json()
if 'error' in json: if 'error' in json:
if json['error'] in (9, '9'): if json['error'] in (9, '9'):
self.__user.lastfm_status = False self.__user.lastfm_status = False
self.__logger.warn('LastFM error %i: %s' % (json['error'], json['message'])) self.__logger.warn('LastFM error %i: %s' % (json['error'], json['message']))
return json return json

View File

@ -23,101 +23,101 @@ from supysonic.db import Folder, Artist, Album, Track, StarredFolder, RatingFold
from supysonic.scanner import Scanner from supysonic.scanner import Scanner
class FolderManager: class FolderManager:
SUCCESS = 0 SUCCESS = 0
INVALID_ID = 1 INVALID_ID = 1
NAME_EXISTS = 2 NAME_EXISTS = 2
INVALID_PATH = 3 INVALID_PATH = 3
PATH_EXISTS = 4 PATH_EXISTS = 4
NO_SUCH_FOLDER = 5 NO_SUCH_FOLDER = 5
SUBPATH_EXISTS = 6 SUBPATH_EXISTS = 6
@staticmethod @staticmethod
def get(store, uid): def get(store, uid):
if isinstance(uid, basestring): if isinstance(uid, basestring):
try: try:
uid = uuid.UUID(uid) uid = uuid.UUID(uid)
except: except:
return FolderManager.INVALID_ID, None return FolderManager.INVALID_ID, None
elif type(uid) is uuid.UUID: elif type(uid) is uuid.UUID:
pass pass
else: else:
return FolderManager.INVALID_ID, None return FolderManager.INVALID_ID, None
folder = store.get(Folder, uid) folder = store.get(Folder, uid)
if not folder: if not folder:
return FolderManager.NO_SUCH_FOLDER, None return FolderManager.NO_SUCH_FOLDER, None
return FolderManager.SUCCESS, folder return FolderManager.SUCCESS, folder
@staticmethod @staticmethod
def add(store, name, path): def add(store, name, path):
if not store.find(Folder, Folder.name == name, Folder.root == True).is_empty(): if not store.find(Folder, Folder.name == name, Folder.root == True).is_empty():
return FolderManager.NAME_EXISTS return FolderManager.NAME_EXISTS
path = unicode(os.path.abspath(path)) path = unicode(os.path.abspath(path))
if not os.path.isdir(path): if not os.path.isdir(path):
return FolderManager.INVALID_PATH return FolderManager.INVALID_PATH
if not store.find(Folder, Folder.path == path).is_empty(): if not store.find(Folder, Folder.path == path).is_empty():
return FolderManager.PATH_EXISTS return FolderManager.PATH_EXISTS
if any(path.startswith(p) for p in store.find(Folder).values(Folder.path)): if any(path.startswith(p) for p in store.find(Folder).values(Folder.path)):
return FolderManager.PATH_EXISTS return FolderManager.PATH_EXISTS
if not store.find(Folder, Folder.path.startswith(path)).is_empty(): if not store.find(Folder, Folder.path.startswith(path)).is_empty():
return FolderManager.SUBPATH_EXISTS return FolderManager.SUBPATH_EXISTS
folder = Folder() folder = Folder()
folder.root = True folder.root = True
folder.name = name folder.name = name
folder.path = path folder.path = path
store.add(folder) store.add(folder)
store.commit() store.commit()
return FolderManager.SUCCESS return FolderManager.SUCCESS
@staticmethod @staticmethod
def delete(store, uid): def delete(store, uid):
status, folder = FolderManager.get(store, uid) status, folder = FolderManager.get(store, uid)
if status != FolderManager.SUCCESS: if status != FolderManager.SUCCESS:
return status return status
if not folder.root: if not folder.root:
return FolderManager.NO_SUCH_FOLDER return FolderManager.NO_SUCH_FOLDER
scanner = Scanner(store) scanner = Scanner(store)
for track in store.find(Track, Track.root_folder_id == folder.id): for track in store.find(Track, Track.root_folder_id == folder.id):
scanner.remove_file(track.path) scanner.remove_file(track.path)
scanner.finish() scanner.finish()
store.find(StarredFolder, StarredFolder.starred_id == uid).remove() store.find(StarredFolder, StarredFolder.starred_id == uid).remove()
store.find(RatingFolder, RatingFolder.rated_id == uid).remove() store.find(RatingFolder, RatingFolder.rated_id == uid).remove()
store.remove(folder) store.remove(folder)
store.commit() store.commit()
return FolderManager.SUCCESS return FolderManager.SUCCESS
@staticmethod @staticmethod
def delete_by_name(store, name): def delete_by_name(store, name):
folder = store.find(Folder, Folder.name == name, Folder.root == True).one() folder = store.find(Folder, Folder.name == name, Folder.root == True).one()
if not folder: if not folder:
return FolderManager.NO_SUCH_FOLDER return FolderManager.NO_SUCH_FOLDER
return FolderManager.delete(store, folder.id) return FolderManager.delete(store, folder.id)
@staticmethod @staticmethod
def error_str(err): def error_str(err):
if err == FolderManager.SUCCESS: if err == FolderManager.SUCCESS:
return 'No error' return 'No error'
elif err == FolderManager.INVALID_ID: elif err == FolderManager.INVALID_ID:
return 'Invalid folder id' return 'Invalid folder id'
elif err == FolderManager.NAME_EXISTS: elif err == FolderManager.NAME_EXISTS:
return 'There is already a folder with that name. Please pick another one.' return 'There is already a folder with that name. Please pick another one.'
elif err == FolderManager.INVALID_PATH: elif err == FolderManager.INVALID_PATH:
return "The path doesn't exists or isn't a directory" return "The path doesn't exists or isn't a directory"
elif err == FolderManager.PATH_EXISTS: elif err == FolderManager.PATH_EXISTS:
return 'This path is already registered' return 'This path is already registered'
elif err == FolderManager.NO_SUCH_FOLDER: elif err == FolderManager.NO_SUCH_FOLDER:
return 'No such folder' return 'No such folder'
elif err == FolderManager.SUBPATH_EXISTS: elif err == FolderManager.SUBPATH_EXISTS:
return 'This path contains a folder that is already registered' return 'This path contains a folder that is already registered'
return 'Unknown error' return 'Unknown error'

View File

@ -32,288 +32,288 @@ from supysonic.db import RatingFolder, RatingTrack
# Hacking in support for a concatenation expression # Hacking in support for a concatenation expression
class Concat(ComparableExpr): class Concat(ComparableExpr):
__slots__ = ("left", "right", "db") __slots__ = ("left", "right", "db")
def __init__(self, left, right, db): def __init__(self, left, right, db):
self.left = left self.left = left
self.right = right self.right = right
self.db = db self.db = db
@compile.when(Concat) @compile.when(Concat)
def compile_concat(compile, concat, state): def compile_concat(compile, concat, state):
left = compile(concat.left, state) left = compile(concat.left, state)
right = compile(concat.right, state) right = compile(concat.right, state)
if concat.db in ('sqlite', 'postgres'): if concat.db in ('sqlite', 'postgres'):
statement = "%s||%s" statement = "%s||%s"
elif concat.db == 'mysql': elif concat.db == 'mysql':
statement = "CONCAT(%s, %s)" statement = "CONCAT(%s, %s)"
else: else:
raise NotSupportedError("Unspported database (%s)" % concat.db) raise NotSupportedError("Unspported database (%s)" % concat.db)
return statement % (left, right) return statement % (left, right)
class Scanner: class Scanner:
def __init__(self, store, force = False): def __init__(self, store, force = False):
self.__store = store self.__store = store
self.__force = force self.__force = force
self.__added_artists = 0 self.__added_artists = 0
self.__added_albums = 0 self.__added_albums = 0
self.__added_tracks = 0 self.__added_tracks = 0
self.__deleted_artists = 0 self.__deleted_artists = 0
self.__deleted_albums = 0 self.__deleted_albums = 0
self.__deleted_tracks = 0 self.__deleted_tracks = 0
extensions = config.get('base', 'scanner_extensions') extensions = config.get('base', 'scanner_extensions')
self.__extensions = map(str.lower, extensions.split()) if extensions else None self.__extensions = map(str.lower, extensions.split()) if extensions else None
self.__folders_to_check = set() self.__folders_to_check = set()
self.__artists_to_check = set() self.__artists_to_check = set()
self.__albums_to_check = set() self.__albums_to_check = set()
def __del__(self): def __del__(self):
if self.__folders_to_check or self.__artists_to_check or self.__albums_to_check: if self.__folders_to_check or self.__artists_to_check or self.__albums_to_check:
raise Exception("There's still something to check. Did you run Scanner.finish()?") raise Exception("There's still something to check. Did you run Scanner.finish()?")
def scan(self, folder, progress_callback = None): def scan(self, folder, progress_callback = None):
# Scan new/updated files # Scan new/updated files
files = [ os.path.join(root, f) for root, _, fs in os.walk(folder.path) for f in fs if self.__is_valid_path(os.path.join(root, f)) ] files = [ os.path.join(root, f) for root, _, fs in os.walk(folder.path) for f in fs if self.__is_valid_path(os.path.join(root, f)) ]
total = len(files) total = len(files)
current = 0 current = 0
for path in files: for path in files:
self.scan_file(path) self.scan_file(path)
current += 1 current += 1
if progress_callback: if progress_callback:
progress_callback(current, total) progress_callback(current, total)
# Remove files that have been deleted # Remove files that have been deleted
for track in [ t for t in self.__store.find(Track, Track.root_folder_id == folder.id) if not self.__is_valid_path(t.path) ]: for track in [ t for t in self.__store.find(Track, Track.root_folder_id == folder.id) if not self.__is_valid_path(t.path) ]:
self.remove_file(track.path) self.remove_file(track.path)
# Update cover art info # Update cover art info
folders = [ folder ] folders = [ folder ]
while folders: while folders:
f = folders.pop() f = folders.pop()
f.has_cover_art = os.path.isfile(os.path.join(f.path, 'cover.jpg')) f.has_cover_art = os.path.isfile(os.path.join(f.path, 'cover.jpg'))
folders += f.children folders += f.children
folder.last_scan = int(time.time()) folder.last_scan = int(time.time())
def finish(self): def finish(self):
for album in [ a for a in self.__albums_to_check if not a.tracks.count() ]: for album in [ a for a in self.__albums_to_check if not a.tracks.count() ]:
self.__store.find(StarredAlbum, StarredAlbum.starred_id == album.id).remove() self.__store.find(StarredAlbum, StarredAlbum.starred_id == album.id).remove()
self.__artists_to_check.add(album.artist) self.__artists_to_check.add(album.artist)
self.__store.remove(album) self.__store.remove(album)
self.__deleted_albums += 1 self.__deleted_albums += 1
self.__albums_to_check.clear() self.__albums_to_check.clear()
for artist in [ a for a in self.__artists_to_check if not a.albums.count() and not a.tracks.count() ]: for artist in [ a for a in self.__artists_to_check if not a.albums.count() and not a.tracks.count() ]:
self.__store.find(StarredArtist, StarredArtist.starred_id == artist.id).remove() self.__store.find(StarredArtist, StarredArtist.starred_id == artist.id).remove()
self.__store.remove(artist) self.__store.remove(artist)
self.__deleted_artists += 1 self.__deleted_artists += 1
self.__artists_to_check.clear() self.__artists_to_check.clear()
while self.__folders_to_check: while self.__folders_to_check:
folder = self.__folders_to_check.pop() folder = self.__folders_to_check.pop()
if folder.root: if folder.root:
continue continue
if not folder.tracks.count() and not folder.children.count(): if not folder.tracks.count() and not folder.children.count():
self.__store.find(StarredFolder, StarredFolder.starred_id == folder.id).remove() self.__store.find(StarredFolder, StarredFolder.starred_id == folder.id).remove()
self.__store.find(RatingFolder, RatingFolder.rated_id == folder.id).remove() self.__store.find(RatingFolder, RatingFolder.rated_id == folder.id).remove()
self.__folders_to_check.add(folder.parent) self.__folders_to_check.add(folder.parent)
self.__store.remove(folder) self.__store.remove(folder)
def __is_valid_path(self, path): def __is_valid_path(self, path):
if not os.path.exists(path): if not os.path.exists(path):
return False return False
if not self.__extensions: if not self.__extensions:
return True return True
return os.path.splitext(path)[1][1:].lower() in self.__extensions return os.path.splitext(path)[1][1:].lower() in self.__extensions
def scan_file(self, path): def scan_file(self, path):
tr = self.__store.find(Track, Track.path == path).one() tr = self.__store.find(Track, Track.path == path).one()
add = False add = False
if tr: if tr:
if not self.__force and not int(os.path.getmtime(path)) > tr.last_modification: if not self.__force and not int(os.path.getmtime(path)) > tr.last_modification:
return return
tag = self.__try_load_tag(path) tag = self.__try_load_tag(path)
if not tag: if not tag:
self.remove_file(path) self.remove_file(path)
return return
else: else:
tag = self.__try_load_tag(path) tag = self.__try_load_tag(path)
if not tag: if not tag:
return return
tr = Track() tr = Track()
tr.path = path tr.path = path
add = True add = True
artist = self.__try_read_tag(tag, 'artist', '') artist = self.__try_read_tag(tag, 'artist', '')
album = self.__try_read_tag(tag, 'album', '') album = self.__try_read_tag(tag, 'album', '')
albumartist = self.__try_read_tag(tag, 'albumartist', artist) albumartist = self.__try_read_tag(tag, 'albumartist', artist)
tr.disc = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x[0].split('/')[0])) tr.disc = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x[0].split('/')[0]))
tr.number = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x[0].split('/')[0])) tr.number = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x[0].split('/')[0]))
tr.title = self.__try_read_tag(tag, 'title', '') tr.title = self.__try_read_tag(tag, 'title', '')
tr.year = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0])) tr.year = self.__try_read_tag(tag, 'date', None, lambda x: int(x[0].split('-')[0]))
tr.genre = self.__try_read_tag(tag, 'genre') tr.genre = self.__try_read_tag(tag, 'genre')
tr.duration = int(tag.info.length) tr.duration = int(tag.info.length)
tr.bitrate = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) / 1000 tr.bitrate = (tag.info.bitrate if hasattr(tag.info, 'bitrate') else int(os.path.getsize(path) * 8 / tag.info.length)) / 1000
tr.content_type = config.get_mime(os.path.splitext(path)[1][1:]) tr.content_type = config.get_mime(os.path.splitext(path)[1][1:])
tr.last_modification = os.path.getmtime(path) tr.last_modification = os.path.getmtime(path)
tralbum = self.__find_album(albumartist, album) tralbum = self.__find_album(albumartist, album)
trartist = self.__find_artist(artist) trartist = self.__find_artist(artist)
if add: if add:
trroot = self.__find_root_folder(path) trroot = self.__find_root_folder(path)
trfolder = self.__find_folder(path) trfolder = self.__find_folder(path)
# Set the references at the very last as searching for them will cause the added track to be flushed, even if # Set the references at the very last as searching for them will cause the added track to be flushed, even if
# it is incomplete, causing not null constraints errors. # it is incomplete, causing not null constraints errors.
tr.album = tralbum tr.album = tralbum
tr.artist = trartist tr.artist = trartist
tr.folder = trfolder tr.folder = trfolder
tr.root_folder = trroot tr.root_folder = trroot
self.__store.add(tr) self.__store.add(tr)
self.__added_tracks += 1 self.__added_tracks += 1
else: else:
if tr.album.id != tralbum.id: if tr.album.id != tralbum.id:
self.__albums_to_check.add(tr.album) self.__albums_to_check.add(tr.album)
tr.album = tralbum tr.album = tralbum
if tr.artist.id != trartist.id: if tr.artist.id != trartist.id:
self.__artists_to_check.add(tr.artist) self.__artists_to_check.add(tr.artist)
tr.artist = trartist tr.artist = trartist
def remove_file(self, path): def remove_file(self, path):
tr = self.__store.find(Track, Track.path == path).one() tr = self.__store.find(Track, Track.path == path).one()
if not tr: if not tr:
return return
self.__store.find(StarredTrack, StarredTrack.starred_id == tr.id).remove() self.__store.find(StarredTrack, StarredTrack.starred_id == tr.id).remove()
self.__store.find(RatingTrack, RatingTrack.rated_id == tr.id).remove() self.__store.find(RatingTrack, RatingTrack.rated_id == tr.id).remove()
# Playlist autofix themselves # Playlist autofix themselves
self.__store.find(User, User.last_play_id == tr.id).set(last_play_id = None) self.__store.find(User, User.last_play_id == tr.id).set(last_play_id = None)
self.__folders_to_check.add(tr.folder) self.__folders_to_check.add(tr.folder)
self.__albums_to_check.add(tr.album) self.__albums_to_check.add(tr.album)
self.__artists_to_check.add(tr.artist) self.__artists_to_check.add(tr.artist)
self.__store.remove(tr) self.__store.remove(tr)
self.__deleted_tracks += 1 self.__deleted_tracks += 1
def move_file(self, src_path, dst_path): def move_file(self, src_path, dst_path):
tr = self.__store.find(Track, Track.path == src_path).one() tr = self.__store.find(Track, Track.path == src_path).one()
if not tr: if not tr:
return return
self.__folders_to_check.add(tr.folder) self.__folders_to_check.add(tr.folder)
tr_dst = self.__store.find(Track, Track.path == dst_path).one() tr_dst = self.__store.find(Track, Track.path == dst_path).one()
if tr_dst: if tr_dst:
tr.root_folder = tr_dst.root_folder tr.root_folder = tr_dst.root_folder
tr.folder = tr_dst.folder tr.folder = tr_dst.folder
self.remove_file(dst_path) self.remove_file(dst_path)
else: else:
root = self.__find_root_folder(dst_path) root = self.__find_root_folder(dst_path)
folder = self.__find_folder(dst_path) folder = self.__find_folder(dst_path)
tr.root_folder = root tr.root_folder = root
tr.folder = folder tr.folder = folder
tr.path = dst_path tr.path = dst_path
def __find_album(self, artist, album): def __find_album(self, artist, album):
ar = self.__find_artist(artist) ar = self.__find_artist(artist)
al = ar.albums.find(name = album).one() al = ar.albums.find(name = album).one()
if al: if al:
return al return al
al = Album() al = Album()
al.name = album al.name = album
al.artist = ar al.artist = ar
self.__store.add(al) self.__store.add(al)
self.__added_albums += 1 self.__added_albums += 1
return al return al
def __find_artist(self, artist): def __find_artist(self, artist):
ar = self.__store.find(Artist, Artist.name == artist).one() ar = self.__store.find(Artist, Artist.name == artist).one()
if ar: if ar:
return ar return ar
ar = Artist() ar = Artist()
ar.name = artist ar.name = artist
self.__store.add(ar) self.__store.add(ar)
self.__added_artists += 1 self.__added_artists += 1
return ar return ar
def __find_root_folder(self, path): def __find_root_folder(self, path):
path = os.path.dirname(path) path = os.path.dirname(path)
db = self.__store.get_database().__module__[len('storm.databases.'):] db = self.__store.get_database().__module__[len('storm.databases.'):]
folders = self.__store.find(Folder, Like(path, Concat(Folder.path, u'%', db)), Folder.root == True) folders = self.__store.find(Folder, Like(path, Concat(Folder.path, u'%', db)), Folder.root == True)
count = folders.count() count = folders.count()
if count > 1: if count > 1:
raise Exception("Found multiple root folders for '{}'.".format(path)) raise Exception("Found multiple root folders for '{}'.".format(path))
elif count == 0: elif count == 0:
raise Exception("Couldn't find the root folder for '{}'.\nDon't scan files that aren't located in a defined music folder".format(path)) raise Exception("Couldn't find the root folder for '{}'.\nDon't scan files that aren't located in a defined music folder".format(path))
return folders.one() return folders.one()
def __find_folder(self, path): def __find_folder(self, path):
path = os.path.dirname(path) path = os.path.dirname(path)
folders = self.__store.find(Folder, Folder.path == path) folders = self.__store.find(Folder, Folder.path == path)
count = folders.count() count = folders.count()
if count > 1: if count > 1:
raise Exception("Found multiple folders for '{}'.".format(path)) raise Exception("Found multiple folders for '{}'.".format(path))
elif count == 1: elif count == 1:
return folders.one() return folders.one()
db = self.__store.get_database().__module__[len('storm.databases.'):] db = self.__store.get_database().__module__[len('storm.databases.'):]
folder = self.__store.find(Folder, Like(path, Concat(Folder.path, os.sep + u'%', db))).order_by(Folder.path).last() folder = self.__store.find(Folder, Like(path, Concat(Folder.path, os.sep + u'%', db))).order_by(Folder.path).last()
full_path = folder.path full_path = folder.path
path = path[len(folder.path) + 1:] path = path[len(folder.path) + 1:]
for name in path.split(os.sep): for name in path.split(os.sep):
full_path = os.path.join(full_path, name) full_path = os.path.join(full_path, name)
fold = Folder() fold = Folder()
fold.root = False fold.root = False
fold.name = name fold.name = name
fold.path = full_path fold.path = full_path
fold.parent = folder fold.parent = folder
self.__store.add(fold) self.__store.add(fold)
folder = fold folder = fold
return folder return folder
def __try_load_tag(self, path): def __try_load_tag(self, path):
try: try:
return mutagen.File(path, easy = True) return mutagen.File(path, easy = True)
except: except:
return None return None
def __try_read_tag(self, metadata, field, default = None, transform = lambda x: x[0]): def __try_read_tag(self, metadata, field, default = None, transform = lambda x: x[0]):
try: try:
value = metadata[field] value = metadata[field]
if not value: if not value:
return default return default
if transform: if transform:
value = transform(value) value = transform(value)
return value if value else default return value if value else default
except: except:
return default return default
def stats(self): def stats(self):
return (self.__added_artists, self.__added_albums, self.__added_tracks), (self.__deleted_artists, self.__deleted_albums, self.__deleted_tracks) return (self.__added_artists, self.__added_albums, self.__added_tracks), (self.__deleted_artists, self.__deleted_albums, self.__deleted_tracks)