diff --git a/setup.py b/setup.py index 49187d7..6626d55 100755 --- a/setup.py +++ b/setup.py @@ -15,49 +15,51 @@ from setuptools import setup from setuptools import find_packages reqs = [ - 'flask>=0.11', - 'pony>=0.7.6', - 'Pillow', - 'requests>=1.0.0', - 'mutagen>=1.33', - 'scandir<2.0.0', - 'watchdog>=0.8.0', - 'zipstream' + "flask>=0.11", + "pony>=0.7.6", + "Pillow", + "requests>=1.0.0", + "mutagen>=1.33", + "scandir<2.0.0", + "watchdog>=0.8.0", + "zipstream", ] setup( - name=project.NAME, - version=project.VERSION, - description=project.DESCRIPTION, - keywords=project.KEYWORDS, - long_description=project.LONG_DESCRIPTION, - author=project.AUTHOR_NAME, - author_email=project.AUTHOR_EMAIL, - url=project.URL, - license=project.LICENSE, - packages=find_packages(exclude=['tests*']), - install_requires = reqs, - entry_points={ 'console_scripts': [ - 'supysonic-cli=supysonic.cli:main', - 'supysonic-daemon=supysonic.daemon:main' - ] }, - zip_safe=False, - include_package_data=True, - test_suite='tests.suite', - tests_require = [ 'lxml' ], - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Environment :: Console', - 'Environment :: Web Environment', - 'Framework :: Flask', - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: GNU Affero General Public License v3', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Topic :: Multimedia :: Sound/Audio' + name=project.NAME, + version=project.VERSION, + description=project.DESCRIPTION, + keywords=project.KEYWORDS, + long_description=project.LONG_DESCRIPTION, + author=project.AUTHOR_NAME, + author_email=project.AUTHOR_EMAIL, + url=project.URL, + license=project.LICENSE, + packages=find_packages(exclude=["tests*"]), + install_requires=reqs, + entry_points={ + "console_scripts": [ + "supysonic-cli=supysonic.cli:main", + "supysonic-daemon=supysonic.daemon:main", ] - ) + }, + zip_safe=False, + include_package_data=True, + test_suite="tests.suite", + tests_require=["lxml"], + classifiers=[ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Environment :: Web Environment", + "Framework :: Flask", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Topic :: Multimedia :: Sound/Audio", + ], +) diff --git a/supysonic/__init__.py b/supysonic/__init__.py index 83c217c..0e3878e 100644 --- a/supysonic/__init__.py +++ b/supysonic/__init__.py @@ -8,15 +8,15 @@ # # Distributed under terms of the GNU AGPLv3 license. -NAME = 'supysonic' -VERSION = '0.4' -DESCRIPTION = 'Python implementation of the Subsonic server API.' -KEYWORDS = 'subsonic music api' -AUTHOR_NAME = 'Alban Féron' -AUTHOR_EMAIL = 'alban.feron@gmail.com' -URL = 'https://github.com/spl0k/supysonic' -LICENSE = 'GNU AGPLv3' -LONG_DESCRIPTION = '''Supysonic is a Python implementation of the Subsonic server API. +NAME = "supysonic" +VERSION = "0.4" +DESCRIPTION = "Python implementation of the Subsonic server API." +KEYWORDS = "subsonic music api" +AUTHOR_NAME = "Alban Féron" +AUTHOR_EMAIL = "alban.feron@gmail.com" +URL = "https://github.com/spl0k/supysonic" +LICENSE = "GNU AGPLv3" +LONG_DESCRIPTION = """Supysonic is a Python implementation of the Subsonic server API. Current supported features are: * browsing (by folders or tags) * streaming of various audio file formats @@ -24,4 +24,4 @@ Current supported features are: * user or random playlists * cover arts (cover.jpg files in the same folder as music files) * starred tracks/albums and ratings -* Last.FM scrobbling''' +* Last.FM scrobbling""" diff --git a/supysonic/api/__init__.py b/supysonic/api/__init__.py index ddf0896..3427acf 100644 --- a/supysonic/api/__init__.py +++ b/supysonic/api/__init__.py @@ -7,7 +7,7 @@ # # Distributed under terms of the GNU AGPLv3 license. -API_VERSION = '1.9.0' +API_VERSION = "1.9.0" import binascii import uuid @@ -23,39 +23,44 @@ from ..py23 import dict from .exceptions import Unauthorized from .formatters import JSONFormatter, JSONPFormatter, XMLFormatter -api = Blueprint('api', __name__) +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': + f, callback = map(request.values.get, ["f", "callback"]) + if f == "jsonp": request.formatter = JSONPFormatter(callback) - elif f == 'json': + elif f == "json": request.formatter = JSONFormatter() else: request.formatter = XMLFormatter() + def decode_password(password): - if not password.startswith('enc:'): + if not password.startswith("enc:"): return password try: - return binascii.unhexlify(password[4:].encode('utf-8')).decode('utf-8') + return binascii.unhexlify(password[4:].encode("utf-8")).decode("utf-8") except: return password + @api.before_request def authorize(): if request.authorization: - user = UserManager.try_auth(request.authorization.username, request.authorization.password) + user = UserManager.try_auth( + request.authorization.username, request.authorization.password + ) if user is not None: request.user = user return raise Unauthorized() - username = request.values['u'] - password = request.values['p'] + username = request.values["u"] + password = request.values["p"] password = decode_password(password) user = UserManager.try_auth(username, password) @@ -64,21 +69,24 @@ def authorize(): request.user = user + @api.before_request def get_client_prefs(): - client = request.values['c'] + client = request.values["c"] try: request.client = ClientPrefs[request.user, client] except ObjectNotFound: - request.client = ClientPrefs(user = request.user, client_name = client) + request.client = ClientPrefs(user=request.user, client_name=client) commit() -def get_entity(cls, param = 'id'): + +def get_entity(cls, param="id"): eid = request.values[param] eid = uuid.UUID(eid) entity = cls[eid] return entity + from .errors import * from .system import * @@ -91,4 +99,3 @@ from .chat import * from .search import * from .playlists import * from .unsupported import * - diff --git a/supysonic/api/albums_songs.py b/supysonic/api/albums_songs.py index 6ceeca8..4d502ad 100644 --- a/supysonic/api/albums_songs.py +++ b/supysonic/api/albums_songs.py @@ -13,17 +13,31 @@ from datetime import timedelta from flask import request from pony.orm import select, desc, avg, max, min, count -from ..db import Folder, Artist, Album, Track, RatingFolder, StarredFolder, StarredArtist, StarredAlbum, StarredTrack, User +from ..db import ( + Folder, + Artist, + Album, + Track, + RatingFolder, + StarredFolder, + StarredArtist, + StarredAlbum, + StarredTrack, + User, +) from ..db import now from ..py23 import dict from . import api from .exceptions import GenericError, NotFound -@api.route('/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' ]) + size = request.values.get("size", "10") + genre, fromYear, toYear, musicFolderId = map( + request.values.get, ["genre", "fromYear", "toYear", "musicFolderId"] + ) size = int(size) if size else 10 fromYear = int(fromYear) if fromYear else None @@ -38,120 +52,196 @@ def rand_songs(): if genre: query = query.filter(lambda t: t.genre == genre) if fid: - if not Folder.exists(id = fid, root = True): - raise NotFound('Folder') + if not Folder.exists(id=fid, root=True): + raise NotFound("Folder") query = query.filter(lambda t: t.root_folder.id == fid) - return request.formatter('randomSongs', dict( - song = [ t.as_subsonic_child(request.user, request.client) for t in query.without_distinct().random(size) ] - )) + return request.formatter( + "randomSongs", + dict( + song=[ + t.as_subsonic_child(request.user, request.client) + for t in query.without_distinct().random(size) + ] + ), + ) -@api.route('/getAlbumList.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getAlbumList.view", methods=["GET", "POST"]) def album_list(): - ltype = request.values['type'] + ltype = request.values["type"] - size, offset = map(request.values.get, [ 'size', 'offset' ]) + size, offset = map(request.values.get, ["size", "offset"]) size = int(size) if size else 10 offset = int(offset) if offset else 0 query = select(t.folder for t in Track) - if ltype == 'random': - return request.formatter('albumList', dict( - album = [ a.as_subsonic_child(request.user) for a in query.without_distinct().random(size) ] - )) - elif ltype == 'newest': + if ltype == "random": + return request.formatter( + "albumList", + dict( + album=[ + a.as_subsonic_child(request.user) + for a in query.without_distinct().random(size) + ] + ), + ) + elif ltype == "newest": query = query.order_by(desc(Folder.created)) - elif ltype == 'highest': + elif ltype == "highest": query = query.order_by(lambda f: desc(avg(f.ratings.rating))) - elif ltype == 'frequent': + elif ltype == "frequent": query = query.order_by(lambda f: desc(avg(f.tracks.play_count))) - elif ltype == 'recent': - query = select(t.folder for t in Track if max(t.folder.tracks.last_play) is not None).order_by(lambda f: desc(max(f.tracks.last_play))) - elif ltype == 'starred': - query = select(s.starred for s in StarredFolder if s.user.id == request.user.id and count(s.starred.tracks) > 0) - elif ltype == 'alphabeticalByName': + elif ltype == "recent": + query = select( + t.folder for t in Track if max(t.folder.tracks.last_play) is not None + ).order_by(lambda f: desc(max(f.tracks.last_play))) + elif ltype == "starred": + query = select( + s.starred + for s in StarredFolder + if s.user.id == request.user.id and count(s.starred.tracks) > 0 + ) + elif ltype == "alphabeticalByName": query = query.order_by(Folder.name) - elif ltype == 'alphabeticalByArtist': + elif ltype == "alphabeticalByArtist": query = query.order_by(lambda f: f.parent.name + f.name) else: - raise GenericError('Unknown search type') + raise GenericError("Unknown search type") - return request.formatter('albumList', dict( - album = [ f.as_subsonic_child(request.user) for f in query.limit(size, offset) ] - )) + return request.formatter( + "albumList", + dict( + album=[f.as_subsonic_child(request.user) for f in query.limit(size, offset)] + ), + ) -@api.route('/getAlbumList2.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getAlbumList2.view", methods=["GET", "POST"]) def album_list_id3(): - ltype = request.values['type'] + ltype = request.values["type"] - size, offset = map(request.values.get, [ 'size', 'offset' ]) + size, offset = map(request.values.get, ["size", "offset"]) size = int(size) if size else 10 offset = int(offset) if offset else 0 query = Album.select() - if ltype == 'random': - return request.formatter('albumList2', dict( - album = [ a.as_subsonic_album(request.user) for a in query.random(size) ] - )) - elif ltype == 'newest': + if ltype == "random": + return request.formatter( + "albumList2", + dict(album=[a.as_subsonic_album(request.user) for a in query.random(size)]), + ) + elif ltype == "newest": query = query.order_by(lambda a: desc(min(a.tracks.created))) - elif ltype == 'frequent': + elif ltype == "frequent": query = query.order_by(lambda a: desc(avg(a.tracks.play_count))) - elif ltype == 'recent': - query = Album.select(lambda a: max(a.tracks.last_play) is not None).order_by(lambda a: desc(max(a.tracks.last_play))) - elif ltype == 'starred': + elif ltype == "recent": + query = Album.select(lambda a: max(a.tracks.last_play) is not None).order_by( + lambda a: desc(max(a.tracks.last_play)) + ) + elif ltype == "starred": query = select(s.starred for s in StarredAlbum if s.user.id == request.user.id) - elif ltype == 'alphabeticalByName': + elif ltype == "alphabeticalByName": query = query.order_by(Album.name) - elif ltype == 'alphabeticalByArtist': + elif ltype == "alphabeticalByArtist": query = query.order_by(lambda a: a.artist.name + a.name) else: - raise GenericError('Unknown search type') + raise GenericError("Unknown search type") - return request.formatter('albumList2', dict( - album = [ f.as_subsonic_album(request.user) for f in query.limit(size, offset) ] - )) + return request.formatter( + "albumList2", + dict( + album=[f.as_subsonic_album(request.user) for f in query.limit(size, offset)] + ), + ) -@api.route('/getSongsByGenre.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getSongsByGenre.view", methods=["GET", "POST"]) def songs_by_genre(): - genre = request.values['genre'] + genre = request.values["genre"] - count, offset = map(request.values.get, [ 'count', 'offset' ]) + count, offset = map(request.values.get, ["count", "offset"]) count = int(count) if count else 10 offset = int(offset) if offset else 0 query = select(t for t in Track if t.genre == genre).limit(count, offset) - return request.formatter('songsByGenre', dict( - song = [ t.as_subsonic_child(request.user, request.client) for t in query ] - )) + return request.formatter( + "songsByGenre", + dict(song=[t.as_subsonic_child(request.user, request.client) for t in query]), + ) -@api.route('/getNowPlaying.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getNowPlaying.view", methods=["GET", "POST"]) def now_playing(): - query = User.select(lambda u: u.last_play is not None and u.last_play_date + timedelta(minutes = 3) > now()) + query = User.select( + lambda u: u.last_play is not None + and u.last_play_date + timedelta(minutes=3) > now() + ) - return request.formatter('nowPlaying', dict( - entry = [ dict( - u.last_play.as_subsonic_child(request.user, request.client), - username = u.name, minutesAgo = (now() - u.last_play_date).seconds / 60, playerId = 0 - ) for u in query ] - )) + return request.formatter( + "nowPlaying", + dict( + entry=[ + dict( + u.last_play.as_subsonic_child(request.user, request.client), + username=u.name, + minutesAgo=(now() - u.last_play_date).seconds / 60, + playerId=0, + ) + for u in query + ] + ), + ) -@api.route('/getStarred.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getStarred.view", methods=["GET", "POST"]) def get_starred(): folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id) - return request.formatter('starred', dict( - artist = [ dict(id = str(sf.id), name = sf.name) for sf in folders.filter(lambda f: count(f.tracks) == 0) ], - album = [ sf.as_subsonic_child(request.user) for sf in folders.filter(lambda f: count(f.tracks) > 0) ], - song = [ st.as_subsonic_child(request.user, request.client) for st in select(s.starred for s in StarredTrack if s.user.id == request.user.id) ] - )) + return request.formatter( + "starred", + dict( + artist=[ + dict(id=str(sf.id), name=sf.name) + for sf in folders.filter(lambda f: count(f.tracks) == 0) + ], + album=[ + sf.as_subsonic_child(request.user) + for sf in folders.filter(lambda f: count(f.tracks) > 0) + ], + song=[ + st.as_subsonic_child(request.user, request.client) + for st in select( + s.starred for s in StarredTrack if s.user.id == request.user.id + ) + ], + ), + ) -@api.route('/getStarred2.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getStarred2.view", methods=["GET", "POST"]) def get_starred_id3(): - return request.formatter('starred2', dict( - artist = [ sa.as_subsonic_artist(request.user) for sa in select(s.starred for s in StarredArtist if s.user.id == request.user.id) ], - album = [ sa.as_subsonic_album(request.user) for sa in select(s.starred for s in StarredAlbum if s.user.id == request.user.id) ], - song = [ st.as_subsonic_child(request.user, request.client) for st in select(s.starred for s in StarredTrack if s.user.id == request.user.id) ] - )) - + return request.formatter( + "starred2", + dict( + artist=[ + sa.as_subsonic_artist(request.user) + for sa in select( + s.starred for s in StarredArtist if s.user.id == request.user.id + ) + ], + album=[ + sa.as_subsonic_album(request.user) + for sa in select( + s.starred for s in StarredAlbum if s.user.id == request.user.id + ) + ], + song=[ + st.as_subsonic_child(request.user, request.client) + for st in select( + s.starred for s in StarredTrack if s.user.id == request.user.id + ) + ], + ), + ) diff --git a/supysonic/api/annotation.py b/supysonic/api/annotation.py index 1d73f7f..08612fb 100644 --- a/supysonic/api/annotation.py +++ b/supysonic/api/annotation.py @@ -24,6 +24,7 @@ from ..py23 import dict from . import api, get_entity from .exceptions import AggregateException, GenericError, MissingParameter, NotFound + def star_single(cls, eid): """ Stars an entity @@ -34,14 +35,15 @@ def star_single(cls, eid): uid = uuid.UUID(eid) e = cls[uid] - starred_cls = getattr(sys.modules[__name__], 'Starred' + cls.__name__) + starred_cls = getattr(sys.modules[__name__], "Starred" + cls.__name__) try: starred_cls[request.user, uid] - raise GenericError('{} {} already starred'.format(cls.__name__, eid)) + raise GenericError("{} {} already starred".format(cls.__name__, eid)) except ObjectNotFound: pass - starred_cls(user = request.user, starred = e) + starred_cls(user=request.user, starred=e) + def unstar_single(cls, eid): """ Unstars an entity @@ -51,15 +53,18 @@ def unstar_single(cls, eid): """ uid = uuid.UUID(eid) - starred_cls = getattr(sys.modules[__name__], 'Starred' + cls.__name__) - delete(s for s in starred_cls if s.user.id == request.user.id and s.starred.id == uid) + starred_cls = getattr(sys.modules[__name__], "Starred" + cls.__name__) + delete( + s for s in starred_cls if s.user.id == request.user.id and s.starred.id == uid + ) return None + def handle_star_request(func): - id, albumId, artistId = map(request.values.getlist, [ 'id', 'albumId', 'artistId' ]) + id, albumId, artistId = map(request.values.getlist, ["id", "albumId", "artistId"]) if not id and not albumId and not artistId: - raise MissingParameter('id, albumId or artistId') + raise MissingParameter("id, albumId or artistId") errors = [] for eid in id: @@ -76,7 +81,7 @@ def handle_star_request(func): ferr = e if terr and ferr: - errors += [ terr, ferr ] + errors += [terr, ferr] for alId in albumId: try: @@ -94,28 +99,37 @@ def handle_star_request(func): raise AggregateException(errors) return request.formatter.empty -@api.route('/star.view', methods = [ 'GET', 'POST' ]) + +@api.route("/star.view", methods=["GET", "POST"]) def star(): return handle_star_request(star_single) -@api.route('/unstar.view', methods = [ 'GET', 'POST' ]) + +@api.route("/unstar.view", methods=["GET", "POST"]) def unstar(): return handle_star_request(unstar_single) -@api.route('/setRating.view', methods = [ 'GET', 'POST' ]) + +@api.route("/setRating.view", methods=["GET", "POST"]) def rate(): - id = request.values['id'] - rating = request.values['rating'] + id = request.values["id"] + rating = request.values["rating"] uid = uuid.UUID(id) rating = int(rating) if not 0 <= rating <= 5: - raise GenericError('rating must be between 0 and 5 (inclusive)') + raise GenericError("rating must be between 0 and 5 (inclusive)") if rating == 0: - delete(r for r in RatingTrack if r.user.id == request.user.id and r.rated.id == uid) - delete(r for r in RatingFolder if r.user.id == request.user.id and r.rated.id == uid) + delete( + r for r in RatingTrack if r.user.id == request.user.id and r.rated.id == uid + ) + delete( + r + for r in RatingFolder + if r.user.id == request.user.id and r.rated.id == uid + ) else: try: rated = Track[uid] @@ -125,28 +139,28 @@ def rate(): rated = Folder[uid] rating_cls = RatingFolder except ObjectNotFound: - raise NotFound('Track or Folder') + raise NotFound("Track or Folder") try: rating_info = rating_cls[request.user, uid] rating_info.rating = rating except ObjectNotFound: - rating_cls(user = request.user, rated = rated, rating = rating) + rating_cls(user=request.user, rated=rated, rating=rating) return request.formatter.empty -@api.route('/scrobble.view', methods = [ 'GET', 'POST' ]) + +@api.route("/scrobble.view", methods=["GET", "POST"]) def scrobble(): res = get_entity(Track) - t, submission = map(request.values.get, [ 'time', 'submission' ]) + t, submission = map(request.values.get, ["time", "submission"]) t = int(t) / 1000 if t else int(time.time()) - lfm = LastFm(current_app.config['LASTFM'], request.user) + lfm = LastFm(current_app.config["LASTFM"], request.user) - if submission in (None, '', True, 'true', 'True', 1, '1'): + if submission in (None, "", True, "true", "True", 1, "1"): lfm.scrobble(res, t) else: lfm.now_playing(res) return request.formatter.empty - diff --git a/supysonic/api/browse.py b/supysonic/api/browse.py index 21ae316..72949f5 100644 --- a/supysonic/api/browse.py +++ b/supysonic/api/browse.py @@ -18,19 +18,24 @@ from ..py23 import dict from . import api, get_entity -@api.route('/getMusicFolders.view', methods = [ 'GET', 'POST' ]) -def list_folders(): - return request.formatter('musicFolders', dict( - musicFolder = [ dict( - id = str(f.id), - name = f.name - ) for f in Folder.select(lambda f: f.root).order_by(Folder.name) ] - )) -@api.route('/getIndexes.view', methods = [ 'GET', 'POST' ]) +@api.route("/getMusicFolders.view", methods=["GET", "POST"]) +def list_folders(): + return request.formatter( + "musicFolders", + dict( + musicFolder=[ + dict(id=str(f.id), name=f.name) + for f in Folder.select(lambda f: f.root).order_by(Folder.name) + ] + ), + ) + + +@api.route("/getIndexes.view", methods=["GET", "POST"]) def list_indexes(): - musicFolderId = request.values.get('musicFolderId') - ifModifiedSince = request.values.get('ifModifiedSince') + musicFolderId = request.values.get("musicFolderId") + ifModifiedSince = request.values.get("ifModifiedSince") if ifModifiedSince: ifModifiedSince = int(ifModifiedSince) / 1000 @@ -42,11 +47,11 @@ def list_indexes(): if not folder.root: raise ObjectNotFound(Folder, mfid) - folders = [ folder ] + folders = [folder] last_modif = max(map(lambda f: f.last_scan, folders)) if ifModifiedSince is not None and last_modif < ifModifiedSince: - return request.formatter('indexes', dict(lastModified = last_modif * 1000)) + return request.formatter("indexes", dict(lastModified=last_modif * 1000)) # The XSD lies, we don't return artists but a directory structure artists = [] @@ -59,89 +64,132 @@ def list_indexes(): for artist in artists: index = artist.name[0].upper() if index in string.digits: - index = '#' + index = "#" elif index not in string.ascii_letters: - index = '?' + index = "?" if index not in indexes: indexes[index] = [] indexes[index].append(artist) - return request.formatter('indexes', dict( - lastModified = last_modif * 1000, - index = [ dict( - name = k, - artist = [ dict( - id = str(a.id), - name = a.name - ) for a in sorted(v, key = lambda a: a.name.lower()) ] - ) for k, v in sorted(indexes.items()) ], - child = [ c.as_subsonic_child(request.user, request.client) for c in sorted(children, key = lambda t: t.sort_key()) ] - )) + return request.formatter( + "indexes", + dict( + lastModified=last_modif * 1000, + index=[ + dict( + name=k, + artist=[ + dict(id=str(a.id), name=a.name) + for a in sorted(v, key=lambda a: a.name.lower()) + ], + ) + for k, v in sorted(indexes.items()) + ], + child=[ + c.as_subsonic_child(request.user, request.client) + for c in sorted(children, key=lambda t: t.sort_key()) + ], + ), + ) -@api.route('/getMusicDirectory.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getMusicDirectory.view", methods=["GET", "POST"]) def show_directory(): res = get_entity(Folder) directory = dict( - id = str(res.id), - name = res.name, - child = [ f.as_subsonic_child(request.user) for f in res.children.order_by(lambda c: c.name.lower()) ] + [ t.as_subsonic_child(request.user, request.client) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] + id=str(res.id), + name=res.name, + child=[ + f.as_subsonic_child(request.user) + for f in res.children.order_by(lambda c: c.name.lower()) + ] + + [ + t.as_subsonic_child(request.user, request.client) + for t in sorted(res.tracks, key=lambda t: t.sort_key()) + ], ) if not res.root: - directory['parent'] = str(res.parent.id) + directory["parent"] = str(res.parent.id) - return request.formatter('directory', directory) + return request.formatter("directory", directory) -@api.route('/getGenres.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getGenres.view", methods=["GET", "POST"]) def list_genres(): - return request.formatter('genres', dict( - genre = [ dict(value = genre) for genre in select(t.genre for t in Track if t.genre) ] - )) + return request.formatter( + "genres", + dict( + genre=[ + dict(value=genre) for genre in select(t.genre for t in Track if t.genre) + ] + ), + ) -@api.route('/getArtists.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getArtists.view", methods=["GET", "POST"]) def list_artists(): # According to the API page, there are no parameters? indexes = dict() for artist in Artist.select(): - index = artist.name[0].upper() if artist.name else '?' + index = artist.name[0].upper() if artist.name else "?" if index in string.digits: - index = '#' + index = "#" elif index not in string.ascii_letters: - index = '?' + index = "?" if index not in indexes: indexes[index] = [] indexes[index].append(artist) - return request.formatter('artists', dict( - index = [ dict( - name = k, - artist = [ a.as_subsonic_artist(request.user) for a in sorted(v, key = lambda a: a.name.lower()) ] - ) for k, v in sorted(indexes.items()) ] - )) + return request.formatter( + "artists", + dict( + index=[ + dict( + name=k, + artist=[ + a.as_subsonic_artist(request.user) + for a in sorted(v, key=lambda a: a.name.lower()) + ], + ) + for k, v in sorted(indexes.items()) + ] + ), + ) -@api.route('/getArtist.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getArtist.view", methods=["GET", "POST"]) def artist_info(): res = get_entity(Artist) info = res.as_subsonic_artist(request.user) - albums = set(res.albums) - albums |= { t.album for t in res.tracks } - info['album'] = [ a.as_subsonic_album(request.user) for a in sorted(albums, key = lambda a: a.sort_key()) ] + albums = set(res.albums) + albums |= {t.album for t in res.tracks} + info["album"] = [ + a.as_subsonic_album(request.user) + for a in sorted(albums, key=lambda a: a.sort_key()) + ] - return request.formatter('artist', info) + return request.formatter("artist", info) -@api.route('/getAlbum.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getAlbum.view", methods=["GET", "POST"]) def album_info(): res = get_entity(Album) info = res.as_subsonic_album(request.user) - info['song'] = [ t.as_subsonic_child(request.user, request.client) for t in sorted(res.tracks, key = lambda t: t.sort_key()) ] + info["song"] = [ + t.as_subsonic_child(request.user, request.client) + for t in sorted(res.tracks, key=lambda t: t.sort_key()) + ] - return request.formatter('album', info) + return request.formatter("album", info) -@api.route('/getSong.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getSong.view", methods=["GET", "POST"]) def track_info(): res = get_entity(Track) - return request.formatter('song', res.as_subsonic_child(request.user, request.client)) - + return request.formatter( + "song", res.as_subsonic_child(request.user, request.client) + ) diff --git a/supysonic/api/chat.py b/supysonic/api/chat.py index dbc7340..f7535d0 100644 --- a/supysonic/api/chat.py +++ b/supysonic/api/chat.py @@ -13,21 +13,24 @@ from ..db import ChatMessage, User from ..py23 import dict from . import api -@api.route('/getChatMessages.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getChatMessages.view", methods=["GET", "POST"]) def get_chat(): - since = request.values.get('since') + since = request.values.get("since") since = int(since) / 1000 if since else None query = ChatMessage.select().order_by(ChatMessage.time) if since: query = query.filter(lambda m: m.time > since) - return request.formatter('chatMessages', dict(chatMessage = [ msg.responsize() for msg in query ] )) + return request.formatter( + "chatMessages", dict(chatMessage=[msg.responsize() for msg in query]) + ) -@api.route('/addChatMessage.view', methods = [ 'GET', 'POST' ]) + +@api.route("/addChatMessage.view", methods=["GET", "POST"]) def add_chat_message(): - msg = request.values['message'] - ChatMessage(user = request.user, message = msg) + msg = request.values["message"] + ChatMessage(user=request.user, message=msg) return request.formatter.empty - diff --git a/supysonic/api/errors.py b/supysonic/api/errors.py index 55da786..f04973e 100644 --- a/supysonic/api/errors.py +++ b/supysonic/api/errors.py @@ -15,28 +15,32 @@ from werkzeug.exceptions import BadRequestKeyError from . import api from .exceptions import GenericError, MissingParameter, NotFound, ServerError + @api.errorhandler(ValueError) def value_error(e): rollback() return GenericError("{0.__class__.__name__}: {0}".format(e)) + @api.errorhandler(BadRequestKeyError) def key_error(e): rollback() return MissingParameter() + @api.errorhandler(ObjectNotFound) def not_found(e): rollback() return NotFound(e.entity.__name__) + @api.errorhandler(500) -def generic_error(e): # pragma: nocover +def generic_error(e): # pragma: nocover rollback() return ServerError("{0.__class__.__name__}: {0}".format(e)) -#@api.errorhandler(404) -@api.route('/', methods = [ 'GET', 'POST' ]) # blueprint 404 workaround -def not_found(*args, **kwargs): - return GenericError('Unknown method'), 404 +# @api.errorhandler(404) +@api.route("/", methods=["GET", "POST"]) # blueprint 404 workaround +def not_found(*args, **kwargs): + return GenericError("Unknown method"), 404 diff --git a/supysonic/api/exceptions.py b/supysonic/api/exceptions.py index 6bfec0e..46a2d28 100644 --- a/supysonic/api/exceptions.py +++ b/supysonic/api/exceptions.py @@ -10,19 +10,21 @@ from flask import current_app, request from werkzeug.exceptions import HTTPException + class SubsonicAPIException(HTTPException): code = 400 api_code = None message = None - def get_response(self, environ = None): + def get_response(self, environ=None): rv = request.formatter.error(self.api_code, self.message) rv.status_code = self.code return rv def __str__(self): - code = self.api_code if self.api_code is not None else '??' - return '{}: {}'.format(code, self.message) + code = self.api_code if self.api_code is not None else "??" + return "{}: {}".format(code, self.message) + class GenericError(SubsonicAPIException): api_code = 0 @@ -31,14 +33,17 @@ class GenericError(SubsonicAPIException): super(GenericError, self).__init__(*args, **kwargs) self.message = message + class ServerError(GenericError): code = 500 + class UnsupportedParameter(GenericError): def __init__(self, parameter, *args, **kwargs): message = "Unsupported parameter '{}'".format(parameter) super(UnsupportedParameter, self).__init__(message, *args, **kwargs) + class MissingParameter(SubsonicAPIException): api_code = 10 @@ -46,31 +51,39 @@ class MissingParameter(SubsonicAPIException): super(MissingParameter, self).__init__(*args, **kwargs) self.message = "A required parameter is missing." + class ClientMustUpgrade(SubsonicAPIException): api_code = 20 - message = 'Incompatible Subsonic REST protocol version. Client must upgrade.' + message = "Incompatible Subsonic REST protocol version. Client must upgrade." + class ServerMustUpgrade(SubsonicAPIException): code = 501 api_code = 30 - message = 'Incompatible Subsonic REST protocol version. Server must upgrade.' + message = "Incompatible Subsonic REST protocol version. Server must upgrade." + class Unauthorized(SubsonicAPIException): code = 401 api_code = 40 - message = 'Wrong username or password.' + message = "Wrong username or password." + class Forbidden(SubsonicAPIException): code = 403 api_code = 50 - message = 'User is not authorized for the given operation.' + message = "User is not authorized for the given operation." + class TrialExpired(SubsonicAPIException): code = 402 api_code = 60 - message = ("The trial period for the Supysonic server is over." + message = ( + "The trial period for the Supysonic server is over." "But since it doesn't use any licensing you shouldn't be seeing this error ever." - "So something went wrong or you got scammed.") + "So something went wrong or you got scammed." + ) + class NotFound(SubsonicAPIException): code = 404 @@ -78,7 +91,8 @@ class NotFound(SubsonicAPIException): def __init__(self, entity, *args, **kwargs): super(NotFound, self).__init__(*args, **kwargs) - self.message = '{} not found'.format(entity) + self.message = "{} not found".format(entity) + class AggregateException(SubsonicAPIException): def __init__(self, exceptions, *args, **kwargs): @@ -88,7 +102,7 @@ class AggregateException(SubsonicAPIException): for exc in exceptions: if not isinstance(exc, SubsonicAPIException): # Try to convert regular exceptions to SubsonicAPIExceptions - handler = current_app._find_error_handler(exc) # meh + handler = current_app._find_error_handler(exc) # meh if handler: exc = handler(exc) assert isinstance(exc, SubsonicAPIException) @@ -96,14 +110,17 @@ class AggregateException(SubsonicAPIException): exc = GenericError(str(exc)) self.exceptions.append(exc) - def get_response(self, environ = None): + def get_response(self, environ=None): if len(self.exceptions) == 1: return self.exceptions[0].get_response() codes = set(exc.api_code for exc in self.exceptions) - errors = [ dict(code = exc.api_code, message = exc.message) for exc in self.exceptions ] + errors = [ + dict(code=exc.api_code, message=exc.message) for exc in self.exceptions + ] - rv = request.formatter('error', dict(code = list(codes)[0] if len(codes) == 1 else 0, error = errors)) + rv = request.formatter( + "error", dict(code=list(codes)[0] if len(codes) == 1 else 0, error=errors) + ) rv.status_code = self.code return rv - diff --git a/supysonic/api/formatters.py b/supysonic/api/formatters.py index 3ffa013..4448afb 100644 --- a/supysonic/api/formatters.py +++ b/supysonic/api/formatters.py @@ -13,12 +13,13 @@ from xml.etree import ElementTree from ..py23 import dict, strtype from . import API_VERSION + class BaseFormatter(object): def make_response(self, elem, data): raise NotImplementedError() def make_error(self, code, message): - return self.make_response('error', dict(code = code, message = message)) + return self.make_response("error", dict(code=code, message=message)) def make_empty(self): return self.make_response(None, None) @@ -29,10 +30,11 @@ class BaseFormatter(object): error = make_error empty = property(make_empty) + class JSONBaseFormatter(BaseFormatter): def __remove_empty_lists(self, d): if not isinstance(d, dict): - raise TypeError('Expecting a dict got ' + type(d).__name__) + raise TypeError("Expecting a dict got " + type(d).__name__) keys_to_remove = [] for key, value in d.items(): @@ -42,7 +44,12 @@ class JSONBaseFormatter(BaseFormatter): if len(value) == 0: keys_to_remove.append(key) else: - d[key] = [ self.__remove_empty_lists(item) if isinstance(item, dict) else item for item in value ] + d[key] = [ + self.__remove_empty_lists(item) + if isinstance(item, dict) + else item + for item in value + ] for key in keys_to_remove: del d[key] @@ -51,37 +58,39 @@ class JSONBaseFormatter(BaseFormatter): def _subsonicify(self, elem, data): if (elem is None) != (data is None): - raise ValueError('Expecting both elem and data or neither of them') + raise ValueError("Expecting both elem and data or neither of them") - rv = { - 'status': 'failed' if elem is 'error' else 'ok', - 'version': API_VERSION - } + rv = {"status": "failed" if elem is "error" else "ok", "version": API_VERSION} if data: rv[elem] = self.__remove_empty_lists(data) - return { 'subsonic-response': rv } + return {"subsonic-response": rv} + class JSONFormatter(JSONBaseFormatter): def make_response(self, elem, data): rv = jsonify(self._subsonicify(elem, data)) - rv.headers.add('Access-Control-Allow-Origin', '*') + rv.headers.add("Access-Control-Allow-Origin", "*") return rv + class JSONPFormatter(JSONBaseFormatter): def __init__(self, callback): self.__callback = callback def make_response(self, elem, data): if not self.__callback: - return jsonify(self._subsonicify('error', dict(code = 10, message = 'Missing callback'))) + return jsonify( + self._subsonicify("error", dict(code=10, message="Missing callback")) + ) rv = self._subsonicify(elem, data) - rv = '{}({})'.format(self.__callback, json.dumps(rv)) + rv = "{}({})".format(self.__callback, json.dumps(rv)) rv = make_response(rv) - rv.mimetype = 'application/javascript' + rv.mimetype = "application/javascript" return rv + class XMLFormatter(BaseFormatter): def __dict2xml(self, elem, dictionary): """Convert a dict structure to xml. The game is trivial. Nesting uses the [] parenthesis. @@ -93,12 +102,12 @@ class XMLFormatter(BaseFormatter): "status": "ok","version": "1.7.0","xmlns": "http://subsonic.org/restapi"}} """ if not isinstance(dictionary, dict): - raise TypeError('Expecting a dict') + raise TypeError("Expecting a dict") if not all(map(lambda x: isinstance(x, strtype), dictionary)): - raise TypeError('Dictionary keys must be strings') + raise TypeError("Dictionary keys must be strings") for name, value in dictionary.items(): - if name == 'value': + if name == "value": elem.text = self.__value_tostring(value) elif isinstance(value, dict): subelem = ElementTree.SubElement(elem, name) @@ -124,20 +133,19 @@ class XMLFormatter(BaseFormatter): def make_response(self, elem, data): if (elem is None) != (data is None): - raise ValueError('Expecting both elem and data or neither of them') + raise ValueError("Expecting both elem and data or neither of them") response = { - 'status': 'failed' if elem is 'error' else 'ok', - 'version': API_VERSION, - 'xmlns': "http://subsonic.org/restapi" + "status": "failed" if elem is "error" else "ok", + "version": API_VERSION, + "xmlns": "http://subsonic.org/restapi", } if elem: response[elem] = data - root = ElementTree.Element('subsonic-response') + root = ElementTree.Element("subsonic-response") self.__dict2xml(root, response) rv = make_response(ElementTree.tostring(root)) - rv.mimetype = 'text/xml' + rv.mimetype = "text/xml" return rv - diff --git a/supysonic/api/media.py b/supysonic/api/media.py index 49ae9a7..b76aa61 100644 --- a/supysonic/api/media.py +++ b/supysonic/api/media.py @@ -35,30 +35,45 @@ from ..db import Track, Album, Artist, Folder, User, ClientPrefs, now from ..py23 import dict from . import api, get_entity -from .exceptions import GenericError, MissingParameter, NotFound, ServerError, UnsupportedParameter +from .exceptions import ( + GenericError, + MissingParameter, + NotFound, + ServerError, + UnsupportedParameter, +) logger = logging.getLogger(__name__) -def prepare_transcoding_cmdline(base_cmdline, input_file, input_format, output_format, output_bitrate): + +def prepare_transcoding_cmdline( + base_cmdline, input_file, input_format, output_format, output_bitrate +): if not base_cmdline: return None ret = shlex.split(base_cmdline) ret = [ - part.replace('%srcpath', input_file).replace('%srcfmt', input_format).replace('%outfmt', output_format).replace('%outrate', str(output_bitrate)) + part.replace("%srcpath", input_file) + .replace("%srcfmt", input_format) + .replace("%outfmt", output_format) + .replace("%outrate", str(output_bitrate)) for part in ret ] return ret -@api.route('/stream.view', methods = [ 'GET', 'POST' ]) + +@api.route("/stream.view", methods=["GET", "POST"]) def stream_media(): res = get_entity(Track) - if 'timeOffset' in request.values: - raise UnsupportedParameter('timeOffset') - if 'size' in request.values: - raise UnsupportedParameter('size') + if "timeOffset" in request.values: + raise UnsupportedParameter("timeOffset") + if "size" in request.values: + raise UnsupportedParameter("size") - maxBitRate, format, estimateContentLength = map(request.values.get, [ 'maxBitRate', 'format', 'estimateContentLength' ]) + maxBitRate, format, estimateContentLength = map( + request.values.get, ["maxBitRate", "format", "estimateContentLength"] + ) if format: format = format.lower() @@ -79,39 +94,53 @@ def stream_media(): if dst_bitrate > maxBitRate and maxBitRate != 0: dst_bitrate = maxBitRate - if format and format != 'raw' and format != src_suffix: + if format and format != "raw" and format != src_suffix: dst_suffix = format - dst_mimetype = mimetypes.guess_type('dummyname.' + dst_suffix, False)[0] or 'application/octet-stream' + 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): + if format != "raw" and (dst_suffix != src_suffix or dst_bitrate != res.bitrate): # Requires transcoding cache = current_app.transcode_cache cache_key = "{}-{}.{}".format(res.id, dst_bitrate, dst_suffix) try: - response = send_file(cache.get(cache_key), mimetype=dst_mimetype, conditional=True) + response = send_file( + cache.get(cache_key), mimetype=dst_mimetype, conditional=True + ) except CacheMiss: - 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') + 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") if not transcoder and (not decoder or not encoder): - transcoder = config.get('transcoder') + transcoder = config.get("transcoder") if not transcoder: - message = 'No way to transcode from {} to {}'.format(src_suffix, dst_suffix) + message = "No way to transcode from {} to {}".format( + src_suffix, dst_suffix + ) logger.info(message) raise GenericError(message) - transcoder, decoder, encoder = map(lambda x: prepare_transcoding_cmdline(x, res.path, src_suffix, dst_suffix, dst_bitrate), [ transcoder, decoder, encoder ]) + transcoder, decoder, encoder = map( + lambda x: prepare_transcoding_cmdline( + x, res.path, src_suffix, dst_suffix, dst_bitrate + ), + [transcoder, decoder, encoder], + ) try: if transcoder: dec_proc = None - proc = subprocess.Popen(transcoder, stdout = subprocess.PIPE) + proc = subprocess.Popen(transcoder, stdout=subprocess.PIPE) else: - dec_proc = subprocess.Popen(decoder, stdout = subprocess.PIPE) - proc = subprocess.Popen(encoder, stdin = dec_proc.stdout, stdout = subprocess.PIPE) + dec_proc = subprocess.Popen(decoder, stdout=subprocess.PIPE) + proc = subprocess.Popen( + encoder, stdin=dec_proc.stdout, stdout=subprocess.PIPE + ) except OSError: - raise ServerError('Error while running the transcoding process') + raise ServerError("Error while running the transcoding process") def transcode(): try: @@ -120,7 +149,7 @@ def stream_media(): if not data: break yield data - except: # pragma: nocover + except: # pragma: nocover if dec_proc != None: dec_proc.kill() proc.kill() @@ -129,12 +158,19 @@ def stream_media(): if dec_proc != None: dec_proc.wait() proc.wait() + resp_content = cache.set_generated(cache_key, transcode) - 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)) + 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(resp_content, mimetype=dst_mimetype) - if estimateContentLength == 'true': - response.headers.add('Content-Length', dst_bitrate * 1000 * res.duration // 8) + if estimateContentLength == "true": + response.headers.add( + "Content-Length", dst_bitrate * 1000 * res.duration // 8 + ) else: response = send_file(res.path, mimetype=dst_mimetype, conditional=True) @@ -146,40 +182,44 @@ def stream_media(): return response -@api.route('/download.view', methods = [ 'GET', 'POST' ]) + +@api.route("/download.view", methods=["GET", "POST"]) def download_media(): - id = request.values['id'] + id = request.values["id"] uid = uuid.UUID(id) - try: # Track -> direct download + try: # Track -> direct download rv = Track[uid] - return send_file(rv.path, mimetype = rv.mimetype, conditional=True) + return send_file(rv.path, mimetype=rv.mimetype, conditional=True) except ObjectNotFound: pass - try: # Folder -> stream zipped tracks, non recursive + try: # Folder -> stream zipped tracks, non recursive rv = Folder[uid] except ObjectNotFound: - try: # Album -> stream zipped tracks + try: # Album -> stream zipped tracks rv = Album[uid] except ObjectNotFound: - raise NotFound('Track, Folder or Album') + raise NotFound("Track, Folder or Album") - z = ZipFile(compression = ZIP_DEFLATED) + z = ZipFile(compression=ZIP_DEFLATED) for track in rv.tracks: z.write(track.path, os.path.basename(track.path)) - resp = Response(z, mimetype = 'application/zip') - resp.headers['Content-Disposition'] = 'attachment; filename={}.zip'.format(rv.name) + resp = Response(z, mimetype="application/zip") + resp.headers["Content-Disposition"] = "attachment; filename={}.zip".format(rv.name) return resp -@api.route('/getCoverArt.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getCoverArt.view", methods=["GET", "POST"]) def cover_art(): cache = current_app.cache - eid = request.values['id'] + eid = request.values["id"] if Folder.exists(id=eid): res = get_entity(Folder) - if not res.cover_art or not os.path.isfile(os.path.join(res.path, res.cover_art)): - raise NotFound('Cover art') + if not res.cover_art or not os.path.isfile( + os.path.join(res.path, res.cover_art) + ): + raise NotFound("Cover art") cover_path = os.path.join(res.path, res.cover_art) elif Track.exists(id=eid): cache_key = "{}-cover".format(eid) @@ -189,19 +229,19 @@ def cover_art(): res = get_entity(Track) art = res.extract_cover_art() if not art: - raise NotFound('Cover art') + raise NotFound("Cover art") cover_path = cache.set(cache_key, art) else: - raise NotFound('Entity') + raise NotFound("Entity") - size = request.values.get('size') + size = request.values.get("size") if size: size = int(size) else: return send_file(cover_path) im = Image.open(cover_path) - mimetype = 'image/{}'.format(im.format.lower()) + mimetype = "image/{}".format(im.format.lower()) if size > im.width and size > im.height: return send_file(cover_path, mimetype=mimetype) @@ -214,77 +254,81 @@ def cover_art(): im.save(fp, im.format) return send_file(cache.get(cache_key), mimetype=mimetype) -@api.route('/getLyrics.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getLyrics.view", methods=["GET", "POST"]) def lyrics(): - artist = request.values['artist'] - title = request.values['title'] + artist = request.values["artist"] + title = request.values["title"] query = Track.select(lambda t: title in t.title and artist in t.artist.name) for track in query: - lyrics_path = os.path.splitext(track.path)[0] + '.txt' + lyrics_path = os.path.splitext(track.path)[0] + ".txt" if os.path.exists(lyrics_path): - logger.debug('Found lyrics file: ' + lyrics_path) + 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. - logger.warning('Unsupported encoding for lyrics file ' + lyrics_path) + logger.warning("Unsupported encoding for lyrics file " + lyrics_path) continue - return request.formatter('lyrics', dict( - artist = track.album.artist.name, - title = track.title, - value = lyrics - )) + return request.formatter( + "lyrics", + dict(artist=track.album.artist.name, title=track.title, value=lyrics), + ) # Create a stable, unique, filesystem-compatible identifier for the artist+title - unique = hashlib.md5(json.dumps([x.lower() for x in (artist, title)]).encode('utf-8')).hexdigest() + unique = hashlib.md5( + json.dumps([x.lower() for x in (artist, title)]).encode("utf-8") + ).hexdigest() cache_key = "lyrics-{}".format(unique) lyrics = dict() try: lyrics = json.loads( - zlib.decompress( - current_app.cache.get_value(cache_key) - ).decode('utf-8') + zlib.decompress(current_app.cache.get_value(cache_key)).decode("utf-8") ) except (CacheMiss, zlib.error, TypeError, ValueError): try: - r = requests.get("http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect", - params={'artist': artist, 'song': title}, timeout=5) + r = requests.get( + "http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect", + params={"artist": artist, "song": title}, + timeout=5, + ) root = ElementTree.fromstring(r.content) - ns = {'cl': 'http://api.chartlyrics.com/'} + ns = {"cl": "http://api.chartlyrics.com/"} lyrics = dict( - artist = root.find('cl:LyricArtist', namespaces=ns).text, - title = root.find('cl:LyricSong', namespaces=ns).text, - value = root.find('cl:Lyric', namespaces=ns).text + artist=root.find("cl:LyricArtist", namespaces=ns).text, + title=root.find("cl:LyricSong", namespaces=ns).text, + value=root.find("cl:Lyric", namespaces=ns).text, ) current_app.cache.set( - cache_key, zlib.compress(json.dumps(lyrics).encode('utf-8'), 9) + cache_key, zlib.compress(json.dumps(lyrics).encode("utf-8"), 9) ) - except requests.exceptions.RequestException as e: # pragma: nocover - logger.warning('Error while requesting the ChartLyrics API: ' + str(e)) + except requests.exceptions.RequestException as e: # pragma: nocover + logger.warning("Error while requesting the ChartLyrics API: " + str(e)) + + return request.formatter("lyrics", lyrics) - return request.formatter('lyrics', lyrics) def read_file_as_unicode(path): """ Opens a file trying with different encodings and returns the contents as a unicode string """ - encodings = [ 'utf-8', 'latin1' ] # Should be extended to support more encodings + encodings = ["utf-8", "latin1"] # Should be extended to support more encodings for enc in encodings: try: - contents = codecs.open(path, 'r', encoding = enc).read() - logger.debug('Read file {} with {} encoding'.format(path, enc)) + contents = codecs.open(path, "r", encoding=enc).read() + 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 - logger.debug('Reading file {} with ascii encoding'.format(path)) - return unicode(open(path, 'r').read()) + 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 981bd25..f7315bc 100644 --- a/supysonic/api/playlists.py +++ b/supysonic/api/playlists.py @@ -17,38 +17,50 @@ from ..py23 import dict from . import api, get_entity from .exceptions import Forbidden, MissingParameter, NotFound -@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) - username = request.values.get('username') +@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) + + username = request.values.get("username") if username: if not request.user.admin: raise Forbidden() - user = User.get(name = username) + user = User.get(name=username) if user is None: - raise NotFound('User') + raise NotFound("User") - query = Playlist.select(lambda p: p.user.name == username).order_by(Playlist.name) + query = Playlist.select(lambda p: p.user.name == username).order_by( + Playlist.name + ) - return request.formatter('playlists', dict(playlist = [ p.as_subsonic_playlist(request.user) for p in query ] )) + return request.formatter( + "playlists", + dict(playlist=[p.as_subsonic_playlist(request.user) for p in query]), + ) -@api.route('/getPlaylist.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getPlaylist.view", methods=["GET", "POST"]) def show_playlist(): res = get_entity(Playlist) if res.user.id != request.user.id and not res.public and not request.user.admin: raise Forbidden() info = res.as_subsonic_playlist(request.user) - info['entry'] = [ t.as_subsonic_child(request.user, request.client) for t in res.get_tracks() ] - return request.formatter('playlist', info) + info["entry"] = [ + t.as_subsonic_child(request.user, request.client) for t in res.get_tracks() + ] + return request.formatter("playlist", info) -@api.route('/createPlaylist.view', methods = [ 'GET', 'POST' ]) + +@api.route("/createPlaylist.view", methods=["GET", "POST"]) def create_playlist(): - playlist_id, name = map(request.values.get, [ 'playlistId', 'name' ]) + playlist_id, name = map(request.values.get, ["playlistId", "name"]) # songId actually doesn't seem to be required - songs = request.values.getlist('songId') + songs = request.values.getlist("songId") playlist_id = uuid.UUID(playlist_id) if playlist_id else None if playlist_id: @@ -61,9 +73,9 @@ def create_playlist(): if name: playlist.name = name elif name: - playlist = Playlist(user = request.user, name = name) + playlist = Playlist(user=request.user, name=name) else: - raise MissingParameter('playlistId or name') + raise MissingParameter("playlistId or name") for sid in songs: sid = uuid.UUID(sid) @@ -72,7 +84,8 @@ def create_playlist(): return request.formatter.empty -@api.route('/deletePlaylist.view', methods = [ 'GET', 'POST' ]) + +@api.route("/deletePlaylist.view", methods=["GET", "POST"]) def delete_playlist(): res = get_entity(Playlist) if res.user.id != request.user.id and not request.user.admin: @@ -81,22 +94,25 @@ def delete_playlist(): res.delete() return request.formatter.empty -@api.route('/updatePlaylist.view', methods = [ 'GET', 'POST' ]) + +@api.route("/updatePlaylist.view", methods=["GET", "POST"]) def update_playlist(): - res = get_entity(Playlist, 'playlistId') + res = get_entity(Playlist, "playlistId") if res.user.id != request.user.id and not request.user.admin: raise Forbidden() playlist = res - name, comment, public = map(request.values.get, [ 'name', 'comment', 'public' ]) - to_add, to_remove = map(request.values.getlist, [ 'songIdToAdd', 'songIndexToRemove' ]) + name, comment, public = map(request.values.get, ["name", "comment", "public"]) + to_add, to_remove = map( + request.values.getlist, ["songIdToAdd", "songIndexToRemove"] + ) if name: playlist.name = name if comment: playlist.comment = comment if public: - playlist.public = public in (True, 'True', 'true', 1, '1') + playlist.public = public in (True, "True", "true", 1, "1") to_add = map(uuid.UUID, to_add) to_remove = map(int, to_remove) @@ -108,4 +124,3 @@ def update_playlist(): playlist.remove_at_indexes(to_remove) return request.formatter.empty - diff --git a/supysonic/api/search.py b/supysonic/api/search.py index 758e7d1..32b14bc 100644 --- a/supysonic/api/search.py +++ b/supysonic/api/search.py @@ -18,9 +18,13 @@ from ..py23 import dict from . import api from .exceptions import MissingParameter -@api.route('/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' ]) + artist, album, title, anyf, count, offset, newer_than = map( + request.values.get, + ["artist", "album", "title", "any", "count", "offset", "newerThan"], + ) count = int(count) if count else 20 offset = int(offset) if offset else 0 @@ -28,9 +32,17 @@ def old_search(): min_date = datetime.fromtimestamp(newer_than) if artist: - query = select(t.folder.parent for t in Track if artist in t.folder.parent.name and t.folder.parent.created > min_date) + query = select( + t.folder.parent + for t in Track + if artist in t.folder.parent.name and t.folder.parent.created > min_date + ) elif album: - query = select(t.folder for t in Track if album in t.folder.name and t.folder.created > min_date) + query = select( + t.folder + for t in Track + if album in t.folder.name and t.folder.created > min_date + ) elif title: query = Track.select(lambda t: title in t.title and t.created > min_date) elif anyf: @@ -41,65 +53,122 @@ def old_search(): if offset + count > fcount: toff = max(0, offset - fcount) tend = offset + count - fcount - res = res[:] + tracks[toff : tend][:] + res = res[:] + tracks[toff:tend][:] - return request.formatter('searchResult', dict( - totalHits = folders.count() + tracks.count(), - offset = offset, - match = [ r.as_subsonic_child(request.user) if isinstance(r, Folder) else r.as_subsonic_child(request.user, request.client) for r in res ] - )) + return request.formatter( + "searchResult", + dict( + totalHits=folders.count() + tracks.count(), + offset=offset, + match=[ + r.as_subsonic_child(request.user) + if isinstance(r, Folder) + else r.as_subsonic_child(request.user, request.client) + for r in res + ], + ), + ) else: - raise MissingParameter('search') + raise MissingParameter("search") - return request.formatter('searchResult', dict( - totalHits = query.count(), - offset = offset, - 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] ] - )) + return request.formatter( + "searchResult", + dict( + totalHits=query.count(), + offset=offset, + 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] + ], + ), + ) -@api.route('/search2.view', methods = [ 'GET', 'POST' ]) + +@api.route("/search2.view", methods=["GET", "POST"]) def new_search(): - query = request.values['query'] + query = request.values["query"] artist_count, artist_offset, album_count, album_offset, song_count, song_offset = map( - request.values.get, [ 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ]) + request.values.get, + [ + "artistCount", + "artistOffset", + "albumCount", + "albumOffset", + "songCount", + "songOffset", + ], + ) - artist_count = int(artist_count) if artist_count else 20 + artist_count = int(artist_count) if artist_count else 20 artist_offset = int(artist_offset) if artist_offset else 0 - album_count = int(album_count) if album_count else 20 - album_offset = int(album_offset) if album_offset else 0 - song_count = int(song_count) if song_count else 20 - song_offset = int(song_offset) if song_offset else 0 + album_count = int(album_count) if album_count else 20 + album_offset = int(album_offset) if album_offset else 0 + song_count = int(song_count) if song_count else 20 + song_offset = int(song_offset) if song_offset else 0 - artists = select(t.folder.parent for t in Track if query in t.folder.parent.name).limit(artist_count, artist_offset) - albums = select(t.folder for t in Track if query in t.folder.name).limit(album_count, album_offset) + artists = select( + t.folder.parent for t in Track if query in t.folder.parent.name + ).limit(artist_count, artist_offset) + albums = select(t.folder for t in Track if query in t.folder.name).limit( + album_count, album_offset + ) songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset) - return request.formatter('searchResult2', OrderedDict(( - ('artist', [ dict(id = str(a.id), name = a.name) for a in artists ]), - ('album', [ f.as_subsonic_child(request.user) for f in albums ]), - ('song', [ t.as_subsonic_child(request.user, request.client) for t in songs ]) - ))) + return request.formatter( + "searchResult2", + OrderedDict( + ( + ("artist", [dict(id=str(a.id), name=a.name) for a in artists]), + ("album", [f.as_subsonic_child(request.user) for f in albums]), + ( + "song", + [t.as_subsonic_child(request.user, request.client) for t in songs], + ), + ) + ), + ) -@api.route('/search3.view', methods = [ 'GET', 'POST' ]) + +@api.route("/search3.view", methods=["GET", "POST"]) def search_id3(): - query = request.values['query'] + query = request.values["query"] artist_count, artist_offset, album_count, album_offset, song_count, song_offset = map( - request.values.get, [ 'artistCount', 'artistOffset', 'albumCount', 'albumOffset', 'songCount', 'songOffset' ]) + request.values.get, + [ + "artistCount", + "artistOffset", + "albumCount", + "albumOffset", + "songCount", + "songOffset", + ], + ) - artist_count = int(artist_count) if artist_count else 20 + artist_count = int(artist_count) if artist_count else 20 artist_offset = int(artist_offset) if artist_offset else 0 - album_count = int(album_count) if album_count else 20 - album_offset = int(album_offset) if album_offset else 0 - song_count = int(song_count) if song_count else 20 - song_offset = int(song_offset) if song_offset else 0 + album_count = int(album_count) if album_count else 20 + album_offset = int(album_offset) if album_offset else 0 + song_count = int(song_count) if song_count else 20 + song_offset = int(song_offset) if song_offset else 0 - artists = Artist.select(lambda a: query in a.name).limit(artist_count, artist_offset) + artists = Artist.select(lambda a: query in a.name).limit( + artist_count, artist_offset + ) albums = Album.select(lambda a: query in a.name).limit(album_count, album_offset) songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset) - return request.formatter('searchResult3', OrderedDict(( - ('artist', [ a.as_subsonic_artist(request.user) for a in artists ]), - ('album', [ a.as_subsonic_album(request.user) for a in albums ]), - ('song', [ t.as_subsonic_child(request.user, request.client) for t in songs ]) - ))) - + return request.formatter( + "searchResult3", + OrderedDict( + ( + ("artist", [a.as_subsonic_artist(request.user) for a in artists]), + ("album", [a.as_subsonic_album(request.user) for a in albums]), + ( + "song", + [t.as_subsonic_child(request.user, request.client) for t in songs], + ), + ) + ), + ) diff --git a/supysonic/api/system.py b/supysonic/api/system.py index 6776078..d0c8587 100644 --- a/supysonic/api/system.py +++ b/supysonic/api/system.py @@ -12,11 +12,12 @@ from flask import request from ..py23 import dict from . import api -@api.route('/ping.view', methods = [ 'GET', 'POST' ]) + +@api.route("/ping.view", methods=["GET", "POST"]) def ping(): return request.formatter.empty -@api.route('/getLicense.view', methods = [ 'GET', 'POST' ]) -def license(): - return request.formatter('license', dict(valid = True)) +@api.route("/getLicense.view", methods=["GET", "POST"]) +def license(): + return request.formatter("license", dict(valid=True)) diff --git a/supysonic/api/unsupported.py b/supysonic/api/unsupported.py index 6e12a5c..dd46dfc 100644 --- a/supysonic/api/unsupported.py +++ b/supysonic/api/unsupported.py @@ -11,12 +11,20 @@ from . import api from .exceptions import GenericError methods = ( - 'getVideos', 'getAvatar', 'getShares', 'createShare', 'updateShare', 'deleteShare', + "getVideos", + "getAvatar", + "getShares", + "createShare", + "updateShare", + "deleteShare", ) + def unsupported(): - return GenericError('Not supported by Supysonic'), 501 + return GenericError("Not supported by Supysonic"), 501 + for m in methods: - api.add_url_rule('/{}.view'.format(m), 'unsupported', unsupported, methods = [ 'GET', 'POST' ]) - + api.add_url_rule( + "/{}.view".format(m), "unsupported", unsupported, methods=["GET", "POST"] + ) diff --git a/supysonic/api/user.py b/supysonic/api/user.py index 85cf6c0..b906572 100644 --- a/supysonic/api/user.py +++ b/supysonic/api/user.py @@ -17,58 +17,67 @@ from ..py23 import dict from . import api, decode_password from .exceptions import Forbidden, GenericError, NotFound + def admin_only(f): @wraps(f) def decorated(*args, **kwargs): if not request.user.admin: raise Forbidden() return f(*args, **kwargs) + return decorated -@api.route('/getUser.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getUser.view", methods=["GET", "POST"]) def user_info(): - username = request.values['username'] + username = request.values["username"] if username != request.user.name and not request.user.admin: raise Forbidden() - user = User.get(name = username) + user = User.get(name=username) if user is None: - raise NotFound('User') + raise NotFound("User") - return request.formatter('user', user.as_subsonic_user()) + return request.formatter("user", user.as_subsonic_user()) -@api.route('/getUsers.view', methods = [ 'GET', 'POST' ]) + +@api.route("/getUsers.view", methods=["GET", "POST"]) @admin_only def users_info(): - return request.formatter('users', dict(user = [ u.as_subsonic_user() for u in User.select() ] )) + return request.formatter( + "users", dict(user=[u.as_subsonic_user() for u in User.select()]) + ) -@api.route('/createUser.view', methods = [ 'GET', 'POST' ]) + +@api.route("/createUser.view", methods=["GET", "POST"]) @admin_only def user_add(): - username = request.values['username'] - password = request.values['password'] - email = request.values['email'] - admin = request.values.get('adminRole') - admin = True if admin in (True, 'True', 'true', 1, '1') else False + username = request.values["username"] + password = request.values["password"] + email = request.values["email"] + admin = request.values.get("adminRole") + admin = True if admin in (True, "True", "true", 1, "1") else False password = decode_password(password) UserManager.add(username, password, email, admin) return request.formatter.empty -@api.route('/deleteUser.view', methods = [ 'GET', 'POST' ]) + +@api.route("/deleteUser.view", methods=["GET", "POST"]) @admin_only def user_del(): - username = request.values['username'] + username = request.values["username"] UserManager.delete_by_name(username) return request.formatter.empty -@api.route('/changePassword.view', methods = [ 'GET', 'POST' ]) + +@api.route("/changePassword.view", methods=["GET", "POST"]) def user_changepass(): - username = request.values['username'] - password = request.values['password'] + username = request.values["username"] + password = request.values["password"] if username != request.user.name and not request.user.admin: raise Forbidden() @@ -77,4 +86,3 @@ def user_changepass(): UserManager.change_password2(username, password) return request.formatter.empty - diff --git a/supysonic/cache.py b/supysonic/cache.py index 0953977..0b4bd1c 100644 --- a/supysonic/cache.py +++ b/supysonic/cache.py @@ -26,19 +26,23 @@ logger = logging.getLogger(__name__) class CacheMiss(KeyError): """The requested data is not in the cache""" + pass class ProtectedError(Exception): """The data cannot be purged from the cache""" + pass CacheEntry = namedtuple("CacheEntry", ["size", "expires"]) NULL_ENTRY = CacheEntry(0, 0) + class Cache(object): """Provides a common interface for caching files to disk""" + # Modeled after werkzeug.contrib.cache.FileSystemCache # keys must be filename-compatible strings (no paths) @@ -73,9 +77,13 @@ class Cache(object): # Make a key -> CacheEntry(size, expiry) map ordered by mtime self._size = 0 self._files = OrderedDict() - for mtime, size, key in sorted([(f.stat().st_mtime, f.stat().st_size, f.name) - for f in scandir(self._cache_dir) - if f.is_file()]): + for mtime, size, key in sorted( + [ + (f.stat().st_mtime, f.stat().st_size, f.name) + for f in scandir(self._cache_dir) + if f.is_file() + ] + ): self._files[key] = CacheEntry(size, mtime + self.min_time) self._size += size @@ -138,7 +146,9 @@ class Cache(object): ... json.dump(some_data, fp) """ try: - with tempfile.NamedTemporaryFile(dir=self._cache_dir, suffix=".part", delete=True) as f: + with tempfile.NamedTemporaryFile( + dir=self._cache_dir, suffix=".part", delete=True + ) as f: yield f # seek to end and get position to get filesize @@ -185,7 +195,7 @@ class Cache(object): @contextlib.contextmanager def get_fileobj(self, key): """Yields a file object that can be used to read cached bytes""" - with open(self.get(key), 'rb') as f: + with open(self.get(key), "rb") as f: yield f def get_value(self, key): diff --git a/supysonic/cli.py b/supysonic/cli.py index 7d444d5..540ed65 100755 --- a/supysonic/cli.py +++ b/supysonic/cli.py @@ -24,8 +24,9 @@ from .managers.folder import FolderManager from .managers.user import UserManager from .scanner import Scanner + class TimedProgressDisplay: - def __init__(self, stdout, interval = 5): + def __init__(self, stdout, interval=5): self.__stdout = stdout self.__interval = interval self.__last_display = 0 @@ -34,35 +35,39 @@ class TimedProgressDisplay: def __call__(self, name, scanned): if time.time() - self.__last_display > self.__interval: progress = "Scanning '{0}': {1} files scanned".format(name, scanned) - self.__stdout.write('\b' * self.__last_len) + self.__stdout.write("\b" * self.__last_len) self.__stdout.write(progress) self.__stdout.flush() self.__last_len = len(progress) self.__last_display = time.time() + class CLIParser(argparse.ArgumentParser): def error(self, message): self.print_usage(sys.stderr) raise RuntimeError(message) + class SupysonicCLI(cmd.Cmd): prompt = "supysonic> " def _make_do(self, command): def method(obj, line): try: - args = getattr(obj, command + '_parser').parse_args(line.split()) + args = getattr(obj, command + "_parser").parse_args(line.split()) except RuntimeError as e: self.write_error_line(str(e)) return - if hasattr(obj.__class__, command + '_subparsers'): + if hasattr(obj.__class__, command + "_subparsers"): try: - func = getattr(obj, '{}_{}'.format(command, args.action)) + 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' }) + return func( + **{key: vars(args)[key] for key in vars(args) if key != "action"} + ) else: try: func = getattr(obj, command) @@ -81,26 +86,39 @@ class SupysonicCLI(cmd.Cmd): self.stderr = sys.stderr self.__config = config - self.__daemon = DaemonClient(config.DAEMON['socket']) + self.__daemon = DaemonClient(config.DAEMON["socket"]) # Generate do_* and help_* methods - for parser_name in filter(lambda attr: attr.endswith('_parser') and '_' not in attr[:-7], dir(self.__class__)): + 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.items(): - 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.items(): + setattr( + self, "help_{} {}".format(command, action), subparser.print_help + ) - def write_line(self, line = ''): - self.stdout.write(line + '\n') + def write_line(self, line=""): + self.stdout.write(line + "\n") - def write_error_line(self, line = ''): - self.stderr.write(line + '\n') + def write_error_line(self, line=""): + self.stderr.write(line + "\n") def do_EOF(self, line): return True @@ -108,7 +126,7 @@ class SupysonicCLI(cmd.Cmd): do_exit = do_EOF def default(self, line): - self.write_line('Unknown command %s' % line.split()[0]) + self.write_line("Unknown command %s" % line.split()[0]) self.do_help(None) def postloop(self): @@ -116,34 +134,65 @@ class SupysonicCLI(cmd.Cmd): def completedefault(self, text, line, begidx, endidx): command = line.split()[0] - parsers = getattr(self.__class__, command + '_subparsers', None) + parsers = getattr(self.__class__, command + "_subparsers", None) if not parsers: return [] - num_words = len(line[len(command):begidx].split()) + num_words = len(line[len(command) : begidx].split()) if num_words == 0: - return [ a for a in parsers.choices if a.startswith(text) ] + return [a for a in parsers.choices 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", + ) folder_scan_target_group = folder_scan_parser.add_mutually_exclusive_group() - folder_scan_target_group.add_argument('--background', action = 'store_true', help = 'Scan the folder(s) in the background. Requires the daemon to be running.') - folder_scan_target_group.add_argument('--foreground', action = 'store_true', help = 'Scan the folder(s) in the foreground, blocking the processus while the scan is running.') + folder_scan_target_group.add_argument( + "--background", + action="store_true", + help="Scan the folder(s) in the background. Requires the daemon to be running.", + ) + folder_scan_target_group.add_argument( + "--foreground", + action="store_true", + help="Scan the folder(s) in the foreground, blocking the processus while the scan is running.", + ) @db_session def folder_list(self): - self.write_line('Name\t\tPath\n----\t\t----') - self.write_line('\n'.join('{0: <16}{1}'.format(f.name, f.path) for f in Folder.select(lambda f: f.root))) + self.write_line("Name\t\tPath\n----\t\t----") + self.write_line( + "\n".join( + "{0: <16}{1}".format(f.name, f.path) + for f in Folder.select(lambda f: f.root) + ) + ) @db_session def folder_add(self, name, path): @@ -167,13 +216,17 @@ class SupysonicCLI(cmd.Cmd): try: self.__folder_scan_background(folders, force) except DaemonUnavailableError: - self.write_error_line("Couldn't connect to the daemon, scanning in foreground") + self.write_error_line( + "Couldn't connect to the daemon, scanning in foreground" + ) self.__folder_scan_foreground(folders, force) elif background: try: self.__folder_scan_background(folders, force) except DaemonUnavailableError: - self.write_error_line("Couldn't connect to the daemon, please use the '--foreground' option") + self.write_error_line( + "Couldn't connect to the daemon, please use the '--foreground' option" + ) elif foreground: self.__folder_scan_foreground(folders, force) @@ -184,25 +237,34 @@ class SupysonicCLI(cmd.Cmd): try: progress = self.__daemon.get_scanning_progress() if progress is not None: - self.write_error_line("The daemon is currently scanning, can't start a scan now") + self.write_error_line( + "The daemon is currently scanning, can't start a scan now" + ) return except DaemonUnavailableError: pass - extensions = self.__config.BASE['scanner_extensions'] + extensions = self.__config.BASE["scanner_extensions"] if extensions: - extensions = extensions.split(' ') + extensions = extensions.split(" ") - scanner = Scanner(force = force, extensions = extensions, progress = TimedProgressDisplay(self.stdout), - on_folder_start = self.__unwatch_folder, on_folder_end = self.__watch_folder) + scanner = Scanner( + force=force, + extensions=extensions, + progress=TimedProgressDisplay(self.stdout), + on_folder_start=self.__unwatch_folder, + on_folder_end=self.__watch_folder, + ) if folders: fstrs = folders with db_session: - folders = select(f.name for f in Folder if f.root and f.name in fstrs)[:] + folders = select(f.name for f in Folder if f.root and f.name in fstrs)[ + : + ] notfound = set(fstrs) - set(folders) if notfound: - self.write_line("No such folder(s): " + ' '.join(notfound)) + self.write_line("No such folder(s): " + " ".join(notfound)) for folder in folders: scanner.queue_folder(folder) else: @@ -213,47 +275,86 @@ class SupysonicCLI(cmd.Cmd): scanner.run() stats = scanner.stats() - self.write_line('Scanning done') - self.write_line('Added: {0.artists} artists, {0.albums} albums, {0.tracks} tracks'.format(stats.added)) - self.write_line('Deleted: {0.artists} artists, {0.albums} albums, {0.tracks} tracks'.format(stats.deleted)) + self.write_line("Scanning done") + self.write_line( + "Added: {0.artists} artists, {0.albums} albums, {0.tracks} tracks".format( + stats.added + ) + ) + self.write_line( + "Deleted: {0.artists} artists, {0.albums} albums, {0.tracks} tracks".format( + stats.deleted + ) + ) if stats.errors: - self.write_line('Errors in:') + self.write_line("Errors in:") for err in stats.errors: - self.write_line('- ' + err) + self.write_line("- " + err) def __unwatch_folder(self, folder): - try: self.__daemon.remove_watched_folder(folder.path) - except DaemonUnavailableError: pass + try: + self.__daemon.remove_watched_folder(folder.path) + except DaemonUnavailableError: + pass def __watch_folder(self, folder): - try: self.__daemon.add_watched_folder(folder.path) - except DaemonUnavailableError: pass + try: + self.__daemon.add_watched_folder(folder.path) + except DaemonUnavailableError: + pass - 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") @db_session def user_list(self): - self.write_line('Name\t\tAdmin\tEmail\n----\t\t-----\t-----') - self.write_line('\n'.join('{0: <16}{1}\t{2}'.format(u.name, '*' if u.admin else '', u.mail) for u in User.select())) + self.write_line("Name\t\tAdmin\tEmail\n----\t\t-----\t-----") + self.write_line( + "\n".join( + "{0: <16}{1}\t{2}".format(u.name, "*" if u.admin else "", u.mail) + for u in User.select() + ) + ) - def _ask_password(self): # pragma: nocover + def _ask_password(self): # pragma: nocover password = getpass.getpass() - confirm = getpass.getpass('Confirm password: ') + confirm = getpass.getpass("Confirm password: ") if password != confirm: raise ValueError("Passwords don't match") return password @@ -262,7 +363,7 @@ class SupysonicCLI(cmd.Cmd): def user_add(self, name, admin, password, email): try: if not password: - password = self._ask_password() # pragma: nocover + password = self._ask_password() # pragma: nocover UserManager.add(name, password, email, admin) except ValueError as e: self.write_error_line(str(e)) @@ -277,34 +378,38 @@ class SupysonicCLI(cmd.Cmd): @db_session def user_setadmin(self, name, off): - user = User.get(name = name) + user = User.get(name=name) if user is None: - self.write_error_line('No such user') + self.write_error_line("No such user") else: user.admin = not off - self.write_line("{0} '{1}' admin rights".format('Revoked' if off else 'Granted', name)) + self.write_line( + "{0} '{1}' admin rights".format("Revoked" if off else "Granted", name) + ) @db_session def user_changepass(self, name, password): try: if not password: - password = self._ask_password() # pragma: nocover + password = self._ask_password() # pragma: nocover UserManager.change_password2(name, password) self.write_line("Successfully changed '{}' password".format(name)) except ObjectNotFound as e: self.write_error_line(str(e)) + def main(): config = IniConfig.from_common_locations() - init_database(config.BASE['database_uri']) + init_database(config.BASE["database_uri"]) cli = SupysonicCLI(config) if len(sys.argv) > 1: - cli.onecmd(' '.join(sys.argv[1:])) + cli.onecmd(" ".join(sys.argv[1:])) else: cli.cmdloop() release_database() + if __name__ == "__main__": main() diff --git a/supysonic/config.py b/supysonic/config.py index 5dc4da3..dade606 100644 --- a/supysonic/config.py +++ b/supysonic/config.py @@ -17,50 +17,50 @@ import os import tempfile current_config = None + + def get_current_config(): return current_config or DefaultConfig() + class DefaultConfig(object): DEBUG = False - tempdir = os.path.join(tempfile.gettempdir(), 'supysonic') + tempdir = os.path.join(tempfile.gettempdir(), "supysonic") BASE = { - 'database_uri': 'sqlite:///' + os.path.join(tempdir, 'supysonic.db'), - 'scanner_extensions': None + "database_uri": "sqlite:///" + os.path.join(tempdir, "supysonic.db"), + "scanner_extensions": None, } WEBAPP = { - 'cache_dir': tempdir, - 'cache_size': 1024, - 'transcode_cache_size': 512, - 'log_file': None, - 'log_level': 'WARNING', - - 'mount_webui': True, - 'mount_api': True + "cache_dir": tempdir, + "cache_size": 1024, + "transcode_cache_size": 512, + "log_file": None, + "log_level": "WARNING", + "mount_webui": True, + "mount_api": True, } DAEMON = { - 'socket': os.path.join(tempdir, 'supysonic.sock'), - 'run_watcher': True, - 'wait_delay': 5, - 'log_file': None, - 'log_level': 'WARNING' - } - LASTFM = { - 'api_key': None, - 'secret': None + "socket": os.path.join(tempdir, "supysonic.sock"), + "run_watcher": True, + "wait_delay": 5, + "log_file": None, + "log_level": "WARNING", } + LASTFM = {"api_key": None, "secret": None} TRANSCODING = {} MIMETYPES = {} def __init__(self): current_config = self + class IniConfig(DefaultConfig): common_paths = [ - '/etc/supysonic', - os.path.expanduser('~/.supysonic'), - os.path.expanduser('~/.config/supysonic/supysonic.conf'), - 'supysonic.conf' + "/etc/supysonic", + os.path.expanduser("~/.supysonic"), + os.path.expanduser("~/.config/supysonic/supysonic.conf"), + "supysonic.conf", ] def __init__(self, paths): @@ -70,7 +70,7 @@ class IniConfig(DefaultConfig): parser.read(paths) for section in parser.sections(): - options = { k: self.__try_parse(v) for k, v in parser.items(section) } + options = {k: self.__try_parse(v) for k, v in parser.items(section)} section = section.upper() if hasattr(self, section): @@ -87,13 +87,12 @@ class IniConfig(DefaultConfig): return float(value) except ValueError: lv = value.lower() - if lv in ('yes', 'true', 'on'): + if lv in ("yes", "true", "on"): return True - elif lv in ('no', 'false', 'off'): + elif lv in ("no", "false", "off"): return False return value @classmethod def from_common_locations(cls): return IniConfig(cls.common_paths) - diff --git a/supysonic/covers.py b/supysonic/covers.py index c0a9ef3..e683d0b 100644 --- a/supysonic/covers.py +++ b/supysonic/covers.py @@ -12,24 +12,26 @@ import re from PIL import Image -EXTENSIONS = ('.jpg', '.jpeg', '.png', '.bmp') +EXTENSIONS = (".jpg", ".jpeg", ".png", ".bmp") NAMING_SCORE_RULES = ( - ('cover', 5), - ('albumart', 5), - ('folder', 5), - ('front', 10), - ('back', -10), - ('large', 2), - ('small', -2) + ("cover", 5), + ("albumart", 5), + ("folder", 5), + ("front", 10), + ("back", -10), + ("large", 2), + ("small", -2), ) + class CoverFile(object): - __clean_regex = re.compile(r'[^a-z]') + __clean_regex = re.compile(r"[^a-z]") + @staticmethod def __clean_name(name): - return CoverFile.__clean_regex.sub('', name.lower()) + return CoverFile.__clean_regex.sub("", name.lower()) - def __init__(self, name, album_name = None): + def __init__(self, name, album_name=None): self.name = name self.score = 0 @@ -44,6 +46,7 @@ class CoverFile(object): if clean in album_name or album_name in clean: self.score += 20 + def is_valid_cover(path): if not os.path.isfile(path): return False @@ -52,15 +55,16 @@ def is_valid_cover(path): if ext.lower() not in EXTENSIONS: return False - try: # Ensure the image can be read + try: # Ensure the image can be read with Image.open(path): return True except IOError: return False -def find_cover_in_folder(path, album_name = None): + +def find_cover_in_folder(path, album_name=None): if not os.path.isdir(path): - raise ValueError('Invalid path') + raise ValueError("Invalid path") candidates = [] for f in os.listdir(path): @@ -80,5 +84,4 @@ def find_cover_in_folder(path, album_name = None): if len(candidates) == 1: return candidates[0] - return sorted(candidates, key = lambda c: c.score, reverse = True)[0] - + return sorted(candidates, key=lambda c: c.score, reverse=True)[0] diff --git a/supysonic/daemon/__init__.py b/supysonic/daemon/__init__.py index 18591f6..9833732 100644 --- a/supysonic/daemon/__init__.py +++ b/supysonic/daemon/__init__.py @@ -18,27 +18,31 @@ from .server import Daemon from ..config import IniConfig from ..db import init_database, release_database -__all__ = [ 'Daemon', 'DaemonClient' ] +__all__ = ["Daemon", "DaemonClient"] logger = logging.getLogger("supysonic") daemon = None + def setup_logging(config): - if config['log_file']: - if config['log_file'] == '/dev/null': + if config["log_file"]: + if config["log_file"] == "/dev/null": log_handler = logging.NullHandler() else: - log_handler = TimedRotatingFileHandler(config['log_file'], when = 'midnight') - log_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")) + log_handler = TimedRotatingFileHandler(config["log_file"], when="midnight") + log_handler.setFormatter( + logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") + ) else: log_handler = logging.StreamHandler() log_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s")) logger.addHandler(log_handler) - if 'log_level' in config: - level = getattr(logging, config['log_level'].upper(), logging.NOTSET) + if "log_level" in config: + level = getattr(logging, config["log_level"].upper(), logging.NOTSET) logger.setLevel(level) + def __terminate(signum, frame): global daemon @@ -46,6 +50,7 @@ def __terminate(signum, frame): daemon.terminate() release_database() + def main(): global daemon @@ -55,7 +60,7 @@ def main(): signal(SIGTERM, __terminate) signal(SIGINT, __terminate) - init_database(config.BASE['database_uri']) + init_database(config.BASE["database_uri"]) daemon = Daemon(config) daemon.run() release_database() diff --git a/supysonic/daemon/client.py b/supysonic/daemon/client.py index 7436a7e..9ebfebc 100644 --- a/supysonic/daemon/client.py +++ b/supysonic/daemon/client.py @@ -14,74 +14,86 @@ from ..config import get_current_config from ..py23 import strtype from ..utils import get_secret_key -__all__ = [ 'DaemonClient' ] +__all__ = ["DaemonClient"] + class DaemonCommand(object): def apply(self, connection, daemon): raise NotImplementedError() + class WatcherCommand(DaemonCommand): def __init__(self, folder): self._folder = folder + class AddWatchedFolderCommand(WatcherCommand): def apply(self, connection, daemon): if daemon.watcher is not None: daemon.watcher.add_folder(self._folder) + class RemoveWatchedFolder(WatcherCommand): def apply(self, connection, daemon): if daemon.watcher is not None: daemon.watcher.remove_folder(self._folder) + class ScannerCommand(DaemonCommand): pass + class ScannerProgressCommand(ScannerCommand): def apply(self, connection, daemon): scanner = daemon.scanner rv = scanner.scanned if scanner is not None and scanner.is_alive() else None connection.send(ScannerProgressResult(rv)) + class ScannerStartCommand(ScannerCommand): - def __init__(self, folders = [], force = False): + def __init__(self, folders=[], force=False): self.__folders = folders self.__force = force def apply(self, connection, daemon): daemon.start_scan(self.__folders, self.__force) + class DaemonCommandResult(object): pass + class ScannerProgressResult(DaemonCommandResult): def __init__(self, scanned): self.__scanned = scanned scanned = property(lambda self: self.__scanned) + class DaemonClient(object): - def __init__(self, address = None): - self.__address = address or get_current_config().DAEMON['socket'] - self.__key = get_secret_key('daemon_key') + def __init__(self, address=None): + self.__address = address or get_current_config().DAEMON["socket"] + self.__key = get_secret_key("daemon_key") def __get_connection(self): if not self.__address: - raise DaemonUnavailableError('No daemon address set') + raise DaemonUnavailableError("No daemon address set") try: - return Client(address = self.__address, authkey = self.__key) + return Client(address=self.__address, authkey=self.__key) except IOError: - raise DaemonUnavailableError("Couldn't connect to daemon at {}".format(self.__address)) + raise DaemonUnavailableError( + "Couldn't connect to daemon at {}".format(self.__address) + ) def add_watched_folder(self, folder): if not isinstance(folder, strtype): - raise TypeError('Expecting string, got ' + str(type(folder))) + raise TypeError("Expecting string, got " + str(type(folder))) with self.__get_connection() as c: c.send(AddWatchedFolderCommand(folder)) def remove_watched_folder(self, folder): if not isinstance(folder, strtype): - raise TypeError('Expecting string, got ' + str(type(folder))) + raise TypeError("Expecting string, got " + str(type(folder))) with self.__get_connection() as c: c.send(RemoveWatchedFolder(folder)) @@ -90,8 +102,8 @@ class DaemonClient(object): c.send(ScannerProgressCommand()) return c.recv().scanned - def scan(self, folders = [], force = False): + def scan(self, folders=[], force=False): if not isinstance(folders, list): - raise TypeError('Expecting list, got ' + str(type(folders))) + raise TypeError("Expecting list, got " + str(type(folders))) with self.__get_connection() as c: c.send(ScannerStartCommand(folders, force)) diff --git a/supysonic/daemon/exceptions.py b/supysonic/daemon/exceptions.py index 3c6f511..3be86df 100644 --- a/supysonic/daemon/exceptions.py +++ b/supysonic/daemon/exceptions.py @@ -7,5 +7,6 @@ # # Distributed under terms of the GNU AGPLv3 license. + class DaemonUnavailableError(Exception): pass diff --git a/supysonic/daemon/server.py b/supysonic/daemon/server.py index afae6eb..1f1bf31 100644 --- a/supysonic/daemon/server.py +++ b/supysonic/daemon/server.py @@ -20,10 +20,11 @@ from ..scanner import Scanner from ..utils import get_secret_key from ..watcher import SupysonicWatcher -__all__ = [ 'Daemon' ] +__all__ = ["Daemon"] logger = logging.getLogger(__name__) + class Daemon(object): def __init__(self, config): self.__config = config @@ -37,19 +38,21 @@ class Daemon(object): def __handle_connection(self, connection): cmd = connection.recv() - logger.debug('Received %s', cmd) + logger.debug("Received %s", cmd) if cmd is None: pass elif isinstance(cmd, DaemonCommand): cmd.apply(connection, self) else: - logger.warn('Received unknown command %s', cmd) + logger.warn("Received unknown command %s", cmd) def run(self): - self.__listener = Listener(address = self.__config.DAEMON['socket'], authkey = get_secret_key('daemon_key')) + self.__listener = Listener( + address=self.__config.DAEMON["socket"], authkey=get_secret_key("daemon_key") + ) logger.info("Listening to %s", self.__listener.address) - if self.__config.DAEMON['run_watcher']: + if self.__config.DAEMON["run_watcher"]: self.__watcher = SupysonicWatcher(self.__config) self.__watcher.start() @@ -62,7 +65,7 @@ class Daemon(object): conn = self.__listener.accept() self.__handle_connection(conn) - def start_scan(self, folders = [], force = False): + def start_scan(self, folders=[], force=False): if not folders: with db_session: folders = select(f.name for f in Folder if f.root)[:] @@ -72,11 +75,16 @@ class Daemon(object): self.__scanner.queue_folder(f) return - extensions = self.__config.BASE['scanner_extensions'] + extensions = self.__config.BASE["scanner_extensions"] if extensions: - extensions = extensions.split(' ') + extensions = extensions.split(" ") - self.__scanner = Scanner(force = force, extensions = extensions, on_folder_start = self.__unwatch, on_folder_end = self.__watch) + self.__scanner = Scanner( + force=force, + extensions=extensions, + on_folder_start=self.__unwatch, + on_folder_end=self.__watch, + ) for f in folders: self.__scanner.queue_folder(f) @@ -92,7 +100,7 @@ class Daemon(object): def terminate(self): self.__stopped.set() - with Client(self.__listener.address, authkey = self.__listener._authkey) as c: + with Client(self.__listener.address, authkey=self.__listener._authkey) as c: c.send(None) if self.__scanner is not None: diff --git a/supysonic/db.py b/supysonic/db.py index cb85005..d9c6b29 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -31,104 +31,120 @@ try: except ImportError: from urlparse import urlparse, parse_qsl -SCHEMA_VERSION = '20190518' +SCHEMA_VERSION = "20190518" + def now(): - return datetime.now().replace(microsecond = 0) + return datetime.now().replace(microsecond=0) + metadb = Database() + class Meta(metadb.Entity): - _table_ = 'meta' + _table_ = "meta" key = PrimaryKey(str, 32) value = Required(str, 256) + db = Database() -@db.on_connect(provider = 'sqlite') + +@db.on_connect(provider="sqlite") def sqlite_case_insensitive_like(db, connection): cursor = connection.cursor() - cursor.execute('PRAGMA case_sensitive_like = OFF') + cursor.execute("PRAGMA case_sensitive_like = OFF") + class PathMixin(object): @classmethod def get(cls, *args, **kwargs): if kwargs: - path = kwargs.pop('path', None) + path = kwargs.pop("path", None) if path: - kwargs['_path_hash'] = sha1(path.encode('utf-8')).digest() + kwargs["_path_hash"] = sha1(path.encode("utf-8")).digest() return db.Entity.get.__func__(cls, *args, **kwargs) def __init__(self, *args, **kwargs): - path = kwargs['path'] - kwargs['_path_hash'] = sha1(path.encode('utf-8')).digest() + path = kwargs["path"] + kwargs["_path_hash"] = sha1(path.encode("utf-8")).digest() db.Entity.__init__(self, *args, **kwargs) def __setattr__(self, attr, value): db.Entity.__setattr__(self, attr, value) - if attr == 'path': - db.Entity.__setattr__(self, '_path_hash', sha1(value.encode('utf-8')).digest()) + if attr == "path": + db.Entity.__setattr__( + self, "_path_hash", sha1(value.encode("utf-8")).digest() + ) + class Folder(PathMixin, db.Entity): - _table_ = 'folder' + _table_ = "folder" - id = PrimaryKey(UUID, default = uuid4) - root = Required(bool, default = False) - name = Required(str, autostrip = False) - path = Required(str, 4096, autostrip = False) # unique - _path_hash = Required(buffer, column = 'path_hash') - created = Required(datetime, precision = 0, default = now) - cover_art = Optional(str, nullable = True, autostrip = False) - last_scan = Required(int, default = 0) + id = PrimaryKey(UUID, default=uuid4) + root = Required(bool, default=False) + name = Required(str, autostrip=False) + path = Required(str, 4096, autostrip=False) # unique + _path_hash = Required(buffer, column="path_hash") + created = Required(datetime, precision=0, default=now) + cover_art = Optional(str, nullable=True, autostrip=False) + last_scan = Required(int, default=0) - parent = Optional(lambda: Folder, reverse = 'children', column = 'parent_id') - children = Set(lambda: Folder, reverse = 'parent') + parent = Optional(lambda: Folder, reverse="children", column="parent_id") + children = Set(lambda: Folder, reverse="parent") - __alltracks = Set(lambda: Track, lazy = True, reverse = 'root_folder') # Never used, hide it. Could be huge, lazy load - tracks = Set(lambda: Track, reverse = 'folder') + __alltracks = Set( + lambda: Track, lazy=True, reverse="root_folder" + ) # Never used, hide it. Could be huge, lazy load + tracks = Set(lambda: Track, reverse="folder") stars = Set(lambda: StarredFolder) ratings = Set(lambda: RatingFolder) def as_subsonic_child(self, user): info = dict( - id = str(self.id), - isDir = True, - title = self.name, - album = self.name, - created = self.created.isoformat() + id=str(self.id), + isDir=True, + title=self.name, + album=self.name, + created=self.created.isoformat(), ) if not self.root: - info['parent'] = str(self.parent.id) - info['artist'] = self.parent.name + info["parent"] = str(self.parent.id) + info["artist"] = self.parent.name if self.cover_art: - info['coverArt'] = str(self.id) + info["coverArt"] = str(self.id) else: for track in self.tracks: if track.has_art: - info['coverArt'] = str(track.id) + info["coverArt"] = str(track.id) break try: starred = StarredFolder[user.id, self.id] - info['starred'] = starred.date.isoformat() - except ObjectNotFound: pass + info["starred"] = starred.date.isoformat() + except ObjectNotFound: + pass try: rating = RatingFolder[user.id, self.id] - info['userRating'] = rating.rating - except ObjectNotFound: pass + info["userRating"] = rating.rating + except ObjectNotFound: + pass avgRating = avg(self.ratings.rating) if avgRating: - info['averageRating'] = avgRating + info["averageRating"] = avgRating return info @classmethod def prune(cls): - query = cls.select(lambda self: not exists(t for t in Track if t.folder == self) and \ - not exists(f for f in Folder if f.parent == self) and not self.root) + query = cls.select( + lambda self: not exists(t for t in Track if t.folder == self) + and not exists(f for f in Folder if f.parent == self) + and not self.root + ) total = 0 while True: count = query.delete() @@ -136,11 +152,12 @@ class Folder(PathMixin, db.Entity): if not count: return total -class Artist(db.Entity): - _table_ = 'artist' - id = PrimaryKey(UUID, default = uuid4) - name = Required(str) # unique +class Artist(db.Entity): + _table_ = "artist" + + id = PrimaryKey(UUID, default=uuid4) + name = Required(str) # unique albums = Set(lambda: Album) tracks = Set(lambda: Track) @@ -148,169 +165,194 @@ class Artist(db.Entity): def as_subsonic_artist(self, user): info = dict( - id = str(self.id), - name = self.name, + id=str(self.id), + name=self.name, # coverArt - albumCount = self.albums.count() + albumCount=self.albums.count(), ) try: starred = StarredArtist[user.id, self.id] - info['starred'] = starred.date.isoformat() - except ObjectNotFound: pass + info["starred"] = starred.date.isoformat() + except ObjectNotFound: + pass return info @classmethod def prune(cls): - return cls.select(lambda self: not exists(a for a in Album if a.artist == self) and \ - not exists(t for t in Track if t.artist == self)).delete() + return cls.select( + lambda self: not exists(a for a in Album if a.artist == self) + and not exists(t for t in Track if t.artist == self) + ).delete() + class Album(db.Entity): - _table_ = 'album' + _table_ = "album" - id = PrimaryKey(UUID, default = uuid4) + id = PrimaryKey(UUID, default=uuid4) name = Required(str) - artist = Required(Artist, column = 'artist_id') + artist = Required(Artist, column="artist_id") tracks = Set(lambda: Track) stars = Set(lambda: StarredAlbum) def as_subsonic_album(self, user): info = dict( - id = str(self.id), - name = self.name, - artist = self.artist.name, - artistId = str(self.artist.id), - songCount = self.tracks.count(), - duration = sum(self.tracks.duration), - created = min(self.tracks.created).isoformat() + id=str(self.id), + name=self.name, + artist=self.artist.name, + artistId=str(self.artist.id), + songCount=self.tracks.count(), + duration=sum(self.tracks.duration), + created=min(self.tracks.created).isoformat(), ) - track_with_cover = self.tracks.select(lambda t: t.folder.cover_art is not None).first() + track_with_cover = self.tracks.select( + lambda t: t.folder.cover_art is not None + ).first() if track_with_cover is not None: - info['coverArt'] = str(track_with_cover.folder.id) + info["coverArt"] = str(track_with_cover.folder.id) else: track_with_cover = self.tracks.select(lambda t: t.has_art).first() if track_with_cover is not None: - info['coverArt'] = str(track_with_cover.id) + info["coverArt"] = str(track_with_cover.id) try: starred = StarredAlbum[user.id, self.id] - info['starred'] = starred.date.isoformat() - except ObjectNotFound: pass + info["starred"] = starred.date.isoformat() + except ObjectNotFound: + pass return info def sort_key(self): year = min(map(lambda t: t.year if t.year else 9999, self.tracks)) - return '%i%s' % (year, self.name.lower()) + return "%i%s" % (year, self.name.lower()) @classmethod def prune(cls): - return cls.select(lambda self: not exists(t for t in Track if t.album == self)).delete() + return cls.select( + lambda self: not exists(t for t in Track if t.album == self) + ).delete() + class Track(PathMixin, db.Entity): - _table_ = 'track' + _table_ = "track" - id = PrimaryKey(UUID, default = uuid4) + id = PrimaryKey(UUID, default=uuid4) disc = Required(int) number = Required(int) title = Required(str) year = Optional(int) - genre = Optional(str, nullable = True) + genre = Optional(str, nullable=True) duration = Required(int) has_art = Required(bool, default=False) - album = Required(Album, column = 'album_id') - artist = Required(Artist, column = 'artist_id') + album = Required(Album, column="album_id") + artist = Required(Artist, column="artist_id") bitrate = Required(int) - path = Required(str, 4096, autostrip = False) # unique - _path_hash = Required(buffer, column = 'path_hash') - created = Required(datetime, precision = 0, default = now) + path = Required(str, 4096, autostrip=False) # unique + _path_hash = Required(buffer, column="path_hash") + created = Required(datetime, precision=0, default=now) last_modification = Required(int) - play_count = Required(int, default = 0) - last_play = Optional(datetime, precision = 0) + play_count = Required(int, default=0) + last_play = Optional(datetime, precision=0) - root_folder = Required(Folder, column = 'root_folder_id') - folder = Required(Folder, column = 'folder_id') + root_folder = Required(Folder, column="root_folder_id") + folder = Required(Folder, column="folder_id") - __lastly_played_by = Set(lambda: User) # Never used, hide it + __lastly_played_by = Set(lambda: User) # Never used, hide it stars = Set(lambda: StarredTrack) ratings = Set(lambda: RatingTrack) def as_subsonic_child(self, user, prefs): info = dict( - id = str(self.id), - parent = str(self.folder.id), - isDir = False, - title = self.title, - album = self.album.name, - artist = self.artist.name, - track = self.number, - size = os.path.getsize(self.path) if os.path.isfile(self.path) else -1, - contentType = self.mimetype, - suffix = self.suffix(), - duration = self.duration, - bitRate = self.bitrate, - path = self.path[len(self.root_folder.path) + 1:], - isVideo = False, - discNumber = self.disc, - created = self.created.isoformat(), - albumId = str(self.album.id), - artistId = str(self.artist.id), - type = 'music' + id=str(self.id), + parent=str(self.folder.id), + isDir=False, + title=self.title, + album=self.album.name, + artist=self.artist.name, + track=self.number, + size=os.path.getsize(self.path) if os.path.isfile(self.path) else -1, + contentType=self.mimetype, + suffix=self.suffix(), + duration=self.duration, + bitRate=self.bitrate, + path=self.path[len(self.root_folder.path) + 1 :], + isVideo=False, + discNumber=self.disc, + created=self.created.isoformat(), + albumId=str(self.album.id), + artistId=str(self.artist.id), + type="music", ) if self.year: - info['year'] = self.year + info["year"] = self.year if self.genre: - info['genre'] = self.genre + info["genre"] = self.genre if self.has_art: - info['coverArt'] = str(self.id) + info["coverArt"] = str(self.id) elif self.folder.cover_art: - info['coverArt'] = str(self.folder.id) + info["coverArt"] = str(self.folder.id) try: starred = StarredTrack[user.id, self.id] - info['starred'] = starred.date.isoformat() - except ObjectNotFound: pass + info["starred"] = starred.date.isoformat() + except ObjectNotFound: + pass try: rating = RatingTrack[user.id, self.id] - info['userRating'] = rating.rating - except ObjectNotFound: pass + info["userRating"] = rating.rating + except ObjectNotFound: + pass avgRating = avg(self.ratings.rating) if avgRating: - info['averageRating'] = avgRating + info["averageRating"] = avgRating - if prefs is not None and prefs.format is not None and prefs.format != self.suffix(): - info['transcodedSuffix'] = prefs.format - info['transcodedContentType'] = mimetypes.guess_type('dummyname.' + prefs.format, False)[0] or 'application/octet-stream' + if ( + prefs is not None + and prefs.format is not None + and prefs.format != self.suffix() + ): + info["transcodedSuffix"] = prefs.format + info["transcodedContentType"] = ( + mimetypes.guess_type("dummyname." + prefs.format, False)[0] + or "application/octet-stream" + ) return info @property def mimetype(self): - return mimetypes.guess_type(self.path, False)[0] or 'application/octet-stream' + return mimetypes.guess_type(self.path, False)[0] or "application/octet-stream" def duration_str(self): - ret = '%02i:%02i' % ((self.duration % 3600) / 60, self.duration % 60) + ret = "%02i:%02i" % ((self.duration % 3600) / 60, self.duration % 60) if self.duration >= 3600: - ret = '%02i:%s' % (self.duration / 3600, ret) + ret = "%02i:%s" % (self.duration / 3600, ret) return ret def suffix(self): return os.path.splitext(self.path)[1][1:].lower() def sort_key(self): - return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower() - + return ( + self.album.artist.name + + self.album.name + + ("%02i" % self.disc) + + ("%02i" % self.number) + + self.title + ).lower() + def extract_cover_art(self): return Track._extract_cover_art(self.path) @@ -319,159 +361,180 @@ class Track(PathMixin, db.Entity): if os.path.exists(path): metadata = mutagen.File(path) if metadata: - if isinstance(metadata.tags, mutagen.id3.ID3Tags) and len(metadata.tags.getall('APIC')) > 0: - return metadata.tags.getall('APIC')[0].data + if ( + isinstance(metadata.tags, mutagen.id3.ID3Tags) + and len(metadata.tags.getall("APIC")) > 0 + ): + return metadata.tags.getall("APIC")[0].data elif isinstance(metadata, mutagen.flac.FLAC) and len(metadata.pictures): return metadata.pictures[0].data - elif isinstance(metadata.tags, mutagen._vorbis.VCommentDict) and 'METADATA_BLOCK_PICTURE' in metadata.tags and len(metadata.tags['METADATA_BLOCK_PICTURE']) > 0: - picture = mutagen.flac.Picture(base64.b64decode(metadata.tags['METADATA_BLOCK_PICTURE'][0])) + elif ( + isinstance(metadata.tags, mutagen._vorbis.VCommentDict) + and "METADATA_BLOCK_PICTURE" in metadata.tags + and len(metadata.tags["METADATA_BLOCK_PICTURE"]) > 0 + ): + picture = mutagen.flac.Picture( + base64.b64decode(metadata.tags["METADATA_BLOCK_PICTURE"][0]) + ) return picture.data return None -class User(db.Entity): - _table_ = 'user' - id = PrimaryKey(UUID, default = uuid4) - name = Required(str, 64) # unique +class User(db.Entity): + _table_ = "user" + + id = PrimaryKey(UUID, default=uuid4) + name = Required(str, 64) # unique mail = Optional(str) password = Required(str, 40) salt = Required(str, 6) - admin = Required(bool, default = False) - lastfm_session = Optional(str, 32, nullable = True) - lastfm_status = Required(bool, default = True) # True: ok/unlinked, False: invalid session + admin = Required(bool, default=False) + lastfm_session = Optional(str, 32, nullable=True) + lastfm_status = Required( + bool, default=True + ) # True: ok/unlinked, False: invalid session - last_play = Optional(Track, column = 'last_play_id') - last_play_date = Optional(datetime, precision = 0) + last_play = Optional(Track, column="last_play_id") + last_play_date = Optional(datetime, precision=0) clients = Set(lambda: ClientPrefs) playlists = Set(lambda: Playlist) - __messages = Set(lambda: ChatMessage, lazy = True) # Never used, hide it + __messages = Set(lambda: ChatMessage, lazy=True) # Never used, hide it - starred_folders = Set(lambda: StarredFolder, lazy = True) - starred_artists = Set(lambda: StarredArtist, lazy = True) - starred_albums = Set(lambda: StarredAlbum, lazy = True) - starred_tracks = Set(lambda: StarredTrack, lazy = True) - folder_ratings = Set(lambda: RatingFolder, lazy = True) - track_ratings = Set(lambda: RatingTrack, lazy = True) + starred_folders = Set(lambda: StarredFolder, lazy=True) + starred_artists = Set(lambda: StarredArtist, lazy=True) + starred_albums = Set(lambda: StarredAlbum, lazy=True) + starred_tracks = Set(lambda: StarredTrack, lazy=True) + folder_ratings = Set(lambda: RatingFolder, lazy=True) + track_ratings = Set(lambda: RatingTrack, lazy=True) def as_subsonic_user(self): return dict( - username = self.name, - email = self.mail, - scrobblingEnabled = self.lastfm_session is not None and self.lastfm_status, - adminRole = self.admin, - settingsRole = True, - downloadRole = True, - uploadRole = False, - playlistRole = True, - coverArtRole = False, - commentRole = False, - podcastRole = False, - streamRole = True, - jukeboxRole = False, - shareRole = False + username=self.name, + email=self.mail, + scrobblingEnabled=self.lastfm_session is not None and self.lastfm_status, + adminRole=self.admin, + settingsRole=True, + downloadRole=True, + uploadRole=False, + playlistRole=True, + coverArtRole=False, + commentRole=False, + podcastRole=False, + streamRole=True, + jukeboxRole=False, + shareRole=False, ) -class ClientPrefs(db.Entity): - _table_ = 'client_prefs' - user = Required(User, column = 'user_id') +class ClientPrefs(db.Entity): + _table_ = "client_prefs" + + user = Required(User, column="user_id") client_name = Required(str, 32) PrimaryKey(user, client_name) - format = Optional(str, 8, nullable = True) + format = Optional(str, 8, nullable=True) bitrate = Optional(int) -class StarredFolder(db.Entity): - _table_ = 'starred_folder' - user = Required(User, column = 'user_id') - starred = Required(Folder, column = 'starred_id') - date = Required(datetime, precision = 0, default = now) +class StarredFolder(db.Entity): + _table_ = "starred_folder" + + user = Required(User, column="user_id") + starred = Required(Folder, column="starred_id") + date = Required(datetime, precision=0, default=now) PrimaryKey(user, starred) + class StarredArtist(db.Entity): - _table_ = 'starred_artist' + _table_ = "starred_artist" - user = Required(User, column = 'user_id') - starred = Required(Artist, column = 'starred_id') - date = Required(datetime, precision = 0, default = now) + user = Required(User, column="user_id") + starred = Required(Artist, column="starred_id") + date = Required(datetime, precision=0, default=now) PrimaryKey(user, starred) + class StarredAlbum(db.Entity): - _table_ = 'starred_album' + _table_ = "starred_album" - user = Required(User, column = 'user_id') - starred = Required(Album, column = 'starred_id') - date = Required(datetime, precision = 0, default = now) + user = Required(User, column="user_id") + starred = Required(Album, column="starred_id") + date = Required(datetime, precision=0, default=now) PrimaryKey(user, starred) + class StarredTrack(db.Entity): - _table_ = 'starred_track' + _table_ = "starred_track" - user = Required(User, column = 'user_id') - starred = Required(Track, column = 'starred_id') - date = Required(datetime, precision = 0, default = now) + user = Required(User, column="user_id") + starred = Required(Track, column="starred_id") + date = Required(datetime, precision=0, default=now) PrimaryKey(user, starred) + class RatingFolder(db.Entity): - _table_ = 'rating_folder' - user = Required(User, column = 'user_id') - rated = Required(Folder, column = 'rated_id') - rating = Required(int, min = 1, max = 5) + _table_ = "rating_folder" + user = Required(User, column="user_id") + rated = Required(Folder, column="rated_id") + rating = Required(int, min=1, max=5) PrimaryKey(user, rated) + class RatingTrack(db.Entity): - _table_ = 'rating_track' - user = Required(User, column = 'user_id') - rated = Required(Track, column = 'rated_id') - rating = Required(int, min = 1, max = 5) + _table_ = "rating_track" + user = Required(User, column="user_id") + rated = Required(Track, column="rated_id") + rating = Required(int, min=1, max=5) PrimaryKey(user, rated) -class ChatMessage(db.Entity): - _table_ = 'chat_message' - id = PrimaryKey(UUID, default = uuid4) - user = Required(User, column = 'user_id') - time = Required(int, default = lambda: int(time.time())) +class ChatMessage(db.Entity): + _table_ = "chat_message" + + id = PrimaryKey(UUID, default=uuid4) + user = Required(User, column="user_id") + time = Required(int, default=lambda: int(time.time())) message = Required(str, 512) def responsize(self): return dict( - username = self.user.name, - time = self.time * 1000, - message = self.message + username=self.user.name, time=self.time * 1000, message=self.message ) -class Playlist(db.Entity): - _table_ = 'playlist' - id = PrimaryKey(UUID, default = uuid4) - user = Required(User, column = 'user_id') +class Playlist(db.Entity): + _table_ = "playlist" + + id = PrimaryKey(UUID, default=uuid4) + user = Required(User, column="user_id") name = Required(str) comment = Optional(str) - public = Required(bool, default = False) - created = Required(datetime, precision = 0, default = now) + public = Required(bool, default=False) + created = Required(datetime, precision=0, default=now) tracks = Optional(LongStr) def as_subsonic_playlist(self, user): tracks = self.get_tracks() info = dict( - id = str(self.id), - name = self.name if self.user.id == user.id else '[%s] %s' % (self.user.name, self.name), - owner = self.user.name, - public = self.public, - songCount = len(tracks), - duration = sum(map(lambda t: t.duration, tracks)), - created = self.created.isoformat() + id=str(self.id), + name=self.name + if self.user.id == user.id + else "[%s] %s" % (self.user.name, self.name), + owner=self.user.name, + public=self.public, + songCount=len(tracks), + duration=sum(map(lambda t: t.duration, tracks)), + created=self.created.isoformat(), ) if self.comment: - info['comment'] = self.comment + info["comment"] = self.comment return info def get_tracks(self): @@ -481,7 +544,7 @@ class Playlist(db.Entity): tracks = [] should_fix = False - for t in self.tracks.split(','): + for t in self.tracks.split(","): try: tid = UUID(t) track = Track[tid] @@ -490,13 +553,13 @@ class Playlist(db.Entity): should_fix = True if should_fix: - self.tracks = ','.join(map(lambda t: str(t.id), tracks)) + self.tracks = ",".join(map(lambda t: str(t.id), tracks)) db.commit() return tracks def clear(self): - self.tracks = '' + self.tracks = "" def add(self, track): if isinstance(track, UUID): @@ -507,92 +570,118 @@ class Playlist(db.Entity): tid = UUID(track) if self.tracks and len(self.tracks) > 0: - self.tracks = '{},{}'.format(self.tracks, tid) + self.tracks = "{},{}".format(self.tracks, tid) else: self.tracks = str(tid) def remove_at_indexes(self, indexes): - tracks = self.tracks.split(',') + tracks = self.tracks.split(",") for i in indexes: if i < 0 or i >= len(tracks): continue tracks[i] = None - self.tracks = ','.join(t for t in tracks if t) + self.tracks = ",".join(t for t in tracks if t) + def parse_uri(database_uri): if not isinstance(database_uri, strtype): - raise TypeError('Expecting a string') + raise TypeError("Expecting a string") uri = urlparse(database_uri) args = dict(parse_qsl(uri.query)) - if uri.scheme == 'sqlite': + if uri.scheme == "sqlite": path = uri.path if not path: - path = ':memory:' - elif path[0] == '/': + path = ":memory:" + elif path[0] == "/": path = path[1:] - return dict(provider = 'sqlite', filename = path, create_db = True, **args) - elif uri.scheme in ('postgres', 'postgresql'): - return dict(provider = 'postgres', user = uri.username, password = uri.password, host = uri.hostname, dbname = uri.path[1:], **args) - elif uri.scheme == 'mysql': - args.setdefault('charset', 'utf8mb4') - args.setdefault('binary_prefix', True) - return dict(provider = 'mysql', user = uri.username, passwd = uri.password, host = uri.hostname, db = uri.path[1:], **args) + return dict(provider="sqlite", filename=path, create_db=True, **args) + elif uri.scheme in ("postgres", "postgresql"): + return dict( + provider="postgres", + user=uri.username, + password=uri.password, + host=uri.hostname, + dbname=uri.path[1:], + **args + ) + elif uri.scheme == "mysql": + args.setdefault("charset", "utf8mb4") + args.setdefault("binary_prefix", True) + return dict( + provider="mysql", + user=uri.username, + passwd=uri.password, + host=uri.hostname, + db=uri.path[1:], + **args + ) return dict() + def execute_sql_resource_script(respath): - sql = pkg_resources.resource_string(__package__, respath).decode('utf-8') - for statement in sql.split(';'): + sql = pkg_resources.resource_string(__package__, respath).decode("utf-8") + for statement in sql.split(";"): statement = statement.strip() - if statement and not statement.startswith('--'): + if statement and not statement.startswith("--"): metadb.execute(statement) + def init_database(database_uri): settings = parse_uri(database_uri) metadb.bind(**settings) - metadb.generate_mapping(check_tables = False) + metadb.generate_mapping(check_tables=False) # Check if we should create the tables try: metadb.check_tables() except DatabaseError: with db_session: - execute_sql_resource_script('schema/' + settings['provider'] + '.sql') - Meta(key = 'schema_version', value = SCHEMA_VERSION) + execute_sql_resource_script("schema/" + settings["provider"] + ".sql") + Meta(key="schema_version", value=SCHEMA_VERSION) # Check for schema changes with db_session: - version = Meta['schema_version'] + version = Meta["schema_version"] if version.value < SCHEMA_VERSION: - migrations = sorted(pkg_resources.resource_listdir(__package__, 'schema/migration/' + settings['provider'])) + migrations = sorted( + pkg_resources.resource_listdir( + __package__, "schema/migration/" + settings["provider"] + ) + ) for migration in migrations: date, ext = os.path.splitext(migration) if date <= version.value: continue - if ext == '.sql': - execute_sql_resource_script('schema/migration/{}/{}'.format(settings['provider'], migration)) - elif ext == '.py': - m = importlib.import_module('.schema.migration.{}.{}'.format(settings['provider'], date), __package__) + if ext == ".sql": + execute_sql_resource_script( + "schema/migration/{}/{}".format(settings["provider"], migration) + ) + elif ext == ".py": + m = importlib.import_module( + ".schema.migration.{}.{}".format(settings["provider"], date), + __package__, + ) m.apply(settings.copy()) version.value = SCHEMA_VERSION # Hack for in-memory SQLite databases (used in tests), otherwise 'db' and 'metadb' would be two distinct databases # and 'db' wouldn't have any table - if settings['provider'] == 'sqlite' and settings['filename'] == ':memory:': + if settings["provider"] == "sqlite" and settings["filename"] == ":memory:": db.provider = metadb.provider else: metadb.disconnect() db.bind(**settings) - db.generate_mapping(check_tables = False) + db.generate_mapping(check_tables=False) + def release_database(): metadb.disconnect() db.disconnect() db.provider = metadb.provider = None db.schema = metadb.schema = None - diff --git a/supysonic/frontend/__init__.py b/supysonic/frontend/__init__.py index 30c9d0e..92c05bb 100644 --- a/supysonic/frontend/__init__.py +++ b/supysonic/frontend/__init__.py @@ -18,23 +18,31 @@ from ..daemon.exceptions import DaemonUnavailableError from ..db import Artist, Album, Track from ..managers.user import UserManager -frontend = Blueprint('frontend', __name__) +frontend = Blueprint("frontend", __name__) + @frontend.before_request def login_check(): request.user = None should_login = True - if session.get('userid'): + if session.get("userid"): try: - user = UserManager.get(session.get('userid')) + user = UserManager.get(session.get("userid")) request.user = user should_login = False except (ValueError, ObjectNotFound): session.clear() - if should_login and request.endpoint != 'frontend.login': - flash('Please login') - return redirect(url_for('frontend.login', returnUrl = request.script_root + request.url[len(request.url_root)-1:])) + if should_login and request.endpoint != "frontend.login": + flash("Please login") + return redirect( + url_for( + "frontend.login", + returnUrl=request.script_root + + request.url[len(request.url_root) - 1 :], + ) + ) + @frontend.before_request def scan_status(): @@ -42,30 +50,35 @@ def scan_status(): return try: - scanned = DaemonClient(current_app.config['DAEMON']['socket']).get_scanning_progress() + scanned = DaemonClient( + current_app.config["DAEMON"]["socket"] + ).get_scanning_progress() if scanned is not None: - flash('Scanning in progress, {} files scanned.'.format(scanned)) + flash("Scanning in progress, {} files scanned.".format(scanned)) except DaemonUnavailableError: pass -@frontend.route('/') + +@frontend.route("/") def index(): stats = { - 'artists': Artist.select().count(), - 'albums': Album.select().count(), - 'tracks': Track.select().count() + "artists": Artist.select().count(), + "albums": Album.select().count(), + "tracks": Track.select().count(), } - return render_template('home.html', stats = stats) + return render_template("home.html", stats=stats) + def admin_only(f): @wraps(f) def decorated_func(*args, **kwargs): if not request.user or not request.user.admin: - return redirect(url_for('frontend.index')) + return redirect(url_for("frontend.index")) return f(*args, **kwargs) + return decorated_func + from .user import * from .folder import * from .playlist import * - diff --git a/supysonic/frontend/folder.py b/supysonic/frontend/folder.py index a8be3bb..6e5578a 100644 --- a/supysonic/frontend/folder.py +++ b/supysonic/frontend/folder.py @@ -21,74 +21,84 @@ from ..scanner import Scanner from . import admin_only, frontend -@frontend.route('/folder') + +@frontend.route("/folder") @admin_only def folder_index(): try: - DaemonClient(current_app.config['DAEMON']['socket']).get_scanning_progress() + DaemonClient(current_app.config["DAEMON"]["socket"]).get_scanning_progress() allow_scan = True except DaemonUnavailableError: allow_scan = False - flash("The daemon is unavailable, can't scan from the web interface, use the CLI to do so.", 'warning') - return render_template('folders.html', folders = Folder.select(lambda f: f.root), allow_scan = allow_scan) + flash( + "The daemon is unavailable, can't scan from the web interface, use the CLI to do so.", + "warning", + ) + return render_template( + "folders.html", folders=Folder.select(lambda f: f.root), allow_scan=allow_scan + ) -@frontend.route('/folder/add') + +@frontend.route("/folder/add") @admin_only def add_folder_form(): - return render_template('addfolder.html') + return render_template("addfolder.html") -@frontend.route('/folder/add', methods = [ 'POST' ]) + +@frontend.route("/folder/add", methods=["POST"]) @admin_only def add_folder_post(): error = False - (name, path) = map(request.form.get, [ 'name', 'path' ]) - if name in (None, ''): - flash('The name is required.') + (name, path) = map(request.form.get, ["name", "path"]) + if name in (None, ""): + flash("The name is required.") error = True - if path in (None, ''): - flash('The path is required.') + if path in (None, ""): + flash("The path is required.") error = True if error: - return render_template('addfolder.html') + return render_template("addfolder.html") try: FolderManager.add(name, path) except ValueError as e: - flash(str(e), 'error') - return render_template('addfolder.html') + flash(str(e), "error") + return render_template("addfolder.html") flash("Folder '%s' created. You should now run a scan" % name) - return redirect(url_for('frontend.folder_index')) + return redirect(url_for("frontend.folder_index")) -@frontend.route('/folder/del/') + +@frontend.route("/folder/del/") @admin_only def del_folder(id): try: FolderManager.delete(id) - flash('Deleted folder') + flash("Deleted folder") except ValueError as e: - flash(str(e), 'error') + flash(str(e), "error") except ObjectNotFound: - flash('No such folder', 'error') + flash("No such folder", "error") - return redirect(url_for('frontend.folder_index')) + return redirect(url_for("frontend.folder_index")) -@frontend.route('/folder/scan') -@frontend.route('/folder/scan/') + +@frontend.route("/folder/scan") +@frontend.route("/folder/scan/") @admin_only -def scan_folder(id = None): +def scan_folder(id=None): try: if id is not None: - folders = [ FolderManager.get(id).name ] + folders = [FolderManager.get(id).name] else: folders = [] - DaemonClient(current_app.config['DAEMON']['socket']).scan(folders) - flash('Scanning started') + DaemonClient(current_app.config["DAEMON"]["socket"]).scan(folders) + flash("Scanning started") except ValueError as e: - flash(str(e), 'error') + flash(str(e), "error") except ObjectNotFound: - flash('No such folder', 'error') + flash("No such folder", "error") except DaemonUnavailableError: - flash("Can't start scan", 'error') + flash("Can't start scan", "error") - return redirect(url_for('frontend.folder_index')) + return redirect(url_for("frontend.folder_index")) diff --git a/supysonic/frontend/playlist.py b/supysonic/frontend/playlist.py index ca1116b..eaef8fe 100644 --- a/supysonic/frontend/playlist.py +++ b/supysonic/frontend/playlist.py @@ -16,72 +16,84 @@ from ..db import Playlist from . import frontend -@frontend.route('/playlist') -def playlist_index(): - return render_template('playlists.html', - mine = Playlist.select(lambda p: p.user == request.user), - others = Playlist.select(lambda p: p.user != request.user and p.public)) -@frontend.route('/playlist/') +@frontend.route("/playlist") +def playlist_index(): + return render_template( + "playlists.html", + mine=Playlist.select(lambda p: p.user == request.user), + others=Playlist.select(lambda p: p.user != request.user and p.public), + ) + + +@frontend.route("/playlist/") def playlist_details(uid): try: uid = uuid.UUID(uid) except ValueError: - flash('Invalid playlist id') - return redirect(url_for('frontend.playlist_index')) + flash("Invalid playlist id") + return redirect(url_for("frontend.playlist_index")) try: playlist = Playlist[uid] except ObjectNotFound: - flash('Unknown playlist') - return redirect(url_for('frontend.playlist_index')) + flash("Unknown playlist") + return redirect(url_for("frontend.playlist_index")) - return render_template('playlist.html', playlist = playlist) + return render_template("playlist.html", playlist=playlist) -@frontend.route('/playlist/', methods = [ 'POST' ]) + +@frontend.route("/playlist/", methods=["POST"]) def playlist_update(uid): try: uid = uuid.UUID(uid) except ValueError: - flash('Invalid playlist id') - return redirect(url_for('frontend.playlist_index')) + flash("Invalid playlist id") + return redirect(url_for("frontend.playlist_index")) try: playlist = Playlist[uid] except ObjectNotFound: - flash('Unknown playlist') - return redirect(url_for('frontend.playlist_index')) + flash("Unknown playlist") + return redirect(url_for("frontend.playlist_index")) if playlist.user.id != request.user.id: flash("You're not allowed to edit this playlist") - elif not request.form.get('name'): - flash('Missing playlist name') + elif not request.form.get("name"): + flash("Missing playlist name") else: - playlist.name = request.form.get('name') - playlist.public = request.form.get('public') in (True, 'True', 1, '1', 'on', 'checked') - flash('Playlist updated.') + playlist.name = request.form.get("name") + playlist.public = request.form.get("public") in ( + True, + "True", + 1, + "1", + "on", + "checked", + ) + flash("Playlist updated.") return playlist_details(str(uid)) -@frontend.route('/playlist/del/') + +@frontend.route("/playlist/del/") def playlist_delete(uid): try: uid = uuid.UUID(uid) except ValueError: - flash('Invalid playlist id') - return redirect(url_for('frontend.playlist_index')) + flash("Invalid playlist id") + return redirect(url_for("frontend.playlist_index")) try: playlist = Playlist[uid] except ObjectNotFound: - flash('Unknown playlist') - return redirect(url_for('frontend.playlist_index')) + flash("Unknown playlist") + return redirect(url_for("frontend.playlist_index")) if playlist.user.id != request.user.id: flash("You're not allowed to delete this playlist") else: playlist.delete() - flash('Playlist deleted') - - return redirect(url_for('frontend.playlist_index')) + flash("Playlist deleted") + return redirect(url_for("frontend.playlist_index")) diff --git a/supysonic/frontend/user.py b/supysonic/frontend/user.py index 5da4a04..8cac6de 100644 --- a/supysonic/frontend/user.py +++ b/supysonic/frontend/user.py @@ -23,7 +23,8 @@ from . import admin_only, frontend logger = logging.getLogger(__name__) -def me_or_uuid(f, arg = 'uid'): + +def me_or_uuid(f, arg="uid"): @wraps(f) def decorated_func(*args, **kwargs): if kwargs: @@ -31,22 +32,22 @@ def me_or_uuid(f, arg = 'uid'): else: uid = args[0] - if uid == 'me': + if uid == "me": user = request.user elif not request.user.admin: - return redirect(url_for('frontend.index')) + return redirect(url_for("frontend.index")) else: try: user = UserManager.get(uid) except ValueError as e: - flash(str(e), 'error') - return redirect(url_for('frontend.index')) + flash(str(e), "error") + return redirect(url_for("frontend.index")) except ObjectNotFound: - flash('No such user', 'error') - return redirect(url_for('frontend.index')) + flash("No such user", "error") + return redirect(url_for("frontend.index")) if kwargs: - kwargs['user'] = user + kwargs["user"] = user else: args = (uid, user) @@ -54,24 +55,32 @@ def me_or_uuid(f, arg = 'uid'): return decorated_func -@frontend.route('/user') + +@frontend.route("/user") @admin_only def user_index(): - return render_template('users.html', users = User.select()) + return render_template("users.html", users=User.select()) -@frontend.route('/user/') + +@frontend.route("/user/") @me_or_uuid def user_profile(uid, user): - return render_template('profile.html', user = user, api_key = current_app.config['LASTFM']['api_key'], clients = user.clients) + return render_template( + "profile.html", + user=user, + api_key=current_app.config["LASTFM"]["api_key"], + clients=user.clients, + ) -@frontend.route('/user/', methods = [ 'POST' ]) + +@frontend.route("/user/", methods=["POST"]) @me_or_uuid def update_clients(uid, user): clients_opts = dict() for key, value in request.form.items(): - if '_' not in key: + if "_" not in key: continue - parts = key.split('_') + parts = key.split("_") if len(parts) != 2: continue client, opt = parts @@ -79,7 +88,7 @@ def update_clients(uid, user): continue if client not in clients_opts: - clients_opts[client] = dict([ (opt, value) ]) + clients_opts[client] = dict([(opt, value)]) else: clients_opts[client][opt] = value logger.debug(clients_opts) @@ -89,51 +98,61 @@ def update_clients(uid, user): if prefs is None: continue - if 'delete' in opts and opts['delete'] in [ 'on', 'true', 'checked', 'selected', '1' ]: + if "delete" in opts and opts["delete"] in [ + "on", + "true", + "checked", + "selected", + "1", + ]: prefs.delete() continue - prefs.format = opts['format'] if 'format' in opts and opts['format'] else None - prefs.bitrate = int(opts['bitrate']) if 'bitrate' in opts and opts['bitrate'] else None + prefs.format = opts["format"] if "format" in opts and opts["format"] else None + prefs.bitrate = ( + int(opts["bitrate"]) if "bitrate" in opts and opts["bitrate"] else None + ) - flash('Clients preferences updated.') + flash("Clients preferences updated.") return user_profile(uid, user) -@frontend.route('/user//changeusername') + +@frontend.route("/user//changeusername") @admin_only def change_username_form(uid): try: user = UserManager.get(uid) except ValueError as e: - flash(str(e), 'error') - return redirect(url_for('frontend.index')) + flash(str(e), "error") + return redirect(url_for("frontend.index")) except ObjectNotFound: - flash('No such user', 'error') - return redirect(url_for('frontend.index')) + flash("No such user", "error") + return redirect(url_for("frontend.index")) - return render_template('change_username.html', user = user) + return render_template("change_username.html", user=user) -@frontend.route('/user//changeusername', methods = [ 'POST' ]) + +@frontend.route("/user//changeusername", methods=["POST"]) @admin_only def change_username_post(uid): try: user = UserManager.get(uid) except ValueError as e: - flash(str(e), 'error') - return redirect(url_for('frontend.index')) + flash(str(e), "error") + return redirect(url_for("frontend.index")) except ObjectNotFound: - flash('No such user', 'error') - return redirect(url_for('frontend.index')) + flash("No such user", "error") + return redirect(url_for("frontend.index")) - username = request.form.get('user') - if username in ('', None): - flash('The username is required') - return render_template('change_username.html', user = user) - if user.name != username and User.get(name = username) is not None: - flash('This name is already taken') - return render_template('change_username.html', user = user) + username = request.form.get("user") + if username in ("", None): + flash("The username is required") + return render_template("change_username.html", user=user) + if user.name != username and User.get(name=username) is not None: + flash("This name is already taken") + return render_template("change_username.html", user=user) - if request.form.get('admin') is None: + if request.form.get("admin") is None: admin = False else: admin = True @@ -145,40 +164,44 @@ def change_username_post(uid): else: flash("No changes for '%s'." % username) - return redirect(url_for('frontend.user_profile', uid = uid)) + return redirect(url_for("frontend.user_profile", uid=uid)) -@frontend.route('/user//changemail') + +@frontend.route("/user//changemail") @me_or_uuid def change_mail_form(uid, user): - return render_template('change_mail.html', user = user) + return render_template("change_mail.html", user=user) -@frontend.route('/user//changemail', methods = [ 'POST' ]) + +@frontend.route("/user//changemail", methods=["POST"]) @me_or_uuid def change_mail_post(uid, user): - mail = request.form.get('mail', '') + mail = request.form.get("mail", "") # No validation, lol. user.mail = mail - return redirect(url_for('frontend.user_profile', uid = uid)) + return redirect(url_for("frontend.user_profile", uid=uid)) -@frontend.route('/user//changepass') + +@frontend.route("/user//changepass") @me_or_uuid def change_password_form(uid, user): - return render_template('change_pass.html', user = user) + return render_template("change_pass.html", user=user) -@frontend.route('/user//changepass', methods = [ 'POST' ]) + +@frontend.route("/user//changepass", methods=["POST"]) @me_or_uuid def change_password_post(uid, user): error = False if user.id == request.user.id: - current = request.form.get('current') + current = request.form.get("current") if not current: - flash('The current password is required') + flash("The current password is required") error = True - new, confirm = map(request.form.get, [ 'new', 'confirm' ]) + new, confirm = map(request.form.get, ["new", "confirm"]) if not new: - flash('The new password is required') + flash("The new password is required") error = True if new != confirm: flash("The new password and its confirmation don't match") @@ -191,28 +214,32 @@ def change_password_post(uid, user): else: UserManager.change_password2(user.name, new) - flash('Password changed') - return redirect(url_for('frontend.user_profile', uid = uid)) + flash("Password changed") + return redirect(url_for("frontend.user_profile", uid=uid)) except ValueError as e: - flash(str(e), 'error') + flash(str(e), "error") return change_password_form(uid, user) -@frontend.route('/user/add') + +@frontend.route("/user/add") @admin_only def add_user_form(): - return render_template('adduser.html') + return render_template("adduser.html") -@frontend.route('/user/add', methods = [ 'POST' ]) + +@frontend.route("/user/add", methods=["POST"]) @admin_only def add_user_post(): error = False - (name, passwd, passwd_confirm, mail, admin) = map(request.form.get, [ 'user', 'passwd', 'passwd_confirm', 'mail', 'admin' ]) + (name, passwd, passwd_confirm, mail, admin) = map( + request.form.get, ["user", "passwd", "passwd_confirm", "mail", "admin"] + ) if not name: - flash('The name is required.') + flash("The name is required.") error = True if not passwd: - flash('Please provide a password.') + flash("Please provide a password.") error = True elif passwd != passwd_confirm: flash("The passwords don't match.") @@ -220,86 +247,90 @@ def add_user_post(): admin = admin is not None if mail is None: - mail = '' + mail = "" if not error: try: UserManager.add(name, passwd, mail, admin) flash("User '%s' successfully added" % name) - return redirect(url_for('frontend.user_index')) + return redirect(url_for("frontend.user_index")) except ValueError as e: - flash(str(e), 'error') + flash(str(e), "error") return add_user_form() -@frontend.route('/user/del/') + +@frontend.route("/user/del/") @admin_only def del_user(uid): try: UserManager.delete(uid) - flash('Deleted user') + flash("Deleted user") except ValueError as e: - flash(str(e), 'error') + flash(str(e), "error") except ObjectNotFound: - flash('No such user', 'error') + flash("No such user", "error") - return redirect(url_for('frontend.user_index')) + return redirect(url_for("frontend.user_index")) -@frontend.route('/user//lastfm/link') + +@frontend.route("/user//lastfm/link") @me_or_uuid def lastfm_reg(uid, user): - token = request.args.get('token') + token = request.args.get("token") if not token: - flash('Missing LastFM auth token') - return redirect(url_for('frontend.user_profile', uid = uid)) + flash("Missing LastFM auth token") + return redirect(url_for("frontend.user_profile", uid=uid)) - lfm = LastFm(current_app.config['LASTFM'], user) + lfm = LastFm(current_app.config["LASTFM"], user) status, error = lfm.link_account(token) - flash(error if not status else 'Successfully linked LastFM account') + flash(error if not status else "Successfully linked LastFM account") - return redirect(url_for('frontend.user_profile', uid = uid)) + return redirect(url_for("frontend.user_profile", uid=uid)) -@frontend.route('/user//lastfm/unlink') + +@frontend.route("/user//lastfm/unlink") @me_or_uuid def lastfm_unreg(uid, user): - lfm = LastFm(current_app.config['LASTFM'], user) + lfm = LastFm(current_app.config["LASTFM"], user) lfm.unlink_account() - flash('Unlinked LastFM account') - return redirect(url_for('frontend.user_profile', uid = uid)) + flash("Unlinked LastFM account") + return redirect(url_for("frontend.user_profile", uid=uid)) -@frontend.route('/user/login', methods = [ 'GET', 'POST']) + +@frontend.route("/user/login", methods=["GET", "POST"]) def login(): - return_url = request.args.get('returnUrl') or url_for('frontend.index') + return_url = request.args.get("returnUrl") or url_for("frontend.index") if request.user: - flash('Already logged in') + flash("Already logged in") return redirect(return_url) - if request.method == 'GET': - return render_template('login.html') + if request.method == "GET": + return render_template("login.html") - name, password = map(request.form.get, [ 'user', 'password' ]) + name, password = map(request.form.get, ["user", "password"]) error = False if not name: - flash('Missing user name') + flash("Missing user name") error = True if not password: - flash('Missing password') + flash("Missing password") error = True if not error: user = UserManager.try_auth(name, password) if user: - session['userid'] = str(user.id) - flash('Logged in!') + session["userid"] = str(user.id) + flash("Logged in!") return redirect(return_url) else: - flash('Wrong username or password') + flash("Wrong username or password") - return render_template('login.html') + return render_template("login.html") -@frontend.route('/user/logout') + +@frontend.route("/user/logout") def logout(): session.clear() - flash('Logged out!') - return redirect(url_for('frontend.login')) - + flash("Logged out!") + return redirect(url_for("frontend.login")) diff --git a/supysonic/lastfm.py b/supysonic/lastfm.py index 4bad762..b7424b6 100644 --- a/supysonic/lastfm.py +++ b/supysonic/lastfm.py @@ -15,11 +15,12 @@ from .py23 import strtype logger = logging.getLogger(__name__) + class LastFm: def __init__(self, config, user): - if config['api_key'] is not None and config['secret'] is not None: - self.__api_key = config['api_key'] - self.__api_secret = config['secret'].encode('utf-8') + if config["api_key"] is not None and config["secret"] is not None: + self.__api_key = config["api_key"] + self.__api_secret = config["secret"].encode("utf-8") self.__enabled = True else: self.__enabled = False @@ -27,17 +28,17 @@ class LastFm: def link_account(self, token): if not self.__enabled: - return False, 'No API key set' + return False, "No API key set" - res = self.__api_request(False, method = 'auth.getSession', token = token) + res = self.__api_request(False, method="auth.getSession", token=token) if not res: - return False, 'Error connecting to LastFM' - elif 'error' in res: - return False, 'Error %i: %s' % (res['error'], res['message']) + return False, "Error connecting to LastFM" + elif "error" in res: + return False, "Error %i: %s" % (res["error"], res["message"]) else: - self.__user.lastfm_session = res['session']['key'] + self.__user.lastfm_session = res["session"]["key"] self.__user.lastfm_status = True - return True, 'OK' + return True, "OK" def unlink_account(self): self.__user.lastfm_session = None @@ -47,15 +48,30 @@ class LastFm: if not self.__enabled: return - self.__api_request(True, method = 'track.updateNowPlaying', artist = track.album.artist.name, track = track.title, album = track.album.name, - trackNumber = track.number, duration = track.duration) + self.__api_request( + True, + method="track.updateNowPlaying", + artist=track.album.artist.name, + track=track.title, + album=track.album.name, + trackNumber=track.number, + duration=track.duration, + ) def scrobble(self, track, ts): if not self.__enabled: return - self.__api_request(True, method = 'track.scrobble', artist = track.album.artist.name, track = track.title, album = track.album.name, - timestamp = ts, trackNumber = track.number, duration = track.duration) + self.__api_request( + True, + method="track.scrobble", + artist=track.album.artist.name, + track=track.title, + album=track.album.name, + timestamp=ts, + trackNumber=track.number, + duration=track.duration, + ) def __api_request(self, write, **kwargs): if not self.__enabled: @@ -64,34 +80,37 @@ class LastFm: if write: if not self.__user.lastfm_session or not self.__user.lastfm_status: return - kwargs['sk'] = self.__user.lastfm_session + kwargs["sk"] = self.__user.lastfm_session - kwargs['api_key'] = self.__api_key + kwargs["api_key"] = self.__api_key - sig_str = b'' + sig_str = b"" for k, v in sorted(kwargs.items()): - k = k.encode('utf-8') - v = v.encode('utf-8') if isinstance(v, strtype) else str(v).encode('utf-8') + k = k.encode("utf-8") + v = v.encode("utf-8") if isinstance(v, strtype) else str(v).encode("utf-8") sig_str += k + v sig = hashlib.md5(sig_str + self.__api_secret).hexdigest() - kwargs['api_sig'] = sig - kwargs['format'] = 'json' + kwargs["api_sig"] = sig + kwargs["format"] = "json" try: if write: - r = requests.post('https://ws.audioscrobbler.com/2.0/', data = kwargs, timeout = 5) + r = requests.post( + "https://ws.audioscrobbler.com/2.0/", data=kwargs, timeout=5 + ) else: - r = requests.get('https://ws.audioscrobbler.com/2.0/', params = kwargs, timeout = 5) + r = requests.get( + "https://ws.audioscrobbler.com/2.0/", params=kwargs, timeout=5 + ) except requests.exceptions.RequestException as e: - logger.warning('Error while connecting to LastFM: ' + str(e)) + logger.warning("Error while connecting to LastFM: " + str(e)) return None json = r.json() - if 'error' in json: - if json['error'] in (9, '9'): + if "error" in json: + if json["error"] in (9, "9"): self.__user.lastfm_status = False - logger.warning('LastFM error %i: %s' % (json['error'], json['message'])) + logger.warning("LastFM error %i: %s" % (json["error"], json["message"])) return json - diff --git a/supysonic/managers/__init__.py b/supysonic/managers/__init__.py index b046fc5..f87fa98 100644 --- a/supysonic/managers/__init__.py +++ b/supysonic/managers/__init__.py @@ -6,4 +6,3 @@ # Copyright (C) 2013 Alban 'spl0k' Féron # # Distributed under terms of the GNU AGPLv3 license. - diff --git a/supysonic/managers/folder.py b/supysonic/managers/folder.py index dfbd647..db8cb46 100644 --- a/supysonic/managers/folder.py +++ b/supysonic/managers/folder.py @@ -18,6 +18,7 @@ from ..daemon.exceptions import DaemonUnavailableError from ..db import Folder, Track, Artist, Album, User, RatingTrack, StarredTrack from ..py23 import strtype + class FolderManager: @staticmethod def get(uid): @@ -26,26 +27,26 @@ class FolderManager: elif isinstance(uid, uuid.UUID): pass else: - raise ValueError('Invalid folder id') + raise ValueError("Invalid folder id") return Folder[uid] @staticmethod def add(name, path): - if Folder.get(name = name, root = True) is not None: + if Folder.get(name=name, root=True) is not None: raise ValueError("Folder '{}' exists".format(name)) path = os.path.abspath(os.path.expanduser(path)) if not os.path.isdir(path): raise ValueError("The path doesn't exits or isn't a directory") - if Folder.get(path = path) is not None: - raise ValueError('This path is already registered') + if Folder.get(path=path) is not None: + raise ValueError("This path is already registered") if any(path.startswith(p) for p in select(f.path for f in Folder if f.root)): - raise ValueError('This path is already registered') + raise ValueError("This path is already registered") if Folder.exists(lambda f: f.path.startswith(path)): - raise ValueError('This path contains a folder that is already registered') + raise ValueError("This path contains a folder that is already registered") - folder = Folder(root = True, name = name, path = path) + folder = Folder(root=True, name=name, path=path) try: DaemonClient().add_watched_folder(path) except DaemonUnavailableError: @@ -66,20 +67,21 @@ class FolderManager: for user in User.select(lambda u: u.last_play.root_folder == folder): user.last_play = None - RatingTrack.select(lambda r: r.rated.root_folder == folder).delete(bulk = True) - StarredTrack.select(lambda s: s.starred.root_folder == folder).delete(bulk = True) + RatingTrack.select(lambda r: r.rated.root_folder == folder).delete(bulk=True) + StarredTrack.select(lambda s: s.starred.root_folder == folder).delete(bulk=True) - Track.select(lambda t: t.root_folder == folder).delete(bulk = True) + Track.select(lambda t: t.root_folder == folder).delete(bulk=True) Album.prune() Artist.prune() - Folder.select(lambda f: not f.root and f.path.startswith(folder.path)).delete(bulk = True) + Folder.select(lambda f: not f.root and f.path.startswith(folder.path)).delete( + bulk=True + ) folder.delete() @staticmethod def delete_by_name(name): - folder = Folder.get(name = name, root = True) + folder = Folder.get(name=name, root=True) if not folder: raise ObjectNotFound(Folder) FolderManager.delete(folder.id) - diff --git a/supysonic/managers/user.py b/supysonic/managers/user.py index ccb2db6..225ebb9 100644 --- a/supysonic/managers/user.py +++ b/supysonic/managers/user.py @@ -18,6 +18,7 @@ from pony.orm import ObjectNotFound from ..db import User from ..py23 import strtype + class UserManager: @staticmethod def get(uid): @@ -26,24 +27,18 @@ class UserManager: elif isinstance(uid, strtype): uid = uuid.UUID(uid) else: - raise ValueError('Invalid user id') + raise ValueError("Invalid user id") return User[uid] @staticmethod def add(name, password, mail, admin): - if User.exists(name = name): + if User.exists(name=name): raise ValueError("User '{}' exists".format(name)) crypt, salt = UserManager.__encrypt_password(password) - user = User( - name = name, - mail = mail, - password = crypt, - salt = salt, - admin = admin - ) + user = User(name=name, mail=mail, password=crypt, salt=salt, admin=admin) return user @@ -54,14 +49,14 @@ class UserManager: @staticmethod def delete_by_name(name): - user = User.get(name = name) + user = User.get(name=name) if user is None: raise ObjectNotFound(User) user.delete() @staticmethod def try_auth(name, password): - user = User.get(name = name) + user = User.get(name=name) if user is None: return None elif UserManager.__encrypt_password(password, user.salt)[0] != user.password: @@ -73,21 +68,23 @@ class UserManager: def change_password(uid, old_pass, new_pass): user = UserManager.get(uid) if UserManager.__encrypt_password(old_pass, user.salt)[0] != user.password: - raise ValueError('Wrong password') + raise ValueError("Wrong password") user.password = UserManager.__encrypt_password(new_pass, user.salt)[0] @staticmethod def change_password2(name, new_pass): - user = User.get(name = name) + user = User.get(name=name) if user is None: raise ObjectNotFound(User) user.password = UserManager.__encrypt_password(new_pass, user.salt)[0] @staticmethod - def __encrypt_password(password, salt = None): + def __encrypt_password(password, salt=None): if salt is None: - salt = ''.join(random.choice(string.printable.strip()) for _ in range(6)) - return hashlib.sha1(salt.encode('utf-8') + password.encode('utf-8')).hexdigest(), salt - + salt = "".join(random.choice(string.printable.strip()) for _ in range(6)) + return ( + hashlib.sha1(salt.encode("utf-8") + password.encode("utf-8")).hexdigest(), + salt, + ) diff --git a/supysonic/py23.py b/supysonic/py23.py index ea9d754..653f7aa 100644 --- a/supysonic/py23.py +++ b/supysonic/py23.py @@ -22,10 +22,12 @@ except ImportError: # On Windows an existing file will not be overwritten # This fallback just attempts to delete the dst file before using rename import sys - if sys.platform != 'win32': + + if sys.platform != "win32": from os import rename as osreplace else: import os + def osreplace(src, dst): try: os.remove(dst) @@ -33,6 +35,7 @@ except ImportError: pass os.rename(src, dst) + try: from queue import Queue, Empty as QueueEmpty except ImportError: @@ -60,6 +63,7 @@ try: def items(self): return self.viewitems() + except NameError: # Python 3 strtype = str diff --git a/supysonic/scanner.py b/supysonic/scanner.py index c6d0eb8..fe7cd82 100644 --- a/supysonic/scanner.py +++ b/supysonic/scanner.py @@ -24,12 +24,14 @@ from .py23 import strtype, Queue, QueueEmpty logger = logging.getLogger(__name__) + class StatsDetails(object): def __init__(self): self.artists = 0 self.albums = 0 self.tracks = 0 + class Stats(object): def __init__(self): self.scanned = 0 @@ -37,6 +39,7 @@ class Stats(object): self.deleted = StatsDetails() self.errors = [] + class ScanQueue(Queue): def _init(self, maxsize): self.queue = set() @@ -50,13 +53,21 @@ class ScanQueue(Queue): self.__last_got = self.queue.pop() return self.__last_got + class Scanner(Thread): - def __init__(self, force = False, extensions = None, progress = None, - on_folder_start = None, on_folder_end = None, on_done = None): + def __init__( + self, + force=False, + extensions=None, + progress=None, + on_folder_start=None, + on_folder_end=None, + on_done=None, + ): super(Scanner, self).__init__() if extensions is not None and not isinstance(extensions, list): - raise TypeError('Invalid extensions type') + raise TypeError("Invalid extensions type") self.__force = force self.__extensions = extensions @@ -80,7 +91,7 @@ class Scanner(Thread): def queue_folder(self, folder_name): if not isinstance(folder_name, strtype): - raise TypeError('Expecting string, got ' + str(type(folder_name))) + raise TypeError("Expecting string, got " + str(type(folder_name))) self.__queue.put(folder_name) @@ -92,7 +103,7 @@ class Scanner(Thread): break with db_session: - folder = Folder.get(name = folder_name, root = True) + folder = Folder.get(name=folder_name, root=True) if folder is None: continue @@ -107,13 +118,13 @@ class Scanner(Thread): self.__stopped.set() def __scan_folder(self, folder): - logger.info('Scanning folder %s', folder.name) + logger.info("Scanning folder %s", folder.name) if self.__on_folder_start is not None: self.__on_folder_start(folder) # Scan new/updated files - to_scan = [ folder.path ] + to_scan = [folder.path] scanned = 0 while not self.__stopped.is_set() and to_scan: path = to_scan.pop() @@ -124,8 +135,8 @@ class Scanner(Thread): continue for f in entries: - try: # test for badly encoded filenames - f.encode('utf-8') + try: # test for badly encoded filenames + f.encode("utf-8") except UnicodeError: self.__stats.errors.append(path) continue @@ -150,15 +161,17 @@ class Scanner(Thread): self.remove_file(track.path) # Remove deleted/moved folders and update cover art info - folders = [ folder ] + folders = [folder] while not self.__stopped.is_set() and folders: f = folders.pop() with db_session: - f = Folder[f.id] # f has been fetched from another session, refetch or Pony will complain + f = Folder[ + f.id + ] # f has been fetched from another session, refetch or Pony will complain if not f.root and not os.path.isdir(f.path): - f.delete() # Pony will cascade + f.delete() # Pony will cascade continue self.find_cover(f.path) @@ -190,10 +203,12 @@ class Scanner(Thread): @db_session def scan_file(self, path): if not isinstance(path, strtype): - raise TypeError('Expecting string, got ' + str(type(path))) + raise TypeError("Expecting string, got " + str(type(path))) - tr = Track.get(path = path) - mtime = int(os.path.getmtime(path)) if os.path.exists(path) else 0 # condition for some tests + tr = Track.get(path=path) + mtime = ( + int(os.path.getmtime(path)) if os.path.exists(path) else 0 + ) # condition for some tests if tr is not None: if not self.__force and not mtime > tr.last_modification: return @@ -208,50 +223,65 @@ class Scanner(Thread): if tag is None: return - trdict = { 'path': path } + trdict = {"path": path} - artist = self.__try_read_tag(tag, 'artist', '[unknown]')[:255] - album = self.__try_read_tag(tag, 'album', '[non-album tracks]')[:255] - albumartist = self.__try_read_tag(tag, 'albumartist', artist)[:255] + artist = self.__try_read_tag(tag, "artist", "[unknown]")[:255] + album = self.__try_read_tag(tag, "album", "[non-album tracks]")[:255] + albumartist = self.__try_read_tag(tag, "albumartist", artist)[:255] - trdict['disc'] = self.__try_read_tag(tag, 'discnumber', 1, lambda x: int(x.split('/')[0])) - trdict['number'] = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x.split('/')[0])) - trdict['title'] = self.__try_read_tag(tag, 'title', os.path.basename(path))[:255] - trdict['year'] = self.__try_read_tag(tag, 'date', None, lambda x: int(x.split('-')[0])) - trdict['genre'] = self.__try_read_tag(tag, 'genre') - trdict['duration'] = int(tag.info.length) - trdict['has_art'] = bool(Track._extract_cover_art(path)) + trdict["disc"] = self.__try_read_tag( + tag, "discnumber", 1, lambda x: int(x.split("/")[0]) + ) + trdict["number"] = self.__try_read_tag( + tag, "tracknumber", 1, lambda x: int(x.split("/")[0]) + ) + trdict["title"] = self.__try_read_tag(tag, "title", os.path.basename(path))[ + :255 + ] + trdict["year"] = self.__try_read_tag( + tag, "date", None, lambda x: int(x.split("-")[0]) + ) + trdict["genre"] = self.__try_read_tag(tag, "genre") + trdict["duration"] = int(tag.info.length) + trdict["has_art"] = bool(Track._extract_cover_art(path)) - trdict['bitrate'] = int(tag.info.bitrate if hasattr(tag.info, 'bitrate') else os.path.getsize(path) * 8 / tag.info.length) // 1000 - trdict['last_modification'] = mtime + trdict["bitrate"] = ( + int( + tag.info.bitrate + if hasattr(tag.info, "bitrate") + else os.path.getsize(path) * 8 / tag.info.length + ) + // 1000 + ) + trdict["last_modification"] = mtime tralbum = self.__find_album(albumartist, album) trartist = self.__find_artist(artist) if tr is None: - trdict['root_folder'] = self.__find_root_folder(path) - trdict['folder'] = self.__find_folder(path) - trdict['album'] = tralbum - trdict['artist'] = trartist - trdict['created'] = datetime.fromtimestamp(mtime) + trdict["root_folder"] = self.__find_root_folder(path) + trdict["folder"] = self.__find_folder(path) + trdict["album"] = tralbum + trdict["artist"] = trartist + trdict["created"] = datetime.fromtimestamp(mtime) Track(**trdict) self.__stats.added.tracks += 1 else: if tr.album.id != tralbum.id: - trdict['album'] = tralbum + trdict["album"] = tralbum if tr.artist.id != trartist.id: - trdict['artist'] = trartist + trdict["artist"] = trartist tr.set(**trdict) @db_session def remove_file(self, path): if not isinstance(path, strtype): - raise TypeError('Expecting string, got ' + str(type(path))) + raise TypeError("Expecting string, got " + str(type(path))) - tr = Track.get(path = path) + tr = Track.get(path=path) if not tr: return @@ -261,18 +291,18 @@ class Scanner(Thread): @db_session def move_file(self, src_path, dst_path): if not isinstance(src_path, strtype): - raise TypeError('Expecting string, got ' + str(type(src_path))) + raise TypeError("Expecting string, got " + str(type(src_path))) if not isinstance(dst_path, strtype): - raise TypeError('Expecting string, got ' + str(type(dst_path))) + raise TypeError("Expecting string, got " + str(type(dst_path))) if src_path == dst_path: return - tr = Track.get(path = src_path) + tr = Track.get(path=src_path) if tr is None: return - tr_dst = Track.get(path = dst_path) + tr_dst = Track.get(path=dst_path) if tr_dst is not None: root = tr_dst.root_folder folder = tr_dst.folder @@ -288,13 +318,13 @@ class Scanner(Thread): @db_session def find_cover(self, dirpath): - if not isinstance(dirpath, strtype): # pragma: nocover - raise TypeError('Expecting string, got ' + str(type(dirpath))) + if not isinstance(dirpath, strtype): # pragma: nocover + raise TypeError("Expecting string, got " + str(type(dirpath))) if not os.path.exists(dirpath): return - folder = Folder.get(path = dirpath) + folder = Folder.get(path=dirpath) if folder is None: return @@ -308,10 +338,10 @@ class Scanner(Thread): @db_session def add_cover(self, path): - if not isinstance(path, strtype): # pragma: nocover - raise TypeError('Expecting string, got ' + str(type(path))) + if not isinstance(path, strtype): # pragma: nocover + raise TypeError("Expecting string, got " + str(type(path))) - folder = Folder.get(path = os.path.dirname(path)) + folder = Folder.get(path=os.path.dirname(path)) if folder is None: return @@ -335,17 +365,17 @@ class Scanner(Thread): if al: return al - al = Album(name = album, artist = ar) + al = Album(name=album, artist=ar) self.__stats.added.albums += 1 return al def __find_artist(self, artist): - ar = Artist.get(name = artist) + ar = Artist.get(name=artist) if ar: return ar - ar = Artist(name = artist) + ar = Artist(name=artist) self.__stats.added.artists += 1 return ar @@ -356,37 +386,45 @@ class Scanner(Thread): if path.startswith(folder.path): return folder - raise Exception("Couldn't find the root folder for '{}'.\nDon't scan files that aren't located in a defined music folder".format(path)) + raise Exception( + "Couldn't find the root folder for '{}'.\nDon't scan files that aren't located in a defined music folder".format( + path + ) + ) def __find_folder(self, path): children = [] drive, _ = os.path.splitdrive(path) path = os.path.dirname(path) - while path != drive and path != '/': - folder = Folder.get(path = path) + while path != drive and path != "/": + folder = Folder.get(path=path) if folder is not None: break created = datetime.fromtimestamp(os.path.getmtime(path)) - children.append(dict(root = False, name = os.path.basename(path), path = path, created = created)) + children.append( + dict( + root=False, name=os.path.basename(path), path=path, created=created + ) + ) path = os.path.dirname(path) assert folder is not None while children: - folder = Folder(parent = folder, **children.pop()) + folder = Folder(parent=folder, **children.pop()) return folder def __try_load_tag(self, path): try: - return mutagen.File(path, easy = True) + return mutagen.File(path, easy=True) except mutagen.MutagenError: return None - def __try_read_tag(self, metadata, field, default = None, transform = None): + def __try_read_tag(self, metadata, field, default=None, transform=None): try: value = metadata[field][0] - value = value.replace('\x00', '').strip() + value = value.replace("\x00", "").strip() if not value: return default @@ -401,4 +439,3 @@ class Scanner(Thread): def stats(self): return self.__stats - diff --git a/supysonic/schema/migration/mysql/20171230.py b/supysonic/schema/migration/mysql/20171230.py index 00e80a9..9b467a0 100644 --- a/supysonic/schema/migration/mysql/20171230.py +++ b/supysonic/schema/migration/mysql/20171230.py @@ -11,6 +11,7 @@ # Converts ids from hex-encoded strings to binary data import argparse + try: import MySQLdb as provider except ImportError: @@ -20,17 +21,18 @@ from uuid import UUID from warnings import filterwarnings parser = argparse.ArgumentParser() -parser.add_argument('username') -parser.add_argument('password') -parser.add_argument('database') -parser.add_argument('-H', '--host', default = 'localhost', help = 'default: localhost') +parser.add_argument("username") +parser.add_argument("password") +parser.add_argument("database") +parser.add_argument("-H", "--host", default="localhost", help="default: localhost") args = parser.parse_args() -def process_table(connection, table, fields, nullable_fields = ()): - to_update = { field: set() for field in fields + nullable_fields } + +def process_table(connection, table, fields, nullable_fields=()): + to_update = {field: set() for field in fields + nullable_fields} c = connection.cursor() - c.execute('SELECT {1} FROM {0}'.format(table, ','.join(fields + nullable_fields))) + c.execute("SELECT {1} FROM {0}".format(table, ",".join(fields + nullable_fields))) for row in c: for field, value in zip(fields + nullable_fields, row): if value is None or not isinstance(value, basestring): @@ -40,36 +42,40 @@ def process_table(connection, table, fields, nullable_fields = ()): for field, values in to_update.iteritems(): if not values: continue - sql = 'UPDATE {0} SET {1}=%s WHERE {1}=%s'.format(table, field) + sql = "UPDATE {0} SET {1}=%s WHERE {1}=%s".format(table, field) c.executemany(sql, map(lambda v: (UUID(v).bytes, v), values)) for field in fields: - sql = 'ALTER TABLE {0} MODIFY {1} BINARY(16) NOT NULL'.format(table, field) + sql = "ALTER TABLE {0} MODIFY {1} BINARY(16) NOT NULL".format(table, field) c.execute(sql) for field in nullable_fields: - sql = 'ALTER TABLE {0} MODIFY {1} BINARY(16)'.format(table, field) + sql = "ALTER TABLE {0} MODIFY {1} BINARY(16)".format(table, field) c.execute(sql) connection.commit() -filterwarnings('ignore', category = provider.Warning) -conn = provider.connect(host = args.host, user = args.username, passwd = args.password, db = args.database) -conn.cursor().execute('SET FOREIGN_KEY_CHECKS = 0') -process_table(conn, 'folder', ('id',), ('parent_id',)) -process_table(conn, 'artist', ('id',)) -process_table(conn, 'album', ('id', 'artist_id')) -process_table(conn, 'track', ('id', 'album_id', 'artist_id', 'root_folder_id', 'folder_id')) -process_table(conn, 'user', ('id',), ('last_play_id',)) -process_table(conn, 'client_prefs', ('user_id',)) -process_table(conn, 'starred_folder', ('user_id', 'starred_id')) -process_table(conn, 'starred_artist', ('user_id', 'starred_id')) -process_table(conn, 'starred_album', ('user_id', 'starred_id')) -process_table(conn, 'starred_track', ('user_id', 'starred_id')) -process_table(conn, 'rating_folder', ('user_id', 'rated_id')) -process_table(conn, 'rating_track', ('user_id', 'rated_id')) -process_table(conn, 'chat_message', ('id', 'user_id')) -process_table(conn, 'playlist', ('id', 'user_id')) +filterwarnings("ignore", category=provider.Warning) +conn = provider.connect( + host=args.host, user=args.username, passwd=args.password, db=args.database +) +conn.cursor().execute("SET FOREIGN_KEY_CHECKS = 0") -conn.cursor().execute('SET FOREIGN_KEY_CHECKS = 1') +process_table(conn, "folder", ("id",), ("parent_id",)) +process_table(conn, "artist", ("id",)) +process_table(conn, "album", ("id", "artist_id")) +process_table( + conn, "track", ("id", "album_id", "artist_id", "root_folder_id", "folder_id") +) +process_table(conn, "user", ("id",), ("last_play_id",)) +process_table(conn, "client_prefs", ("user_id",)) +process_table(conn, "starred_folder", ("user_id", "starred_id")) +process_table(conn, "starred_artist", ("user_id", "starred_id")) +process_table(conn, "starred_album", ("user_id", "starred_id")) +process_table(conn, "starred_track", ("user_id", "starred_id")) +process_table(conn, "rating_folder", ("user_id", "rated_id")) +process_table(conn, "rating_track", ("user_id", "rated_id")) +process_table(conn, "chat_message", ("id", "user_id")) +process_table(conn, "playlist", ("id", "user_id")) + +conn.cursor().execute("SET FOREIGN_KEY_CHECKS = 1") conn.close() - diff --git a/supysonic/schema/migration/postgres/20180317.py b/supysonic/schema/migration/postgres/20180317.py index c17e369..a9c3dfe 100644 --- a/supysonic/schema/migration/postgres/20180317.py +++ b/supysonic/schema/migration/postgres/20180317.py @@ -9,26 +9,36 @@ except: pass parser = argparse.ArgumentParser() -parser.add_argument('username') -parser.add_argument('password') -parser.add_argument('database') -parser.add_argument('-H', '--host', default = 'localhost', help = 'default: localhost') +parser.add_argument("username") +parser.add_argument("password") +parser.add_argument("database") +parser.add_argument("-H", "--host", default="localhost", help="default: localhost") args = parser.parse_args() + def process_table(connection, table): c = connection.cursor() - c.execute(r"ALTER TABLE {0} ADD COLUMN path_hash BYTEA NOT NULL DEFAULT E'\\0000'".format(table)) + c.execute( + r"ALTER TABLE {0} ADD COLUMN path_hash BYTEA NOT NULL DEFAULT E'\\0000'".format( + table + ) + ) hashes = dict() - c.execute('SELECT path FROM {0}'.format(table)) + c.execute("SELECT path FROM {0}".format(table)) for row in c.fetchall(): - hashes[row[0]] = hashlib.sha1(row[0].encode('utf-8')).digest() - c.executemany('UPDATE {0} SET path_hash=%s WHERE path=%s'.format(table), [ (bytes(h), p) for p, h in hashes.items() ]) + hashes[row[0]] = hashlib.sha1(row[0].encode("utf-8")).digest() + c.executemany( + "UPDATE {0} SET path_hash=%s WHERE path=%s".format(table), + [(bytes(h), p) for p, h in hashes.items()], + ) - c.execute('CREATE UNIQUE INDEX index_{0}_path ON {0}(path_hash)'.format(table)) + c.execute("CREATE UNIQUE INDEX index_{0}_path ON {0}(path_hash)".format(table)) -with psycopg2.connect(host = args.host, user = args.username, password = args.password, dbname = args.database) as conn: - process_table(conn, 'folder') - process_table(conn, 'track') +with psycopg2.connect( + host=args.host, user=args.username, password=args.password, dbname=args.database +) as conn: + process_table(conn, "folder") + process_table(conn, "track") diff --git a/supysonic/schema/migration/sqlite/20171230.py b/supysonic/schema/migration/sqlite/20171230.py index e1ebf52..7871614 100644 --- a/supysonic/schema/migration/sqlite/20171230.py +++ b/supysonic/schema/migration/sqlite/20171230.py @@ -16,40 +16,43 @@ import sqlite3 from uuid import UUID parser = argparse.ArgumentParser() -parser.add_argument('dbfile', help = 'Path to the SQLite database file') +parser.add_argument("dbfile", help="Path to the SQLite database file") args = parser.parse_args() + def process_table(connection, table, fields): - to_update = { field: set() for field in fields } + to_update = {field: set() for field in fields} c = connection.cursor() - for row in c.execute('SELECT {1} FROM {0}'.format(table, ','.join(fields))): + for row in c.execute("SELECT {1} FROM {0}".format(table, ",".join(fields))): for field, value in zip(fields, row): if value is None or not isinstance(value, basestring): continue to_update[field].add(value) for field, values in to_update.iteritems(): - sql = 'UPDATE {0} SET {1}=? WHERE {1}=?'.format(table, field) + sql = "UPDATE {0} SET {1}=? WHERE {1}=?".format(table, field) c.executemany(sql, map(lambda v: (buffer(UUID(v).bytes), v), values)) connection.commit() + with sqlite3.connect(args.dbfile) as conn: - conn.cursor().execute('PRAGMA foreign_keys = OFF') - - process_table(conn, 'folder', ('id', 'parent_id')) - process_table(conn, 'artist', ('id',)) - process_table(conn, 'album', ('id', 'artist_id')) - process_table(conn, 'track', ('id', 'album_id', 'artist_id', 'root_folder_id', 'folder_id')) - process_table(conn, 'user', ('id', 'last_play_id')) - process_table(conn, 'client_prefs', ('user_id',)) - process_table(conn, 'starred_folder', ('user_id', 'starred_id')) - process_table(conn, 'starred_artist', ('user_id', 'starred_id')) - process_table(conn, 'starred_album', ('user_id', 'starred_id')) - process_table(conn, 'starred_track', ('user_id', 'starred_id')) - process_table(conn, 'rating_folder', ('user_id', 'rated_id')) - process_table(conn, 'rating_track', ('user_id', 'rated_id')) - process_table(conn, 'chat_message', ('id', 'user_id')) - process_table(conn, 'playlist', ('id', 'user_id')) + conn.cursor().execute("PRAGMA foreign_keys = OFF") + process_table(conn, "folder", ("id", "parent_id")) + process_table(conn, "artist", ("id",)) + process_table(conn, "album", ("id", "artist_id")) + process_table( + conn, "track", ("id", "album_id", "artist_id", "root_folder_id", "folder_id") + ) + process_table(conn, "user", ("id", "last_play_id")) + process_table(conn, "client_prefs", ("user_id",)) + process_table(conn, "starred_folder", ("user_id", "starred_id")) + process_table(conn, "starred_artist", ("user_id", "starred_id")) + process_table(conn, "starred_album", ("user_id", "starred_id")) + process_table(conn, "starred_track", ("user_id", "starred_id")) + process_table(conn, "rating_folder", ("user_id", "rated_id")) + process_table(conn, "rating_track", ("user_id", "rated_id")) + process_table(conn, "chat_message", ("id", "user_id")) + process_table(conn, "playlist", ("id", "user_id")) diff --git a/supysonic/schema/migration/sqlite/20180317.py b/supysonic/schema/migration/sqlite/20180317.py index 90ca091..08c09ee 100644 --- a/supysonic/schema/migration/sqlite/20180317.py +++ b/supysonic/schema/migration/sqlite/20180317.py @@ -9,23 +9,29 @@ except: pass parser = argparse.ArgumentParser() -parser.add_argument('dbfile', help = 'Path to the SQLite database file') +parser.add_argument("dbfile", help="Path to the SQLite database file") args = parser.parse_args() + def process_table(connection, table): c = connection.cursor() - c.execute('ALTER TABLE {0} ADD COLUMN path_hash BLOB NOT NULL DEFAULT ROWID'.format(table)) + c.execute( + "ALTER TABLE {0} ADD COLUMN path_hash BLOB NOT NULL DEFAULT ROWID".format(table) + ) hashes = dict() - for row in c.execute('SELECT path FROM {0}'.format(table)): - hashes[row[0]] = hashlib.sha1(row[0].encode('utf-8')).digest() - c.executemany('UPDATE {0} SET path_hash=? WHERE path=?'.format(table), [ (bytes(h), p) for p, h in hashes.items() ]) + for row in c.execute("SELECT path FROM {0}".format(table)): + hashes[row[0]] = hashlib.sha1(row[0].encode("utf-8")).digest() + c.executemany( + "UPDATE {0} SET path_hash=? WHERE path=?".format(table), + [(bytes(h), p) for p, h in hashes.items()], + ) + + c.execute("CREATE UNIQUE INDEX index_{0}_path ON {0}(path_hash)".format(table)) - c.execute('CREATE UNIQUE INDEX index_{0}_path ON {0}(path_hash)'.format(table)) with sqlite3.connect(args.dbfile) as conn: - process_table(conn, 'folder') - process_table(conn, 'track') - conn.cursor().execute('VACUUM') - + process_table(conn, "folder") + process_table(conn, "track") + conn.cursor().execute("VACUUM") diff --git a/supysonic/utils.py b/supysonic/utils.py index a20458f..eb0fbbe 100644 --- a/supysonic/utils.py +++ b/supysonic/utils.py @@ -13,6 +13,7 @@ from pony.orm import db_session, commit, ObjectNotFound from supysonic.db import Meta + @db_session def get_secret_key(keyname): # Commit both at enter and exit. The metadb/db split (from supysonic.db) @@ -22,6 +23,6 @@ def get_secret_key(keyname): key = b64decode(Meta[keyname].value) except ObjectNotFound: key = urandom(128) - Meta(key = keyname, value = b64encode(key).decode()) + Meta(key=keyname, value=b64encode(key).decode()) commit() return key diff --git a/supysonic/watcher.py b/supysonic/watcher.py index 88cf112..f72b588 100644 --- a/supysonic/watcher.py +++ b/supysonic/watcher.py @@ -21,25 +21,30 @@ from .db import Folder from .py23 import dict, strtype from .scanner import Scanner -OP_SCAN = 1 -OP_REMOVE = 2 -OP_MOVE = 4 +OP_SCAN = 1 +OP_REMOVE = 2 +OP_MOVE = 4 FLAG_CREATE = 8 -FLAG_COVER = 16 +FLAG_COVER = 16 logger = logging.getLogger(__name__) + class SupysonicWatcherEventHandler(PatternMatchingEventHandler): def __init__(self, extensions): patterns = None if extensions: - patterns = list(map(lambda e: "*." + e.lower(), extensions.split())) + list(map(lambda e: "*" + e, covers.EXTENSIONS)) - super(SupysonicWatcherEventHandler, self).__init__(patterns = patterns, ignore_directories = True) + patterns = list(map(lambda e: "*." + e.lower(), extensions.split())) + list( + map(lambda e: "*" + e, covers.EXTENSIONS) + ) + super(SupysonicWatcherEventHandler, self).__init__( + patterns=patterns, ignore_directories=True + ) def dispatch(self, event): try: super(SupysonicWatcherEventHandler, self).dispatch(event) - except Exception as e: # pragma: nocover + except Exception as e: # pragma: nocover logger.critical(e) def on_created(self, event): @@ -51,7 +56,7 @@ class SupysonicWatcherEventHandler(PatternMatchingEventHandler): dirname = os.path.dirname(event.src_path) with db_session: - folder = Folder.get(path = dirname) + folder = Folder.get(path=dirname) if folder is None: self.queue.put(dirname, op | FLAG_COVER) else: @@ -78,12 +83,13 @@ class SupysonicWatcherEventHandler(PatternMatchingEventHandler): _, ext = os.path.splitext(event.src_path) if ext in covers.EXTENSIONS: op |= FLAG_COVER - self.queue.put(event.dest_path, op, src_path = event.src_path) + self.queue.put(event.dest_path, op, src_path=event.src_path) + class Event(object): def __init__(self, path, operation, **kwargs): if operation & (OP_SCAN | OP_REMOVE) == (OP_SCAN | OP_REMOVE): - raise Exception("Flags SCAN and REMOVE both set") # pragma: nocover + raise Exception("Flags SCAN and REMOVE both set") # pragma: nocover self.__path = path self.__time = time.time() @@ -92,7 +98,7 @@ class Event(object): def set(self, operation, **kwargs): if operation & (OP_SCAN | OP_REMOVE) == (OP_SCAN | OP_REMOVE): - raise Exception("Flags SCAN and REMOVE both set") # pragma: nocover + raise Exception("Flags SCAN and REMOVE both set") # pragma: nocover self.__time = time.time() if operation & OP_SCAN: @@ -123,6 +129,7 @@ class Event(object): def src_path(self): return self.__src + class ScannerProcessingQueue(Thread): def __init__(self, delay): super(ScannerProcessingQueue, self).__init__() @@ -136,7 +143,7 @@ class ScannerProcessingQueue(Thread): def run(self): try: self.__run() - except Exception as e: # pragma: nocover + except Exception as e: # pragma: nocover logger.critical(e) raise e @@ -216,7 +223,7 @@ class ScannerProcessingQueue(Thread): if operation & OP_MOVE and kwargs["src_path"] in self.__queue: previous = self.__queue[kwargs["src_path"]] - event.set(previous.operation, src_path = previous.src_path) + event.set(previous.operation, src_path=previous.src_path) del self.__queue[kwargs["src_path"]] if self.__timer: @@ -240,17 +247,18 @@ class ScannerProcessingQueue(Thread): if not self.__queue: return None - next = min(self.__queue.items(), key = lambda i: i[1].time) + next = min(self.__queue.items(), key=lambda i: i[1].time) if not self.__running or next[1].time + self.__timeout <= time.time(): del self.__queue[next[0]] return next[1] return None + class SupysonicWatcher(object): def __init__(self, config): - self.__delay = config.DAEMON['wait_delay'] - self.__handler = SupysonicWatcherEventHandler(config.BASE['scanner_extensions']) + self.__delay = config.DAEMON["wait_delay"] + self.__handler = SupysonicWatcherEventHandler(config.BASE["scanner_extensions"]) self.__folders = {} self.__queue = None @@ -262,10 +270,10 @@ class SupysonicWatcher(object): elif isinstance(folder, strtype): path = folder else: - raise TypeError('Expecting string or Folder, got ' + str(type(folder))) + raise TypeError("Expecting string or Folder, got " + str(type(folder))) logger.info("Scheduling watcher for %s", path) - watch = self.__observer.schedule(self.__handler, path, recursive = True) + watch = self.__observer.schedule(self.__handler, path, recursive=True) self.__folders[path] = watch def remove_folder(self, folder): @@ -274,7 +282,7 @@ class SupysonicWatcher(object): elif isinstance(folder, strtype): path = folder else: - raise TypeError('Expecting string or Folder, got ' + str(type(folder))) + raise TypeError("Expecting string or Folder, got " + str(type(folder))) logger.info("Unscheduling watcher for %s", path) self.__observer.unschedule(self.__folders[path]) @@ -309,4 +317,9 @@ class SupysonicWatcher(object): @property def running(self): - return self.__queue is not None and self.__observer is not None and self.__queue.is_alive() and self.__observer.is_alive() + return ( + self.__queue is not None + and self.__observer is not None + and self.__queue.is_alive() + and self.__observer.is_alive() + ) diff --git a/supysonic/web.py b/supysonic/web.py index e773c99..7bc7282 100644 --- a/supysonic/web.py +++ b/supysonic/web.py @@ -24,61 +24,66 @@ from .utils import get_secret_key logger = logging.getLogger(__package__) -def create_application(config = None): + +def create_application(config=None): global app # Flask! app = Flask(__name__) - app.config.from_object('supysonic.config.DefaultConfig') + app.config.from_object("supysonic.config.DefaultConfig") - if not config: # pragma: nocover + if not config: # pragma: nocover config = IniConfig.from_common_locations() app.config.from_object(config) # Set loglevel - logfile = app.config['WEBAPP']['log_file'] - if logfile: # pragma: nocover + logfile = app.config["WEBAPP"]["log_file"] + if logfile: # pragma: nocover from logging.handlers import TimedRotatingFileHandler - handler = TimedRotatingFileHandler(logfile, when = 'midnight') - handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")) + + handler = TimedRotatingFileHandler(logfile, when="midnight") + handler.setFormatter( + logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") + ) logger.addHandler(handler) - loglevel = app.config['WEBAPP']['log_level'] + loglevel = app.config["WEBAPP"]["log_level"] if loglevel: logger.setLevel(getattr(logging, loglevel.upper(), logging.NOTSET)) # Initialize database - init_database(app.config['BASE']['database_uri']) + init_database(app.config["BASE"]["database_uri"]) app.wsgi_app = db_session(app.wsgi_app) # Insert unknown mimetypes - for k, v in app.config['MIMETYPES'].items(): - extension = '.' + k.lower() + for k, v in app.config["MIMETYPES"].items(): + extension = "." + k.lower() if extension not in mimetypes.types_map: mimetypes.add_type(v, extension, False) # Initialize Cache objects # Max size is MB in the config file but Cache expects bytes - cache_dir = app.config['WEBAPP']['cache_dir'] - max_size_cache = app.config['WEBAPP']['cache_size'] * 1024**2 - max_size_transcodes = app.config['WEBAPP']['transcode_cache_size'] * 1024**2 + cache_dir = app.config["WEBAPP"]["cache_dir"] + max_size_cache = app.config["WEBAPP"]["cache_size"] * 1024 ** 2 + max_size_transcodes = app.config["WEBAPP"]["transcode_cache_size"] * 1024 ** 2 app.cache = Cache(path.join(cache_dir, "cache"), max_size_cache) app.transcode_cache = Cache(path.join(cache_dir, "transcodes"), max_size_transcodes) # Test for the cache directory - cache_path = app.config['WEBAPP']['cache_dir'] + cache_path = app.config["WEBAPP"]["cache_dir"] if not path.exists(cache_path): - makedirs(cache_path) # pragma: nocover + makedirs(cache_path) # pragma: nocover # Read or create secret key - app.secret_key = get_secret_key('cookies_secret') + app.secret_key = get_secret_key("cookies_secret") # Import app sections - if app.config['WEBAPP']['mount_webui']: + if app.config["WEBAPP"]["mount_webui"]: from .frontend import frontend + app.register_blueprint(frontend) - if app.config['WEBAPP']['mount_api']: + if app.config["WEBAPP"]["mount_api"]: from .api import api - app.register_blueprint(api, url_prefix = '/rest') + + app.register_blueprint(api, url_prefix="/rest") return app - diff --git a/tests/__init__.py b/tests/__init__.py index cdc0b64..1ca37dc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -21,6 +21,7 @@ from .issue133 import Issue133TestCase from .issue139 import Issue139TestCase from .issue148 import Issue148TestCase + def suite(): suite = unittest.TestSuite() @@ -35,4 +36,3 @@ def suite(): suite.addTest(unittest.makeSuite(Issue148TestCase)) return suite - diff --git a/tests/api/__init__.py b/tests/api/__init__.py index 27093f6..979dbc2 100644 --- a/tests/api/__init__.py +++ b/tests/api/__init__.py @@ -22,6 +22,7 @@ from .test_annotation import AnnotationTestCase from .test_media import MediaTestCase from .test_transcoding import TranscodingTestCase + def suite(): suite = unittest.TestSuite() @@ -39,4 +40,3 @@ def suite(): suite.addTest(unittest.makeSuite(TranscodingTestCase)) return suite - diff --git a/tests/api/apitestbase.py b/tests/api/apitestbase.py index 38af4b0..24f3f20 100644 --- a/tests/api/apitestbase.py +++ b/tests/api/apitestbase.py @@ -15,10 +15,11 @@ from supysonic.py23 import strtype from ..testbase import TestBase -path_replace_regexp = re.compile(r'/(\w+)') +path_replace_regexp = re.compile(r"/(\w+)") + +NS = "http://subsonic.org/restapi" +NSMAP = {"sub": NS} -NS = 'http://subsonic.org/restapi' -NSMAP = { 'sub': NS } class ApiTestBase(TestBase): __with_api__ = True @@ -26,7 +27,7 @@ class ApiTestBase(TestBase): def setUp(self): super(ApiTestBase, self).setUp() - xsd = etree.parse('tests/assets/subsonic-rest-api-1.9.0.xsd') + xsd = etree.parse("tests/assets/subsonic-rest-api-1.9.0.xsd") self.schema = etree.XMLSchema(xsd) def _find(self, xml, path): @@ -34,7 +35,7 @@ class ApiTestBase(TestBase): Helper method that insert the namespace in ElementPath 'path' """ - path = path_replace_regexp.sub(r'/{{{}}}\1'.format(NS), path) + path = path_replace_regexp.sub(r"/{{{}}}\1".format(NS), path) return xml.find(path) def _xpath(self, elem, path): @@ -42,10 +43,12 @@ class ApiTestBase(TestBase): Helper method that insert a prefix and map the namespace in XPath 'path' """ - path = path_replace_regexp.sub(r'/sub:\1', path) - return elem.xpath(path, namespaces = NSMAP) + path = path_replace_regexp.sub(r"/sub:\1", path) + return elem.xpath(path, namespaces=NSMAP) - def _make_request(self, endpoint, args = {}, tag = None, error = None, skip_post = False, skip_xsd = False): + def _make_request( + self, endpoint, args={}, tag=None, error=None, skip_post=False, skip_xsd=False + ): """ Makes both a GET and POST requests against the API, assert both get the same response. If the user isn't provided with the 'u' and 'p' in 'args', the default 'alice' is used. @@ -68,31 +71,30 @@ class ApiTestBase(TestBase): if tag and not isinstance(tag, strtype): raise TypeError("'tag', expecting a str, got " + type(tag).__name__) - args.update({ 'c': 'tests', 'v': '1.9.0' }) - if 'u' not in args: - args.update({ 'u': 'alice', 'p': 'Alic3' }) + args.update({"c": "tests", "v": "1.9.0"}) + if "u" not in args: + args.update({"u": "alice", "p": "Alic3"}) - uri = '/rest/{}.view'.format(endpoint) - rg = self.client.get(uri, query_string = args) + uri = "/rest/{}.view".format(endpoint) + rg = self.client.get(uri, query_string=args) if not skip_post: - rp = self.client.post(uri, data = args) + rp = self.client.post(uri, data=args) self.assertEqual(rg.data, rp.data) xml = etree.fromstring(rg.data) if not skip_xsd: self.schema.assert_(xml) - if xml.get('status') == 'ok': + if xml.get("status") == "ok": self.assertIsNone(error) if tag: - self.assertEqual(xml[0].tag, '{{{}}}{}'.format(NS, tag)) + self.assertEqual(xml[0].tag, "{{{}}}{}".format(NS, tag)) return rg, xml[0] else: self.assertEqual(len(xml), 0) return rg, None else: self.assertIsNone(tag) - self.assertEqual(xml[0].tag, '{{{}}}error'.format(NS)) - self.assertEqual(xml[0].get('code'), str(error)) + self.assertEqual(xml[0].tag, "{{{}}}error".format(NS)) + self.assertEqual(xml[0].get("code"), str(error)) return rg - diff --git a/tests/api/test_album_songs.py b/tests/api/test_album_songs.py index 7ff080e..10fefbc 100644 --- a/tests/api/test_album_songs.py +++ b/tests/api/test_album_songs.py @@ -16,6 +16,7 @@ from supysonic.db import Folder, Artist, Album, Track from .apitestbase import ApiTestBase + class AlbumSongsTestCase(ApiTestBase): # I'm too lazy to write proper tests concerning the data on those endpoints # Let's just check paramter validation and ensure coverage @@ -24,82 +25,127 @@ class AlbumSongsTestCase(ApiTestBase): super(AlbumSongsTestCase, self).setUp() with db_session: - folder = Folder(name = 'Root', root = True, path = 'tests/assets') - artist = Artist(name = 'Artist') - album = Album(name = 'Album', artist = artist) + folder = Folder(name="Root", root=True, path="tests/assets") + artist = Artist(name="Artist") + album = Album(name="Album", artist=artist) track = Track( - title = 'Track', - album = album, - artist = artist, - disc = 1, - number = 1, - path = 'tests/assets/empty', - folder = folder, - root_folder = folder, - duration = 2, - bitrate = 320, - last_modification = 0 + title="Track", + album=album, + artist=artist, + disc=1, + number=1, + path="tests/assets/empty", + folder=folder, + root_folder=folder, + duration=2, + bitrate=320, + last_modification=0, ) def test_get_album_list(self): - self._make_request('getAlbumList', error = 10) - self._make_request('getAlbumList', { 'type': 'kraken' }, error = 0) - self._make_request('getAlbumList', { 'type': 'random', 'size': 'huge' }, error = 0) - self._make_request('getAlbumList', { 'type': 'newest', 'offset': 'minus one' }, error = 0) + self._make_request("getAlbumList", error=10) + self._make_request("getAlbumList", {"type": "kraken"}, error=0) + self._make_request("getAlbumList", {"type": "random", "size": "huge"}, error=0) + self._make_request( + "getAlbumList", {"type": "newest", "offset": "minus one"}, error=0 + ) - types = [ 'random', 'newest', 'highest', 'frequent', 'recent', 'alphabeticalByName', - 'alphabeticalByArtist', 'starred' ] + types = [ + "random", + "newest", + "highest", + "frequent", + "recent", + "alphabeticalByName", + "alphabeticalByArtist", + "starred", + ] for t in types: - self._make_request('getAlbumList', { 'type': t }, tag = 'albumList', skip_post = True) + self._make_request( + "getAlbumList", {"type": t}, tag="albumList", skip_post=True + ) - rv, child = self._make_request('getAlbumList', { 'type': 'random' }, tag = 'albumList', skip_post = True) + rv, child = self._make_request( + "getAlbumList", {"type": "random"}, tag="albumList", skip_post=True + ) with db_session: Folder.get().delete() - rv, child = self._make_request('getAlbumList', { 'type': 'random' }, tag = 'albumList') + rv, child = self._make_request( + "getAlbumList", {"type": "random"}, tag="albumList" + ) self.assertEqual(len(child), 0) def test_get_album_list2(self): - self._make_request('getAlbumList2', error = 10) - self._make_request('getAlbumList2', { 'type': 'void' }, error = 0) - self._make_request('getAlbumList2', { 'type': 'random', 'size': 'size_t' }, error = 0) - self._make_request('getAlbumList2', { 'type': 'newest', 'offset': '&v + 2' }, error = 0) + self._make_request("getAlbumList2", error=10) + self._make_request("getAlbumList2", {"type": "void"}, error=0) + self._make_request( + "getAlbumList2", {"type": "random", "size": "size_t"}, error=0 + ) + self._make_request( + "getAlbumList2", {"type": "newest", "offset": "&v + 2"}, error=0 + ) - types = [ 'random', 'newest', 'frequent', 'recent', 'starred', 'alphabeticalByName', 'alphabeticalByArtist' ] + types = [ + "random", + "newest", + "frequent", + "recent", + "starred", + "alphabeticalByName", + "alphabeticalByArtist", + ] for t in types: - self._make_request('getAlbumList2', { 'type': t }, tag = 'albumList2', skip_post = True) + self._make_request( + "getAlbumList2", {"type": t}, tag="albumList2", skip_post=True + ) - rv, child = self._make_request('getAlbumList2', { 'type': 'random' }, tag = 'albumList2', skip_post = True) + rv, child = self._make_request( + "getAlbumList2", {"type": "random"}, tag="albumList2", skip_post=True + ) with db_session: Track.get().delete() Album.get().delete() - rv, child = self._make_request('getAlbumList2', { 'type': 'random' }, tag = 'albumList2') + rv, child = self._make_request( + "getAlbumList2", {"type": "random"}, tag="albumList2" + ) self.assertEqual(len(child), 0) def test_get_random_songs(self): - self._make_request('getRandomSongs', { 'size': '8 floors' }, error = 0) - self._make_request('getRandomSongs', { 'fromYear': 'year' }, error = 0) - self._make_request('getRandomSongs', { 'toYear': 'year' }, error = 0) - self._make_request('getRandomSongs', { 'musicFolderId': 'idid' }, error = 0) - self._make_request('getRandomSongs', { 'musicFolderId': uuid.uuid4() }, error = 70) + self._make_request("getRandomSongs", {"size": "8 floors"}, error=0) + self._make_request("getRandomSongs", {"fromYear": "year"}, error=0) + self._make_request("getRandomSongs", {"toYear": "year"}, error=0) + self._make_request("getRandomSongs", {"musicFolderId": "idid"}, error=0) + self._make_request("getRandomSongs", {"musicFolderId": uuid.uuid4()}, error=70) - rv, child = self._make_request('getRandomSongs', tag = 'randomSongs', skip_post = True) + rv, child = self._make_request( + "getRandomSongs", tag="randomSongs", skip_post=True + ) with db_session: fid = Folder.get().id - self._make_request('getRandomSongs', { 'fromYear': -52, 'toYear': '1984', 'genre': 'some cryptic subgenre youve never heard of', 'musicFolderId': fid }, tag = 'randomSongs') + self._make_request( + "getRandomSongs", + { + "fromYear": -52, + "toYear": "1984", + "genre": "some cryptic subgenre youve never heard of", + "musicFolderId": fid, + }, + tag="randomSongs", + ) def test_now_playing(self): - self._make_request('getNowPlaying', tag = 'nowPlaying') + self._make_request("getNowPlaying", tag="nowPlaying") def test_get_starred(self): - self._make_request('getStarred', tag = 'starred') + self._make_request("getStarred", tag="starred") def test_get_starred2(self): - self._make_request('getStarred2', tag = 'starred2') + self._make_request("getStarred2", tag="starred2") -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/api/test_annotation.py b/tests/api/test_annotation.py index b9aae03..ef2b732 100644 --- a/tests/api/test_annotation.py +++ b/tests/api/test_annotation.py @@ -16,145 +16,209 @@ from supysonic.db import Folder, Artist, Album, Track, User, ClientPrefs from .apitestbase import ApiTestBase + class AnnotationTestCase(ApiTestBase): def setUp(self): super(AnnotationTestCase, self).setUp() with db_session: - root = Folder(name = 'Root', root = True, path = 'tests') - folder = Folder(name = 'Folder', path = 'tests/assets', parent = root) - artist = Artist(name = 'Artist') - album = Album(name = 'Album', artist = artist) + root = Folder(name="Root", root=True, path="tests") + folder = Folder(name="Folder", path="tests/assets", parent=root) + artist = Artist(name="Artist") + album = Album(name="Album", artist=artist) track = Track( - title = 'Track', - album = album, - artist = artist, - disc = 1, - number = 1, - path = 'tests/assets/empty', - folder = folder, - root_folder = root, - duration = 2, - bitrate = 320, - last_modification = 0 + title="Track", + album=album, + artist=artist, + disc=1, + number=1, + path="tests/assets/empty", + folder=folder, + root_folder=root, + duration=2, + bitrate=320, + last_modification=0, ) self.folderid = folder.id self.artistid = artist.id self.albumid = album.id self.trackid = track.id - self.user = User.get(name = 'alice') + self.user = User.get(name="alice") def test_star(self): - self._make_request('star', error = 10) - self._make_request('star', { 'id': 'unknown' }, error = 0, skip_xsd = True) - self._make_request('star', { 'albumId': 'unknown' }, error = 0) - self._make_request('star', { 'artistId': 'unknown' }, error = 0) - self._make_request('star', { 'id': str(uuid.uuid4()) }, error = 70, skip_xsd = True) - self._make_request('star', { 'albumId': str(uuid.uuid4()) }, error = 70) - self._make_request('star', { 'artistId': str(uuid.uuid4()) }, error = 70) + self._make_request("star", error=10) + self._make_request("star", {"id": "unknown"}, error=0, skip_xsd=True) + self._make_request("star", {"albumId": "unknown"}, error=0) + self._make_request("star", {"artistId": "unknown"}, error=0) + self._make_request("star", {"id": str(uuid.uuid4())}, error=70, skip_xsd=True) + self._make_request("star", {"albumId": str(uuid.uuid4())}, error=70) + self._make_request("star", {"artistId": str(uuid.uuid4())}, error=70) - self._make_request('star', { 'id': str(self.artistid) }, error = 70, skip_xsd = True) - self._make_request('star', { 'id': str(self.albumid) }, error = 70, skip_xsd = True) - self._make_request('star', { 'id': str(self.trackid) }, skip_post = True) + self._make_request("star", {"id": str(self.artistid)}, error=70, skip_xsd=True) + self._make_request("star", {"id": str(self.albumid)}, error=70, skip_xsd=True) + self._make_request("star", {"id": str(self.trackid)}, skip_post=True) with db_session: - prefs = ClientPrefs.get(lambda p: p.user.name == 'alice' and p.client_name == 'tests') - self.assertIn('starred', Track[self.trackid].as_subsonic_child(self.user, prefs)) - self._make_request('star', { 'id': str(self.trackid) }, error = 0, skip_xsd = True) + prefs = ClientPrefs.get( + lambda p: p.user.name == "alice" and p.client_name == "tests" + ) + self.assertIn( + "starred", Track[self.trackid].as_subsonic_child(self.user, prefs) + ) + self._make_request("star", {"id": str(self.trackid)}, error=0, skip_xsd=True) - self._make_request('star', { 'id': str(self.folderid) }, skip_post = True) + self._make_request("star", {"id": str(self.folderid)}, skip_post=True) with db_session: - self.assertIn('starred', Folder[self.folderid].as_subsonic_child(self.user)) - self._make_request('star', { 'id': str(self.folderid) }, error = 0, skip_xsd = True) + self.assertIn("starred", Folder[self.folderid].as_subsonic_child(self.user)) + self._make_request("star", {"id": str(self.folderid)}, error=0, skip_xsd=True) - self._make_request('star', { 'albumId': str(self.folderid) }, error = 70) - self._make_request('star', { 'albumId': str(self.artistid) }, error = 70) - self._make_request('star', { 'albumId': str(self.trackid) }, error = 70) - self._make_request('star', { 'albumId': str(self.albumid) }, skip_post = True) + self._make_request("star", {"albumId": str(self.folderid)}, error=70) + self._make_request("star", {"albumId": str(self.artistid)}, error=70) + self._make_request("star", {"albumId": str(self.trackid)}, error=70) + self._make_request("star", {"albumId": str(self.albumid)}, skip_post=True) with db_session: - self.assertIn('starred', Album[self.albumid].as_subsonic_album(self.user)) - self._make_request('star', { 'albumId': str(self.albumid) }, error = 0) + self.assertIn("starred", Album[self.albumid].as_subsonic_album(self.user)) + self._make_request("star", {"albumId": str(self.albumid)}, error=0) - self._make_request('star', { 'artistId': str(self.folderid) }, error = 70) - self._make_request('star', { 'artistId': str(self.albumid) }, error = 70) - self._make_request('star', { 'artistId': str(self.trackid) }, error = 70) - self._make_request('star', { 'artistId': str(self.artistid) }, skip_post = True) + self._make_request("star", {"artistId": str(self.folderid)}, error=70) + self._make_request("star", {"artistId": str(self.albumid)}, error=70) + self._make_request("star", {"artistId": str(self.trackid)}, error=70) + self._make_request("star", {"artistId": str(self.artistid)}, skip_post=True) with db_session: - self.assertIn('starred', Artist[self.artistid].as_subsonic_artist(self.user)) - self._make_request('star', { 'artistId': str(self.artistid) }, error = 0) + self.assertIn( + "starred", Artist[self.artistid].as_subsonic_artist(self.user) + ) + self._make_request("star", {"artistId": str(self.artistid)}, error=0) def test_unstar(self): - self._make_request('star', { 'id': [ str(self.folderid), str(self.trackid) ], 'artistId': str(self.artistid), 'albumId': str(self.albumid) }, skip_post = True) + self._make_request( + "star", + { + "id": [str(self.folderid), str(self.trackid)], + "artistId": str(self.artistid), + "albumId": str(self.albumid), + }, + skip_post=True, + ) - self._make_request('unstar', error = 10) - self._make_request('unstar', { 'id': 'unknown' }, error = 0, skip_xsd = True) - self._make_request('unstar', { 'albumId': 'unknown' }, error = 0) - self._make_request('unstar', { 'artistId': 'unknown' }, error = 0) + self._make_request("unstar", error=10) + self._make_request("unstar", {"id": "unknown"}, error=0, skip_xsd=True) + self._make_request("unstar", {"albumId": "unknown"}, error=0) + self._make_request("unstar", {"artistId": "unknown"}, error=0) - self._make_request('unstar', { 'id': str(self.trackid) }, skip_post = True) + self._make_request("unstar", {"id": str(self.trackid)}, skip_post=True) with db_session: - prefs = ClientPrefs.get(lambda p: p.user.name == 'alice' and p.client_name == 'tests') - self.assertNotIn('starred', Track[self.trackid].as_subsonic_child(self.user, prefs)) + prefs = ClientPrefs.get( + lambda p: p.user.name == "alice" and p.client_name == "tests" + ) + self.assertNotIn( + "starred", Track[self.trackid].as_subsonic_child(self.user, prefs) + ) - self._make_request('unstar', { 'id': str(self.folderid) }, skip_post = True) + self._make_request("unstar", {"id": str(self.folderid)}, skip_post=True) with db_session: - self.assertNotIn('starred', Folder[self.folderid].as_subsonic_child(self.user)) + self.assertNotIn( + "starred", Folder[self.folderid].as_subsonic_child(self.user) + ) - self._make_request('unstar', { 'albumId': str(self.albumid) }, skip_post = True) + self._make_request("unstar", {"albumId": str(self.albumid)}, skip_post=True) with db_session: - self.assertNotIn('starred', Album[self.albumid].as_subsonic_album(self.user)) + self.assertNotIn( + "starred", Album[self.albumid].as_subsonic_album(self.user) + ) - self._make_request('unstar', { 'artistId': str(self.artistid) }, skip_post = True) + self._make_request("unstar", {"artistId": str(self.artistid)}, skip_post=True) with db_session: - self.assertNotIn('starred', Artist[self.artistid].as_subsonic_artist(self.user)) + self.assertNotIn( + "starred", Artist[self.artistid].as_subsonic_artist(self.user) + ) def test_set_rating(self): - self._make_request('setRating', error = 10) - self._make_request('setRating', { 'id': str(self.trackid) }, error = 10) - self._make_request('setRating', { 'rating': 3 }, error = 10) - self._make_request('setRating', { 'id': 'string', 'rating': 3 }, error = 0) - self._make_request('setRating', { 'id': str(uuid.uuid4()), 'rating': 3 }, error = 70) - self._make_request('setRating', { 'id': str(self.artistid), 'rating': 3 }, error = 70) - self._make_request('setRating', { 'id': str(self.albumid), 'rating': 3 }, error = 70) - self._make_request('setRating', { 'id': str(self.trackid), 'rating': 'string' }, error = 0) - self._make_request('setRating', { 'id': str(self.trackid), 'rating': -1 }, error = 0) - self._make_request('setRating', { 'id': str(self.trackid), 'rating': 6 }, error = 0) + self._make_request("setRating", error=10) + self._make_request("setRating", {"id": str(self.trackid)}, error=10) + self._make_request("setRating", {"rating": 3}, error=10) + self._make_request("setRating", {"id": "string", "rating": 3}, error=0) + self._make_request( + "setRating", {"id": str(uuid.uuid4()), "rating": 3}, error=70 + ) + self._make_request( + "setRating", {"id": str(self.artistid), "rating": 3}, error=70 + ) + self._make_request( + "setRating", {"id": str(self.albumid), "rating": 3}, error=70 + ) + self._make_request( + "setRating", {"id": str(self.trackid), "rating": "string"}, error=0 + ) + self._make_request( + "setRating", {"id": str(self.trackid), "rating": -1}, error=0 + ) + self._make_request("setRating", {"id": str(self.trackid), "rating": 6}, error=0) with db_session: - prefs = ClientPrefs.get(lambda p: p.user.name == 'alice' and p.client_name == 'tests') - self.assertNotIn('userRating', Track[self.trackid].as_subsonic_child(self.user, prefs)) + prefs = ClientPrefs.get( + lambda p: p.user.name == "alice" and p.client_name == "tests" + ) + self.assertNotIn( + "userRating", Track[self.trackid].as_subsonic_child(self.user, prefs) + ) for i in range(1, 6): - self._make_request('setRating', { 'id': str(self.trackid), 'rating': i }, skip_post = True) + self._make_request( + "setRating", {"id": str(self.trackid), "rating": i}, skip_post=True + ) with db_session: - prefs = ClientPrefs.get(lambda p: p.user.name == 'alice' and p.client_name == 'tests') - self.assertEqual(Track[self.trackid].as_subsonic_child(self.user, prefs)['userRating'], i) + prefs = ClientPrefs.get( + lambda p: p.user.name == "alice" and p.client_name == "tests" + ) + self.assertEqual( + Track[self.trackid].as_subsonic_child(self.user, prefs)[ + "userRating" + ], + i, + ) - self._make_request('setRating', { 'id': str(self.trackid), 'rating': 0 }, skip_post = True) + self._make_request( + "setRating", {"id": str(self.trackid), "rating": 0}, skip_post=True + ) with db_session: - prefs = ClientPrefs.get(lambda p: p.user.name == 'alice' and p.client_name == 'tests') - self.assertNotIn('userRating', Track[self.trackid].as_subsonic_child(self.user, prefs)) + prefs = ClientPrefs.get( + lambda p: p.user.name == "alice" and p.client_name == "tests" + ) + self.assertNotIn( + "userRating", Track[self.trackid].as_subsonic_child(self.user, prefs) + ) - self.assertNotIn('userRating', Folder[self.folderid].as_subsonic_child(self.user)) + self.assertNotIn( + "userRating", Folder[self.folderid].as_subsonic_child(self.user) + ) for i in range(1, 6): - self._make_request('setRating', { 'id': str(self.folderid), 'rating': i }, skip_post = True) + self._make_request( + "setRating", {"id": str(self.folderid), "rating": i}, skip_post=True + ) with db_session: - self.assertEqual(Folder[self.folderid].as_subsonic_child(self.user)['userRating'], i) - self._make_request('setRating', { 'id': str(self.folderid), 'rating': 0 }, skip_post = True) + self.assertEqual( + Folder[self.folderid].as_subsonic_child(self.user)["userRating"], i + ) + self._make_request( + "setRating", {"id": str(self.folderid), "rating": 0}, skip_post=True + ) with db_session: - self.assertNotIn('userRating', Folder[self.folderid].as_subsonic_child(self.user)) + self.assertNotIn( + "userRating", Folder[self.folderid].as_subsonic_child(self.user) + ) def test_scrobble(self): - self._make_request('scrobble', error = 10) - self._make_request('scrobble', { 'id': 'song' }, error = 0) - self._make_request('scrobble', { 'id': str(uuid.uuid4()) }, error = 70) - self._make_request('scrobble', { 'id': str(self.folderid) }, error = 70) + self._make_request("scrobble", error=10) + self._make_request("scrobble", {"id": "song"}, error=0) + self._make_request("scrobble", {"id": str(uuid.uuid4())}, error=70) + self._make_request("scrobble", {"id": str(self.folderid)}, error=70) - self._make_request('scrobble', { 'id': str(self.trackid) }) - self._make_request('scrobble', { 'id': str(self.trackid), 'submission': True }) - self._make_request('scrobble', { 'id': str(self.trackid), 'submission': False }) + self._make_request("scrobble", {"id": str(self.trackid)}) + self._make_request("scrobble", {"id": str(self.trackid), "submission": True}) + self._make_request("scrobble", {"id": str(self.trackid), "submission": False}) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/api/test_api_setup.py b/tests/api/test_api_setup.py index e328f32..55bc01a 100644 --- a/tests/api/test_api_setup.py +++ b/tests/api/test_api_setup.py @@ -17,6 +17,7 @@ from xml.etree import ElementTree from ..testbase import TestBase from ..utils import hexlify + class ApiSetupTestCase(TestBase): __with_api__ = True @@ -25,43 +26,49 @@ class ApiSetupTestCase(TestBase): self._patch_client() def __basic_auth_get(self, username, password): - hashed = base64.b64encode('{}:{}'.format(username, password).encode('utf-8')) - headers = { 'Authorization': 'Basic ' + hashed.decode('utf-8') } - return self.client.get('/rest/ping.view', headers = headers, query_string = { 'c': 'tests' }) + hashed = base64.b64encode("{}:{}".format(username, password).encode("utf-8")) + headers = {"Authorization": "Basic " + hashed.decode("utf-8")} + return self.client.get( + "/rest/ping.view", headers=headers, query_string={"c": "tests"} + ) def __query_params_auth_get(self, username, password): - return self.client.get('/rest/ping.view', query_string = { 'c': 'tests', 'u': username, 'p': password }) + return self.client.get( + "/rest/ping.view", query_string={"c": "tests", "u": username, "p": password} + ) def __query_params_auth_enc_get(self, username, password): - return self.__query_params_auth_get(username, 'enc:' + hexlify(password)) + return self.__query_params_auth_get(username, "enc:" + hexlify(password)) def __form_auth_post(self, username, password): - return self.client.post('/rest/ping.view', data = { 'c': 'tests', 'u': username, 'p': password }) + return self.client.post( + "/rest/ping.view", data={"c": "tests", "u": username, "p": password} + ) def __form_auth_enc_post(self, username, password): - return self.__form_auth_post(username, 'enc:' + hexlify(password)) + return self.__form_auth_post(username, "enc:" + hexlify(password)) def __test_auth(self, method): # non-existent user - rv = method('null', 'null') + rv = method("null", "null") self.assertEqual(rv.status_code, 401) self.assertIn('status="failed"', rv.data) self.assertIn('code="40"', rv.data) # user request with bad password - rv = method('alice', 'wrong password') + rv = method("alice", "wrong password") self.assertEqual(rv.status_code, 401) self.assertIn('status="failed"', rv.data) self.assertIn('code="40"', rv.data) # user request - rv = method('alice', 'Alic3') + rv = method("alice", "Alic3") self.assertEqual(rv.status_code, 200) self.assertIn('status="ok"', rv.data) def test_auth_basic(self): # No auth info - rv = self.client.get('/rest/ping.view?c=tests') + rv = self.client.get("/rest/ping.view?c=tests") self.assertEqual(rv.status_code, 400) self.assertIn('status="failed"', rv.data) self.assertIn('code="10"', rv.data) @@ -69,7 +76,7 @@ class ApiSetupTestCase(TestBase): self.__test_auth(self.__basic_auth_get) # Shouldn't accept 'enc:' passwords - rv = self.__basic_auth_get('alice', 'enc:' + hexlify('Alic3')) + rv = self.__basic_auth_get("alice", "enc:" + hexlify("Alic3")) self.assertEqual(rv.status_code, 401) self.assertIn('status="failed"', rv.data) self.assertIn('code="40"', rv.data) @@ -83,72 +90,85 @@ class ApiSetupTestCase(TestBase): self.__test_auth(self.__form_auth_enc_post) def test_required_client(self): - rv = self.client.get('/rest/ping.view', query_string = { 'u': 'alice', 'p': 'Alic3' }) + rv = self.client.get( + "/rest/ping.view", query_string={"u": "alice", "p": "Alic3"} + ) self.assertIn('status="failed"', rv.data) self.assertIn('code="10"', rv.data) - rv = self.client.get('/rest/ping.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) + rv = self.client.get( + "/rest/ping.view", query_string={"u": "alice", "p": "Alic3", "c": "tests"} + ) self.assertIn('status="ok"', rv.data) def test_format(self): - args = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' } - rv = self.client.get('/rest/getLicense.view', query_string = args) + args = {"u": "alice", "p": "Alic3", "c": "tests"} + rv = self.client.get("/rest/getLicense.view", query_string=args) self.assertEqual(rv.status_code, 200) - self.assertTrue(rv.mimetype.endswith('/xml')) # application/xml or text/xml + self.assertTrue(rv.mimetype.endswith("/xml")) # application/xml or text/xml self.assertIn('status="ok"', rv.data) xml = ElementTree.fromstring(rv.data) - self.assertIsNotNone(xml.find('./{http://subsonic.org/restapi}license')) + self.assertIsNotNone(xml.find("./{http://subsonic.org/restapi}license")) - args.update({ 'f': 'json' }) - rv = self.client.get('/rest/getLicense.view', query_string = args) + args.update({"f": "json"}) + rv = self.client.get("/rest/getLicense.view", query_string=args) self.assertEqual(rv.status_code, 200) - self.assertEqual(rv.mimetype, 'application/json') + self.assertEqual(rv.mimetype, "application/json") json = flask.json.loads(rv.data) - self.assertIn('subsonic-response', json) - self.assertEqual(json['subsonic-response']['status'], 'ok') - self.assertIn('license', json['subsonic-response']) + self.assertIn("subsonic-response", json) + self.assertEqual(json["subsonic-response"]["status"], "ok") + self.assertIn("license", json["subsonic-response"]) - args.update({ 'f': 'jsonp' }) - rv = self.client.get('/rest/getLicense.view', query_string = args) - self.assertEqual(rv.mimetype, 'application/json') + args.update({"f": "jsonp"}) + rv = self.client.get("/rest/getLicense.view", query_string=args) + self.assertEqual(rv.mimetype, "application/json") json = flask.json.loads(rv.data) - self.assertIn('subsonic-response', json) - self.assertEqual(json['subsonic-response']['status'], 'failed') - self.assertEqual(json['subsonic-response']['error']['code'], 10) + self.assertIn("subsonic-response", json) + self.assertEqual(json["subsonic-response"]["status"], "failed") + self.assertEqual(json["subsonic-response"]["error"]["code"], 10) - args.update({ 'callback': 'dummy_cb' }) - rv = self.client.get('/rest/getLicense.view', query_string = args) + args.update({"callback": "dummy_cb"}) + rv = self.client.get("/rest/getLicense.view", query_string=args) self.assertEqual(rv.status_code, 200) - self.assertEqual(rv.mimetype, 'application/javascript') - self.assertTrue(rv.data.startswith('dummy_cb({')) - self.assertTrue(rv.data.endswith('})')) + self.assertEqual(rv.mimetype, "application/javascript") + self.assertTrue(rv.data.startswith("dummy_cb({")) + self.assertTrue(rv.data.endswith("})")) json = flask.json.loads(rv.data[9:-1]) - self.assertIn('subsonic-response', json) - self.assertEqual(json['subsonic-response']['status'], 'ok') - self.assertIn('license', json['subsonic-response']) + self.assertIn("subsonic-response", json) + self.assertEqual(json["subsonic-response"]["status"], "ok") + self.assertIn("license", json["subsonic-response"]) def test_not_implemented(self): # Access to not implemented/unknown endpoint - rv = self.client.get('/rest/unknown', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) + rv = self.client.get( + "/rest/unknown", query_string={"u": "alice", "p": "Alic3", "c": "tests"} + ) self.assertEqual(rv.status_code, 404) self.assertIn('status="failed"', rv.data) self.assertIn('code="0"', rv.data) - rv = self.client.post('/rest/unknown', data = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) + rv = self.client.post( + "/rest/unknown", data={"u": "alice", "p": "Alic3", "c": "tests"} + ) self.assertEqual(rv.status_code, 404) self.assertIn('status="failed"', rv.data) self.assertIn('code="0"', rv.data) - rv = self.client.get('/rest/getVideos.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) + rv = self.client.get( + "/rest/getVideos.view", + query_string={"u": "alice", "p": "Alic3", "c": "tests"}, + ) self.assertEqual(rv.status_code, 501) self.assertIn('status="failed"', rv.data) self.assertIn('code="0"', rv.data) - rv = self.client.post('/rest/getVideos.view', data = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' }) + rv = self.client.post( + "/rest/getVideos.view", data={"u": "alice", "p": "Alic3", "c": "tests"} + ) self.assertEqual(rv.status_code, 501) self.assertIn('status="failed"', rv.data) self.assertIn('code="0"', rv.data) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/api/test_browse.py b/tests/api/test_browse.py index 990590f..6d40bd8 100644 --- a/tests/api/test_browse.py +++ b/tests/api/test_browse.py @@ -18,45 +18,48 @@ from supysonic.db import Folder, Artist, Album, Track from .apitestbase import ApiTestBase + class BrowseTestCase(ApiTestBase): def setUp(self): super(BrowseTestCase, self).setUp() with db_session: - Folder(root = True, name = 'Empty root', path = '/tmp') - root = Folder(root = True, name = 'Root folder', path = 'tests/assets') + Folder(root=True, name="Empty root", path="/tmp") + root = Folder(root=True, name="Root folder", path="tests/assets") - for letter in 'ABC': + for letter in "ABC": folder = Folder( - name = letter + 'rtist', - path = 'tests/assets/{}rtist'.format(letter), - parent = root + name=letter + "rtist", + path="tests/assets/{}rtist".format(letter), + parent=root, ) - artist = Artist(name = letter + 'rtist') + artist = Artist(name=letter + "rtist") - for lether in 'AB': + for lether in "AB": afolder = Folder( - name = letter + lether + 'lbum', - path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether), - parent = folder + name=letter + lether + "lbum", + path="tests/assets/{0}rtist/{0}{1}lbum".format(letter, lether), + parent=folder, ) - album = Album(name = letter + lether + 'lbum', artist = artist) + album = Album(name=letter + lether + "lbum", artist=artist) - for num, song in enumerate([ 'One', 'Two', 'Three' ]): + for num, song in enumerate(["One", "Two", "Three"]): track = Track( - disc = 1, - number = num, - title = song, - duration = 2, - album = album, - artist = artist, - bitrate = 320, - path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song), - last_modification = 0, - root_folder = root, - folder = afolder + disc=1, + number=num, + title=song, + duration=2, + album=album, + artist=artist, + bitrate=320, + path="tests/assets/{0}rtist/{0}{1}lbum/{2}".format( + letter, lether, song + ), + last_modification=0, + root_folder=root, + folder=afolder, ) self.assertEqual(Folder.select().count(), 11) @@ -68,107 +71,136 @@ class BrowseTestCase(ApiTestBase): def test_get_music_folders(self): # Do not validate against the XSD here, this is the only place where the API should return ids as ints # all our ids are uuids :/ - rv, child = self._make_request('getMusicFolders', tag = 'musicFolders', skip_xsd = True) + rv, child = self._make_request( + "getMusicFolders", tag="musicFolders", skip_xsd=True + ) self.assertEqual(len(child), 2) - self.assertSequenceEqual(sorted(self._xpath(child, './musicFolder/@name')), [ 'Empty root', 'Root folder' ]) + self.assertSequenceEqual( + sorted(self._xpath(child, "./musicFolder/@name")), + ["Empty root", "Root folder"], + ) def test_get_indexes(self): - self._make_request('getIndexes', { 'musicFolderId': 'abcdef' }, error = 0) - self._make_request('getIndexes', { 'musicFolderId': str(uuid.uuid4()) }, error = 70) - self._make_request('getIndexes', { 'ifModifiedSince': 'quoi' }, error = 0) + self._make_request("getIndexes", {"musicFolderId": "abcdef"}, error=0) + self._make_request("getIndexes", {"musicFolderId": str(uuid.uuid4())}, error=70) + self._make_request("getIndexes", {"ifModifiedSince": "quoi"}, error=0) - rv, child = self._make_request('getIndexes', { 'ifModifiedSince': int(time.time() * 1000 + 1000) }, tag = 'indexes') + rv, child = self._make_request( + "getIndexes", + {"ifModifiedSince": int(time.time() * 1000 + 1000)}, + tag="indexes", + ) self.assertEqual(len(child), 0) with db_session: - fid = Folder.get(name = 'Empty root').id - rv, child = self._make_request('getIndexes', { 'musicFolderId': str(fid) }, tag = 'indexes') + fid = Folder.get(name="Empty root").id + rv, child = self._make_request( + "getIndexes", {"musicFolderId": str(fid)}, tag="indexes" + ) self.assertEqual(len(child), 0) - rv, child = self._make_request('getIndexes', tag = 'indexes') + rv, child = self._make_request("getIndexes", tag="indexes") self.assertEqual(len(child), 3) - for i, letter in enumerate([ 'A', 'B', 'C' ]): - self.assertEqual(child[i].get('name'), letter) + for i, letter in enumerate(["A", "B", "C"]): + self.assertEqual(child[i].get("name"), letter) self.assertEqual(len(child[i]), 1) - self.assertEqual(child[i][0].get('name'), letter + 'rtist') + self.assertEqual(child[i][0].get("name"), letter + "rtist") def test_get_music_directory(self): - self._make_request('getMusicDirectory', error = 10) - self._make_request('getMusicDirectory', { 'id': 'id' }, error = 0) - self._make_request('getMusicDirectory', { 'id': str(uuid.uuid4()) }, error = 70) + self._make_request("getMusicDirectory", error=10) + self._make_request("getMusicDirectory", {"id": "id"}, error=0) + self._make_request("getMusicDirectory", {"id": str(uuid.uuid4())}, error=70) # should test with folders with both children folders and tracks. this code would break in that case with db_session: for f in Folder.select(): - rv, child = self._make_request('getMusicDirectory', { 'id': str(f.id) }, tag = 'directory') - self.assertEqual(child.get('id'), str(f.id)) - self.assertEqual(child.get('name'), f.name) + rv, child = self._make_request( + "getMusicDirectory", {"id": str(f.id)}, tag="directory" + ) + self.assertEqual(child.get("id"), str(f.id)) + self.assertEqual(child.get("name"), f.name) self.assertEqual(len(child), f.children.count() + f.tracks.count()) - for dbc, xmlc in zip(sorted(f.children, key = lambda c: c.name), sorted(child, key = lambda c: c.get('title'))): - self.assertEqual(dbc.name, xmlc.get('title')) - self.assertEqual(xmlc.get('artist'), f.name) - self.assertEqual(xmlc.get('parent'), str(f.id)) - for t, xmlc in zip(sorted(f.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))): - self.assertEqual(t.title, xmlc.get('title')) - self.assertEqual(xmlc.get('parent'), str(f.id)) + for dbc, xmlc in zip( + sorted(f.children, key=lambda c: c.name), + sorted(child, key=lambda c: c.get("title")), + ): + self.assertEqual(dbc.name, xmlc.get("title")) + self.assertEqual(xmlc.get("artist"), f.name) + self.assertEqual(xmlc.get("parent"), str(f.id)) + for t, xmlc in zip( + sorted(f.tracks, key=lambda t: t.title), + sorted(child, key=lambda c: c.get("title")), + ): + self.assertEqual(t.title, xmlc.get("title")) + self.assertEqual(xmlc.get("parent"), str(f.id)) def test_get_artists(self): # same as getIndexes standard case # dataset should be improved to have a different directory structure than /root/Artist/Album/Track - rv, child = self._make_request('getArtists', tag = 'artists') + rv, child = self._make_request("getArtists", tag="artists") self.assertEqual(len(child), 3) - for i, letter in enumerate([ 'A', 'B', 'C' ]): - self.assertEqual(child[i].get('name'), letter) + for i, letter in enumerate(["A", "B", "C"]): + self.assertEqual(child[i].get("name"), letter) self.assertEqual(len(child[i]), 1) - self.assertEqual(child[i][0].get('name'), letter + 'rtist') + self.assertEqual(child[i][0].get("name"), letter + "rtist") def test_get_artist(self): # dataset should be improved to have tracks by a different artist than the album's artist - self._make_request('getArtist', error = 10) - self._make_request('getArtist', { 'id': 'artist' }, error = 0) - self._make_request('getArtist', { 'id': str(uuid.uuid4()) }, error = 70) + self._make_request("getArtist", error=10) + self._make_request("getArtist", {"id": "artist"}, error=0) + self._make_request("getArtist", {"id": str(uuid.uuid4())}, error=70) with db_session: for ar in Artist.select(): - rv, child = self._make_request('getArtist', { 'id': str(ar.id) }, tag = 'artist') - self.assertEqual(child.get('id'), str(ar.id)) - self.assertEqual(child.get('albumCount'), str(len(child))) + rv, child = self._make_request( + "getArtist", {"id": str(ar.id)}, tag="artist" + ) + self.assertEqual(child.get("id"), str(ar.id)) + self.assertEqual(child.get("albumCount"), str(len(child))) self.assertEqual(len(child), ar.albums.count()) - for dal, xal in zip(sorted(ar.albums, key = lambda a: a.name), sorted(child, key = lambda c: c.get('name'))): - self.assertEqual(dal.name, xal.get('name')) - self.assertEqual(xal.get('artist'), ar.name) # could break with a better dataset - self.assertEqual(xal.get('artistId'), str(ar.id)) # see above + for dal, xal in zip( + sorted(ar.albums, key=lambda a: a.name), + sorted(child, key=lambda c: c.get("name")), + ): + self.assertEqual(dal.name, xal.get("name")) + self.assertEqual( + xal.get("artist"), ar.name + ) # could break with a better dataset + self.assertEqual(xal.get("artistId"), str(ar.id)) # see above def test_get_album(self): - self._make_request('getAlbum', error = 10) - self._make_request('getAlbum', { 'id': 'nastynasty' }, error = 0) - self._make_request('getAlbum', { 'id': str(uuid.uuid4()) }, error = 70) + self._make_request("getAlbum", error=10) + self._make_request("getAlbum", {"id": "nastynasty"}, error=0) + self._make_request("getAlbum", {"id": str(uuid.uuid4())}, error=70) with db_session: a = Album.select().first() - rv, child = self._make_request('getAlbum', { 'id': str(a.id) }, tag = 'album') - self.assertEqual(child.get('id'), str(a.id)) - self.assertEqual(child.get('songCount'), str(len(child))) + rv, child = self._make_request("getAlbum", {"id": str(a.id)}, tag="album") + self.assertEqual(child.get("id"), str(a.id)) + self.assertEqual(child.get("songCount"), str(len(child))) self.assertEqual(len(child), a.tracks.count()) - for dal, xal in zip(sorted(a.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))): - self.assertEqual(dal.title, xal.get('title')) - self.assertEqual(xal.get('album'), a.name) - self.assertEqual(xal.get('albumId'), str(a.id)) + for dal, xal in zip( + sorted(a.tracks, key=lambda t: t.title), + sorted(child, key=lambda c: c.get("title")), + ): + self.assertEqual(dal.title, xal.get("title")) + self.assertEqual(xal.get("album"), a.name) + self.assertEqual(xal.get("albumId"), str(a.id)) def test_get_song(self): - self._make_request('getSong', error = 10) - self._make_request('getSong', { 'id': 'nastynasty' }, error = 0) - self._make_request('getSong', { 'id': str(uuid.uuid4()) }, error = 70) + self._make_request("getSong", error=10) + self._make_request("getSong", {"id": "nastynasty"}, error=0) + self._make_request("getSong", {"id": str(uuid.uuid4())}, error=70) with db_session: s = Track.select().first() - self._make_request('getSong', { 'id': str(s.id) }, tag = 'song') + self._make_request("getSong", {"id": str(s.id)}, tag="song") def test_get_videos(self): - self._make_request('getVideos', error = 0) + self._make_request("getVideos", error=0) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/api/test_chat.py b/tests/api/test_chat.py index e05f6c0..60f158f 100644 --- a/tests/api/test_chat.py +++ b/tests/api/test_chat.py @@ -14,32 +14,41 @@ import time from .apitestbase import ApiTestBase + class ChatTestCase(ApiTestBase): def test_add_message(self): - self._make_request('addChatMessage', error = 10) - rv, child = self._make_request('getChatMessages', tag = 'chatMessages') + self._make_request("addChatMessage", error=10) + rv, child = self._make_request("getChatMessages", tag="chatMessages") self.assertEqual(len(child), 0) - self._make_request('addChatMessage', { 'message': 'Heres a message' }, skip_post = True) - rv, child = self._make_request('getChatMessages', tag = 'chatMessages') + self._make_request( + "addChatMessage", {"message": "Heres a message"}, skip_post=True + ) + rv, child = self._make_request("getChatMessages", tag="chatMessages") self.assertEqual(len(child), 1) - self.assertEqual(child[0].get('username'), 'alice') - self.assertEqual(child[0].get('message'), 'Heres a message') + self.assertEqual(child[0].get("username"), "alice") + self.assertEqual(child[0].get("message"), "Heres a message") def test_get_messages(self): - self._make_request('addChatMessage', { 'message': 'Hello' }, skip_post = True) + self._make_request("addChatMessage", {"message": "Hello"}, skip_post=True) time.sleep(1) - self._make_request('addChatMessage', { 'message': 'Is someone there?' }, skip_post = True) + self._make_request( + "addChatMessage", {"message": "Is someone there?"}, skip_post=True + ) - rv, child = self._make_request('getChatMessages', tag = 'chatMessages') + rv, child = self._make_request("getChatMessages", tag="chatMessages") self.assertEqual(len(child), 2) - rv, child = self._make_request('getChatMessages', { 'since': int(time.time()) * 1000 - 500 }, tag = 'chatMessages') + rv, child = self._make_request( + "getChatMessages", + {"since": int(time.time()) * 1000 - 500}, + tag="chatMessages", + ) self.assertEqual(len(child), 1) - self.assertEqual(child[0].get('message'), 'Is someone there?') + self.assertEqual(child[0].get("message"), "Is someone there?") - self._make_request('getChatMessages', { 'since': 'invalid timestamp' }, error = 0) + self._make_request("getChatMessages", {"since": "invalid timestamp"}, error=0) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/api/test_media.py b/tests/api/test_media.py index fec96d7..397fca0 100644 --- a/tests/api/test_media.py +++ b/tests/api/test_media.py @@ -21,167 +21,222 @@ from supysonic.db import Folder, Artist, Album, Track from .apitestbase import ApiTestBase + class MediaTestCase(ApiTestBase): def setUp(self): super(MediaTestCase, self).setUp() with db_session: folder = Folder( - name = 'Root', - path = os.path.abspath('tests/assets'), - root = True, - cover_art = 'cover.jpg' + name="Root", + path=os.path.abspath("tests/assets"), + root=True, + cover_art="cover.jpg", ) self.folderid = folder.id - artist = Artist(name = 'Artist') - album = Album(artist = artist, name = 'Album') + artist = Artist(name="Artist") + album = Album(artist=artist, name="Album") track = Track( - title = '23bytes', - number = 1, - disc = 1, - artist = artist, - album = album, - path = os.path.abspath('tests/assets/23bytes'), - root_folder = folder, - folder = folder, - duration = 2, - bitrate = 320, - last_modification = 0 + title="23bytes", + number=1, + disc=1, + artist=artist, + album=album, + path=os.path.abspath("tests/assets/23bytes"), + root_folder=folder, + folder=folder, + duration=2, + bitrate=320, + last_modification=0, ) self.trackid = track.id - self.formats = [('mp3','mpeg'), ('flac','flac'), ('ogg','ogg')] + self.formats = [("mp3", "mpeg"), ("flac", "flac"), ("ogg", "ogg")] for i in range(len(self.formats)): track_embeded_art = Track( - title = '[silence]', - number = 1, - disc = 1, - artist = artist, - album = album, - path = os.path.abspath('tests/assets/formats/silence.{0}'.format(self.formats[i][0])), - root_folder = folder, - folder = folder, - duration = 2, - bitrate = 320, - last_modification = 0 + title="[silence]", + number=1, + disc=1, + artist=artist, + album=album, + path=os.path.abspath( + "tests/assets/formats/silence.{0}".format(self.formats[i][0]) + ), + root_folder=folder, + folder=folder, + duration=2, + bitrate=320, + last_modification=0, ) self.formats[i] = track_embeded_art.id def test_stream(self): - self._make_request('stream', error = 10) - self._make_request('stream', { 'id': 'string' }, error = 0) - self._make_request('stream', { 'id': str(uuid.uuid4()) }, error = 70) - self._make_request('stream', { 'id': str(self.folderid) }, error = 70) - self._make_request('stream', { 'id': str(self.trackid), 'maxBitRate': 'string' }, error = 0) - self._make_request('stream', { 'id': str(self.trackid), 'timeOffset': 2 }, error = 0) - self._make_request('stream', { 'id': str(self.trackid), 'size': '640x480' }, error = 0) + self._make_request("stream", error=10) + self._make_request("stream", {"id": "string"}, error=0) + self._make_request("stream", {"id": str(uuid.uuid4())}, error=70) + self._make_request("stream", {"id": str(self.folderid)}, error=70) + self._make_request( + "stream", {"id": str(self.trackid), "maxBitRate": "string"}, error=0 + ) + self._make_request( + "stream", {"id": str(self.trackid), "timeOffset": 2}, error=0 + ) + self._make_request( + "stream", {"id": str(self.trackid), "size": "640x480"}, error=0 + ) - rv = self.client.get('/rest/stream.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.trackid) }) + rv = self.client.get( + "/rest/stream.view", + query_string={ + "u": "alice", + "p": "Alic3", + "c": "tests", + "id": str(self.trackid), + }, + ) self.assertEqual(rv.status_code, 200) self.assertEqual(len(rv.data), 23) with db_session: self.assertEqual(Track[self.trackid].play_count, 1) def test_download(self): - self._make_request('download', error = 10) - self._make_request('download', { 'id': 'string' }, error = 0) - self._make_request('download', { 'id': str(uuid.uuid4()) }, error = 70) + self._make_request("download", error=10) + self._make_request("download", {"id": "string"}, error=0) + self._make_request("download", {"id": str(uuid.uuid4())}, error=70) # download single file - rv = self.client.get('/rest/download.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.trackid) }) + rv = self.client.get( + "/rest/download.view", + query_string={ + "u": "alice", + "p": "Alic3", + "c": "tests", + "id": str(self.trackid), + }, + ) self.assertEqual(rv.status_code, 200) self.assertEqual(len(rv.data), 23) with db_session: self.assertEqual(Track[self.trackid].play_count, 0) # dowload folder - rv = self.client.get('/rest/download.view', query_string = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.folderid) }) + rv = self.client.get( + "/rest/download.view", + query_string={ + "u": "alice", + "p": "Alic3", + "c": "tests", + "id": str(self.folderid), + }, + ) self.assertEqual(rv.status_code, 200) - self.assertEqual(rv.mimetype, 'application/zip') + self.assertEqual(rv.mimetype, "application/zip") def test_get_cover_art(self): - self._make_request('getCoverArt', error = 10) - self._make_request('getCoverArt', { 'id': 'string' }, error = 0) - self._make_request('getCoverArt', { 'id': str(uuid.uuid4()) }, error = 70) - self._make_request('getCoverArt', { 'id': str(self.trackid) }, error = 70) - self._make_request('getCoverArt', { 'id': str(self.folderid), 'size': 'large' }, error = 0) + self._make_request("getCoverArt", error=10) + self._make_request("getCoverArt", {"id": "string"}, error=0) + self._make_request("getCoverArt", {"id": str(uuid.uuid4())}, error=70) + self._make_request("getCoverArt", {"id": str(self.trackid)}, error=70) + self._make_request( + "getCoverArt", {"id": str(self.folderid), "size": "large"}, error=0 + ) - args = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.folderid) } - rv = self.client.get('/rest/getCoverArt.view', query_string = args) + args = {"u": "alice", "p": "Alic3", "c": "tests", "id": str(self.folderid)} + rv = self.client.get("/rest/getCoverArt.view", query_string=args) self.assertEqual(rv.status_code, 200) - self.assertEqual(rv.mimetype, 'image/jpeg') + self.assertEqual(rv.mimetype, "image/jpeg") im = Image.open(BytesIO(rv.data)) - self.assertEqual(im.format, 'JPEG') + self.assertEqual(im.format, "JPEG") self.assertEqual(im.size, (420, 420)) - args['size'] = 600 - rv = self.client.get('/rest/getCoverArt.view', query_string = args) + args["size"] = 600 + rv = self.client.get("/rest/getCoverArt.view", query_string=args) self.assertEqual(rv.status_code, 200) - self.assertEqual(rv.mimetype, 'image/jpeg') + self.assertEqual(rv.mimetype, "image/jpeg") im = Image.open(BytesIO(rv.data)) - self.assertEqual(im.format, 'JPEG') + self.assertEqual(im.format, "JPEG") self.assertEqual(im.size, (420, 420)) - args['size'] = 120 - rv = self.client.get('/rest/getCoverArt.view', query_string = args) + args["size"] = 120 + rv = self.client.get("/rest/getCoverArt.view", query_string=args) self.assertEqual(rv.status_code, 200) - self.assertEqual(rv.mimetype, 'image/jpeg') + self.assertEqual(rv.mimetype, "image/jpeg") im = Image.open(BytesIO(rv.data)) - self.assertEqual(im.format, 'JPEG') + self.assertEqual(im.format, "JPEG") self.assertEqual(im.size, (120, 120)) # rerequest, just in case - rv = self.client.get('/rest/getCoverArt.view', query_string = args) + rv = self.client.get("/rest/getCoverArt.view", query_string=args) self.assertEqual(rv.status_code, 200) - self.assertEqual(rv.mimetype, 'image/jpeg') + self.assertEqual(rv.mimetype, "image/jpeg") im = Image.open(BytesIO(rv.data)) - self.assertEqual(im.format, 'JPEG') + self.assertEqual(im.format, "JPEG") self.assertEqual(im.size, (120, 120)) # TODO test non square covers # Test extracting cover art from embeded media - for args['id'] in self.formats: - rv = self.client.get('/rest/getCoverArt.view', query_string = args) + for args["id"] in self.formats: + rv = self.client.get("/rest/getCoverArt.view", query_string=args) self.assertEqual(rv.status_code, 200) - self.assertEqual(rv.mimetype, 'image/png') + self.assertEqual(rv.mimetype, "image/png") im = Image.open(BytesIO(rv.data)) - self.assertEqual(im.format, 'PNG') + self.assertEqual(im.format, "PNG") self.assertEqual(im.size, (120, 120)) def test_get_lyrics(self): - self._make_request('getLyrics', error = 10) - self._make_request('getLyrics', { 'artist': 'artist' }, error = 10) - self._make_request('getLyrics', { 'title': 'title' }, error = 10) + self._make_request("getLyrics", error=10) + self._make_request("getLyrics", {"artist": "artist"}, error=10) + self._make_request("getLyrics", {"title": "title"}, error=10) # Potentially skip the tests if ChartLyrics is down (which happens quite often) try: - requests.get('http://api.chartlyrics.com/', timeout = 5) + requests.get("http://api.chartlyrics.com/", timeout=5) except requests.exceptions.Timeout: - self.skipTest('ChartLyrics down') + self.skipTest("ChartLyrics down") - rv, child = self._make_request('getLyrics', { 'artist': 'some really long name hoping', 'title': 'to get absolutely no result' }, tag = 'lyrics') + rv, child = self._make_request( + "getLyrics", + { + "artist": "some really long name hoping", + "title": "to get absolutely no result", + }, + tag="lyrics", + ) self.assertIsNone(child.text) # ChartLyrics - rv, child = self._make_request('getLyrics', { 'artist': 'The Clash', 'title': 'London Calling' }, tag = 'lyrics') - self.assertIn('live by the river', child.text) + rv, child = self._make_request( + "getLyrics", + {"artist": "The Clash", "title": "London Calling"}, + tag="lyrics", + ) + self.assertIn("live by the river", child.text) # ChartLyrics, JSON format - args = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'f': 'json', 'artist': 'The Clash', 'title': 'London Calling' } - rv = self.client.get('/rest/getLyrics.view', query_string = args) + args = { + "u": "alice", + "p": "Alic3", + "c": "tests", + "f": "json", + "artist": "The Clash", + "title": "London Calling", + } + rv = self.client.get("/rest/getLyrics.view", query_string=args) json = flask.json.loads(rv.data) - self.assertIn('value', json['subsonic-response']['lyrics']) - self.assertIn('live by the river', json['subsonic-response']['lyrics']['value']) + self.assertIn("value", json["subsonic-response"]["lyrics"]) + self.assertIn("live by the river", json["subsonic-response"]["lyrics"]["value"]) # Local file - rv, child = self._make_request('getLyrics', { 'artist': 'artist', 'title': '23bytes' }, tag = 'lyrics') - self.assertIn('null', child.text) + rv, child = self._make_request( + "getLyrics", {"artist": "artist", "title": "23bytes"}, tag="lyrics" + ) + self.assertIn("null", child.text) def test_get_avatar(self): - self._make_request('getAvatar', error = 0) + self._make_request("getAvatar", error=0) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/api/test_playlist.py b/tests/api/test_playlist.py index 5e3e682..327d01d 100644 --- a/tests/api/test_playlist.py +++ b/tests/api/test_playlist.py @@ -16,146 +16,196 @@ from supysonic.db import Folder, Artist, Album, Track, Playlist, User from .apitestbase import ApiTestBase + class PlaylistTestCase(ApiTestBase): def setUp(self): super(PlaylistTestCase, self).setUp() with db_session: - root = Folder(root = True, name = 'Root folder', path = 'tests/assets') - artist = Artist(name = 'Artist') - album = Album(name = 'Album', artist = artist) + root = Folder(root=True, name="Root folder", path="tests/assets") + artist = Artist(name="Artist") + album = Album(name="Album", artist=artist) songs = {} - for num, song in enumerate([ 'One', 'Two', 'Three', 'Four' ]): + for num, song in enumerate(["One", "Two", "Three", "Four"]): track = Track( - disc = 1, - number = num, - title = song, - duration = 2, - album = album, - artist = artist, - bitrate = 320, - path = 'tests/assets/' + song, - last_modification = 0, - root_folder = root, - folder = root + disc=1, + number=num, + title=song, + duration=2, + album=album, + artist=artist, + bitrate=320, + path="tests/assets/" + song, + last_modification=0, + root_folder=root, + folder=root, ) songs[song] = track - users = { u.name: u for u in User.select() } + users = {u.name: u for u in User.select()} - playlist = Playlist(user = users['alice'], name = "Alice's") - playlist.add(songs['One']) - playlist.add(songs['Three']) + playlist = Playlist(user=users["alice"], name="Alice's") + playlist.add(songs["One"]) + playlist.add(songs["Three"]) - playlist = Playlist(user = users['alice'], public = True, name = "Alice's public") - playlist.add(songs['One']) - playlist.add(songs['Two']) + playlist = Playlist(user=users["alice"], public=True, name="Alice's public") + playlist.add(songs["One"]) + playlist.add(songs["Two"]) - playlist = Playlist(user = users['bob'], name = "Bob's") - playlist.add(songs['Two']) - playlist.add(songs['Four']) + playlist = Playlist(user=users["bob"], name="Bob's") + playlist.add(songs["Two"]) + playlist.add(songs["Four"]) def test_get_playlists(self): # get own playlists - rv, child = self._make_request('getPlaylists', tag = 'playlists') + rv, child = self._make_request("getPlaylists", tag="playlists") self.assertEqual(len(child), 2) - self.assertEqual(child[0].get('owner'), 'alice') - self.assertEqual(child[1].get('owner'), 'alice') + self.assertEqual(child[0].get("owner"), "alice") + self.assertEqual(child[1].get("owner"), "alice") # get own and public - rv, child = self._make_request('getPlaylists', { 'u': 'bob', 'p': 'B0b' }, tag = 'playlists') + rv, child = self._make_request( + "getPlaylists", {"u": "bob", "p": "B0b"}, tag="playlists" + ) self.assertEqual(len(child), 2) - self.assertTrue(child[0].get('owner') == 'alice' or child[1].get('owner') == 'alice') - self.assertTrue(child[0].get('owner') == 'bob' or child[1].get('owner') == 'bob') - self.assertIsNotNone(self._find(child, "./playlist[@owner='alice'][@public='true']")) + self.assertTrue( + child[0].get("owner") == "alice" or child[1].get("owner") == "alice" + ) + self.assertTrue( + child[0].get("owner") == "bob" or child[1].get("owner") == "bob" + ) + self.assertIsNotNone( + self._find(child, "./playlist[@owner='alice'][@public='true']") + ) # get other - rv, child = self._make_request('getPlaylists', { 'username': 'bob' }, tag = 'playlists') + rv, child = self._make_request( + "getPlaylists", {"username": "bob"}, tag="playlists" + ) self.assertEqual(len(child), 1) - self.assertEqual(child[0].get('owner'), 'bob') + self.assertEqual(child[0].get("owner"), "bob") # get other when not admin - self._make_request('getPlaylists', { 'u': 'bob', 'p': 'B0b', 'username': 'alice' }, error = 50) + self._make_request( + "getPlaylists", {"u": "bob", "p": "B0b", "username": "alice"}, error=50 + ) # get from unknown user - self._make_request('getPlaylists', { 'username': 'johndoe' }, error = 70) + self._make_request("getPlaylists", {"username": "johndoe"}, error=70) def test_get_playlist(self): # missing param - self._make_request('getPlaylist', error = 10) + self._make_request("getPlaylist", error=10) # invalid id - self._make_request('getPlaylist', { 'id': 1234 }, error = 0) + self._make_request("getPlaylist", {"id": 1234}, error=0) # unknown - self._make_request('getPlaylist', { 'id': str(uuid.uuid4()) }, error = 70) + self._make_request("getPlaylist", {"id": str(uuid.uuid4())}, error=70) # other's private from non admin with db_session: - playlist = Playlist.get(lambda p: not p.public and p.user.name == 'alice') - self._make_request('getPlaylist', { 'u': 'bob', 'p': 'B0b', 'id': str(playlist.id) }, error = 50) + playlist = Playlist.get(lambda p: not p.public and p.user.name == "alice") + self._make_request( + "getPlaylist", {"u": "bob", "p": "B0b", "id": str(playlist.id)}, error=50 + ) # standard - rv, child = self._make_request('getPlaylists', tag = 'playlists') - self._make_request('getPlaylist', { 'id': child[0].get('id') }, tag = 'playlist') - rv, child = self._make_request('getPlaylist', { 'id': child[1].get('id') }, tag = 'playlist') - self.assertEqual(child.get('songCount'), '2') - self.assertEqual(self._xpath(child, 'count(./entry)'), 2) # don't count children, there may be 'allowedUser's (even though not supported by supysonic) - self.assertEqual(child.get('duration'), '4') - self.assertEqual(child[0].get('title'), 'One') - self.assertTrue(child[1].get('title') == 'Two' or child[1].get('title') == 'Three') # depending on 'getPlaylists' result ordering + rv, child = self._make_request("getPlaylists", tag="playlists") + self._make_request("getPlaylist", {"id": child[0].get("id")}, tag="playlist") + rv, child = self._make_request( + "getPlaylist", {"id": child[1].get("id")}, tag="playlist" + ) + self.assertEqual(child.get("songCount"), "2") + self.assertEqual( + self._xpath(child, "count(./entry)"), 2 + ) # don't count children, there may be 'allowedUser's (even though not supported by supysonic) + self.assertEqual(child.get("duration"), "4") + self.assertEqual(child[0].get("title"), "One") + self.assertTrue( + child[1].get("title") == "Two" or child[1].get("title") == "Three" + ) # depending on 'getPlaylists' result ordering def test_create_playlist(self): - self._make_request('createPlaylist', error = 10) - self._make_request('createPlaylist', { 'name': 'wrongId', 'songId': 'abc' }, error = 0) - self._make_request('createPlaylist', { 'name': 'unknownId', 'songId': str(uuid.uuid4()) }, error = 70) - self._make_request('createPlaylist', { 'playlistId': 'abc' }, error = 0) - self._make_request('createPlaylist', { 'playlistId': str(uuid.uuid4()) }, error = 70) + self._make_request("createPlaylist", error=10) + self._make_request( + "createPlaylist", {"name": "wrongId", "songId": "abc"}, error=0 + ) + self._make_request( + "createPlaylist", + {"name": "unknownId", "songId": str(uuid.uuid4())}, + error=70, + ) + self._make_request("createPlaylist", {"playlistId": "abc"}, error=0) + self._make_request( + "createPlaylist", {"playlistId": str(uuid.uuid4())}, error=70 + ) # create - self._make_request('createPlaylist', { 'name': 'new playlist' }, skip_post = True) - rv, child = self._make_request('getPlaylists', tag = 'playlists') + self._make_request("createPlaylist", {"name": "new playlist"}, skip_post=True) + rv, child = self._make_request("getPlaylists", tag="playlists") self.assertEqual(len(child), 3) playlist = self._find(child, "./playlist[@name='new playlist']") self.assertEqual(len(playlist), 0) # "update" newly created - self._make_request('createPlaylist', { 'playlistId': playlist.get('id') }) - rv, child = self._make_request('getPlaylists', tag = 'playlists') + self._make_request("createPlaylist", {"playlistId": playlist.get("id")}) + rv, child = self._make_request("getPlaylists", tag="playlists") self.assertEqual(len(child), 3) # renaming - self._make_request('createPlaylist', { 'playlistId': playlist.get('id'), 'name': 'renamed' }) - rv, child = self._make_request('getPlaylists', tag = 'playlists') + self._make_request( + "createPlaylist", {"playlistId": playlist.get("id"), "name": "renamed"} + ) + rv, child = self._make_request("getPlaylists", tag="playlists") self.assertEqual(len(child), 3) self.assertIsNone(self._find(child, "./playlist[@name='new playlist']")) playlist = self._find(child, "./playlist[@name='renamed']") self.assertIsNotNone(playlist) # update from other user - self._make_request('createPlaylist', { 'u': 'bob', 'p': 'B0b', 'playlistId': playlist.get('id') }, error = 50) + self._make_request( + "createPlaylist", + {"u": "bob", "p": "B0b", "playlistId": playlist.get("id")}, + error=50, + ) # create more useful playlist with db_session: - songs = { s.title: str(s.id) for s in Track.select() } - self._make_request('createPlaylist', { 'name': 'songs', 'songId': list(map(lambda s: songs[s], [ 'Three', 'One', 'Two' ])) }, skip_post = True) + songs = {s.title: str(s.id) for s in Track.select()} + self._make_request( + "createPlaylist", + { + "name": "songs", + "songId": list(map(lambda s: songs[s], ["Three", "One", "Two"])), + }, + skip_post=True, + ) with db_session: - playlist = Playlist.get(name = 'songs') + playlist = Playlist.get(name="songs") self.assertIsNotNone(playlist) - rv, child = self._make_request('getPlaylist', { 'id': str(playlist.id) }, tag = 'playlist') - self.assertEqual(child.get('songCount'), '3') - self.assertEqual(self._xpath(child, 'count(./entry)'), 3) - self.assertEqual(child[0].get('title'), 'Three') - self.assertEqual(child[1].get('title'), 'One') - self.assertEqual(child[2].get('title'), 'Two') + rv, child = self._make_request( + "getPlaylist", {"id": str(playlist.id)}, tag="playlist" + ) + self.assertEqual(child.get("songCount"), "3") + self.assertEqual(self._xpath(child, "count(./entry)"), 3) + self.assertEqual(child[0].get("title"), "Three") + self.assertEqual(child[1].get("title"), "One") + self.assertEqual(child[2].get("title"), "Two") # update - self._make_request('createPlaylist', { 'playlistId': str(playlist.id), 'songId': songs['Two'] }, skip_post = True) - rv, child = self._make_request('getPlaylist', { 'id': str(playlist.id) }, tag = 'playlist') - self.assertEqual(child.get('songCount'), '1') - self.assertEqual(self._xpath(child, 'count(./entry)'), 1) - self.assertEqual(child[0].get('title'), 'Two') + self._make_request( + "createPlaylist", + {"playlistId": str(playlist.id), "songId": songs["Two"]}, + skip_post=True, + ) + rv, child = self._make_request( + "getPlaylist", {"id": str(playlist.id)}, tag="playlist" + ) + self.assertEqual(child.get("songCount"), "1") + self.assertEqual(self._xpath(child, "count(./entry)"), 1) + self.assertEqual(child[0].get("title"), "Two") @db_session def assertPlaylistCountEqual(self, count): @@ -163,79 +213,132 @@ class PlaylistTestCase(ApiTestBase): def test_delete_playlist(self): # check params - self._make_request('deletePlaylist', error = 10) - self._make_request('deletePlaylist', { 'id': 'string' }, error = 0) - self._make_request('deletePlaylist', { 'id': str(uuid.uuid4()) }, error = 70) + self._make_request("deletePlaylist", error=10) + self._make_request("deletePlaylist", {"id": "string"}, error=0) + self._make_request("deletePlaylist", {"id": str(uuid.uuid4())}, error=70) # delete unowned when not admin with db_session: - playlist = Playlist.select(lambda p: p.user.name == 'alice').first() - self._make_request('deletePlaylist', { 'u': 'bob', 'p': 'B0b', 'id': str(playlist.id) }, error = 50) - self.assertPlaylistCountEqual(3); + playlist = Playlist.select(lambda p: p.user.name == "alice").first() + self._make_request( + "deletePlaylist", {"u": "bob", "p": "B0b", "id": str(playlist.id)}, error=50 + ) + self.assertPlaylistCountEqual(3) # delete owned - self._make_request('deletePlaylist', { 'id': str(playlist.id) }, skip_post = True) - self.assertPlaylistCountEqual(2); - self._make_request('deletePlaylist', { 'id': str(playlist.id) }, error = 70) - self.assertPlaylistCountEqual(2); + self._make_request("deletePlaylist", {"id": str(playlist.id)}, skip_post=True) + self.assertPlaylistCountEqual(2) + self._make_request("deletePlaylist", {"id": str(playlist.id)}, error=70) + self.assertPlaylistCountEqual(2) # delete unowned when admin with db_session: - playlist = Playlist.get(lambda p: p.user.name == 'bob') - self._make_request('deletePlaylist', { 'id': str(playlist.id) }, skip_post = True) - self.assertPlaylistCountEqual(1); + playlist = Playlist.get(lambda p: p.user.name == "bob") + self._make_request("deletePlaylist", {"id": str(playlist.id)}, skip_post=True) + self.assertPlaylistCountEqual(1) def test_update_playlist(self): - self._make_request('updatePlaylist', error = 10) - self._make_request('updatePlaylist', { 'playlistId': 1234 }, error = 0) - self._make_request('updatePlaylist', { 'playlistId': str(uuid.uuid4()) }, error = 70) + self._make_request("updatePlaylist", error=10) + self._make_request("updatePlaylist", {"playlistId": 1234}, error=0) + self._make_request( + "updatePlaylist", {"playlistId": str(uuid.uuid4())}, error=70 + ) with db_session: - playlist = Playlist.select(lambda p: p.user.name == 'alice').order_by(Playlist.created).first() + playlist = ( + Playlist.select(lambda p: p.user.name == "alice") + .order_by(Playlist.created) + .first() + ) pid = str(playlist.id) - self._make_request('updatePlaylist', { 'playlistId': pid, 'songIdToAdd': 'string' }, error = 0) - self._make_request('updatePlaylist', { 'playlistId': pid, 'songIndexToRemove': 'string' }, error = 0) + self._make_request( + "updatePlaylist", {"playlistId": pid, "songIdToAdd": "string"}, error=0 + ) + self._make_request( + "updatePlaylist", + {"playlistId": pid, "songIndexToRemove": "string"}, + error=0, + ) name = str(playlist.name) - self._make_request('updatePlaylist', { 'u': 'bob', 'p': 'B0b', 'playlistId': pid, 'name': 'new name' }, error = 50) - rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') - self.assertEqual(child.get('name'), name) - self.assertEqual(self._xpath(child, 'count(./entry)'), 2) + self._make_request( + "updatePlaylist", + {"u": "bob", "p": "B0b", "playlistId": pid, "name": "new name"}, + error=50, + ) + rv, child = self._make_request("getPlaylist", {"id": pid}, tag="playlist") + self.assertEqual(child.get("name"), name) + self.assertEqual(self._xpath(child, "count(./entry)"), 2) - self._make_request('updatePlaylist', { 'playlistId': pid, 'name': 'new name' }, skip_post = True) - rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') - self.assertEqual(child.get('name'), 'new name') - self.assertEqual(self._xpath(child, 'count(./entry)'), 2) + self._make_request( + "updatePlaylist", {"playlistId": pid, "name": "new name"}, skip_post=True + ) + rv, child = self._make_request("getPlaylist", {"id": pid}, tag="playlist") + self.assertEqual(child.get("name"), "new name") + self.assertEqual(self._xpath(child, "count(./entry)"), 2) - self._make_request('updatePlaylist', { 'playlistId': pid, 'songIndexToRemove': [ -1, 2 ] }, skip_post = True) - rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') - self.assertEqual(self._xpath(child, 'count(./entry)'), 2) + self._make_request( + "updatePlaylist", + {"playlistId": pid, "songIndexToRemove": [-1, 2]}, + skip_post=True, + ) + rv, child = self._make_request("getPlaylist", {"id": pid}, tag="playlist") + self.assertEqual(self._xpath(child, "count(./entry)"), 2) - self._make_request('updatePlaylist', { 'playlistId': pid, 'songIndexToRemove': [ 0, 2 ] }, skip_post = True) - rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') - self.assertEqual(self._xpath(child, 'count(./entry)'), 1) - self.assertEqual(self._find(child, './entry').get('title'), 'Three') + self._make_request( + "updatePlaylist", + {"playlistId": pid, "songIndexToRemove": [0, 2]}, + skip_post=True, + ) + rv, child = self._make_request("getPlaylist", {"id": pid}, tag="playlist") + self.assertEqual(self._xpath(child, "count(./entry)"), 1) + self.assertEqual(self._find(child, "./entry").get("title"), "Three") with db_session: - songs = { s.title: str(s.id) for s in Track.select() } + songs = {s.title: str(s.id) for s in Track.select()} - self._make_request('updatePlaylist', { 'playlistId': pid, 'songIdToAdd': [ songs['One'], songs['Two'], songs['Two'] ] }, skip_post = True) - rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') - self.assertSequenceEqual(self._xpath(child, './entry/@title'), [ 'Three', 'One', 'Two', 'Two' ]) + self._make_request( + "updatePlaylist", + { + "playlistId": pid, + "songIdToAdd": [songs["One"], songs["Two"], songs["Two"]], + }, + skip_post=True, + ) + rv, child = self._make_request("getPlaylist", {"id": pid}, tag="playlist") + self.assertSequenceEqual( + self._xpath(child, "./entry/@title"), ["Three", "One", "Two", "Two"] + ) - self._make_request('updatePlaylist', { 'playlistId': pid, 'songIndexToRemove': [ 2, 1 ] }, skip_post = True) - rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') - self.assertSequenceEqual(self._xpath(child, './entry/@title'), [ 'Three', 'Two' ]) + self._make_request( + "updatePlaylist", + {"playlistId": pid, "songIndexToRemove": [2, 1]}, + skip_post=True, + ) + rv, child = self._make_request("getPlaylist", {"id": pid}, tag="playlist") + self.assertSequenceEqual(self._xpath(child, "./entry/@title"), ["Three", "Two"]) - self._make_request('updatePlaylist', { 'playlistId': pid, 'songIdToAdd': songs['One'] }, skip_post = True) - self._make_request('updatePlaylist', { 'playlistId': pid, 'songIndexToRemove': [ 1, 1 ] }, skip_post = True) - rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') - self.assertSequenceEqual(self._xpath(child, './entry/@title'), [ 'Three', 'One' ]) + self._make_request( + "updatePlaylist", + {"playlistId": pid, "songIdToAdd": songs["One"]}, + skip_post=True, + ) + self._make_request( + "updatePlaylist", + {"playlistId": pid, "songIndexToRemove": [1, 1]}, + skip_post=True, + ) + rv, child = self._make_request("getPlaylist", {"id": pid}, tag="playlist") + self.assertSequenceEqual(self._xpath(child, "./entry/@title"), ["Three", "One"]) - self._make_request('updatePlaylist', { 'playlistId': pid, 'songIdToAdd': str(uuid.uuid4()) }, error = 70) - rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') - self.assertEqual(self._xpath(child, 'count(./entry)'), 2) + self._make_request( + "updatePlaylist", + {"playlistId": pid, "songIdToAdd": str(uuid.uuid4())}, + error=70, + ) + rv, child = self._make_request("getPlaylist", {"id": pid}, tag="playlist") + self.assertEqual(self._xpath(child, "count(./entry)"), 2) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/api/test_response_helper.py b/tests/api/test_response_helper.py index 360eede..1200ead 100644 --- a/tests/api/test_response_helper.py +++ b/tests/api/test_response_helper.py @@ -18,207 +18,185 @@ from supysonic.py23 import strtype from ..testbase import TestBase + class UnwrapperMixin(object): def make_response(self, elem, data): with self.request_context(): rv = super(UnwrapperMixin, self).make_response(elem, data) - return rv.get_data(as_text = True) + return rv.get_data(as_text=True) @staticmethod def create_from(cls): class Unwrapper(UnwrapperMixin, cls): pass + return Unwrapper + class ResponseHelperJsonTestCase(TestBase, UnwrapperMixin.create_from(JSONFormatter)): def make_response(self, elem, data): rv = super(ResponseHelperJsonTestCase, self).make_response(elem, data) return flask.json.loads(rv) def process_and_extract(self, d): - return self.make_response('tag', d)['subsonic-response']['tag'] + return self.make_response("tag", d)["subsonic-response"]["tag"] def test_basic(self): empty = self.empty self.assertEqual(len(empty), 1) - self.assertIn('subsonic-response', empty) - self.assertIsInstance(empty['subsonic-response'], dict) + self.assertIn("subsonic-response", empty) + self.assertIsInstance(empty["subsonic-response"], dict) - resp = empty['subsonic-response'] + resp = empty["subsonic-response"] self.assertEqual(len(resp), 2) - self.assertIn('status', resp) - self.assertIn('version', resp) - self.assertEqual(resp['status'], 'ok') + self.assertIn("status", resp) + self.assertIn("version", resp) + self.assertEqual(resp["status"], "ok") - resp = self.error(0, 'message')['subsonic-response'] - self.assertEqual(resp['status'], 'failed') + resp = self.error(0, "message")["subsonic-response"] + self.assertEqual(resp["status"], "failed") - some_dict = { - 'intValue': 2, - 'someString': 'Hello world!' - } + some_dict = {"intValue": 2, "someString": "Hello world!"} resp = self.process_and_extract(some_dict) - self.assertIn('intValue', resp) - self.assertIn('someString', resp) + self.assertIn("intValue", resp) + self.assertIn("someString", resp) def test_lists(self): - resp = self.process_and_extract({ - 'someList': [ 2, 4, 8, 16 ], - 'emptyList': [] - }) - self.assertIn('someList', resp) - self.assertNotIn('emptyList', resp) - self.assertListEqual(resp['someList'], [ 2, 4, 8, 16 ]) + resp = self.process_and_extract({"someList": [2, 4, 8, 16], "emptyList": []}) + self.assertIn("someList", resp) + self.assertNotIn("emptyList", resp) + self.assertListEqual(resp["someList"], [2, 4, 8, 16]) def test_dicts(self): - resp = self.process_and_extract({ - 'dict': { 's': 'Blah', 'i': 20 }, - 'empty': {} - }) - self.assertIn('dict', resp) - self.assertIn('empty', resp) - self.assertDictEqual(resp['dict'], { 's': 'Blah', 'i': 20 }) - self.assertDictEqual(resp['empty'], {}) + resp = self.process_and_extract({"dict": {"s": "Blah", "i": 20}, "empty": {}}) + self.assertIn("dict", resp) + self.assertIn("empty", resp) + self.assertDictEqual(resp["dict"], {"s": "Blah", "i": 20}) + self.assertDictEqual(resp["empty"], {}) def test_nesting(self): - resp = self.process_and_extract({ - 'dict': { - 'value': 'hey look! a string', - 'list': [ 1, 2, 3 ], - 'emptyList': [], - 'subdict': { 'a': 'A' } - }, - 'list': [ - { 'b': 'B' }, - { 'c': 'C' }, - [ 4, 5, 6 ], - 'final string' - ] - }) + resp = self.process_and_extract( + { + "dict": { + "value": "hey look! a string", + "list": [1, 2, 3], + "emptyList": [], + "subdict": {"a": "A"}, + }, + "list": [{"b": "B"}, {"c": "C"}, [4, 5, 6], "final string"], + } + ) self.assertEqual(len(resp), 2) - self.assertIn('dict', resp) - self.assertIn('list', resp) + self.assertIn("dict", resp) + self.assertIn("list", resp) - d = resp['dict'] - l = resp['list'] + d = resp["dict"] + l = resp["list"] - self.assertIn('value', d) - self.assertIn('list', d) - self.assertNotIn('emptyList', d) - self.assertIn('subdict', d) - self.assertIsInstance(d['value'], strtype) - self.assertIsInstance(d['list'], list) - self.assertIsInstance(d['subdict'], dict) + self.assertIn("value", d) + self.assertIn("list", d) + self.assertNotIn("emptyList", d) + self.assertIn("subdict", d) + self.assertIsInstance(d["value"], strtype) + self.assertIsInstance(d["list"], list) + self.assertIsInstance(d["subdict"], dict) + + self.assertEqual(l, [{"b": "B"}, {"c": "C"}, [4, 5, 6], "final string"]) - self.assertEqual(l, [ - { 'b': 'B' }, - { 'c': 'C' }, - [ 4, 5, 6 ], - 'final string' - ]) class ResponseHelperJsonpTestCase(TestBase, UnwrapperMixin.create_from(JSONPFormatter)): def test_basic(self): - self._JSONPFormatter__callback = 'callback' # hacky + self._JSONPFormatter__callback = "callback" # hacky result = self.empty - self.assertTrue(result.startswith('callback({')) - self.assertTrue(result.endswith('})')) + self.assertTrue(result.startswith("callback({")) + self.assertTrue(result.endswith("})")) json = flask.json.loads(result[9:-1]) - self.assertIn('subsonic-response', json) + self.assertIn("subsonic-response", json) + class ResponseHelperXMLTestCase(TestBase, UnwrapperMixin.create_from(XMLFormatter)): def make_response(self, elem, data): xml = super(ResponseHelperXMLTestCase, self).make_response(elem, data) - xml = xml.replace('xmlns="http://subsonic.org/restapi"', '') + xml = xml.replace('xmlns="http://subsonic.org/restapi"', "") root = ElementTree.fromstring(xml) return root def process_and_extract(self, d): - rv = self.make_response('tag', d) - return rv.find('tag') + rv = self.make_response("tag", d) + return rv.find("tag") def assertAttributesMatchDict(self, elem, d): - d = { k: str(v) for k, v in d.items() } + d = {k: str(v) for k, v in d.items()} self.assertDictEqual(elem.attrib, d) def test_root(self): - xml = super(ResponseHelperXMLTestCase, self).make_response('tag', {}) - self.assertIn('')) + self.assertTrue(xml.strip().endswith("")) def test_basic(self): empty = self.empty - self.assertIsNotNone(empty.find('.[@version]')) + self.assertIsNotNone(empty.find(".[@version]")) self.assertIsNotNone(empty.find(".[@status='ok']")) - resp = self.error(0, 'message') + resp = self.error(0, "message") self.assertIsNotNone(resp.find(".[@status='failed']")) - some_dict = { - 'intValue': 2, - 'someString': 'Hello world!' - } + some_dict = {"intValue": 2, "someString": "Hello world!"} resp = self.process_and_extract(some_dict) - self.assertIsNotNone(resp.find('.[@intValue]')) - self.assertIsNotNone(resp.find('.[@someString]')) + self.assertIsNotNone(resp.find(".[@intValue]")) + self.assertIsNotNone(resp.find(".[@someString]")) def test_lists(self): - resp = self.process_and_extract({ - 'someList': [ 2, 4, 8, 16 ], - 'emptyList': [] - }) + resp = self.process_and_extract({"someList": [2, 4, 8, 16], "emptyList": []}) - elems = resp.findall('./someList') + elems = resp.findall("./someList") self.assertEqual(len(elems), 4) - self.assertIsNone(resp.find('./emptyList')) + self.assertIsNone(resp.find("./emptyList")) - for e, i in zip(elems, [ 2, 4, 8, 16 ]): + for e, i in zip(elems, [2, 4, 8, 16]): self.assertEqual(int(e.text), i) def test_dicts(self): - resp = self.process_and_extract({ - 'dict': { 's': 'Blah', 'i': 20 }, - 'empty': {} - }) + resp = self.process_and_extract({"dict": {"s": "Blah", "i": 20}, "empty": {}}) - d = resp.find('./dict') + d = resp.find("./dict") self.assertIsNotNone(d) - self.assertIsNotNone(resp.find('./empty')) - self.assertAttributesMatchDict(d, { 's': 'Blah', 'i': 20 }) + self.assertIsNotNone(resp.find("./empty")) + self.assertAttributesMatchDict(d, {"s": "Blah", "i": 20}) def test_nesting(self): - resp = self.process_and_extract({ - 'dict': { - 'somevalue': 'hey look! a string', - 'list': [ 1, 2, 3 ], - 'emptyList': [], - 'subdict': { 'a': 'A' } - }, - 'list': [ - { 'b': 'B' }, - { 'c': 'C' }, - 'final string' - ] - }) + resp = self.process_and_extract( + { + "dict": { + "somevalue": "hey look! a string", + "list": [1, 2, 3], + "emptyList": [], + "subdict": {"a": "A"}, + }, + "list": [{"b": "B"}, {"c": "C"}, "final string"], + } + ) - self.assertEqual(len(resp), 4) # 'dict' and 3 'list's + self.assertEqual(len(resp), 4) # 'dict' and 3 'list's - d = resp.find('./dict') - lists = resp.findall('./list') + d = resp.find("./dict") + lists = resp.findall("./list") self.assertIsNotNone(d) - self.assertAttributesMatchDict(d, { 'somevalue': 'hey look! a string' }) - self.assertEqual(len(d.findall('./list')), 3) - self.assertEqual(len(d.findall('./emptyList')), 0) - self.assertIsNotNone(d.find('./subdict')) + self.assertAttributesMatchDict(d, {"somevalue": "hey look! a string"}) + self.assertEqual(len(d.findall("./list")), 3) + self.assertEqual(len(d.findall("./emptyList")), 0) + self.assertIsNotNone(d.find("./subdict")) self.assertEqual(len(lists), 3) - self.assertAttributesMatchDict(lists[0], { 'b': 'B' }) - self.assertAttributesMatchDict(lists[1], { 'c': 'C' }) - self.assertEqual(lists[2].text, 'final string') + self.assertAttributesMatchDict(lists[0], {"b": "B"}) + self.assertAttributesMatchDict(lists[1], {"c": "C"}) + self.assertEqual(lists[2].text, "final string") + def suite(): suite = unittest.TestSuite() @@ -229,6 +207,6 @@ def suite(): return suite -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/tests/api/test_search.py b/tests/api/test_search.py index 1f6b55d..1c96ee0 100644 --- a/tests/api/test_search.py +++ b/tests/api/test_search.py @@ -17,39 +17,46 @@ from supysonic.db import Folder, Artist, Album, Track from .apitestbase import ApiTestBase + class SearchTestCase(ApiTestBase): def setUp(self): super(SearchTestCase, self).setUp() with db_session: - root = Folder(root = True, name = 'Root folder', path = 'tests/assets') + root = Folder(root=True, name="Root folder", path="tests/assets") - for letter in 'ABC': - folder = Folder(name = letter + 'rtist', path = 'tests/assets/{}rtist'.format(letter), parent = root) - artist = Artist(name = letter + 'rtist') + for letter in "ABC": + folder = Folder( + name=letter + "rtist", + path="tests/assets/{}rtist".format(letter), + parent=root, + ) + artist = Artist(name=letter + "rtist") - for lether in 'AB': + for lether in "AB": afolder = Folder( - name = letter + lether + 'lbum', - path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether), - parent = folder + name=letter + lether + "lbum", + path="tests/assets/{0}rtist/{0}{1}lbum".format(letter, lether), + parent=folder, ) - album = Album(name = letter + lether + 'lbum', artist = artist) + album = Album(name=letter + lether + "lbum", artist=artist) - for num, song in enumerate([ 'One', 'Two', 'Three' ]): + for num, song in enumerate(["One", "Two", "Three"]): track = Track( - disc = 1, - number = num, - title = song, - duration = 2, - album = album, - artist = artist, - bitrate = 320, - path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song), - last_modification = 0, - root_folder = root, - folder = afolder + disc=1, + number=num, + title=song, + duration=2, + album=album, + artist=artist, + bitrate=320, + path="tests/assets/{0}rtist/{0}{1}lbum/{2}".format( + letter, lether, song + ), + last_modification=0, + root_folder=root, + folder=afolder, ) commit() @@ -60,176 +67,212 @@ class SearchTestCase(ApiTestBase): self.assertEqual(Track.select().count(), 18) def __track_as_pseudo_unique_str(self, elem): - return elem.get('artist') + elem.get('album') + elem.get('title') + return elem.get("artist") + elem.get("album") + elem.get("title") def test_search(self): # invalid parameters - self._make_request('search', { 'count': 'string' }, error = 0) - self._make_request('search', { 'offset': 'sstring' }, error = 0) - self._make_request('search', { 'newerThan': 'ssstring' }, error = 0) + self._make_request("search", {"count": "string"}, error=0) + self._make_request("search", {"offset": "sstring"}, error=0) + self._make_request("search", {"newerThan": "ssstring"}, error=0) # no search - self._make_request('search', error = 10) + self._make_request("search", error=10) # non existent artist (but searched string present in other fields) - rv, child = self._make_request('search', { 'artist': 'One' }, tag = 'searchResult') + rv, child = self._make_request("search", {"artist": "One"}, tag="searchResult") self.assertEqual(len(child), 0) - self.assertEqual(child.get('totalHits'), '0') - self.assertEqual(child.get('offset'), '0') + self.assertEqual(child.get("totalHits"), "0") + self.assertEqual(child.get("offset"), "0") - rv, child = self._make_request('search', { 'artist': 'AAlbum' }, tag = 'searchResult') + rv, child = self._make_request( + "search", {"artist": "AAlbum"}, tag="searchResult" + ) self.assertEqual(len(child), 0) # non existent album (but search present in other fields) - rv, child = self._make_request('search', { 'album': 'One' }, tag = 'searchResult') + rv, child = self._make_request("search", {"album": "One"}, tag="searchResult") self.assertEqual(len(child), 0) - rv, child = self._make_request('search', { 'album': 'Artist' }, tag = 'searchResult') + rv, child = self._make_request( + "search", {"album": "Artist"}, tag="searchResult" + ) self.assertEqual(len(child), 0) # non existent title (but search present in other fields) - rv, child = self._make_request('search', { 'title': 'AAlbum' }, tag = 'searchResult') + rv, child = self._make_request( + "search", {"title": "AAlbum"}, tag="searchResult" + ) self.assertEqual(len(child), 0) - rv, child = self._make_request('search', { 'title': 'Artist' }, tag = 'searchResult') + rv, child = self._make_request( + "search", {"title": "Artist"}, tag="searchResult" + ) self.assertEqual(len(child), 0) # non existent anything - rv, child = self._make_request('search', { 'any': 'Chaos' }, tag = 'searchResult') + rv, child = self._make_request("search", {"any": "Chaos"}, tag="searchResult") self.assertEqual(len(child), 0) # artist search - rv, child = self._make_request('search', { 'artist': 'Artist' }, tag = 'searchResult') + rv, child = self._make_request( + "search", {"artist": "Artist"}, tag="searchResult" + ) self.assertEqual(len(child), 1) - self.assertEqual(child.get('totalHits'), '1') - self.assertEqual(child[0].get('title'), 'Artist') + self.assertEqual(child.get("totalHits"), "1") + self.assertEqual(child[0].get("title"), "Artist") - rv, child = self._make_request('search', { 'artist': 'rti' }, tag = 'searchResult') + rv, child = self._make_request("search", {"artist": "rti"}, tag="searchResult") self.assertEqual(len(child), 3) - self.assertEqual(child.get('totalHits'), '3') + self.assertEqual(child.get("totalHits"), "3") # same as above, but created in the future future = int(time.time() * 1000 + 1000) - rv, child = self._make_request('search', { 'artist': 'rti', 'newerThan': future }, tag = 'searchResult') + rv, child = self._make_request( + "search", {"artist": "rti", "newerThan": future}, tag="searchResult" + ) self.assertEqual(len(child), 0) # album search - rv, child = self._make_request('search', { 'album': 'AAlbum' }, tag = 'searchResult') + rv, child = self._make_request( + "search", {"album": "AAlbum"}, tag="searchResult" + ) self.assertEqual(len(child), 1) - self.assertEqual(child[0].get('title'), 'AAlbum') - self.assertEqual(child[0].get('artist'), 'Artist') + self.assertEqual(child[0].get("title"), "AAlbum") + self.assertEqual(child[0].get("artist"), "Artist") - rv, child = self._make_request('search', { 'album': 'lbu' }, tag = 'searchResult') + rv, child = self._make_request("search", {"album": "lbu"}, tag="searchResult") self.assertEqual(len(child), 6) # same as above, but created in the future - rv, child = self._make_request('search', { 'album': 'lbu', 'newerThan': future }, tag = 'searchResult') + rv, child = self._make_request( + "search", {"album": "lbu", "newerThan": future}, tag="searchResult" + ) self.assertEqual(len(child), 0) # song search - rv, child = self._make_request('search', { 'title': 'One' }, tag = 'searchResult') + rv, child = self._make_request("search", {"title": "One"}, tag="searchResult") self.assertEqual(len(child), 6) for i in range(6): - self.assertEqual(child[i].get('title'), 'One') + self.assertEqual(child[i].get("title"), "One") - rv, child = self._make_request('search', { 'title': 'e' }, tag = 'searchResult') + rv, child = self._make_request("search", {"title": "e"}, tag="searchResult") self.assertEqual(len(child), 12) # same as above, but created in the future - rv, child = self._make_request('search', { 'title': 'e', 'newerThan': future }, tag = 'searchResult') + rv, child = self._make_request( + "search", {"title": "e", "newerThan": future}, tag="searchResult" + ) self.assertEqual(len(child), 0) # any field search - rv, child = self._make_request('search', { 'any': 'r' }, tag = 'searchResult') - self.assertEqual(len(child), 10) # root + 3 artists (*rtist) + 6 songs (Three) + rv, child = self._make_request("search", {"any": "r"}, tag="searchResult") + self.assertEqual(len(child), 10) # root + 3 artists (*rtist) + 6 songs (Three) # same as above, but created in the future - rv, child = self._make_request('search', { 'any': 'r', 'newerThan': future }, tag = 'searchResult') + rv, child = self._make_request( + "search", {"any": "r", "newerThan": future}, tag="searchResult" + ) self.assertEqual(len(child), 0) # paging songs = [] for offset in range(0, 12, 2): - rv, child = self._make_request('search', { 'title': 'e', 'count': 2, 'offset': offset }, tag = 'searchResult') + rv, child = self._make_request( + "search", + {"title": "e", "count": 2, "offset": offset}, + tag="searchResult", + ) self.assertEqual(len(child), 2) - self.assertEqual(child.get('totalHits'), '12') - self.assertEqual(child.get('offset'), str(offset)) + self.assertEqual(child.get("totalHits"), "12") + self.assertEqual(child.get("offset"), str(offset)) for song in map(self.__track_as_pseudo_unique_str, child): self.assertNotIn(song, songs) songs.append(song) def test_search2(self): # invalid parameters - self._make_request('search2', { 'query': 'a', 'artistCount': 'string' }, error = 0) - self._make_request('search2', { 'query': 'a', 'artistOffset': 'sstring' }, error = 0) - self._make_request('search2', { 'query': 'a', 'albumCount': 'string' }, error = 0) - self._make_request('search2', { 'query': 'a', 'albumOffset': 'sstring' }, error = 0) - self._make_request('search2', { 'query': 'a', 'songCount': 'string' }, error = 0) - self._make_request('search2', { 'query': 'a', 'songOffset': 'sstring' }, error = 0) + self._make_request("search2", {"query": "a", "artistCount": "string"}, error=0) + self._make_request( + "search2", {"query": "a", "artistOffset": "sstring"}, error=0 + ) + self._make_request("search2", {"query": "a", "albumCount": "string"}, error=0) + self._make_request("search2", {"query": "a", "albumOffset": "sstring"}, error=0) + self._make_request("search2", {"query": "a", "songCount": "string"}, error=0) + self._make_request("search2", {"query": "a", "songOffset": "sstring"}, error=0) # no search - self._make_request('search2', error = 10) + self._make_request("search2", error=10) # non existent anything - rv, child = self._make_request('search2', { 'query': 'Chaos' }, tag = 'searchResult2') + rv, child = self._make_request( + "search2", {"query": "Chaos"}, tag="searchResult2" + ) self.assertEqual(len(child), 0) # artist search - rv, child = self._make_request('search2', { 'query': 'Artist' }, tag = 'searchResult2') + rv, child = self._make_request( + "search2", {"query": "Artist"}, tag="searchResult2" + ) self.assertEqual(len(child), 1) - self.assertEqual(len(self._xpath(child, './artist')), 1) - self.assertEqual(len(self._xpath(child, './album')), 0) - self.assertEqual(len(self._xpath(child, './song')), 0) - self.assertEqual(child[0].get('name'), 'Artist') + self.assertEqual(len(self._xpath(child, "./artist")), 1) + self.assertEqual(len(self._xpath(child, "./album")), 0) + self.assertEqual(len(self._xpath(child, "./song")), 0) + self.assertEqual(child[0].get("name"), "Artist") - rv, child = self._make_request('search2', { 'query': 'rti' }, tag = 'searchResult2') + rv, child = self._make_request("search2", {"query": "rti"}, tag="searchResult2") self.assertEqual(len(child), 3) - self.assertEqual(len(self._xpath(child, './artist')), 3) - self.assertEqual(len(self._xpath(child, './album')), 0) - self.assertEqual(len(self._xpath(child, './song')), 0) + self.assertEqual(len(self._xpath(child, "./artist")), 3) + self.assertEqual(len(self._xpath(child, "./album")), 0) + self.assertEqual(len(self._xpath(child, "./song")), 0) # album search - rv, child = self._make_request('search2', { 'query': 'AAlbum' }, tag = 'searchResult2') + rv, child = self._make_request( + "search2", {"query": "AAlbum"}, tag="searchResult2" + ) self.assertEqual(len(child), 1) - self.assertEqual(len(self._xpath(child, './artist')), 0) - self.assertEqual(len(self._xpath(child, './album')), 1) - self.assertEqual(len(self._xpath(child, './song')), 0) - self.assertEqual(child[0].get('title'), 'AAlbum') - self.assertEqual(child[0].get('artist'), 'Artist') + self.assertEqual(len(self._xpath(child, "./artist")), 0) + self.assertEqual(len(self._xpath(child, "./album")), 1) + self.assertEqual(len(self._xpath(child, "./song")), 0) + self.assertEqual(child[0].get("title"), "AAlbum") + self.assertEqual(child[0].get("artist"), "Artist") - rv, child = self._make_request('search2', { 'query': 'lbu' }, tag = 'searchResult2') + rv, child = self._make_request("search2", {"query": "lbu"}, tag="searchResult2") self.assertEqual(len(child), 6) - self.assertEqual(len(self._xpath(child, './artist')), 0) - self.assertEqual(len(self._xpath(child, './album')), 6) - self.assertEqual(len(self._xpath(child, './song')), 0) + self.assertEqual(len(self._xpath(child, "./artist")), 0) + self.assertEqual(len(self._xpath(child, "./album")), 6) + self.assertEqual(len(self._xpath(child, "./song")), 0) # song search - rv, child = self._make_request('search2', { 'query': 'One' }, tag = 'searchResult2') + rv, child = self._make_request("search2", {"query": "One"}, tag="searchResult2") self.assertEqual(len(child), 6) - self.assertEqual(len(self._xpath(child, './artist')), 0) - self.assertEqual(len(self._xpath(child, './album')), 0) - self.assertEqual(len(self._xpath(child, './song')), 6) + self.assertEqual(len(self._xpath(child, "./artist")), 0) + self.assertEqual(len(self._xpath(child, "./album")), 0) + self.assertEqual(len(self._xpath(child, "./song")), 6) for i in range(6): - self.assertEqual(child[i].get('title'), 'One') + self.assertEqual(child[i].get("title"), "One") - rv, child = self._make_request('search2', { 'query': 'e' }, tag = 'searchResult2') + rv, child = self._make_request("search2", {"query": "e"}, tag="searchResult2") self.assertEqual(len(child), 12) - self.assertEqual(len(self._xpath(child, './artist')), 0) - self.assertEqual(len(self._xpath(child, './album')), 0) - self.assertEqual(len(self._xpath(child, './song')), 12) + self.assertEqual(len(self._xpath(child, "./artist")), 0) + self.assertEqual(len(self._xpath(child, "./album")), 0) + self.assertEqual(len(self._xpath(child, "./song")), 12) # any field search - rv, child = self._make_request('search2', { 'query': 'r' }, tag = 'searchResult2') + rv, child = self._make_request("search2", {"query": "r"}, tag="searchResult2") self.assertEqual(len(child), 9) - self.assertEqual(len(self._xpath(child, './artist')), 3) - self.assertEqual(len(self._xpath(child, './album')), 0) - self.assertEqual(len(self._xpath(child, './song')), 6) + self.assertEqual(len(self._xpath(child, "./artist")), 3) + self.assertEqual(len(self._xpath(child, "./album")), 0) + self.assertEqual(len(self._xpath(child, "./song")), 6) # paging artists = [] for offset in range(0, 4, 2): - rv, child = self._make_request('search2', { 'query': 'r', 'artistCount': 2, 'artistOffset': offset }, tag = 'searchResult2') - names = self._xpath(child, './artist/@name') + rv, child = self._make_request( + "search2", + {"query": "r", "artistCount": 2, "artistOffset": offset}, + tag="searchResult2", + ) + names = self._xpath(child, "./artist/@name") self.assertLessEqual(len(names), 2) for name in names: self.assertNotIn(name, artists) @@ -237,8 +280,12 @@ class SearchTestCase(ApiTestBase): songs = [] for offset in range(0, 6, 2): - rv, child = self._make_request('search2', { 'query': 'r', 'songCount': 2, 'songOffset': offset }, tag = 'searchResult2') - elems = self._xpath(child, './song') + rv, child = self._make_request( + "search2", + {"query": "r", "songCount": 2, "songOffset": offset}, + tag="searchResult2", + ) + elems = self._xpath(child, "./song") self.assertEqual(len(elems), 2) for song in map(self.__track_as_pseudo_unique_str, elems): self.assertNotIn(song, songs) @@ -248,76 +295,88 @@ class SearchTestCase(ApiTestBase): # to have folders that don't share names with artists or albums def test_search3(self): # invalid parameters - self._make_request('search3', { 'query': 'a', 'artistCount': 'string' }, error = 0) - self._make_request('search3', { 'query': 'a', 'artistOffset': 'sstring' }, error = 0) - self._make_request('search3', { 'query': 'a', 'albumCount': 'string' }, error = 0) - self._make_request('search3', { 'query': 'a', 'albumOffset': 'sstring' }, error = 0) - self._make_request('search3', { 'query': 'a', 'songCount': 'string' }, error = 0) - self._make_request('search3', { 'query': 'a', 'songOffset': 'sstring' }, error = 0) + self._make_request("search3", {"query": "a", "artistCount": "string"}, error=0) + self._make_request( + "search3", {"query": "a", "artistOffset": "sstring"}, error=0 + ) + self._make_request("search3", {"query": "a", "albumCount": "string"}, error=0) + self._make_request("search3", {"query": "a", "albumOffset": "sstring"}, error=0) + self._make_request("search3", {"query": "a", "songCount": "string"}, error=0) + self._make_request("search3", {"query": "a", "songOffset": "sstring"}, error=0) # no search - self._make_request('search3', error = 10) + self._make_request("search3", error=10) # non existent anything - rv, child = self._make_request('search3', { 'query': 'Chaos' }, tag = 'searchResult3') + rv, child = self._make_request( + "search3", {"query": "Chaos"}, tag="searchResult3" + ) self.assertEqual(len(child), 0) # artist search - rv, child = self._make_request('search3', { 'query': 'Artist' }, tag = 'searchResult3') + rv, child = self._make_request( + "search3", {"query": "Artist"}, tag="searchResult3" + ) self.assertEqual(len(child), 1) - self.assertEqual(len(self._xpath(child, './artist')), 1) - self.assertEqual(len(self._xpath(child, './album')), 0) - self.assertEqual(len(self._xpath(child, './song')), 0) - self.assertEqual(child[0].get('name'), 'Artist') + self.assertEqual(len(self._xpath(child, "./artist")), 1) + self.assertEqual(len(self._xpath(child, "./album")), 0) + self.assertEqual(len(self._xpath(child, "./song")), 0) + self.assertEqual(child[0].get("name"), "Artist") - rv, child = self._make_request('search3', { 'query': 'rti' }, tag = 'searchResult3') + rv, child = self._make_request("search3", {"query": "rti"}, tag="searchResult3") self.assertEqual(len(child), 3) - self.assertEqual(len(self._xpath(child, './artist')), 3) - self.assertEqual(len(self._xpath(child, './album')), 0) - self.assertEqual(len(self._xpath(child, './song')), 0) + self.assertEqual(len(self._xpath(child, "./artist")), 3) + self.assertEqual(len(self._xpath(child, "./album")), 0) + self.assertEqual(len(self._xpath(child, "./song")), 0) # album search - rv, child = self._make_request('search3', { 'query': 'AAlbum' }, tag = 'searchResult3') + rv, child = self._make_request( + "search3", {"query": "AAlbum"}, tag="searchResult3" + ) self.assertEqual(len(child), 1) - self.assertEqual(len(self._xpath(child, './artist')), 0) - self.assertEqual(len(self._xpath(child, './album')), 1) - self.assertEqual(len(self._xpath(child, './song')), 0) - self.assertEqual(child[0].get('name'), 'AAlbum') - self.assertEqual(child[0].get('artist'), 'Artist') + self.assertEqual(len(self._xpath(child, "./artist")), 0) + self.assertEqual(len(self._xpath(child, "./album")), 1) + self.assertEqual(len(self._xpath(child, "./song")), 0) + self.assertEqual(child[0].get("name"), "AAlbum") + self.assertEqual(child[0].get("artist"), "Artist") - rv, child = self._make_request('search3', { 'query': 'lbu' }, tag = 'searchResult3') + rv, child = self._make_request("search3", {"query": "lbu"}, tag="searchResult3") self.assertEqual(len(child), 6) - self.assertEqual(len(self._xpath(child, './artist')), 0) - self.assertEqual(len(self._xpath(child, './album')), 6) - self.assertEqual(len(self._xpath(child, './song')), 0) + self.assertEqual(len(self._xpath(child, "./artist")), 0) + self.assertEqual(len(self._xpath(child, "./album")), 6) + self.assertEqual(len(self._xpath(child, "./song")), 0) # song search - rv, child = self._make_request('search3', { 'query': 'One' }, tag = 'searchResult3') + rv, child = self._make_request("search3", {"query": "One"}, tag="searchResult3") self.assertEqual(len(child), 6) - self.assertEqual(len(self._xpath(child, './artist')), 0) - self.assertEqual(len(self._xpath(child, './album')), 0) - self.assertEqual(len(self._xpath(child, './song')), 6) + self.assertEqual(len(self._xpath(child, "./artist")), 0) + self.assertEqual(len(self._xpath(child, "./album")), 0) + self.assertEqual(len(self._xpath(child, "./song")), 6) for i in range(6): - self.assertEqual(child[i].get('title'), 'One') + self.assertEqual(child[i].get("title"), "One") - rv, child = self._make_request('search3', { 'query': 'e' }, tag = 'searchResult3') + rv, child = self._make_request("search3", {"query": "e"}, tag="searchResult3") self.assertEqual(len(child), 12) - self.assertEqual(len(self._xpath(child, './artist')), 0) - self.assertEqual(len(self._xpath(child, './album')), 0) - self.assertEqual(len(self._xpath(child, './song')), 12) + self.assertEqual(len(self._xpath(child, "./artist")), 0) + self.assertEqual(len(self._xpath(child, "./album")), 0) + self.assertEqual(len(self._xpath(child, "./song")), 12) # any field search - rv, child = self._make_request('search3', { 'query': 'r' }, tag = 'searchResult3') + rv, child = self._make_request("search3", {"query": "r"}, tag="searchResult3") self.assertEqual(len(child), 9) - self.assertEqual(len(self._xpath(child, './artist')), 3) - self.assertEqual(len(self._xpath(child, './album')), 0) - self.assertEqual(len(self._xpath(child, './song')), 6) + self.assertEqual(len(self._xpath(child, "./artist")), 3) + self.assertEqual(len(self._xpath(child, "./album")), 0) + self.assertEqual(len(self._xpath(child, "./song")), 6) # paging artists = [] for offset in range(0, 4, 2): - rv, child = self._make_request('search3', { 'query': 'r', 'artistCount': 2, 'artistOffset': offset }, tag = 'searchResult3') - names = self._xpath(child, './artist/@name') + rv, child = self._make_request( + "search3", + {"query": "r", "artistCount": 2, "artistOffset": offset}, + tag="searchResult3", + ) + names = self._xpath(child, "./artist/@name") self.assertLessEqual(len(names), 2) for name in names: self.assertNotIn(name, artists) @@ -325,13 +384,17 @@ class SearchTestCase(ApiTestBase): songs = [] for offset in range(0, 6, 2): - rv, child = self._make_request('search3', { 'query': 'r', 'songCount': 2, 'songOffset': offset }, tag = 'searchResult3') - elems = self._xpath(child, './song') + rv, child = self._make_request( + "search3", + {"query": "r", "songCount": 2, "songOffset": offset}, + tag="searchResult3", + ) + elems = self._xpath(child, "./song") self.assertEqual(len(elems), 2) for song in map(self.__track_as_pseudo_unique_str, elems): self.assertNotIn(song, songs) songs.append(song) -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/tests/api/test_system.py b/tests/api/test_system.py index d56a0de..bc9dfeb 100644 --- a/tests/api/test_system.py +++ b/tests/api/test_system.py @@ -11,14 +11,15 @@ from .apitestbase import ApiTestBase + class SystemTestCase(ApiTestBase): def test_ping(self): - self._make_request('ping') + self._make_request("ping") def test_get_license(self): - rv, child = self._make_request('getLicense', tag = 'license') - self.assertEqual(child.get('valid'), 'true') + rv, child = self._make_request("getLicense", tag="license") + self.assertEqual(child.get("valid"), "true") -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/api/test_transcoding.py b/tests/api/test_transcoding.py index 0608070..afd8d2c 100644 --- a/tests/api/test_transcoding.py +++ b/tests/api/test_transcoding.py @@ -18,43 +18,46 @@ from supysonic.scanner import Scanner from .apitestbase import ApiTestBase + class TranscodingTestCase(ApiTestBase): def setUp(self): super(TranscodingTestCase, self).setUp() self._patch_client() with db_session: - folder = FolderManager.add('Folder', 'tests/assets/folder') + folder = FolderManager.add("Folder", "tests/assets/folder") scanner = Scanner() - scanner.queue_folder('Folder') + scanner.queue_folder("Folder") scanner.run() self.trackid = Track.get().id def _stream(self, **kwargs): - kwargs.update({ 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'v': '1.9.0', 'id': self.trackid }) + kwargs.update( + {"u": "alice", "p": "Alic3", "c": "tests", "v": "1.9.0", "id": self.trackid} + ) - rv = self.client.get('/rest/stream.view', query_string = kwargs) + rv = self.client.get("/rest/stream.view", query_string=kwargs) self.assertEqual(rv.status_code, 200) - self.assertFalse(rv.mimetype.startswith('text/')) + self.assertFalse(rv.mimetype.startswith("text/")) return rv def test_no_transcoding_available(self): - self._make_request('stream', { 'id': self.trackid, 'format': 'wat' }, error = 0) + self._make_request("stream", {"id": self.trackid, "format": "wat"}, error=0) def test_direct_transcode(self): - rv = self._stream(maxBitRate = 96, estimateContentLength = 'true') - self.assertIn('tests/assets/folder/silence.mp3', rv.data) - self.assertTrue(rv.data.endswith('96')) + rv = self._stream(maxBitRate=96, estimateContentLength="true") + self.assertIn("tests/assets/folder/silence.mp3", rv.data) + self.assertTrue(rv.data.endswith("96")) def test_decode_encode(self): - rv = self._stream(format = 'cat') - self.assertEqual(rv.data, 'Pushing out some mp3 data...') + rv = self._stream(format="cat") + self.assertEqual(rv.data, "Pushing out some mp3 data...") - rv = self._stream(format = 'md5') - self.assertTrue(rv.data.startswith('dbb16c0847e5d8c3b1867604828cb50b')) + rv = self._stream(format="md5") + self.assertTrue(rv.data.startswith("dbb16c0847e5d8c3b1867604828cb50b")) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/api/test_user.py b/tests/api/test_user.py index 82dd85a..5e76727 100644 --- a/tests/api/test_user.py +++ b/tests/api/test_user.py @@ -12,140 +12,217 @@ from ..utils import hexlify from .apitestbase import ApiTestBase + class UserTestCase(ApiTestBase): def test_get_user(self): # missing username - self._make_request('getUser', error = 10) + self._make_request("getUser", error=10) # non-existent user - self._make_request('getUser', { 'username': 'non existent' }, error = 70) + self._make_request("getUser", {"username": "non existent"}, error=70) # self - rv, child = self._make_request('getUser', { 'username': 'alice' }, tag = 'user') - self.assertEqual(child.get('username'), 'alice') - self.assertEqual(child.get('adminRole'), 'true') + rv, child = self._make_request("getUser", {"username": "alice"}, tag="user") + self.assertEqual(child.get("username"), "alice") + self.assertEqual(child.get("adminRole"), "true") # other - rv, child = self._make_request('getUser', { 'username': 'bob' }, tag = 'user') - self.assertEqual(child.get('username'), 'bob') - self.assertEqual(child.get('adminRole'), 'false') + rv, child = self._make_request("getUser", {"username": "bob"}, tag="user") + self.assertEqual(child.get("username"), "bob") + self.assertEqual(child.get("adminRole"), "false") # self from non-admin - rv, child = self._make_request('getUser', { 'u': 'bob', 'p': 'B0b', 'username': 'bob' }, tag = 'user') - self.assertEqual(child.get('username'), 'bob') - self.assertEqual(child.get('adminRole'), 'false') + rv, child = self._make_request( + "getUser", {"u": "bob", "p": "B0b", "username": "bob"}, tag="user" + ) + self.assertEqual(child.get("username"), "bob") + self.assertEqual(child.get("adminRole"), "false") # other from non-admin - self._make_request('getUser', { 'u': 'bob', 'p': 'B0b', 'username': 'alice' }, error = 50) + self._make_request( + "getUser", {"u": "bob", "p": "B0b", "username": "alice"}, error=50 + ) def test_get_users(self): # non-admin - self._make_request('getUsers', { 'u': 'bob', 'p': 'B0b' }, error = 50) + self._make_request("getUsers", {"u": "bob", "p": "B0b"}, error=50) # admin - rv, child = self._make_request('getUsers', tag = 'users') + rv, child = self._make_request("getUsers", tag="users") self.assertEqual(len(child), 2) self.assertIsNotNone(self._find(child, "./user[@username='alice']")) self.assertIsNotNone(self._find(child, "./user[@username='bob']")) def test_create_user(self): # non admin - self._make_request('createUser', { 'u': 'bob', 'p': 'B0b' }, error = 50) + self._make_request("createUser", {"u": "bob", "p": "B0b"}, error=50) # missing params, testing every combination, maybe overkill - self._make_request('createUser', error = 10) - self._make_request('createUser', { 'username': 'user' }, error = 10) - self._make_request('createUser', { 'password': 'pass' }, error = 10) - self._make_request('createUser', { 'email': 'email@example.com' }, error = 10) - self._make_request('createUser', { 'username': 'user', 'password': 'pass' }, error = 10) - self._make_request('createUser', { 'username': 'user', 'email': 'email@example.com' }, error = 10) - self._make_request('createUser', { 'password': 'pass', 'email': 'email@example.com' }, error = 10) + self._make_request("createUser", error=10) + self._make_request("createUser", {"username": "user"}, error=10) + self._make_request("createUser", {"password": "pass"}, error=10) + self._make_request("createUser", {"email": "email@example.com"}, error=10) + self._make_request( + "createUser", {"username": "user", "password": "pass"}, error=10 + ) + self._make_request( + "createUser", {"username": "user", "email": "email@example.com"}, error=10 + ) + self._make_request( + "createUser", {"password": "pass", "email": "email@example.com"}, error=10 + ) # duplicate - self._make_request('createUser', { 'username': 'bob', 'password': 'pass', 'email': 'me@bob.com' }, error = 0) + self._make_request( + "createUser", + {"username": "bob", "password": "pass", "email": "me@bob.com"}, + error=0, + ) # test we only got our two initial users - rv, child = self._make_request('getUsers', tag = 'users') + rv, child = self._make_request("getUsers", tag="users") self.assertEqual(len(child), 2) # create users - self._make_request('createUser', { 'username': 'charlie', 'password': 'Ch4rl1e', 'email': 'unicorn@example.com', 'adminRole': True }, skip_post = True) - rv, child = self._make_request('getUser', { 'username': 'charlie' }, tag = 'user') - self.assertEqual(child.get('username'), 'charlie') - self.assertEqual(child.get('email'), 'unicorn@example.com') - self.assertEqual(child.get('adminRole'), 'true') + self._make_request( + "createUser", + { + "username": "charlie", + "password": "Ch4rl1e", + "email": "unicorn@example.com", + "adminRole": True, + }, + skip_post=True, + ) + rv, child = self._make_request("getUser", {"username": "charlie"}, tag="user") + self.assertEqual(child.get("username"), "charlie") + self.assertEqual(child.get("email"), "unicorn@example.com") + self.assertEqual(child.get("adminRole"), "true") - self._make_request('createUser', { 'username': 'dave', 'password': 'Dav3', 'email': 'dave@example.com' }, skip_post = True) - rv, child = self._make_request('getUser', { 'username': 'dave' }, tag = 'user') - self.assertEqual(child.get('username'), 'dave') - self.assertEqual(child.get('email'), 'dave@example.com') - self.assertEqual(child.get('adminRole'), 'false') + self._make_request( + "createUser", + {"username": "dave", "password": "Dav3", "email": "dave@example.com"}, + skip_post=True, + ) + rv, child = self._make_request("getUser", {"username": "dave"}, tag="user") + self.assertEqual(child.get("username"), "dave") + self.assertEqual(child.get("email"), "dave@example.com") + self.assertEqual(child.get("adminRole"), "false") - rv, child = self._make_request('getUsers', tag = 'users') + rv, child = self._make_request("getUsers", tag="users") self.assertEqual(len(child), 4) def test_delete_user(self): # non admin - self._make_request('deleteUser', { 'u': 'bob', 'p': 'B0b', 'username': 'alice' }, error = 50) + self._make_request( + "deleteUser", {"u": "bob", "p": "B0b", "username": "alice"}, error=50 + ) # missing param - self._make_request('deleteUser', error = 10) + self._make_request("deleteUser", error=10) # non existing - self._make_request('deleteUser', { 'username': 'charlie' }, error = 70) + self._make_request("deleteUser", {"username": "charlie"}, error=70) # test we still got our two initial users - rv, child = self._make_request('getUsers', tag = 'users') + rv, child = self._make_request("getUsers", tag="users") self.assertEqual(len(child), 2) # delete user - self._make_request('deleteUser', { 'username': 'bob' }, skip_post = True) - rv, child = self._make_request('getUsers', tag = 'users') + self._make_request("deleteUser", {"username": "bob"}, skip_post=True) + rv, child = self._make_request("getUsers", tag="users") self.assertEqual(len(child), 1) def test_change_password(self): # missing parameter - self._make_request('changePassword', error = 10) - self._make_request('changePassword', { 'username': 'alice' }, error = 10) - self._make_request('changePassword', { 'password': 'newpass' }, error = 10) + self._make_request("changePassword", error=10) + self._make_request("changePassword", {"username": "alice"}, error=10) + self._make_request("changePassword", {"password": "newpass"}, error=10) # admin change self - self._make_request('changePassword', { 'username': 'alice', 'password': 'newpass' }, skip_post = True) - self._make_request('ping', error = 40) - self._make_request('ping', { 'u': 'alice', 'p': 'newpass' }) - self._make_request('changePassword', { 'u': 'alice', 'p': 'newpass', 'username': 'alice', 'password': 'Alic3' }, skip_post = True) + self._make_request( + "changePassword", + {"username": "alice", "password": "newpass"}, + skip_post=True, + ) + self._make_request("ping", error=40) + self._make_request("ping", {"u": "alice", "p": "newpass"}) + self._make_request( + "changePassword", + {"u": "alice", "p": "newpass", "username": "alice", "password": "Alic3"}, + skip_post=True, + ) # admin change other - self._make_request('changePassword', { 'username': 'bob', 'password': 'newbob' }, skip_post = True) - self._make_request('ping', { 'u': 'bob', 'p': 'B0b' }, error = 40) - self._make_request('ping', { 'u': 'bob', 'p': 'newbob' }) + self._make_request( + "changePassword", {"username": "bob", "password": "newbob"}, skip_post=True + ) + self._make_request("ping", {"u": "bob", "p": "B0b"}, error=40) + self._make_request("ping", {"u": "bob", "p": "newbob"}) # non-admin change self - self._make_request('changePassword', { 'u': 'bob', 'p': 'newbob', 'username': 'bob', 'password': 'B0b' }, skip_post = True) - self._make_request('ping', { 'u': 'bob', 'p': 'newbob' }, error = 40) - self._make_request('ping', { 'u': 'bob', 'p': 'B0b' }) + self._make_request( + "changePassword", + {"u": "bob", "p": "newbob", "username": "bob", "password": "B0b"}, + skip_post=True, + ) + self._make_request("ping", {"u": "bob", "p": "newbob"}, error=40) + self._make_request("ping", {"u": "bob", "p": "B0b"}) # non-admin change other - self._make_request('changePassword', { 'u': 'bob', 'p': 'B0b', 'username': 'alice', 'password': 'newpass' }, skip_post = True, error = 50) - self._make_request('ping', { 'u': 'alice', 'p': 'newpass' }, error = 40) - self._make_request('ping') + self._make_request( + "changePassword", + {"u": "bob", "p": "B0b", "username": "alice", "password": "newpass"}, + skip_post=True, + error=50, + ) + self._make_request("ping", {"u": "alice", "p": "newpass"}, error=40) + self._make_request("ping") # change non existing - self._make_request('changePassword', { 'username': 'nonexsistent', 'password': 'pass' }, error = 70) + self._make_request( + "changePassword", {"username": "nonexsistent", "password": "pass"}, error=70 + ) # non ASCII chars - self._make_request('changePassword', { 'username': 'alice', 'password': 'новыйпароль' }, skip_post = True) - self._make_request('ping', { 'u': 'alice', 'p': 'новыйпароль' }) - self._make_request('changePassword', { 'username': 'alice', 'password': 'Alic3', 'u': 'alice', 'p': 'новыйпароль' }, skip_post = True) + self._make_request( + "changePassword", + {"username": "alice", "password": "новыйпароль"}, + skip_post=True, + ) + self._make_request("ping", {"u": "alice", "p": "новыйпароль"}) + self._make_request( + "changePassword", + { + "username": "alice", + "password": "Alic3", + "u": "alice", + "p": "новыйпароль", + }, + skip_post=True, + ) # non ASCII in hex encoded password - self._make_request('changePassword', { 'username': 'alice', 'password': 'enc:' + hexlify(u'новыйпароль') }, skip_post = True) - self._make_request('ping', { 'u': 'alice', 'p': 'новыйпароль' }) + self._make_request( + "changePassword", + {"username": "alice", "password": "enc:" + hexlify(u"новыйпароль")}, + skip_post=True, + ) + self._make_request("ping", {"u": "alice", "p": "новыйпароль"}) # new password starting with 'enc:' followed by non hex chars - self._make_request('changePassword', { 'username': 'alice', 'password': 'enc:randomstring', 'u': 'alice', 'p': 'новыйпароль' }, skip_post = True) - self._make_request('ping', { 'u': 'alice', 'p': 'enc:randomstring' }) + self._make_request( + "changePassword", + { + "username": "alice", + "password": "enc:randomstring", + "u": "alice", + "p": "новыйпароль", + }, + skip_post=True, + ) + self._make_request("ping", {"u": "alice", "p": "enc:randomstring"}) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/base/__init__.py b/tests/base/__init__.py index 4343e7b..c1f3612 100644 --- a/tests/base/__init__.py +++ b/tests/base/__init__.py @@ -18,6 +18,7 @@ from .test_scanner import ScannerTestCase from .test_secret import SecretTestCase from .test_watcher import suite as watcher_suite + def suite(): suite = unittest.TestSuite() @@ -31,4 +32,3 @@ def suite(): suite.addTest(unittest.makeSuite(SecretTestCase)) return suite - diff --git a/tests/base/test_cache.py b/tests/base/test_cache.py index 4ae1b49..13fe2b8 100644 --- a/tests/base/test_cache.py +++ b/tests/base/test_cache.py @@ -27,7 +27,7 @@ class CacheTestCase(unittest.TestCase): def test_existing_files_order(self): cache = Cache(self.__dir, 30) - val = b'0123456789' + val = b"0123456789" cache.set("key1", val) cache.set("key2", val) cache.set("key3", val) @@ -63,7 +63,7 @@ class CacheTestCase(unittest.TestCase): def test_store_literal(self): cache = Cache(self.__dir, 10) - val = b'0123456789' + val = b"0123456789" cache.set("key", val) self.assertEqual(cache.size, 10) self.assertTrue(cache.has("key")) @@ -71,7 +71,8 @@ class CacheTestCase(unittest.TestCase): def test_store_generated(self): cache = Cache(self.__dir, 10) - val = [b'0', b'12', b'345', b'6789'] + val = [b"0", b"12", b"345", b"6789"] + def gen(): for b in val: yield b @@ -84,11 +85,11 @@ class CacheTestCase(unittest.TestCase): self.assertEqual(t, val) self.assertEqual(cache.size, 10) - self.assertEqual(cache.get_value("key"), b''.join(val)) + self.assertEqual(cache.get_value("key"), b"".join(val)) def test_store_to_fp(self): cache = Cache(self.__dir, 10) - val = b'0123456789' + val = b"0123456789" with cache.set_fileobj("key") as fp: fp.write(val) self.assertEqual(cache.size, 0) @@ -98,7 +99,7 @@ class CacheTestCase(unittest.TestCase): def test_access_data(self): cache = Cache(self.__dir, 25, min_time=0) - val = b'0123456789' + val = b"0123456789" cache.set("key", val) self.assertEqual(cache.get_value("key"), val) @@ -106,13 +107,12 @@ class CacheTestCase(unittest.TestCase): with cache.get_fileobj("key") as f: self.assertEqual(f.read(), val) - with open(cache.get("key"), 'rb') as f: + with open(cache.get("key"), "rb") as f: self.assertEqual(f.read(), val) - def test_accessing_preserves(self): cache = Cache(self.__dir, 25, min_time=0) - val = b'0123456789' + val = b"0123456789" cache.set("key1", val) cache.set("key2", val) self.assertEqual(cache.size, 20) @@ -127,7 +127,7 @@ class CacheTestCase(unittest.TestCase): def test_automatic_delete_oldest(self): cache = Cache(self.__dir, 25, min_time=0) - val = b'0123456789' + val = b"0123456789" cache.set("key1", val) self.assertTrue(cache.has("key1")) self.assertEqual(cache.size, 10) @@ -145,7 +145,7 @@ class CacheTestCase(unittest.TestCase): def test_delete(self): cache = Cache(self.__dir, 25, min_time=0) - val = b'0123456789' + val = b"0123456789" cache.set("key1", val) self.assertTrue(cache.has("key1")) self.assertEqual(cache.size, 10) @@ -157,9 +157,10 @@ class CacheTestCase(unittest.TestCase): def test_cleanup_on_error(self): cache = Cache(self.__dir, 10) + def gen(): # Cause a TypeError halfway through - for b in [b'0', b'12', object(), b'345', b'6789']: + for b in [b"0", b"12", object(), b"345", b"6789"]: yield b with self.assertRaises(TypeError): @@ -171,8 +172,9 @@ class CacheTestCase(unittest.TestCase): def test_parallel_generation(self): cache = Cache(self.__dir, 20) + def gen(): - for b in [b'0', b'12', b'345', b'6789']: + for b in [b"0", b"12", b"345", b"6789"]: yield b g1 = cache.set_generated("key", gen) @@ -207,8 +209,8 @@ class CacheTestCase(unittest.TestCase): def test_replace(self): cache = Cache(self.__dir, 20) - val_small = b'0' - val_big = b'0123456789' + val_small = b"0" + val_big = b"0123456789" cache.set("key", val_small) self.assertEqual(cache.size, 1) @@ -221,7 +223,7 @@ class CacheTestCase(unittest.TestCase): def test_no_auto_prune(self): cache = Cache(self.__dir, 10, min_time=0, auto_prune=False) - val = b'0123456789' + val = b"0123456789" cache.set("key1", val) cache.set("key2", val) @@ -234,7 +236,7 @@ class CacheTestCase(unittest.TestCase): def test_min_time_clear(self): cache = Cache(self.__dir, 40, min_time=1) - val = b'0123456789' + val = b"0123456789" cache.set("key1", val) cache.set("key2", val) @@ -251,7 +253,7 @@ class CacheTestCase(unittest.TestCase): def test_not_expired(self): cache = Cache(self.__dir, 40, min_time=1) - val = b'0123456789' + val = b"0123456789" cache.set("key1", val) with self.assertRaises(ProtectedError): cache.delete("key1") @@ -261,7 +263,7 @@ class CacheTestCase(unittest.TestCase): def test_missing_cache_file(self): cache = Cache(self.__dir, 10, min_time=0) - val = b'0123456789' + val = b"0123456789" os.remove(cache.set("key", val)) self.assertEqual(cache.size, 10) @@ -275,5 +277,5 @@ class CacheTestCase(unittest.TestCase): self.assertEqual(cache.size, 0) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/base/test_cli.py b/tests/base/test_cli.py index ab2f4f7..f0174d4 100644 --- a/tests/base/test_cli.py +++ b/tests/base/test_cli.py @@ -17,7 +17,7 @@ import unittest from contextlib import contextmanager from pony.orm import db_session -try: # Don't use io.StringIO on py2, it only accepts unicode and the CLI spits strs +try: # Don't use io.StringIO on py2, it only accepts unicode and the CLI spits strs from StringIO import StringIO except ImportError: from io import StringIO @@ -27,18 +27,19 @@ from supysonic.cli import SupysonicCLI from ..testbase import TestConfig + class CLITestCase(unittest.TestCase): """ Really basic tests. Some even don't check anything but are just there for coverage """ def setUp(self): conf = TestConfig(False, False) self.__dbfile = tempfile.mkstemp()[1] - conf.BASE['database_uri'] = 'sqlite:///' + self.__dbfile - init_database(conf.BASE['database_uri']) + conf.BASE["database_uri"] = "sqlite:///" + self.__dbfile + init_database(conf.BASE["database_uri"]) self.__stdout = StringIO() self.__stderr = StringIO() - self.__cli = SupysonicCLI(conf, stdout = self.__stdout, stderr = self.__stderr) + self.__cli = SupysonicCLI(conf, stdout=self.__stdout, stderr=self.__stderr) def tearDown(self): self.__stdout.close() @@ -56,7 +57,7 @@ class CLITestCase(unittest.TestCase): def test_folder_add(self): with self._tempdir() as d: - self.__cli.onecmd('folder add tmpfolder ' + d) + self.__cli.onecmd("folder add tmpfolder " + d) with db_session: f = Folder.select().first() @@ -65,76 +66,76 @@ class CLITestCase(unittest.TestCase): def test_folder_add_errors(self): with self._tempdir() as d: - self.__cli.onecmd('folder add f1 ' + d) - self.__cli.onecmd('folder add f2 ' + d) + self.__cli.onecmd("folder add f1 " + d) + self.__cli.onecmd("folder add f2 " + d) with self._tempdir() as d: - self.__cli.onecmd('folder add f1 ' + d) - self.__cli.onecmd('folder add f3 /invalid/path') + self.__cli.onecmd("folder add f1 " + d) + self.__cli.onecmd("folder add f3 /invalid/path") with db_session: self.assertEqual(Folder.select().count(), 1) def test_folder_delete(self): with self._tempdir() as d: - self.__cli.onecmd('folder add tmpfolder ' + d) - self.__cli.onecmd('folder delete randomfolder') - self.__cli.onecmd('folder delete tmpfolder') + self.__cli.onecmd("folder add tmpfolder " + d) + self.__cli.onecmd("folder delete randomfolder") + self.__cli.onecmd("folder delete tmpfolder") with db_session: self.assertEqual(Folder.select().count(), 0) def test_folder_list(self): with self._tempdir() as d: - self.__cli.onecmd('folder add tmpfolder ' + d) - self.__cli.onecmd('folder list') - self.assertIn('tmpfolder', self.__stdout.getvalue()) + self.__cli.onecmd("folder add tmpfolder " + d) + self.__cli.onecmd("folder list") + self.assertIn("tmpfolder", self.__stdout.getvalue()) self.assertIn(d, self.__stdout.getvalue()) def test_folder_scan(self): with self._tempdir() as d: - self.__cli.onecmd('folder add tmpfolder ' + d) - with tempfile.NamedTemporaryFile(dir = d): - self.__cli.onecmd('folder scan') - self.__cli.onecmd('folder scan tmpfolder nonexistent') + self.__cli.onecmd("folder add tmpfolder " + d) + with tempfile.NamedTemporaryFile(dir=d): + self.__cli.onecmd("folder scan") + self.__cli.onecmd("folder scan tmpfolder nonexistent") def test_user_add(self): - self.__cli.onecmd('user add -p Alic3 alice') - self.__cli.onecmd('user add -p alice alice') + self.__cli.onecmd("user add -p Alic3 alice") + self.__cli.onecmd("user add -p alice alice") with db_session: self.assertEqual(User.select().count(), 1) def test_user_delete(self): - self.__cli.onecmd('user add -p Alic3 alice') - self.__cli.onecmd('user delete alice') - self.__cli.onecmd('user delete bob') + self.__cli.onecmd("user add -p Alic3 alice") + self.__cli.onecmd("user delete alice") + self.__cli.onecmd("user delete bob") with db_session: self.assertEqual(User.select().count(), 0) def test_user_list(self): - self.__cli.onecmd('user add -p Alic3 alice') - self.__cli.onecmd('user list') - self.assertIn('alice', self.__stdout.getvalue()) + self.__cli.onecmd("user add -p Alic3 alice") + self.__cli.onecmd("user list") + self.assertIn("alice", self.__stdout.getvalue()) def test_user_setadmin(self): - self.__cli.onecmd('user add -p Alic3 alice') - self.__cli.onecmd('user setadmin alice') - self.__cli.onecmd('user setadmin bob') + self.__cli.onecmd("user add -p Alic3 alice") + self.__cli.onecmd("user setadmin alice") + self.__cli.onecmd("user setadmin bob") with db_session: - self.assertTrue(User.get(name = 'alice').admin) + self.assertTrue(User.get(name="alice").admin) def test_user_changepass(self): - self.__cli.onecmd('user add -p Alic3 alice') - self.__cli.onecmd('user changepass alice newpass') - self.__cli.onecmd('user changepass bob B0b') + self.__cli.onecmd("user add -p Alic3 alice") + self.__cli.onecmd("user changepass alice newpass") + self.__cli.onecmd("user changepass bob B0b") def test_other(self): - self.assertTrue(self.__cli.do_EOF('')) - self.__cli.onecmd('unknown command') + self.assertTrue(self.__cli.do_EOF("")) + self.__cli.onecmd("unknown command") self.__cli.postloop() - self.__cli.completedefault('user', 'user', 4, 4) + self.__cli.completedefault("user", "user", 4, 4) + if __name__ == "__main__": unittest.main() - diff --git a/tests/base/test_config.py b/tests/base/test_config.py index eff37cd..5d2a060 100644 --- a/tests/base/test_config.py +++ b/tests/base/test_config.py @@ -13,32 +13,33 @@ import unittest from supysonic.config import IniConfig from supysonic.py23 import strtype + class ConfigTestCase(unittest.TestCase): def test_sections(self): - conf = IniConfig('tests/assets/sample.ini') - for attr in ('TYPES', 'BOOLEANS'): + conf = IniConfig("tests/assets/sample.ini") + for attr in ("TYPES", "BOOLEANS"): self.assertTrue(hasattr(conf, attr)) self.assertIsInstance(getattr(conf, attr), dict) def test_types(self): - conf = IniConfig('tests/assets/sample.ini') + conf = IniConfig("tests/assets/sample.ini") - self.assertIsInstance(conf.TYPES['float'], float) - self.assertIsInstance(conf.TYPES['int'], int) - self.assertIsInstance(conf.TYPES['string'], strtype) + self.assertIsInstance(conf.TYPES["float"], float) + self.assertIsInstance(conf.TYPES["int"], int) + self.assertIsInstance(conf.TYPES["string"], strtype) - for t in ('bool', 'switch', 'yn'): - self.assertIsInstance(conf.BOOLEANS[t + '_false'], bool) - self.assertIsInstance(conf.BOOLEANS[t + '_true'], bool) - self.assertFalse(conf.BOOLEANS[t + '_false']) - self.assertTrue(conf.BOOLEANS[t + '_true']) + for t in ("bool", "switch", "yn"): + self.assertIsInstance(conf.BOOLEANS[t + "_false"], bool) + self.assertIsInstance(conf.BOOLEANS[t + "_true"], bool) + self.assertFalse(conf.BOOLEANS[t + "_false"]) + self.assertTrue(conf.BOOLEANS[t + "_true"]) def test_no_interpolation(self): - conf = IniConfig('tests/assets/sample.ini') + conf = IniConfig("tests/assets/sample.ini") - self.assertEqual(conf.ISSUE84['variable'], 'value') - self.assertEqual(conf.ISSUE84['key'], 'some value with a %variable') + self.assertEqual(conf.ISSUE84["variable"], "value") + self.assertEqual(conf.ISSUE84["key"], "some value with a %variable") -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/base/test_db.py b/tests/base/test_db.py index 1095599..5251081 100644 --- a/tests/base/test_db.py +++ b/tests/base/test_db.py @@ -17,11 +17,12 @@ from pony.orm import db_session from supysonic import db -date_regex = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$') +date_regex = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$") + class DbTestCase(unittest.TestCase): def setUp(self): - db.init_database('sqlite:') + db.init_database("sqlite:") try: self.assertRegex @@ -32,100 +33,89 @@ class DbTestCase(unittest.TestCase): db.release_database() def create_some_folders(self): - root_folder = db.Folder( - root = True, - name = 'Root folder', - path = 'tests' - ) + root_folder = db.Folder(root=True, name="Root folder", path="tests") child_folder = db.Folder( - root = False, - name = 'Child folder', - path = 'tests/assets', - cover_art = 'cover.jpg', - parent = root_folder + root=False, + name="Child folder", + path="tests/assets", + cover_art="cover.jpg", + parent=root_folder, ) child_2 = db.Folder( - root = False, - name = 'Child folder (No Art)', - path = 'tests/formats', - parent = root_folder + root=False, + name="Child folder (No Art)", + path="tests/formats", + parent=root_folder, ) return root_folder, child_folder, child_2 - def create_some_tracks(self, artist = None, album = None): + def create_some_tracks(self, artist=None, album=None): root, child, child_2 = self.create_some_folders() if not artist: - artist = db.Artist(name = 'Test artist') + artist = db.Artist(name="Test artist") if not album: - album = db.Album(artist = artist, name = 'Test Album') + album = db.Album(artist=artist, name="Test Album") track1 = db.Track( - title = 'Track Title', - album = album, - artist = artist, - disc = 1, - number = 1, - duration = 3, - has_art = True, - bitrate = 320, - path = 'tests/assets/formats/silence.ogg', - last_modification = 1234, - root_folder = root, - folder = child + title="Track Title", + album=album, + artist=artist, + disc=1, + number=1, + duration=3, + has_art=True, + bitrate=320, + path="tests/assets/formats/silence.ogg", + last_modification=1234, + root_folder=root, + folder=child, ) track2 = db.Track( - title = 'One Awesome Song', - album = album, - artist = artist, - disc = 1, - number = 2, - duration = 5, - bitrate = 96, - path = 'tests/assets/23bytes', - last_modification = 1234, - root_folder = root, - folder = child + title="One Awesome Song", + album=album, + artist=artist, + disc=1, + number=2, + duration=5, + bitrate=96, + path="tests/assets/23bytes", + last_modification=1234, + root_folder=root, + folder=child, ) return track1, track2 - def create_track_in(self, folder, root, artist = None, album = None, has_art = True): - artist = artist or db.Artist(name = 'Snazzy Artist') - album = album or db.Album(artist = artist, name = 'Rockin\' Album') + def create_track_in(self, folder, root, artist=None, album=None, has_art=True): + artist = artist or db.Artist(name="Snazzy Artist") + album = album or db.Album(artist=artist, name="Rockin' Album") return db.Track( - title = 'Nifty Number', - album = album, - artist = artist, - disc = 1, - number = 1, - duration = 5, - has_art = has_art, - bitrate = 96, - path = 'tests/assets/formats/silence.flac', - last_modification = 1234, - root_folder = root, - folder = folder + title="Nifty Number", + album=album, + artist=artist, + disc=1, + number=1, + duration=5, + has_art=has_art, + bitrate=96, + path="tests/assets/formats/silence.flac", + last_modification=1234, + root_folder=root, + folder=folder, ) - def create_user(self, name = 'Test User'): - return db.User( - name = name, - password = 'secret', - salt = 'ABC+', - ) + def create_user(self, name="Test User"): + return db.User(name=name, password="secret", salt="ABC+") def create_playlist(self): - playlist = db.Playlist( - user = self.create_user(), - name = 'Playlist!' - ) + playlist = db.Playlist(user=self.create_user(), name="Playlist!") return playlist @@ -134,146 +124,134 @@ class DbTestCase(unittest.TestCase): root_folder, child_folder, child_noart = self.create_some_folders() track_embededart = self.create_track_in(child_noart, root_folder) - MockUser = namedtuple('User', [ 'id' ]) + MockUser = namedtuple("User", ["id"]) user = MockUser(uuid.uuid4()) root = root_folder.as_subsonic_child(user) self.assertIsInstance(root, dict) - self.assertIn('id', root) - self.assertIn('isDir', root) - self.assertIn('title', root) - self.assertIn('album', root) - self.assertIn('created', root) - self.assertTrue(root['isDir']) - self.assertEqual(root['title'], 'Root folder') - self.assertEqual(root['album'], 'Root folder') - self.assertRegex(root['created'], date_regex) + self.assertIn("id", root) + self.assertIn("isDir", root) + self.assertIn("title", root) + self.assertIn("album", root) + self.assertIn("created", root) + self.assertTrue(root["isDir"]) + self.assertEqual(root["title"], "Root folder") + self.assertEqual(root["album"], "Root folder") + self.assertRegex(root["created"], date_regex) child = child_folder.as_subsonic_child(user) - self.assertIn('parent', child) - self.assertIn('artist', child) - self.assertIn('coverArt', child) - self.assertEqual(child['parent'], str(root_folder.id)) - self.assertEqual(child['artist'], root_folder.name) - self.assertEqual(child['coverArt'], child['id']) + self.assertIn("parent", child) + self.assertIn("artist", child) + self.assertIn("coverArt", child) + self.assertEqual(child["parent"], str(root_folder.id)) + self.assertEqual(child["artist"], root_folder.name) + self.assertEqual(child["coverArt"], child["id"]) noart = child_noart.as_subsonic_child(user) - self.assertIn('coverArt', noart) - self.assertEqual(noart['coverArt'], str(track_embededart.id)) + self.assertIn("coverArt", noart) + self.assertEqual(noart["coverArt"], str(track_embededart.id)) @db_session def test_folder_annotation(self): root_folder, child_folder, _ = self.create_some_folders() user = self.create_user() - star = db.StarredFolder( - user = user, - starred = root_folder - ) - rating_user = db.RatingFolder( - user = user, - rated = root_folder, - rating = 2 - ) - other = self.create_user('Other') - rating_other = db.RatingFolder( - user = other, - rated = root_folder, - rating = 5 - ) + star = db.StarredFolder(user=user, starred=root_folder) + rating_user = db.RatingFolder(user=user, rated=root_folder, rating=2) + other = self.create_user("Other") + rating_other = db.RatingFolder(user=other, rated=root_folder, rating=5) root = root_folder.as_subsonic_child(user) - self.assertIn('starred', root) - self.assertIn('userRating', root) - self.assertIn('averageRating', root) - self.assertRegex(root['starred'], date_regex) - self.assertEqual(root['userRating'], 2) - self.assertEqual(root['averageRating'], 3.5) + self.assertIn("starred", root) + self.assertIn("userRating", root) + self.assertIn("averageRating", root) + self.assertRegex(root["starred"], date_regex) + self.assertEqual(root["userRating"], 2) + self.assertEqual(root["averageRating"], 3.5) child = child_folder.as_subsonic_child(user) - self.assertNotIn('starred', child) - self.assertNotIn('userRating', child) + self.assertNotIn("starred", child) + self.assertNotIn("userRating", child) @db_session def test_artist(self): - artist = db.Artist(name = 'Test Artist') + artist = db.Artist(name="Test Artist") user = self.create_user() - star = db.StarredArtist(user = user, starred = artist) + star = db.StarredArtist(user=user, starred=artist) artist_dict = artist.as_subsonic_artist(user) self.assertIsInstance(artist_dict, dict) - self.assertIn('id', artist_dict) - self.assertIn('name', artist_dict) - self.assertIn('albumCount', artist_dict) - self.assertIn('starred', artist_dict) - self.assertEqual(artist_dict['name'], 'Test Artist') - self.assertEqual(artist_dict['albumCount'], 0) - self.assertRegex(artist_dict['starred'], date_regex) + self.assertIn("id", artist_dict) + self.assertIn("name", artist_dict) + self.assertIn("albumCount", artist_dict) + self.assertIn("starred", artist_dict) + self.assertEqual(artist_dict["name"], "Test Artist") + self.assertEqual(artist_dict["albumCount"], 0) + self.assertRegex(artist_dict["starred"], date_regex) - db.Album(name = 'Test Artist', artist = artist) # self-titled - db.Album(name = 'The Album After The First One', artist = artist) + db.Album(name="Test Artist", artist=artist) # self-titled + db.Album(name="The Album After The First One", artist=artist) artist_dict = artist.as_subsonic_artist(user) - self.assertEqual(artist_dict['albumCount'], 2) + self.assertEqual(artist_dict["albumCount"], 2) @db_session def test_album(self): - artist = db.Artist(name = 'Test Artist') - album = db.Album(artist = artist, name = 'Test Album') + artist = db.Artist(name="Test Artist") + album = db.Album(artist=artist, name="Test Album") user = self.create_user() - star = db.StarredAlbum( - user = user, - starred = album - ) + star = db.StarredAlbum(user=user, starred=album) # No tracks, shouldn't be stored under normal circumstances self.assertRaises(ValueError, album.as_subsonic_album, user) root_folder, folder_art, folder_noart = self.create_some_folders() - track1 = self.create_track_in(root_folder, folder_noart, artist = artist, album = album) + track1 = self.create_track_in( + root_folder, folder_noart, artist=artist, album=album + ) album_dict = album.as_subsonic_album(user) self.assertIsInstance(album_dict, dict) - self.assertIn('id', album_dict) - self.assertIn('name', album_dict) - self.assertIn('artist', album_dict) - self.assertIn('artistId', album_dict) - self.assertIn('songCount', album_dict) - self.assertIn('duration', album_dict) - self.assertIn('created', album_dict) - self.assertIn('starred', album_dict) - self.assertIn('coverArt', album_dict) - self.assertEqual(album_dict['name'], album.name) - self.assertEqual(album_dict['artist'], artist.name) - self.assertEqual(album_dict['artistId'], str(artist.id)) - self.assertEqual(album_dict['songCount'], 1) - self.assertEqual(album_dict['duration'], 5) - self.assertEqual(album_dict['coverArt'], str(track1.id)) - self.assertRegex(album_dict['created'], date_regex) - self.assertRegex(album_dict['starred'], date_regex) + self.assertIn("id", album_dict) + self.assertIn("name", album_dict) + self.assertIn("artist", album_dict) + self.assertIn("artistId", album_dict) + self.assertIn("songCount", album_dict) + self.assertIn("duration", album_dict) + self.assertIn("created", album_dict) + self.assertIn("starred", album_dict) + self.assertIn("coverArt", album_dict) + self.assertEqual(album_dict["name"], album.name) + self.assertEqual(album_dict["artist"], artist.name) + self.assertEqual(album_dict["artistId"], str(artist.id)) + self.assertEqual(album_dict["songCount"], 1) + self.assertEqual(album_dict["duration"], 5) + self.assertEqual(album_dict["coverArt"], str(track1.id)) + self.assertRegex(album_dict["created"], date_regex) + self.assertRegex(album_dict["starred"], date_regex) @db_session def test_track(self): track1, track2 = self.create_some_tracks() # Assuming SQLite doesn't enforce foreign key constraints - MockUser = namedtuple('User', [ 'id' ]) + MockUser = namedtuple("User", ["id"]) user = MockUser(uuid.uuid4()) track1_dict = track1.as_subsonic_child(user, None) self.assertIsInstance(track1_dict, dict) - self.assertIn('id', track1_dict) - self.assertIn('parent', track1_dict) - self.assertIn('isDir', track1_dict) - self.assertIn('title', track1_dict) - self.assertFalse(track1_dict['isDir']) - self.assertIn('coverArt', track1_dict) - self.assertEqual(track1_dict['coverArt'], track1_dict['id']) + self.assertIn("id", track1_dict) + self.assertIn("parent", track1_dict) + self.assertIn("isDir", track1_dict) + self.assertIn("title", track1_dict) + self.assertFalse(track1_dict["isDir"]) + self.assertIn("coverArt", track1_dict) + self.assertEqual(track1_dict["coverArt"], track1_dict["id"]) track2_dict = track2.as_subsonic_child(user, None) - self.assertEqual(track2_dict['coverArt'], track2_dict['parent']) + self.assertEqual(track2_dict["coverArt"], track2_dict["parent"]) # ... we'll test the rest against the API XSD. @db_session @@ -287,15 +265,12 @@ class DbTestCase(unittest.TestCase): def test_chat(self): user = self.create_user() - line = db.ChatMessage( - user = user, - message = 'Hello world!' - ) + line = db.ChatMessage(user=user, message="Hello world!") line_dict = line.responsize() self.assertIsInstance(line_dict, dict) - self.assertIn('username', line_dict) - self.assertEqual(line_dict['username'], user.name) + self.assertIn("username", line_dict) + self.assertEqual(line_dict["username"], user.name) @db_session def test_playlist(self): @@ -310,19 +285,21 @@ class DbTestCase(unittest.TestCase): playlist.add(track1) playlist.add(track2) - self.assertSequenceEqual(playlist.get_tracks(), [ track1, track2 ]) + self.assertSequenceEqual(playlist.get_tracks(), [track1, track2]) playlist.add(track2.id) playlist.add(track1.id) - self.assertSequenceEqual(playlist.get_tracks(), [ track1, track2, track2, track1 ]) + self.assertSequenceEqual( + playlist.get_tracks(), [track1, track2, track2, track1] + ) playlist.clear() self.assertSequenceEqual(playlist.get_tracks(), []) playlist.add(str(track1.id)) - self.assertSequenceEqual(playlist.get_tracks(), [ track1 ]) + self.assertSequenceEqual(playlist.get_tracks(), [track1]) - self.assertRaises(ValueError, playlist.add, 'some string') + self.assertRaises(ValueError, playlist.add, "some string") self.assertRaises(NameError, playlist.add, 2345) @db_session @@ -332,18 +309,18 @@ class DbTestCase(unittest.TestCase): playlist.add(track1) playlist.add(track2) - playlist.remove_at_indexes([ 0, 2 ]) - self.assertSequenceEqual(playlist.get_tracks(), [ track2 ]) + playlist.remove_at_indexes([0, 2]) + self.assertSequenceEqual(playlist.get_tracks(), [track2]) playlist.add(track1) playlist.add(track2) playlist.add(track2) - playlist.remove_at_indexes([ 2, 1 ]) - self.assertSequenceEqual(playlist.get_tracks(), [ track2, track2 ]) + playlist.remove_at_indexes([2, 1]) + self.assertSequenceEqual(playlist.get_tracks(), [track2, track2]) playlist.add(track1) - playlist.remove_at_indexes([ 1, 1 ]) - self.assertSequenceEqual(playlist.get_tracks(), [ track2, track1 ]) + playlist.remove_at_indexes([1, 1]) + self.assertSequenceEqual(playlist.get_tracks(), [track2, track1]) @db_session def test_playlist_fixing(self): @@ -353,14 +330,14 @@ class DbTestCase(unittest.TestCase): playlist.add(track1) playlist.add(uuid.uuid4()) playlist.add(track2) - self.assertSequenceEqual(playlist.get_tracks(), [ track1, track2 ]) + self.assertSequenceEqual(playlist.get_tracks(), [track1, track2]) track2.delete() - self.assertSequenceEqual(playlist.get_tracks(), [ track1 ]) + self.assertSequenceEqual(playlist.get_tracks(), [track1]) - playlist.tracks = '{0},{0},some random garbage,{0}'.format(track1.id) - self.assertSequenceEqual(playlist.get_tracks(), [ track1, track1, track1 ]) + playlist.tracks = "{0},{0},some random garbage,{0}".format(track1.id) + self.assertSequenceEqual(playlist.get_tracks(), [track1, track1, track1]) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/base/test_lastfm.py b/tests/base/test_lastfm.py index 1806dd7..9ade280 100644 --- a/tests/base/test_lastfm.py +++ b/tests/base/test_lastfm.py @@ -13,16 +13,17 @@ import unittest from supysonic.lastfm import LastFm + class LastFmTestCase(unittest.TestCase): """ Designed only to have coverage on the most important method """ def test_request(self): - logging.getLogger('supysonic.lastfm').addHandler(logging.NullHandler()) - lastfm = LastFm({ 'api_key': 'key', 'secret': 'secret' }, None) + logging.getLogger("supysonic.lastfm").addHandler(logging.NullHandler()) + lastfm = LastFm({"api_key": "key", "secret": "secret"}, None) - rv = lastfm._LastFm__api_request(False, method = 'dummy', accents = u'àéèùö') + rv = lastfm._LastFm__api_request(False, method="dummy", accents=u"àéèùö") self.assertIsInstance(rv, dict) -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/tests/base/test_scanner.py b/tests/base/test_scanner.py index 1c34804..9cecdee 100644 --- a/tests/base/test_scanner.py +++ b/tests/base/test_scanner.py @@ -21,12 +21,13 @@ from supysonic import db from supysonic.managers.folder import FolderManager from supysonic.scanner import Scanner + class ScannerTestCase(unittest.TestCase): def setUp(self): - db.init_database('sqlite:') + db.init_database("sqlite:") with db_session: - folder = FolderManager.add('folder', os.path.abspath('tests/assets/folder')) + folder = FolderManager.add("folder", os.path.abspath("tests/assets/folder")) self.assertIsNotNone(folder) self.folderid = folder.id @@ -38,14 +39,14 @@ class ScannerTestCase(unittest.TestCase): @contextmanager def __temporary_track_copy(self): track = db.Track.select().first() - with tempfile.NamedTemporaryFile(dir = os.path.dirname(track.path)) as tf: - with io.open(track.path, 'rb') as f: + with tempfile.NamedTemporaryFile(dir=os.path.dirname(track.path)) as tf: + with io.open(track.path, "rb") as f: tf.write(f.read()) yield tf - def __scan(self, force = False): + def __scan(self, force=False): self.scanner = Scanner(force) - self.scanner.queue_folder('folder') + self.scanner.queue_folder("folder") self.scanner.run() @db_session @@ -53,7 +54,9 @@ class ScannerTestCase(unittest.TestCase): self.assertEqual(db.Track.select().count(), 1) self.assertRaises(TypeError, self.scanner.queue_folder, None) - self.assertRaises(TypeError, self.scanner.queue_folder, db.Folder[self.folderid]) + self.assertRaises( + TypeError, self.scanner.queue_folder, db.Folder[self.folderid] + ) @db_session def test_rescan(self): @@ -73,7 +76,7 @@ class ScannerTestCase(unittest.TestCase): self.assertRaises(TypeError, self.scanner.scan_file, None) self.assertRaises(TypeError, self.scanner.scan_file, track) - self.scanner.scan_file('/some/inexistent/path') + self.scanner.scan_file("/some/inexistent/path") commit() self.assertEqual(db.Track.select().count(), 1) @@ -83,7 +86,7 @@ class ScannerTestCase(unittest.TestCase): self.assertRaises(TypeError, self.scanner.remove_file, None) self.assertRaises(TypeError, self.scanner.remove_file, track) - self.scanner.remove_file('/some/inexistent/path') + self.scanner.remove_file("/some/inexistent/path") commit() self.assertEqual(db.Track.select().count(), 1) @@ -97,12 +100,12 @@ class ScannerTestCase(unittest.TestCase): @db_session def test_move_file(self): track = db.Track.select().first() - self.assertRaises(TypeError, self.scanner.move_file, None, 'string') - self.assertRaises(TypeError, self.scanner.move_file, track, 'string') - self.assertRaises(TypeError, self.scanner.move_file, 'string', None) - self.assertRaises(TypeError, self.scanner.move_file, 'string', track) + self.assertRaises(TypeError, self.scanner.move_file, None, "string") + self.assertRaises(TypeError, self.scanner.move_file, track, "string") + self.assertRaises(TypeError, self.scanner.move_file, "string", None) + self.assertRaises(TypeError, self.scanner.move_file, "string", track) - self.scanner.move_file('/some/inexistent/path', track.path) + self.scanner.move_file("/some/inexistent/path", track.path) commit() self.assertEqual(db.Track.select().count(), 1) @@ -110,7 +113,9 @@ class ScannerTestCase(unittest.TestCase): commit() self.assertEqual(db.Track.select().count(), 1) - self.assertRaises(Exception, self.scanner.move_file, track.path, '/some/inexistent/path') + self.assertRaises( + Exception, self.scanner.move_file, track.path, "/some/inexistent/path" + ) with self.__temporary_track_copy() as tf: self.__scan() @@ -121,7 +126,7 @@ class ScannerTestCase(unittest.TestCase): self.assertEqual(db.Track.select().count(), 1) track = db.Track.select().first() - new_path = track.path.replace('silence','silence_moved') + new_path = track.path.replace("silence", "silence_moved") self.scanner.move_file(track.path, new_path) commit() self.assertEqual(db.Track.select().count(), 1) @@ -137,7 +142,7 @@ class ScannerTestCase(unittest.TestCase): self.assertEqual(db.Track.select().count(), 2) tf.seek(0, 0) - tf.write(b'\x00' * 4096) + tf.write(b"\x00" * 4096) tf.truncate() self.__scan(True) @@ -164,20 +169,20 @@ class ScannerTestCase(unittest.TestCase): with self.__temporary_track_copy() as tf: self.__scan() commit() - copy = db.Track.get(path = tf.name) - self.assertEqual(copy.artist.name, 'Some artist') - self.assertEqual(copy.album.name, 'Awesome album') + copy = db.Track.get(path=tf.name) + self.assertEqual(copy.artist.name, "Some artist") + self.assertEqual(copy.album.name, "Awesome album") - tags = mutagen.File(copy.path, easy = True) - tags['artist'] = 'Renamed artist' - tags['album'] = 'Crappy album' + tags = mutagen.File(copy.path, easy=True) + tags["artist"] = "Renamed artist" + tags["album"] = "Crappy album" tags.save() self.__scan(True) - self.assertEqual(copy.artist.name, 'Renamed artist') - self.assertEqual(copy.album.name, 'Crappy album') - self.assertIsNotNone(db.Artist.get(name = 'Some artist')) - self.assertIsNotNone(db.Album.get(name = 'Awesome album')) + self.assertEqual(copy.artist.name, "Renamed artist") + self.assertEqual(copy.album.name, "Crappy album") + self.assertIsNotNone(db.Artist.get(name="Some artist")) + self.assertIsNotNone(db.Album.get(name="Awesome album")) def test_stats(self): stats = self.scanner.stats() @@ -188,6 +193,6 @@ class ScannerTestCase(unittest.TestCase): self.assertEqual(stats.deleted.albums, 0) self.assertEqual(stats.deleted.tracks, 0) -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/tests/base/test_secret.py b/tests/base/test_secret.py index f892a1b..81d8f07 100644 --- a/tests/base/test_secret.py +++ b/tests/base/test_secret.py @@ -18,22 +18,22 @@ from supysonic.web import create_application from ..testbase import TestConfig + class SecretTestCase(unittest.TestCase): def setUp(self): self.__dbfile = tempfile.mkstemp()[1] self.__dir = tempfile.mkdtemp() self.config = TestConfig(False, False) - self.config.BASE['database_uri'] = 'sqlite:///' + self.__dbfile - self.config.WEBAPP['cache_dir'] = self.__dir + self.config.BASE["database_uri"] = "sqlite:///" + self.__dbfile + self.config.WEBAPP["cache_dir"] = self.__dir - init_database(self.config.BASE['database_uri']) + init_database(self.config.BASE["database_uri"]) release_database() def tearDown(self): shutil.rmtree(self.__dir) os.remove(self.__dbfile) - def test_key(self): app1 = create_application(self.config) release_database() @@ -43,6 +43,6 @@ class SecretTestCase(unittest.TestCase): self.assertEqual(app1.secret_key, app2.secret_key) -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/tests/base/test_watcher.py b/tests/base/test_watcher.py index c801e16..c88e581 100644 --- a/tests/base/test_watcher.py +++ b/tests/base/test_watcher.py @@ -26,25 +26,23 @@ from supysonic.watcher import SupysonicWatcher from ..testbase import TestConfig + class WatcherTestConfig(TestConfig): - DAEMON = { - 'wait_delay': 0.5, - 'log_file': '/dev/null', - 'log_level': 'DEBUG' - } + DAEMON = {"wait_delay": 0.5, "log_file": "/dev/null", "log_level": "DEBUG"} def __init__(self, db_uri): super(WatcherTestConfig, self).__init__(False, False) - self.BASE['database_uri'] = db_uri + self.BASE["database_uri"] = db_uri + class WatcherTestBase(unittest.TestCase): def setUp(self): self.__dbfile = tempfile.mkstemp()[1] - dburi = 'sqlite:///' + self.__dbfile + dburi = "sqlite:///" + self.__dbfile init_database(dburi) conf = WatcherTestConfig(dburi) - self.__sleep_time = conf.DAEMON['wait_delay'] + 1 + self.__sleep_time = conf.DAEMON["wait_delay"] + 1 self.__watcher = SupysonicWatcher(conf) @@ -65,12 +63,13 @@ class WatcherTestBase(unittest.TestCase): def _sleep(self): time.sleep(self.__sleep_time) + class WatcherTestCase(WatcherTestBase): def setUp(self): super(WatcherTestCase, self).setUp() self.__dir = tempfile.mkdtemp() with db_session: - FolderManager.add('Folder', self.__dir) + FolderManager.add("Folder", self.__dir) self._start() def tearDown(self): @@ -83,25 +82,28 @@ class WatcherTestCase(WatcherTestBase): with tempfile.NamedTemporaryFile() as f: return os.path.basename(f.name) - def _temppath(self, suffix, depth = 0): + def _temppath(self, suffix, depth=0): if depth > 0: - dirpath = os.path.join(self.__dir, *(self._tempname() for _ in range(depth))) + dirpath = os.path.join( + self.__dir, *(self._tempname() for _ in range(depth)) + ) os.makedirs(dirpath) else: dirpath = self.__dir return os.path.join(dirpath, self._tempname() + suffix) - def _addfile(self, depth = 0): - path = self._temppath('.mp3', depth) - shutil.copyfile('tests/assets/folder/silence.mp3', path) + def _addfile(self, depth=0): + path = self._temppath(".mp3", depth) + shutil.copyfile("tests/assets/folder/silence.mp3", path) return path - def _addcover(self, suffix = None, depth = 0): - suffix = '.jpg' if suffix is None else (suffix + '.jpg') + def _addcover(self, suffix=None, depth=0): + suffix = ".jpg" if suffix is None else (suffix + ".jpg") path = self._temppath(suffix, depth) - shutil.copyfile('tests/assets/cover.jpg', path) + shutil.copyfile("tests/assets/cover.jpg", path) return path + class AudioWatcherTestCase(WatcherTestCase): @db_session def assertTrackCountEqual(self, expected): @@ -114,7 +116,7 @@ class AudioWatcherTestCase(WatcherTestCase): self.assertTrackCountEqual(1) # This test now fails and I don't understand why - #def test_add_nowait_stop(self): + # def test_add_nowait_stop(self): # self._addfile() # self._stop() # self.assertTrackCountEqual(1) @@ -136,18 +138,22 @@ class AudioWatcherTestCase(WatcherTestCase): trackid = None with db_session: self.assertEqual(Track.select().count(), 1) - self.assertEqual(Artist.select(lambda a: a.name == 'Some artist').count(), 1) + self.assertEqual( + Artist.select(lambda a: a.name == "Some artist").count(), 1 + ) trackid = Track.select().first().id - tags = mutagen.File(path, easy = True) - tags['artist'] = 'Renamed' + tags = mutagen.File(path, easy=True) + tags["artist"] = "Renamed" tags.save() self._sleep() with db_session: self.assertEqual(Track.select().count(), 1) - self.assertEqual(Artist.select(lambda a: a.name == 'Some artist').count(), 0) - self.assertEqual(Artist.select(lambda a: a.name == 'Renamed').count(), 1) + self.assertEqual( + Artist.select(lambda a: a.name == "Some artist").count(), 0 + ) + self.assertEqual(Artist.select(lambda a: a.name == "Renamed").count(), 1) self.assertEqual(Track.select().first().id, trackid) def test_rename(self): @@ -159,7 +165,7 @@ class AudioWatcherTestCase(WatcherTestCase): self.assertEqual(Track.select().count(), 1) trackid = Track.select().first().id - newpath = self._temppath('.mp3') + newpath = self._temppath(".mp3") shutil.move(path, newpath) self._sleep() @@ -168,14 +174,16 @@ class AudioWatcherTestCase(WatcherTestCase): self.assertIsNotNone(track) self.assertNotEqual(track.path, path) self.assertEqual(track.path, newpath) - self.assertEqual(track._path_hash, memoryview(sha1(newpath.encode('utf-8')).digest())) + self.assertEqual( + track._path_hash, memoryview(sha1(newpath.encode("utf-8")).digest()) + ) self.assertEqual(track.id, trackid) def test_move_in(self): - filename = self._tempname() + '.mp3' + filename = self._tempname() + ".mp3" initialpath = os.path.join(tempfile.gettempdir(), filename) - shutil.copyfile('tests/assets/folder/silence.mp3', initialpath) - shutil.move(initialpath, self._temppath('.mp3')) + shutil.copyfile("tests/assets/folder/silence.mp3", initialpath) + shutil.move(initialpath, self._temppath(".mp3")) self._sleep() self.assertTrackCountEqual(1) @@ -208,7 +216,7 @@ class AudioWatcherTestCase(WatcherTestCase): def test_add_rename(self): path = self._addfile() - shutil.move(path, self._temppath('.mp3')) + shutil.move(path, self._temppath(".mp3")) self._sleep() self.assertTrackCountEqual(1) @@ -217,7 +225,7 @@ class AudioWatcherTestCase(WatcherTestCase): self._sleep() self.assertTrackCountEqual(1) - newpath = self._temppath('.mp3') + newpath = self._temppath(".mp3") shutil.move(path, newpath) os.unlink(newpath) self._sleep() @@ -225,7 +233,7 @@ class AudioWatcherTestCase(WatcherTestCase): def test_add_rename_delete(self): path = self._addfile() - newpath = self._temppath('.mp3') + newpath = self._temppath(".mp3") shutil.move(path, newpath) os.unlink(newpath) self._sleep() @@ -236,13 +244,14 @@ class AudioWatcherTestCase(WatcherTestCase): self._sleep() self.assertTrackCountEqual(1) - newpath = self._temppath('.mp3') - finalpath = self._temppath('.mp3') + newpath = self._temppath(".mp3") + finalpath = self._temppath(".mp3") shutil.move(path, newpath) shutil.move(newpath, finalpath) self._sleep() self.assertTrackCountEqual(1) + class CoverWatcherTestCase(WatcherTestCase): def test_add_file_then_cover(self): self._addfile() @@ -274,14 +283,14 @@ class CoverWatcherTestCase(WatcherTestCase): def test_naming_add_good(self): bad = os.path.basename(self._addcover()) self._sleep() - good = os.path.basename(self._addcover('cover')) + good = os.path.basename(self._addcover("cover")) self._sleep() with db_session: self.assertEqual(Folder.select().first().cover_art, good) def test_naming_add_bad(self): - good = os.path.basename(self._addcover('cover')) + good = os.path.basename(self._addcover("cover")) self._sleep() bad = os.path.basename(self._addcover()) self._sleep() @@ -291,7 +300,7 @@ class CoverWatcherTestCase(WatcherTestCase): def test_naming_remove_good(self): bad = self._addcover() - good = self._addcover('cover') + good = self._addcover("cover") self._sleep() os.unlink(good) self._sleep() @@ -301,7 +310,7 @@ class CoverWatcherTestCase(WatcherTestCase): def test_naming_remove_bad(self): bad = self._addcover() - good = self._addcover('cover') + good = self._addcover("cover") self._sleep() os.unlink(bad) self._sleep() @@ -312,22 +321,24 @@ class CoverWatcherTestCase(WatcherTestCase): def test_rename(self): path = self._addcover() self._sleep() - newpath = self._temppath('.jpg') + newpath = self._temppath(".jpg") shutil.move(path, newpath) self._sleep() with db_session: - self.assertEqual(Folder.select().first().cover_art, os.path.basename(newpath)) + self.assertEqual( + Folder.select().first().cover_art, os.path.basename(newpath) + ) def test_add_to_folder_without_track(self): - path = self._addcover(depth = 1) + path = self._addcover(depth=1) self._sleep() with db_session: - self.assertFalse(Folder.exists(cover_art = os.path.basename(path))) + self.assertFalse(Folder.exists(cover_art=os.path.basename(path))) def test_remove_from_folder_without_track(self): - path = self._addcover(depth = 1) + path = self._addcover(depth=1) self._sleep() os.unlink(path) self._sleep() @@ -336,6 +347,7 @@ class CoverWatcherTestCase(WatcherTestCase): self._addfile(1) self._sleep() + def suite(): suite = unittest.TestSuite() @@ -344,6 +356,6 @@ def suite(): return suite -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/tests/frontend/__init__.py b/tests/frontend/__init__.py index bc6aefa..1aad44b 100644 --- a/tests/frontend/__init__.py +++ b/tests/frontend/__init__.py @@ -15,6 +15,7 @@ from .test_folder import FolderTestCase from .test_playlist import PlaylistTestCase from .test_user import UserTestCase + def suite(): suite = unittest.TestSuite() @@ -24,4 +25,3 @@ def suite(): suite.addTest(unittest.makeSuite(UserTestCase)) return suite - diff --git a/tests/frontend/frontendtestbase.py b/tests/frontend/frontendtestbase.py index 081736a..a05a96c 100644 --- a/tests/frontend/frontendtestbase.py +++ b/tests/frontend/frontendtestbase.py @@ -9,6 +9,7 @@ from ..testbase import TestBase + class FrontendTestBase(TestBase): __with_webui__ = True @@ -17,8 +18,11 @@ class FrontendTestBase(TestBase): self._patch_client() def _login(self, username, password): - return self.client.post('/user/login', data = { 'user': username, 'password': password }, follow_redirects = True) + return self.client.post( + "/user/login", + data={"user": username, "password": password}, + follow_redirects=True, + ) def _logout(self): - return self.client.get('/user/logout', follow_redirects = True) - + return self.client.get("/user/logout", follow_redirects=True) diff --git a/tests/frontend/test_folder.py b/tests/frontend/test_folder.py index d1a9491..c9f3221 100644 --- a/tests/frontend/test_folder.py +++ b/tests/frontend/test_folder.py @@ -16,87 +16,85 @@ from supysonic.db import Folder from .frontendtestbase import FrontendTestBase + class FolderTestCase(FrontendTestBase): def test_index(self): - self._login('bob', 'B0b') - rv = self.client.get('/folder', follow_redirects = True) - self.assertIn('There\'s nothing much to see', rv.data) - self.assertNotIn('Music folders', rv.data) + self._login("bob", "B0b") + rv = self.client.get("/folder", follow_redirects=True) + self.assertIn("There's nothing much to see", rv.data) + self.assertNotIn("Music folders", rv.data) self._logout() - self._login('alice', 'Alic3') - rv = self.client.get('/folder') - self.assertIn('Music folders', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/folder") + self.assertIn("Music folders", rv.data) def test_add_get(self): - self._login('bob', 'B0b') - rv = self.client.get('/folder/add', follow_redirects = True) - self.assertIn('There\'s nothing much to see', rv.data) - self.assertNotIn('Add Folder', rv.data) + self._login("bob", "B0b") + rv = self.client.get("/folder/add", follow_redirects=True) + self.assertIn("There's nothing much to see", rv.data) + self.assertNotIn("Add Folder", rv.data) self._logout() - self._login('alice', 'Alic3') - rv = self.client.get('/folder/add') - self.assertIn('Add Folder', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/folder/add") + self.assertIn("Add Folder", rv.data) def test_add_post(self): - self._login('alice', 'Alic3') - rv = self.client.post('/folder/add') - self.assertIn('required', rv.data) - rv = self.client.post('/folder/add', data = { 'name': 'name' }) - self.assertIn('required', rv.data) - rv = self.client.post('/folder/add', data = { 'path': 'path' }) - self.assertIn('required', rv.data) - rv = self.client.post('/folder/add', data = { 'name': 'name', 'path': 'path' }) - self.assertIn('Add Folder', rv.data) - rv = self.client.post('/folder/add', data = { 'name': 'name', 'path': 'tests/assets' }, follow_redirects = True) - self.assertIn('created', rv.data) + self._login("alice", "Alic3") + rv = self.client.post("/folder/add") + self.assertIn("required", rv.data) + rv = self.client.post("/folder/add", data={"name": "name"}) + self.assertIn("required", rv.data) + rv = self.client.post("/folder/add", data={"path": "path"}) + self.assertIn("required", rv.data) + rv = self.client.post("/folder/add", data={"name": "name", "path": "path"}) + self.assertIn("Add Folder", rv.data) + rv = self.client.post( + "/folder/add", + data={"name": "name", "path": "tests/assets"}, + follow_redirects=True, + ) + self.assertIn("created", rv.data) with db_session: self.assertEqual(Folder.select().count(), 1) def test_delete(self): with db_session: - folder = Folder( - name = 'folder', - path = 'tests/assets', - root = True - ) + folder = Folder(name="folder", path="tests/assets", root=True) - self._login('bob', 'B0b') - rv = self.client.get('/folder/del/' + str(folder.id), follow_redirects = True) - self.assertIn('There\'s nothing much to see', rv.data) + self._login("bob", "B0b") + rv = self.client.get("/folder/del/" + str(folder.id), follow_redirects=True) + self.assertIn("There's nothing much to see", rv.data) with db_session: self.assertEqual(Folder.select().count(), 1) self._logout() - self._login('alice', 'Alic3') - rv = self.client.get('/folder/del/string', follow_redirects = True) - self.assertIn('badly formed', rv.data) - rv = self.client.get('/folder/del/' + str(uuid.uuid4()), follow_redirects = True) - self.assertIn('No such folder', rv.data) - rv = self.client.get('/folder/del/' + str(folder.id), follow_redirects = True) - self.assertIn('Music folders', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/folder/del/string", follow_redirects=True) + self.assertIn("badly formed", rv.data) + rv = self.client.get("/folder/del/" + str(uuid.uuid4()), follow_redirects=True) + self.assertIn("No such folder", rv.data) + rv = self.client.get("/folder/del/" + str(folder.id), follow_redirects=True) + self.assertIn("Music folders", rv.data) with db_session: self.assertEqual(Folder.select().count(), 0) def test_scan(self): with db_session: - folder = Folder( - name = 'folder', - path = 'tests/assets/folder', - root = True, - ) + folder = Folder(name="folder", path="tests/assets/folder", root=True) - self._login('alice', 'Alic3') + self._login("alice", "Alic3") - rv = self.client.get('/folder/scan/string', follow_redirects = True) - self.assertIn('badly formed', rv.data) - rv = self.client.get('/folder/scan/' + str(uuid.uuid4()), follow_redirects = True) - self.assertIn('No such folder', rv.data) - rv = self.client.get('/folder/scan/' + str(folder.id), follow_redirects = True) - self.assertIn('start', rv.data) - rv = self.client.get('/folder/scan', follow_redirects = True) - self.assertIn('start', rv.data) + rv = self.client.get("/folder/scan/string", follow_redirects=True) + self.assertIn("badly formed", rv.data) + rv = self.client.get("/folder/scan/" + str(uuid.uuid4()), follow_redirects=True) + self.assertIn("No such folder", rv.data) + rv = self.client.get("/folder/scan/" + str(folder.id), follow_redirects=True) + self.assertIn("start", rv.data) + rv = self.client.get("/folder/scan", follow_redirects=True) + self.assertIn("start", rv.data) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/tests/frontend/test_login.py b/tests/frontend/test_login.py index 5dde4ff..d2c1d3c 100644 --- a/tests/frontend/test_login.py +++ b/tests/frontend/test_login.py @@ -17,61 +17,62 @@ from supysonic.db import User from .frontendtestbase import FrontendTestBase + class LoginTestCase(FrontendTestBase): def test_unauthorized_request(self): # Unauthorized request - rv = self.client.get('/', follow_redirects=True) - self.assertIn('Please login', rv.data) + rv = self.client.get("/", follow_redirects=True) + self.assertIn("Please login", rv.data) def test_login_with_bad_data(self): # Login with not blank user or password - rv = self._login('', '') - self.assertIn('Missing user name', rv.data) - self.assertIn('Missing password', rv.data) + rv = self._login("", "") + self.assertIn("Missing user name", rv.data) + self.assertIn("Missing password", rv.data) # Login with not valid user or password - rv = self._login('nonexistent', 'nonexistent') - self.assertIn('Wrong username or password', rv.data) - rv = self._login('alice', 'badpassword') - self.assertIn('Wrong username or password', rv.data) + rv = self._login("nonexistent", "nonexistent") + self.assertIn("Wrong username or password", rv.data) + rv = self._login("alice", "badpassword") + self.assertIn("Wrong username or password", rv.data) def test_login_admin(self): # Login with a valid admin user - rv = self._login('alice', 'Alic3') - self.assertIn('Logged in', rv.data) - self.assertIn('Users', rv.data) - self.assertIn('Folders', rv.data) + rv = self._login("alice", "Alic3") + self.assertIn("Logged in", rv.data) + self.assertIn("Users", rv.data) + self.assertIn("Folders", rv.data) def test_login_non_admin(self): # Login with a valid non-admin user - rv = self._login('bob', 'B0b') - self.assertIn('Logged in', rv.data) + rv = self._login("bob", "B0b") + self.assertIn("Logged in", rv.data) # Non-admin user cannot acces to users and folders - self.assertNotIn('Users', rv.data) - self.assertNotIn('Folders', rv.data) + self.assertNotIn("Users", rv.data) + self.assertNotIn("Folders", rv.data) def test_root_with_valid_session(self): # Root with valid session with db_session: with self.client.session_transaction() as sess: - sess['userid'] = User.get(name = 'alice').id - rv = self.client.get('/', follow_redirects=True) - self.assertIn('alice', rv.data) - self.assertIn('Log out', rv.data) - self.assertIn('There\'s nothing much to see here.', rv.data) + sess["userid"] = User.get(name="alice").id + rv = self.client.get("/", follow_redirects=True) + self.assertIn("alice", rv.data) + self.assertIn("Log out", rv.data) + self.assertIn("There's nothing much to see here.", rv.data) def test_root_with_non_valid_session(self): # Root with a no-valid session with self.client.session_transaction() as sess: - sess['userid'] = uuid.uuid4() - rv = self.client.get('/', follow_redirects=True) - self.assertIn('Please login', rv.data) + sess["userid"] = uuid.uuid4() + rv = self.client.get("/", follow_redirects=True) + self.assertIn("Please login", rv.data) def test_multiple_login(self): - self._login('alice', 'Alic3') - rv = self._login('bob', 'B0b') - self.assertIn('Already logged in', rv.data) - self.assertIn('alice', rv.data) + self._login("alice", "Alic3") + rv = self._login("bob", "B0b") + self.assertIn("Already logged in", rv.data) + self.assertIn("alice", rv.data) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/frontend/test_playlist.py b/tests/frontend/test_playlist.py index 453c460..3c403fa 100644 --- a/tests/frontend/test_playlist.py +++ b/tests/frontend/test_playlist.py @@ -16,99 +16,111 @@ from supysonic.db import Folder, Artist, Album, Track, Playlist, User from .frontendtestbase import FrontendTestBase + class PlaylistTestCase(FrontendTestBase): def setUp(self): super(PlaylistTestCase, self).setUp() with db_session: - folder = Folder(name = 'Root', path = 'tests/assets', root = True) - artist = Artist(name = 'Artist!') - album = Album(name = 'Album!', artist = artist) + folder = Folder(name="Root", path="tests/assets", root=True) + artist = Artist(name="Artist!") + album = Album(name="Album!", artist=artist) track = Track( - path = 'tests/assets/23bytes', - title = '23bytes', - artist = artist, - album = album, - folder = folder, - root_folder = folder, - duration = 2, - disc = 1, - number = 1, - bitrate = 320, - last_modification = 0 + path="tests/assets/23bytes", + title="23bytes", + artist=artist, + album=album, + folder=folder, + root_folder=folder, + duration=2, + disc=1, + number=1, + bitrate=320, + last_modification=0, ) - playlist = Playlist( - name = 'Playlist!', - user = User.get(name = 'alice') - ) + playlist = Playlist(name="Playlist!", user=User.get(name="alice")) for _ in range(4): playlist.add(track) self.playlistid = playlist.id def test_index(self): - self._login('alice', 'Alic3') - rv = self.client.get('/playlist') - self.assertIn('My playlists', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/playlist") + self.assertIn("My playlists", rv.data) def test_details(self): - self._login('alice', 'Alic3') - rv = self.client.get('/playlist/string', follow_redirects = True) - self.assertIn('Invalid', rv.data) - rv = self.client.get('/playlist/' + str(uuid.uuid4()), follow_redirects = True) - self.assertIn('Unknown', rv.data) - rv = self.client.get('/playlist/' + str(self.playlistid)) - self.assertIn('Playlist!', rv.data) - self.assertIn('23bytes', rv.data) - self.assertIn('Artist!', rv.data) - self.assertIn('Album!', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/playlist/string", follow_redirects=True) + self.assertIn("Invalid", rv.data) + rv = self.client.get("/playlist/" + str(uuid.uuid4()), follow_redirects=True) + self.assertIn("Unknown", rv.data) + rv = self.client.get("/playlist/" + str(self.playlistid)) + self.assertIn("Playlist!", rv.data) + self.assertIn("23bytes", rv.data) + self.assertIn("Artist!", rv.data) + self.assertIn("Album!", rv.data) def test_update(self): - self._login('bob', 'B0b') - rv = self.client.post('/playlist/string', follow_redirects = True) - self.assertIn('Invalid', rv.data) - rv = self.client.post('/playlist/' + str(uuid.uuid4()), follow_redirects = True) - self.assertIn('Unknown', rv.data) - rv = self.client.post('/playlist/' + str(self.playlistid), follow_redirects = True) - self.assertNotIn('updated', rv.data) - self.assertIn('not allowed', rv.data) + self._login("bob", "B0b") + rv = self.client.post("/playlist/string", follow_redirects=True) + self.assertIn("Invalid", rv.data) + rv = self.client.post("/playlist/" + str(uuid.uuid4()), follow_redirects=True) + self.assertIn("Unknown", rv.data) + rv = self.client.post( + "/playlist/" + str(self.playlistid), follow_redirects=True + ) + self.assertNotIn("updated", rv.data) + self.assertIn("not allowed", rv.data) self._logout() - self._login('alice', 'Alic3') - rv = self.client.post('/playlist/' + str(self.playlistid), follow_redirects = True) - self.assertNotIn('updated', rv.data) - self.assertIn('Missing', rv.data) + self._login("alice", "Alic3") + rv = self.client.post( + "/playlist/" + str(self.playlistid), follow_redirects=True + ) + self.assertNotIn("updated", rv.data) + self.assertIn("Missing", rv.data) with db_session: - self.assertEqual(Playlist[self.playlistid].name, 'Playlist!') + self.assertEqual(Playlist[self.playlistid].name, "Playlist!") - rv = self.client.post('/playlist/' + str(self.playlistid), data = { 'name': 'abc', 'public': True }, follow_redirects = True) - self.assertIn('updated', rv.data) - self.assertNotIn('not allowed', rv.data) + rv = self.client.post( + "/playlist/" + str(self.playlistid), + data={"name": "abc", "public": True}, + follow_redirects=True, + ) + self.assertIn("updated", rv.data) + self.assertNotIn("not allowed", rv.data) with db_session: playlist = Playlist[self.playlistid] - self.assertEqual(playlist.name, 'abc') + self.assertEqual(playlist.name, "abc") self.assertTrue(playlist.public) def test_delete(self): - self._login('bob', 'B0b') - rv = self.client.get('/playlist/del/string', follow_redirects = True) - self.assertIn('Invalid', rv.data) - rv = self.client.get('/playlist/del/' + str(uuid.uuid4()), follow_redirects = True) - self.assertIn('Unknown', rv.data) - rv = self.client.get('/playlist/del/' + str(self.playlistid), follow_redirects = True) - self.assertIn('not allowed', rv.data) + self._login("bob", "B0b") + rv = self.client.get("/playlist/del/string", follow_redirects=True) + self.assertIn("Invalid", rv.data) + rv = self.client.get( + "/playlist/del/" + str(uuid.uuid4()), follow_redirects=True + ) + self.assertIn("Unknown", rv.data) + rv = self.client.get( + "/playlist/del/" + str(self.playlistid), follow_redirects=True + ) + self.assertIn("not allowed", rv.data) with db_session: self.assertEqual(Playlist.select().count(), 1) self._logout() - self._login('alice', 'Alic3') - rv = self.client.get('/playlist/del/' + str(self.playlistid), follow_redirects = True) - self.assertIn('deleted', rv.data) + self._login("alice", "Alic3") + rv = self.client.get( + "/playlist/del/" + str(self.playlistid), follow_redirects=True + ) + self.assertIn("deleted", rv.data) with db_session: self.assertEqual(Playlist.select().count(), 0) -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() diff --git a/tests/frontend/test_user.py b/tests/frontend/test_user.py index 67f72c3..ae361bc 100644 --- a/tests/frontend/test_user.py +++ b/tests/frontend/test_user.py @@ -17,225 +17,258 @@ from supysonic.db import User, ClientPrefs from .frontendtestbase import FrontendTestBase + class UserTestCase(FrontendTestBase): def setUp(self): super(UserTestCase, self).setUp() with db_session: - self.users = { u.name: u.id for u in User.select() } + self.users = {u.name: u.id for u in User.select()} def test_index(self): - self._login('bob', 'B0b') - rv = self.client.get('/user', follow_redirects = True) - self.assertIn('There\'s nothing much to see', rv.data) - self.assertNotIn('Users', rv.data) + self._login("bob", "B0b") + rv = self.client.get("/user", follow_redirects=True) + self.assertIn("There's nothing much to see", rv.data) + self.assertNotIn("Users", rv.data) self._logout() - self._login('alice', 'Alic3') - rv = self.client.get('/user') - self.assertIn('Users', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/user") + self.assertIn("Users", rv.data) def test_details(self): - self._login('alice', 'Alic3') - rv = self.client.get('/user/string', follow_redirects = True) - self.assertIn('badly formed', rv.data) - rv = self.client.get('/user/' + str(uuid.uuid4()), follow_redirects = True) - self.assertIn('No such user', rv.data) - rv = self.client.get('/user/' + str(self.users['bob'])) - self.assertIn('bob', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/user/string", follow_redirects=True) + self.assertIn("badly formed", rv.data) + rv = self.client.get("/user/" + str(uuid.uuid4()), follow_redirects=True) + self.assertIn("No such user", rv.data) + rv = self.client.get("/user/" + str(self.users["bob"])) + self.assertIn("bob", rv.data) self._logout() with db_session: - ClientPrefs(user = User[self.users['bob']], client_name = 'tests') + ClientPrefs(user=User[self.users["bob"]], client_name="tests") - self._login('bob', 'B0b') - rv = self.client.get('/user/' + str(self.users['alice']), follow_redirects = True) - self.assertIn('There\'s nothing much to see', rv.data) - self.assertNotIn('

bob

', rv.data) - rv = self.client.get('/user/me') - self.assertIn('

bob

', rv.data) - self.assertIn('tests', rv.data) + self._login("bob", "B0b") + rv = self.client.get("/user/" + str(self.users["alice"]), follow_redirects=True) + self.assertIn("There's nothing much to see", rv.data) + self.assertNotIn("

bob

", rv.data) + rv = self.client.get("/user/me") + self.assertIn("

bob

", rv.data) + self.assertIn("tests", rv.data) def test_update_client_prefs(self): - self._login('alice', 'Alic3') - rv = self.client.post('/user/me') - self.assertIn('updated', rv.data) # does nothing, says it's updated anyway + self._login("alice", "Alic3") + rv = self.client.post("/user/me") + self.assertIn("updated", rv.data) # does nothing, says it's updated anyway # error cases, silently ignored - self.client.post('/user/me', data = { 'garbage': 'trash' }) - self.client.post('/user/me', data = { 'a_b_c_d_e_f': 'g_h_i_j_k' }) - self.client.post('/user/me', data = { '_l': 'm' }) - self.client.post('/user/me', data = { 'n_': 'o' }) - self.client.post('/user/me', data = { 'inexisting_client': 'setting' }) + self.client.post("/user/me", data={"garbage": "trash"}) + self.client.post("/user/me", data={"a_b_c_d_e_f": "g_h_i_j_k"}) + self.client.post("/user/me", data={"_l": "m"}) + self.client.post("/user/me", data={"n_": "o"}) + self.client.post("/user/me", data={"inexisting_client": "setting"}) with db_session: - ClientPrefs(user = User[self.users['alice']], client_name = 'tests') + ClientPrefs(user=User[self.users["alice"]], client_name="tests") - rv = self.client.post('/user/me', data = { 'tests_format': 'mp3', 'tests_bitrate': 128 }) - self.assertIn('updated', rv.data) + rv = self.client.post( + "/user/me", data={"tests_format": "mp3", "tests_bitrate": 128} + ) + self.assertIn("updated", rv.data) with db_session: - prefs = ClientPrefs[User[self.users['alice']], 'tests'] - self.assertEqual(prefs.format, 'mp3') + prefs = ClientPrefs[User[self.users["alice"]], "tests"] + self.assertEqual(prefs.format, "mp3") self.assertEqual(prefs.bitrate, 128) - self.client.post('/user/me', data = { 'tests_delete': 1 }) + self.client.post("/user/me", data={"tests_delete": 1}) with db_session: self.assertEqual(ClientPrefs.select().count(), 0) def test_change_username_get(self): - self._login('bob', 'B0b') - rv = self.client.get('/user/whatever/changeusername', follow_redirects = True) - self.assertIn('There\'s nothing much to see', rv.data) + self._login("bob", "B0b") + rv = self.client.get("/user/whatever/changeusername", follow_redirects=True) + self.assertIn("There's nothing much to see", rv.data) self._logout() - self._login('alice', 'Alic3') - rv = self.client.get('/user/whatever/changeusername', follow_redirects = True) - self.assertIn('badly formed', rv.data) - rv = self.client.get('/user/{}/changeusername'.format(uuid.uuid4()), follow_redirects = True) - self.assertIn('No such user', rv.data) - self.client.get('/user/{}/changeusername'.format(self.users['bob'])) + self._login("alice", "Alic3") + rv = self.client.get("/user/whatever/changeusername", follow_redirects=True) + self.assertIn("badly formed", rv.data) + rv = self.client.get( + "/user/{}/changeusername".format(uuid.uuid4()), follow_redirects=True + ) + self.assertIn("No such user", rv.data) + self.client.get("/user/{}/changeusername".format(self.users["bob"])) def test_change_username_post(self): - self._login('alice', 'Alic3') - rv = self.client.post('/user/whatever/changeusername', follow_redirects = True) - self.assertIn('badly formed', rv.data) - rv = self.client.post('/user/{}/changeusername'.format(uuid.uuid4()), follow_redirects = True) - self.assertIn('No such user', rv.data) + self._login("alice", "Alic3") + rv = self.client.post("/user/whatever/changeusername", follow_redirects=True) + self.assertIn("badly formed", rv.data) + rv = self.client.post( + "/user/{}/changeusername".format(uuid.uuid4()), follow_redirects=True + ) + self.assertIn("No such user", rv.data) - path = '/user/{}/changeusername'.format(self.users['bob']) - rv = self.client.post(path, follow_redirects = True) - self.assertIn('required', rv.data) - rv = self.client.post(path, data = { 'user': 'bob' }, follow_redirects = True) - self.assertIn('No changes', rv.data) - rv = self.client.post(path, data = { 'user': 'b0b', 'admin': 1 }, follow_redirects = True) - self.assertIn('updated', rv.data) - self.assertIn('b0b', rv.data) + path = "/user/{}/changeusername".format(self.users["bob"]) + rv = self.client.post(path, follow_redirects=True) + self.assertIn("required", rv.data) + rv = self.client.post(path, data={"user": "bob"}, follow_redirects=True) + self.assertIn("No changes", rv.data) + rv = self.client.post( + path, data={"user": "b0b", "admin": 1}, follow_redirects=True + ) + self.assertIn("updated", rv.data) + self.assertIn("b0b", rv.data) with db_session: - bob = User[self.users['bob']] - self.assertEqual(bob.name, 'b0b') + bob = User[self.users["bob"]] + self.assertEqual(bob.name, "b0b") self.assertTrue(bob.admin) - rv = self.client.post(path, data = { 'user': 'alice' }, follow_redirects = True) + rv = self.client.post(path, data={"user": "alice"}, follow_redirects=True) with db_session: - self.assertEqual(User[self.users['bob']].name, 'b0b') + self.assertEqual(User[self.users["bob"]].name, "b0b") def test_change_mail_get(self): - self._login('alice', 'Alic3') - self.client.get('/user/me/changemail') + self._login("alice", "Alic3") + self.client.get("/user/me/changemail") # whatever def test_change_mail_post(self): - self._login('alice', 'Alic3') - self.client.post('/user/me/changemail') + self._login("alice", "Alic3") + self.client.post("/user/me/changemail") # whatever def test_change_password_get(self): - self._login('alice', 'Alic3') - rv = self.client.get('/user/me/changepass') - self.assertIn('Current password', rv.data) - rv = self.client.get('/user/{}/changepass'.format(self.users['bob'])) - self.assertNotIn('Current password', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/user/me/changepass") + self.assertIn("Current password", rv.data) + rv = self.client.get("/user/{}/changepass".format(self.users["bob"])) + self.assertNotIn("Current password", rv.data) def test_change_password_post(self): - self._login('alice', 'Alic3') - path = '/user/me/changepass' + self._login("alice", "Alic3") + path = "/user/me/changepass" rv = self.client.post(path) - self.assertIn('required', rv.data) - rv = self.client.post(path, data = { 'current': 'alice' }) - self.assertIn('required', rv.data) - rv = self.client.post(path, data = { 'new': 'alice' }) - self.assertIn('required', rv.data) - rv = self.client.post(path, data = { 'current': 'alice', 'new': 'alice' }) - self.assertIn('password and its confirmation don', rv.data) - rv = self.client.post(path, data = { 'current': 'alice', 'new': 'alice', 'confirm': 'alice' }) - self.assertIn('Wrong password', rv.data) + self.assertIn("required", rv.data) + rv = self.client.post(path, data={"current": "alice"}) + self.assertIn("required", rv.data) + rv = self.client.post(path, data={"new": "alice"}) + self.assertIn("required", rv.data) + rv = self.client.post(path, data={"current": "alice", "new": "alice"}) + self.assertIn("password and its confirmation don", rv.data) + rv = self.client.post( + path, data={"current": "alice", "new": "alice", "confirm": "alice"} + ) + self.assertIn("Wrong password", rv.data) self._logout() - rv = self._login('alice', 'Alic3') - self.assertIn('Logged in', rv.data) - rv = self.client.post(path, data = { 'current': 'Alic3', 'new': 'alice', 'confirm': 'alice' }, follow_redirects = True) - self.assertIn('changed', rv.data) + rv = self._login("alice", "Alic3") + self.assertIn("Logged in", rv.data) + rv = self.client.post( + path, + data={"current": "Alic3", "new": "alice", "confirm": "alice"}, + follow_redirects=True, + ) + self.assertIn("changed", rv.data) self._logout() - rv = self._login('alice', 'alice') - self.assertIn('Logged in', rv.data) + rv = self._login("alice", "alice") + self.assertIn("Logged in", rv.data) - path = '/user/{}/changepass'.format(self.users['bob']) + path = "/user/{}/changepass".format(self.users["bob"]) rv = self.client.post(path) - self.assertIn('required', rv.data) - rv = self.client.post(path, data = { 'new': 'alice' }) - self.assertIn('password and its confirmation don', rv.data) - rv = self.client.post(path, data = { 'new': 'alice', 'confirm': 'alice' }, follow_redirects = True) - self.assertIn('changed', rv.data) + self.assertIn("required", rv.data) + rv = self.client.post(path, data={"new": "alice"}) + self.assertIn("password and its confirmation don", rv.data) + rv = self.client.post( + path, data={"new": "alice", "confirm": "alice"}, follow_redirects=True + ) + self.assertIn("changed", rv.data) self._logout() - rv = self._login('bob', 'alice') - self.assertIn('Logged in', rv.data) + rv = self._login("bob", "alice") + self.assertIn("Logged in", rv.data) def test_add_get(self): - self._login('bob', 'B0b') - rv = self.client.get('/user/add', follow_redirects = True) - self.assertIn('There\'s nothing much to see', rv.data) - self.assertNotIn('Add User', rv.data) + self._login("bob", "B0b") + rv = self.client.get("/user/add", follow_redirects=True) + self.assertIn("There's nothing much to see", rv.data) + self.assertNotIn("Add User", rv.data) self._logout() - self._login('alice', 'Alic3') - rv = self.client.get('/user/add') - self.assertIn('Add User', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/user/add") + self.assertIn("Add User", rv.data) def test_add_post(self): - self._login('alice', 'Alic3') - rv = self.client.post('/user/add') - self.assertIn('required', rv.data) - rv = self.client.post('/user/add', data = { 'user': 'user' }) - self.assertIn('Please provide a password', rv.data) - rv = self.client.post('/user/add', data = { 'passwd': 'passwd' }) - self.assertIn('required', rv.data) - rv = self.client.post('/user/add', data = { 'user': 'name', 'passwd': 'passwd' }) - self.assertIn('passwords don', rv.data) - rv = self.client.post('/user/add', data = { 'user': 'alice', 'passwd': 'passwd', 'passwd_confirm': 'passwd' }) + self._login("alice", "Alic3") + rv = self.client.post("/user/add") + self.assertIn("required", rv.data) + rv = self.client.post("/user/add", data={"user": "user"}) + self.assertIn("Please provide a password", rv.data) + rv = self.client.post("/user/add", data={"passwd": "passwd"}) + self.assertIn("required", rv.data) + rv = self.client.post("/user/add", data={"user": "name", "passwd": "passwd"}) + self.assertIn("passwords don", rv.data) + rv = self.client.post( + "/user/add", + data={"user": "alice", "passwd": "passwd", "passwd_confirm": "passwd"}, + ) self.assertIn(escape("User 'alice' exists"), rv.data) with db_session: self.assertEqual(User.select().count(), 2) - rv = self.client.post('/user/add', data = { 'user': 'user', 'passwd': 'passwd', 'passwd_confirm': 'passwd', 'admin': 1 }, follow_redirects = True) - self.assertIn('added', rv.data) + rv = self.client.post( + "/user/add", + data={ + "user": "user", + "passwd": "passwd", + "passwd_confirm": "passwd", + "admin": 1, + }, + follow_redirects=True, + ) + self.assertIn("added", rv.data) with db_session: self.assertEqual(User.select().count(), 3) self._logout() - rv = self._login('user', 'passwd') - self.assertIn('Logged in', rv.data) + rv = self._login("user", "passwd") + self.assertIn("Logged in", rv.data) def test_delete(self): - path = '/user/del/{}'.format(self.users['bob']) + path = "/user/del/{}".format(self.users["bob"]) - self._login('bob', 'B0b') - rv = self.client.get(path, follow_redirects = True) - self.assertIn('There\'s nothing much to see', rv.data) + self._login("bob", "B0b") + rv = self.client.get(path, follow_redirects=True) + self.assertIn("There's nothing much to see", rv.data) with db_session: self.assertEqual(User.select().count(), 2) self._logout() - self._login('alice', 'Alic3') - rv = self.client.get('/user/del/string', follow_redirects = True) - self.assertIn('badly formed', rv.data) - rv = self.client.get('/user/del/' + str(uuid.uuid4()), follow_redirects = True) - self.assertIn('No such user', rv.data) - rv = self.client.get(path, follow_redirects = True) - self.assertIn('Deleted', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/user/del/string", follow_redirects=True) + self.assertIn("badly formed", rv.data) + rv = self.client.get("/user/del/" + str(uuid.uuid4()), follow_redirects=True) + self.assertIn("No such user", rv.data) + rv = self.client.get(path, follow_redirects=True) + self.assertIn("Deleted", rv.data) with db_session: self.assertEqual(User.select().count(), 1) self._logout() - rv = self._login('bob', 'B0b') - self.assertIn('Wrong username or password', rv.data) + rv = self._login("bob", "B0b") + self.assertIn("Wrong username or password", rv.data) def test_lastfm_link(self): - self._login('alice', 'Alic3') - rv = self.client.get('/user/me/lastfm/link', follow_redirects = True) - self.assertIn('Missing LastFM auth token', rv.data) - rv = self.client.get('/user/me/lastfm/link', query_string = { 'token': 'abcdef' }, follow_redirects = True) - self.assertIn('No API key set', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/user/me/lastfm/link", follow_redirects=True) + self.assertIn("Missing LastFM auth token", rv.data) + rv = self.client.get( + "/user/me/lastfm/link", + query_string={"token": "abcdef"}, + follow_redirects=True, + ) + self.assertIn("No API key set", rv.data) def test_lastfm_unlink(self): - self._login('alice', 'Alic3') - rv = self.client.get('/user/me/lastfm/unlink', follow_redirects = True) - self.assertIn('Unlinked', rv.data) + self._login("alice", "Alic3") + rv = self.client.get("/user/me/lastfm/unlink", follow_redirects=True) + self.assertIn("Unlinked", rv.data) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/issue101.py b/tests/issue101.py index f176f63..189b0ca 100644 --- a/tests/issue101.py +++ b/tests/issue101.py @@ -19,37 +19,39 @@ from supysonic.db import Folder from supysonic.managers.folder import FolderManager from supysonic.scanner import Scanner + class Issue101TestCase(unittest.TestCase): def setUp(self): self.__dir = tempfile.mkdtemp() - init_database('sqlite:') + init_database("sqlite:") with db_session: - FolderManager.add('folder', self.__dir) + FolderManager.add("folder", self.__dir) def tearDown(self): release_database() shutil.rmtree(self.__dir) def test_issue(self): - firstsubdir = tempfile.mkdtemp(dir = self.__dir) + firstsubdir = tempfile.mkdtemp(dir=self.__dir) subdir = firstsubdir for _ in range(4): - subdir = tempfile.mkdtemp(dir = subdir) - shutil.copyfile('tests/assets/folder/silence.mp3', os.path.join(subdir, 'silence.mp3')) + subdir = tempfile.mkdtemp(dir=subdir) + shutil.copyfile( + "tests/assets/folder/silence.mp3", os.path.join(subdir, "silence.mp3") + ) with db_session: scanner = Scanner() - scanner.queue_folder('folder') + scanner.queue_folder("folder") scanner.run() shutil.rmtree(firstsubdir) with db_session: scanner = Scanner() - scanner.queue_folder('folder') + scanner.queue_folder("folder") scanner.run() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() - diff --git a/tests/issue129.py b/tests/issue129.py index 86879a6..8eb2930 100644 --- a/tests/issue129.py +++ b/tests/issue129.py @@ -18,35 +18,36 @@ from supysonic.scanner import Scanner from .testbase import TestBase + class Issue129TestCase(TestBase): def setUp(self): super(Issue129TestCase, self).setUp() with db_session: - folder = FolderManager.add('folder', os.path.abspath('tests/assets/folder')) + folder = FolderManager.add("folder", os.path.abspath("tests/assets/folder")) scanner = Scanner() - scanner.queue_folder('folder') + scanner.queue_folder("folder") scanner.run() self.trackid = Track.select().first().id - self.userid = User.get(name = 'alice').id + self.userid = User.get(name="alice").id def test_last_play(self): with db_session: User[self.userid].last_play = Track[self.trackid] with db_session: - FolderManager.delete_by_name('folder') + FolderManager.delete_by_name("folder") def test_starred(self): with db_session: - StarredTrack(user = self.userid, starred = self.trackid) - FolderManager.delete_by_name('folder') + StarredTrack(user=self.userid, starred=self.trackid) + FolderManager.delete_by_name("folder") def test_rating(self): with db_session: - RatingTrack(user = self.userid, rated = self.trackid, rating = 5) - FolderManager.delete_by_name('folder') + RatingTrack(user=self.userid, rated=self.trackid, rating=5) + FolderManager.delete_by_name("folder") -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/issue133.py b/tests/issue133.py index a79e5c5..076d75b 100644 --- a/tests/issue133.py +++ b/tests/issue133.py @@ -18,13 +18,14 @@ from supysonic.db import Folder, Track from supysonic.managers.folder import FolderManager from supysonic.scanner import Scanner + class Issue133TestCase(unittest.TestCase): def setUp(self): self.__dir = tempfile.mkdtemp() - shutil.copy('tests/assets/issue133.flac', self.__dir) - init_database('sqlite:') + shutil.copy("tests/assets/issue133.flac", self.__dir) + init_database("sqlite:") with db_session: - FolderManager.add('folder', self.__dir) + FolderManager.add("folder", self.__dir) def tearDown(self): release_database() @@ -33,13 +34,13 @@ class Issue133TestCase(unittest.TestCase): @db_session def test_issue133(self): scanner = Scanner() - scanner.queue_folder('folder') + scanner.queue_folder("folder") scanner.run() del scanner track = Track.select().first() - self.assertNotIn('\x00', track.title) + self.assertNotIn("\x00", track.title) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/issue139.py b/tests/issue139.py index 3e36a73..c888e34 100644 --- a/tests/issue139.py +++ b/tests/issue139.py @@ -18,12 +18,13 @@ from supysonic.db import Folder, Track from supysonic.managers.folder import FolderManager from supysonic.scanner import Scanner + class Issue139TestCase(unittest.TestCase): def setUp(self): self.__dir = tempfile.mkdtemp() - init_database('sqlite:') + init_database("sqlite:") with db_session: - FolderManager.add('folder', self.__dir) + FolderManager.add("folder", self.__dir) def tearDown(self): release_database() @@ -32,17 +33,18 @@ class Issue139TestCase(unittest.TestCase): @db_session def do_scan(self): scanner = Scanner() - scanner.queue_folder('folder') + scanner.queue_folder("folder") scanner.run() del scanner def test_null_genre(self): - shutil.copy('tests/assets/issue139.mp3', self.__dir) + shutil.copy("tests/assets/issue139.mp3", self.__dir) self.do_scan() def test_float_bitrate(self): - shutil.copy('tests/assets/issue139.aac', self.__dir) + shutil.copy("tests/assets/issue139.aac", self.__dir) self.do_scan() -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/tests/issue148.py b/tests/issue148.py index 15bfb46..b4cbfdc 100644 --- a/tests/issue148.py +++ b/tests/issue148.py @@ -19,28 +19,30 @@ from supysonic.db import Folder from supysonic.managers.folder import FolderManager from supysonic.scanner import Scanner + class Issue148TestCase(unittest.TestCase): def setUp(self): self.__dir = tempfile.mkdtemp() - init_database('sqlite:') + init_database("sqlite:") with db_session: - FolderManager.add('folder', self.__dir) + FolderManager.add("folder", self.__dir) def tearDown(self): release_database() shutil.rmtree(self.__dir) def test_issue(self): - subdir = os.path.join(self.__dir, ' ') + subdir = os.path.join(self.__dir, " ") os.makedirs(subdir) - shutil.copyfile('tests/assets/folder/silence.mp3', os.path.join(subdir, 'silence.mp3')) + shutil.copyfile( + "tests/assets/folder/silence.mp3", os.path.join(subdir, "silence.mp3") + ) scanner = Scanner() - scanner.queue_folder('folder') + scanner.queue_folder("folder") scanner.run() del scanner -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() - diff --git a/tests/managers/__init__.py b/tests/managers/__init__.py index a454aa2..b547c90 100644 --- a/tests/managers/__init__.py +++ b/tests/managers/__init__.py @@ -13,6 +13,7 @@ import unittest from .test_manager_folder import FolderManagerTestCase from .test_manager_user import UserManagerTestCase + def suite(): suite = unittest.TestSuite() @@ -20,4 +21,3 @@ def suite(): suite.addTest(unittest.makeSuite(UserManagerTestCase)) return suite - diff --git a/tests/managers/test_manager_folder.py b/tests/managers/test_manager_folder.py index 9198057..84e9dd1 100644 --- a/tests/managers/test_manager_folder.py +++ b/tests/managers/test_manager_folder.py @@ -20,10 +20,11 @@ import uuid from pony.orm import db_session, ObjectNotFound + class FolderManagerTestCase(unittest.TestCase): def setUp(self): # Create an empty sqlite database in memory - db.init_database('sqlite:') + db.init_database("sqlite:") # Create some temporary directories self.media_dir = tempfile.mkdtemp() @@ -36,31 +37,29 @@ class FolderManagerTestCase(unittest.TestCase): def create_folders(self): # Add test folders - self.assertIsNotNone(FolderManager.add('media', self.media_dir)) - self.assertIsNotNone(FolderManager.add('music', self.music_dir)) + self.assertIsNotNone(FolderManager.add("media", self.media_dir)) + self.assertIsNotNone(FolderManager.add("music", self.music_dir)) folder = db.Folder( - root = False, - name = 'non-root', - path = os.path.join(self.music_dir, 'subfolder') + root=False, name="non-root", path=os.path.join(self.music_dir, "subfolder") ) - artist = db.Artist(name = 'Artist') - album = db.Album(name = 'Album', artist = artist) + artist = db.Artist(name="Artist") + album = db.Album(name="Album", artist=artist) - root = db.Folder.get(name = 'media') + root = db.Folder.get(name="media") track = db.Track( - title = 'Track', - artist = artist, - album = album, - disc = 1, - number = 1, - path = os.path.join(self.media_dir, 'somefile'), - folder = root, - root_folder = root, - duration = 2, - bitrate = 320, - last_modification = 0 + title="Track", + artist=artist, + album=album, + disc=1, + number=1, + path=os.path.join(self.media_dir, "somefile"), + folder=root, + root_folder=root, + duration=2, + bitrate=320, + last_modification=0, ) @db_session @@ -68,13 +67,13 @@ class FolderManagerTestCase(unittest.TestCase): self.create_folders() # Get existing folders - for name in ['media', 'music']: - folder = db.Folder.get(name = name, root = True) + for name in ["media", "music"]: + folder = db.Folder.get(name=name, root=True) self.assertEqual(FolderManager.get(folder.id), folder) # Get with invalid UUID - self.assertRaises(ValueError, FolderManager.get, 'invalid-uuid') - self.assertRaises(ValueError, FolderManager.get, 0xdeadbeef) + self.assertRaises(ValueError, FolderManager.get, "invalid-uuid") + self.assertRaises(ValueError, FolderManager.get, 0xDEADBEEF) # Non-existent folder self.assertRaises(ObjectNotFound, FolderManager.get, uuid.uuid4()) @@ -85,27 +84,29 @@ class FolderManagerTestCase(unittest.TestCase): self.assertEqual(db.Folder.select().count(), 3) # Create duplicate - self.assertRaises(ValueError, FolderManager.add, 'media', self.media_dir) - self.assertEqual(db.Folder.select(lambda f: f.name == 'media').count(), 1) + self.assertRaises(ValueError, FolderManager.add, "media", self.media_dir) + self.assertEqual(db.Folder.select(lambda f: f.name == "media").count(), 1) # Duplicate path - self.assertRaises(ValueError, FolderManager.add, 'new-folder', self.media_dir) - self.assertEqual(db.Folder.select(lambda f: f.path == self.media_dir).count(), 1) + self.assertRaises(ValueError, FolderManager.add, "new-folder", self.media_dir) + self.assertEqual( + db.Folder.select(lambda f: f.path == self.media_dir).count(), 1 + ) # Invalid path - path = os.path.abspath('/this/not/is/valid') - self.assertRaises(ValueError, FolderManager.add, 'invalid-path', path) - self.assertFalse(db.Folder.exists(path = path)) + path = os.path.abspath("/this/not/is/valid") + self.assertRaises(ValueError, FolderManager.add, "invalid-path", path) + self.assertFalse(db.Folder.exists(path=path)) # Subfolder of already added path - path = os.path.join(self.media_dir, 'subfolder') + path = os.path.join(self.media_dir, "subfolder") os.mkdir(path) - self.assertRaises(ValueError, FolderManager.add, 'subfolder', path) + self.assertRaises(ValueError, FolderManager.add, "subfolder", path) self.assertEqual(db.Folder.select().count(), 3) # Parent folder of an already added path - path = os.path.join(self.media_dir, '..') - self.assertRaises(ValueError, FolderManager.add, 'parent', path) + path = os.path.join(self.media_dir, "..") + self.assertRaises(ValueError, FolderManager.add, "parent", path) self.assertEqual(db.Folder.select().count(), 3) def test_delete_folder(self): @@ -114,7 +115,7 @@ class FolderManagerTestCase(unittest.TestCase): with db_session: # Delete invalid UUID - self.assertRaises(ValueError, FolderManager.delete, 'invalid-uuid') + self.assertRaises(ValueError, FolderManager.delete, "invalid-uuid") self.assertEqual(db.Folder.select().count(), 3) # Delete non-existent folder @@ -122,14 +123,14 @@ class FolderManagerTestCase(unittest.TestCase): self.assertEqual(db.Folder.select().count(), 3) # Delete non-root folder - folder = db.Folder.get(name = 'non-root') + folder = db.Folder.get(name="non-root") self.assertRaises(ObjectNotFound, FolderManager.delete, folder.id) self.assertEqual(db.Folder.select().count(), 3) with db_session: # Delete existing folders - for name in ['media', 'music']: - folder = db.Folder.get(name = name, root = True) + for name in ["media", "music"]: + folder = db.Folder.get(name=name, root=True) FolderManager.delete(folder.id) self.assertRaises(ObjectNotFound, db.Folder.__getitem__, folder.id) @@ -142,15 +143,15 @@ class FolderManagerTestCase(unittest.TestCase): with db_session: # Delete non-existent folder - self.assertRaises(ObjectNotFound, FolderManager.delete_by_name, 'null') + self.assertRaises(ObjectNotFound, FolderManager.delete_by_name, "null") self.assertEqual(db.Folder.select().count(), 3) with db_session: # Delete existing folders - for name in ['media', 'music']: + for name in ["media", "music"]: FolderManager.delete_by_name(name) - self.assertFalse(db.Folder.exists(name = name)) + self.assertFalse(db.Folder.exists(name=name)) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/managers/test_manager_user.py b/tests/managers/test_manager_user.py index 07a5b89..a8a6946 100644 --- a/tests/managers/test_manager_user.py +++ b/tests/managers/test_manager_user.py @@ -20,10 +20,11 @@ import uuid from pony.orm import db_session, commit from pony.orm import ObjectNotFound + class UserManagerTestCase(unittest.TestCase): def setUp(self): # Create an empty sqlite database in memory - db.init_database('sqlite:') + db.init_database("sqlite:") def tearDown(self): db.release_database() @@ -31,51 +32,62 @@ class UserManagerTestCase(unittest.TestCase): @db_session def create_data(self): # Create some users - self.assertIsInstance(UserManager.add('alice', 'ALICE', 'test@example.com', True), db.User) - self.assertIsInstance(UserManager.add('bob', 'BOB', 'bob@example.com', False), db.User) - self.assertIsInstance(UserManager.add('charlie', 'CHARLIE', 'charlie@example.com', False), db.User) + self.assertIsInstance( + UserManager.add("alice", "ALICE", "test@example.com", True), db.User + ) + self.assertIsInstance( + UserManager.add("bob", "BOB", "bob@example.com", False), db.User + ) + self.assertIsInstance( + UserManager.add("charlie", "CHARLIE", "charlie@example.com", False), db.User + ) - folder = db.Folder(name = 'Root', path = 'tests/assets', root = True) - artist = db.Artist(name = 'Artist') - album = db.Album(name = 'Album', artist = artist) + folder = db.Folder(name="Root", path="tests/assets", root=True) + artist = db.Artist(name="Artist") + album = db.Album(name="Album", artist=artist) track = db.Track( - title = 'Track', - disc = 1, - number = 1, - duration = 1, - artist = artist, - album = album, - path = 'tests/assets/empty', - folder = folder, - root_folder = folder, - bitrate = 320, - last_modification = 0 + title="Track", + disc=1, + number=1, + duration=1, + artist=artist, + album=album, + path="tests/assets/empty", + folder=folder, + root_folder=folder, + bitrate=320, + last_modification=0, ) - playlist = db.Playlist( - name = 'Playlist', - user = db.User.get(name = 'alice') - ) + playlist = db.Playlist(name="Playlist", user=db.User.get(name="alice")) playlist.add(track) def test_encrypt_password(self): func = UserManager._UserManager__encrypt_password - self.assertEqual(func('password','salt'), ('59b3e8d637cf97edbe2384cf59cb7453dfe30789', 'salt')) - self.assertEqual(func('pass-word','pepper'), ('d68c95a91ed7773aa57c7c044d2309a5bf1da2e7', 'pepper')) - self.assertEqual(func(u'éèàïô', 'ABC+'), ('b639ba5217b89c906019d89d5816b407d8730898', 'ABC+')) + self.assertEqual( + func("password", "salt"), + ("59b3e8d637cf97edbe2384cf59cb7453dfe30789", "salt"), + ) + self.assertEqual( + func("pass-word", "pepper"), + ("d68c95a91ed7773aa57c7c044d2309a5bf1da2e7", "pepper"), + ) + self.assertEqual( + func(u"éèàïô", "ABC+"), ("b639ba5217b89c906019d89d5816b407d8730898", "ABC+") + ) @db_session def test_get_user(self): self.create_data() # Get existing users - for name in ['alice', 'bob', 'charlie']: - user = db.User.get(name = name) + for name in ["alice", "bob", "charlie"]: + user = db.User.get(name=name) self.assertEqual(UserManager.get(user.id), user) # Get with invalid UUID - self.assertRaises(ValueError, UserManager.get, 'invalid-uuid') - self.assertRaises(ValueError, UserManager.get, 0xfee1bad) + self.assertRaises(ValueError, UserManager.get, "invalid-uuid") + self.assertRaises(ValueError, UserManager.get, 0xFEE1BAD) # Non-existent user self.assertRaises(ObjectNotFound, UserManager.get, uuid.uuid4()) @@ -86,15 +98,17 @@ class UserManagerTestCase(unittest.TestCase): self.assertEqual(db.User.select().count(), 3) # Create duplicate - self.assertRaises(ValueError, UserManager.add, 'alice', 'Alic3', 'alice@example.com', True) + self.assertRaises( + ValueError, UserManager.add, "alice", "Alic3", "alice@example.com", True + ) @db_session def test_delete_user(self): self.create_data() # Delete invalid UUID - self.assertRaises(ValueError, UserManager.delete, 'invalid-uuid') - self.assertRaises(ValueError, UserManager.delete, 0xfee1b4d) + self.assertRaises(ValueError, UserManager.delete, "invalid-uuid") + self.assertRaises(ValueError, UserManager.delete, 0xFEE1B4D) self.assertEqual(db.User.select().count(), 3) # Delete non-existent user @@ -102,8 +116,8 @@ class UserManagerTestCase(unittest.TestCase): self.assertEqual(db.User.select().count(), 3) # Delete existing users - for name in ['alice', 'bob', 'charlie']: - user = db.User.get(name = name) + for name in ["alice", "bob", "charlie"]: + user = db.User.get(name=name) UserManager.delete(user.id) self.assertRaises(ObjectNotFound, db.User.__getitem__, user.id) commit() @@ -114,68 +128,84 @@ class UserManagerTestCase(unittest.TestCase): self.create_data() # Delete existing users - for name in ['alice', 'bob', 'charlie']: + for name in ["alice", "bob", "charlie"]: UserManager.delete_by_name(name) - self.assertFalse(db.User.exists(name = name)) + self.assertFalse(db.User.exists(name=name)) # Delete non-existent user - self.assertRaises(ObjectNotFound, UserManager.delete_by_name, 'null') + self.assertRaises(ObjectNotFound, UserManager.delete_by_name, "null") @db_session def test_try_auth(self): self.create_data() # Test authentication - for name in ['alice', 'bob', 'charlie']: - user = db.User.get(name = name) + for name in ["alice", "bob", "charlie"]: + user = db.User.get(name=name) authed = UserManager.try_auth(name, name.upper()) self.assertEqual(authed, user) # Wrong password - self.assertIsNone(UserManager.try_auth('alice', 'bad')) - self.assertIsNone(UserManager.try_auth('alice', 'alice')) + self.assertIsNone(UserManager.try_auth("alice", "bad")) + self.assertIsNone(UserManager.try_auth("alice", "alice")) # Non-existent user - self.assertIsNone(UserManager.try_auth('null', 'null')) + self.assertIsNone(UserManager.try_auth("null", "null")) @db_session def test_change_password(self): self.create_data() # With existing users - for name in ['alice', 'bob', 'charlie']: - user = db.User.get(name = name) + for name in ["alice", "bob", "charlie"]: + user = db.User.get(name=name) # Good password - UserManager.change_password(user.id, name.upper(), 'newpass') - self.assertEqual(UserManager.try_auth(name, 'newpass'), user) + UserManager.change_password(user.id, name.upper(), "newpass") + self.assertEqual(UserManager.try_auth(name, "newpass"), user) # Old password self.assertEqual(UserManager.try_auth(name, name.upper()), None) # Wrong password - self.assertRaises(ValueError, UserManager.change_password, user.id, 'badpass', 'newpass') + self.assertRaises( + ValueError, UserManager.change_password, user.id, "badpass", "newpass" + ) # Ensure we still got the same number of users self.assertEqual(db.User.select().count(), 3) # With invalid UUID - self.assertRaises(ValueError, UserManager.change_password, 'invalid-uuid', 'oldpass', 'newpass') + self.assertRaises( + ValueError, + UserManager.change_password, + "invalid-uuid", + "oldpass", + "newpass", + ) # Non-existent user - self.assertRaises(ObjectNotFound, UserManager.change_password, uuid.uuid4(), 'oldpass', 'newpass') + self.assertRaises( + ObjectNotFound, + UserManager.change_password, + uuid.uuid4(), + "oldpass", + "newpass", + ) @db_session def test_change_password2(self): self.create_data() # With existing users - for name in ['alice', 'bob', 'charlie']: - UserManager.change_password2(name, 'newpass') - user = db.User.get(name = name) - self.assertEqual(UserManager.try_auth(name, 'newpass'), user) + for name in ["alice", "bob", "charlie"]: + UserManager.change_password2(name, "newpass") + user = db.User.get(name=name) + self.assertEqual(UserManager.try_auth(name, "newpass"), user) self.assertEqual(UserManager.try_auth(name, name.upper()), None) # Non-existent user - self.assertRaises(ObjectNotFound, UserManager.change_password2, 'null', 'newpass') + self.assertRaises( + ObjectNotFound, UserManager.change_password2, "null", "newpass" + ) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() - diff --git a/tests/testbase.py b/tests/testbase.py index dbbdc81..9acbc37 100644 --- a/tests/testbase.py +++ b/tests/testbase.py @@ -20,18 +20,16 @@ from supysonic.config import DefaultConfig from supysonic.managers.user import UserManager from supysonic.web import create_application + class TestConfig(DefaultConfig): TESTING = True - LOGGER_HANDLER_POLICY = 'never' - MIMETYPES = { - 'mp3': 'audio/mpeg', - 'weirdextension': 'application/octet-stream' - } + LOGGER_HANDLER_POLICY = "never" + MIMETYPES = {"mp3": "audio/mpeg", "weirdextension": "application/octet-stream"} TRANSCODING = { - 'transcoder_mp3_mp3': 'echo -n %srcpath %outrate', - 'decoder_mp3': 'echo -n Pushing out some mp3 data...', - 'encoder_cat': 'cat -', - 'encoder_md5': 'md5sum' + "transcoder_mp3_mp3": "echo -n %srcpath %outrate", + "decoder_mp3": "echo -n Pushing out some mp3 data...", + "encoder_cat": "cat -", + "encoder_md5": "md5sum", } def __init__(self, with_webui, with_api): @@ -39,7 +37,7 @@ class TestConfig(DefaultConfig): for cls in reversed(inspect.getmro(self.__class__)): for attr, value in cls.__dict__.items(): - if attr.startswith('_') or attr != attr.upper(): + if attr.startswith("_") or attr != attr.upper(): continue if isinstance(value, dict): @@ -47,15 +45,13 @@ class TestConfig(DefaultConfig): else: setattr(self, attr, value) - self.WEBAPP.update({ - 'mount_webui': with_webui, - 'mount_api': with_api - }) + self.WEBAPP.update({"mount_webui": with_webui, "mount_api": with_api}) + class MockResponse(object): def __init__(self, response): self.__status_code = response.status_code - self.__data = response.get_data(as_text = True) + self.__data = response.get_data(as_text=True) self.__mimetype = response.mimetype @property @@ -70,14 +66,17 @@ class MockResponse(object): def mimetype(self): return self.__mimetype + def patch_method(f): original = f + def patched(*args, **kwargs): rv = original(*args, **kwargs) return MockResponse(rv) return patched + class TestBase(unittest.TestCase): __with_webui__ = False __with_api__ = False @@ -86,18 +85,18 @@ class TestBase(unittest.TestCase): self.__dbfile = tempfile.mkstemp()[1] self.__dir = tempfile.mkdtemp() config = TestConfig(self.__with_webui__, self.__with_api__) - config.BASE['database_uri'] = 'sqlite:///' + self.__dbfile - config.WEBAPP['cache_dir'] = self.__dir + config.BASE["database_uri"] = "sqlite:///" + self.__dbfile + config.WEBAPP["cache_dir"] = self.__dir - init_database(config.BASE['database_uri']) + init_database(config.BASE["database_uri"]) release_database() self.__app = create_application(config) self.client = self.__app.test_client() with db_session: - UserManager.add('alice', 'Alic3', 'test@example.com', True) - UserManager.add('bob', 'B0b', 'bob@example.com', False) + UserManager.add("alice", "Alic3", "test@example.com", True) + UserManager.add("bob", "B0b", "bob@example.com", False) def _patch_client(self): self.client.get = patch_method(self.client.get) @@ -110,4 +109,3 @@ class TestBase(unittest.TestCase): release_database() shutil.rmtree(self.__dir) os.remove(self.__dbfile) - diff --git a/tests/utils.py b/tests/utils.py index b1df541..a095571 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -9,6 +9,6 @@ import binascii -def hexlify(s): - return binascii.hexlify(s.encode('utf-8')).decode('utf-8') +def hexlify(s): + return binascii.hexlify(s.encode("utf-8")).decode("utf-8")