1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-12-22 17:06:17 +00:00

Reworked config handling

This commit is contained in:
spl0k 2017-11-27 22:30:13 +01:00
parent 4ca48fd31c
commit a62976ba9d
32 changed files with 393 additions and 390 deletions

View File

@ -20,8 +20,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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
@ -58,8 +58,9 @@ class CLI(cmd.Cmd):
return method
def __init__(self, store):
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__)):
@ -74,7 +75,7 @@ class CLI(cmd.Cmd):
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
@ -151,7 +152,10 @@ class CLI(cmd.Cmd):
self.__last_len = len(progress)
self.__last_display = time.time()
scanner = Scanner(self.__store, force)
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)):
@ -235,10 +239,9 @@ class CLI(cmd.Cmd):
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')))
cli = CLI(config)
if len(sys.argv) > 1:
cli.onecmd(' '.join(sys.argv[1:]))
else:

View File

@ -19,9 +19,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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()

View File

@ -18,14 +18,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -18,14 +18,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -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)

View File

@ -18,8 +18,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -18,8 +18,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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' ])

View File

@ -18,16 +18,18 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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)

View File

@ -18,10 +18,10 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -19,9 +19,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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' ])

View File

@ -18,8 +18,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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():

View File

@ -18,8 +18,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -18,11 +18,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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/<id>')
@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)

View File

@ -18,9 +18,9 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -18,15 +18,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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/<uid>', 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/<uid>/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')

View File

@ -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 <http://www.gnu.org/licenses/>.
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

View File

@ -19,13 +19,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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)

View File

@ -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:

View File

@ -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
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

View File

@ -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()

View File

@ -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))

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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]