From 0de87e64b02e914bbe360f546d2ed0dd8d718ebb Mon Sep 17 00:00:00 2001 From: spl0k Date: Sun, 28 Jan 2018 22:50:21 +0100 Subject: [PATCH] API as blueprint Ref #76 --- supysonic/api/__init__.py | 27 +++++++++------------------ supysonic/api/albums_songs.py | 15 ++++++++------- supysonic/api/annotation.py | 14 +++++++------- supysonic/api/browse.py | 20 ++++++++++---------- supysonic/api/chat.py | 7 ++++--- supysonic/api/media.py | 31 ++++++++++++++++--------------- supysonic/api/playlists.py | 14 +++++++------- supysonic/api/search.py | 9 +++++---- supysonic/api/system.py | 7 ++++--- supysonic/api/user.py | 14 +++++++------- supysonic/web.py | 3 ++- 11 files changed, 79 insertions(+), 82 deletions(-) diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py index 7f11491..56f6fb1 100644 --- a/supysonic/api/__init__.py +++ b/supysonic/api/__init__.py @@ -24,7 +24,7 @@ import binascii import uuid from flask import request -from flask import current_app as app +from flask import Blueprint from pony.orm import db_session, ObjectNotFound from ..managers.user import UserManager @@ -33,11 +33,10 @@ from ..py23 import dict from .formatters import make_json_response, make_jsonp_response, make_xml_response from .formatters import make_error_response_func -@app.before_request -def set_formatter(): - if not request.path.startswith('/rest/'): - return +api = Blueprint('api', __name__) +@api.before_request +def set_formatter(): """Return a function to create the response.""" f, callback = map(request.values.get, ['f', 'callback']) if f == 'jsonp': @@ -58,11 +57,8 @@ def decode_password(password): except: return password -@app.before_request +@api.before_request def authorize(): - if not request.path.startswith('/rest/'): - return - error = request.error_formatter(40, 'Unauthorized'), 401 if request.authorization: @@ -84,11 +80,8 @@ def authorize(): request.username = username request.user = user -@app.before_request +@api.before_request def get_client_prefs(): - if not request.path.startswith('/rest/'): - return - if 'c' not in request.values: return request.error_formatter(10, 'Missing required parameter') @@ -101,11 +94,9 @@ def get_client_prefs(): request.client = client -@app.errorhandler(404) -def not_found(error): - if not request.path.startswith('/rest/'): - return error - +#@api.errorhandler(404) +@api.route('/', methods = [ 'GET', 'POST' ]) # blueprint 404 workaround +def not_found(*args, **kwargs): return request.error_formatter(0, 'Not implemented'), 501 def get_entity(req, cls, param = 'id'): diff --git a/supysonic/api/albums_songs.py b/supysonic/api/albums_songs.py index e54b5f6..1b9ecb9 100644 --- a/supysonic/api/albums_songs.py +++ b/supysonic/api/albums_songs.py @@ -22,14 +22,15 @@ import random import uuid from datetime import timedelta -from flask import request, current_app as app +from flask import request from pony.orm import db_session, select, desc, avg, max, min, count from ..db import Folder, Artist, Album, Track, RatingFolder, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, User from ..db import now from ..py23 import dict +from . import api -@app.route('/rest/getRandomSongs.view', methods = [ 'GET', 'POST' ]) +@api.route('/getRandomSongs.view', methods = [ 'GET', 'POST' ]) def rand_songs(): size = request.values.get('size', '10') genre, fromYear, toYear, musicFolderId = map(request.values.get, [ 'genre', 'fromYear', 'toYear', 'musicFolderId' ]) @@ -63,7 +64,7 @@ def rand_songs(): ) )) -@app.route('/rest/getAlbumList.view', methods = [ 'GET', 'POST' ]) +@api.route('/getAlbumList.view', methods = [ 'GET', 'POST' ]) def album_list(): ltype, size, offset = map(request.values.get, [ 'type', 'size', 'offset' ]) if not ltype: @@ -106,7 +107,7 @@ def album_list(): ) )) -@app.route('/rest/getAlbumList2.view', methods = [ 'GET', 'POST' ]) +@api.route('/getAlbumList2.view', methods = [ 'GET', 'POST' ]) def album_list_id3(): ltype, size, offset = map(request.values.get, [ 'type', 'size', 'offset' ]) if not ltype: @@ -147,7 +148,7 @@ def album_list_id3(): ) )) -@app.route('/rest/getNowPlaying.view', methods = [ 'GET', 'POST' ]) +@api.route('/getNowPlaying.view', methods = [ 'GET', 'POST' ]) @db_session def now_playing(): query = User.select(lambda u: u.last_play is not None and u.last_play_date + timedelta(minutes = 3) > now()) @@ -161,7 +162,7 @@ def now_playing(): ) )) -@app.route('/rest/getStarred.view', methods = [ 'GET', 'POST' ]) +@api.route('/getStarred.view', methods = [ 'GET', 'POST' ]) @db_session def get_starred(): folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id) @@ -174,7 +175,7 @@ def get_starred(): ) )) -@app.route('/rest/getStarred2.view', methods = [ 'GET', 'POST' ]) +@api.route('/getStarred2.view', methods = [ 'GET', 'POST' ]) @db_session def get_starred_id3(): return request.formatter(dict( diff --git a/supysonic/api/annotation.py b/supysonic/api/annotation.py index d6ebc9c..be3b65e 100644 --- a/supysonic/api/annotation.py +++ b/supysonic/api/annotation.py @@ -21,7 +21,7 @@ import time import uuid -from flask import request, current_app as app +from flask import current_app, request from pony.orm import db_session, delete from pony.orm import ObjectNotFound @@ -31,7 +31,7 @@ from ..db import RatingTrack, RatingFolder from ..lastfm import LastFm from ..py23 import dict -from . import get_entity +from . import api, get_entity @db_session def try_star(cls, starred_cls, eid): @@ -88,7 +88,7 @@ def merge_errors(errors): return error -@app.route('/rest/star.view', methods = [ 'GET', 'POST' ]) +@api.route('/star.view', methods = [ 'GET', 'POST' ]) def star(): id, albumId, artistId = map(request.values.getlist, [ 'id', 'albumId', 'artistId' ]) @@ -111,7 +111,7 @@ def star(): error = merge_errors(errors) return request.formatter(dict(error = error), error = True) if error else request.formatter(dict()) -@app.route('/rest/unstar.view', methods = [ 'GET', 'POST' ]) +@api.route('/unstar.view', methods = [ 'GET', 'POST' ]) def unstar(): id, albumId, artistId = map(request.values.getlist, [ 'id', 'albumId', 'artistId' ]) @@ -134,7 +134,7 @@ def unstar(): error = merge_errors(errors) return request.formatter(dict(error = error), error = True) if error else request.formatter(dict()) -@app.route('/rest/setRating.view', methods = [ 'GET', 'POST' ]) +@api.route('/setRating.view', methods = [ 'GET', 'POST' ]) def rate(): id, rating = map(request.values.get, [ 'id', 'rating' ]) if not id or not rating: @@ -172,7 +172,7 @@ def rate(): return request.formatter(dict()) -@app.route('/rest/scrobble.view', methods = [ 'GET', 'POST' ]) +@api.route('/scrobble.view', methods = [ 'GET', 'POST' ]) @db_session def scrobble(): status, res = get_entity(request, Track) @@ -189,7 +189,7 @@ def scrobble(): else: t = int(time.time()) - lfm = LastFm(app.config['LASTFM'], User[request.user.id], app.logger) + lfm = LastFm(current_app.config['LASTFM'], User[request.user.id], current_app.logger) if submission in (None, '', True, 'true', 'True', 1, '1'): lfm.scrobble(res, t) diff --git a/supysonic/api/browse.py b/supysonic/api/browse.py index 150ef43..354d42e 100644 --- a/supysonic/api/browse.py +++ b/supysonic/api/browse.py @@ -21,16 +21,16 @@ import string import uuid -from flask import request, current_app as app +from flask import request from pony.orm import db_session from pony.orm import ObjectNotFound from ..db import Folder, Artist, Album, Track from ..py23 import dict -from . import get_entity +from . import api, get_entity -@app.route('/rest/getMusicFolders.view', methods = [ 'GET', 'POST' ]) +@api.route('/getMusicFolders.view', methods = [ 'GET', 'POST' ]) @db_session def list_folders(): return request.formatter(dict( @@ -42,7 +42,7 @@ def list_folders(): ) )) -@app.route('/rest/getIndexes.view', methods = [ 'GET', 'POST' ]) +@api.route('/getIndexes.view', methods = [ 'GET', 'POST' ]) @db_session def list_indexes(): musicFolderId = request.values.get('musicFolderId') @@ -105,7 +105,7 @@ def list_indexes(): ) )) -@app.route('/rest/getMusicDirectory.view', methods = [ 'GET', 'POST' ]) +@api.route('/getMusicDirectory.view', methods = [ 'GET', 'POST' ]) @db_session def show_directory(): status, res = get_entity(request, Folder) @@ -122,7 +122,7 @@ def show_directory(): return request.formatter(dict(directory = directory)) -@app.route('/rest/getArtists.view', methods = [ 'GET', 'POST' ]) +@api.route('/getArtists.view', methods = [ 'GET', 'POST' ]) @db_session def list_artists(): # According to the API page, there are no parameters? @@ -148,7 +148,7 @@ def list_artists(): ) )) -@app.route('/rest/getArtist.view', methods = [ 'GET', 'POST' ]) +@api.route('/getArtist.view', methods = [ 'GET', 'POST' ]) @db_session def artist_info(): status, res = get_entity(request, Artist) @@ -162,7 +162,7 @@ def artist_info(): return request.formatter(dict(artist = info)) -@app.route('/rest/getAlbum.view', methods = [ 'GET', 'POST' ]) +@api.route('/getAlbum.view', methods = [ 'GET', 'POST' ]) @db_session def album_info(): status, res = get_entity(request, Album) @@ -174,7 +174,7 @@ def album_info(): return request.formatter(dict(album = info)) -@app.route('/rest/getSong.view', methods = [ 'GET', 'POST' ]) +@api.route('/getSong.view', methods = [ 'GET', 'POST' ]) @db_session def track_info(): status, res = get_entity(request, Track) @@ -183,7 +183,7 @@ def track_info(): return request.formatter(dict(song = res.as_subsonic_child(request.user, request.client))) -@app.route('/rest/getVideos.view', methods = [ 'GET', 'POST' ]) +@api.route('/getVideos.view', methods = [ 'GET', 'POST' ]) def list_videos(): return request.error_formatter(0, 'Video streaming not supported') diff --git a/supysonic/api/chat.py b/supysonic/api/chat.py index 8e1421a..b2349e8 100644 --- a/supysonic/api/chat.py +++ b/supysonic/api/chat.py @@ -18,13 +18,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from flask import request, current_app as app +from flask import request from pony.orm import db_session from ..db import ChatMessage, User from ..py23 import dict +from . import api -@app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ]) +@api.route('/getChatMessages.view', methods = [ 'GET', 'POST' ]) def get_chat(): since = request.values.get('since') try: @@ -39,7 +40,7 @@ def get_chat(): return request.formatter(dict(chatMessages = dict(chatMessage = [ msg.responsize() for msg in query ] ))) -@app.route('/rest/addChatMessage.view', methods = [ 'GET', 'POST' ]) +@api.route('/addChatMessage.view', methods = [ 'GET', 'POST' ]) def add_chat_message(): msg = request.values.get('message') if not msg: diff --git a/supysonic/api/media.py b/supysonic/api/media.py index 3203c53..72c5284 100644 --- a/supysonic/api/media.py +++ b/supysonic/api/media.py @@ -24,7 +24,8 @@ import os.path import requests import subprocess -from flask import request, send_file, Response, current_app as app +from flask import request, Response, send_file +from flask import current_app from PIL import Image from pony.orm import db_session from xml.etree import ElementTree @@ -33,7 +34,7 @@ from .. import scanner from ..db import Track, Album, Artist, Folder, User, ClientPrefs, now from ..py23 import dict -from . import get_entity +from . import api, get_entity def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate): if not base_cmdline: @@ -45,7 +46,7 @@ def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_f ] return ret -@app.route('/rest/stream.view', methods = [ 'GET', 'POST' ]) +@api.route('/stream.view', methods = [ 'GET', 'POST' ]) @db_session def stream_media(): status, res = get_entity(request, Track) @@ -81,7 +82,7 @@ def stream_media(): dst_mimetype = mimetypes.guess_type('dummyname.' + dst_suffix, False)[0] or 'application/octet-stream' if format != 'raw' and (dst_suffix != src_suffix or dst_bitrate != res.bitrate): - config = app.config['TRANSCODING'] + config = current_app.config['TRANSCODING'] transcoder = config.get('transcoder_{}_{}'.format(src_suffix, dst_suffix)) decoder = config.get('decoder_' + src_suffix) or config.get('decoder') encoder = config.get('encoder_' + dst_suffix) or config.get('encoder') @@ -89,7 +90,7 @@ def stream_media(): transcoder = config.get('transcoder') if not transcoder: message = 'No way to transcode from {} to {}'.format(src_suffix, dst_suffix) - app.logger.info(message) + current_app.logger.info(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 ]) @@ -119,7 +120,7 @@ def stream_media(): dec_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)) + current_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) else: response = send_file(res.path, mimetype = dst_mimetype, conditional=True) @@ -132,7 +133,7 @@ def stream_media(): return response -@app.route('/rest/download.view', methods = [ 'GET', 'POST' ]) +@api.route('/download.view', methods = [ 'GET', 'POST' ]) def download_media(): with db_session: status, res = get_entity(request, Track) @@ -141,7 +142,7 @@ def download_media(): return send_file(res.path, mimetype = res.content_type, conditional=True) -@app.route('/rest/getCoverArt.view', methods = [ 'GET', 'POST' ]) +@api.route('/getCoverArt.view', methods = [ 'GET', 'POST' ]) def cover_art(): with db_session: status, res = get_entity(request, Folder) @@ -164,7 +165,7 @@ def cover_art(): if size > im.size[0] and size > im.size[1]: return send_file(os.path.join(res.path, 'cover.jpg')) - size_path = os.path.join(app.config['WEBAPP']['cache_dir'], str(size)) + size_path = os.path.join(current_app.config['WEBAPP']['cache_dir'], str(size)) path = os.path.abspath(os.path.join(size_path, str(res.id))) if os.path.exists(path): return send_file(path, mimetype = 'image/jpeg') @@ -175,7 +176,7 @@ def cover_art(): im.save(path, 'JPEG') return send_file(path, mimetype = 'image/jpeg') -@app.route('/rest/getLyrics.view', methods = [ 'GET', 'POST' ]) +@api.route('/getLyrics.view', methods = [ 'GET', 'POST' ]) def lyrics(): artist, title = map(request.values.get, [ 'artist', 'title' ]) if not artist: @@ -188,14 +189,14 @@ def lyrics(): for track in query: lyrics_path = os.path.splitext(track.path)[0] + '.txt' if os.path.exists(lyrics_path): - app.logger.debug('Found lyrics file: ' + lyrics_path) + current_app.logger.debug('Found lyrics file: ' + lyrics_path) try: lyrics = read_file_as_unicode(lyrics_path) except UnicodeError: # Lyrics file couldn't be decoded. Rather than displaying an error, try with the potential next files or # return no lyrics. Log it anyway. - app.logger.warning('Unsupported encoding for lyrics file ' + lyrics_path) + current_app.logger.warning('Unsupported encoding for lyrics file ' + lyrics_path) continue return request.formatter(dict(lyrics = dict( @@ -216,7 +217,7 @@ def lyrics(): _value_ = root.find('cl:Lyric', namespaces = ns).text ))) except requests.exceptions.RequestException as e: - app.logger.warning('Error while requesting the ChartLyrics API: ' + str(e)) + current_app.logger.warning('Error while requesting the ChartLyrics API: ' + str(e)) return request.formatter(dict(lyrics = dict())) @@ -228,13 +229,13 @@ def read_file_as_unicode(path): for enc in encodings: try: contents = codecs.open(path, 'r', encoding = enc).read() - app.logger.debug('Read file {} with {} encoding'.format(path, enc)) + current_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 return contents except UnicodeError: pass # Fallback to ASCII - app.logger.debug('Reading file {} with ascii encoding'.format(path)) + current_app.logger.debug('Reading file {} with ascii encoding'.format(path)) return unicode(open(path, 'r').read()) diff --git a/supysonic/api/playlists.py b/supysonic/api/playlists.py index 4d2fee6..a3ed19b 100644 --- a/supysonic/api/playlists.py +++ b/supysonic/api/playlists.py @@ -20,16 +20,16 @@ import uuid -from flask import request, current_app as app +from flask import request from pony.orm import db_session, rollback from pony.orm import ObjectNotFound from ..db import Playlist, User, Track from ..py23 import dict -from . import get_entity +from . import api, get_entity -@app.route('/rest/getPlaylists.view', methods = [ 'GET', 'POST' ]) +@api.route('/getPlaylists.view', methods = [ 'GET', 'POST' ]) def list_playlists(): query = Playlist.select(lambda p: p.user.id == request.user.id or p.public).order_by(Playlist.name) @@ -48,7 +48,7 @@ def list_playlists(): with db_session: return request.formatter(dict(playlists = dict(playlist = [ p.as_subsonic_playlist(request.user) for p in query ] ))) -@app.route('/rest/getPlaylist.view', methods = [ 'GET', 'POST' ]) +@api.route('/getPlaylist.view', methods = [ 'GET', 'POST' ]) @db_session def show_playlist(): status, res = get_entity(request, Playlist) @@ -62,7 +62,7 @@ def show_playlist(): info['entry'] = [ t.as_subsonic_child(request.user, request.client) for t in res.get_tracks() ] return request.formatter(dict(playlist = info)) -@app.route('/rest/createPlaylist.view', methods = [ 'GET', 'POST' ]) +@api.route('/createPlaylist.view', methods = [ 'GET', 'POST' ]) @db_session def create_playlist(): playlist_id, name = map(request.values.get, [ 'playlistId', 'name' ]) @@ -104,7 +104,7 @@ def create_playlist(): return request.formatter(dict()) -@app.route('/rest/deletePlaylist.view', methods = [ 'GET', 'POST' ]) +@api.route('/deletePlaylist.view', methods = [ 'GET', 'POST' ]) @db_session def delete_playlist(): status, res = get_entity(request, Playlist) @@ -117,7 +117,7 @@ def delete_playlist(): res.delete() return request.formatter(dict()) -@app.route('/rest/updatePlaylist.view', methods = [ 'GET', 'POST' ]) +@api.route('/updatePlaylist.view', methods = [ 'GET', 'POST' ]) @db_session def update_playlist(): status, res = get_entity(request, Playlist, 'playlistId') diff --git a/supysonic/api/search.py b/supysonic/api/search.py index 9366a85..afd4ada 100644 --- a/supysonic/api/search.py +++ b/supysonic/api/search.py @@ -20,13 +20,14 @@ from collections import OrderedDict from datetime import datetime -from flask import request, current_app as app +from flask import request from pony.orm import db_session, select from ..db import Folder, Track, Artist, Album from ..py23 import dict +from . import api -@app.route('/rest/search.view', methods = [ 'GET', 'POST' ]) +@api.route('/search.view', methods = [ 'GET', 'POST' ]) def old_search(): artist, album, title, anyf, count, offset, newer_than = map(request.values.get, [ 'artist', 'album', 'title', 'any', 'count', 'offset', 'newerThan' ]) try: @@ -70,7 +71,7 @@ def old_search(): match = [ r.as_subsonic_child(request.user) if isinstance(r, Folder) else r.as_subsonic_child(request.user, request.client) for r in query[offset : offset + count] ] ))) -@app.route('/rest/search2.view', methods = [ 'GET', 'POST' ]) +@api.route('/search2.view', methods = [ 'GET', 'POST' ]) def new_search(): query, artist_count, artist_offset, album_count, album_offset, song_count, song_offset = map( request.values.get, [ 'query', 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ]) @@ -99,7 +100,7 @@ def new_search(): ('song', [ t.as_subsonic_child(request.user, request.client) for t in songs ]) )))) -@app.route('/rest/search3.view', methods = [ 'GET', 'POST' ]) +@api.route('/search3.view', methods = [ 'GET', 'POST' ]) def search_id3(): query, artist_count, artist_offset, album_count, album_offset, song_count, song_offset = map( request.values.get, [ 'query', 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ]) diff --git a/supysonic/api/system.py b/supysonic/api/system.py index a9d71e5..3d9b2de 100644 --- a/supysonic/api/system.py +++ b/supysonic/api/system.py @@ -18,15 +18,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from flask import request, current_app as app +from flask import request from ..py23 import dict +from . import api -@app.route('/rest/ping.view', methods = [ 'GET', 'POST' ]) +@api.route('/ping.view', methods = [ 'GET', 'POST' ]) def ping(): return request.formatter(dict()) -@app.route('/rest/getLicense.view', methods = [ 'GET', 'POST' ]) +@api.route('/getLicense.view', methods = [ 'GET', 'POST' ]) def license(): return request.formatter(dict(license = dict(valid = True ))) diff --git a/supysonic/api/user.py b/supysonic/api/user.py index e9ba26b..b1498a9 100644 --- a/supysonic/api/user.py +++ b/supysonic/api/user.py @@ -18,16 +18,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from flask import request, current_app as app +from flask import request from pony.orm import db_session from ..db import User from ..managers.user import UserManager from ..py23 import dict -from . import decode_password +from . import api, decode_password -@app.route('/rest/getUser.view', methods = [ 'GET', 'POST' ]) +@api.route('/getUser.view', methods = [ 'GET', 'POST' ]) def user_info(): username = request.values.get('username') if username is None: @@ -43,7 +43,7 @@ def user_info(): return request.formatter(dict(user = user.as_subsonic_user())) -@app.route('/rest/getUsers.view', methods = [ 'GET', 'POST' ]) +@api.route('/getUsers.view', methods = [ 'GET', 'POST' ]) def users_info(): if not request.user.admin: return request.error_formatter(50, 'Admin restricted') @@ -51,7 +51,7 @@ def users_info(): with db_session: return request.formatter(dict(users = dict(user = [ u.as_subsonic_user() for u in User.select() ] ))) -@app.route('/rest/createUser.view', methods = [ 'GET', 'POST' ]) +@api.route('/createUser.view', methods = [ 'GET', 'POST' ]) def user_add(): if not request.user.admin: return request.error_formatter(50, 'Admin restricted') @@ -68,7 +68,7 @@ def user_add(): return request.formatter(dict()) -@app.route('/rest/deleteUser.view', methods = [ 'GET', 'POST' ]) +@api.route('/deleteUser.view', methods = [ 'GET', 'POST' ]) def user_del(): if not request.user.admin: return request.error_formatter(50, 'Admin restricted') @@ -88,7 +88,7 @@ def user_del(): return request.formatter(dict()) -@app.route('/rest/changePassword.view', methods = [ 'GET', 'POST' ]) +@api.route('/changePassword.view', methods = [ 'GET', 'POST' ]) def user_changepass(): username, password = map(request.values.get, [ 'username', 'password' ]) if not username or not password: diff --git a/supysonic/web.py b/supysonic/web.py index bef1380..4676dcf 100644 --- a/supysonic/web.py +++ b/supysonic/web.py @@ -65,7 +65,8 @@ def create_application(config = None): if app.config['WEBAPP']['mount_webui']: from . import frontend if app.config['WEBAPP']['mount_api']: - from . import api + from .api import api + app.register_blueprint(api, url_prefix = '/rest') return app