diff --git a/bin/supysonic-cli b/bin/supysonic-cli
index 99ddd85..13df019 100755
--- a/bin/supysonic-cli
+++ b/bin/supysonic-cli
@@ -20,227 +20,230 @@
# along with this program. If not, see .
import sys, cmd, argparse, getpass, time
-from supysonic import config
+from supysonic.config import IniConfig
from supysonic.db import get_store, Folder, User
from supysonic.managers.folder import FolderManager
from supysonic.managers.user import UserManager
from supysonic.scanner import Scanner
class CLIParser(argparse.ArgumentParser):
- def error(self, message):
- self.print_usage(sys.stderr)
- raise RuntimeError(message)
+ def error(self, message):
+ self.print_usage(sys.stderr)
+ raise RuntimeError(message)
class CLI(cmd.Cmd):
- prompt = "supysonic> "
+ prompt = "supysonic> "
- def _make_do(self, command):
- def method(obj, line):
- try:
- args = getattr(obj, command + '_parser').parse_args(line.split())
- except RuntimeError, e:
- print >>sys.stderr, e
- return
+ def _make_do(self, command):
+ def method(obj, line):
+ try:
+ args = getattr(obj, command + '_parser').parse_args(line.split())
+ except RuntimeError, e:
+ print >>sys.stderr, e
+ return
- if hasattr(obj.__class__, command + '_subparsers'):
- try:
- func = getattr(obj, '{}_{}'.format(command, args.action))
- except AttributeError:
- return obj.default(line)
- return func(** { key: vars(args)[key] for key in vars(args) if key != 'action' })
- else:
- try:
- func = getattr(obj, command)
- except AttributeError:
- return obj.default(line)
- return func(**vars(args))
+ if hasattr(obj.__class__, command + '_subparsers'):
+ try:
+ func = getattr(obj, '{}_{}'.format(command, args.action))
+ except AttributeError:
+ return obj.default(line)
+ return func(** { key: vars(args)[key] for key in vars(args) if key != 'action' })
+ else:
+ try:
+ func = getattr(obj, command)
+ except AttributeError:
+ return obj.default(line)
+ return func(**vars(args))
- return method
+ return method
- def __init__(self, store):
- cmd.Cmd.__init__(self)
+ def __init__(self, config):
+ cmd.Cmd.__init__(self)
+ self.__config = config
- # Generate do_* and help_* methods
- for parser_name in filter(lambda attr: attr.endswith('_parser') and '_' not in attr[:-7], dir(self.__class__)):
- command = parser_name[:-7]
+ # Generate do_* and help_* methods
+ for parser_name in filter(lambda attr: attr.endswith('_parser') and '_' not in attr[:-7], dir(self.__class__)):
+ command = parser_name[:-7]
- if not hasattr(self.__class__, 'do_' + command):
- setattr(self.__class__, 'do_' + command, self._make_do(command))
+ if not hasattr(self.__class__, 'do_' + command):
+ setattr(self.__class__, 'do_' + command, self._make_do(command))
- if hasattr(self.__class__, 'do_' + command) and not hasattr(self.__class__, 'help_' + command):
- setattr(self.__class__, 'help_' + command, getattr(self.__class__, parser_name).print_help)
- if hasattr(self.__class__, command + '_subparsers'):
- for action, subparser in getattr(self.__class__, command + '_subparsers').choices.iteritems():
- setattr(self, 'help_{} {}'.format(command, action), subparser.print_help)
+ if hasattr(self.__class__, 'do_' + command) and not hasattr(self.__class__, 'help_' + command):
+ setattr(self.__class__, 'help_' + command, getattr(self.__class__, parser_name).print_help)
+ if hasattr(self.__class__, command + '_subparsers'):
+ for action, subparser in getattr(self.__class__, command + '_subparsers').choices.iteritems():
+ setattr(self, 'help_{} {}'.format(command, action), subparser.print_help)
- self.__store = store
+ self.__store = get_store(config.BASE['database_uri'])
- def do_EOF(self, line):
- return True
+ def do_EOF(self, line):
+ return True
- do_exit = do_EOF
+ do_exit = do_EOF
- def default(self, line):
- print 'Unknown command %s' % line.split()[0]
- self.do_help(None)
+ def default(self, line):
+ print 'Unknown command %s' % line.split()[0]
+ self.do_help(None)
- def postloop(self):
- print
+ def postloop(self):
+ print
- def completedefault(self, text, line, begidx, endidx):
- command = line.split()[0]
- parsers = getattr(self.__class__, command + '_subparsers', None)
- if not parsers:
- return []
+ def completedefault(self, text, line, begidx, endidx):
+ command = line.split()[0]
+ parsers = getattr(self.__class__, command + '_subparsers', None)
+ if not parsers:
+ return []
- num_words = len(line[len(command):begidx].split())
- if num_words == 0:
- return [ a for a in parsers.choices.keys() if a.startswith(text) ]
- return []
+ num_words = len(line[len(command):begidx].split())
+ if num_words == 0:
+ return [ a for a in parsers.choices.keys() if a.startswith(text) ]
+ return []
- folder_parser = CLIParser(prog = 'folder', add_help = False)
- folder_subparsers = folder_parser.add_subparsers(dest = 'action')
- folder_subparsers.add_parser('list', help = 'Lists folders', add_help = False)
- folder_add_parser = folder_subparsers.add_parser('add', help = 'Adds a folder', add_help = False)
- folder_add_parser.add_argument('name', help = 'Name of the folder to add')
- folder_add_parser.add_argument('path', help = 'Path to the directory pointed by the folder')
- folder_del_parser = folder_subparsers.add_parser('delete', help = 'Deletes a folder', add_help = False)
- folder_del_parser.add_argument('name', help = 'Name of the folder to delete')
- folder_scan_parser = folder_subparsers.add_parser('scan', help = 'Run a scan on specified folders', add_help = False)
- folder_scan_parser.add_argument('folders', metavar = 'folder', nargs = '*', help = 'Folder(s) to be scanned. If ommitted, all folders are scanned')
- folder_scan_parser.add_argument('-f', '--force', action = 'store_true', help = "Force scan of already know files even if they haven't changed")
+ folder_parser = CLIParser(prog = 'folder', add_help = False)
+ folder_subparsers = folder_parser.add_subparsers(dest = 'action')
+ folder_subparsers.add_parser('list', help = 'Lists folders', add_help = False)
+ folder_add_parser = folder_subparsers.add_parser('add', help = 'Adds a folder', add_help = False)
+ folder_add_parser.add_argument('name', help = 'Name of the folder to add')
+ folder_add_parser.add_argument('path', help = 'Path to the directory pointed by the folder')
+ folder_del_parser = folder_subparsers.add_parser('delete', help = 'Deletes a folder', add_help = False)
+ folder_del_parser.add_argument('name', help = 'Name of the folder to delete')
+ folder_scan_parser = folder_subparsers.add_parser('scan', help = 'Run a scan on specified folders', add_help = False)
+ folder_scan_parser.add_argument('folders', metavar = 'folder', nargs = '*', help = 'Folder(s) to be scanned. If ommitted, all folders are scanned')
+ folder_scan_parser.add_argument('-f', '--force', action = 'store_true', help = "Force scan of already know files even if they haven't changed")
- def folder_list(self):
- print 'Name\t\tPath\n----\t\t----'
- print '\n'.join('{0: <16}{1}'.format(f.name, f.path) for f in self.__store.find(Folder, Folder.root == True))
+ def folder_list(self):
+ print 'Name\t\tPath\n----\t\t----'
+ print '\n'.join('{0: <16}{1}'.format(f.name, f.path) for f in self.__store.find(Folder, Folder.root == True))
- def folder_add(self, name, path):
- ret = FolderManager.add(self.__store, name, path)
- if ret != FolderManager.SUCCESS:
- print FolderManager.error_str(ret)
- else:
- print "Folder '{}' added".format(name)
+ def folder_add(self, name, path):
+ ret = FolderManager.add(self.__store, name, path)
+ if ret != FolderManager.SUCCESS:
+ print FolderManager.error_str(ret)
+ else:
+ print "Folder '{}' added".format(name)
- def folder_delete(self, name):
- ret = FolderManager.delete_by_name(self.__store, name)
- if ret != FolderManager.SUCCESS:
- print FolderManager.error_str(ret)
- else:
- print "Deleted folder '{}'".format(name)
+ def folder_delete(self, name):
+ ret = FolderManager.delete_by_name(self.__store, name)
+ if ret != FolderManager.SUCCESS:
+ print FolderManager.error_str(ret)
+ else:
+ print "Deleted folder '{}'".format(name)
- def folder_scan(self, folders, force):
+ def folder_scan(self, folders, force):
- class TimedProgressDisplay:
- def __init__(self, name, interval = 5):
- self.__name = name
- self.__interval = interval
- self.__last_display = 0
- self.__last_len = 0
+ class TimedProgressDisplay:
+ def __init__(self, name, interval = 5):
+ self.__name = name
+ self.__interval = interval
+ self.__last_display = 0
+ self.__last_len = 0
- def __call__(self, scanned, total):
- if time.time() - self.__last_display > self.__interval or scanned == total:
- if not self.__last_len:
- sys.stdout.write("Scanning '{0}': ".format(self.__name))
+ def __call__(self, scanned, total):
+ if time.time() - self.__last_display > self.__interval or scanned == total:
+ if not self.__last_len:
+ sys.stdout.write("Scanning '{0}': ".format(self.__name))
- progress = "{0}% ({1}/{2})".format((scanned * 100) / total, scanned, total)
- sys.stdout.write('\b' * self.__last_len)
- sys.stdout.write(progress)
- sys.stdout.flush()
+ progress = "{0}% ({1}/{2})".format((scanned * 100) / total, scanned, total)
+ sys.stdout.write('\b' * self.__last_len)
+ sys.stdout.write(progress)
+ sys.stdout.flush()
- self.__last_len = len(progress)
- self.__last_display = time.time()
+ self.__last_len = len(progress)
+ self.__last_display = time.time()
- scanner = Scanner(self.__store, force)
- if folders:
- folders = map(lambda n: self.__store.find(Folder, Folder.name == n, Folder.root == True).one() or n, folders)
- if any(map(lambda f: isinstance(f, basestring), folders)):
- print "No such folder(s): " + ' '.join(f for f in folders if isinstance(f, basestring))
- for folder in filter(lambda f: isinstance(f, Folder), folders):
- scanner.scan(folder, TimedProgressDisplay(folder.name))
- else:
- for folder in self.__store.find(Folder, Folder.root == True):
- scanner.scan(folder, TimedProgressDisplay(folder.name))
+ extensions = self.__config.BASE['scanner_extensions']
+ if extensions:
+ extensions = extensions.split(' ')
+ scanner = Scanner(self.__store, force = force, extensions = extensions)
+ if folders:
+ folders = map(lambda n: self.__store.find(Folder, Folder.name == n, Folder.root == True).one() or n, folders)
+ if any(map(lambda f: isinstance(f, basestring), folders)):
+ print "No such folder(s): " + ' '.join(f for f in folders if isinstance(f, basestring))
+ for folder in filter(lambda f: isinstance(f, Folder), folders):
+ scanner.scan(folder, TimedProgressDisplay(folder.name))
+ else:
+ for folder in self.__store.find(Folder, Folder.root == True):
+ scanner.scan(folder, TimedProgressDisplay(folder.name))
- scanner.finish()
- added, deleted = scanner.stats()
- self.__store.commit()
+ scanner.finish()
+ added, deleted = scanner.stats()
+ self.__store.commit()
- print
- print "Scanning done"
- print 'Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2])
- print 'Deleted: %i artists, %i albums, %i tracks' % (deleted[0], deleted[1], deleted[2])
+ print
+ print "Scanning done"
+ print 'Added: %i artists, %i albums, %i tracks' % (added[0], added[1], added[2])
+ print 'Deleted: %i artists, %i albums, %i tracks' % (deleted[0], deleted[1], deleted[2])
- user_parser = CLIParser(prog = 'user', add_help = False)
- user_subparsers = user_parser.add_subparsers(dest = 'action')
- user_subparsers.add_parser('list', help = 'List users', add_help = False)
- user_add_parser = user_subparsers.add_parser('add', help = 'Adds a user', add_help = False)
- user_add_parser.add_argument('name', help = 'Name/login of the user to add')
- user_add_parser.add_argument('-a', '--admin', action = 'store_true', help = 'Give admin rights to the new user')
- user_add_parser.add_argument('-p', '--password', help = "Specifies the user's password")
- user_add_parser.add_argument('-e', '--email', default = '', help = "Sets the user's email address")
- user_del_parser = user_subparsers.add_parser('delete', help = 'Deletes a user', add_help = False)
- user_del_parser.add_argument('name', help = 'Name/login of the user to delete')
- user_admin_parser = user_subparsers.add_parser('setadmin', help = 'Enable/disable admin rights for a user', add_help = False)
- user_admin_parser.add_argument('name', help = 'Name/login of the user to grant/revoke admin rights')
- user_admin_parser.add_argument('--off', action = 'store_true', help = 'Revoke admin rights if present, grant them otherwise')
- user_pass_parser = user_subparsers.add_parser('changepass', help = "Changes a user's password", add_help = False)
- user_pass_parser.add_argument('name', help = 'Name/login of the user to which change the password')
- user_pass_parser.add_argument('password', nargs = '?', help = 'New password')
+ user_parser = CLIParser(prog = 'user', add_help = False)
+ user_subparsers = user_parser.add_subparsers(dest = 'action')
+ user_subparsers.add_parser('list', help = 'List users', add_help = False)
+ user_add_parser = user_subparsers.add_parser('add', help = 'Adds a user', add_help = False)
+ user_add_parser.add_argument('name', help = 'Name/login of the user to add')
+ user_add_parser.add_argument('-a', '--admin', action = 'store_true', help = 'Give admin rights to the new user')
+ user_add_parser.add_argument('-p', '--password', help = "Specifies the user's password")
+ user_add_parser.add_argument('-e', '--email', default = '', help = "Sets the user's email address")
+ user_del_parser = user_subparsers.add_parser('delete', help = 'Deletes a user', add_help = False)
+ user_del_parser.add_argument('name', help = 'Name/login of the user to delete')
+ user_admin_parser = user_subparsers.add_parser('setadmin', help = 'Enable/disable admin rights for a user', add_help = False)
+ user_admin_parser.add_argument('name', help = 'Name/login of the user to grant/revoke admin rights')
+ user_admin_parser.add_argument('--off', action = 'store_true', help = 'Revoke admin rights if present, grant them otherwise')
+ user_pass_parser = user_subparsers.add_parser('changepass', help = "Changes a user's password", add_help = False)
+ user_pass_parser.add_argument('name', help = 'Name/login of the user to which change the password')
+ user_pass_parser.add_argument('password', nargs = '?', help = 'New password')
- def user_list(self):
- print 'Name\t\tAdmin\tEmail\n----\t\t-----\t-----'
- print '\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in self.__store.find(User))
+ def user_list(self):
+ print 'Name\t\tAdmin\tEmail\n----\t\t-----\t-----'
+ print '\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in self.__store.find(User))
- def user_add(self, name, admin, password, email):
- if not password:
- password = getpass.getpass()
- confirm = getpass.getpass('Confirm password: ')
- if password != confirm:
- print >>sys.stderr, "Passwords don't match"
- return
- status = UserManager.add(self.__store, name, password, email, admin)
- if status != UserManager.SUCCESS:
- print >>sys.stderr, UserManager.error_str(status)
+ def user_add(self, name, admin, password, email):
+ if not password:
+ password = getpass.getpass()
+ confirm = getpass.getpass('Confirm password: ')
+ if password != confirm:
+ print >>sys.stderr, "Passwords don't match"
+ return
+ status = UserManager.add(self.__store, name, password, email, admin)
+ if status != UserManager.SUCCESS:
+ print >>sys.stderr, UserManager.error_str(status)
- def user_delete(self, name):
- user = self.__store.find(User, User.name == name).one()
- if not user:
- print >>sys.stderr, 'No such user'
- else:
- self.__store.remove(user)
- self.__store.commit()
- print "User '{}' deleted".format(name)
+ def user_delete(self, name):
+ user = self.__store.find(User, User.name == name).one()
+ if not user:
+ print >>sys.stderr, 'No such user'
+ else:
+ self.__store.remove(user)
+ self.__store.commit()
+ print "User '{}' deleted".format(name)
- def user_setadmin(self, name, off):
- user = self.__store.find(User, User.name == name).one()
- if not user:
- print >>sys.stderr, 'No such user'
- else:
- user.admin = not off
- self.__store.commit()
- print "{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name)
+ def user_setadmin(self, name, off):
+ user = self.__store.find(User, User.name == name).one()
+ if not user:
+ print >>sys.stderr, 'No such user'
+ else:
+ user.admin = not off
+ self.__store.commit()
+ print "{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name)
- def user_changepass(self, name, password):
- if not password:
- password = getpass.getpass()
- confirm = getpass.getpass('Confirm password: ')
- if password != confirm:
- print >>sys.stderr, "Passwords don't match"
- return
- status = UserManager.change_password2(self.__store, name, password)
- if status != UserManager.SUCCESS:
- print >>sys.stderr, UserManager.error_str(status)
- else:
- print "Successfully changed '{}' password".format(name)
+ def user_changepass(self, name, password):
+ if not password:
+ password = getpass.getpass()
+ confirm = getpass.getpass('Confirm password: ')
+ if password != confirm:
+ print >>sys.stderr, "Passwords don't match"
+ return
+ status = UserManager.change_password2(self.__store, name, password)
+ if status != UserManager.SUCCESS:
+ print >>sys.stderr, UserManager.error_str(status)
+ else:
+ print "Successfully changed '{}' password".format(name)
if __name__ == "__main__":
- if not config.check():
- sys.exit(1)
+ config = IniConfig.from_common_locations()
- cli = CLI(get_store(config.get('base', 'database_uri')))
- if len(sys.argv) > 1:
- cli.onecmd(' '.join(sys.argv[1:]))
- else:
- cli.cmdloop()
+ cli = CLI(config)
+ if len(sys.argv) > 1:
+ cli.onecmd(' '.join(sys.argv[1:]))
+ else:
+ cli.cmdloop()
diff --git a/bin/supysonic-watcher b/bin/supysonic-watcher
index f2b0856..c81de0e 100755
--- a/bin/supysonic-watcher
+++ b/bin/supysonic-watcher
@@ -19,9 +19,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from supysonic.config import IniConfig
from supysonic.watcher import SupysonicWatcher
if __name__ == "__main__":
- watcher = SupysonicWatcher()
+ config = IniConfig.from_common_locations()
+ watcher = SupysonicWatcher(config)
watcher.run()
diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py
index bc07943..0d603f2 100644
--- a/supysonic/api/__init__.py
+++ b/supysonic/api/__init__.py
@@ -18,14 +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
+from flask import request, current_app as app
from xml.etree import ElementTree
from xml.dom import minidom
import simplejson
import uuid
import binascii
-from supysonic.web import app, store
+from supysonic.web import store
from supysonic.managers.user import UserManager
@app.before_request
diff --git a/supysonic/api/albums_songs.py b/supysonic/api/albums_songs.py
index 5244d41..0c0dcf5 100644
--- a/supysonic/api/albums_songs.py
+++ b/supysonic/api/albums_songs.py
@@ -18,14 +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
+from flask import request, current_app as app
from storm.expr import Desc, Avg, Min, Max
from storm.info import ClassAlias
from datetime import timedelta
import random
import uuid
-from supysonic.web import app, store
+from supysonic.web import store
from supysonic.db import Folder, Artist, Album, Track, RatingFolder, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, User
from supysonic.db import now
diff --git a/supysonic/api/annotation.py b/supysonic/api/annotation.py
index b0d1436..9d84d82 100644
--- a/supysonic/api/annotation.py
+++ b/supysonic/api/annotation.py
@@ -20,8 +20,9 @@
import time
import uuid
-from flask import request
-from supysonic.web import app, store
+from flask import request, current_app as app
+
+from supysonic.web import store
from . import get_entity
from supysonic.lastfm import LastFm
from supysonic.db import Track, Album, Artist, Folder
@@ -187,7 +188,7 @@ def scrobble():
else:
t = int(time.time())
- lfm = LastFm(request.user, app.logger)
+ lfm = LastFm(app.config['LASTFM'], request.user, 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 53ba84e..138ee13 100644
--- a/supysonic/api/browse.py
+++ b/supysonic/api/browse.py
@@ -18,8 +18,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from flask import request
-from supysonic.web import app, store
+from flask import request, current_app as app
+from supysonic.web import store
from supysonic.db import Folder, Artist, Album, Track
from . import get_entity
import uuid, string
diff --git a/supysonic/api/chat.py b/supysonic/api/chat.py
index afb3e24..9384e72 100644
--- a/supysonic/api/chat.py
+++ b/supysonic/api/chat.py
@@ -18,8 +18,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from flask import request
-from supysonic.web import app, store
+from flask import request, current_app as app
+from supysonic.web import store
from supysonic.db import ChatMessage
@app.route('/rest/getChatMessages.view', methods = [ 'GET', 'POST' ])
diff --git a/supysonic/api/media.py b/supysonic/api/media.py
index 07d6922..b0d4dfe 100644
--- a/supysonic/api/media.py
+++ b/supysonic/api/media.py
@@ -18,16 +18,18 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from flask import request, send_file, Response
-import requests
-import os.path
-from PIL import Image
-import subprocess
import codecs
+import mimetypes
+import os.path
+import requests
+import subprocess
+
+from flask import request, send_file, Response, current_app as app
+from PIL import Image
from xml.etree import ElementTree
-from supysonic import config, scanner
-from supysonic.web import app, store
+from supysonic import scanner
+from supysonic.web import store
from supysonic.db import Track, Album, Artist, Folder, User, ClientPrefs, now
from . import get_entity
@@ -70,14 +72,15 @@ def stream_media():
if format and format != 'raw' and format != src_suffix:
dst_suffix = format
- dst_mimetype = config.get_mime(dst_suffix)
+ 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):
- transcoder = config.get('transcoding', 'transcoder_{}_{}'.format(src_suffix, dst_suffix))
- decoder = config.get('transcoding', 'decoder_' + src_suffix) or config.get('transcoding', 'decoder')
- encoder = config.get('transcoding', 'encoder_' + dst_suffix) or config.get('transcoding', 'encoder')
+ config = 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')
if not transcoder and (not decoder or not encoder):
- transcoder = config.get('transcoding', 'transcoder')
+ transcoder = config.get('transcoder')
if not transcoder:
message = 'No way to transcode from {} to {}'.format(src_suffix, dst_suffix)
app.logger.info(message)
@@ -153,7 +156,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(config.get('webapp', 'cache_dir'), str(size))
+ size_path = os.path.join(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)
diff --git a/supysonic/api/playlists.py b/supysonic/api/playlists.py
index 23cc5b5..fcb9935 100644
--- a/supysonic/api/playlists.py
+++ b/supysonic/api/playlists.py
@@ -18,10 +18,10 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from flask import request
+from flask import request, current_app as app
from storm.expr import Or
import uuid
-from supysonic.web import app, store
+from supysonic.web import store
from supysonic.db import Playlist, User, Track
from . import get_entity
diff --git a/supysonic/api/search.py b/supysonic/api/search.py
index e964ac3..9eb365e 100644
--- a/supysonic/api/search.py
+++ b/supysonic/api/search.py
@@ -19,9 +19,9 @@
# along with this program. If not, see .
from datetime import datetime
-from flask import request
+from flask import request, current_app as app
from storm.info import ClassAlias
-from supysonic.web import app, store
+from supysonic.web import store
from supysonic.db import Folder, Track, Artist, Album
@app.route('/rest/search.view', methods = [ 'GET', 'POST' ])
diff --git a/supysonic/api/system.py b/supysonic/api/system.py
index 228400f..fde270c 100644
--- a/supysonic/api/system.py
+++ b/supysonic/api/system.py
@@ -18,8 +18,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from flask import request
-from supysonic.web import app
+from flask import request, current_app as app
@app.route('/rest/ping.view', methods = [ 'GET', 'POST' ])
def ping():
diff --git a/supysonic/api/user.py b/supysonic/api/user.py
index 91a9dca..cfd07b7 100644
--- a/supysonic/api/user.py
+++ b/supysonic/api/user.py
@@ -18,8 +18,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from flask import request
-from supysonic.web import app, store
+from flask import request, current_app as app
+from supysonic.web import store
from supysonic.db import User
from supysonic.managers.user import UserManager
from . import decode_password
diff --git a/supysonic/config.py b/supysonic/config.py
index 01013e6..bfb740a 100644
--- a/supysonic/config.py
+++ b/supysonic/config.py
@@ -9,63 +9,60 @@
#
# Distributed under terms of the GNU AGPLv3 license.
-from ConfigParser import ConfigParser, NoOptionError, NoSectionError
+from ConfigParser import SafeConfigParser
-import mimetypes
import os
import tempfile
-# Seek for standard locations
-config_file = [
- 'supysonic.conf',
- os.path.expanduser('~/.config/supysonic/supysonic.conf'),
+class DefaultConfig(object):
+ DEBUG = False
+ SECRET_KEY = os.urandom(128)
+
+ tempdir = os.path.join(tempfile.gettempdir(), 'supysonic')
+ BASE = {
+ 'database_uri': 'sqlite://' + os.path.join(tempdir, 'supysonic.db'),
+ 'scanner_extensions': None
+ }
+ WEBAPP = {
+ 'cache_dir': tempdir,
+ 'log_file': None,
+ 'log_level': 'WARNING',
+
+ 'mount_webui': True,
+ 'mount_api': True
+ }
+ DAEMON = {
+ 'log_file': None,
+ 'log_level': 'WARNING'
+ }
+ LASTFM = {
+ 'api_key': None,
+ 'secret': None
+ }
+ TRANSCODING = {}
+ MIMETYPES = {}
+
+class IniConfig(DefaultConfig):
+ common_paths = [
+ '/etc/supysonic',
os.path.expanduser('~/.supysonic'),
- '/etc/supysonic'
- ]
+ os.path.expanduser('~/.config/supysonic/supysonic.conf'),
+ 'supysonic.conf'
+ ]
-config = ConfigParser({ 'cache_dir': os.path.join(tempfile.gettempdir(), 'supysonic') })
+ def __init__(self, paths):
+ parser = SafeConfigParser()
+ parser.read(paths)
+ for section in parser.sections():
+ options = { k: v for k, v in parser.items(section) }
+ section = section.upper()
+ if hasattr(self, section):
+ getattr(self, section).update(options)
+ else:
+ setattr(self, section, options)
-def check():
- """
- Checks the config file and mandatory fields
- """
- try:
- config.read(config_file)
- except Exception as e:
- err = 'Config file is corrupted.\n{0}'.format(e)
- raise SystemExit(err)
+ @classmethod
+ def from_common_locations(cls):
+ return IniConfig(cls.common_paths)
- try:
- config.get('base', 'database_uri')
- except (NoSectionError, NoOptionError):
- raise SystemExit('No database URI set')
-
- return True
-
-def get(section, option):
- """
- Returns a config option value from config file
-
- :param section: section where the option is stored
- :param option: option name
- :return: a config option value
- :rtype: string
- """
- try:
- return config.get(section, option)
- except (NoSectionError, NoOptionError):
- return None
-
-def get_mime(extension):
- """
- Returns mimetype of an extension based on config file
-
- :param extension: extension string
- :return: mimetype
- :rtype: string
- """
- guessed_mime = mimetypes.guess_type('dummy.' + extension, False)[0]
- config_mime = get('mimetypes', extension)
- default_mime = 'application/octet-stream'
- return guessed_mime or config_mime or default_mime
diff --git a/supysonic/db.py b/supysonic/db.py
index 25243e2..4acbc18 100644
--- a/supysonic/db.py
+++ b/supysonic/db.py
@@ -25,10 +25,9 @@ from storm.store import Store
from storm.variables import Variable
import uuid, datetime, time
+import mimetypes
import os.path
-from supysonic import config
-
def now():
return datetime.datetime.now().replace(microsecond = 0)
@@ -213,7 +212,7 @@ class Track(object):
if prefs and prefs.format and prefs.format != self.suffix():
info['transcodedSuffix'] = prefs.format
- info['transcodedContentType'] = config.get_mime(prefs.format)
+ info['transcodedContentType'] = mimetypes.guess_type('dummyname.' + prefs.format, False)[0] or 'application/octet-stream'
return info
diff --git a/supysonic/frontend/__init__.py b/supysonic/frontend/__init__.py
index 0dae398..6c7a208 100644
--- a/supysonic/frontend/__init__.py
+++ b/supysonic/frontend/__init__.py
@@ -9,8 +9,8 @@
#
# Distributed under terms of the GNU AGPLv3 license.
-from flask import session, request, redirect, url_for
-from supysonic.web import app, store
+from flask import session, request, redirect, url_for, current_app as app
+from supysonic.web import store
from supysonic.db import Artist, Album, Track
from supysonic.managers.user import UserManager
from functools import wraps
diff --git a/supysonic/frontend/folder.py b/supysonic/frontend/folder.py
index 80e0f0b..e375862 100644
--- a/supysonic/frontend/folder.py
+++ b/supysonic/frontend/folder.py
@@ -18,11 +18,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from flask import request, flash, render_template, redirect, url_for
+from flask import request, flash, render_template, redirect, url_for, current_app as app
import os.path
import uuid
-from supysonic.web import app, store
+from supysonic.web import store
from supysonic.db import Folder
from supysonic.scanner import Scanner
from supysonic.managers.user import UserManager
@@ -84,7 +84,10 @@ def del_folder(id):
@app.route('/folder/scan/')
@admin_only
def scan_folder(id = None):
- scanner = Scanner(store)
+ extensions = app.config['BASE']['scanner_extensions']
+ if extensions:
+ extensions = extensions.split(' ')
+ scanner = Scanner(store, extensions = extensions)
if id is None:
for folder in store.find(Folder, Folder.root == True):
scanner.scan(folder)
diff --git a/supysonic/frontend/playlist.py b/supysonic/frontend/playlist.py
index 98c02b8..9f81ddd 100644
--- a/supysonic/frontend/playlist.py
+++ b/supysonic/frontend/playlist.py
@@ -18,9 +18,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from flask import request, flash, render_template, redirect, url_for
+from flask import request, flash, render_template, redirect, url_for, current_app as app
import uuid
-from supysonic.web import app, store
+from supysonic.web import store
from supysonic.db import Playlist
from supysonic.managers.user import UserManager
diff --git a/supysonic/frontend/user.py b/supysonic/frontend/user.py
index db9484e..6ebb0fc 100644
--- a/supysonic/frontend/user.py
+++ b/supysonic/frontend/user.py
@@ -18,15 +18,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import uuid, csv
-
-from flask import request, session, flash, render_template, redirect, url_for, make_response
+from flask import request, session, flash, render_template, redirect, url_for, current_app as app
from functools import wraps
-from supysonic.web import app, store
+from supysonic.web import store
from supysonic.managers.user import UserManager
from supysonic.db import User, ClientPrefs
-from supysonic import config
from supysonic.lastfm import LastFm
from . import admin_only
@@ -67,7 +64,7 @@ def user_index():
@me_or_uuid
def user_profile(uid, user):
prefs = store.find(ClientPrefs, ClientPrefs.user_id == user.id)
- return render_template('profile.html', user = user, has_lastfm = config.get('lastfm', 'api_key') != None, clients = prefs)
+ return render_template('profile.html', user = user, has_lastfm = app.config['LASTFM']['api_key'] != None, clients = prefs)
@app.route('/user/', methods = [ 'POST' ])
@me_or_uuid
@@ -251,7 +248,7 @@ def lastfm_reg(uid, user):
flash('Missing LastFM auth token')
return redirect(url_for('user_profile', uid = uid))
- lfm = LastFm(user, app.logger)
+ lfm = LastFm(app.config['LASTFM'], user, app.logger)
status, error = lfm.link_account(token)
store.commit()
flash(error if not status else 'Successfully linked LastFM account')
@@ -261,7 +258,7 @@ def lastfm_reg(uid, user):
@app.route('/user//lastfm/unlink')
@me_or_uuid
def lastfm_unreg(uid, user):
- lfm = LastFm(user, app.logger)
+ lfm = LastFm(app.config['LASTFM'], user, app.logger)
lfm.unlink_account()
store.commit()
flash('Unlinked LastFM account')
diff --git a/supysonic/lastfm.py b/supysonic/lastfm.py
index 801b07c..d382854 100644
--- a/supysonic/lastfm.py
+++ b/supysonic/lastfm.py
@@ -3,7 +3,7 @@
# This file is part of Supysonic.
#
# Supysonic is a Python implementation of the Subsonic server API.
-# Copyright (C) 2013 Alban 'spl0k' Féron
+# Copyright (C) 2013-2017 Alban 'spl0k' Féron
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -19,13 +19,12 @@
# along with this program. If not, see .
import requests, hashlib
-from supysonic import config
class LastFm:
- def __init__(self, user, logger):
+ def __init__(self, config, user, logger):
+ self.__api_key = config['api_key']
+ self.__api_secret = config['secret']
self.__user = user
- self.__api_key = config.get('lastfm', 'api_key')
- self.__api_secret = config.get('lastfm', 'secret')
self.__enabled = self.__api_key is not None and self.__api_secret is not None
self.__logger = logger
diff --git a/supysonic/scanner.py b/supysonic/scanner.py
index 1cb0567..789bdbc 100644
--- a/supysonic/scanner.py
+++ b/supysonic/scanner.py
@@ -19,13 +19,13 @@
# along with this program. If not, see .
import os, os.path
-import time
+import mimetypes
import mutagen
+import time
from storm.expr import ComparableExpr, compile, Like
from storm.exceptions import NotSupportedError
-from supysonic import config
from supysonic.db import Folder, Artist, Album, Track, User
from supysonic.db import StarredFolder, StarredArtist, StarredAlbum, StarredTrack
from supysonic.db import RatingFolder, RatingTrack
@@ -52,7 +52,10 @@ def compile_concat(compile, concat, state):
return statement % (left, right)
class Scanner:
- def __init__(self, store, force = False):
+ def __init__(self, store, force = False, extensions = None):
+ if extensions is not None and not isinstance(extensions, list):
+ raise TypeError('Invalid extensions type')
+
self.__store = store
self.__force = force
@@ -63,8 +66,7 @@ class Scanner:
self.__deleted_albums = 0
self.__deleted_tracks = 0
- extensions = config.get('base', 'scanner_extensions')
- self.__extensions = map(str.lower, extensions.split()) if extensions else None
+ self.__extensions = extensions
self.__folders_to_check = set()
self.__artists_to_check = set()
@@ -172,7 +174,7 @@ class Scanner:
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.content_type = config.get_mime(os.path.splitext(path)[1][1:])
+ tr.content_type = mimetypes.guess_type(path, False)[0] or 'application/octet-stream'
tr.last_modification = os.path.getmtime(path)
tralbum = self.__find_album(albumartist, album)
diff --git a/supysonic/watcher.py b/supysonic/watcher.py
index 1e762df..2b5fda7 100644
--- a/supysonic/watcher.py
+++ b/supysonic/watcher.py
@@ -26,7 +26,7 @@ from logging.handlers import TimedRotatingFileHandler
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
-from supysonic import config, db
+from supysonic import db
from supysonic.scanner import Scanner
OP_SCAN = 1
@@ -35,8 +35,7 @@ OP_MOVE = 4
FLAG_CREATE = 8
class SupysonicWatcherEventHandler(PatternMatchingEventHandler):
- def __init__(self, queue, logger):
- extensions = config.get('base', 'scanner_extensions')
+ def __init__(self, extensions, queue, logger):
patterns = map(lambda e: "*." + e.lower(), extensions.split()) if extensions else None
super(SupysonicWatcherEventHandler, self).__init__(patterns = patterns, ignore_directories = True)
@@ -109,10 +108,11 @@ class Event(object):
return self.__src
class ScannerProcessingQueue(Thread):
- def __init__(self, logger):
+ def __init__(self, database_uri, logger):
super(ScannerProcessingQueue, self).__init__()
self.__logger = logger
+ self.__database_uri = database_uri
self.__cond = Condition()
self.__timer = None
self.__queue = {}
@@ -135,7 +135,7 @@ class ScannerProcessingQueue(Thread):
continue
self.__logger.debug("Instantiating scanner")
- store = db.get_store(config.get('base', 'database_uri'))
+ store = db.get_store(self.__database_uri)
scanner = Scanner(store)
item = self.__next_item()
@@ -202,18 +202,18 @@ class ScannerProcessingQueue(Thread):
return None
class SupysonicWatcher(object):
- def run(self):
- if not config.check():
- return
+ def __init__(self, config):
+ self.__config = config
+ def run(self):
logger = logging.getLogger(__name__)
- if config.get('daemon', 'log_file'):
- log_handler = TimedRotatingFileHandler(config.get('daemon', 'log_file'), when = 'midnight')
+ if self.__config.DAEMON['log_file']:
+ log_handler = TimedRotatingFileHandler(self.__config.DAEMON['log_file'], when = 'midnight')
else:
log_handler = logging.NullHandler()
log_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
logger.addHandler(log_handler)
- if config.get('daemon', 'log_level'):
+ if self.__config.DAEMON['log_level']:
mapping = {
'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
@@ -221,9 +221,9 @@ class SupysonicWatcher(object):
'ERROR': logging.ERROR,
'CRTICAL': logging.CRITICAL
}
- logger.setLevel(mapping.get(config.get('daemon', 'log_level').upper(), logging.NOTSET))
+ logger.setLevel(mapping.get(self.__config.DAEMON['log_level'].upper(), logging.NOTSET))
- store = db.get_store(config.get('base', 'database_uri'))
+ store = db.get_store(self.__config.BASE['database_uri'])
folders = store.find(db.Folder, db.Folder.root == True)
if not folders.count():
@@ -231,8 +231,8 @@ class SupysonicWatcher(object):
store.close()
return
- queue = ScannerProcessingQueue(logger)
- handler = SupysonicWatcherEventHandler(queue, logger)
+ queue = ScannerProcessingQueue(self.__config.BASE['database_uri'], logger)
+ handler = SupysonicWatcherEventHandler(self.__config.BASE['scanner_extensions'], queue, logger)
observer = Observer()
for folder in folders:
diff --git a/supysonic/web.py b/supysonic/web.py
index 862b484..7890037 100644
--- a/supysonic/web.py
+++ b/supysonic/web.py
@@ -9,17 +9,19 @@
#
# Distributed under terms of the GNU AGPLv3 license.
-from flask import Flask, g
+import mimetypes
+
+from flask import Flask, g, current_app
from os import makedirs, path
from werkzeug.local import LocalProxy
-from supysonic import config
+from supysonic.config import IniConfig
from supysonic.db import get_store
# Supysonic database open
def get_db():
if not hasattr(g, 'database'):
- g.database = get_store(config.get('base', 'database_uri'))
+ g.database = get_store(current_app.config['BASE']['database_uri'])
return g.database
# Supysonic database close
@@ -29,36 +31,28 @@ def close_db(error):
store = LocalProxy(get_db)
-def create_application():
+def create_application(config = None):
global app
- # Check config for mandatory fields
- config.check()
-
- # Test for the cache directory
- if not path.exists(config.get('webapp', 'cache_dir')):
- makedirs(config.get('webapp', 'cache_dir'))
-
# Flask!
app = Flask(__name__)
+ app.config.from_object('supysonic.config.DefaultConfig')
- # Set a secret key for sessions
- secret_key = config.get('base', 'secret_key')
- # If secret key is not defined in config, set develop key
- if secret_key is None:
- app.secret_key = 'd3v3l0p'
- else:
- app.secret_key = secret_key
+ if not config:
+ config = IniConfig.from_common_locations()
+ app.config.from_object(config)
# Close database connection on teardown
app.teardown_appcontext(close_db)
# Set loglevel
- if config.get('webapp', 'log_file'):
+ logfile = app.config['WEBAPP']['log_file']
+ if logfile:
import logging
from logging.handlers import TimedRotatingFileHandler
- handler = TimedRotatingFileHandler(config.get('webapp', 'log_file'), when = 'midnight')
- if config.get('webapp', 'log_level'):
+ handler = TimedRotatingFileHandler(logfile, when = 'midnight')
+ loglevel = app.config['WEBAPP']['log_level']
+ if loglevel:
mapping = {
'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
@@ -66,11 +60,26 @@ def create_application():
'ERROR': logging.ERROR,
'CRTICAL': logging.CRITICAL
}
- handler.setLevel(mapping.get(config.get('webapp', 'log_level').upper(), logging.NOTSET))
+ handler.setLevel(mapping.get(loglevel.upper(), logging.NOTSET))
app.logger.addHandler(handler)
+ # Insert unknown mimetypes
+ for k, v in app.config['MIMETYPES'].iteritems():
+ extension = '.' + k.lower()
+ if extension not in mimetypes.types_map:
+ mimetypes.add_type(v, extension, False)
+
+ # Test for the cache directory
+ cache_path = app.config['WEBAPP']['cache_dir']
+ if not path.exists(cache_path):
+ makedirs(cache_path)
+
# Import app sections
- from supysonic import frontend
- from supysonic import api
+ with app.app_context():
+ if app.config['WEBAPP']['mount_webui']:
+ from supysonic import frontend
+ if app.config['WEBAPP']['mount_api']:
+ from supysonic import api
return app
+
diff --git a/tests/api/apitestbase.py b/tests/api/apitestbase.py
index 9a45b19..cc3d540 100644
--- a/tests/api/apitestbase.py
+++ b/tests/api/apitestbase.py
@@ -21,7 +21,7 @@ NS = 'http://subsonic.org/restapi'
NSMAP = { 'sub': NS }
class ApiTestBase(TestBase):
- __module_to_test__ = 'supysonic.api'
+ __with_api__ = True
def setUp(self):
super(ApiTestBase, self).setUp()
diff --git a/tests/api/test_api_setup.py b/tests/api/test_api_setup.py
index 10354cc..04bfcca 100644
--- a/tests/api/test_api_setup.py
+++ b/tests/api/test_api_setup.py
@@ -19,7 +19,7 @@ from xml.etree import ElementTree
from ..testbase import TestBase
class ApiSetupTestCase(TestBase):
- __module_to_test__ = 'supysonic.api'
+ __with_api__ = True
def __basic_auth_get(self, username, password):
hashed = base64.b64encode('{}:{}'.format(username, password))
diff --git a/tests/api/test_response_helper.py b/tests/api/test_response_helper.py
index 8f7048f..d445b44 100644
--- a/tests/api/test_response_helper.py
+++ b/tests/api/test_response_helper.py
@@ -9,24 +9,20 @@
#
# Distributed under terms of the GNU AGPLv3 license.
-import unittest, sys
+import unittest
import simplejson
from xml.etree import ElementTree
-from ..appmock import AppMock
+from ..testbase import TestBase
-class ResponseHelperBaseCase(unittest.TestCase):
+class ResponseHelperBaseCase(TestBase):
def setUp(self):
- sys.modules[u'supysonic.web'] = AppMock(with_store = False)
+ super(ResponseHelperBaseCase, self).setUp()
+
from supysonic.api import ResponseHelper
self.helper = ResponseHelper
- def tearDown(self):
- to_unload = [ m for m in sys.modules if m.startswith('supysonic') ]
- for m in to_unload:
- del sys.modules[m]
-
class ResponseHelperJsonTestCase(ResponseHelperBaseCase):
def serialize_and_deserialize(self, d, error = False):
if not isinstance(d, dict):
diff --git a/tests/appmock.py b/tests/appmock.py
deleted file mode 100644
index 6567b20..0000000
--- a/tests/appmock.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# -*- coding: utf-8 -*-
-# vim:fenc=utf-8
-#
-# This file is part of Supysonic.
-# Supysonic is a Python implementation of the Subsonic server API.
-#
-# Copyright (C) 2017 Alban 'spl0k' Féron
-#
-# Distributed under terms of the GNU AGPLv3 license.
-
-import io
-from flask import Flask
-from supysonic.db import get_store
-
-class AppMock(object):
- def __init__(self, with_store = True):
- self.app = Flask(__name__, template_folder = '../supysonic/templates')
- self.app.testing = True
- self.app.secret_key = 'Testing secret'
-
- if with_store:
- self.store = get_store('sqlite:')
- with io.open('schema/sqlite.sql', 'r') as sql:
- schema = sql.read()
- for statement in schema.split(';'):
- self.store.execute(statement)
- else:
- self.store = None
-
diff --git a/tests/frontend/frontendtestbase.py b/tests/frontend/frontendtestbase.py
index ac379cc..e290836 100644
--- a/tests/frontend/frontendtestbase.py
+++ b/tests/frontend/frontendtestbase.py
@@ -11,6 +11,8 @@
from ..testbase import TestBase
class FrontendTestBase(TestBase):
+ __with_webui__ = True
+
def _login(self, username, password):
return self.client.post('/user/login', data = { 'user': username, 'password': password }, follow_redirects = True)
diff --git a/tests/frontend/test_folder.py b/tests/frontend/test_folder.py
index 65ed866..30c9251 100644
--- a/tests/frontend/test_folder.py
+++ b/tests/frontend/test_folder.py
@@ -16,8 +16,6 @@ from supysonic.db import Folder
from .frontendtestbase import FrontendTestBase
class FolderTestCase(FrontendTestBase):
- __module_to_test__ = 'supysonic.frontend'
-
def test_index(self):
self._login('bob', 'B0b')
rv = self.client.get('/folder', follow_redirects = True)
diff --git a/tests/frontend/test_login.py b/tests/frontend/test_login.py
index 60b7ae6..b908abe 100644
--- a/tests/frontend/test_login.py
+++ b/tests/frontend/test_login.py
@@ -17,8 +17,6 @@ from supysonic.db import User
from .frontendtestbase import FrontendTestBase
class LoginTestCase(FrontendTestBase):
- __module_to_test__ = 'supysonic.frontend'
-
def test_unauthorized_request(self):
# Unauthorized request
rv = self.client.get('/', follow_redirects=True)
diff --git a/tests/frontend/test_playlist.py b/tests/frontend/test_playlist.py
index f0de738..80e6bed 100644
--- a/tests/frontend/test_playlist.py
+++ b/tests/frontend/test_playlist.py
@@ -16,8 +16,6 @@ from supysonic.db import Folder, Artist, Album, Track, Playlist, User
from .frontendtestbase import FrontendTestBase
class PlaylistTestCase(FrontendTestBase):
- __module_to_test__ = 'supysonic.frontend'
-
def setUp(self):
super(PlaylistTestCase, self).setUp()
diff --git a/tests/frontend/test_user.py b/tests/frontend/test_user.py
index 4cd3447..cb695d4 100644
--- a/tests/frontend/test_user.py
+++ b/tests/frontend/test_user.py
@@ -16,8 +16,6 @@ from supysonic.db import User, ClientPrefs
from .frontendtestbase import FrontendTestBase
class UserTestCase(FrontendTestBase):
- __module_to_test__ = 'supysonic.frontend'
-
def setUp(self):
super(UserTestCase, self).setUp()
diff --git a/tests/testbase.py b/tests/testbase.py
index 35ec190..ec4ad6f 100644
--- a/tests/testbase.py
+++ b/tests/testbase.py
@@ -8,29 +8,57 @@
#
# Distributed under terms of the GNU AGPLv3 license.
-import importlib
-import unittest
+import io
import sys
+import unittest
+from supysonic.config import DefaultConfig
from supysonic.managers.user import UserManager
+from supysonic.web import create_application, store
-from .appmock import AppMock
+class TestConfig(DefaultConfig):
+ TESTING = True
+ LOGGER_HANDLER_POLICY = 'never'
+ BASE = {
+ 'database_uri': 'sqlite:',
+ 'scanner_extensions': None
+ }
+ MIMETYPES = {
+ 'mp3': 'audio/mpeg',
+ 'weirdextension': 'application/octet-stream'
+ }
+
+ def __init__(self, with_webui, with_api):
+ super(TestConfig, self).__init__
+
+ self.WEBAPP.update({
+ 'mount_webui': with_webui,
+ 'mount_api': with_api
+ })
class TestBase(unittest.TestCase):
- def setUp(self):
- app_mock = AppMock()
- self.app = app_mock.app
- self.store = app_mock.store
- self.client = self.app.test_client()
+ __with_webui__ = False
+ __with_api__ = False
- sys.modules['supysonic.web'] = app_mock
- importlib.import_module(self.__module_to_test__)
+ def setUp(self):
+ app = create_application(TestConfig(self.__with_webui__, self.__with_api__))
+ self.__ctx = app.app_context()
+ self.__ctx.push()
+
+ self.store = store
+ with io.open('schema/sqlite.sql', 'r') as sql:
+ schema = sql.read()
+ for statement in schema.split(';'):
+ self.store.execute(statement)
+
+ self.client = app.test_client()
UserManager.add(self.store, 'alice', 'Alic3', 'test@example.com', True)
UserManager.add(self.store, 'bob', 'B0b', 'bob@example.com', False)
def tearDown(self):
- self.store.close()
+ self.__ctx.pop()
+
to_unload = [ m for m in sys.modules if m.startswith('supysonic') ]
for m in to_unload:
del sys.modules[m]