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

Runnig black on everything

This commit is contained in:
Alban Féron 2019-06-29 17:25:44 +02:00
parent 7966f767ca
commit 7c8a75d45c
No known key found for this signature in database
GPG Key ID: 8CE0313646D16165
82 changed files with 4158 additions and 2883 deletions

View File

@ -15,49 +15,51 @@ from setuptools import setup
from setuptools import find_packages from setuptools import find_packages
reqs = [ reqs = [
'flask>=0.11', "flask>=0.11",
'pony>=0.7.6', "pony>=0.7.6",
'Pillow', "Pillow",
'requests>=1.0.0', "requests>=1.0.0",
'mutagen>=1.33', "mutagen>=1.33",
'scandir<2.0.0', "scandir<2.0.0",
'watchdog>=0.8.0', "watchdog>=0.8.0",
'zipstream' "zipstream",
] ]
setup( setup(
name=project.NAME, name=project.NAME,
version=project.VERSION, version=project.VERSION,
description=project.DESCRIPTION, description=project.DESCRIPTION,
keywords=project.KEYWORDS, keywords=project.KEYWORDS,
long_description=project.LONG_DESCRIPTION, long_description=project.LONG_DESCRIPTION,
author=project.AUTHOR_NAME, author=project.AUTHOR_NAME,
author_email=project.AUTHOR_EMAIL, author_email=project.AUTHOR_EMAIL,
url=project.URL, url=project.URL,
license=project.LICENSE, license=project.LICENSE,
packages=find_packages(exclude=['tests*']), packages=find_packages(exclude=["tests*"]),
install_requires = reqs, install_requires=reqs,
entry_points={ 'console_scripts': [ entry_points={
'supysonic-cli=supysonic.cli:main', "console_scripts": [
'supysonic-daemon=supysonic.daemon:main' "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'
] ]
) },
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",
],
)

View File

@ -8,15 +8,15 @@
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
NAME = 'supysonic' NAME = "supysonic"
VERSION = '0.4' VERSION = "0.4"
DESCRIPTION = 'Python implementation of the Subsonic server API.' DESCRIPTION = "Python implementation of the Subsonic server API."
KEYWORDS = 'subsonic music api' KEYWORDS = "subsonic music api"
AUTHOR_NAME = 'Alban Féron' AUTHOR_NAME = "Alban Féron"
AUTHOR_EMAIL = 'alban.feron@gmail.com' AUTHOR_EMAIL = "alban.feron@gmail.com"
URL = 'https://github.com/spl0k/supysonic' URL = "https://github.com/spl0k/supysonic"
LICENSE = 'GNU AGPLv3' LICENSE = "GNU AGPLv3"
LONG_DESCRIPTION = '''Supysonic is a Python implementation of the Subsonic server API. LONG_DESCRIPTION = """Supysonic is a Python implementation of the Subsonic server API.
Current supported features are: Current supported features are:
* browsing (by folders or tags) * browsing (by folders or tags)
* streaming of various audio file formats * streaming of various audio file formats
@ -24,4 +24,4 @@ Current supported features are:
* user or random playlists * user or random playlists
* cover arts (cover.jpg files in the same folder as music files) * cover arts (cover.jpg files in the same folder as music files)
* starred tracks/albums and ratings * starred tracks/albums and ratings
* Last.FM scrobbling''' * Last.FM scrobbling"""

View File

@ -7,7 +7,7 @@
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
API_VERSION = '1.9.0' API_VERSION = "1.9.0"
import binascii import binascii
import uuid import uuid
@ -23,39 +23,44 @@ from ..py23 import dict
from .exceptions import Unauthorized from .exceptions import Unauthorized
from .formatters import JSONFormatter, JSONPFormatter, XMLFormatter from .formatters import JSONFormatter, JSONPFormatter, XMLFormatter
api = Blueprint('api', __name__) api = Blueprint("api", __name__)
@api.before_request @api.before_request
def set_formatter(): def set_formatter():
"""Return a function to create the response.""" """Return a function to create the response."""
f, callback = map(request.values.get, ['f', 'callback']) f, callback = map(request.values.get, ["f", "callback"])
if f == 'jsonp': if f == "jsonp":
request.formatter = JSONPFormatter(callback) request.formatter = JSONPFormatter(callback)
elif f == 'json': elif f == "json":
request.formatter = JSONFormatter() request.formatter = JSONFormatter()
else: else:
request.formatter = XMLFormatter() request.formatter = XMLFormatter()
def decode_password(password): def decode_password(password):
if not password.startswith('enc:'): if not password.startswith("enc:"):
return password return password
try: try:
return binascii.unhexlify(password[4:].encode('utf-8')).decode('utf-8') return binascii.unhexlify(password[4:].encode("utf-8")).decode("utf-8")
except: except:
return password return password
@api.before_request @api.before_request
def authorize(): def authorize():
if request.authorization: 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: if user is not None:
request.user = user request.user = user
return return
raise Unauthorized() raise Unauthorized()
username = request.values['u'] username = request.values["u"]
password = request.values['p'] password = request.values["p"]
password = decode_password(password) password = decode_password(password)
user = UserManager.try_auth(username, password) user = UserManager.try_auth(username, password)
@ -64,21 +69,24 @@ def authorize():
request.user = user request.user = user
@api.before_request @api.before_request
def get_client_prefs(): def get_client_prefs():
client = request.values['c'] client = request.values["c"]
try: try:
request.client = ClientPrefs[request.user, client] request.client = ClientPrefs[request.user, client]
except ObjectNotFound: except ObjectNotFound:
request.client = ClientPrefs(user = request.user, client_name = client) request.client = ClientPrefs(user=request.user, client_name=client)
commit() commit()
def get_entity(cls, param = 'id'):
def get_entity(cls, param="id"):
eid = request.values[param] eid = request.values[param]
eid = uuid.UUID(eid) eid = uuid.UUID(eid)
entity = cls[eid] entity = cls[eid]
return entity return entity
from .errors import * from .errors import *
from .system import * from .system import *
@ -91,4 +99,3 @@ from .chat import *
from .search import * from .search import *
from .playlists import * from .playlists import *
from .unsupported import * from .unsupported import *

View File

@ -13,17 +13,31 @@ from datetime import timedelta
from flask import request from flask import request
from pony.orm import select, desc, avg, max, min, count 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 ..db import now
from ..py23 import dict from ..py23 import dict
from . import api from . import api
from .exceptions import GenericError, NotFound from .exceptions import GenericError, NotFound
@api.route('/getRandomSongs.view', methods = [ 'GET', 'POST' ])
@api.route("/getRandomSongs.view", methods=["GET", "POST"])
def rand_songs(): def rand_songs():
size = request.values.get('size', '10') size = request.values.get("size", "10")
genre, fromYear, toYear, musicFolderId = map(request.values.get, [ 'genre', 'fromYear', 'toYear', 'musicFolderId' ]) genre, fromYear, toYear, musicFolderId = map(
request.values.get, ["genre", "fromYear", "toYear", "musicFolderId"]
)
size = int(size) if size else 10 size = int(size) if size else 10
fromYear = int(fromYear) if fromYear else None fromYear = int(fromYear) if fromYear else None
@ -38,120 +52,196 @@ def rand_songs():
if genre: if genre:
query = query.filter(lambda t: t.genre == genre) query = query.filter(lambda t: t.genre == genre)
if fid: if fid:
if not Folder.exists(id = fid, root = True): if not Folder.exists(id=fid, root=True):
raise NotFound('Folder') raise NotFound("Folder")
query = query.filter(lambda t: t.root_folder.id == fid) query = query.filter(lambda t: t.root_folder.id == fid)
return request.formatter('randomSongs', dict( return request.formatter(
song = [ t.as_subsonic_child(request.user, request.client) for t in query.without_distinct().random(size) ] "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(): 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 size = int(size) if size else 10
offset = int(offset) if offset else 0 offset = int(offset) if offset else 0
query = select(t.folder for t in Track) query = select(t.folder for t in Track)
if ltype == 'random': if ltype == "random":
return request.formatter('albumList', dict( return request.formatter(
album = [ a.as_subsonic_child(request.user) for a in query.without_distinct().random(size) ] "albumList",
)) dict(
elif ltype == 'newest': 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)) query = query.order_by(desc(Folder.created))
elif ltype == 'highest': elif ltype == "highest":
query = query.order_by(lambda f: desc(avg(f.ratings.rating))) 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))) query = query.order_by(lambda f: desc(avg(f.tracks.play_count)))
elif ltype == 'recent': 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))) query = select(
elif ltype == 'starred': t.folder for t in Track if max(t.folder.tracks.last_play) is not None
query = select(s.starred for s in StarredFolder if s.user.id == request.user.id and count(s.starred.tracks) > 0) ).order_by(lambda f: desc(max(f.tracks.last_play)))
elif ltype == 'alphabeticalByName': 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) query = query.order_by(Folder.name)
elif ltype == 'alphabeticalByArtist': elif ltype == "alphabeticalByArtist":
query = query.order_by(lambda f: f.parent.name + f.name) query = query.order_by(lambda f: f.parent.name + f.name)
else: else:
raise GenericError('Unknown search type') raise GenericError("Unknown search type")
return request.formatter('albumList', dict( return request.formatter(
album = [ f.as_subsonic_child(request.user) for f in query.limit(size, offset) ] "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(): 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 size = int(size) if size else 10
offset = int(offset) if offset else 0 offset = int(offset) if offset else 0
query = Album.select() query = Album.select()
if ltype == 'random': if ltype == "random":
return request.formatter('albumList2', dict( return request.formatter(
album = [ a.as_subsonic_album(request.user) for a in query.random(size) ] "albumList2",
)) dict(album=[a.as_subsonic_album(request.user) for a in query.random(size)]),
elif ltype == 'newest': )
elif ltype == "newest":
query = query.order_by(lambda a: desc(min(a.tracks.created))) 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))) query = query.order_by(lambda a: desc(avg(a.tracks.play_count)))
elif ltype == 'recent': 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))) query = Album.select(lambda a: max(a.tracks.last_play) is not None).order_by(
elif ltype == 'starred': 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) 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) query = query.order_by(Album.name)
elif ltype == 'alphabeticalByArtist': elif ltype == "alphabeticalByArtist":
query = query.order_by(lambda a: a.artist.name + a.name) query = query.order_by(lambda a: a.artist.name + a.name)
else: else:
raise GenericError('Unknown search type') raise GenericError("Unknown search type")
return request.formatter('albumList2', dict( return request.formatter(
album = [ f.as_subsonic_album(request.user) for f in query.limit(size, offset) ] "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(): 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 count = int(count) if count else 10
offset = int(offset) if offset else 0 offset = int(offset) if offset else 0
query = select(t for t in Track if t.genre == genre).limit(count, offset) query = select(t for t in Track if t.genre == genre).limit(count, offset)
return request.formatter('songsByGenre', dict( return request.formatter(
song = [ t.as_subsonic_child(request.user, request.client) for t in query ] "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(): 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( return request.formatter(
entry = [ dict( "nowPlaying",
u.last_play.as_subsonic_child(request.user, request.client), dict(
username = u.name, minutesAgo = (now() - u.last_play_date).seconds / 60, playerId = 0 entry=[
) for u in query ] 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(): def get_starred():
folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id) folders = select(s.starred for s in StarredFolder if s.user.id == request.user.id)
return request.formatter('starred', dict( return request.formatter(
artist = [ dict(id = str(sf.id), name = sf.name) for sf in folders.filter(lambda f: count(f.tracks) == 0) ], "starred",
album = [ sf.as_subsonic_child(request.user) for sf in folders.filter(lambda f: count(f.tracks) > 0) ], dict(
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) ] 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(): def get_starred_id3():
return request.formatter('starred2', dict( return request.formatter(
artist = [ sa.as_subsonic_artist(request.user) for sa in select(s.starred for s in StarredArtist if s.user.id == request.user.id) ], "starred2",
album = [ sa.as_subsonic_album(request.user) for sa in select(s.starred for s in StarredAlbum if s.user.id == request.user.id) ], dict(
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) ] 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
)
],
),
)

View File

@ -24,6 +24,7 @@ from ..py23 import dict
from . import api, get_entity from . import api, get_entity
from .exceptions import AggregateException, GenericError, MissingParameter, NotFound from .exceptions import AggregateException, GenericError, MissingParameter, NotFound
def star_single(cls, eid): def star_single(cls, eid):
""" Stars an entity """ Stars an entity
@ -34,14 +35,15 @@ def star_single(cls, eid):
uid = uuid.UUID(eid) uid = uuid.UUID(eid)
e = cls[uid] e = cls[uid]
starred_cls = getattr(sys.modules[__name__], 'Starred' + cls.__name__) starred_cls = getattr(sys.modules[__name__], "Starred" + cls.__name__)
try: try:
starred_cls[request.user, uid] starred_cls[request.user, uid]
raise GenericError('{} {} already starred'.format(cls.__name__, eid)) raise GenericError("{} {} already starred".format(cls.__name__, eid))
except ObjectNotFound: except ObjectNotFound:
pass pass
starred_cls(user = request.user, starred = e) starred_cls(user=request.user, starred=e)
def unstar_single(cls, eid): def unstar_single(cls, eid):
""" Unstars an entity """ Unstars an entity
@ -51,15 +53,18 @@ def unstar_single(cls, eid):
""" """
uid = uuid.UUID(eid) uid = uuid.UUID(eid)
starred_cls = getattr(sys.modules[__name__], 'Starred' + cls.__name__) 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) delete(
s for s in starred_cls if s.user.id == request.user.id and s.starred.id == uid
)
return None return None
def handle_star_request(func): 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: if not id and not albumId and not artistId:
raise MissingParameter('id, albumId or artistId') raise MissingParameter("id, albumId or artistId")
errors = [] errors = []
for eid in id: for eid in id:
@ -76,7 +81,7 @@ def handle_star_request(func):
ferr = e ferr = e
if terr and ferr: if terr and ferr:
errors += [ terr, ferr ] errors += [terr, ferr]
for alId in albumId: for alId in albumId:
try: try:
@ -94,28 +99,37 @@ def handle_star_request(func):
raise AggregateException(errors) raise AggregateException(errors)
return request.formatter.empty return request.formatter.empty
@api.route('/star.view', methods = [ 'GET', 'POST' ])
@api.route("/star.view", methods=["GET", "POST"])
def star(): def star():
return handle_star_request(star_single) return handle_star_request(star_single)
@api.route('/unstar.view', methods = [ 'GET', 'POST' ])
@api.route("/unstar.view", methods=["GET", "POST"])
def unstar(): def unstar():
return handle_star_request(unstar_single) return handle_star_request(unstar_single)
@api.route('/setRating.view', methods = [ 'GET', 'POST' ])
@api.route("/setRating.view", methods=["GET", "POST"])
def rate(): def rate():
id = request.values['id'] id = request.values["id"]
rating = request.values['rating'] rating = request.values["rating"]
uid = uuid.UUID(id) uid = uuid.UUID(id)
rating = int(rating) rating = int(rating)
if not 0 <= rating <= 5: 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: if rating == 0:
delete(r for r in RatingTrack if r.user.id == request.user.id and r.rated.id == uid) delete(
delete(r for r in RatingFolder if r.user.id == request.user.id and r.rated.id == uid) 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: else:
try: try:
rated = Track[uid] rated = Track[uid]
@ -125,28 +139,28 @@ def rate():
rated = Folder[uid] rated = Folder[uid]
rating_cls = RatingFolder rating_cls = RatingFolder
except ObjectNotFound: except ObjectNotFound:
raise NotFound('Track or Folder') raise NotFound("Track or Folder")
try: try:
rating_info = rating_cls[request.user, uid] rating_info = rating_cls[request.user, uid]
rating_info.rating = rating rating_info.rating = rating
except ObjectNotFound: except ObjectNotFound:
rating_cls(user = request.user, rated = rated, rating = rating) rating_cls(user=request.user, rated=rated, rating=rating)
return request.formatter.empty return request.formatter.empty
@api.route('/scrobble.view', methods = [ 'GET', 'POST' ])
@api.route("/scrobble.view", methods=["GET", "POST"])
def scrobble(): def scrobble():
res = get_entity(Track) 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()) 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) lfm.scrobble(res, t)
else: else:
lfm.now_playing(res) lfm.now_playing(res)
return request.formatter.empty return request.formatter.empty

View File

@ -18,19 +18,24 @@ from ..py23 import dict
from . import api, get_entity 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(): def list_indexes():
musicFolderId = request.values.get('musicFolderId') musicFolderId = request.values.get("musicFolderId")
ifModifiedSince = request.values.get('ifModifiedSince') ifModifiedSince = request.values.get("ifModifiedSince")
if ifModifiedSince: if ifModifiedSince:
ifModifiedSince = int(ifModifiedSince) / 1000 ifModifiedSince = int(ifModifiedSince) / 1000
@ -42,11 +47,11 @@ def list_indexes():
if not folder.root: if not folder.root:
raise ObjectNotFound(Folder, mfid) raise ObjectNotFound(Folder, mfid)
folders = [ folder ] folders = [folder]
last_modif = max(map(lambda f: f.last_scan, folders)) last_modif = max(map(lambda f: f.last_scan, folders))
if ifModifiedSince is not None and last_modif < ifModifiedSince: 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 # The XSD lies, we don't return artists but a directory structure
artists = [] artists = []
@ -59,89 +64,132 @@ def list_indexes():
for artist in artists: for artist in artists:
index = artist.name[0].upper() index = artist.name[0].upper()
if index in string.digits: if index in string.digits:
index = '#' index = "#"
elif index not in string.ascii_letters: elif index not in string.ascii_letters:
index = '?' index = "?"
if index not in indexes: if index not in indexes:
indexes[index] = [] indexes[index] = []
indexes[index].append(artist) indexes[index].append(artist)
return request.formatter('indexes', dict( return request.formatter(
lastModified = last_modif * 1000, "indexes",
index = [ dict( dict(
name = k, lastModified=last_modif * 1000,
artist = [ dict( index=[
id = str(a.id), dict(
name = a.name name=k,
) for a in sorted(v, key = lambda a: a.name.lower()) ] artist=[
) for k, v in sorted(indexes.items()) ], dict(id=str(a.id), name=a.name)
child = [ c.as_subsonic_child(request.user, request.client) for c in sorted(children, key = lambda t: t.sort_key()) ] 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(): def show_directory():
res = get_entity(Folder) res = get_entity(Folder)
directory = dict( directory = dict(
id = str(res.id), id=str(res.id),
name = res.name, 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()) ] 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: 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(): def list_genres():
return request.formatter('genres', dict( return request.formatter(
genre = [ dict(value = genre) for genre in select(t.genre for t in Track if t.genre) ] "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(): def list_artists():
# According to the API page, there are no parameters? # According to the API page, there are no parameters?
indexes = dict() indexes = dict()
for artist in Artist.select(): 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: if index in string.digits:
index = '#' index = "#"
elif index not in string.ascii_letters: elif index not in string.ascii_letters:
index = '?' index = "?"
if index not in indexes: if index not in indexes:
indexes[index] = [] indexes[index] = []
indexes[index].append(artist) indexes[index].append(artist)
return request.formatter('artists', dict( return request.formatter(
index = [ dict( "artists",
name = k, dict(
artist = [ a.as_subsonic_artist(request.user) for a in sorted(v, key = lambda a: a.name.lower()) ] index=[
) for k, v in sorted(indexes.items()) ] 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(): def artist_info():
res = get_entity(Artist) res = get_entity(Artist)
info = res.as_subsonic_artist(request.user) info = res.as_subsonic_artist(request.user)
albums = set(res.albums) albums = set(res.albums)
albums |= { t.album for t in res.tracks } albums |= {t.album for t in res.tracks}
info['album'] = [ a.as_subsonic_album(request.user) for a in sorted(albums, key = lambda a: a.sort_key()) ] info["album"] = [
a.as_subsonic_album(request.user)
for a in sorted(albums, key=lambda a: a.sort_key())
]
return request.formatter('artist', info) return request.formatter("artist", info)
@api.route('/getAlbum.view', methods = [ 'GET', 'POST' ])
@api.route("/getAlbum.view", methods=["GET", "POST"])
def album_info(): def album_info():
res = get_entity(Album) res = get_entity(Album)
info = res.as_subsonic_album(request.user) 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(): def track_info():
res = get_entity(Track) 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)
)

View File

@ -13,21 +13,24 @@ from ..db import ChatMessage, User
from ..py23 import dict from ..py23 import dict
from . import api from . import api
@api.route('/getChatMessages.view', methods = [ 'GET', 'POST' ])
@api.route("/getChatMessages.view", methods=["GET", "POST"])
def get_chat(): def get_chat():
since = request.values.get('since') since = request.values.get("since")
since = int(since) / 1000 if since else None since = int(since) / 1000 if since else None
query = ChatMessage.select().order_by(ChatMessage.time) query = ChatMessage.select().order_by(ChatMessage.time)
if since: if since:
query = query.filter(lambda m: m.time > 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(): def add_chat_message():
msg = request.values['message'] msg = request.values["message"]
ChatMessage(user = request.user, message = msg) ChatMessage(user=request.user, message=msg)
return request.formatter.empty return request.formatter.empty

View File

@ -15,28 +15,32 @@ from werkzeug.exceptions import BadRequestKeyError
from . import api from . import api
from .exceptions import GenericError, MissingParameter, NotFound, ServerError from .exceptions import GenericError, MissingParameter, NotFound, ServerError
@api.errorhandler(ValueError) @api.errorhandler(ValueError)
def value_error(e): def value_error(e):
rollback() rollback()
return GenericError("{0.__class__.__name__}: {0}".format(e)) return GenericError("{0.__class__.__name__}: {0}".format(e))
@api.errorhandler(BadRequestKeyError) @api.errorhandler(BadRequestKeyError)
def key_error(e): def key_error(e):
rollback() rollback()
return MissingParameter() return MissingParameter()
@api.errorhandler(ObjectNotFound) @api.errorhandler(ObjectNotFound)
def not_found(e): def not_found(e):
rollback() rollback()
return NotFound(e.entity.__name__) return NotFound(e.entity.__name__)
@api.errorhandler(500) @api.errorhandler(500)
def generic_error(e): # pragma: nocover def generic_error(e): # pragma: nocover
rollback() rollback()
return ServerError("{0.__class__.__name__}: {0}".format(e)) return ServerError("{0.__class__.__name__}: {0}".format(e))
#@api.errorhandler(404)
@api.route('/<path:invalid>', methods = [ 'GET', 'POST' ]) # blueprint 404 workaround
def not_found(*args, **kwargs):
return GenericError('Unknown method'), 404
# @api.errorhandler(404)
@api.route("/<path:invalid>", methods=["GET", "POST"]) # blueprint 404 workaround
def not_found(*args, **kwargs):
return GenericError("Unknown method"), 404

View File

@ -10,19 +10,21 @@
from flask import current_app, request from flask import current_app, request
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
class SubsonicAPIException(HTTPException): class SubsonicAPIException(HTTPException):
code = 400 code = 400
api_code = None api_code = None
message = 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 = request.formatter.error(self.api_code, self.message)
rv.status_code = self.code rv.status_code = self.code
return rv return rv
def __str__(self): def __str__(self):
code = self.api_code if self.api_code is not None else '??' code = self.api_code if self.api_code is not None else "??"
return '{}: {}'.format(code, self.message) return "{}: {}".format(code, self.message)
class GenericError(SubsonicAPIException): class GenericError(SubsonicAPIException):
api_code = 0 api_code = 0
@ -31,14 +33,17 @@ class GenericError(SubsonicAPIException):
super(GenericError, self).__init__(*args, **kwargs) super(GenericError, self).__init__(*args, **kwargs)
self.message = message self.message = message
class ServerError(GenericError): class ServerError(GenericError):
code = 500 code = 500
class UnsupportedParameter(GenericError): class UnsupportedParameter(GenericError):
def __init__(self, parameter, *args, **kwargs): def __init__(self, parameter, *args, **kwargs):
message = "Unsupported parameter '{}'".format(parameter) message = "Unsupported parameter '{}'".format(parameter)
super(UnsupportedParameter, self).__init__(message, *args, **kwargs) super(UnsupportedParameter, self).__init__(message, *args, **kwargs)
class MissingParameter(SubsonicAPIException): class MissingParameter(SubsonicAPIException):
api_code = 10 api_code = 10
@ -46,31 +51,39 @@ class MissingParameter(SubsonicAPIException):
super(MissingParameter, self).__init__(*args, **kwargs) super(MissingParameter, self).__init__(*args, **kwargs)
self.message = "A required parameter is missing." self.message = "A required parameter is missing."
class ClientMustUpgrade(SubsonicAPIException): class ClientMustUpgrade(SubsonicAPIException):
api_code = 20 api_code = 20
message = 'Incompatible Subsonic REST protocol version. Client must upgrade.' message = "Incompatible Subsonic REST protocol version. Client must upgrade."
class ServerMustUpgrade(SubsonicAPIException): class ServerMustUpgrade(SubsonicAPIException):
code = 501 code = 501
api_code = 30 api_code = 30
message = 'Incompatible Subsonic REST protocol version. Server must upgrade.' message = "Incompatible Subsonic REST protocol version. Server must upgrade."
class Unauthorized(SubsonicAPIException): class Unauthorized(SubsonicAPIException):
code = 401 code = 401
api_code = 40 api_code = 40
message = 'Wrong username or password.' message = "Wrong username or password."
class Forbidden(SubsonicAPIException): class Forbidden(SubsonicAPIException):
code = 403 code = 403
api_code = 50 api_code = 50
message = 'User is not authorized for the given operation.' message = "User is not authorized for the given operation."
class TrialExpired(SubsonicAPIException): class TrialExpired(SubsonicAPIException):
code = 402 code = 402
api_code = 60 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." "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): class NotFound(SubsonicAPIException):
code = 404 code = 404
@ -78,7 +91,8 @@ class NotFound(SubsonicAPIException):
def __init__(self, entity, *args, **kwargs): def __init__(self, entity, *args, **kwargs):
super(NotFound, self).__init__(*args, **kwargs) super(NotFound, self).__init__(*args, **kwargs)
self.message = '{} not found'.format(entity) self.message = "{} not found".format(entity)
class AggregateException(SubsonicAPIException): class AggregateException(SubsonicAPIException):
def __init__(self, exceptions, *args, **kwargs): def __init__(self, exceptions, *args, **kwargs):
@ -88,7 +102,7 @@ class AggregateException(SubsonicAPIException):
for exc in exceptions: for exc in exceptions:
if not isinstance(exc, SubsonicAPIException): if not isinstance(exc, SubsonicAPIException):
# Try to convert regular exceptions to SubsonicAPIExceptions # 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: if handler:
exc = handler(exc) exc = handler(exc)
assert isinstance(exc, SubsonicAPIException) assert isinstance(exc, SubsonicAPIException)
@ -96,14 +110,17 @@ class AggregateException(SubsonicAPIException):
exc = GenericError(str(exc)) exc = GenericError(str(exc))
self.exceptions.append(exc) self.exceptions.append(exc)
def get_response(self, environ = None): def get_response(self, environ=None):
if len(self.exceptions) == 1: if len(self.exceptions) == 1:
return self.exceptions[0].get_response() return self.exceptions[0].get_response()
codes = set(exc.api_code for exc in self.exceptions) 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 rv.status_code = self.code
return rv return rv

View File

@ -13,12 +13,13 @@ from xml.etree import ElementTree
from ..py23 import dict, strtype from ..py23 import dict, strtype
from . import API_VERSION from . import API_VERSION
class BaseFormatter(object): class BaseFormatter(object):
def make_response(self, elem, data): def make_response(self, elem, data):
raise NotImplementedError() raise NotImplementedError()
def make_error(self, code, message): 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): def make_empty(self):
return self.make_response(None, None) return self.make_response(None, None)
@ -29,10 +30,11 @@ class BaseFormatter(object):
error = make_error error = make_error
empty = property(make_empty) empty = property(make_empty)
class JSONBaseFormatter(BaseFormatter): class JSONBaseFormatter(BaseFormatter):
def __remove_empty_lists(self, d): def __remove_empty_lists(self, d):
if not isinstance(d, dict): 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 = [] keys_to_remove = []
for key, value in d.items(): for key, value in d.items():
@ -42,7 +44,12 @@ class JSONBaseFormatter(BaseFormatter):
if len(value) == 0: if len(value) == 0:
keys_to_remove.append(key) keys_to_remove.append(key)
else: 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: for key in keys_to_remove:
del d[key] del d[key]
@ -51,37 +58,39 @@ class JSONBaseFormatter(BaseFormatter):
def _subsonicify(self, elem, data): def _subsonicify(self, elem, data):
if (elem is None) != (data is None): 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 = { rv = {"status": "failed" if elem is "error" else "ok", "version": API_VERSION}
'status': 'failed' if elem is 'error' else 'ok',
'version': API_VERSION
}
if data: if data:
rv[elem] = self.__remove_empty_lists(data) rv[elem] = self.__remove_empty_lists(data)
return { 'subsonic-response': rv } return {"subsonic-response": rv}
class JSONFormatter(JSONBaseFormatter): class JSONFormatter(JSONBaseFormatter):
def make_response(self, elem, data): def make_response(self, elem, data):
rv = jsonify(self._subsonicify(elem, data)) rv = jsonify(self._subsonicify(elem, data))
rv.headers.add('Access-Control-Allow-Origin', '*') rv.headers.add("Access-Control-Allow-Origin", "*")
return rv return rv
class JSONPFormatter(JSONBaseFormatter): class JSONPFormatter(JSONBaseFormatter):
def __init__(self, callback): def __init__(self, callback):
self.__callback = callback self.__callback = callback
def make_response(self, elem, data): def make_response(self, elem, data):
if not self.__callback: 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 = self._subsonicify(elem, data)
rv = '{}({})'.format(self.__callback, json.dumps(rv)) rv = "{}({})".format(self.__callback, json.dumps(rv))
rv = make_response(rv) rv = make_response(rv)
rv.mimetype = 'application/javascript' rv.mimetype = "application/javascript"
return rv return rv
class XMLFormatter(BaseFormatter): class XMLFormatter(BaseFormatter):
def __dict2xml(self, elem, dictionary): def __dict2xml(self, elem, dictionary):
"""Convert a dict structure to xml. The game is trivial. Nesting uses the [] parenthesis. """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"}} "status": "ok","version": "1.7.0","xmlns": "http://subsonic.org/restapi"}}
""" """
if not isinstance(dictionary, dict): if not isinstance(dictionary, dict):
raise TypeError('Expecting a dict') raise TypeError("Expecting a dict")
if not all(map(lambda x: isinstance(x, strtype), dictionary)): 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(): for name, value in dictionary.items():
if name == 'value': if name == "value":
elem.text = self.__value_tostring(value) elem.text = self.__value_tostring(value)
elif isinstance(value, dict): elif isinstance(value, dict):
subelem = ElementTree.SubElement(elem, name) subelem = ElementTree.SubElement(elem, name)
@ -124,20 +133,19 @@ class XMLFormatter(BaseFormatter):
def make_response(self, elem, data): def make_response(self, elem, data):
if (elem is None) != (data is None): 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 = { response = {
'status': 'failed' if elem is 'error' else 'ok', "status": "failed" if elem is "error" else "ok",
'version': API_VERSION, "version": API_VERSION,
'xmlns': "http://subsonic.org/restapi" "xmlns": "http://subsonic.org/restapi",
} }
if elem: if elem:
response[elem] = data response[elem] = data
root = ElementTree.Element('subsonic-response') root = ElementTree.Element("subsonic-response")
self.__dict2xml(root, response) self.__dict2xml(root, response)
rv = make_response(ElementTree.tostring(root)) rv = make_response(ElementTree.tostring(root))
rv.mimetype = 'text/xml' rv.mimetype = "text/xml"
return rv return rv

View File

@ -35,30 +35,45 @@ from ..db import Track, Album, Artist, Folder, User, ClientPrefs, now
from ..py23 import dict from ..py23 import dict
from . import api, get_entity 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__) 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: if not base_cmdline:
return None return None
ret = shlex.split(base_cmdline) ret = shlex.split(base_cmdline)
ret = [ 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 for part in ret
] ]
return ret return ret
@api.route('/stream.view', methods = [ 'GET', 'POST' ])
@api.route("/stream.view", methods=["GET", "POST"])
def stream_media(): def stream_media():
res = get_entity(Track) res = get_entity(Track)
if 'timeOffset' in request.values: if "timeOffset" in request.values:
raise UnsupportedParameter('timeOffset') raise UnsupportedParameter("timeOffset")
if 'size' in request.values: if "size" in request.values:
raise UnsupportedParameter('size') 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: if format:
format = format.lower() format = format.lower()
@ -79,39 +94,53 @@ def stream_media():
if dst_bitrate > maxBitRate and maxBitRate != 0: if dst_bitrate > maxBitRate and maxBitRate != 0:
dst_bitrate = maxBitRate dst_bitrate = maxBitRate
if format and format != 'raw' and format != src_suffix: if format and format != "raw" and format != src_suffix:
dst_suffix = format dst_suffix = format
dst_mimetype = 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 # Requires transcoding
cache = current_app.transcode_cache cache = current_app.transcode_cache
cache_key = "{}-{}.{}".format(res.id, dst_bitrate, dst_suffix) cache_key = "{}-{}.{}".format(res.id, dst_bitrate, dst_suffix)
try: 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: except CacheMiss:
config = current_app.config['TRANSCODING'] config = current_app.config["TRANSCODING"]
transcoder = config.get('transcoder_{}_{}'.format(src_suffix, dst_suffix)) transcoder = config.get("transcoder_{}_{}".format(src_suffix, dst_suffix))
decoder = config.get('decoder_' + src_suffix) or config.get('decoder') decoder = config.get("decoder_" + src_suffix) or config.get("decoder")
encoder = config.get('encoder_' + dst_suffix) or config.get('encoder') encoder = config.get("encoder_" + dst_suffix) or config.get("encoder")
if not transcoder and (not decoder or not encoder): if not transcoder and (not decoder or not encoder):
transcoder = config.get('transcoder') transcoder = config.get("transcoder")
if not transcoder: if not transcoder:
message = 'No way to transcode from {} to {}'.format(src_suffix, dst_suffix) message = "No way to transcode from {} to {}".format(
src_suffix, dst_suffix
)
logger.info(message) logger.info(message)
raise GenericError(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: try:
if transcoder: if transcoder:
dec_proc = None dec_proc = None
proc = subprocess.Popen(transcoder, stdout = subprocess.PIPE) proc = subprocess.Popen(transcoder, stdout=subprocess.PIPE)
else: else:
dec_proc = subprocess.Popen(decoder, stdout = subprocess.PIPE) dec_proc = subprocess.Popen(decoder, stdout=subprocess.PIPE)
proc = subprocess.Popen(encoder, stdin = dec_proc.stdout, stdout = subprocess.PIPE) proc = subprocess.Popen(
encoder, stdin=dec_proc.stdout, stdout=subprocess.PIPE
)
except OSError: except OSError:
raise ServerError('Error while running the transcoding process') raise ServerError("Error while running the transcoding process")
def transcode(): def transcode():
try: try:
@ -120,7 +149,7 @@ def stream_media():
if not data: if not data:
break break
yield data yield data
except: # pragma: nocover except: # pragma: nocover
if dec_proc != None: if dec_proc != None:
dec_proc.kill() dec_proc.kill()
proc.kill() proc.kill()
@ -129,12 +158,19 @@ def stream_media():
if dec_proc != None: if dec_proc != None:
dec_proc.wait() dec_proc.wait()
proc.wait() proc.wait()
resp_content = cache.set_generated(cache_key, transcode) 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) response = Response(resp_content, mimetype=dst_mimetype)
if estimateContentLength == 'true': if estimateContentLength == "true":
response.headers.add('Content-Length', dst_bitrate * 1000 * res.duration // 8) response.headers.add(
"Content-Length", dst_bitrate * 1000 * res.duration // 8
)
else: else:
response = send_file(res.path, mimetype=dst_mimetype, conditional=True) response = send_file(res.path, mimetype=dst_mimetype, conditional=True)
@ -146,40 +182,44 @@ def stream_media():
return response return response
@api.route('/download.view', methods = [ 'GET', 'POST' ])
@api.route("/download.view", methods=["GET", "POST"])
def download_media(): def download_media():
id = request.values['id'] id = request.values["id"]
uid = uuid.UUID(id) uid = uuid.UUID(id)
try: # Track -> direct download try: # Track -> direct download
rv = Track[uid] 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: except ObjectNotFound:
pass pass
try: # Folder -> stream zipped tracks, non recursive try: # Folder -> stream zipped tracks, non recursive
rv = Folder[uid] rv = Folder[uid]
except ObjectNotFound: except ObjectNotFound:
try: # Album -> stream zipped tracks try: # Album -> stream zipped tracks
rv = Album[uid] rv = Album[uid]
except ObjectNotFound: 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: for track in rv.tracks:
z.write(track.path, os.path.basename(track.path)) z.write(track.path, os.path.basename(track.path))
resp = Response(z, mimetype = 'application/zip') resp = Response(z, mimetype="application/zip")
resp.headers['Content-Disposition'] = 'attachment; filename={}.zip'.format(rv.name) resp.headers["Content-Disposition"] = "attachment; filename={}.zip".format(rv.name)
return resp return resp
@api.route('/getCoverArt.view', methods = [ 'GET', 'POST' ])
@api.route("/getCoverArt.view", methods=["GET", "POST"])
def cover_art(): def cover_art():
cache = current_app.cache cache = current_app.cache
eid = request.values['id'] eid = request.values["id"]
if Folder.exists(id=eid): if Folder.exists(id=eid):
res = get_entity(Folder) res = get_entity(Folder)
if not res.cover_art or not os.path.isfile(os.path.join(res.path, res.cover_art)): if not res.cover_art or not os.path.isfile(
raise NotFound('Cover art') os.path.join(res.path, res.cover_art)
):
raise NotFound("Cover art")
cover_path = os.path.join(res.path, res.cover_art) cover_path = os.path.join(res.path, res.cover_art)
elif Track.exists(id=eid): elif Track.exists(id=eid):
cache_key = "{}-cover".format(eid) cache_key = "{}-cover".format(eid)
@ -189,19 +229,19 @@ def cover_art():
res = get_entity(Track) res = get_entity(Track)
art = res.extract_cover_art() art = res.extract_cover_art()
if not art: if not art:
raise NotFound('Cover art') raise NotFound("Cover art")
cover_path = cache.set(cache_key, art) cover_path = cache.set(cache_key, art)
else: else:
raise NotFound('Entity') raise NotFound("Entity")
size = request.values.get('size') size = request.values.get("size")
if size: if size:
size = int(size) size = int(size)
else: else:
return send_file(cover_path) return send_file(cover_path)
im = Image.open(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: if size > im.width and size > im.height:
return send_file(cover_path, mimetype=mimetype) return send_file(cover_path, mimetype=mimetype)
@ -214,77 +254,81 @@ def cover_art():
im.save(fp, im.format) im.save(fp, im.format)
return send_file(cache.get(cache_key), mimetype=mimetype) 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(): def lyrics():
artist = request.values['artist'] artist = request.values["artist"]
title = request.values['title'] title = request.values["title"]
query = Track.select(lambda t: title in t.title and artist in t.artist.name) query = Track.select(lambda t: title in t.title and artist in t.artist.name)
for track in query: for track in query:
lyrics_path = os.path.splitext(track.path)[0] + '.txt' lyrics_path = os.path.splitext(track.path)[0] + ".txt"
if os.path.exists(lyrics_path): if os.path.exists(lyrics_path):
logger.debug('Found lyrics file: ' + lyrics_path) logger.debug("Found lyrics file: " + lyrics_path)
try: try:
lyrics = read_file_as_unicode(lyrics_path) lyrics = read_file_as_unicode(lyrics_path)
except UnicodeError: except UnicodeError:
# Lyrics file couldn't be decoded. Rather than displaying an error, try with the potential next files or # Lyrics file couldn't be decoded. Rather than displaying an error, try with the potential next files or
# return no lyrics. Log it anyway. # return no lyrics. Log it anyway.
logger.warning('Unsupported encoding for lyrics file ' + lyrics_path) logger.warning("Unsupported encoding for lyrics file " + lyrics_path)
continue continue
return request.formatter('lyrics', dict( return request.formatter(
artist = track.album.artist.name, "lyrics",
title = track.title, dict(artist=track.album.artist.name, title=track.title, value=lyrics),
value = lyrics )
))
# Create a stable, unique, filesystem-compatible identifier for the artist+title # 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) cache_key = "lyrics-{}".format(unique)
lyrics = dict() lyrics = dict()
try: try:
lyrics = json.loads( lyrics = json.loads(
zlib.decompress( zlib.decompress(current_app.cache.get_value(cache_key)).decode("utf-8")
current_app.cache.get_value(cache_key)
).decode('utf-8')
) )
except (CacheMiss, zlib.error, TypeError, ValueError): except (CacheMiss, zlib.error, TypeError, ValueError):
try: try:
r = requests.get("http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect", r = requests.get(
params={'artist': artist, 'song': title}, timeout=5) "http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect",
params={"artist": artist, "song": title},
timeout=5,
)
root = ElementTree.fromstring(r.content) root = ElementTree.fromstring(r.content)
ns = {'cl': 'http://api.chartlyrics.com/'} ns = {"cl": "http://api.chartlyrics.com/"}
lyrics = dict( lyrics = dict(
artist = root.find('cl:LyricArtist', namespaces=ns).text, artist=root.find("cl:LyricArtist", namespaces=ns).text,
title = root.find('cl:LyricSong', namespaces=ns).text, title=root.find("cl:LyricSong", namespaces=ns).text,
value = root.find('cl:Lyric', namespaces=ns).text value=root.find("cl:Lyric", namespaces=ns).text,
) )
current_app.cache.set( 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 except requests.exceptions.RequestException as e: # pragma: nocover
logger.warning('Error while requesting the ChartLyrics API: ' + str(e)) 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): def read_file_as_unicode(path):
""" Opens a file trying with different encodings and returns the contents as a unicode string """ """ Opens a file trying with different encodings and returns the contents as a unicode string """
encodings = [ 'utf-8', 'latin1' ] # Should be extended to support more encodings encodings = ["utf-8", "latin1"] # Should be extended to support more encodings
for enc in encodings: for enc in encodings:
try: try:
contents = codecs.open(path, 'r', encoding = enc).read() contents = codecs.open(path, "r", encoding=enc).read()
logger.debug('Read file {} with {} encoding'.format(path, enc)) logger.debug("Read file {} with {} encoding".format(path, enc))
# Maybe save the encoding somewhere to prevent going through this loop each time for the same file # Maybe save the encoding somewhere to prevent going through this loop each time for the same file
return contents return contents
except UnicodeError: except UnicodeError:
pass pass
# Fallback to ASCII # Fallback to ASCII
logger.debug('Reading file {} with ascii encoding'.format(path)) logger.debug("Reading file {} with ascii encoding".format(path))
return unicode(open(path, 'r').read()) return unicode(open(path, "r").read())

View File

@ -17,38 +17,50 @@ from ..py23 import dict
from . import api, get_entity from . import api, get_entity
from .exceptions import Forbidden, MissingParameter, NotFound 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 username:
if not request.user.admin: if not request.user.admin:
raise Forbidden() raise Forbidden()
user = User.get(name = username) user = User.get(name=username)
if user is None: 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(): def show_playlist():
res = get_entity(Playlist) res = get_entity(Playlist)
if res.user.id != request.user.id and not res.public and not request.user.admin: if res.user.id != request.user.id and not res.public and not request.user.admin:
raise Forbidden() raise Forbidden()
info = res.as_subsonic_playlist(request.user) info = res.as_subsonic_playlist(request.user)
info['entry'] = [ t.as_subsonic_child(request.user, request.client) for t in res.get_tracks() ] info["entry"] = [
return request.formatter('playlist', info) 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(): 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 # 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 playlist_id = uuid.UUID(playlist_id) if playlist_id else None
if playlist_id: if playlist_id:
@ -61,9 +73,9 @@ def create_playlist():
if name: if name:
playlist.name = name playlist.name = name
elif name: elif name:
playlist = Playlist(user = request.user, name = name) playlist = Playlist(user=request.user, name=name)
else: else:
raise MissingParameter('playlistId or name') raise MissingParameter("playlistId or name")
for sid in songs: for sid in songs:
sid = uuid.UUID(sid) sid = uuid.UUID(sid)
@ -72,7 +84,8 @@ def create_playlist():
return request.formatter.empty return request.formatter.empty
@api.route('/deletePlaylist.view', methods = [ 'GET', 'POST' ])
@api.route("/deletePlaylist.view", methods=["GET", "POST"])
def delete_playlist(): def delete_playlist():
res = get_entity(Playlist) res = get_entity(Playlist)
if res.user.id != request.user.id and not request.user.admin: if res.user.id != request.user.id and not request.user.admin:
@ -81,22 +94,25 @@ def delete_playlist():
res.delete() res.delete()
return request.formatter.empty return request.formatter.empty
@api.route('/updatePlaylist.view', methods = [ 'GET', 'POST' ])
@api.route("/updatePlaylist.view", methods=["GET", "POST"])
def update_playlist(): 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: if res.user.id != request.user.id and not request.user.admin:
raise Forbidden() raise Forbidden()
playlist = res playlist = res
name, comment, public = map(request.values.get, [ 'name', 'comment', 'public' ]) name, comment, public = map(request.values.get, ["name", "comment", "public"])
to_add, to_remove = map(request.values.getlist, [ 'songIdToAdd', 'songIndexToRemove' ]) to_add, to_remove = map(
request.values.getlist, ["songIdToAdd", "songIndexToRemove"]
)
if name: if name:
playlist.name = name playlist.name = name
if comment: if comment:
playlist.comment = comment playlist.comment = comment
if public: if public:
playlist.public = public in (True, 'True', 'true', 1, '1') playlist.public = public in (True, "True", "true", 1, "1")
to_add = map(uuid.UUID, to_add) to_add = map(uuid.UUID, to_add)
to_remove = map(int, to_remove) to_remove = map(int, to_remove)
@ -108,4 +124,3 @@ def update_playlist():
playlist.remove_at_indexes(to_remove) playlist.remove_at_indexes(to_remove)
return request.formatter.empty return request.formatter.empty

View File

@ -18,9 +18,13 @@ from ..py23 import dict
from . import api from . import api
from .exceptions import MissingParameter from .exceptions import MissingParameter
@api.route('/search.view', methods = [ 'GET', 'POST' ])
@api.route("/search.view", methods=["GET", "POST"])
def old_search(): def old_search():
artist, album, title, anyf, count, offset, newer_than = map(request.values.get, [ 'artist', 'album', 'title', 'any', 'count', 'offset', 'newerThan' ]) artist, album, title, anyf, count, offset, newer_than = map(
request.values.get,
["artist", "album", "title", "any", "count", "offset", "newerThan"],
)
count = int(count) if count else 20 count = int(count) if count else 20
offset = int(offset) if offset else 0 offset = int(offset) if offset else 0
@ -28,9 +32,17 @@ def old_search():
min_date = datetime.fromtimestamp(newer_than) min_date = datetime.fromtimestamp(newer_than)
if artist: 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: 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: elif title:
query = Track.select(lambda t: title in t.title and t.created > min_date) query = Track.select(lambda t: title in t.title and t.created > min_date)
elif anyf: elif anyf:
@ -41,65 +53,122 @@ def old_search():
if offset + count > fcount: if offset + count > fcount:
toff = max(0, offset - fcount) toff = max(0, offset - fcount)
tend = offset + count - fcount tend = offset + count - fcount
res = res[:] + tracks[toff : tend][:] res = res[:] + tracks[toff:tend][:]
return request.formatter('searchResult', dict( return request.formatter(
totalHits = folders.count() + tracks.count(), "searchResult",
offset = offset, dict(
match = [ r.as_subsonic_child(request.user) if isinstance(r, Folder) else r.as_subsonic_child(request.user, request.client) for r in res ] 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: else:
raise MissingParameter('search') raise MissingParameter("search")
return request.formatter('searchResult', dict( return request.formatter(
totalHits = query.count(), "searchResult",
offset = offset, dict(
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] ] 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(): def new_search():
query = request.values['query'] query = request.values["query"]
artist_count, artist_offset, album_count, album_offset, song_count, song_offset = map( 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 artist_offset = int(artist_offset) if artist_offset else 0
album_count = int(album_count) if album_count else 20 album_count = int(album_count) if album_count else 20
album_offset = int(album_offset) if album_offset else 0 album_offset = int(album_offset) if album_offset else 0
song_count = int(song_count) if song_count else 20 song_count = int(song_count) if song_count else 20
song_offset = int(song_offset) if song_offset else 0 song_offset = int(song_offset) if song_offset else 0
artists = select(t.folder.parent for t in Track if query in t.folder.parent.name).limit(artist_count, artist_offset) artists = select(
albums = select(t.folder for t in Track if query in t.folder.name).limit(album_count, album_offset) 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) songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset)
return request.formatter('searchResult2', OrderedDict(( return request.formatter(
('artist', [ dict(id = str(a.id), name = a.name) for a in artists ]), "searchResult2",
('album', [ f.as_subsonic_child(request.user) for f in albums ]), OrderedDict(
('song', [ t.as_subsonic_child(request.user, request.client) for t in songs ]) (
))) ("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(): def search_id3():
query = request.values['query'] query = request.values["query"]
artist_count, artist_offset, album_count, album_offset, song_count, song_offset = map( 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 artist_offset = int(artist_offset) if artist_offset else 0
album_count = int(album_count) if album_count else 20 album_count = int(album_count) if album_count else 20
album_offset = int(album_offset) if album_offset else 0 album_offset = int(album_offset) if album_offset else 0
song_count = int(song_count) if song_count else 20 song_count = int(song_count) if song_count else 20
song_offset = int(song_offset) if song_offset else 0 song_offset = int(song_offset) if song_offset else 0
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) 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) songs = Track.select(lambda t: query in t.title).limit(song_count, song_offset)
return request.formatter('searchResult3', OrderedDict(( return request.formatter(
('artist', [ a.as_subsonic_artist(request.user) for a in artists ]), "searchResult3",
('album', [ a.as_subsonic_album(request.user) for a in albums ]), OrderedDict(
('song', [ t.as_subsonic_child(request.user, request.client) for t in songs ]) (
))) ("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],
),
)
),
)

View File

@ -12,11 +12,12 @@ from flask import request
from ..py23 import dict from ..py23 import dict
from . import api from . import api
@api.route('/ping.view', methods = [ 'GET', 'POST' ])
@api.route("/ping.view", methods=["GET", "POST"])
def ping(): def ping():
return request.formatter.empty 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))

View File

@ -11,12 +11,20 @@ from . import api
from .exceptions import GenericError from .exceptions import GenericError
methods = ( methods = (
'getVideos', 'getAvatar', 'getShares', 'createShare', 'updateShare', 'deleteShare', "getVideos",
"getAvatar",
"getShares",
"createShare",
"updateShare",
"deleteShare",
) )
def unsupported(): def unsupported():
return GenericError('Not supported by Supysonic'), 501 return GenericError("Not supported by Supysonic"), 501
for m in methods: 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"]
)

View File

@ -17,58 +17,67 @@ from ..py23 import dict
from . import api, decode_password from . import api, decode_password
from .exceptions import Forbidden, GenericError, NotFound from .exceptions import Forbidden, GenericError, NotFound
def admin_only(f): def admin_only(f):
@wraps(f) @wraps(f)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
if not request.user.admin: if not request.user.admin:
raise Forbidden() raise Forbidden()
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated return decorated
@api.route('/getUser.view', methods = [ 'GET', 'POST' ])
@api.route("/getUser.view", methods=["GET", "POST"])
def user_info(): def user_info():
username = request.values['username'] username = request.values["username"]
if username != request.user.name and not request.user.admin: if username != request.user.name and not request.user.admin:
raise Forbidden() raise Forbidden()
user = User.get(name = username) user = User.get(name=username)
if user is None: 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 @admin_only
def users_info(): 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 @admin_only
def user_add(): def user_add():
username = request.values['username'] username = request.values["username"]
password = request.values['password'] password = request.values["password"]
email = request.values['email'] email = request.values["email"]
admin = request.values.get('adminRole') admin = request.values.get("adminRole")
admin = True if admin in (True, 'True', 'true', 1, '1') else False admin = True if admin in (True, "True", "true", 1, "1") else False
password = decode_password(password) password = decode_password(password)
UserManager.add(username, password, email, admin) UserManager.add(username, password, email, admin)
return request.formatter.empty return request.formatter.empty
@api.route('/deleteUser.view', methods = [ 'GET', 'POST' ])
@api.route("/deleteUser.view", methods=["GET", "POST"])
@admin_only @admin_only
def user_del(): def user_del():
username = request.values['username'] username = request.values["username"]
UserManager.delete_by_name(username) UserManager.delete_by_name(username)
return request.formatter.empty return request.formatter.empty
@api.route('/changePassword.view', methods = [ 'GET', 'POST' ])
@api.route("/changePassword.view", methods=["GET", "POST"])
def user_changepass(): def user_changepass():
username = request.values['username'] username = request.values["username"]
password = request.values['password'] password = request.values["password"]
if username != request.user.name and not request.user.admin: if username != request.user.name and not request.user.admin:
raise Forbidden() raise Forbidden()
@ -77,4 +86,3 @@ def user_changepass():
UserManager.change_password2(username, password) UserManager.change_password2(username, password)
return request.formatter.empty return request.formatter.empty

View File

@ -26,19 +26,23 @@ logger = logging.getLogger(__name__)
class CacheMiss(KeyError): class CacheMiss(KeyError):
"""The requested data is not in the cache""" """The requested data is not in the cache"""
pass pass
class ProtectedError(Exception): class ProtectedError(Exception):
"""The data cannot be purged from the cache""" """The data cannot be purged from the cache"""
pass pass
CacheEntry = namedtuple("CacheEntry", ["size", "expires"]) CacheEntry = namedtuple("CacheEntry", ["size", "expires"])
NULL_ENTRY = CacheEntry(0, 0) NULL_ENTRY = CacheEntry(0, 0)
class Cache(object): class Cache(object):
"""Provides a common interface for caching files to disk""" """Provides a common interface for caching files to disk"""
# Modeled after werkzeug.contrib.cache.FileSystemCache # Modeled after werkzeug.contrib.cache.FileSystemCache
# keys must be filename-compatible strings (no paths) # 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 # Make a key -> CacheEntry(size, expiry) map ordered by mtime
self._size = 0 self._size = 0
self._files = OrderedDict() self._files = OrderedDict()
for mtime, size, key in sorted([(f.stat().st_mtime, f.stat().st_size, f.name) for mtime, size, key in sorted(
for f in scandir(self._cache_dir) [
if f.is_file()]): (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._files[key] = CacheEntry(size, mtime + self.min_time)
self._size += size self._size += size
@ -138,7 +146,9 @@ class Cache(object):
... json.dump(some_data, fp) ... json.dump(some_data, fp)
""" """
try: 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 yield f
# seek to end and get position to get filesize # seek to end and get position to get filesize
@ -185,7 +195,7 @@ class Cache(object):
@contextlib.contextmanager @contextlib.contextmanager
def get_fileobj(self, key): def get_fileobj(self, key):
"""Yields a file object that can be used to read cached bytes""" """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 yield f
def get_value(self, key): def get_value(self, key):

View File

@ -24,8 +24,9 @@ from .managers.folder import FolderManager
from .managers.user import UserManager from .managers.user import UserManager
from .scanner import Scanner from .scanner import Scanner
class TimedProgressDisplay: class TimedProgressDisplay:
def __init__(self, stdout, interval = 5): def __init__(self, stdout, interval=5):
self.__stdout = stdout self.__stdout = stdout
self.__interval = interval self.__interval = interval
self.__last_display = 0 self.__last_display = 0
@ -34,35 +35,39 @@ class TimedProgressDisplay:
def __call__(self, name, scanned): def __call__(self, name, scanned):
if time.time() - self.__last_display > self.__interval: if time.time() - self.__last_display > self.__interval:
progress = "Scanning '{0}': {1} files scanned".format(name, scanned) 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.write(progress)
self.__stdout.flush() self.__stdout.flush()
self.__last_len = len(progress) self.__last_len = len(progress)
self.__last_display = time.time() self.__last_display = time.time()
class CLIParser(argparse.ArgumentParser): class CLIParser(argparse.ArgumentParser):
def error(self, message): def error(self, message):
self.print_usage(sys.stderr) self.print_usage(sys.stderr)
raise RuntimeError(message) raise RuntimeError(message)
class SupysonicCLI(cmd.Cmd): class SupysonicCLI(cmd.Cmd):
prompt = "supysonic> " prompt = "supysonic> "
def _make_do(self, command): def _make_do(self, command):
def method(obj, line): def method(obj, line):
try: try:
args = getattr(obj, command + '_parser').parse_args(line.split()) args = getattr(obj, command + "_parser").parse_args(line.split())
except RuntimeError as e: except RuntimeError as e:
self.write_error_line(str(e)) self.write_error_line(str(e))
return return
if hasattr(obj.__class__, command + '_subparsers'): if hasattr(obj.__class__, command + "_subparsers"):
try: try:
func = getattr(obj, '{}_{}'.format(command, args.action)) func = getattr(obj, "{}_{}".format(command, args.action))
except AttributeError: except AttributeError:
return obj.default(line) 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: else:
try: try:
func = getattr(obj, command) func = getattr(obj, command)
@ -81,26 +86,39 @@ class SupysonicCLI(cmd.Cmd):
self.stderr = sys.stderr self.stderr = sys.stderr
self.__config = config self.__config = config
self.__daemon = DaemonClient(config.DAEMON['socket']) self.__daemon = DaemonClient(config.DAEMON["socket"])
# Generate do_* and help_* methods # 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] command = parser_name[:-7]
if not hasattr(self.__class__, 'do_' + command): if not hasattr(self.__class__, "do_" + command):
setattr(self.__class__, 'do_' + command, self._make_do(command)) setattr(self.__class__, "do_" + command, self._make_do(command))
if hasattr(self.__class__, 'do_' + command) and not hasattr(self.__class__, 'help_' + command): if hasattr(self.__class__, "do_" + command) and not hasattr(
setattr(self.__class__, 'help_' + command, getattr(self.__class__, parser_name).print_help) self.__class__, "help_" + command
if hasattr(self.__class__, command + '_subparsers'): ):
for action, subparser in getattr(self.__class__, command + '_subparsers').choices.items(): setattr(
setattr(self, 'help_{} {}'.format(command, action), subparser.print_help) 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 = ''): def write_line(self, line=""):
self.stdout.write(line + '\n') self.stdout.write(line + "\n")
def write_error_line(self, line = ''): def write_error_line(self, line=""):
self.stderr.write(line + '\n') self.stderr.write(line + "\n")
def do_EOF(self, line): def do_EOF(self, line):
return True return True
@ -108,7 +126,7 @@ class SupysonicCLI(cmd.Cmd):
do_exit = do_EOF do_exit = do_EOF
def default(self, line): 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) self.do_help(None)
def postloop(self): def postloop(self):
@ -116,34 +134,65 @@ class SupysonicCLI(cmd.Cmd):
def completedefault(self, text, line, begidx, endidx): def completedefault(self, text, line, begidx, endidx):
command = line.split()[0] command = line.split()[0]
parsers = getattr(self.__class__, command + '_subparsers', None) parsers = getattr(self.__class__, command + "_subparsers", None)
if not parsers: if not parsers:
return [] return []
num_words = len(line[len(command):begidx].split()) num_words = len(line[len(command) : begidx].split())
if num_words == 0: 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 [] return []
folder_parser = CLIParser(prog = 'folder', add_help = False) folder_parser = CLIParser(prog="folder", add_help=False)
folder_subparsers = folder_parser.add_subparsers(dest = 'action') folder_subparsers = folder_parser.add_subparsers(dest="action")
folder_subparsers.add_parser('list', help = 'Lists folders', add_help = False) 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 = folder_subparsers.add_parser(
folder_add_parser.add_argument('name', help = 'Name of the folder to add') "add", help="Adds a folder", add_help=False
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_add_parser.add_argument("name", help="Name of the folder to add")
folder_del_parser.add_argument('name', help = 'Name of the folder to delete') folder_add_parser.add_argument(
folder_scan_parser = folder_subparsers.add_parser('scan', help = 'Run a scan on specified folders', add_help = False) "path", help="Path to the directory pointed by the folder"
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_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 = 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(
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.') "--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 @db_session
def folder_list(self): def folder_list(self):
self.write_line('Name\t\tPath\n----\t\t----') 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(
"\n".join(
"{0: <16}{1}".format(f.name, f.path)
for f in Folder.select(lambda f: f.root)
)
)
@db_session @db_session
def folder_add(self, name, path): def folder_add(self, name, path):
@ -167,13 +216,17 @@ class SupysonicCLI(cmd.Cmd):
try: try:
self.__folder_scan_background(folders, force) self.__folder_scan_background(folders, force)
except DaemonUnavailableError: 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) self.__folder_scan_foreground(folders, force)
elif background: elif background:
try: try:
self.__folder_scan_background(folders, force) self.__folder_scan_background(folders, force)
except DaemonUnavailableError: 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: elif foreground:
self.__folder_scan_foreground(folders, force) self.__folder_scan_foreground(folders, force)
@ -184,25 +237,34 @@ class SupysonicCLI(cmd.Cmd):
try: try:
progress = self.__daemon.get_scanning_progress() progress = self.__daemon.get_scanning_progress()
if progress is not None: 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 return
except DaemonUnavailableError: except DaemonUnavailableError:
pass pass
extensions = self.__config.BASE['scanner_extensions'] extensions = self.__config.BASE["scanner_extensions"]
if extensions: if extensions:
extensions = extensions.split(' ') extensions = extensions.split(" ")
scanner = Scanner(force = force, extensions = extensions, progress = TimedProgressDisplay(self.stdout), scanner = Scanner(
on_folder_start = self.__unwatch_folder, on_folder_end = self.__watch_folder) force=force,
extensions=extensions,
progress=TimedProgressDisplay(self.stdout),
on_folder_start=self.__unwatch_folder,
on_folder_end=self.__watch_folder,
)
if folders: if folders:
fstrs = folders fstrs = folders
with db_session: 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) notfound = set(fstrs) - set(folders)
if notfound: if notfound:
self.write_line("No such folder(s): " + ' '.join(notfound)) self.write_line("No such folder(s): " + " ".join(notfound))
for folder in folders: for folder in folders:
scanner.queue_folder(folder) scanner.queue_folder(folder)
else: else:
@ -213,47 +275,86 @@ class SupysonicCLI(cmd.Cmd):
scanner.run() scanner.run()
stats = scanner.stats() stats = scanner.stats()
self.write_line('Scanning done') self.write_line("Scanning done")
self.write_line('Added: {0.artists} artists, {0.albums} albums, {0.tracks} tracks'.format(stats.added)) self.write_line(
self.write_line('Deleted: {0.artists} artists, {0.albums} albums, {0.tracks} tracks'.format(stats.deleted)) "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: if stats.errors:
self.write_line('Errors in:') self.write_line("Errors in:")
for err in stats.errors: for err in stats.errors:
self.write_line('- ' + err) self.write_line("- " + err)
def __unwatch_folder(self, folder): def __unwatch_folder(self, folder):
try: self.__daemon.remove_watched_folder(folder.path) try:
except DaemonUnavailableError: pass self.__daemon.remove_watched_folder(folder.path)
except DaemonUnavailableError:
pass
def __watch_folder(self, folder): def __watch_folder(self, folder):
try: self.__daemon.add_watched_folder(folder.path) try:
except DaemonUnavailableError: pass self.__daemon.add_watched_folder(folder.path)
except DaemonUnavailableError:
pass
user_parser = CLIParser(prog = 'user', add_help = False) user_parser = CLIParser(prog="user", add_help=False)
user_subparsers = user_parser.add_subparsers(dest = 'action') user_subparsers = user_parser.add_subparsers(dest="action")
user_subparsers.add_parser('list', help = 'List users', add_help = False) 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 = user_subparsers.add_parser(
user_add_parser.add_argument('name', help = 'Name/login of the user to add') "add", help="Adds a user", add_help=False
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("name", help="Name/login of the user to add")
user_add_parser.add_argument('-e', '--email', default = '', help = "Sets the user's email address") user_add_parser.add_argument(
user_del_parser = user_subparsers.add_parser('delete', help = 'Deletes a user', add_help = False) "-a", "--admin", action="store_true", help="Give admin rights to the new user"
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_add_parser.add_argument(
user_admin_parser.add_argument('name', help = 'Name/login of the user to grant/revoke admin rights') "-p", "--password", help="Specifies the user's password"
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_add_parser.add_argument(
user_pass_parser.add_argument('name', help = 'Name/login of the user to which change the password') "-e", "--email", default="", help="Sets the user's email address"
user_pass_parser.add_argument('password', nargs = '?', help = 'New password') )
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 @db_session
def user_list(self): def user_list(self):
self.write_line('Name\t\tAdmin\tEmail\n----\t\t-----\t-----') 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(
"\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() password = getpass.getpass()
confirm = getpass.getpass('Confirm password: ') confirm = getpass.getpass("Confirm password: ")
if password != confirm: if password != confirm:
raise ValueError("Passwords don't match") raise ValueError("Passwords don't match")
return password return password
@ -262,7 +363,7 @@ class SupysonicCLI(cmd.Cmd):
def user_add(self, name, admin, password, email): def user_add(self, name, admin, password, email):
try: try:
if not password: if not password:
password = self._ask_password() # pragma: nocover password = self._ask_password() # pragma: nocover
UserManager.add(name, password, email, admin) UserManager.add(name, password, email, admin)
except ValueError as e: except ValueError as e:
self.write_error_line(str(e)) self.write_error_line(str(e))
@ -277,34 +378,38 @@ class SupysonicCLI(cmd.Cmd):
@db_session @db_session
def user_setadmin(self, name, off): def user_setadmin(self, name, off):
user = User.get(name = name) user = User.get(name=name)
if user is None: if user is None:
self.write_error_line('No such user') self.write_error_line("No such user")
else: else:
user.admin = not off 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 @db_session
def user_changepass(self, name, password): def user_changepass(self, name, password):
try: try:
if not password: if not password:
password = self._ask_password() # pragma: nocover password = self._ask_password() # pragma: nocover
UserManager.change_password2(name, password) UserManager.change_password2(name, password)
self.write_line("Successfully changed '{}' password".format(name)) self.write_line("Successfully changed '{}' password".format(name))
except ObjectNotFound as e: except ObjectNotFound as e:
self.write_error_line(str(e)) self.write_error_line(str(e))
def main(): def main():
config = IniConfig.from_common_locations() config = IniConfig.from_common_locations()
init_database(config.BASE['database_uri']) init_database(config.BASE["database_uri"])
cli = SupysonicCLI(config) cli = SupysonicCLI(config)
if len(sys.argv) > 1: if len(sys.argv) > 1:
cli.onecmd(' '.join(sys.argv[1:])) cli.onecmd(" ".join(sys.argv[1:]))
else: else:
cli.cmdloop() cli.cmdloop()
release_database() release_database()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -17,50 +17,50 @@ import os
import tempfile import tempfile
current_config = None current_config = None
def get_current_config(): def get_current_config():
return current_config or DefaultConfig() return current_config or DefaultConfig()
class DefaultConfig(object): class DefaultConfig(object):
DEBUG = False DEBUG = False
tempdir = os.path.join(tempfile.gettempdir(), 'supysonic') tempdir = os.path.join(tempfile.gettempdir(), "supysonic")
BASE = { BASE = {
'database_uri': 'sqlite:///' + os.path.join(tempdir, 'supysonic.db'), "database_uri": "sqlite:///" + os.path.join(tempdir, "supysonic.db"),
'scanner_extensions': None "scanner_extensions": None,
} }
WEBAPP = { WEBAPP = {
'cache_dir': tempdir, "cache_dir": tempdir,
'cache_size': 1024, "cache_size": 1024,
'transcode_cache_size': 512, "transcode_cache_size": 512,
'log_file': None, "log_file": None,
'log_level': 'WARNING', "log_level": "WARNING",
"mount_webui": True,
'mount_webui': True, "mount_api": True,
'mount_api': True
} }
DAEMON = { DAEMON = {
'socket': os.path.join(tempdir, 'supysonic.sock'), "socket": os.path.join(tempdir, "supysonic.sock"),
'run_watcher': True, "run_watcher": True,
'wait_delay': 5, "wait_delay": 5,
'log_file': None, "log_file": None,
'log_level': 'WARNING' "log_level": "WARNING",
}
LASTFM = {
'api_key': None,
'secret': None
} }
LASTFM = {"api_key": None, "secret": None}
TRANSCODING = {} TRANSCODING = {}
MIMETYPES = {} MIMETYPES = {}
def __init__(self): def __init__(self):
current_config = self current_config = self
class IniConfig(DefaultConfig): class IniConfig(DefaultConfig):
common_paths = [ common_paths = [
'/etc/supysonic', "/etc/supysonic",
os.path.expanduser('~/.supysonic'), os.path.expanduser("~/.supysonic"),
os.path.expanduser('~/.config/supysonic/supysonic.conf'), os.path.expanduser("~/.config/supysonic/supysonic.conf"),
'supysonic.conf' "supysonic.conf",
] ]
def __init__(self, paths): def __init__(self, paths):
@ -70,7 +70,7 @@ class IniConfig(DefaultConfig):
parser.read(paths) parser.read(paths)
for section in parser.sections(): 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() section = section.upper()
if hasattr(self, section): if hasattr(self, section):
@ -87,13 +87,12 @@ class IniConfig(DefaultConfig):
return float(value) return float(value)
except ValueError: except ValueError:
lv = value.lower() lv = value.lower()
if lv in ('yes', 'true', 'on'): if lv in ("yes", "true", "on"):
return True return True
elif lv in ('no', 'false', 'off'): elif lv in ("no", "false", "off"):
return False return False
return value return value
@classmethod @classmethod
def from_common_locations(cls): def from_common_locations(cls):
return IniConfig(cls.common_paths) return IniConfig(cls.common_paths)

View File

@ -12,24 +12,26 @@ import re
from PIL import Image from PIL import Image
EXTENSIONS = ('.jpg', '.jpeg', '.png', '.bmp') EXTENSIONS = (".jpg", ".jpeg", ".png", ".bmp")
NAMING_SCORE_RULES = ( NAMING_SCORE_RULES = (
('cover', 5), ("cover", 5),
('albumart', 5), ("albumart", 5),
('folder', 5), ("folder", 5),
('front', 10), ("front", 10),
('back', -10), ("back", -10),
('large', 2), ("large", 2),
('small', -2) ("small", -2),
) )
class CoverFile(object): class CoverFile(object):
__clean_regex = re.compile(r'[^a-z]') __clean_regex = re.compile(r"[^a-z]")
@staticmethod @staticmethod
def __clean_name(name): 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.name = name
self.score = 0 self.score = 0
@ -44,6 +46,7 @@ class CoverFile(object):
if clean in album_name or album_name in clean: if clean in album_name or album_name in clean:
self.score += 20 self.score += 20
def is_valid_cover(path): def is_valid_cover(path):
if not os.path.isfile(path): if not os.path.isfile(path):
return False return False
@ -52,15 +55,16 @@ def is_valid_cover(path):
if ext.lower() not in EXTENSIONS: if ext.lower() not in EXTENSIONS:
return False return False
try: # Ensure the image can be read try: # Ensure the image can be read
with Image.open(path): with Image.open(path):
return True return True
except IOError: except IOError:
return False 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): if not os.path.isdir(path):
raise ValueError('Invalid path') raise ValueError("Invalid path")
candidates = [] candidates = []
for f in os.listdir(path): for f in os.listdir(path):
@ -80,5 +84,4 @@ def find_cover_in_folder(path, album_name = None):
if len(candidates) == 1: if len(candidates) == 1:
return candidates[0] 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]

View File

@ -18,27 +18,31 @@ from .server import Daemon
from ..config import IniConfig from ..config import IniConfig
from ..db import init_database, release_database from ..db import init_database, release_database
__all__ = [ 'Daemon', 'DaemonClient' ] __all__ = ["Daemon", "DaemonClient"]
logger = logging.getLogger("supysonic") logger = logging.getLogger("supysonic")
daemon = None daemon = None
def setup_logging(config): def setup_logging(config):
if config['log_file']: if config["log_file"]:
if config['log_file'] == '/dev/null': if config["log_file"] == "/dev/null":
log_handler = logging.NullHandler() log_handler = logging.NullHandler()
else: else:
log_handler = TimedRotatingFileHandler(config['log_file'], when = 'midnight') log_handler = TimedRotatingFileHandler(config["log_file"], when="midnight")
log_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")) log_handler.setFormatter(
logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
)
else: else:
log_handler = logging.StreamHandler() log_handler = logging.StreamHandler()
log_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s")) log_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
logger.addHandler(log_handler) logger.addHandler(log_handler)
if 'log_level' in config: if "log_level" in config:
level = getattr(logging, config['log_level'].upper(), logging.NOTSET) level = getattr(logging, config["log_level"].upper(), logging.NOTSET)
logger.setLevel(level) logger.setLevel(level)
def __terminate(signum, frame): def __terminate(signum, frame):
global daemon global daemon
@ -46,6 +50,7 @@ def __terminate(signum, frame):
daemon.terminate() daemon.terminate()
release_database() release_database()
def main(): def main():
global daemon global daemon
@ -55,7 +60,7 @@ def main():
signal(SIGTERM, __terminate) signal(SIGTERM, __terminate)
signal(SIGINT, __terminate) signal(SIGINT, __terminate)
init_database(config.BASE['database_uri']) init_database(config.BASE["database_uri"])
daemon = Daemon(config) daemon = Daemon(config)
daemon.run() daemon.run()
release_database() release_database()

View File

@ -14,74 +14,86 @@ from ..config import get_current_config
from ..py23 import strtype from ..py23 import strtype
from ..utils import get_secret_key from ..utils import get_secret_key
__all__ = [ 'DaemonClient' ] __all__ = ["DaemonClient"]
class DaemonCommand(object): class DaemonCommand(object):
def apply(self, connection, daemon): def apply(self, connection, daemon):
raise NotImplementedError() raise NotImplementedError()
class WatcherCommand(DaemonCommand): class WatcherCommand(DaemonCommand):
def __init__(self, folder): def __init__(self, folder):
self._folder = folder self._folder = folder
class AddWatchedFolderCommand(WatcherCommand): class AddWatchedFolderCommand(WatcherCommand):
def apply(self, connection, daemon): def apply(self, connection, daemon):
if daemon.watcher is not None: if daemon.watcher is not None:
daemon.watcher.add_folder(self._folder) daemon.watcher.add_folder(self._folder)
class RemoveWatchedFolder(WatcherCommand): class RemoveWatchedFolder(WatcherCommand):
def apply(self, connection, daemon): def apply(self, connection, daemon):
if daemon.watcher is not None: if daemon.watcher is not None:
daemon.watcher.remove_folder(self._folder) daemon.watcher.remove_folder(self._folder)
class ScannerCommand(DaemonCommand): class ScannerCommand(DaemonCommand):
pass pass
class ScannerProgressCommand(ScannerCommand): class ScannerProgressCommand(ScannerCommand):
def apply(self, connection, daemon): def apply(self, connection, daemon):
scanner = daemon.scanner scanner = daemon.scanner
rv = scanner.scanned if scanner is not None and scanner.is_alive() else None rv = scanner.scanned if scanner is not None and scanner.is_alive() else None
connection.send(ScannerProgressResult(rv)) connection.send(ScannerProgressResult(rv))
class ScannerStartCommand(ScannerCommand): class ScannerStartCommand(ScannerCommand):
def __init__(self, folders = [], force = False): def __init__(self, folders=[], force=False):
self.__folders = folders self.__folders = folders
self.__force = force self.__force = force
def apply(self, connection, daemon): def apply(self, connection, daemon):
daemon.start_scan(self.__folders, self.__force) daemon.start_scan(self.__folders, self.__force)
class DaemonCommandResult(object): class DaemonCommandResult(object):
pass pass
class ScannerProgressResult(DaemonCommandResult): class ScannerProgressResult(DaemonCommandResult):
def __init__(self, scanned): def __init__(self, scanned):
self.__scanned = scanned self.__scanned = scanned
scanned = property(lambda self: self.__scanned) scanned = property(lambda self: self.__scanned)
class DaemonClient(object): class DaemonClient(object):
def __init__(self, address = None): def __init__(self, address=None):
self.__address = address or get_current_config().DAEMON['socket'] self.__address = address or get_current_config().DAEMON["socket"]
self.__key = get_secret_key('daemon_key') self.__key = get_secret_key("daemon_key")
def __get_connection(self): def __get_connection(self):
if not self.__address: if not self.__address:
raise DaemonUnavailableError('No daemon address set') raise DaemonUnavailableError("No daemon address set")
try: try:
return Client(address = self.__address, authkey = self.__key) return Client(address=self.__address, authkey=self.__key)
except IOError: 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): def add_watched_folder(self, folder):
if not isinstance(folder, strtype): 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: with self.__get_connection() as c:
c.send(AddWatchedFolderCommand(folder)) c.send(AddWatchedFolderCommand(folder))
def remove_watched_folder(self, folder): def remove_watched_folder(self, folder):
if not isinstance(folder, strtype): 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: with self.__get_connection() as c:
c.send(RemoveWatchedFolder(folder)) c.send(RemoveWatchedFolder(folder))
@ -90,8 +102,8 @@ class DaemonClient(object):
c.send(ScannerProgressCommand()) c.send(ScannerProgressCommand())
return c.recv().scanned return c.recv().scanned
def scan(self, folders = [], force = False): def scan(self, folders=[], force=False):
if not isinstance(folders, list): 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: with self.__get_connection() as c:
c.send(ScannerStartCommand(folders, force)) c.send(ScannerStartCommand(folders, force))

View File

@ -7,5 +7,6 @@
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.
class DaemonUnavailableError(Exception): class DaemonUnavailableError(Exception):
pass pass

View File

@ -20,10 +20,11 @@ from ..scanner import Scanner
from ..utils import get_secret_key from ..utils import get_secret_key
from ..watcher import SupysonicWatcher from ..watcher import SupysonicWatcher
__all__ = [ 'Daemon' ] __all__ = ["Daemon"]
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Daemon(object): class Daemon(object):
def __init__(self, config): def __init__(self, config):
self.__config = config self.__config = config
@ -37,19 +38,21 @@ class Daemon(object):
def __handle_connection(self, connection): def __handle_connection(self, connection):
cmd = connection.recv() cmd = connection.recv()
logger.debug('Received %s', cmd) logger.debug("Received %s", cmd)
if cmd is None: if cmd is None:
pass pass
elif isinstance(cmd, DaemonCommand): elif isinstance(cmd, DaemonCommand):
cmd.apply(connection, self) cmd.apply(connection, self)
else: else:
logger.warn('Received unknown command %s', cmd) logger.warn("Received unknown command %s", cmd)
def run(self): 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) 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 = SupysonicWatcher(self.__config)
self.__watcher.start() self.__watcher.start()
@ -62,7 +65,7 @@ class Daemon(object):
conn = self.__listener.accept() conn = self.__listener.accept()
self.__handle_connection(conn) self.__handle_connection(conn)
def start_scan(self, folders = [], force = False): def start_scan(self, folders=[], force=False):
if not folders: if not folders:
with db_session: with db_session:
folders = select(f.name for f in Folder if f.root)[:] folders = select(f.name for f in Folder if f.root)[:]
@ -72,11 +75,16 @@ class Daemon(object):
self.__scanner.queue_folder(f) self.__scanner.queue_folder(f)
return return
extensions = self.__config.BASE['scanner_extensions'] extensions = self.__config.BASE["scanner_extensions"]
if 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: for f in folders:
self.__scanner.queue_folder(f) self.__scanner.queue_folder(f)
@ -92,7 +100,7 @@ class Daemon(object):
def terminate(self): def terminate(self):
self.__stopped.set() 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) c.send(None)
if self.__scanner is not None: if self.__scanner is not None:

View File

@ -31,104 +31,120 @@ try:
except ImportError: except ImportError:
from urlparse import urlparse, parse_qsl from urlparse import urlparse, parse_qsl
SCHEMA_VERSION = '20190518' SCHEMA_VERSION = "20190518"
def now(): def now():
return datetime.now().replace(microsecond = 0) return datetime.now().replace(microsecond=0)
metadb = Database() metadb = Database()
class Meta(metadb.Entity): class Meta(metadb.Entity):
_table_ = 'meta' _table_ = "meta"
key = PrimaryKey(str, 32) key = PrimaryKey(str, 32)
value = Required(str, 256) value = Required(str, 256)
db = Database() db = Database()
@db.on_connect(provider = 'sqlite')
@db.on_connect(provider="sqlite")
def sqlite_case_insensitive_like(db, connection): def sqlite_case_insensitive_like(db, connection):
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute('PRAGMA case_sensitive_like = OFF') cursor.execute("PRAGMA case_sensitive_like = OFF")
class PathMixin(object): class PathMixin(object):
@classmethod @classmethod
def get(cls, *args, **kwargs): def get(cls, *args, **kwargs):
if kwargs: if kwargs:
path = kwargs.pop('path', None) path = kwargs.pop("path", None)
if path: 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) return db.Entity.get.__func__(cls, *args, **kwargs)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
path = kwargs['path'] path = kwargs["path"]
kwargs['_path_hash'] = sha1(path.encode('utf-8')).digest() kwargs["_path_hash"] = sha1(path.encode("utf-8")).digest()
db.Entity.__init__(self, *args, **kwargs) db.Entity.__init__(self, *args, **kwargs)
def __setattr__(self, attr, value): def __setattr__(self, attr, value):
db.Entity.__setattr__(self, attr, value) db.Entity.__setattr__(self, attr, value)
if attr == 'path': if attr == "path":
db.Entity.__setattr__(self, '_path_hash', sha1(value.encode('utf-8')).digest()) db.Entity.__setattr__(
self, "_path_hash", sha1(value.encode("utf-8")).digest()
)
class Folder(PathMixin, db.Entity): class Folder(PathMixin, db.Entity):
_table_ = 'folder' _table_ = "folder"
id = PrimaryKey(UUID, default = uuid4) id = PrimaryKey(UUID, default=uuid4)
root = Required(bool, default = False) root = Required(bool, default=False)
name = Required(str, autostrip = False) name = Required(str, autostrip=False)
path = Required(str, 4096, autostrip = False) # unique path = Required(str, 4096, autostrip=False) # unique
_path_hash = Required(buffer, column = 'path_hash') _path_hash = Required(buffer, column="path_hash")
created = Required(datetime, precision = 0, default = now) created = Required(datetime, precision=0, default=now)
cover_art = Optional(str, nullable = True, autostrip = False) cover_art = Optional(str, nullable=True, autostrip=False)
last_scan = Required(int, default = 0) last_scan = Required(int, default=0)
parent = Optional(lambda: Folder, reverse = 'children', column = 'parent_id') parent = Optional(lambda: Folder, reverse="children", column="parent_id")
children = Set(lambda: Folder, reverse = 'parent') children = Set(lambda: Folder, reverse="parent")
__alltracks = Set(lambda: Track, lazy = True, reverse = 'root_folder') # Never used, hide it. Could be huge, lazy load __alltracks = Set(
tracks = Set(lambda: Track, reverse = 'folder') 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) stars = Set(lambda: StarredFolder)
ratings = Set(lambda: RatingFolder) ratings = Set(lambda: RatingFolder)
def as_subsonic_child(self, user): def as_subsonic_child(self, user):
info = dict( info = dict(
id = str(self.id), id=str(self.id),
isDir = True, isDir=True,
title = self.name, title=self.name,
album = self.name, album=self.name,
created = self.created.isoformat() created=self.created.isoformat(),
) )
if not self.root: if not self.root:
info['parent'] = str(self.parent.id) info["parent"] = str(self.parent.id)
info['artist'] = self.parent.name info["artist"] = self.parent.name
if self.cover_art: if self.cover_art:
info['coverArt'] = str(self.id) info["coverArt"] = str(self.id)
else: else:
for track in self.tracks: for track in self.tracks:
if track.has_art: if track.has_art:
info['coverArt'] = str(track.id) info["coverArt"] = str(track.id)
break break
try: try:
starred = StarredFolder[user.id, self.id] starred = StarredFolder[user.id, self.id]
info['starred'] = starred.date.isoformat() info["starred"] = starred.date.isoformat()
except ObjectNotFound: pass except ObjectNotFound:
pass
try: try:
rating = RatingFolder[user.id, self.id] rating = RatingFolder[user.id, self.id]
info['userRating'] = rating.rating info["userRating"] = rating.rating
except ObjectNotFound: pass except ObjectNotFound:
pass
avgRating = avg(self.ratings.rating) avgRating = avg(self.ratings.rating)
if avgRating: if avgRating:
info['averageRating'] = avgRating info["averageRating"] = avgRating
return info return info
@classmethod @classmethod
def prune(cls): def prune(cls):
query = cls.select(lambda self: not exists(t for t in Track if t.folder == self) and \ query = cls.select(
not exists(f for f in Folder if f.parent == self) and not self.root) 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 total = 0
while True: while True:
count = query.delete() count = query.delete()
@ -136,11 +152,12 @@ class Folder(PathMixin, db.Entity):
if not count: if not count:
return total return total
class Artist(db.Entity):
_table_ = 'artist'
id = PrimaryKey(UUID, default = uuid4) class Artist(db.Entity):
name = Required(str) # unique _table_ = "artist"
id = PrimaryKey(UUID, default=uuid4)
name = Required(str) # unique
albums = Set(lambda: Album) albums = Set(lambda: Album)
tracks = Set(lambda: Track) tracks = Set(lambda: Track)
@ -148,168 +165,193 @@ class Artist(db.Entity):
def as_subsonic_artist(self, user): def as_subsonic_artist(self, user):
info = dict( info = dict(
id = str(self.id), id=str(self.id),
name = self.name, name=self.name,
# coverArt # coverArt
albumCount = self.albums.count() albumCount=self.albums.count(),
) )
try: try:
starred = StarredArtist[user.id, self.id] starred = StarredArtist[user.id, self.id]
info['starred'] = starred.date.isoformat() info["starred"] = starred.date.isoformat()
except ObjectNotFound: pass except ObjectNotFound:
pass
return info return info
@classmethod @classmethod
def prune(cls): def prune(cls):
return cls.select(lambda self: not exists(a for a in Album if a.artist == self) and \ return cls.select(
not exists(t for t in Track if t.artist == self)).delete() 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): class Album(db.Entity):
_table_ = 'album' _table_ = "album"
id = PrimaryKey(UUID, default = uuid4) id = PrimaryKey(UUID, default=uuid4)
name = Required(str) name = Required(str)
artist = Required(Artist, column = 'artist_id') artist = Required(Artist, column="artist_id")
tracks = Set(lambda: Track) tracks = Set(lambda: Track)
stars = Set(lambda: StarredAlbum) stars = Set(lambda: StarredAlbum)
def as_subsonic_album(self, user): def as_subsonic_album(self, user):
info = dict( info = dict(
id = str(self.id), id=str(self.id),
name = self.name, name=self.name,
artist = self.artist.name, artist=self.artist.name,
artistId = str(self.artist.id), artistId=str(self.artist.id),
songCount = self.tracks.count(), songCount=self.tracks.count(),
duration = sum(self.tracks.duration), duration=sum(self.tracks.duration),
created = min(self.tracks.created).isoformat() 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: if track_with_cover is not None:
info['coverArt'] = str(track_with_cover.folder.id) info["coverArt"] = str(track_with_cover.folder.id)
else: else:
track_with_cover = self.tracks.select(lambda t: t.has_art).first() track_with_cover = self.tracks.select(lambda t: t.has_art).first()
if track_with_cover is not None: if track_with_cover is not None:
info['coverArt'] = str(track_with_cover.id) info["coverArt"] = str(track_with_cover.id)
try: try:
starred = StarredAlbum[user.id, self.id] starred = StarredAlbum[user.id, self.id]
info['starred'] = starred.date.isoformat() info["starred"] = starred.date.isoformat()
except ObjectNotFound: pass except ObjectNotFound:
pass
return info return info
def sort_key(self): def sort_key(self):
year = min(map(lambda t: t.year if t.year else 9999, self.tracks)) year = min(map(lambda t: t.year if t.year else 9999, self.tracks))
return '%i%s' % (year, self.name.lower()) return "%i%s" % (year, self.name.lower())
@classmethod @classmethod
def prune(cls): 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): class Track(PathMixin, db.Entity):
_table_ = 'track' _table_ = "track"
id = PrimaryKey(UUID, default = uuid4) id = PrimaryKey(UUID, default=uuid4)
disc = Required(int) disc = Required(int)
number = Required(int) number = Required(int)
title = Required(str) title = Required(str)
year = Optional(int) year = Optional(int)
genre = Optional(str, nullable = True) genre = Optional(str, nullable=True)
duration = Required(int) duration = Required(int)
has_art = Required(bool, default=False) has_art = Required(bool, default=False)
album = Required(Album, column = 'album_id') album = Required(Album, column="album_id")
artist = Required(Artist, column = 'artist_id') artist = Required(Artist, column="artist_id")
bitrate = Required(int) bitrate = Required(int)
path = Required(str, 4096, autostrip = False) # unique path = Required(str, 4096, autostrip=False) # unique
_path_hash = Required(buffer, column = 'path_hash') _path_hash = Required(buffer, column="path_hash")
created = Required(datetime, precision = 0, default = now) created = Required(datetime, precision=0, default=now)
last_modification = Required(int) last_modification = Required(int)
play_count = Required(int, default = 0) play_count = Required(int, default=0)
last_play = Optional(datetime, precision = 0) last_play = Optional(datetime, precision=0)
root_folder = Required(Folder, column = 'root_folder_id') root_folder = Required(Folder, column="root_folder_id")
folder = Required(Folder, column = '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) stars = Set(lambda: StarredTrack)
ratings = Set(lambda: RatingTrack) ratings = Set(lambda: RatingTrack)
def as_subsonic_child(self, user, prefs): def as_subsonic_child(self, user, prefs):
info = dict( info = dict(
id = str(self.id), id=str(self.id),
parent = str(self.folder.id), parent=str(self.folder.id),
isDir = False, isDir=False,
title = self.title, title=self.title,
album = self.album.name, album=self.album.name,
artist = self.artist.name, artist=self.artist.name,
track = self.number, track=self.number,
size = os.path.getsize(self.path) if os.path.isfile(self.path) else -1, size=os.path.getsize(self.path) if os.path.isfile(self.path) else -1,
contentType = self.mimetype, contentType=self.mimetype,
suffix = self.suffix(), suffix=self.suffix(),
duration = self.duration, duration=self.duration,
bitRate = self.bitrate, bitRate=self.bitrate,
path = self.path[len(self.root_folder.path) + 1:], path=self.path[len(self.root_folder.path) + 1 :],
isVideo = False, isVideo=False,
discNumber = self.disc, discNumber=self.disc,
created = self.created.isoformat(), created=self.created.isoformat(),
albumId = str(self.album.id), albumId=str(self.album.id),
artistId = str(self.artist.id), artistId=str(self.artist.id),
type = 'music' type="music",
) )
if self.year: if self.year:
info['year'] = self.year info["year"] = self.year
if self.genre: if self.genre:
info['genre'] = self.genre info["genre"] = self.genre
if self.has_art: if self.has_art:
info['coverArt'] = str(self.id) info["coverArt"] = str(self.id)
elif self.folder.cover_art: elif self.folder.cover_art:
info['coverArt'] = str(self.folder.id) info["coverArt"] = str(self.folder.id)
try: try:
starred = StarredTrack[user.id, self.id] starred = StarredTrack[user.id, self.id]
info['starred'] = starred.date.isoformat() info["starred"] = starred.date.isoformat()
except ObjectNotFound: pass except ObjectNotFound:
pass
try: try:
rating = RatingTrack[user.id, self.id] rating = RatingTrack[user.id, self.id]
info['userRating'] = rating.rating info["userRating"] = rating.rating
except ObjectNotFound: pass except ObjectNotFound:
pass
avgRating = avg(self.ratings.rating) avgRating = avg(self.ratings.rating)
if avgRating: if avgRating:
info['averageRating'] = avgRating info["averageRating"] = avgRating
if prefs is not None and prefs.format is not None and prefs.format != self.suffix(): if (
info['transcodedSuffix'] = prefs.format prefs is not None
info['transcodedContentType'] = mimetypes.guess_type('dummyname.' + prefs.format, False)[0] or 'application/octet-stream' 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 return info
@property @property
def mimetype(self): 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): def duration_str(self):
ret = '%02i:%02i' % ((self.duration % 3600) / 60, self.duration % 60) ret = "%02i:%02i" % ((self.duration % 3600) / 60, self.duration % 60)
if self.duration >= 3600: if self.duration >= 3600:
ret = '%02i:%s' % (self.duration / 3600, ret) ret = "%02i:%s" % (self.duration / 3600, ret)
return ret return ret
def suffix(self): def suffix(self):
return os.path.splitext(self.path)[1][1:].lower() return os.path.splitext(self.path)[1][1:].lower()
def sort_key(self): def sort_key(self):
return (self.album.artist.name + self.album.name + ("%02i" % self.disc) + ("%02i" % self.number) + self.title).lower() return (
self.album.artist.name
+ self.album.name
+ ("%02i" % self.disc)
+ ("%02i" % self.number)
+ self.title
).lower()
def extract_cover_art(self): def extract_cover_art(self):
return Track._extract_cover_art(self.path) return Track._extract_cover_art(self.path)
@ -319,159 +361,180 @@ class Track(PathMixin, db.Entity):
if os.path.exists(path): if os.path.exists(path):
metadata = mutagen.File(path) metadata = mutagen.File(path)
if metadata: if metadata:
if isinstance(metadata.tags, mutagen.id3.ID3Tags) and len(metadata.tags.getall('APIC')) > 0: if (
return metadata.tags.getall('APIC')[0].data 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): elif isinstance(metadata, mutagen.flac.FLAC) and len(metadata.pictures):
return metadata.pictures[0].data 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: elif (
picture = mutagen.flac.Picture(base64.b64decode(metadata.tags['METADATA_BLOCK_PICTURE'][0])) 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 picture.data
return None return None
class User(db.Entity):
_table_ = 'user'
id = PrimaryKey(UUID, default = uuid4) class User(db.Entity):
name = Required(str, 64) # unique _table_ = "user"
id = PrimaryKey(UUID, default=uuid4)
name = Required(str, 64) # unique
mail = Optional(str) mail = Optional(str)
password = Required(str, 40) password = Required(str, 40)
salt = Required(str, 6) salt = Required(str, 6)
admin = Required(bool, default = False) admin = Required(bool, default=False)
lastfm_session = Optional(str, 32, nullable = True) lastfm_session = Optional(str, 32, nullable=True)
lastfm_status = Required(bool, default = True) # True: ok/unlinked, False: invalid session lastfm_status = Required(
bool, default=True
) # True: ok/unlinked, False: invalid session
last_play = Optional(Track, column = 'last_play_id') last_play = Optional(Track, column="last_play_id")
last_play_date = Optional(datetime, precision = 0) last_play_date = Optional(datetime, precision=0)
clients = Set(lambda: ClientPrefs) clients = Set(lambda: ClientPrefs)
playlists = Set(lambda: Playlist) 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_folders = Set(lambda: StarredFolder, lazy=True)
starred_artists = Set(lambda: StarredArtist, lazy = True) starred_artists = Set(lambda: StarredArtist, lazy=True)
starred_albums = Set(lambda: StarredAlbum, lazy = True) starred_albums = Set(lambda: StarredAlbum, lazy=True)
starred_tracks = Set(lambda: StarredTrack, lazy = True) starred_tracks = Set(lambda: StarredTrack, lazy=True)
folder_ratings = Set(lambda: RatingFolder, lazy = True) folder_ratings = Set(lambda: RatingFolder, lazy=True)
track_ratings = Set(lambda: RatingTrack, lazy = True) track_ratings = Set(lambda: RatingTrack, lazy=True)
def as_subsonic_user(self): def as_subsonic_user(self):
return dict( return dict(
username = self.name, username=self.name,
email = self.mail, email=self.mail,
scrobblingEnabled = self.lastfm_session is not None and self.lastfm_status, scrobblingEnabled=self.lastfm_session is not None and self.lastfm_status,
adminRole = self.admin, adminRole=self.admin,
settingsRole = True, settingsRole=True,
downloadRole = True, downloadRole=True,
uploadRole = False, uploadRole=False,
playlistRole = True, playlistRole=True,
coverArtRole = False, coverArtRole=False,
commentRole = False, commentRole=False,
podcastRole = False, podcastRole=False,
streamRole = True, streamRole=True,
jukeboxRole = False, jukeboxRole=False,
shareRole = False shareRole=False,
) )
class ClientPrefs(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) client_name = Required(str, 32)
PrimaryKey(user, client_name) PrimaryKey(user, client_name)
format = Optional(str, 8, nullable = True) format = Optional(str, 8, nullable=True)
bitrate = Optional(int) bitrate = Optional(int)
class StarredFolder(db.Entity):
_table_ = 'starred_folder'
user = Required(User, column = 'user_id') class StarredFolder(db.Entity):
starred = Required(Folder, column = 'starred_id') _table_ = "starred_folder"
date = Required(datetime, precision = 0, default = now)
user = Required(User, column="user_id")
starred = Required(Folder, column="starred_id")
date = Required(datetime, precision=0, default=now)
PrimaryKey(user, starred) PrimaryKey(user, starred)
class StarredArtist(db.Entity): class StarredArtist(db.Entity):
_table_ = 'starred_artist' _table_ = "starred_artist"
user = Required(User, column = 'user_id') user = Required(User, column="user_id")
starred = Required(Artist, column = 'starred_id') starred = Required(Artist, column="starred_id")
date = Required(datetime, precision = 0, default = now) date = Required(datetime, precision=0, default=now)
PrimaryKey(user, starred) PrimaryKey(user, starred)
class StarredAlbum(db.Entity): class StarredAlbum(db.Entity):
_table_ = 'starred_album' _table_ = "starred_album"
user = Required(User, column = 'user_id') user = Required(User, column="user_id")
starred = Required(Album, column = 'starred_id') starred = Required(Album, column="starred_id")
date = Required(datetime, precision = 0, default = now) date = Required(datetime, precision=0, default=now)
PrimaryKey(user, starred) PrimaryKey(user, starred)
class StarredTrack(db.Entity): class StarredTrack(db.Entity):
_table_ = 'starred_track' _table_ = "starred_track"
user = Required(User, column = 'user_id') user = Required(User, column="user_id")
starred = Required(Track, column = 'starred_id') starred = Required(Track, column="starred_id")
date = Required(datetime, precision = 0, default = now) date = Required(datetime, precision=0, default=now)
PrimaryKey(user, starred) PrimaryKey(user, starred)
class RatingFolder(db.Entity): class RatingFolder(db.Entity):
_table_ = 'rating_folder' _table_ = "rating_folder"
user = Required(User, column = 'user_id') user = Required(User, column="user_id")
rated = Required(Folder, column = 'rated_id') rated = Required(Folder, column="rated_id")
rating = Required(int, min = 1, max = 5) rating = Required(int, min=1, max=5)
PrimaryKey(user, rated) PrimaryKey(user, rated)
class RatingTrack(db.Entity): class RatingTrack(db.Entity):
_table_ = 'rating_track' _table_ = "rating_track"
user = Required(User, column = 'user_id') user = Required(User, column="user_id")
rated = Required(Track, column = 'rated_id') rated = Required(Track, column="rated_id")
rating = Required(int, min = 1, max = 5) rating = Required(int, min=1, max=5)
PrimaryKey(user, rated) PrimaryKey(user, rated)
class ChatMessage(db.Entity):
_table_ = 'chat_message'
id = PrimaryKey(UUID, default = uuid4) class ChatMessage(db.Entity):
user = Required(User, column = 'user_id') _table_ = "chat_message"
time = Required(int, default = lambda: int(time.time()))
id = PrimaryKey(UUID, default=uuid4)
user = Required(User, column="user_id")
time = Required(int, default=lambda: int(time.time()))
message = Required(str, 512) message = Required(str, 512)
def responsize(self): def responsize(self):
return dict( return dict(
username = self.user.name, username=self.user.name, time=self.time * 1000, message=self.message
time = self.time * 1000,
message = self.message
) )
class Playlist(db.Entity):
_table_ = 'playlist'
id = PrimaryKey(UUID, default = uuid4) class Playlist(db.Entity):
user = Required(User, column = 'user_id') _table_ = "playlist"
id = PrimaryKey(UUID, default=uuid4)
user = Required(User, column="user_id")
name = Required(str) name = Required(str)
comment = Optional(str) comment = Optional(str)
public = Required(bool, default = False) public = Required(bool, default=False)
created = Required(datetime, precision = 0, default = now) created = Required(datetime, precision=0, default=now)
tracks = Optional(LongStr) tracks = Optional(LongStr)
def as_subsonic_playlist(self, user): def as_subsonic_playlist(self, user):
tracks = self.get_tracks() tracks = self.get_tracks()
info = dict( info = dict(
id = str(self.id), id=str(self.id),
name = self.name if self.user.id == user.id else '[%s] %s' % (self.user.name, self.name), name=self.name
owner = self.user.name, if self.user.id == user.id
public = self.public, else "[%s] %s" % (self.user.name, self.name),
songCount = len(tracks), owner=self.user.name,
duration = sum(map(lambda t: t.duration, tracks)), public=self.public,
created = self.created.isoformat() songCount=len(tracks),
duration=sum(map(lambda t: t.duration, tracks)),
created=self.created.isoformat(),
) )
if self.comment: if self.comment:
info['comment'] = self.comment info["comment"] = self.comment
return info return info
def get_tracks(self): def get_tracks(self):
@ -481,7 +544,7 @@ class Playlist(db.Entity):
tracks = [] tracks = []
should_fix = False should_fix = False
for t in self.tracks.split(','): for t in self.tracks.split(","):
try: try:
tid = UUID(t) tid = UUID(t)
track = Track[tid] track = Track[tid]
@ -490,13 +553,13 @@ class Playlist(db.Entity):
should_fix = True should_fix = True
if should_fix: if should_fix:
self.tracks = ','.join(map(lambda t: str(t.id), tracks)) self.tracks = ",".join(map(lambda t: str(t.id), tracks))
db.commit() db.commit()
return tracks return tracks
def clear(self): def clear(self):
self.tracks = '' self.tracks = ""
def add(self, track): def add(self, track):
if isinstance(track, UUID): if isinstance(track, UUID):
@ -507,92 +570,118 @@ class Playlist(db.Entity):
tid = UUID(track) tid = UUID(track)
if self.tracks and len(self.tracks) > 0: if self.tracks and len(self.tracks) > 0:
self.tracks = '{},{}'.format(self.tracks, tid) self.tracks = "{},{}".format(self.tracks, tid)
else: else:
self.tracks = str(tid) self.tracks = str(tid)
def remove_at_indexes(self, indexes): def remove_at_indexes(self, indexes):
tracks = self.tracks.split(',') tracks = self.tracks.split(",")
for i in indexes: for i in indexes:
if i < 0 or i >= len(tracks): if i < 0 or i >= len(tracks):
continue continue
tracks[i] = None tracks[i] = None
self.tracks = ','.join(t for t in tracks if t) self.tracks = ",".join(t for t in tracks if t)
def parse_uri(database_uri): def parse_uri(database_uri):
if not isinstance(database_uri, strtype): if not isinstance(database_uri, strtype):
raise TypeError('Expecting a string') raise TypeError("Expecting a string")
uri = urlparse(database_uri) uri = urlparse(database_uri)
args = dict(parse_qsl(uri.query)) args = dict(parse_qsl(uri.query))
if uri.scheme == 'sqlite': if uri.scheme == "sqlite":
path = uri.path path = uri.path
if not path: if not path:
path = ':memory:' path = ":memory:"
elif path[0] == '/': elif path[0] == "/":
path = path[1:] path = path[1:]
return dict(provider = 'sqlite', filename = path, create_db = True, **args) return dict(provider="sqlite", filename=path, create_db=True, **args)
elif uri.scheme in ('postgres', 'postgresql'): elif uri.scheme in ("postgres", "postgresql"):
return dict(provider = 'postgres', user = uri.username, password = uri.password, host = uri.hostname, dbname = uri.path[1:], **args) return dict(
elif uri.scheme == 'mysql': provider="postgres",
args.setdefault('charset', 'utf8mb4') user=uri.username,
args.setdefault('binary_prefix', True) password=uri.password,
return dict(provider = 'mysql', user = uri.username, passwd = uri.password, host = uri.hostname, db = uri.path[1:], **args) 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() return dict()
def execute_sql_resource_script(respath): def execute_sql_resource_script(respath):
sql = pkg_resources.resource_string(__package__, respath).decode('utf-8') sql = pkg_resources.resource_string(__package__, respath).decode("utf-8")
for statement in sql.split(';'): for statement in sql.split(";"):
statement = statement.strip() statement = statement.strip()
if statement and not statement.startswith('--'): if statement and not statement.startswith("--"):
metadb.execute(statement) metadb.execute(statement)
def init_database(database_uri): def init_database(database_uri):
settings = parse_uri(database_uri) settings = parse_uri(database_uri)
metadb.bind(**settings) metadb.bind(**settings)
metadb.generate_mapping(check_tables = False) metadb.generate_mapping(check_tables=False)
# Check if we should create the tables # Check if we should create the tables
try: try:
metadb.check_tables() metadb.check_tables()
except DatabaseError: except DatabaseError:
with db_session: with db_session:
execute_sql_resource_script('schema/' + settings['provider'] + '.sql') execute_sql_resource_script("schema/" + settings["provider"] + ".sql")
Meta(key = 'schema_version', value = SCHEMA_VERSION) Meta(key="schema_version", value=SCHEMA_VERSION)
# Check for schema changes # Check for schema changes
with db_session: with db_session:
version = Meta['schema_version'] version = Meta["schema_version"]
if version.value < 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: for migration in migrations:
date, ext = os.path.splitext(migration) date, ext = os.path.splitext(migration)
if date <= version.value: if date <= version.value:
continue continue
if ext == '.sql': if ext == ".sql":
execute_sql_resource_script('schema/migration/{}/{}'.format(settings['provider'], migration)) execute_sql_resource_script(
elif ext == '.py': "schema/migration/{}/{}".format(settings["provider"], migration)
m = importlib.import_module('.schema.migration.{}.{}'.format(settings['provider'], date), __package__) )
elif ext == ".py":
m = importlib.import_module(
".schema.migration.{}.{}".format(settings["provider"], date),
__package__,
)
m.apply(settings.copy()) m.apply(settings.copy())
version.value = SCHEMA_VERSION version.value = SCHEMA_VERSION
# Hack for in-memory SQLite databases (used in tests), otherwise 'db' and 'metadb' would be two distinct databases # 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 # 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 db.provider = metadb.provider
else: else:
metadb.disconnect() metadb.disconnect()
db.bind(**settings) db.bind(**settings)
db.generate_mapping(check_tables = False) db.generate_mapping(check_tables=False)
def release_database(): def release_database():
metadb.disconnect() metadb.disconnect()
db.disconnect() db.disconnect()
db.provider = metadb.provider = None db.provider = metadb.provider = None
db.schema = metadb.schema = None db.schema = metadb.schema = None

View File

@ -18,23 +18,31 @@ from ..daemon.exceptions import DaemonUnavailableError
from ..db import Artist, Album, Track from ..db import Artist, Album, Track
from ..managers.user import UserManager from ..managers.user import UserManager
frontend = Blueprint('frontend', __name__) frontend = Blueprint("frontend", __name__)
@frontend.before_request @frontend.before_request
def login_check(): def login_check():
request.user = None request.user = None
should_login = True should_login = True
if session.get('userid'): if session.get("userid"):
try: try:
user = UserManager.get(session.get('userid')) user = UserManager.get(session.get("userid"))
request.user = user request.user = user
should_login = False should_login = False
except (ValueError, ObjectNotFound): except (ValueError, ObjectNotFound):
session.clear() session.clear()
if should_login and request.endpoint != 'frontend.login': if should_login and request.endpoint != "frontend.login":
flash('Please login') flash("Please login")
return redirect(url_for('frontend.login', returnUrl = request.script_root + request.url[len(request.url_root)-1:])) return redirect(
url_for(
"frontend.login",
returnUrl=request.script_root
+ request.url[len(request.url_root) - 1 :],
)
)
@frontend.before_request @frontend.before_request
def scan_status(): def scan_status():
@ -42,30 +50,35 @@ def scan_status():
return return
try: 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: if scanned is not None:
flash('Scanning in progress, {} files scanned.'.format(scanned)) flash("Scanning in progress, {} files scanned.".format(scanned))
except DaemonUnavailableError: except DaemonUnavailableError:
pass pass
@frontend.route('/')
@frontend.route("/")
def index(): def index():
stats = { stats = {
'artists': Artist.select().count(), "artists": Artist.select().count(),
'albums': Album.select().count(), "albums": Album.select().count(),
'tracks': Track.select().count() "tracks": Track.select().count(),
} }
return render_template('home.html', stats = stats) return render_template("home.html", stats=stats)
def admin_only(f): def admin_only(f):
@wraps(f) @wraps(f)
def decorated_func(*args, **kwargs): def decorated_func(*args, **kwargs):
if not request.user or not request.user.admin: 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 f(*args, **kwargs)
return decorated_func return decorated_func
from .user import * from .user import *
from .folder import * from .folder import *
from .playlist import * from .playlist import *

View File

@ -21,74 +21,84 @@ from ..scanner import Scanner
from . import admin_only, frontend from . import admin_only, frontend
@frontend.route('/folder')
@frontend.route("/folder")
@admin_only @admin_only
def folder_index(): def folder_index():
try: try:
DaemonClient(current_app.config['DAEMON']['socket']).get_scanning_progress() DaemonClient(current_app.config["DAEMON"]["socket"]).get_scanning_progress()
allow_scan = True allow_scan = True
except DaemonUnavailableError: except DaemonUnavailableError:
allow_scan = False allow_scan = False
flash("The daemon is unavailable, can't scan from the web interface, use the CLI to do so.", 'warning') flash(
return render_template('folders.html', folders = Folder.select(lambda f: f.root), allow_scan = allow_scan) "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 @admin_only
def add_folder_form(): 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 @admin_only
def add_folder_post(): def add_folder_post():
error = False error = False
(name, path) = map(request.form.get, [ 'name', 'path' ]) (name, path) = map(request.form.get, ["name", "path"])
if name in (None, ''): if name in (None, ""):
flash('The name is required.') flash("The name is required.")
error = True error = True
if path in (None, ''): if path in (None, ""):
flash('The path is required.') flash("The path is required.")
error = True error = True
if error: if error:
return render_template('addfolder.html') return render_template("addfolder.html")
try: try:
FolderManager.add(name, path) FolderManager.add(name, path)
except ValueError as e: except ValueError as e:
flash(str(e), 'error') flash(str(e), "error")
return render_template('addfolder.html') return render_template("addfolder.html")
flash("Folder '%s' created. You should now run a scan" % name) flash("Folder '%s' created. You should now run a scan" % name)
return redirect(url_for('frontend.folder_index')) return redirect(url_for("frontend.folder_index"))
@frontend.route('/folder/del/<id>')
@frontend.route("/folder/del/<id>")
@admin_only @admin_only
def del_folder(id): def del_folder(id):
try: try:
FolderManager.delete(id) FolderManager.delete(id)
flash('Deleted folder') flash("Deleted folder")
except ValueError as e: except ValueError as e:
flash(str(e), 'error') flash(str(e), "error")
except ObjectNotFound: 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/<id>') @frontend.route("/folder/scan")
@frontend.route("/folder/scan/<id>")
@admin_only @admin_only
def scan_folder(id = None): def scan_folder(id=None):
try: try:
if id is not None: if id is not None:
folders = [ FolderManager.get(id).name ] folders = [FolderManager.get(id).name]
else: else:
folders = [] folders = []
DaemonClient(current_app.config['DAEMON']['socket']).scan(folders) DaemonClient(current_app.config["DAEMON"]["socket"]).scan(folders)
flash('Scanning started') flash("Scanning started")
except ValueError as e: except ValueError as e:
flash(str(e), 'error') flash(str(e), "error")
except ObjectNotFound: except ObjectNotFound:
flash('No such folder', 'error') flash("No such folder", "error")
except DaemonUnavailableError: 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"))

View File

@ -16,72 +16,84 @@ from ..db import Playlist
from . import frontend 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/<uid>') @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/<uid>")
def playlist_details(uid): def playlist_details(uid):
try: try:
uid = uuid.UUID(uid) uid = uuid.UUID(uid)
except ValueError: except ValueError:
flash('Invalid playlist id') flash("Invalid playlist id")
return redirect(url_for('frontend.playlist_index')) return redirect(url_for("frontend.playlist_index"))
try: try:
playlist = Playlist[uid] playlist = Playlist[uid]
except ObjectNotFound: except ObjectNotFound:
flash('Unknown playlist') flash("Unknown playlist")
return redirect(url_for('frontend.playlist_index')) return redirect(url_for("frontend.playlist_index"))
return render_template('playlist.html', playlist = playlist) return render_template("playlist.html", playlist=playlist)
@frontend.route('/playlist/<uid>', methods = [ 'POST' ])
@frontend.route("/playlist/<uid>", methods=["POST"])
def playlist_update(uid): def playlist_update(uid):
try: try:
uid = uuid.UUID(uid) uid = uuid.UUID(uid)
except ValueError: except ValueError:
flash('Invalid playlist id') flash("Invalid playlist id")
return redirect(url_for('frontend.playlist_index')) return redirect(url_for("frontend.playlist_index"))
try: try:
playlist = Playlist[uid] playlist = Playlist[uid]
except ObjectNotFound: except ObjectNotFound:
flash('Unknown playlist') flash("Unknown playlist")
return redirect(url_for('frontend.playlist_index')) return redirect(url_for("frontend.playlist_index"))
if playlist.user.id != request.user.id: if playlist.user.id != request.user.id:
flash("You're not allowed to edit this playlist") flash("You're not allowed to edit this playlist")
elif not request.form.get('name'): elif not request.form.get("name"):
flash('Missing playlist name') flash("Missing playlist name")
else: else:
playlist.name = request.form.get('name') playlist.name = request.form.get("name")
playlist.public = request.form.get('public') in (True, 'True', 1, '1', 'on', 'checked') playlist.public = request.form.get("public") in (
flash('Playlist updated.') True,
"True",
1,
"1",
"on",
"checked",
)
flash("Playlist updated.")
return playlist_details(str(uid)) return playlist_details(str(uid))
@frontend.route('/playlist/del/<uid>')
@frontend.route("/playlist/del/<uid>")
def playlist_delete(uid): def playlist_delete(uid):
try: try:
uid = uuid.UUID(uid) uid = uuid.UUID(uid)
except ValueError: except ValueError:
flash('Invalid playlist id') flash("Invalid playlist id")
return redirect(url_for('frontend.playlist_index')) return redirect(url_for("frontend.playlist_index"))
try: try:
playlist = Playlist[uid] playlist = Playlist[uid]
except ObjectNotFound: except ObjectNotFound:
flash('Unknown playlist') flash("Unknown playlist")
return redirect(url_for('frontend.playlist_index')) return redirect(url_for("frontend.playlist_index"))
if playlist.user.id != request.user.id: if playlist.user.id != request.user.id:
flash("You're not allowed to delete this playlist") flash("You're not allowed to delete this playlist")
else: else:
playlist.delete() playlist.delete()
flash('Playlist deleted') flash("Playlist deleted")
return redirect(url_for('frontend.playlist_index'))
return redirect(url_for("frontend.playlist_index"))

View File

@ -23,7 +23,8 @@ from . import admin_only, frontend
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def me_or_uuid(f, arg = 'uid'):
def me_or_uuid(f, arg="uid"):
@wraps(f) @wraps(f)
def decorated_func(*args, **kwargs): def decorated_func(*args, **kwargs):
if kwargs: if kwargs:
@ -31,22 +32,22 @@ def me_or_uuid(f, arg = 'uid'):
else: else:
uid = args[0] uid = args[0]
if uid == 'me': if uid == "me":
user = request.user user = request.user
elif not request.user.admin: elif not request.user.admin:
return redirect(url_for('frontend.index')) return redirect(url_for("frontend.index"))
else: else:
try: try:
user = UserManager.get(uid) user = UserManager.get(uid)
except ValueError as e: except ValueError as e:
flash(str(e), 'error') flash(str(e), "error")
return redirect(url_for('frontend.index')) return redirect(url_for("frontend.index"))
except ObjectNotFound: except ObjectNotFound:
flash('No such user', 'error') flash("No such user", "error")
return redirect(url_for('frontend.index')) return redirect(url_for("frontend.index"))
if kwargs: if kwargs:
kwargs['user'] = user kwargs["user"] = user
else: else:
args = (uid, user) args = (uid, user)
@ -54,24 +55,32 @@ def me_or_uuid(f, arg = 'uid'):
return decorated_func return decorated_func
@frontend.route('/user')
@frontend.route("/user")
@admin_only @admin_only
def user_index(): def user_index():
return render_template('users.html', users = User.select()) return render_template("users.html", users=User.select())
@frontend.route('/user/<uid>')
@frontend.route("/user/<uid>")
@me_or_uuid @me_or_uuid
def user_profile(uid, user): 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/<uid>', methods = [ 'POST' ])
@frontend.route("/user/<uid>", methods=["POST"])
@me_or_uuid @me_or_uuid
def update_clients(uid, user): def update_clients(uid, user):
clients_opts = dict() clients_opts = dict()
for key, value in request.form.items(): for key, value in request.form.items():
if '_' not in key: if "_" not in key:
continue continue
parts = key.split('_') parts = key.split("_")
if len(parts) != 2: if len(parts) != 2:
continue continue
client, opt = parts client, opt = parts
@ -79,7 +88,7 @@ def update_clients(uid, user):
continue continue
if client not in clients_opts: if client not in clients_opts:
clients_opts[client] = dict([ (opt, value) ]) clients_opts[client] = dict([(opt, value)])
else: else:
clients_opts[client][opt] = value clients_opts[client][opt] = value
logger.debug(clients_opts) logger.debug(clients_opts)
@ -89,51 +98,61 @@ def update_clients(uid, user):
if prefs is None: if prefs is None:
continue 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() prefs.delete()
continue continue
prefs.format = opts['format'] if 'format' in opts and opts['format'] else None prefs.format = opts["format"] if "format" in opts and opts["format"] else None
prefs.bitrate = int(opts['bitrate']) if 'bitrate' in opts and opts['bitrate'] else None prefs.bitrate = (
int(opts["bitrate"]) if "bitrate" in opts and opts["bitrate"] else None
)
flash('Clients preferences updated.') flash("Clients preferences updated.")
return user_profile(uid, user) return user_profile(uid, user)
@frontend.route('/user/<uid>/changeusername')
@frontend.route("/user/<uid>/changeusername")
@admin_only @admin_only
def change_username_form(uid): def change_username_form(uid):
try: try:
user = UserManager.get(uid) user = UserManager.get(uid)
except ValueError as e: except ValueError as e:
flash(str(e), 'error') flash(str(e), "error")
return redirect(url_for('frontend.index')) return redirect(url_for("frontend.index"))
except ObjectNotFound: except ObjectNotFound:
flash('No such user', 'error') flash("No such user", "error")
return redirect(url_for('frontend.index')) 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/<uid>/changeusername', methods = [ 'POST' ])
@frontend.route("/user/<uid>/changeusername", methods=["POST"])
@admin_only @admin_only
def change_username_post(uid): def change_username_post(uid):
try: try:
user = UserManager.get(uid) user = UserManager.get(uid)
except ValueError as e: except ValueError as e:
flash(str(e), 'error') flash(str(e), "error")
return redirect(url_for('frontend.index')) return redirect(url_for("frontend.index"))
except ObjectNotFound: except ObjectNotFound:
flash('No such user', 'error') flash("No such user", "error")
return redirect(url_for('frontend.index')) return redirect(url_for("frontend.index"))
username = request.form.get('user') username = request.form.get("user")
if username in ('', None): if username in ("", None):
flash('The username is required') flash("The username is required")
return render_template('change_username.html', user = user) return render_template("change_username.html", user=user)
if user.name != username and User.get(name = username) is not None: if user.name != username and User.get(name=username) is not None:
flash('This name is already taken') flash("This name is already taken")
return render_template('change_username.html', user = user) return render_template("change_username.html", user=user)
if request.form.get('admin') is None: if request.form.get("admin") is None:
admin = False admin = False
else: else:
admin = True admin = True
@ -145,40 +164,44 @@ def change_username_post(uid):
else: else:
flash("No changes for '%s'." % username) 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/<uid>/changemail')
@frontend.route("/user/<uid>/changemail")
@me_or_uuid @me_or_uuid
def change_mail_form(uid, user): 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/<uid>/changemail', methods = [ 'POST' ])
@frontend.route("/user/<uid>/changemail", methods=["POST"])
@me_or_uuid @me_or_uuid
def change_mail_post(uid, user): def change_mail_post(uid, user):
mail = request.form.get('mail', '') mail = request.form.get("mail", "")
# No validation, lol. # No validation, lol.
user.mail = mail user.mail = mail
return redirect(url_for('frontend.user_profile', uid = uid)) return redirect(url_for("frontend.user_profile", uid=uid))
@frontend.route('/user/<uid>/changepass')
@frontend.route("/user/<uid>/changepass")
@me_or_uuid @me_or_uuid
def change_password_form(uid, user): 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/<uid>/changepass', methods = [ 'POST' ])
@frontend.route("/user/<uid>/changepass", methods=["POST"])
@me_or_uuid @me_or_uuid
def change_password_post(uid, user): def change_password_post(uid, user):
error = False error = False
if user.id == request.user.id: if user.id == request.user.id:
current = request.form.get('current') current = request.form.get("current")
if not current: if not current:
flash('The current password is required') flash("The current password is required")
error = True error = True
new, confirm = map(request.form.get, [ 'new', 'confirm' ]) new, confirm = map(request.form.get, ["new", "confirm"])
if not new: if not new:
flash('The new password is required') flash("The new password is required")
error = True error = True
if new != confirm: if new != confirm:
flash("The new password and its confirmation don't match") flash("The new password and its confirmation don't match")
@ -191,28 +214,32 @@ def change_password_post(uid, user):
else: else:
UserManager.change_password2(user.name, new) UserManager.change_password2(user.name, new)
flash('Password changed') flash("Password changed")
return redirect(url_for('frontend.user_profile', uid = uid)) return redirect(url_for("frontend.user_profile", uid=uid))
except ValueError as e: except ValueError as e:
flash(str(e), 'error') flash(str(e), "error")
return change_password_form(uid, user) return change_password_form(uid, user)
@frontend.route('/user/add')
@frontend.route("/user/add")
@admin_only @admin_only
def add_user_form(): 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 @admin_only
def add_user_post(): def add_user_post():
error = False error = False
(name, passwd, passwd_confirm, mail, admin) = map(request.form.get, [ 'user', 'passwd', 'passwd_confirm', 'mail', 'admin' ]) (name, passwd, passwd_confirm, mail, admin) = map(
request.form.get, ["user", "passwd", "passwd_confirm", "mail", "admin"]
)
if not name: if not name:
flash('The name is required.') flash("The name is required.")
error = True error = True
if not passwd: if not passwd:
flash('Please provide a password.') flash("Please provide a password.")
error = True error = True
elif passwd != passwd_confirm: elif passwd != passwd_confirm:
flash("The passwords don't match.") flash("The passwords don't match.")
@ -220,86 +247,90 @@ def add_user_post():
admin = admin is not None admin = admin is not None
if mail is None: if mail is None:
mail = '' mail = ""
if not error: if not error:
try: try:
UserManager.add(name, passwd, mail, admin) UserManager.add(name, passwd, mail, admin)
flash("User '%s' successfully added" % name) flash("User '%s' successfully added" % name)
return redirect(url_for('frontend.user_index')) return redirect(url_for("frontend.user_index"))
except ValueError as e: except ValueError as e:
flash(str(e), 'error') flash(str(e), "error")
return add_user_form() return add_user_form()
@frontend.route('/user/del/<uid>')
@frontend.route("/user/del/<uid>")
@admin_only @admin_only
def del_user(uid): def del_user(uid):
try: try:
UserManager.delete(uid) UserManager.delete(uid)
flash('Deleted user') flash("Deleted user")
except ValueError as e: except ValueError as e:
flash(str(e), 'error') flash(str(e), "error")
except ObjectNotFound: 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/<uid>/lastfm/link')
@frontend.route("/user/<uid>/lastfm/link")
@me_or_uuid @me_or_uuid
def lastfm_reg(uid, user): def lastfm_reg(uid, user):
token = request.args.get('token') token = request.args.get("token")
if not token: if not token:
flash('Missing LastFM auth token') flash("Missing LastFM auth token")
return redirect(url_for('frontend.user_profile', uid = uid)) 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) 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/<uid>/lastfm/unlink')
@frontend.route("/user/<uid>/lastfm/unlink")
@me_or_uuid @me_or_uuid
def lastfm_unreg(uid, user): def lastfm_unreg(uid, user):
lfm = LastFm(current_app.config['LASTFM'], user) lfm = LastFm(current_app.config["LASTFM"], user)
lfm.unlink_account() lfm.unlink_account()
flash('Unlinked LastFM account') flash("Unlinked LastFM account")
return redirect(url_for('frontend.user_profile', uid = uid)) 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(): 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: if request.user:
flash('Already logged in') flash("Already logged in")
return redirect(return_url) return redirect(return_url)
if request.method == 'GET': if request.method == "GET":
return render_template('login.html') return render_template("login.html")
name, password = map(request.form.get, [ 'user', 'password' ]) name, password = map(request.form.get, ["user", "password"])
error = False error = False
if not name: if not name:
flash('Missing user name') flash("Missing user name")
error = True error = True
if not password: if not password:
flash('Missing password') flash("Missing password")
error = True error = True
if not error: if not error:
user = UserManager.try_auth(name, password) user = UserManager.try_auth(name, password)
if user: if user:
session['userid'] = str(user.id) session["userid"] = str(user.id)
flash('Logged in!') flash("Logged in!")
return redirect(return_url) return redirect(return_url)
else: 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(): def logout():
session.clear() session.clear()
flash('Logged out!') flash("Logged out!")
return redirect(url_for('frontend.login')) return redirect(url_for("frontend.login"))

View File

@ -15,11 +15,12 @@ from .py23 import strtype
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class LastFm: class LastFm:
def __init__(self, config, user): def __init__(self, config, user):
if config['api_key'] is not None and config['secret'] is not None: if config["api_key"] is not None and config["secret"] is not None:
self.__api_key = config['api_key'] self.__api_key = config["api_key"]
self.__api_secret = config['secret'].encode('utf-8') self.__api_secret = config["secret"].encode("utf-8")
self.__enabled = True self.__enabled = True
else: else:
self.__enabled = False self.__enabled = False
@ -27,17 +28,17 @@ class LastFm:
def link_account(self, token): def link_account(self, token):
if not self.__enabled: if not self.__enabled:
return False, 'No API key set' return False, "No API key set"
res = self.__api_request(False, method = 'auth.getSession', token = token) res = self.__api_request(False, method="auth.getSession", token=token)
if not res: if not res:
return False, 'Error connecting to LastFM' return False, "Error connecting to LastFM"
elif 'error' in res: elif "error" in res:
return False, 'Error %i: %s' % (res['error'], res['message']) return False, "Error %i: %s" % (res["error"], res["message"])
else: else:
self.__user.lastfm_session = res['session']['key'] self.__user.lastfm_session = res["session"]["key"]
self.__user.lastfm_status = True self.__user.lastfm_status = True
return True, 'OK' return True, "OK"
def unlink_account(self): def unlink_account(self):
self.__user.lastfm_session = None self.__user.lastfm_session = None
@ -47,15 +48,30 @@ class LastFm:
if not self.__enabled: if not self.__enabled:
return return
self.__api_request(True, method = 'track.updateNowPlaying', artist = track.album.artist.name, track = track.title, album = track.album.name, self.__api_request(
trackNumber = track.number, duration = track.duration) 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): def scrobble(self, track, ts):
if not self.__enabled: if not self.__enabled:
return return
self.__api_request(True, method = 'track.scrobble', artist = track.album.artist.name, track = track.title, album = track.album.name, self.__api_request(
timestamp = ts, trackNumber = track.number, duration = track.duration) 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): def __api_request(self, write, **kwargs):
if not self.__enabled: if not self.__enabled:
@ -64,34 +80,37 @@ class LastFm:
if write: if write:
if not self.__user.lastfm_session or not self.__user.lastfm_status: if not self.__user.lastfm_session or not self.__user.lastfm_status:
return return
kwargs['sk'] = self.__user.lastfm_session kwargs["sk"] = self.__user.lastfm_session
kwargs['api_key'] = self.__api_key kwargs["api_key"] = self.__api_key
sig_str = b'' sig_str = b""
for k, v in sorted(kwargs.items()): for k, v in sorted(kwargs.items()):
k = k.encode('utf-8') k = k.encode("utf-8")
v = v.encode('utf-8') if isinstance(v, strtype) else str(v).encode('utf-8') v = v.encode("utf-8") if isinstance(v, strtype) else str(v).encode("utf-8")
sig_str += k + v sig_str += k + v
sig = hashlib.md5(sig_str + self.__api_secret).hexdigest() sig = hashlib.md5(sig_str + self.__api_secret).hexdigest()
kwargs['api_sig'] = sig kwargs["api_sig"] = sig
kwargs['format'] = 'json' kwargs["format"] = "json"
try: try:
if write: if write:
r = requests.post('https://ws.audioscrobbler.com/2.0/', data = kwargs, timeout = 5) r = requests.post(
"https://ws.audioscrobbler.com/2.0/", data=kwargs, timeout=5
)
else: 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: 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 return None
json = r.json() json = r.json()
if 'error' in json: if "error" in json:
if json['error'] in (9, '9'): if json["error"] in (9, "9"):
self.__user.lastfm_status = False self.__user.lastfm_status = False
logger.warning('LastFM error %i: %s' % (json['error'], json['message'])) logger.warning("LastFM error %i: %s" % (json["error"], json["message"]))
return json return json

View File

@ -6,4 +6,3 @@
# Copyright (C) 2013 Alban 'spl0k' Féron # Copyright (C) 2013 Alban 'spl0k' Féron
# #
# Distributed under terms of the GNU AGPLv3 license. # Distributed under terms of the GNU AGPLv3 license.

View File

@ -18,6 +18,7 @@ from ..daemon.exceptions import DaemonUnavailableError
from ..db import Folder, Track, Artist, Album, User, RatingTrack, StarredTrack from ..db import Folder, Track, Artist, Album, User, RatingTrack, StarredTrack
from ..py23 import strtype from ..py23 import strtype
class FolderManager: class FolderManager:
@staticmethod @staticmethod
def get(uid): def get(uid):
@ -26,26 +27,26 @@ class FolderManager:
elif isinstance(uid, uuid.UUID): elif isinstance(uid, uuid.UUID):
pass pass
else: else:
raise ValueError('Invalid folder id') raise ValueError("Invalid folder id")
return Folder[uid] return Folder[uid]
@staticmethod @staticmethod
def add(name, path): 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)) raise ValueError("Folder '{}' exists".format(name))
path = os.path.abspath(os.path.expanduser(path)) path = os.path.abspath(os.path.expanduser(path))
if not os.path.isdir(path): if not os.path.isdir(path):
raise ValueError("The path doesn't exits or isn't a directory") raise ValueError("The path doesn't exits or isn't a directory")
if Folder.get(path = path) is not None: if Folder.get(path=path) is not None:
raise ValueError('This path is already registered') 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)): 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)): 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: try:
DaemonClient().add_watched_folder(path) DaemonClient().add_watched_folder(path)
except DaemonUnavailableError: except DaemonUnavailableError:
@ -66,20 +67,21 @@ class FolderManager:
for user in User.select(lambda u: u.last_play.root_folder == folder): for user in User.select(lambda u: u.last_play.root_folder == folder):
user.last_play = None user.last_play = None
RatingTrack.select(lambda r: r.rated.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) 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() Album.prune()
Artist.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() folder.delete()
@staticmethod @staticmethod
def delete_by_name(name): def delete_by_name(name):
folder = Folder.get(name = name, root = True) folder = Folder.get(name=name, root=True)
if not folder: if not folder:
raise ObjectNotFound(Folder) raise ObjectNotFound(Folder)
FolderManager.delete(folder.id) FolderManager.delete(folder.id)

View File

@ -18,6 +18,7 @@ from pony.orm import ObjectNotFound
from ..db import User from ..db import User
from ..py23 import strtype from ..py23 import strtype
class UserManager: class UserManager:
@staticmethod @staticmethod
def get(uid): def get(uid):
@ -26,24 +27,18 @@ class UserManager:
elif isinstance(uid, strtype): elif isinstance(uid, strtype):
uid = uuid.UUID(uid) uid = uuid.UUID(uid)
else: else:
raise ValueError('Invalid user id') raise ValueError("Invalid user id")
return User[uid] return User[uid]
@staticmethod @staticmethod
def add(name, password, mail, admin): def add(name, password, mail, admin):
if User.exists(name = name): if User.exists(name=name):
raise ValueError("User '{}' exists".format(name)) raise ValueError("User '{}' exists".format(name))
crypt, salt = UserManager.__encrypt_password(password) crypt, salt = UserManager.__encrypt_password(password)
user = User( user = User(name=name, mail=mail, password=crypt, salt=salt, admin=admin)
name = name,
mail = mail,
password = crypt,
salt = salt,
admin = admin
)
return user return user
@ -54,14 +49,14 @@ class UserManager:
@staticmethod @staticmethod
def delete_by_name(name): def delete_by_name(name):
user = User.get(name = name) user = User.get(name=name)
if user is None: if user is None:
raise ObjectNotFound(User) raise ObjectNotFound(User)
user.delete() user.delete()
@staticmethod @staticmethod
def try_auth(name, password): def try_auth(name, password):
user = User.get(name = name) user = User.get(name=name)
if user is None: if user is None:
return None return None
elif UserManager.__encrypt_password(password, user.salt)[0] != user.password: elif UserManager.__encrypt_password(password, user.salt)[0] != user.password:
@ -73,21 +68,23 @@ class UserManager:
def change_password(uid, old_pass, new_pass): def change_password(uid, old_pass, new_pass):
user = UserManager.get(uid) user = UserManager.get(uid)
if UserManager.__encrypt_password(old_pass, user.salt)[0] != user.password: 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] user.password = UserManager.__encrypt_password(new_pass, user.salt)[0]
@staticmethod @staticmethod
def change_password2(name, new_pass): def change_password2(name, new_pass):
user = User.get(name = name) user = User.get(name=name)
if user is None: if user is None:
raise ObjectNotFound(User) raise ObjectNotFound(User)
user.password = UserManager.__encrypt_password(new_pass, user.salt)[0] user.password = UserManager.__encrypt_password(new_pass, user.salt)[0]
@staticmethod @staticmethod
def __encrypt_password(password, salt = None): def __encrypt_password(password, salt=None):
if salt is None: if salt is None:
salt = ''.join(random.choice(string.printable.strip()) for _ in range(6)) salt = "".join(random.choice(string.printable.strip()) for _ in range(6))
return hashlib.sha1(salt.encode('utf-8') + password.encode('utf-8')).hexdigest(), salt return (
hashlib.sha1(salt.encode("utf-8") + password.encode("utf-8")).hexdigest(),
salt,
)

View File

@ -22,10 +22,12 @@ except ImportError:
# On Windows an existing file will not be overwritten # On Windows an existing file will not be overwritten
# This fallback just attempts to delete the dst file before using rename # This fallback just attempts to delete the dst file before using rename
import sys import sys
if sys.platform != 'win32':
if sys.platform != "win32":
from os import rename as osreplace from os import rename as osreplace
else: else:
import os import os
def osreplace(src, dst): def osreplace(src, dst):
try: try:
os.remove(dst) os.remove(dst)
@ -33,6 +35,7 @@ except ImportError:
pass pass
os.rename(src, dst) os.rename(src, dst)
try: try:
from queue import Queue, Empty as QueueEmpty from queue import Queue, Empty as QueueEmpty
except ImportError: except ImportError:
@ -60,6 +63,7 @@ try:
def items(self): def items(self):
return self.viewitems() return self.viewitems()
except NameError: except NameError:
# Python 3 # Python 3
strtype = str strtype = str

View File

@ -24,12 +24,14 @@ from .py23 import strtype, Queue, QueueEmpty
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class StatsDetails(object): class StatsDetails(object):
def __init__(self): def __init__(self):
self.artists = 0 self.artists = 0
self.albums = 0 self.albums = 0
self.tracks = 0 self.tracks = 0
class Stats(object): class Stats(object):
def __init__(self): def __init__(self):
self.scanned = 0 self.scanned = 0
@ -37,6 +39,7 @@ class Stats(object):
self.deleted = StatsDetails() self.deleted = StatsDetails()
self.errors = [] self.errors = []
class ScanQueue(Queue): class ScanQueue(Queue):
def _init(self, maxsize): def _init(self, maxsize):
self.queue = set() self.queue = set()
@ -50,13 +53,21 @@ class ScanQueue(Queue):
self.__last_got = self.queue.pop() self.__last_got = self.queue.pop()
return self.__last_got return self.__last_got
class Scanner(Thread): class Scanner(Thread):
def __init__(self, force = False, extensions = None, progress = None, def __init__(
on_folder_start = None, on_folder_end = None, on_done = None): self,
force=False,
extensions=None,
progress=None,
on_folder_start=None,
on_folder_end=None,
on_done=None,
):
super(Scanner, self).__init__() super(Scanner, self).__init__()
if extensions is not None and not isinstance(extensions, list): if extensions is not None and not isinstance(extensions, list):
raise TypeError('Invalid extensions type') raise TypeError("Invalid extensions type")
self.__force = force self.__force = force
self.__extensions = extensions self.__extensions = extensions
@ -80,7 +91,7 @@ class Scanner(Thread):
def queue_folder(self, folder_name): def queue_folder(self, folder_name):
if not isinstance(folder_name, strtype): 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) self.__queue.put(folder_name)
@ -92,7 +103,7 @@ class Scanner(Thread):
break break
with db_session: with db_session:
folder = Folder.get(name = folder_name, root = True) folder = Folder.get(name=folder_name, root=True)
if folder is None: if folder is None:
continue continue
@ -107,13 +118,13 @@ class Scanner(Thread):
self.__stopped.set() self.__stopped.set()
def __scan_folder(self, folder): 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: if self.__on_folder_start is not None:
self.__on_folder_start(folder) self.__on_folder_start(folder)
# Scan new/updated files # Scan new/updated files
to_scan = [ folder.path ] to_scan = [folder.path]
scanned = 0 scanned = 0
while not self.__stopped.is_set() and to_scan: while not self.__stopped.is_set() and to_scan:
path = to_scan.pop() path = to_scan.pop()
@ -124,8 +135,8 @@ class Scanner(Thread):
continue continue
for f in entries: for f in entries:
try: # test for badly encoded filenames try: # test for badly encoded filenames
f.encode('utf-8') f.encode("utf-8")
except UnicodeError: except UnicodeError:
self.__stats.errors.append(path) self.__stats.errors.append(path)
continue continue
@ -150,15 +161,17 @@ class Scanner(Thread):
self.remove_file(track.path) self.remove_file(track.path)
# Remove deleted/moved folders and update cover art info # Remove deleted/moved folders and update cover art info
folders = [ folder ] folders = [folder]
while not self.__stopped.is_set() and folders: while not self.__stopped.is_set() and folders:
f = folders.pop() f = folders.pop()
with db_session: 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): if not f.root and not os.path.isdir(f.path):
f.delete() # Pony will cascade f.delete() # Pony will cascade
continue continue
self.find_cover(f.path) self.find_cover(f.path)
@ -190,10 +203,12 @@ class Scanner(Thread):
@db_session @db_session
def scan_file(self, path): def scan_file(self, path):
if not isinstance(path, strtype): 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)
mtime = int(os.path.getmtime(path)) if os.path.exists(path) else 0 # condition for some tests mtime = (
int(os.path.getmtime(path)) if os.path.exists(path) else 0
) # condition for some tests
if tr is not None: if tr is not None:
if not self.__force and not mtime > tr.last_modification: if not self.__force and not mtime > tr.last_modification:
return return
@ -208,50 +223,65 @@ class Scanner(Thread):
if tag is None: if tag is None:
return return
trdict = { 'path': path } trdict = {"path": path}
artist = self.__try_read_tag(tag, 'artist', '[unknown]')[:255] artist = self.__try_read_tag(tag, "artist", "[unknown]")[:255]
album = self.__try_read_tag(tag, 'album', '[non-album tracks]')[:255] album = self.__try_read_tag(tag, "album", "[non-album tracks]")[:255]
albumartist = self.__try_read_tag(tag, 'albumartist', artist)[: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["disc"] = self.__try_read_tag(
trdict['number'] = self.__try_read_tag(tag, 'tracknumber', 1, lambda x: int(x.split('/')[0])) tag, "discnumber", 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["number"] = self.__try_read_tag(
trdict['genre'] = self.__try_read_tag(tag, 'genre') tag, "tracknumber", 1, lambda x: int(x.split("/")[0])
trdict['duration'] = int(tag.info.length) )
trdict['has_art'] = bool(Track._extract_cover_art(path)) 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["bitrate"] = (
trdict['last_modification'] = mtime 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) tralbum = self.__find_album(albumartist, album)
trartist = self.__find_artist(artist) trartist = self.__find_artist(artist)
if tr is None: if tr is None:
trdict['root_folder'] = self.__find_root_folder(path) trdict["root_folder"] = self.__find_root_folder(path)
trdict['folder'] = self.__find_folder(path) trdict["folder"] = self.__find_folder(path)
trdict['album'] = tralbum trdict["album"] = tralbum
trdict['artist'] = trartist trdict["artist"] = trartist
trdict['created'] = datetime.fromtimestamp(mtime) trdict["created"] = datetime.fromtimestamp(mtime)
Track(**trdict) Track(**trdict)
self.__stats.added.tracks += 1 self.__stats.added.tracks += 1
else: else:
if tr.album.id != tralbum.id: if tr.album.id != tralbum.id:
trdict['album'] = tralbum trdict["album"] = tralbum
if tr.artist.id != trartist.id: if tr.artist.id != trartist.id:
trdict['artist'] = trartist trdict["artist"] = trartist
tr.set(**trdict) tr.set(**trdict)
@db_session @db_session
def remove_file(self, path): def remove_file(self, path):
if not isinstance(path, strtype): 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: if not tr:
return return
@ -261,18 +291,18 @@ class Scanner(Thread):
@db_session @db_session
def move_file(self, src_path, dst_path): def move_file(self, src_path, dst_path):
if not isinstance(src_path, strtype): 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): 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: if src_path == dst_path:
return return
tr = Track.get(path = src_path) tr = Track.get(path=src_path)
if tr is None: if tr is None:
return return
tr_dst = Track.get(path = dst_path) tr_dst = Track.get(path=dst_path)
if tr_dst is not None: if tr_dst is not None:
root = tr_dst.root_folder root = tr_dst.root_folder
folder = tr_dst.folder folder = tr_dst.folder
@ -288,13 +318,13 @@ class Scanner(Thread):
@db_session @db_session
def find_cover(self, dirpath): def find_cover(self, dirpath):
if not isinstance(dirpath, strtype): # pragma: nocover if not isinstance(dirpath, strtype): # pragma: nocover
raise TypeError('Expecting string, got ' + str(type(dirpath))) raise TypeError("Expecting string, got " + str(type(dirpath)))
if not os.path.exists(dirpath): if not os.path.exists(dirpath):
return return
folder = Folder.get(path = dirpath) folder = Folder.get(path=dirpath)
if folder is None: if folder is None:
return return
@ -308,10 +338,10 @@ class Scanner(Thread):
@db_session @db_session
def add_cover(self, path): def add_cover(self, path):
if not isinstance(path, strtype): # pragma: nocover if not isinstance(path, strtype): # pragma: nocover
raise TypeError('Expecting string, got ' + str(type(path))) 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: if folder is None:
return return
@ -335,17 +365,17 @@ class Scanner(Thread):
if al: if al:
return al return al
al = Album(name = album, artist = ar) al = Album(name=album, artist=ar)
self.__stats.added.albums += 1 self.__stats.added.albums += 1
return al return al
def __find_artist(self, artist): def __find_artist(self, artist):
ar = Artist.get(name = artist) ar = Artist.get(name=artist)
if ar: if ar:
return ar return ar
ar = Artist(name = artist) ar = Artist(name=artist)
self.__stats.added.artists += 1 self.__stats.added.artists += 1
return ar return ar
@ -356,37 +386,45 @@ class Scanner(Thread):
if path.startswith(folder.path): if path.startswith(folder.path):
return folder 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): def __find_folder(self, path):
children = [] children = []
drive, _ = os.path.splitdrive(path) drive, _ = os.path.splitdrive(path)
path = os.path.dirname(path) path = os.path.dirname(path)
while path != drive and path != '/': while path != drive and path != "/":
folder = Folder.get(path = path) folder = Folder.get(path=path)
if folder is not None: if folder is not None:
break break
created = datetime.fromtimestamp(os.path.getmtime(path)) 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) path = os.path.dirname(path)
assert folder is not None assert folder is not None
while children: while children:
folder = Folder(parent = folder, **children.pop()) folder = Folder(parent=folder, **children.pop())
return folder return folder
def __try_load_tag(self, path): def __try_load_tag(self, path):
try: try:
return mutagen.File(path, easy = True) return mutagen.File(path, easy=True)
except mutagen.MutagenError: except mutagen.MutagenError:
return None 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: try:
value = metadata[field][0] value = metadata[field][0]
value = value.replace('\x00', '').strip() value = value.replace("\x00", "").strip()
if not value: if not value:
return default return default
@ -401,4 +439,3 @@ class Scanner(Thread):
def stats(self): def stats(self):
return self.__stats return self.__stats

View File

@ -11,6 +11,7 @@
# Converts ids from hex-encoded strings to binary data # Converts ids from hex-encoded strings to binary data
import argparse import argparse
try: try:
import MySQLdb as provider import MySQLdb as provider
except ImportError: except ImportError:
@ -20,17 +21,18 @@ from uuid import UUID
from warnings import filterwarnings from warnings import filterwarnings
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('username') parser.add_argument("username")
parser.add_argument('password') parser.add_argument("password")
parser.add_argument('database') parser.add_argument("database")
parser.add_argument('-H', '--host', default = 'localhost', help = 'default: localhost') parser.add_argument("-H", "--host", default="localhost", help="default: localhost")
args = parser.parse_args() 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 = 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 row in c:
for field, value in zip(fields + nullable_fields, row): for field, value in zip(fields + nullable_fields, row):
if value is None or not isinstance(value, basestring): 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(): for field, values in to_update.iteritems():
if not values: if not values:
continue 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)) c.executemany(sql, map(lambda v: (UUID(v).bytes, v), values))
for field in fields: 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) c.execute(sql)
for field in nullable_fields: 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) c.execute(sql)
connection.commit() 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',)) filterwarnings("ignore", category=provider.Warning)
process_table(conn, 'artist', ('id',)) conn = provider.connect(
process_table(conn, 'album', ('id', 'artist_id')) host=args.host, user=args.username, passwd=args.password, db=args.database
process_table(conn, 'track', ('id', 'album_id', 'artist_id', 'root_folder_id', 'folder_id')) )
process_table(conn, 'user', ('id',), ('last_play_id',)) conn.cursor().execute("SET FOREIGN_KEY_CHECKS = 0")
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') 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() conn.close()

View File

@ -9,26 +9,36 @@ except:
pass pass
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('username') parser.add_argument("username")
parser.add_argument('password') parser.add_argument("password")
parser.add_argument('database') parser.add_argument("database")
parser.add_argument('-H', '--host', default = 'localhost', help = 'default: localhost') parser.add_argument("-H", "--host", default="localhost", help="default: localhost")
args = parser.parse_args() args = parser.parse_args()
def process_table(connection, table): def process_table(connection, table):
c = connection.cursor() 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() hashes = dict()
c.execute('SELECT path FROM {0}'.format(table)) c.execute("SELECT path FROM {0}".format(table))
for row in c.fetchall(): for row in c.fetchall():
hashes[row[0]] = hashlib.sha1(row[0].encode('utf-8')).digest() 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.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")

View File

@ -16,40 +16,43 @@ import sqlite3
from uuid import UUID from uuid import UUID
parser = argparse.ArgumentParser() 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() args = parser.parse_args()
def process_table(connection, table, fields): 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() 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): for field, value in zip(fields, row):
if value is None or not isinstance(value, basestring): if value is None or not isinstance(value, basestring):
continue continue
to_update[field].add(value) to_update[field].add(value)
for field, values in to_update.iteritems(): 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)) c.executemany(sql, map(lambda v: (buffer(UUID(v).bytes), v), values))
connection.commit() connection.commit()
with sqlite3.connect(args.dbfile) as conn: with sqlite3.connect(args.dbfile) as conn:
conn.cursor().execute('PRAGMA foreign_keys = OFF') 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'))
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"))

View File

@ -9,23 +9,29 @@ except:
pass pass
parser = argparse.ArgumentParser() 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() args = parser.parse_args()
def process_table(connection, table): def process_table(connection, table):
c = connection.cursor() 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() hashes = dict()
for row in c.execute('SELECT path FROM {0}'.format(table)): for row in c.execute("SELECT path FROM {0}".format(table)):
hashes[row[0]] = hashlib.sha1(row[0].encode('utf-8')).digest() 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.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: with sqlite3.connect(args.dbfile) as conn:
process_table(conn, 'folder') process_table(conn, "folder")
process_table(conn, 'track') process_table(conn, "track")
conn.cursor().execute('VACUUM') conn.cursor().execute("VACUUM")

View File

@ -13,6 +13,7 @@ from pony.orm import db_session, commit, ObjectNotFound
from supysonic.db import Meta from supysonic.db import Meta
@db_session @db_session
def get_secret_key(keyname): def get_secret_key(keyname):
# Commit both at enter and exit. The metadb/db split (from supysonic.db) # 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) key = b64decode(Meta[keyname].value)
except ObjectNotFound: except ObjectNotFound:
key = urandom(128) key = urandom(128)
Meta(key = keyname, value = b64encode(key).decode()) Meta(key=keyname, value=b64encode(key).decode())
commit() commit()
return key return key

View File

@ -21,25 +21,30 @@ from .db import Folder
from .py23 import dict, strtype from .py23 import dict, strtype
from .scanner import Scanner from .scanner import Scanner
OP_SCAN = 1 OP_SCAN = 1
OP_REMOVE = 2 OP_REMOVE = 2
OP_MOVE = 4 OP_MOVE = 4
FLAG_CREATE = 8 FLAG_CREATE = 8
FLAG_COVER = 16 FLAG_COVER = 16
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class SupysonicWatcherEventHandler(PatternMatchingEventHandler): class SupysonicWatcherEventHandler(PatternMatchingEventHandler):
def __init__(self, extensions): def __init__(self, extensions):
patterns = None patterns = None
if extensions: if extensions:
patterns = list(map(lambda e: "*." + e.lower(), extensions.split())) + list(map(lambda e: "*" + e, covers.EXTENSIONS)) patterns = list(map(lambda e: "*." + e.lower(), extensions.split())) + list(
super(SupysonicWatcherEventHandler, self).__init__(patterns = patterns, ignore_directories = True) map(lambda e: "*" + e, covers.EXTENSIONS)
)
super(SupysonicWatcherEventHandler, self).__init__(
patterns=patterns, ignore_directories=True
)
def dispatch(self, event): def dispatch(self, event):
try: try:
super(SupysonicWatcherEventHandler, self).dispatch(event) super(SupysonicWatcherEventHandler, self).dispatch(event)
except Exception as e: # pragma: nocover except Exception as e: # pragma: nocover
logger.critical(e) logger.critical(e)
def on_created(self, event): def on_created(self, event):
@ -51,7 +56,7 @@ class SupysonicWatcherEventHandler(PatternMatchingEventHandler):
dirname = os.path.dirname(event.src_path) dirname = os.path.dirname(event.src_path)
with db_session: with db_session:
folder = Folder.get(path = dirname) folder = Folder.get(path=dirname)
if folder is None: if folder is None:
self.queue.put(dirname, op | FLAG_COVER) self.queue.put(dirname, op | FLAG_COVER)
else: else:
@ -78,12 +83,13 @@ class SupysonicWatcherEventHandler(PatternMatchingEventHandler):
_, ext = os.path.splitext(event.src_path) _, ext = os.path.splitext(event.src_path)
if ext in covers.EXTENSIONS: if ext in covers.EXTENSIONS:
op |= FLAG_COVER 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): class Event(object):
def __init__(self, path, operation, **kwargs): def __init__(self, path, operation, **kwargs):
if operation & (OP_SCAN | OP_REMOVE) == (OP_SCAN | OP_REMOVE): 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.__path = path
self.__time = time.time() self.__time = time.time()
@ -92,7 +98,7 @@ class Event(object):
def set(self, operation, **kwargs): def set(self, operation, **kwargs):
if operation & (OP_SCAN | OP_REMOVE) == (OP_SCAN | OP_REMOVE): 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() self.__time = time.time()
if operation & OP_SCAN: if operation & OP_SCAN:
@ -123,6 +129,7 @@ class Event(object):
def src_path(self): def src_path(self):
return self.__src return self.__src
class ScannerProcessingQueue(Thread): class ScannerProcessingQueue(Thread):
def __init__(self, delay): def __init__(self, delay):
super(ScannerProcessingQueue, self).__init__() super(ScannerProcessingQueue, self).__init__()
@ -136,7 +143,7 @@ class ScannerProcessingQueue(Thread):
def run(self): def run(self):
try: try:
self.__run() self.__run()
except Exception as e: # pragma: nocover except Exception as e: # pragma: nocover
logger.critical(e) logger.critical(e)
raise e raise e
@ -216,7 +223,7 @@ class ScannerProcessingQueue(Thread):
if operation & OP_MOVE and kwargs["src_path"] in self.__queue: if operation & OP_MOVE and kwargs["src_path"] in self.__queue:
previous = self.__queue[kwargs["src_path"]] 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"]] del self.__queue[kwargs["src_path"]]
if self.__timer: if self.__timer:
@ -240,17 +247,18 @@ class ScannerProcessingQueue(Thread):
if not self.__queue: if not self.__queue:
return None 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(): if not self.__running or next[1].time + self.__timeout <= time.time():
del self.__queue[next[0]] del self.__queue[next[0]]
return next[1] return next[1]
return None return None
class SupysonicWatcher(object): class SupysonicWatcher(object):
def __init__(self, config): def __init__(self, config):
self.__delay = config.DAEMON['wait_delay'] self.__delay = config.DAEMON["wait_delay"]
self.__handler = SupysonicWatcherEventHandler(config.BASE['scanner_extensions']) self.__handler = SupysonicWatcherEventHandler(config.BASE["scanner_extensions"])
self.__folders = {} self.__folders = {}
self.__queue = None self.__queue = None
@ -262,10 +270,10 @@ class SupysonicWatcher(object):
elif isinstance(folder, strtype): elif isinstance(folder, strtype):
path = folder path = folder
else: 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) 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 self.__folders[path] = watch
def remove_folder(self, folder): def remove_folder(self, folder):
@ -274,7 +282,7 @@ class SupysonicWatcher(object):
elif isinstance(folder, strtype): elif isinstance(folder, strtype):
path = folder path = folder
else: 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) logger.info("Unscheduling watcher for %s", path)
self.__observer.unschedule(self.__folders[path]) self.__observer.unschedule(self.__folders[path])
@ -309,4 +317,9 @@ class SupysonicWatcher(object):
@property @property
def running(self): 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()
)

View File

@ -24,61 +24,66 @@ from .utils import get_secret_key
logger = logging.getLogger(__package__) logger = logging.getLogger(__package__)
def create_application(config = None):
def create_application(config=None):
global app global app
# Flask! # Flask!
app = Flask(__name__) 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() config = IniConfig.from_common_locations()
app.config.from_object(config) app.config.from_object(config)
# Set loglevel # Set loglevel
logfile = app.config['WEBAPP']['log_file'] logfile = app.config["WEBAPP"]["log_file"]
if logfile: # pragma: nocover if logfile: # pragma: nocover
from logging.handlers import TimedRotatingFileHandler 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) logger.addHandler(handler)
loglevel = app.config['WEBAPP']['log_level'] loglevel = app.config["WEBAPP"]["log_level"]
if loglevel: if loglevel:
logger.setLevel(getattr(logging, loglevel.upper(), logging.NOTSET)) logger.setLevel(getattr(logging, loglevel.upper(), logging.NOTSET))
# Initialize database # Initialize database
init_database(app.config['BASE']['database_uri']) init_database(app.config["BASE"]["database_uri"])
app.wsgi_app = db_session(app.wsgi_app) app.wsgi_app = db_session(app.wsgi_app)
# Insert unknown mimetypes # Insert unknown mimetypes
for k, v in app.config['MIMETYPES'].items(): for k, v in app.config["MIMETYPES"].items():
extension = '.' + k.lower() extension = "." + k.lower()
if extension not in mimetypes.types_map: if extension not in mimetypes.types_map:
mimetypes.add_type(v, extension, False) mimetypes.add_type(v, extension, False)
# Initialize Cache objects # Initialize Cache objects
# Max size is MB in the config file but Cache expects bytes # Max size is MB in the config file but Cache expects bytes
cache_dir = app.config['WEBAPP']['cache_dir'] cache_dir = app.config["WEBAPP"]["cache_dir"]
max_size_cache = app.config['WEBAPP']['cache_size'] * 1024**2 max_size_cache = app.config["WEBAPP"]["cache_size"] * 1024 ** 2
max_size_transcodes = app.config['WEBAPP']['transcode_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.cache = Cache(path.join(cache_dir, "cache"), max_size_cache)
app.transcode_cache = Cache(path.join(cache_dir, "transcodes"), max_size_transcodes) app.transcode_cache = Cache(path.join(cache_dir, "transcodes"), max_size_transcodes)
# Test for the cache directory # 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): if not path.exists(cache_path):
makedirs(cache_path) # pragma: nocover makedirs(cache_path) # pragma: nocover
# Read or create secret key # Read or create secret key
app.secret_key = get_secret_key('cookies_secret') app.secret_key = get_secret_key("cookies_secret")
# Import app sections # Import app sections
if app.config['WEBAPP']['mount_webui']: if app.config["WEBAPP"]["mount_webui"]:
from .frontend import frontend from .frontend import frontend
app.register_blueprint(frontend) app.register_blueprint(frontend)
if app.config['WEBAPP']['mount_api']: if app.config["WEBAPP"]["mount_api"]:
from .api import api from .api import api
app.register_blueprint(api, url_prefix = '/rest')
app.register_blueprint(api, url_prefix="/rest")
return app return app

View File

@ -21,6 +21,7 @@ from .issue133 import Issue133TestCase
from .issue139 import Issue139TestCase from .issue139 import Issue139TestCase
from .issue148 import Issue148TestCase from .issue148 import Issue148TestCase
def suite(): def suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
@ -35,4 +36,3 @@ def suite():
suite.addTest(unittest.makeSuite(Issue148TestCase)) suite.addTest(unittest.makeSuite(Issue148TestCase))
return suite return suite

View File

@ -22,6 +22,7 @@ from .test_annotation import AnnotationTestCase
from .test_media import MediaTestCase from .test_media import MediaTestCase
from .test_transcoding import TranscodingTestCase from .test_transcoding import TranscodingTestCase
def suite(): def suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
@ -39,4 +40,3 @@ def suite():
suite.addTest(unittest.makeSuite(TranscodingTestCase)) suite.addTest(unittest.makeSuite(TranscodingTestCase))
return suite return suite

View File

@ -15,10 +15,11 @@ from supysonic.py23 import strtype
from ..testbase import TestBase 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): class ApiTestBase(TestBase):
__with_api__ = True __with_api__ = True
@ -26,7 +27,7 @@ class ApiTestBase(TestBase):
def setUp(self): def setUp(self):
super(ApiTestBase, self).setUp() 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) self.schema = etree.XMLSchema(xsd)
def _find(self, xml, path): def _find(self, xml, path):
@ -34,7 +35,7 @@ class ApiTestBase(TestBase):
Helper method that insert the namespace in ElementPath 'path' 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) return xml.find(path)
def _xpath(self, elem, 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' Helper method that insert a prefix and map the namespace in XPath 'path'
""" """
path = path_replace_regexp.sub(r'/sub:\1', path) path = path_replace_regexp.sub(r"/sub:\1", path)
return elem.xpath(path, namespaces = NSMAP) 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. 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. 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): if tag and not isinstance(tag, strtype):
raise TypeError("'tag', expecting a str, got " + type(tag).__name__) raise TypeError("'tag', expecting a str, got " + type(tag).__name__)
args.update({ 'c': 'tests', 'v': '1.9.0' }) args.update({"c": "tests", "v": "1.9.0"})
if 'u' not in args: if "u" not in args:
args.update({ 'u': 'alice', 'p': 'Alic3' }) args.update({"u": "alice", "p": "Alic3"})
uri = '/rest/{}.view'.format(endpoint) uri = "/rest/{}.view".format(endpoint)
rg = self.client.get(uri, query_string = args) rg = self.client.get(uri, query_string=args)
if not skip_post: if not skip_post:
rp = self.client.post(uri, data = args) rp = self.client.post(uri, data=args)
self.assertEqual(rg.data, rp.data) self.assertEqual(rg.data, rp.data)
xml = etree.fromstring(rg.data) xml = etree.fromstring(rg.data)
if not skip_xsd: if not skip_xsd:
self.schema.assert_(xml) self.schema.assert_(xml)
if xml.get('status') == 'ok': if xml.get("status") == "ok":
self.assertIsNone(error) self.assertIsNone(error)
if tag: if tag:
self.assertEqual(xml[0].tag, '{{{}}}{}'.format(NS, tag)) self.assertEqual(xml[0].tag, "{{{}}}{}".format(NS, tag))
return rg, xml[0] return rg, xml[0]
else: else:
self.assertEqual(len(xml), 0) self.assertEqual(len(xml), 0)
return rg, None return rg, None
else: else:
self.assertIsNone(tag) self.assertIsNone(tag)
self.assertEqual(xml[0].tag, '{{{}}}error'.format(NS)) self.assertEqual(xml[0].tag, "{{{}}}error".format(NS))
self.assertEqual(xml[0].get('code'), str(error)) self.assertEqual(xml[0].get("code"), str(error))
return rg return rg

View File

@ -16,6 +16,7 @@ from supysonic.db import Folder, Artist, Album, Track
from .apitestbase import ApiTestBase from .apitestbase import ApiTestBase
class AlbumSongsTestCase(ApiTestBase): class AlbumSongsTestCase(ApiTestBase):
# I'm too lazy to write proper tests concerning the data on those endpoints # I'm too lazy to write proper tests concerning the data on those endpoints
# Let's just check paramter validation and ensure coverage # Let's just check paramter validation and ensure coverage
@ -24,82 +25,127 @@ class AlbumSongsTestCase(ApiTestBase):
super(AlbumSongsTestCase, self).setUp() super(AlbumSongsTestCase, self).setUp()
with db_session: with db_session:
folder = Folder(name = 'Root', root = True, path = 'tests/assets') folder = Folder(name="Root", root=True, path="tests/assets")
artist = Artist(name = 'Artist') artist = Artist(name="Artist")
album = Album(name = 'Album', artist = artist) album = Album(name="Album", artist=artist)
track = Track( track = Track(
title = 'Track', title="Track",
album = album, album=album,
artist = artist, artist=artist,
disc = 1, disc=1,
number = 1, number=1,
path = 'tests/assets/empty', path="tests/assets/empty",
folder = folder, folder=folder,
root_folder = folder, root_folder=folder,
duration = 2, duration=2,
bitrate = 320, bitrate=320,
last_modification = 0 last_modification=0,
) )
def test_get_album_list(self): def test_get_album_list(self):
self._make_request('getAlbumList', error = 10) self._make_request("getAlbumList", error=10)
self._make_request('getAlbumList', { 'type': 'kraken' }, error = 0) self._make_request("getAlbumList", {"type": "kraken"}, error=0)
self._make_request('getAlbumList', { 'type': 'random', 'size': 'huge' }, 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", {"type": "newest", "offset": "minus one"}, error=0
)
types = [ 'random', 'newest', 'highest', 'frequent', 'recent', 'alphabeticalByName', types = [
'alphabeticalByArtist', 'starred' ] "random",
"newest",
"highest",
"frequent",
"recent",
"alphabeticalByName",
"alphabeticalByArtist",
"starred",
]
for t in types: 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: with db_session:
Folder.get().delete() 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) self.assertEqual(len(child), 0)
def test_get_album_list2(self): def test_get_album_list2(self):
self._make_request('getAlbumList2', error = 10) self._make_request("getAlbumList2", error=10)
self._make_request('getAlbumList2', { 'type': 'void' }, error = 0) self._make_request("getAlbumList2", {"type": "void"}, error=0)
self._make_request('getAlbumList2', { 'type': 'random', 'size': 'size_t' }, error = 0) self._make_request(
self._make_request('getAlbumList2', { 'type': 'newest', 'offset': '&v + 2' }, error = 0) "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: 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: with db_session:
Track.get().delete() Track.get().delete()
Album.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) self.assertEqual(len(child), 0)
def test_get_random_songs(self): def test_get_random_songs(self):
self._make_request('getRandomSongs', { 'size': '8 floors' }, error = 0) self._make_request("getRandomSongs", {"size": "8 floors"}, error=0)
self._make_request('getRandomSongs', { 'fromYear': 'year' }, error = 0) self._make_request("getRandomSongs", {"fromYear": "year"}, error=0)
self._make_request('getRandomSongs', { 'toYear': 'year' }, error = 0) self._make_request("getRandomSongs", {"toYear": "year"}, error=0)
self._make_request('getRandomSongs', { 'musicFolderId': 'idid' }, error = 0) self._make_request("getRandomSongs", {"musicFolderId": "idid"}, error=0)
self._make_request('getRandomSongs', { 'musicFolderId': uuid.uuid4() }, error = 70) 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: with db_session:
fid = Folder.get().id 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): def test_now_playing(self):
self._make_request('getNowPlaying', tag = 'nowPlaying') self._make_request("getNowPlaying", tag="nowPlaying")
def test_get_starred(self): def test_get_starred(self):
self._make_request('getStarred', tag = 'starred') self._make_request("getStarred", tag="starred")
def test_get_starred2(self): 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() unittest.main()

View File

@ -16,145 +16,209 @@ from supysonic.db import Folder, Artist, Album, Track, User, ClientPrefs
from .apitestbase import ApiTestBase from .apitestbase import ApiTestBase
class AnnotationTestCase(ApiTestBase): class AnnotationTestCase(ApiTestBase):
def setUp(self): def setUp(self):
super(AnnotationTestCase, self).setUp() super(AnnotationTestCase, self).setUp()
with db_session: with db_session:
root = Folder(name = 'Root', root = True, path = 'tests') root = Folder(name="Root", root=True, path="tests")
folder = Folder(name = 'Folder', path = 'tests/assets', parent = root) folder = Folder(name="Folder", path="tests/assets", parent=root)
artist = Artist(name = 'Artist') artist = Artist(name="Artist")
album = Album(name = 'Album', artist = artist) album = Album(name="Album", artist=artist)
track = Track( track = Track(
title = 'Track', title="Track",
album = album, album=album,
artist = artist, artist=artist,
disc = 1, disc=1,
number = 1, number=1,
path = 'tests/assets/empty', path="tests/assets/empty",
folder = folder, folder=folder,
root_folder = root, root_folder=root,
duration = 2, duration=2,
bitrate = 320, bitrate=320,
last_modification = 0 last_modification=0,
) )
self.folderid = folder.id self.folderid = folder.id
self.artistid = artist.id self.artistid = artist.id
self.albumid = album.id self.albumid = album.id
self.trackid = track.id self.trackid = track.id
self.user = User.get(name = 'alice') self.user = User.get(name="alice")
def test_star(self): def test_star(self):
self._make_request('star', error = 10) self._make_request("star", error=10)
self._make_request('star', { 'id': 'unknown' }, error = 0, skip_xsd = True) self._make_request("star", {"id": "unknown"}, error=0, skip_xsd=True)
self._make_request('star', { 'albumId': 'unknown' }, error = 0) self._make_request("star", {"albumId": "unknown"}, error=0)
self._make_request('star', { 'artistId': '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", {"id": str(uuid.uuid4())}, error=70, skip_xsd=True)
self._make_request('star', { 'albumId': str(uuid.uuid4()) }, error = 70) self._make_request("star", {"albumId": str(uuid.uuid4())}, error=70)
self._make_request('star', { 'artistId': 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.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.albumid)}, error=70, skip_xsd=True)
self._make_request('star', { 'id': str(self.trackid) }, skip_post = True) self._make_request("star", {"id": str(self.trackid)}, skip_post=True)
with db_session: with db_session:
prefs = ClientPrefs.get(lambda p: p.user.name == 'alice' and p.client_name == 'tests') prefs = ClientPrefs.get(
self.assertIn('starred', Track[self.trackid].as_subsonic_child(self.user, prefs)) lambda p: p.user.name == "alice" and p.client_name == "tests"
self._make_request('star', { 'id': str(self.trackid) }, error = 0, skip_xsd = True) )
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: with db_session:
self.assertIn('starred', Folder[self.folderid].as_subsonic_child(self.user)) 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", {"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.folderid)}, error=70)
self._make_request('star', { 'albumId': str(self.artistid) }, 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.trackid)}, error=70)
self._make_request('star', { 'albumId': str(self.albumid) }, skip_post = True) self._make_request("star", {"albumId": str(self.albumid)}, skip_post=True)
with db_session: with db_session:
self.assertIn('starred', Album[self.albumid].as_subsonic_album(self.user)) 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", {"albumId": str(self.albumid)}, error=0)
self._make_request('star', { 'artistId': str(self.folderid) }, error = 70) 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.albumid)}, error=70)
self._make_request('star', { 'artistId': str(self.trackid) }, 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.artistid)}, skip_post=True)
with db_session: with db_session:
self.assertIn('starred', Artist[self.artistid].as_subsonic_artist(self.user)) self.assertIn(
self._make_request('star', { 'artistId': str(self.artistid) }, error = 0) "starred", Artist[self.artistid].as_subsonic_artist(self.user)
)
self._make_request("star", {"artistId": str(self.artistid)}, error=0)
def test_unstar(self): 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", error=10)
self._make_request('unstar', { 'id': 'unknown' }, error = 0, skip_xsd = True) self._make_request("unstar", {"id": "unknown"}, error=0, skip_xsd=True)
self._make_request('unstar', { 'albumId': 'unknown' }, error = 0) self._make_request("unstar", {"albumId": "unknown"}, error=0)
self._make_request('unstar', { 'artistId': '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: with db_session:
prefs = ClientPrefs.get(lambda p: p.user.name == 'alice' and p.client_name == 'tests') prefs = ClientPrefs.get(
self.assertNotIn('starred', Track[self.trackid].as_subsonic_child(self.user, prefs)) 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: 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: 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: 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): def test_set_rating(self):
self._make_request('setRating', error = 10) self._make_request("setRating", error=10)
self._make_request('setRating', { 'id': str(self.trackid) }, error = 10) self._make_request("setRating", {"id": str(self.trackid)}, error=10)
self._make_request('setRating', { 'rating': 3 }, 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": "string", "rating": 3}, error=0)
self._make_request('setRating', { 'id': str(uuid.uuid4()), 'rating': 3 }, error = 70) self._make_request(
self._make_request('setRating', { 'id': str(self.artistid), 'rating': 3 }, error = 70) "setRating", {"id": str(uuid.uuid4()), "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(
self._make_request('setRating', { 'id': str(self.trackid), 'rating': -1 }, error = 0) "setRating", {"id": str(self.artistid), "rating": 3}, error=70
self._make_request('setRating', { 'id': str(self.trackid), 'rating': 6 }, error = 0) )
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: with db_session:
prefs = ClientPrefs.get(lambda p: p.user.name == 'alice' and p.client_name == 'tests') prefs = ClientPrefs.get(
self.assertNotIn('userRating', Track[self.trackid].as_subsonic_child(self.user, prefs)) 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): 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: with db_session:
prefs = ClientPrefs.get(lambda p: p.user.name == 'alice' and p.client_name == 'tests') prefs = ClientPrefs.get(
self.assertEqual(Track[self.trackid].as_subsonic_child(self.user, prefs)['userRating'], i) 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: with db_session:
prefs = ClientPrefs.get(lambda p: p.user.name == 'alice' and p.client_name == 'tests') prefs = ClientPrefs.get(
self.assertNotIn('userRating', Track[self.trackid].as_subsonic_child(self.user, prefs)) 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): 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: with db_session:
self.assertEqual(Folder[self.folderid].as_subsonic_child(self.user)['userRating'], i) self.assertEqual(
self._make_request('setRating', { 'id': str(self.folderid), 'rating': 0 }, skip_post = True) 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: 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): def test_scrobble(self):
self._make_request('scrobble', error = 10) self._make_request("scrobble", error=10)
self._make_request('scrobble', { 'id': 'song' }, error = 0) self._make_request("scrobble", {"id": "song"}, error=0)
self._make_request('scrobble', { 'id': str(uuid.uuid4()) }, error = 70) 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.folderid)}, error=70)
self._make_request('scrobble', { 'id': str(self.trackid) }) 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": True})
self._make_request('scrobble', { 'id': str(self.trackid), 'submission': False }) self._make_request("scrobble", {"id": str(self.trackid), "submission": False})
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -17,6 +17,7 @@ from xml.etree import ElementTree
from ..testbase import TestBase from ..testbase import TestBase
from ..utils import hexlify from ..utils import hexlify
class ApiSetupTestCase(TestBase): class ApiSetupTestCase(TestBase):
__with_api__ = True __with_api__ = True
@ -25,43 +26,49 @@ class ApiSetupTestCase(TestBase):
self._patch_client() self._patch_client()
def __basic_auth_get(self, username, password): def __basic_auth_get(self, username, password):
hashed = base64.b64encode('{}:{}'.format(username, password).encode('utf-8')) hashed = base64.b64encode("{}:{}".format(username, password).encode("utf-8"))
headers = { 'Authorization': 'Basic ' + hashed.decode('utf-8') } headers = {"Authorization": "Basic " + hashed.decode("utf-8")}
return self.client.get('/rest/ping.view', headers = headers, query_string = { 'c': 'tests' }) return self.client.get(
"/rest/ping.view", headers=headers, query_string={"c": "tests"}
)
def __query_params_auth_get(self, username, password): 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): 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): 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): 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): def __test_auth(self, method):
# non-existent user # non-existent user
rv = method('null', 'null') rv = method("null", "null")
self.assertEqual(rv.status_code, 401) self.assertEqual(rv.status_code, 401)
self.assertIn('status="failed"', rv.data) self.assertIn('status="failed"', rv.data)
self.assertIn('code="40"', rv.data) self.assertIn('code="40"', rv.data)
# user request with bad password # user request with bad password
rv = method('alice', 'wrong password') rv = method("alice", "wrong password")
self.assertEqual(rv.status_code, 401) self.assertEqual(rv.status_code, 401)
self.assertIn('status="failed"', rv.data) self.assertIn('status="failed"', rv.data)
self.assertIn('code="40"', rv.data) self.assertIn('code="40"', rv.data)
# user request # user request
rv = method('alice', 'Alic3') rv = method("alice", "Alic3")
self.assertEqual(rv.status_code, 200) self.assertEqual(rv.status_code, 200)
self.assertIn('status="ok"', rv.data) self.assertIn('status="ok"', rv.data)
def test_auth_basic(self): def test_auth_basic(self):
# No auth info # 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.assertEqual(rv.status_code, 400)
self.assertIn('status="failed"', rv.data) self.assertIn('status="failed"', rv.data)
self.assertIn('code="10"', rv.data) self.assertIn('code="10"', rv.data)
@ -69,7 +76,7 @@ class ApiSetupTestCase(TestBase):
self.__test_auth(self.__basic_auth_get) self.__test_auth(self.__basic_auth_get)
# Shouldn't accept 'enc:' passwords # 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.assertEqual(rv.status_code, 401)
self.assertIn('status="failed"', rv.data) self.assertIn('status="failed"', rv.data)
self.assertIn('code="40"', rv.data) self.assertIn('code="40"', rv.data)
@ -83,72 +90,85 @@ class ApiSetupTestCase(TestBase):
self.__test_auth(self.__form_auth_enc_post) self.__test_auth(self.__form_auth_enc_post)
def test_required_client(self): 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('status="failed"', rv.data)
self.assertIn('code="10"', 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) self.assertIn('status="ok"', rv.data)
def test_format(self): def test_format(self):
args = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests' } args = {"u": "alice", "p": "Alic3", "c": "tests"}
rv = self.client.get('/rest/getLicense.view', query_string = args) rv = self.client.get("/rest/getLicense.view", query_string=args)
self.assertEqual(rv.status_code, 200) 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) self.assertIn('status="ok"', rv.data)
xml = ElementTree.fromstring(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' }) args.update({"f": "json"})
rv = self.client.get('/rest/getLicense.view', query_string = args) rv = self.client.get("/rest/getLicense.view", query_string=args)
self.assertEqual(rv.status_code, 200) self.assertEqual(rv.status_code, 200)
self.assertEqual(rv.mimetype, 'application/json') self.assertEqual(rv.mimetype, "application/json")
json = flask.json.loads(rv.data) json = flask.json.loads(rv.data)
self.assertIn('subsonic-response', json) self.assertIn("subsonic-response", json)
self.assertEqual(json['subsonic-response']['status'], 'ok') self.assertEqual(json["subsonic-response"]["status"], "ok")
self.assertIn('license', json['subsonic-response']) self.assertIn("license", json["subsonic-response"])
args.update({ 'f': 'jsonp' }) args.update({"f": "jsonp"})
rv = self.client.get('/rest/getLicense.view', query_string = args) rv = self.client.get("/rest/getLicense.view", query_string=args)
self.assertEqual(rv.mimetype, 'application/json') self.assertEqual(rv.mimetype, "application/json")
json = flask.json.loads(rv.data) json = flask.json.loads(rv.data)
self.assertIn('subsonic-response', json) self.assertIn("subsonic-response", json)
self.assertEqual(json['subsonic-response']['status'], 'failed') self.assertEqual(json["subsonic-response"]["status"], "failed")
self.assertEqual(json['subsonic-response']['error']['code'], 10) self.assertEqual(json["subsonic-response"]["error"]["code"], 10)
args.update({ 'callback': 'dummy_cb' }) args.update({"callback": "dummy_cb"})
rv = self.client.get('/rest/getLicense.view', query_string = args) rv = self.client.get("/rest/getLicense.view", query_string=args)
self.assertEqual(rv.status_code, 200) self.assertEqual(rv.status_code, 200)
self.assertEqual(rv.mimetype, 'application/javascript') self.assertEqual(rv.mimetype, "application/javascript")
self.assertTrue(rv.data.startswith('dummy_cb({')) self.assertTrue(rv.data.startswith("dummy_cb({"))
self.assertTrue(rv.data.endswith('})')) self.assertTrue(rv.data.endswith("})"))
json = flask.json.loads(rv.data[9:-1]) json = flask.json.loads(rv.data[9:-1])
self.assertIn('subsonic-response', json) self.assertIn("subsonic-response", json)
self.assertEqual(json['subsonic-response']['status'], 'ok') self.assertEqual(json["subsonic-response"]["status"], "ok")
self.assertIn('license', json['subsonic-response']) self.assertIn("license", json["subsonic-response"])
def test_not_implemented(self): def test_not_implemented(self):
# Access to not implemented/unknown endpoint # 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.assertEqual(rv.status_code, 404)
self.assertIn('status="failed"', rv.data) self.assertIn('status="failed"', rv.data)
self.assertIn('code="0"', 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.assertEqual(rv.status_code, 404)
self.assertIn('status="failed"', rv.data) self.assertIn('status="failed"', rv.data)
self.assertIn('code="0"', 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.assertEqual(rv.status_code, 501)
self.assertIn('status="failed"', rv.data) self.assertIn('status="failed"', rv.data)
self.assertIn('code="0"', 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.assertEqual(rv.status_code, 501)
self.assertIn('status="failed"', rv.data) self.assertIn('status="failed"', rv.data)
self.assertIn('code="0"', rv.data) self.assertIn('code="0"', rv.data)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -18,45 +18,48 @@ from supysonic.db import Folder, Artist, Album, Track
from .apitestbase import ApiTestBase from .apitestbase import ApiTestBase
class BrowseTestCase(ApiTestBase): class BrowseTestCase(ApiTestBase):
def setUp(self): def setUp(self):
super(BrowseTestCase, self).setUp() super(BrowseTestCase, self).setUp()
with db_session: with db_session:
Folder(root = True, name = 'Empty root', path = '/tmp') Folder(root=True, name="Empty root", path="/tmp")
root = Folder(root = True, name = 'Root folder', path = 'tests/assets') root = Folder(root=True, name="Root folder", path="tests/assets")
for letter in 'ABC': for letter in "ABC":
folder = Folder( folder = Folder(
name = letter + 'rtist', name=letter + "rtist",
path = 'tests/assets/{}rtist'.format(letter), path="tests/assets/{}rtist".format(letter),
parent = root parent=root,
) )
artist = Artist(name = letter + 'rtist') artist = Artist(name=letter + "rtist")
for lether in 'AB': for lether in "AB":
afolder = Folder( afolder = Folder(
name = letter + lether + 'lbum', name=letter + lether + "lbum",
path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether), path="tests/assets/{0}rtist/{0}{1}lbum".format(letter, lether),
parent = folder 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( track = Track(
disc = 1, disc=1,
number = num, number=num,
title = song, title=song,
duration = 2, duration=2,
album = album, album=album,
artist = artist, artist=artist,
bitrate = 320, bitrate=320,
path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song), path="tests/assets/{0}rtist/{0}{1}lbum/{2}".format(
last_modification = 0, letter, lether, song
root_folder = root, ),
folder = afolder last_modification=0,
root_folder=root,
folder=afolder,
) )
self.assertEqual(Folder.select().count(), 11) self.assertEqual(Folder.select().count(), 11)
@ -68,107 +71,136 @@ class BrowseTestCase(ApiTestBase):
def test_get_music_folders(self): 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 # 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 :/ # 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.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): def test_get_indexes(self):
self._make_request('getIndexes', { 'musicFolderId': 'abcdef' }, error = 0) self._make_request("getIndexes", {"musicFolderId": "abcdef"}, error=0)
self._make_request('getIndexes', { 'musicFolderId': str(uuid.uuid4()) }, error = 70) self._make_request("getIndexes", {"musicFolderId": str(uuid.uuid4())}, error=70)
self._make_request('getIndexes', { 'ifModifiedSince': 'quoi' }, error = 0) 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) self.assertEqual(len(child), 0)
with db_session: with db_session:
fid = Folder.get(name = 'Empty root').id fid = Folder.get(name="Empty root").id
rv, child = self._make_request('getIndexes', { 'musicFolderId': str(fid) }, tag = 'indexes') rv, child = self._make_request(
"getIndexes", {"musicFolderId": str(fid)}, tag="indexes"
)
self.assertEqual(len(child), 0) 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) self.assertEqual(len(child), 3)
for i, letter in enumerate([ 'A', 'B', 'C' ]): for i, letter in enumerate(["A", "B", "C"]):
self.assertEqual(child[i].get('name'), letter) self.assertEqual(child[i].get("name"), letter)
self.assertEqual(len(child[i]), 1) 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): def test_get_music_directory(self):
self._make_request('getMusicDirectory', error = 10) self._make_request("getMusicDirectory", error=10)
self._make_request('getMusicDirectory', { 'id': 'id' }, error = 0) self._make_request("getMusicDirectory", {"id": "id"}, error=0)
self._make_request('getMusicDirectory', { 'id': str(uuid.uuid4()) }, error = 70) 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 # should test with folders with both children folders and tracks. this code would break in that case
with db_session: with db_session:
for f in Folder.select(): for f in Folder.select():
rv, child = self._make_request('getMusicDirectory', { 'id': str(f.id) }, tag = 'directory') rv, child = self._make_request(
self.assertEqual(child.get('id'), str(f.id)) "getMusicDirectory", {"id": str(f.id)}, tag="directory"
self.assertEqual(child.get('name'), f.name) )
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()) 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'))): for dbc, xmlc in zip(
self.assertEqual(dbc.name, xmlc.get('title')) sorted(f.children, key=lambda c: c.name),
self.assertEqual(xmlc.get('artist'), f.name) sorted(child, key=lambda c: c.get("title")),
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(dbc.name, xmlc.get("title"))
self.assertEqual(t.title, xmlc.get('title')) self.assertEqual(xmlc.get("artist"), f.name)
self.assertEqual(xmlc.get('parent'), str(f.id)) 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): def test_get_artists(self):
# same as getIndexes standard case # same as getIndexes standard case
# dataset should be improved to have a different directory structure than /root/Artist/Album/Track # 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) self.assertEqual(len(child), 3)
for i, letter in enumerate([ 'A', 'B', 'C' ]): for i, letter in enumerate(["A", "B", "C"]):
self.assertEqual(child[i].get('name'), letter) self.assertEqual(child[i].get("name"), letter)
self.assertEqual(len(child[i]), 1) 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): def test_get_artist(self):
# dataset should be improved to have tracks by a different artist than the album's artist # 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", error=10)
self._make_request('getArtist', { 'id': 'artist' }, error = 0) self._make_request("getArtist", {"id": "artist"}, error=0)
self._make_request('getArtist', { 'id': str(uuid.uuid4()) }, error = 70) self._make_request("getArtist", {"id": str(uuid.uuid4())}, error=70)
with db_session: with db_session:
for ar in Artist.select(): for ar in Artist.select():
rv, child = self._make_request('getArtist', { 'id': str(ar.id) }, tag = 'artist') rv, child = self._make_request(
self.assertEqual(child.get('id'), str(ar.id)) "getArtist", {"id": str(ar.id)}, tag="artist"
self.assertEqual(child.get('albumCount'), str(len(child))) )
self.assertEqual(child.get("id"), str(ar.id))
self.assertEqual(child.get("albumCount"), str(len(child)))
self.assertEqual(len(child), ar.albums.count()) 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'))): for dal, xal in zip(
self.assertEqual(dal.name, xal.get('name')) sorted(ar.albums, key=lambda a: a.name),
self.assertEqual(xal.get('artist'), ar.name) # could break with a better dataset sorted(child, key=lambda c: c.get("name")),
self.assertEqual(xal.get('artistId'), str(ar.id)) # see above ):
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): def test_get_album(self):
self._make_request('getAlbum', error = 10) self._make_request("getAlbum", error=10)
self._make_request('getAlbum', { 'id': 'nastynasty' }, error = 0) self._make_request("getAlbum", {"id": "nastynasty"}, error=0)
self._make_request('getAlbum', { 'id': str(uuid.uuid4()) }, error = 70) self._make_request("getAlbum", {"id": str(uuid.uuid4())}, error=70)
with db_session: with db_session:
a = Album.select().first() a = Album.select().first()
rv, child = self._make_request('getAlbum', { 'id': str(a.id) }, tag = 'album') rv, child = self._make_request("getAlbum", {"id": str(a.id)}, tag="album")
self.assertEqual(child.get('id'), str(a.id)) self.assertEqual(child.get("id"), str(a.id))
self.assertEqual(child.get('songCount'), str(len(child))) self.assertEqual(child.get("songCount"), str(len(child)))
self.assertEqual(len(child), a.tracks.count()) 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'))): for dal, xal in zip(
self.assertEqual(dal.title, xal.get('title')) sorted(a.tracks, key=lambda t: t.title),
self.assertEqual(xal.get('album'), a.name) sorted(child, key=lambda c: c.get("title")),
self.assertEqual(xal.get('albumId'), str(a.id)) ):
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): def test_get_song(self):
self._make_request('getSong', error = 10) self._make_request("getSong", error=10)
self._make_request('getSong', { 'id': 'nastynasty' }, error = 0) self._make_request("getSong", {"id": "nastynasty"}, error=0)
self._make_request('getSong', { 'id': str(uuid.uuid4()) }, error = 70) self._make_request("getSong", {"id": str(uuid.uuid4())}, error=70)
with db_session: with db_session:
s = Track.select().first() 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): 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() unittest.main()

View File

@ -14,32 +14,41 @@ import time
from .apitestbase import ApiTestBase from .apitestbase import ApiTestBase
class ChatTestCase(ApiTestBase): class ChatTestCase(ApiTestBase):
def test_add_message(self): def test_add_message(self):
self._make_request('addChatMessage', error = 10) self._make_request("addChatMessage", error=10)
rv, child = self._make_request('getChatMessages', tag = 'chatMessages') rv, child = self._make_request("getChatMessages", tag="chatMessages")
self.assertEqual(len(child), 0) self.assertEqual(len(child), 0)
self._make_request('addChatMessage', { 'message': 'Heres a message' }, skip_post = True) self._make_request(
rv, child = self._make_request('getChatMessages', tag = 'chatMessages') "addChatMessage", {"message": "Heres a message"}, skip_post=True
)
rv, child = self._make_request("getChatMessages", tag="chatMessages")
self.assertEqual(len(child), 1) self.assertEqual(len(child), 1)
self.assertEqual(child[0].get('username'), 'alice') self.assertEqual(child[0].get("username"), "alice")
self.assertEqual(child[0].get('message'), 'Heres a message') self.assertEqual(child[0].get("message"), "Heres a message")
def test_get_messages(self): 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) 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) 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(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() unittest.main()

View File

@ -21,167 +21,222 @@ from supysonic.db import Folder, Artist, Album, Track
from .apitestbase import ApiTestBase from .apitestbase import ApiTestBase
class MediaTestCase(ApiTestBase): class MediaTestCase(ApiTestBase):
def setUp(self): def setUp(self):
super(MediaTestCase, self).setUp() super(MediaTestCase, self).setUp()
with db_session: with db_session:
folder = Folder( folder = Folder(
name = 'Root', name="Root",
path = os.path.abspath('tests/assets'), path=os.path.abspath("tests/assets"),
root = True, root=True,
cover_art = 'cover.jpg' cover_art="cover.jpg",
) )
self.folderid = folder.id self.folderid = folder.id
artist = Artist(name = 'Artist') artist = Artist(name="Artist")
album = Album(artist = artist, name = 'Album') album = Album(artist=artist, name="Album")
track = Track( track = Track(
title = '23bytes', title="23bytes",
number = 1, number=1,
disc = 1, disc=1,
artist = artist, artist=artist,
album = album, album=album,
path = os.path.abspath('tests/assets/23bytes'), path=os.path.abspath("tests/assets/23bytes"),
root_folder = folder, root_folder=folder,
folder = folder, folder=folder,
duration = 2, duration=2,
bitrate = 320, bitrate=320,
last_modification = 0 last_modification=0,
) )
self.trackid = track.id 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)): for i in range(len(self.formats)):
track_embeded_art = Track( track_embeded_art = Track(
title = '[silence]', title="[silence]",
number = 1, number=1,
disc = 1, disc=1,
artist = artist, artist=artist,
album = album, album=album,
path = os.path.abspath('tests/assets/formats/silence.{0}'.format(self.formats[i][0])), path=os.path.abspath(
root_folder = folder, "tests/assets/formats/silence.{0}".format(self.formats[i][0])
folder = folder, ),
duration = 2, root_folder=folder,
bitrate = 320, folder=folder,
last_modification = 0 duration=2,
bitrate=320,
last_modification=0,
) )
self.formats[i] = track_embeded_art.id self.formats[i] = track_embeded_art.id
def test_stream(self): def test_stream(self):
self._make_request('stream', error = 10) self._make_request("stream", error=10)
self._make_request('stream', { 'id': 'string' }, error = 0) self._make_request("stream", {"id": "string"}, error=0)
self._make_request('stream', { 'id': str(uuid.uuid4()) }, error = 70) 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.folderid)}, error=70)
self._make_request('stream', { 'id': str(self.trackid), 'maxBitRate': 'string' }, error = 0) self._make_request(
self._make_request('stream', { 'id': str(self.trackid), 'timeOffset': 2 }, error = 0) "stream", {"id": str(self.trackid), "maxBitRate": "string"}, error=0
self._make_request('stream', { 'id': str(self.trackid), 'size': '640x480' }, 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(rv.status_code, 200)
self.assertEqual(len(rv.data), 23) self.assertEqual(len(rv.data), 23)
with db_session: with db_session:
self.assertEqual(Track[self.trackid].play_count, 1) self.assertEqual(Track[self.trackid].play_count, 1)
def test_download(self): def test_download(self):
self._make_request('download', error = 10) self._make_request("download", error=10)
self._make_request('download', { 'id': 'string' }, error = 0) self._make_request("download", {"id": "string"}, error=0)
self._make_request('download', { 'id': str(uuid.uuid4()) }, error = 70) self._make_request("download", {"id": str(uuid.uuid4())}, error=70)
# download single file # 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(rv.status_code, 200)
self.assertEqual(len(rv.data), 23) self.assertEqual(len(rv.data), 23)
with db_session: with db_session:
self.assertEqual(Track[self.trackid].play_count, 0) self.assertEqual(Track[self.trackid].play_count, 0)
# dowload folder # 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.status_code, 200)
self.assertEqual(rv.mimetype, 'application/zip') self.assertEqual(rv.mimetype, "application/zip")
def test_get_cover_art(self): def test_get_cover_art(self):
self._make_request('getCoverArt', error = 10) self._make_request("getCoverArt", error=10)
self._make_request('getCoverArt', { 'id': 'string' }, error = 0) self._make_request("getCoverArt", {"id": "string"}, error=0)
self._make_request('getCoverArt', { 'id': str(uuid.uuid4()) }, error = 70) 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.trackid)}, error=70)
self._make_request('getCoverArt', { 'id': str(self.folderid), 'size': 'large' }, error = 0) self._make_request(
"getCoverArt", {"id": str(self.folderid), "size": "large"}, error=0
)
args = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'id': str(self.folderid) } args = {"u": "alice", "p": "Alic3", "c": "tests", "id": str(self.folderid)}
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.status_code, 200)
self.assertEqual(rv.mimetype, 'image/jpeg') self.assertEqual(rv.mimetype, "image/jpeg")
im = Image.open(BytesIO(rv.data)) im = Image.open(BytesIO(rv.data))
self.assertEqual(im.format, 'JPEG') self.assertEqual(im.format, "JPEG")
self.assertEqual(im.size, (420, 420)) self.assertEqual(im.size, (420, 420))
args['size'] = 600 args["size"] = 600
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.status_code, 200)
self.assertEqual(rv.mimetype, 'image/jpeg') self.assertEqual(rv.mimetype, "image/jpeg")
im = Image.open(BytesIO(rv.data)) im = Image.open(BytesIO(rv.data))
self.assertEqual(im.format, 'JPEG') self.assertEqual(im.format, "JPEG")
self.assertEqual(im.size, (420, 420)) self.assertEqual(im.size, (420, 420))
args['size'] = 120 args["size"] = 120
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.status_code, 200)
self.assertEqual(rv.mimetype, 'image/jpeg') self.assertEqual(rv.mimetype, "image/jpeg")
im = Image.open(BytesIO(rv.data)) im = Image.open(BytesIO(rv.data))
self.assertEqual(im.format, 'JPEG') self.assertEqual(im.format, "JPEG")
self.assertEqual(im.size, (120, 120)) self.assertEqual(im.size, (120, 120))
# rerequest, just in case # 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.status_code, 200)
self.assertEqual(rv.mimetype, 'image/jpeg') self.assertEqual(rv.mimetype, "image/jpeg")
im = Image.open(BytesIO(rv.data)) im = Image.open(BytesIO(rv.data))
self.assertEqual(im.format, 'JPEG') self.assertEqual(im.format, "JPEG")
self.assertEqual(im.size, (120, 120)) self.assertEqual(im.size, (120, 120))
# TODO test non square covers # TODO test non square covers
# Test extracting cover art from embeded media # Test extracting cover art from embeded media
for args['id'] in self.formats: for args["id"] in self.formats:
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.status_code, 200)
self.assertEqual(rv.mimetype, 'image/png') self.assertEqual(rv.mimetype, "image/png")
im = Image.open(BytesIO(rv.data)) im = Image.open(BytesIO(rv.data))
self.assertEqual(im.format, 'PNG') self.assertEqual(im.format, "PNG")
self.assertEqual(im.size, (120, 120)) self.assertEqual(im.size, (120, 120))
def test_get_lyrics(self): def test_get_lyrics(self):
self._make_request('getLyrics', error = 10) self._make_request("getLyrics", error=10)
self._make_request('getLyrics', { 'artist': 'artist' }, error = 10) self._make_request("getLyrics", {"artist": "artist"}, error=10)
self._make_request('getLyrics', { 'title': 'title' }, error = 10) self._make_request("getLyrics", {"title": "title"}, error=10)
# Potentially skip the tests if ChartLyrics is down (which happens quite often) # Potentially skip the tests if ChartLyrics is down (which happens quite often)
try: try:
requests.get('http://api.chartlyrics.com/', timeout = 5) requests.get("http://api.chartlyrics.com/", timeout=5)
except requests.exceptions.Timeout: 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) self.assertIsNone(child.text)
# ChartLyrics # ChartLyrics
rv, child = self._make_request('getLyrics', { 'artist': 'The Clash', 'title': 'London Calling' }, tag = 'lyrics') rv, child = self._make_request(
self.assertIn('live by the river', child.text) "getLyrics",
{"artist": "The Clash", "title": "London Calling"},
tag="lyrics",
)
self.assertIn("live by the river", child.text)
# ChartLyrics, JSON format # ChartLyrics, JSON format
args = { 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'f': 'json', 'artist': 'The Clash', 'title': 'London Calling' } args = {
rv = self.client.get('/rest/getLyrics.view', query_string = 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) json = flask.json.loads(rv.data)
self.assertIn('value', json['subsonic-response']['lyrics']) self.assertIn("value", json["subsonic-response"]["lyrics"])
self.assertIn('live by the river', json['subsonic-response']['lyrics']['value']) self.assertIn("live by the river", json["subsonic-response"]["lyrics"]["value"])
# Local file # Local file
rv, child = self._make_request('getLyrics', { 'artist': 'artist', 'title': '23bytes' }, tag = 'lyrics') rv, child = self._make_request(
self.assertIn('null', child.text) "getLyrics", {"artist": "artist", "title": "23bytes"}, tag="lyrics"
)
self.assertIn("null", child.text)
def test_get_avatar(self): 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() unittest.main()

View File

@ -16,146 +16,196 @@ from supysonic.db import Folder, Artist, Album, Track, Playlist, User
from .apitestbase import ApiTestBase from .apitestbase import ApiTestBase
class PlaylistTestCase(ApiTestBase): class PlaylistTestCase(ApiTestBase):
def setUp(self): def setUp(self):
super(PlaylistTestCase, self).setUp() super(PlaylistTestCase, self).setUp()
with db_session: with db_session:
root = Folder(root = True, name = 'Root folder', path = 'tests/assets') root = Folder(root=True, name="Root folder", path="tests/assets")
artist = Artist(name = 'Artist') artist = Artist(name="Artist")
album = Album(name = 'Album', artist = artist) album = Album(name="Album", artist=artist)
songs = {} songs = {}
for num, song in enumerate([ 'One', 'Two', 'Three', 'Four' ]): for num, song in enumerate(["One", "Two", "Three", "Four"]):
track = Track( track = Track(
disc = 1, disc=1,
number = num, number=num,
title = song, title=song,
duration = 2, duration=2,
album = album, album=album,
artist = artist, artist=artist,
bitrate = 320, bitrate=320,
path = 'tests/assets/' + song, path="tests/assets/" + song,
last_modification = 0, last_modification=0,
root_folder = root, root_folder=root,
folder = root folder=root,
) )
songs[song] = track 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 = Playlist(user=users["alice"], name="Alice's")
playlist.add(songs['One']) playlist.add(songs["One"])
playlist.add(songs['Three']) playlist.add(songs["Three"])
playlist = Playlist(user = users['alice'], public = True, name = "Alice's public") playlist = Playlist(user=users["alice"], public=True, name="Alice's public")
playlist.add(songs['One']) playlist.add(songs["One"])
playlist.add(songs['Two']) playlist.add(songs["Two"])
playlist = Playlist(user = users['bob'], name = "Bob's") playlist = Playlist(user=users["bob"], name="Bob's")
playlist.add(songs['Two']) playlist.add(songs["Two"])
playlist.add(songs['Four']) playlist.add(songs["Four"])
def test_get_playlists(self): def test_get_playlists(self):
# get own playlists # 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(len(child), 2)
self.assertEqual(child[0].get('owner'), 'alice') self.assertEqual(child[0].get("owner"), "alice")
self.assertEqual(child[1].get('owner'), 'alice') self.assertEqual(child[1].get("owner"), "alice")
# get own and public # 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.assertEqual(len(child), 2)
self.assertTrue(child[0].get('owner') == 'alice' or child[1].get('owner') == 'alice') self.assertTrue(
self.assertTrue(child[0].get('owner') == 'bob' or child[1].get('owner') == 'bob') child[0].get("owner") == "alice" or child[1].get("owner") == "alice"
self.assertIsNotNone(self._find(child, "./playlist[@owner='alice'][@public='true']")) )
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 # 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(len(child), 1)
self.assertEqual(child[0].get('owner'), 'bob') self.assertEqual(child[0].get("owner"), "bob")
# get other when not admin # 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 # 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): def test_get_playlist(self):
# missing param # missing param
self._make_request('getPlaylist', error = 10) self._make_request("getPlaylist", error=10)
# invalid id # invalid id
self._make_request('getPlaylist', { 'id': 1234 }, error = 0) self._make_request("getPlaylist", {"id": 1234}, error=0)
# unknown # 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 # other's private from non admin
with db_session: with db_session:
playlist = Playlist.get(lambda p: not p.public and p.user.name == 'alice') 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) self._make_request(
"getPlaylist", {"u": "bob", "p": "B0b", "id": str(playlist.id)}, error=50
)
# standard # standard
rv, child = self._make_request('getPlaylists', tag = 'playlists') rv, child = self._make_request("getPlaylists", tag="playlists")
self._make_request('getPlaylist', { 'id': child[0].get('id') }, tag = 'playlist') self._make_request("getPlaylist", {"id": child[0].get("id")}, tag="playlist")
rv, child = self._make_request('getPlaylist', { 'id': child[1].get('id') }, tag = 'playlist') rv, child = self._make_request(
self.assertEqual(child.get('songCount'), '2') "getPlaylist", {"id": child[1].get("id")}, tag="playlist"
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.get("songCount"), "2")
self.assertEqual(child[0].get('title'), 'One') self.assertEqual(
self.assertTrue(child[1].get('title') == 'Two' or child[1].get('title') == 'Three') # depending on 'getPlaylists' result ordering 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): def test_create_playlist(self):
self._make_request('createPlaylist', error = 10) self._make_request("createPlaylist", error=10)
self._make_request('createPlaylist', { 'name': 'wrongId', 'songId': 'abc' }, error = 0) self._make_request(
self._make_request('createPlaylist', { 'name': 'unknownId', 'songId': str(uuid.uuid4()) }, error = 70) "createPlaylist", {"name": "wrongId", "songId": "abc"}, error=0
self._make_request('createPlaylist', { 'playlistId': 'abc' }, error = 0) )
self._make_request('createPlaylist', { 'playlistId': str(uuid.uuid4()) }, error = 70) 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 # create
self._make_request('createPlaylist', { 'name': 'new playlist' }, skip_post = True) self._make_request("createPlaylist", {"name": "new playlist"}, skip_post=True)
rv, child = self._make_request('getPlaylists', tag = 'playlists') rv, child = self._make_request("getPlaylists", tag="playlists")
self.assertEqual(len(child), 3) self.assertEqual(len(child), 3)
playlist = self._find(child, "./playlist[@name='new playlist']") playlist = self._find(child, "./playlist[@name='new playlist']")
self.assertEqual(len(playlist), 0) self.assertEqual(len(playlist), 0)
# "update" newly created # "update" newly created
self._make_request('createPlaylist', { 'playlistId': playlist.get('id') }) self._make_request("createPlaylist", {"playlistId": playlist.get("id")})
rv, child = self._make_request('getPlaylists', tag = 'playlists') rv, child = self._make_request("getPlaylists", tag="playlists")
self.assertEqual(len(child), 3) self.assertEqual(len(child), 3)
# renaming # renaming
self._make_request('createPlaylist', { 'playlistId': playlist.get('id'), 'name': 'renamed' }) self._make_request(
rv, child = self._make_request('getPlaylists', tag = 'playlists') "createPlaylist", {"playlistId": playlist.get("id"), "name": "renamed"}
)
rv, child = self._make_request("getPlaylists", tag="playlists")
self.assertEqual(len(child), 3) self.assertEqual(len(child), 3)
self.assertIsNone(self._find(child, "./playlist[@name='new playlist']")) self.assertIsNone(self._find(child, "./playlist[@name='new playlist']"))
playlist = self._find(child, "./playlist[@name='renamed']") playlist = self._find(child, "./playlist[@name='renamed']")
self.assertIsNotNone(playlist) self.assertIsNotNone(playlist)
# update from other user # 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 # create more useful playlist
with db_session: 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('createPlaylist', { 'name': 'songs', 'songId': list(map(lambda s: songs[s], [ 'Three', 'One', 'Two' ])) }, skip_post = True) self._make_request(
"createPlaylist",
{
"name": "songs",
"songId": list(map(lambda s: songs[s], ["Three", "One", "Two"])),
},
skip_post=True,
)
with db_session: with db_session:
playlist = Playlist.get(name = 'songs') playlist = Playlist.get(name="songs")
self.assertIsNotNone(playlist) self.assertIsNotNone(playlist)
rv, child = self._make_request('getPlaylist', { 'id': str(playlist.id) }, tag = 'playlist') rv, child = self._make_request(
self.assertEqual(child.get('songCount'), '3') "getPlaylist", {"id": str(playlist.id)}, tag="playlist"
self.assertEqual(self._xpath(child, 'count(./entry)'), 3) )
self.assertEqual(child[0].get('title'), 'Three') self.assertEqual(child.get("songCount"), "3")
self.assertEqual(child[1].get('title'), 'One') self.assertEqual(self._xpath(child, "count(./entry)"), 3)
self.assertEqual(child[2].get('title'), 'Two') self.assertEqual(child[0].get("title"), "Three")
self.assertEqual(child[1].get("title"), "One")
self.assertEqual(child[2].get("title"), "Two")
# update # update
self._make_request('createPlaylist', { 'playlistId': str(playlist.id), 'songId': songs['Two'] }, skip_post = True) self._make_request(
rv, child = self._make_request('getPlaylist', { 'id': str(playlist.id) }, tag = 'playlist') "createPlaylist",
self.assertEqual(child.get('songCount'), '1') {"playlistId": str(playlist.id), "songId": songs["Two"]},
self.assertEqual(self._xpath(child, 'count(./entry)'), 1) skip_post=True,
self.assertEqual(child[0].get('title'), 'Two') )
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 @db_session
def assertPlaylistCountEqual(self, count): def assertPlaylistCountEqual(self, count):
@ -163,79 +213,132 @@ class PlaylistTestCase(ApiTestBase):
def test_delete_playlist(self): def test_delete_playlist(self):
# check params # check params
self._make_request('deletePlaylist', error = 10) self._make_request("deletePlaylist", error=10)
self._make_request('deletePlaylist', { 'id': 'string' }, error = 0) self._make_request("deletePlaylist", {"id": "string"}, error=0)
self._make_request('deletePlaylist', { 'id': str(uuid.uuid4()) }, error = 70) self._make_request("deletePlaylist", {"id": str(uuid.uuid4())}, error=70)
# delete unowned when not admin # delete unowned when not admin
with db_session: with db_session:
playlist = Playlist.select(lambda p: p.user.name == 'alice').first() 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._make_request(
self.assertPlaylistCountEqual(3); "deletePlaylist", {"u": "bob", "p": "B0b", "id": str(playlist.id)}, error=50
)
self.assertPlaylistCountEqual(3)
# delete owned # delete owned
self._make_request('deletePlaylist', { 'id': str(playlist.id) }, skip_post = True) self._make_request("deletePlaylist", {"id": str(playlist.id)}, skip_post=True)
self.assertPlaylistCountEqual(2); self.assertPlaylistCountEqual(2)
self._make_request('deletePlaylist', { 'id': str(playlist.id) }, error = 70) self._make_request("deletePlaylist", {"id": str(playlist.id)}, error=70)
self.assertPlaylistCountEqual(2); self.assertPlaylistCountEqual(2)
# delete unowned when admin # delete unowned when admin
with db_session: with db_session:
playlist = Playlist.get(lambda p: p.user.name == 'bob') playlist = Playlist.get(lambda p: p.user.name == "bob")
self._make_request('deletePlaylist', { 'id': str(playlist.id) }, skip_post = True) self._make_request("deletePlaylist", {"id": str(playlist.id)}, skip_post=True)
self.assertPlaylistCountEqual(1); self.assertPlaylistCountEqual(1)
def test_update_playlist(self): def test_update_playlist(self):
self._make_request('updatePlaylist', error = 10) self._make_request("updatePlaylist", error=10)
self._make_request('updatePlaylist', { 'playlistId': 1234 }, error = 0) self._make_request("updatePlaylist", {"playlistId": 1234}, error=0)
self._make_request('updatePlaylist', { 'playlistId': str(uuid.uuid4()) }, error = 70) self._make_request(
"updatePlaylist", {"playlistId": str(uuid.uuid4())}, error=70
)
with db_session: 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) pid = str(playlist.id)
self._make_request('updatePlaylist', { 'playlistId': pid, 'songIdToAdd': 'string' }, error = 0) self._make_request(
self._make_request('updatePlaylist', { 'playlistId': pid, 'songIndexToRemove': 'string' }, error = 0) "updatePlaylist", {"playlistId": pid, "songIdToAdd": "string"}, error=0
)
self._make_request(
"updatePlaylist",
{"playlistId": pid, "songIndexToRemove": "string"},
error=0,
)
name = str(playlist.name) name = str(playlist.name)
self._make_request('updatePlaylist', { 'u': 'bob', 'p': 'B0b', 'playlistId': pid, 'name': 'new name' }, error = 50) self._make_request(
rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') "updatePlaylist",
self.assertEqual(child.get('name'), name) {"u": "bob", "p": "B0b", "playlistId": pid, "name": "new name"},
self.assertEqual(self._xpath(child, 'count(./entry)'), 2) 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) self._make_request(
rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') "updatePlaylist", {"playlistId": pid, "name": "new name"}, skip_post=True
self.assertEqual(child.get('name'), 'new name') )
self.assertEqual(self._xpath(child, 'count(./entry)'), 2) 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) self._make_request(
rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') "updatePlaylist",
self.assertEqual(self._xpath(child, 'count(./entry)'), 2) {"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) self._make_request(
rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') "updatePlaylist",
self.assertEqual(self._xpath(child, 'count(./entry)'), 1) {"playlistId": pid, "songIndexToRemove": [0, 2]},
self.assertEqual(self._find(child, './entry').get('title'), 'Three') 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: 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) self._make_request(
rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') "updatePlaylist",
self.assertSequenceEqual(self._xpath(child, './entry/@title'), [ 'Three', 'One', 'Two', 'Two' ]) {
"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) self._make_request(
rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') "updatePlaylist",
self.assertSequenceEqual(self._xpath(child, './entry/@title'), [ 'Three', 'Two' ]) {"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(
self._make_request('updatePlaylist', { 'playlistId': pid, 'songIndexToRemove': [ 1, 1 ] }, skip_post = True) "updatePlaylist",
rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') {"playlistId": pid, "songIdToAdd": songs["One"]},
self.assertSequenceEqual(self._xpath(child, './entry/@title'), [ 'Three', '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) self._make_request(
rv, child = self._make_request('getPlaylist', { 'id': pid }, tag = 'playlist') "updatePlaylist",
self.assertEqual(self._xpath(child, 'count(./entry)'), 2) {"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() unittest.main()

View File

@ -18,207 +18,185 @@ from supysonic.py23 import strtype
from ..testbase import TestBase from ..testbase import TestBase
class UnwrapperMixin(object): class UnwrapperMixin(object):
def make_response(self, elem, data): def make_response(self, elem, data):
with self.request_context(): with self.request_context():
rv = super(UnwrapperMixin, self).make_response(elem, data) rv = super(UnwrapperMixin, self).make_response(elem, data)
return rv.get_data(as_text = True) return rv.get_data(as_text=True)
@staticmethod @staticmethod
def create_from(cls): def create_from(cls):
class Unwrapper(UnwrapperMixin, cls): class Unwrapper(UnwrapperMixin, cls):
pass pass
return Unwrapper return Unwrapper
class ResponseHelperJsonTestCase(TestBase, UnwrapperMixin.create_from(JSONFormatter)): class ResponseHelperJsonTestCase(TestBase, UnwrapperMixin.create_from(JSONFormatter)):
def make_response(self, elem, data): def make_response(self, elem, data):
rv = super(ResponseHelperJsonTestCase, self).make_response(elem, data) rv = super(ResponseHelperJsonTestCase, self).make_response(elem, data)
return flask.json.loads(rv) return flask.json.loads(rv)
def process_and_extract(self, d): 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): def test_basic(self):
empty = self.empty empty = self.empty
self.assertEqual(len(empty), 1) self.assertEqual(len(empty), 1)
self.assertIn('subsonic-response', empty) self.assertIn("subsonic-response", empty)
self.assertIsInstance(empty['subsonic-response'], dict) self.assertIsInstance(empty["subsonic-response"], dict)
resp = empty['subsonic-response'] resp = empty["subsonic-response"]
self.assertEqual(len(resp), 2) self.assertEqual(len(resp), 2)
self.assertIn('status', resp) self.assertIn("status", resp)
self.assertIn('version', resp) self.assertIn("version", resp)
self.assertEqual(resp['status'], 'ok') self.assertEqual(resp["status"], "ok")
resp = self.error(0, 'message')['subsonic-response'] resp = self.error(0, "message")["subsonic-response"]
self.assertEqual(resp['status'], 'failed') self.assertEqual(resp["status"], "failed")
some_dict = { some_dict = {"intValue": 2, "someString": "Hello world!"}
'intValue': 2,
'someString': 'Hello world!'
}
resp = self.process_and_extract(some_dict) resp = self.process_and_extract(some_dict)
self.assertIn('intValue', resp) self.assertIn("intValue", resp)
self.assertIn('someString', resp) self.assertIn("someString", resp)
def test_lists(self): def test_lists(self):
resp = self.process_and_extract({ resp = self.process_and_extract({"someList": [2, 4, 8, 16], "emptyList": []})
'someList': [ 2, 4, 8, 16 ], self.assertIn("someList", resp)
'emptyList': [] self.assertNotIn("emptyList", resp)
}) self.assertListEqual(resp["someList"], [2, 4, 8, 16])
self.assertIn('someList', resp)
self.assertNotIn('emptyList', resp)
self.assertListEqual(resp['someList'], [ 2, 4, 8, 16 ])
def test_dicts(self): def test_dicts(self):
resp = self.process_and_extract({ resp = self.process_and_extract({"dict": {"s": "Blah", "i": 20}, "empty": {}})
'dict': { 's': 'Blah', 'i': 20 }, self.assertIn("dict", resp)
'empty': {} self.assertIn("empty", resp)
}) self.assertDictEqual(resp["dict"], {"s": "Blah", "i": 20})
self.assertIn('dict', resp) self.assertDictEqual(resp["empty"], {})
self.assertIn('empty', resp)
self.assertDictEqual(resp['dict'], { 's': 'Blah', 'i': 20 })
self.assertDictEqual(resp['empty'], {})
def test_nesting(self): def test_nesting(self):
resp = self.process_and_extract({ resp = self.process_and_extract(
'dict': { {
'value': 'hey look! a string', "dict": {
'list': [ 1, 2, 3 ], "value": "hey look! a string",
'emptyList': [], "list": [1, 2, 3],
'subdict': { 'a': 'A' } "emptyList": [],
}, "subdict": {"a": "A"},
'list': [ },
{ 'b': 'B' }, "list": [{"b": "B"}, {"c": "C"}, [4, 5, 6], "final string"],
{ 'c': 'C' }, }
[ 4, 5, 6 ], )
'final string'
]
})
self.assertEqual(len(resp), 2) self.assertEqual(len(resp), 2)
self.assertIn('dict', resp) self.assertIn("dict", resp)
self.assertIn('list', resp) self.assertIn("list", resp)
d = resp['dict'] d = resp["dict"]
l = resp['list'] l = resp["list"]
self.assertIn('value', d) self.assertIn("value", d)
self.assertIn('list', d) self.assertIn("list", d)
self.assertNotIn('emptyList', d) self.assertNotIn("emptyList", d)
self.assertIn('subdict', d) self.assertIn("subdict", d)
self.assertIsInstance(d['value'], strtype) self.assertIsInstance(d["value"], strtype)
self.assertIsInstance(d['list'], list) self.assertIsInstance(d["list"], list)
self.assertIsInstance(d['subdict'], dict) 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)): class ResponseHelperJsonpTestCase(TestBase, UnwrapperMixin.create_from(JSONPFormatter)):
def test_basic(self): def test_basic(self):
self._JSONPFormatter__callback = 'callback' # hacky self._JSONPFormatter__callback = "callback" # hacky
result = self.empty result = self.empty
self.assertTrue(result.startswith('callback({')) self.assertTrue(result.startswith("callback({"))
self.assertTrue(result.endswith('})')) self.assertTrue(result.endswith("})"))
json = flask.json.loads(result[9:-1]) json = flask.json.loads(result[9:-1])
self.assertIn('subsonic-response', json) self.assertIn("subsonic-response", json)
class ResponseHelperXMLTestCase(TestBase, UnwrapperMixin.create_from(XMLFormatter)): class ResponseHelperXMLTestCase(TestBase, UnwrapperMixin.create_from(XMLFormatter)):
def make_response(self, elem, data): def make_response(self, elem, data):
xml = super(ResponseHelperXMLTestCase, self).make_response(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) root = ElementTree.fromstring(xml)
return root return root
def process_and_extract(self, d): def process_and_extract(self, d):
rv = self.make_response('tag', d) rv = self.make_response("tag", d)
return rv.find('tag') return rv.find("tag")
def assertAttributesMatchDict(self, elem, d): 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) self.assertDictEqual(elem.attrib, d)
def test_root(self): def test_root(self):
xml = super(ResponseHelperXMLTestCase, self).make_response('tag', {}) xml = super(ResponseHelperXMLTestCase, self).make_response("tag", {})
self.assertIn('<subsonic-response ', xml) self.assertIn("<subsonic-response ", xml)
self.assertIn('xmlns="http://subsonic.org/restapi"', xml) self.assertIn('xmlns="http://subsonic.org/restapi"', xml)
self.assertTrue(xml.strip().endswith('</subsonic-response>')) self.assertTrue(xml.strip().endswith("</subsonic-response>"))
def test_basic(self): def test_basic(self):
empty = self.empty empty = self.empty
self.assertIsNotNone(empty.find('.[@version]')) self.assertIsNotNone(empty.find(".[@version]"))
self.assertIsNotNone(empty.find(".[@status='ok']")) self.assertIsNotNone(empty.find(".[@status='ok']"))
resp = self.error(0, 'message') resp = self.error(0, "message")
self.assertIsNotNone(resp.find(".[@status='failed']")) self.assertIsNotNone(resp.find(".[@status='failed']"))
some_dict = { some_dict = {"intValue": 2, "someString": "Hello world!"}
'intValue': 2,
'someString': 'Hello world!'
}
resp = self.process_and_extract(some_dict) resp = self.process_and_extract(some_dict)
self.assertIsNotNone(resp.find('.[@intValue]')) self.assertIsNotNone(resp.find(".[@intValue]"))
self.assertIsNotNone(resp.find('.[@someString]')) self.assertIsNotNone(resp.find(".[@someString]"))
def test_lists(self): def test_lists(self):
resp = self.process_and_extract({ resp = self.process_and_extract({"someList": [2, 4, 8, 16], "emptyList": []})
'someList': [ 2, 4, 8, 16 ],
'emptyList': []
})
elems = resp.findall('./someList') elems = resp.findall("./someList")
self.assertEqual(len(elems), 4) 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) self.assertEqual(int(e.text), i)
def test_dicts(self): def test_dicts(self):
resp = self.process_and_extract({ resp = self.process_and_extract({"dict": {"s": "Blah", "i": 20}, "empty": {}})
'dict': { 's': 'Blah', 'i': 20 },
'empty': {}
})
d = resp.find('./dict') d = resp.find("./dict")
self.assertIsNotNone(d) self.assertIsNotNone(d)
self.assertIsNotNone(resp.find('./empty')) self.assertIsNotNone(resp.find("./empty"))
self.assertAttributesMatchDict(d, { 's': 'Blah', 'i': 20 }) self.assertAttributesMatchDict(d, {"s": "Blah", "i": 20})
def test_nesting(self): def test_nesting(self):
resp = self.process_and_extract({ resp = self.process_and_extract(
'dict': { {
'somevalue': 'hey look! a string', "dict": {
'list': [ 1, 2, 3 ], "somevalue": "hey look! a string",
'emptyList': [], "list": [1, 2, 3],
'subdict': { 'a': 'A' } "emptyList": [],
}, "subdict": {"a": "A"},
'list': [ },
{ 'b': 'B' }, "list": [{"b": "B"}, {"c": "C"}, "final string"],
{ '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') d = resp.find("./dict")
lists = resp.findall('./list') lists = resp.findall("./list")
self.assertIsNotNone(d) self.assertIsNotNone(d)
self.assertAttributesMatchDict(d, { 'somevalue': 'hey look! a string' }) self.assertAttributesMatchDict(d, {"somevalue": "hey look! a string"})
self.assertEqual(len(d.findall('./list')), 3) self.assertEqual(len(d.findall("./list")), 3)
self.assertEqual(len(d.findall('./emptyList')), 0) self.assertEqual(len(d.findall("./emptyList")), 0)
self.assertIsNotNone(d.find('./subdict')) self.assertIsNotNone(d.find("./subdict"))
self.assertEqual(len(lists), 3) self.assertEqual(len(lists), 3)
self.assertAttributesMatchDict(lists[0], { 'b': 'B' }) self.assertAttributesMatchDict(lists[0], {"b": "B"})
self.assertAttributesMatchDict(lists[1], { 'c': 'C' }) self.assertAttributesMatchDict(lists[1], {"c": "C"})
self.assertEqual(lists[2].text, 'final string') self.assertEqual(lists[2].text, "final string")
def suite(): def suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
@ -229,6 +207,6 @@ def suite():
return suite return suite
if __name__ == '__main__':
unittest.main()
if __name__ == "__main__":
unittest.main()

View File

@ -17,39 +17,46 @@ from supysonic.db import Folder, Artist, Album, Track
from .apitestbase import ApiTestBase from .apitestbase import ApiTestBase
class SearchTestCase(ApiTestBase): class SearchTestCase(ApiTestBase):
def setUp(self): def setUp(self):
super(SearchTestCase, self).setUp() super(SearchTestCase, self).setUp()
with db_session: 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': for letter in "ABC":
folder = Folder(name = letter + 'rtist', path = 'tests/assets/{}rtist'.format(letter), parent = root) folder = Folder(
artist = Artist(name = letter + 'rtist') 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( afolder = Folder(
name = letter + lether + 'lbum', name=letter + lether + "lbum",
path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether), path="tests/assets/{0}rtist/{0}{1}lbum".format(letter, lether),
parent = folder 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( track = Track(
disc = 1, disc=1,
number = num, number=num,
title = song, title=song,
duration = 2, duration=2,
album = album, album=album,
artist = artist, artist=artist,
bitrate = 320, bitrate=320,
path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song), path="tests/assets/{0}rtist/{0}{1}lbum/{2}".format(
last_modification = 0, letter, lether, song
root_folder = root, ),
folder = afolder last_modification=0,
root_folder=root,
folder=afolder,
) )
commit() commit()
@ -60,176 +67,212 @@ class SearchTestCase(ApiTestBase):
self.assertEqual(Track.select().count(), 18) self.assertEqual(Track.select().count(), 18)
def __track_as_pseudo_unique_str(self, elem): 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): def test_search(self):
# invalid parameters # invalid parameters
self._make_request('search', { 'count': 'string' }, error = 0) self._make_request("search", {"count": "string"}, error=0)
self._make_request('search', { 'offset': 'sstring' }, error = 0) self._make_request("search", {"offset": "sstring"}, error=0)
self._make_request('search', { 'newerThan': 'ssstring' }, error = 0) self._make_request("search", {"newerThan": "ssstring"}, error=0)
# no search # no search
self._make_request('search', error = 10) self._make_request("search", error=10)
# non existent artist (but searched string present in other fields) # 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(len(child), 0)
self.assertEqual(child.get('totalHits'), '0') self.assertEqual(child.get("totalHits"), "0")
self.assertEqual(child.get('offset'), '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) self.assertEqual(len(child), 0)
# non existent album (but search present in other fields) # 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) 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) self.assertEqual(len(child), 0)
# non existent title (but search present in other fields) # 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) 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) self.assertEqual(len(child), 0)
# non existent anything # 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) self.assertEqual(len(child), 0)
# artist search # 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(len(child), 1)
self.assertEqual(child.get('totalHits'), '1') self.assertEqual(child.get("totalHits"), "1")
self.assertEqual(child[0].get('title'), 'Artist') 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(len(child), 3)
self.assertEqual(child.get('totalHits'), '3') self.assertEqual(child.get("totalHits"), "3")
# same as above, but created in the future # same as above, but created in the future
future = int(time.time() * 1000 + 1000) 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) self.assertEqual(len(child), 0)
# album search # 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(len(child), 1)
self.assertEqual(child[0].get('title'), 'AAlbum') self.assertEqual(child[0].get("title"), "AAlbum")
self.assertEqual(child[0].get('artist'), 'Artist') 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) self.assertEqual(len(child), 6)
# same as above, but created in the future # 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) self.assertEqual(len(child), 0)
# song search # 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) self.assertEqual(len(child), 6)
for i in range(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) self.assertEqual(len(child), 12)
# same as above, but created in the future # 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) self.assertEqual(len(child), 0)
# any field search # any field search
rv, child = self._make_request('search', { 'any': 'r' }, tag = 'searchResult') rv, child = self._make_request("search", {"any": "r"}, tag="searchResult")
self.assertEqual(len(child), 10) # root + 3 artists (*rtist) + 6 songs (Three) self.assertEqual(len(child), 10) # root + 3 artists (*rtist) + 6 songs (Three)
# same as above, but created in the future # 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) self.assertEqual(len(child), 0)
# paging # paging
songs = [] songs = []
for offset in range(0, 12, 2): 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(len(child), 2)
self.assertEqual(child.get('totalHits'), '12') self.assertEqual(child.get("totalHits"), "12")
self.assertEqual(child.get('offset'), str(offset)) self.assertEqual(child.get("offset"), str(offset))
for song in map(self.__track_as_pseudo_unique_str, child): for song in map(self.__track_as_pseudo_unique_str, child):
self.assertNotIn(song, songs) self.assertNotIn(song, songs)
songs.append(song) songs.append(song)
def test_search2(self): def test_search2(self):
# invalid parameters # invalid parameters
self._make_request('search2', { 'query': 'a', 'artistCount': 'string' }, 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(
self._make_request('search2', { 'query': 'a', 'albumCount': 'string' }, error = 0) "search2", {"query": "a", "artistOffset": "sstring"}, 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", "albumCount": "string"}, error=0)
self._make_request('search2', { 'query': 'a', 'songOffset': 'sstring' }, 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 # no search
self._make_request('search2', error = 10) self._make_request("search2", error=10)
# non existent anything # 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) self.assertEqual(len(child), 0)
# artist search # 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(child), 1)
self.assertEqual(len(self._xpath(child, './artist')), 1) self.assertEqual(len(self._xpath(child, "./artist")), 1)
self.assertEqual(len(self._xpath(child, './album')), 0) self.assertEqual(len(self._xpath(child, "./album")), 0)
self.assertEqual(len(self._xpath(child, './song')), 0) self.assertEqual(len(self._xpath(child, "./song")), 0)
self.assertEqual(child[0].get('name'), 'Artist') 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(child), 3)
self.assertEqual(len(self._xpath(child, './artist')), 3) self.assertEqual(len(self._xpath(child, "./artist")), 3)
self.assertEqual(len(self._xpath(child, './album')), 0) self.assertEqual(len(self._xpath(child, "./album")), 0)
self.assertEqual(len(self._xpath(child, './song')), 0) self.assertEqual(len(self._xpath(child, "./song")), 0)
# album search # 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(child), 1)
self.assertEqual(len(self._xpath(child, './artist')), 0) self.assertEqual(len(self._xpath(child, "./artist")), 0)
self.assertEqual(len(self._xpath(child, './album')), 1) self.assertEqual(len(self._xpath(child, "./album")), 1)
self.assertEqual(len(self._xpath(child, './song')), 0) self.assertEqual(len(self._xpath(child, "./song")), 0)
self.assertEqual(child[0].get('title'), 'AAlbum') self.assertEqual(child[0].get("title"), "AAlbum")
self.assertEqual(child[0].get('artist'), 'Artist') 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(child), 6)
self.assertEqual(len(self._xpath(child, './artist')), 0) self.assertEqual(len(self._xpath(child, "./artist")), 0)
self.assertEqual(len(self._xpath(child, './album')), 6) self.assertEqual(len(self._xpath(child, "./album")), 6)
self.assertEqual(len(self._xpath(child, './song')), 0) self.assertEqual(len(self._xpath(child, "./song")), 0)
# song search # 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(child), 6)
self.assertEqual(len(self._xpath(child, './artist')), 0) self.assertEqual(len(self._xpath(child, "./artist")), 0)
self.assertEqual(len(self._xpath(child, './album')), 0) self.assertEqual(len(self._xpath(child, "./album")), 0)
self.assertEqual(len(self._xpath(child, './song')), 6) self.assertEqual(len(self._xpath(child, "./song")), 6)
for i in range(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(child), 12)
self.assertEqual(len(self._xpath(child, './artist')), 0) self.assertEqual(len(self._xpath(child, "./artist")), 0)
self.assertEqual(len(self._xpath(child, './album')), 0) self.assertEqual(len(self._xpath(child, "./album")), 0)
self.assertEqual(len(self._xpath(child, './song')), 12) self.assertEqual(len(self._xpath(child, "./song")), 12)
# any field search # 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(child), 9)
self.assertEqual(len(self._xpath(child, './artist')), 3) self.assertEqual(len(self._xpath(child, "./artist")), 3)
self.assertEqual(len(self._xpath(child, './album')), 0) self.assertEqual(len(self._xpath(child, "./album")), 0)
self.assertEqual(len(self._xpath(child, './song')), 6) self.assertEqual(len(self._xpath(child, "./song")), 6)
# paging # paging
artists = [] artists = []
for offset in range(0, 4, 2): for offset in range(0, 4, 2):
rv, child = self._make_request('search2', { 'query': 'r', 'artistCount': 2, 'artistOffset': offset }, tag = 'searchResult2') rv, child = self._make_request(
names = self._xpath(child, './artist/@name') "search2",
{"query": "r", "artistCount": 2, "artistOffset": offset},
tag="searchResult2",
)
names = self._xpath(child, "./artist/@name")
self.assertLessEqual(len(names), 2) self.assertLessEqual(len(names), 2)
for name in names: for name in names:
self.assertNotIn(name, artists) self.assertNotIn(name, artists)
@ -237,8 +280,12 @@ class SearchTestCase(ApiTestBase):
songs = [] songs = []
for offset in range(0, 6, 2): for offset in range(0, 6, 2):
rv, child = self._make_request('search2', { 'query': 'r', 'songCount': 2, 'songOffset': offset }, tag = 'searchResult2') rv, child = self._make_request(
elems = self._xpath(child, './song') "search2",
{"query": "r", "songCount": 2, "songOffset": offset},
tag="searchResult2",
)
elems = self._xpath(child, "./song")
self.assertEqual(len(elems), 2) self.assertEqual(len(elems), 2)
for song in map(self.__track_as_pseudo_unique_str, elems): for song in map(self.__track_as_pseudo_unique_str, elems):
self.assertNotIn(song, songs) self.assertNotIn(song, songs)
@ -248,76 +295,88 @@ class SearchTestCase(ApiTestBase):
# to have folders that don't share names with artists or albums # to have folders that don't share names with artists or albums
def test_search3(self): def test_search3(self):
# invalid parameters # invalid parameters
self._make_request('search3', { 'query': 'a', 'artistCount': 'string' }, 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(
self._make_request('search3', { 'query': 'a', 'albumCount': 'string' }, error = 0) "search3", {"query": "a", "artistOffset": "sstring"}, 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", "albumCount": "string"}, error=0)
self._make_request('search3', { 'query': 'a', 'songOffset': 'sstring' }, 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 # no search
self._make_request('search3', error = 10) self._make_request("search3", error=10)
# non existent anything # 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) self.assertEqual(len(child), 0)
# artist search # 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(child), 1)
self.assertEqual(len(self._xpath(child, './artist')), 1) self.assertEqual(len(self._xpath(child, "./artist")), 1)
self.assertEqual(len(self._xpath(child, './album')), 0) self.assertEqual(len(self._xpath(child, "./album")), 0)
self.assertEqual(len(self._xpath(child, './song')), 0) self.assertEqual(len(self._xpath(child, "./song")), 0)
self.assertEqual(child[0].get('name'), 'Artist') 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(child), 3)
self.assertEqual(len(self._xpath(child, './artist')), 3) self.assertEqual(len(self._xpath(child, "./artist")), 3)
self.assertEqual(len(self._xpath(child, './album')), 0) self.assertEqual(len(self._xpath(child, "./album")), 0)
self.assertEqual(len(self._xpath(child, './song')), 0) self.assertEqual(len(self._xpath(child, "./song")), 0)
# album search # 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(child), 1)
self.assertEqual(len(self._xpath(child, './artist')), 0) self.assertEqual(len(self._xpath(child, "./artist")), 0)
self.assertEqual(len(self._xpath(child, './album')), 1) self.assertEqual(len(self._xpath(child, "./album")), 1)
self.assertEqual(len(self._xpath(child, './song')), 0) self.assertEqual(len(self._xpath(child, "./song")), 0)
self.assertEqual(child[0].get('name'), 'AAlbum') self.assertEqual(child[0].get("name"), "AAlbum")
self.assertEqual(child[0].get('artist'), 'Artist') 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(child), 6)
self.assertEqual(len(self._xpath(child, './artist')), 0) self.assertEqual(len(self._xpath(child, "./artist")), 0)
self.assertEqual(len(self._xpath(child, './album')), 6) self.assertEqual(len(self._xpath(child, "./album")), 6)
self.assertEqual(len(self._xpath(child, './song')), 0) self.assertEqual(len(self._xpath(child, "./song")), 0)
# song search # 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(child), 6)
self.assertEqual(len(self._xpath(child, './artist')), 0) self.assertEqual(len(self._xpath(child, "./artist")), 0)
self.assertEqual(len(self._xpath(child, './album')), 0) self.assertEqual(len(self._xpath(child, "./album")), 0)
self.assertEqual(len(self._xpath(child, './song')), 6) self.assertEqual(len(self._xpath(child, "./song")), 6)
for i in range(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(child), 12)
self.assertEqual(len(self._xpath(child, './artist')), 0) self.assertEqual(len(self._xpath(child, "./artist")), 0)
self.assertEqual(len(self._xpath(child, './album')), 0) self.assertEqual(len(self._xpath(child, "./album")), 0)
self.assertEqual(len(self._xpath(child, './song')), 12) self.assertEqual(len(self._xpath(child, "./song")), 12)
# any field search # 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(child), 9)
self.assertEqual(len(self._xpath(child, './artist')), 3) self.assertEqual(len(self._xpath(child, "./artist")), 3)
self.assertEqual(len(self._xpath(child, './album')), 0) self.assertEqual(len(self._xpath(child, "./album")), 0)
self.assertEqual(len(self._xpath(child, './song')), 6) self.assertEqual(len(self._xpath(child, "./song")), 6)
# paging # paging
artists = [] artists = []
for offset in range(0, 4, 2): for offset in range(0, 4, 2):
rv, child = self._make_request('search3', { 'query': 'r', 'artistCount': 2, 'artistOffset': offset }, tag = 'searchResult3') rv, child = self._make_request(
names = self._xpath(child, './artist/@name') "search3",
{"query": "r", "artistCount": 2, "artistOffset": offset},
tag="searchResult3",
)
names = self._xpath(child, "./artist/@name")
self.assertLessEqual(len(names), 2) self.assertLessEqual(len(names), 2)
for name in names: for name in names:
self.assertNotIn(name, artists) self.assertNotIn(name, artists)
@ -325,13 +384,17 @@ class SearchTestCase(ApiTestBase):
songs = [] songs = []
for offset in range(0, 6, 2): for offset in range(0, 6, 2):
rv, child = self._make_request('search3', { 'query': 'r', 'songCount': 2, 'songOffset': offset }, tag = 'searchResult3') rv, child = self._make_request(
elems = self._xpath(child, './song') "search3",
{"query": "r", "songCount": 2, "songOffset": offset},
tag="searchResult3",
)
elems = self._xpath(child, "./song")
self.assertEqual(len(elems), 2) self.assertEqual(len(elems), 2)
for song in map(self.__track_as_pseudo_unique_str, elems): for song in map(self.__track_as_pseudo_unique_str, elems):
self.assertNotIn(song, songs) self.assertNotIn(song, songs)
songs.append(song) songs.append(song)
if __name__ == '__main__':
unittest.main()
if __name__ == "__main__":
unittest.main()

View File

@ -11,14 +11,15 @@
from .apitestbase import ApiTestBase from .apitestbase import ApiTestBase
class SystemTestCase(ApiTestBase): class SystemTestCase(ApiTestBase):
def test_ping(self): def test_ping(self):
self._make_request('ping') self._make_request("ping")
def test_get_license(self): def test_get_license(self):
rv, child = self._make_request('getLicense', tag = 'license') rv, child = self._make_request("getLicense", tag="license")
self.assertEqual(child.get('valid'), 'true') self.assertEqual(child.get("valid"), "true")
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -18,43 +18,46 @@ from supysonic.scanner import Scanner
from .apitestbase import ApiTestBase from .apitestbase import ApiTestBase
class TranscodingTestCase(ApiTestBase): class TranscodingTestCase(ApiTestBase):
def setUp(self): def setUp(self):
super(TranscodingTestCase, self).setUp() super(TranscodingTestCase, self).setUp()
self._patch_client() self._patch_client()
with db_session: with db_session:
folder = FolderManager.add('Folder', 'tests/assets/folder') folder = FolderManager.add("Folder", "tests/assets/folder")
scanner = Scanner() scanner = Scanner()
scanner.queue_folder('Folder') scanner.queue_folder("Folder")
scanner.run() scanner.run()
self.trackid = Track.get().id self.trackid = Track.get().id
def _stream(self, **kwargs): 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.assertEqual(rv.status_code, 200)
self.assertFalse(rv.mimetype.startswith('text/')) self.assertFalse(rv.mimetype.startswith("text/"))
return rv return rv
def test_no_transcoding_available(self): 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): def test_direct_transcode(self):
rv = self._stream(maxBitRate = 96, estimateContentLength = 'true') rv = self._stream(maxBitRate=96, estimateContentLength="true")
self.assertIn('tests/assets/folder/silence.mp3', rv.data) self.assertIn("tests/assets/folder/silence.mp3", rv.data)
self.assertTrue(rv.data.endswith('96')) self.assertTrue(rv.data.endswith("96"))
def test_decode_encode(self): def test_decode_encode(self):
rv = self._stream(format = 'cat') rv = self._stream(format="cat")
self.assertEqual(rv.data, 'Pushing out some mp3 data...') self.assertEqual(rv.data, "Pushing out some mp3 data...")
rv = self._stream(format = 'md5') rv = self._stream(format="md5")
self.assertTrue(rv.data.startswith('dbb16c0847e5d8c3b1867604828cb50b')) self.assertTrue(rv.data.startswith("dbb16c0847e5d8c3b1867604828cb50b"))
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -12,140 +12,217 @@
from ..utils import hexlify from ..utils import hexlify
from .apitestbase import ApiTestBase from .apitestbase import ApiTestBase
class UserTestCase(ApiTestBase): class UserTestCase(ApiTestBase):
def test_get_user(self): def test_get_user(self):
# missing username # missing username
self._make_request('getUser', error = 10) self._make_request("getUser", error=10)
# non-existent user # non-existent user
self._make_request('getUser', { 'username': 'non existent' }, error = 70) self._make_request("getUser", {"username": "non existent"}, error=70)
# self # self
rv, child = self._make_request('getUser', { 'username': 'alice' }, tag = 'user') rv, child = self._make_request("getUser", {"username": "alice"}, tag="user")
self.assertEqual(child.get('username'), 'alice') self.assertEqual(child.get("username"), "alice")
self.assertEqual(child.get('adminRole'), 'true') self.assertEqual(child.get("adminRole"), "true")
# other # other
rv, child = self._make_request('getUser', { 'username': 'bob' }, tag = 'user') rv, child = self._make_request("getUser", {"username": "bob"}, tag="user")
self.assertEqual(child.get('username'), 'bob') self.assertEqual(child.get("username"), "bob")
self.assertEqual(child.get('adminRole'), 'false') self.assertEqual(child.get("adminRole"), "false")
# self from non-admin # self from non-admin
rv, child = self._make_request('getUser', { 'u': 'bob', 'p': 'B0b', 'username': 'bob' }, tag = 'user') rv, child = self._make_request(
self.assertEqual(child.get('username'), 'bob') "getUser", {"u": "bob", "p": "B0b", "username": "bob"}, tag="user"
self.assertEqual(child.get('adminRole'), 'false') )
self.assertEqual(child.get("username"), "bob")
self.assertEqual(child.get("adminRole"), "false")
# other from non-admin # 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): def test_get_users(self):
# non-admin # non-admin
self._make_request('getUsers', { 'u': 'bob', 'p': 'B0b' }, error = 50) self._make_request("getUsers", {"u": "bob", "p": "B0b"}, error=50)
# admin # admin
rv, child = self._make_request('getUsers', tag = 'users') rv, child = self._make_request("getUsers", tag="users")
self.assertEqual(len(child), 2) self.assertEqual(len(child), 2)
self.assertIsNotNone(self._find(child, "./user[@username='alice']")) self.assertIsNotNone(self._find(child, "./user[@username='alice']"))
self.assertIsNotNone(self._find(child, "./user[@username='bob']")) self.assertIsNotNone(self._find(child, "./user[@username='bob']"))
def test_create_user(self): def test_create_user(self):
# non admin # 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 # missing params, testing every combination, maybe overkill
self._make_request('createUser', error = 10) self._make_request("createUser", error=10)
self._make_request('createUser', { 'username': 'user' }, error = 10) self._make_request("createUser", {"username": "user"}, error=10)
self._make_request('createUser', { 'password': 'pass' }, error = 10) self._make_request("createUser", {"password": "pass"}, error=10)
self._make_request('createUser', { 'email': 'email@example.com' }, 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(
self._make_request('createUser', { 'username': 'user', 'email': 'email@example.com' }, error = 10) "createUser", {"username": "user", "password": "pass"}, error=10
self._make_request('createUser', { 'password': 'pass', 'email': 'email@example.com' }, 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 # 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 # 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) self.assertEqual(len(child), 2)
# create users # create users
self._make_request('createUser', { 'username': 'charlie', 'password': 'Ch4rl1e', 'email': 'unicorn@example.com', 'adminRole': True }, skip_post = True) self._make_request(
rv, child = self._make_request('getUser', { 'username': 'charlie' }, tag = 'user') "createUser",
self.assertEqual(child.get('username'), 'charlie') {
self.assertEqual(child.get('email'), 'unicorn@example.com') "username": "charlie",
self.assertEqual(child.get('adminRole'), 'true') "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) self._make_request(
rv, child = self._make_request('getUser', { 'username': 'dave' }, tag = 'user') "createUser",
self.assertEqual(child.get('username'), 'dave') {"username": "dave", "password": "Dav3", "email": "dave@example.com"},
self.assertEqual(child.get('email'), 'dave@example.com') skip_post=True,
self.assertEqual(child.get('adminRole'), 'false') )
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) self.assertEqual(len(child), 4)
def test_delete_user(self): def test_delete_user(self):
# non admin # 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 # missing param
self._make_request('deleteUser', error = 10) self._make_request("deleteUser", error=10)
# non existing # 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 # 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) self.assertEqual(len(child), 2)
# delete user # delete user
self._make_request('deleteUser', { 'username': 'bob' }, skip_post = True) self._make_request("deleteUser", {"username": "bob"}, skip_post=True)
rv, child = self._make_request('getUsers', tag = 'users') rv, child = self._make_request("getUsers", tag="users")
self.assertEqual(len(child), 1) self.assertEqual(len(child), 1)
def test_change_password(self): def test_change_password(self):
# missing parameter # missing parameter
self._make_request('changePassword', error = 10) self._make_request("changePassword", error=10)
self._make_request('changePassword', { 'username': 'alice' }, error = 10) self._make_request("changePassword", {"username": "alice"}, error=10)
self._make_request('changePassword', { 'password': 'newpass' }, error = 10) self._make_request("changePassword", {"password": "newpass"}, error=10)
# admin change self # admin change self
self._make_request('changePassword', { 'username': 'alice', 'password': 'newpass' }, skip_post = True) self._make_request(
self._make_request('ping', error = 40) "changePassword",
self._make_request('ping', { 'u': 'alice', 'p': 'newpass' }) {"username": "alice", "password": "newpass"},
self._make_request('changePassword', { 'u': 'alice', 'p': 'newpass', 'username': 'alice', 'password': 'Alic3' }, skip_post = True) 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 # admin change other
self._make_request('changePassword', { 'username': 'bob', 'password': 'newbob' }, skip_post = True) self._make_request(
self._make_request('ping', { 'u': 'bob', 'p': 'B0b' }, error = 40) "changePassword", {"username": "bob", "password": "newbob"}, skip_post=True
self._make_request('ping', { 'u': 'bob', 'p': 'newbob' }) )
self._make_request("ping", {"u": "bob", "p": "B0b"}, error=40)
self._make_request("ping", {"u": "bob", "p": "newbob"})
# non-admin change self # non-admin change self
self._make_request('changePassword', { 'u': 'bob', 'p': 'newbob', 'username': 'bob', 'password': 'B0b' }, skip_post = True) self._make_request(
self._make_request('ping', { 'u': 'bob', 'p': 'newbob' }, error = 40) "changePassword",
self._make_request('ping', { 'u': 'bob', 'p': 'B0b' }) {"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 # non-admin change other
self._make_request('changePassword', { 'u': 'bob', 'p': 'B0b', 'username': 'alice', 'password': 'newpass' }, skip_post = True, error = 50) self._make_request(
self._make_request('ping', { 'u': 'alice', 'p': 'newpass' }, error = 40) "changePassword",
self._make_request('ping') {"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 # 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 # non ASCII chars
self._make_request('changePassword', { 'username': 'alice', 'password': 'новыйпароль' }, skip_post = True) self._make_request(
self._make_request('ping', { 'u': 'alice', 'p': 'новыйпароль' }) "changePassword",
self._make_request('changePassword', { 'username': 'alice', 'password': 'Alic3', 'u': 'alice', 'p': 'новыйпароль' }, skip_post = True) {"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 # non ASCII in hex encoded password
self._make_request('changePassword', { 'username': 'alice', 'password': 'enc:' + hexlify(u'новыйпароль') }, skip_post = True) self._make_request(
self._make_request('ping', { 'u': 'alice', 'p': 'новыйпароль' }) "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 # 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(
self._make_request('ping', { 'u': 'alice', 'p': 'enc:randomstring' }) "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() unittest.main()

View File

@ -18,6 +18,7 @@ from .test_scanner import ScannerTestCase
from .test_secret import SecretTestCase from .test_secret import SecretTestCase
from .test_watcher import suite as watcher_suite from .test_watcher import suite as watcher_suite
def suite(): def suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
@ -31,4 +32,3 @@ def suite():
suite.addTest(unittest.makeSuite(SecretTestCase)) suite.addTest(unittest.makeSuite(SecretTestCase))
return suite return suite

View File

@ -27,7 +27,7 @@ class CacheTestCase(unittest.TestCase):
def test_existing_files_order(self): def test_existing_files_order(self):
cache = Cache(self.__dir, 30) cache = Cache(self.__dir, 30)
val = b'0123456789' val = b"0123456789"
cache.set("key1", val) cache.set("key1", val)
cache.set("key2", val) cache.set("key2", val)
cache.set("key3", val) cache.set("key3", val)
@ -63,7 +63,7 @@ class CacheTestCase(unittest.TestCase):
def test_store_literal(self): def test_store_literal(self):
cache = Cache(self.__dir, 10) cache = Cache(self.__dir, 10)
val = b'0123456789' val = b"0123456789"
cache.set("key", val) cache.set("key", val)
self.assertEqual(cache.size, 10) self.assertEqual(cache.size, 10)
self.assertTrue(cache.has("key")) self.assertTrue(cache.has("key"))
@ -71,7 +71,8 @@ class CacheTestCase(unittest.TestCase):
def test_store_generated(self): def test_store_generated(self):
cache = Cache(self.__dir, 10) 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(): def gen():
for b in val: for b in val:
yield b yield b
@ -84,11 +85,11 @@ class CacheTestCase(unittest.TestCase):
self.assertEqual(t, val) self.assertEqual(t, val)
self.assertEqual(cache.size, 10) 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): def test_store_to_fp(self):
cache = Cache(self.__dir, 10) cache = Cache(self.__dir, 10)
val = b'0123456789' val = b"0123456789"
with cache.set_fileobj("key") as fp: with cache.set_fileobj("key") as fp:
fp.write(val) fp.write(val)
self.assertEqual(cache.size, 0) self.assertEqual(cache.size, 0)
@ -98,7 +99,7 @@ class CacheTestCase(unittest.TestCase):
def test_access_data(self): def test_access_data(self):
cache = Cache(self.__dir, 25, min_time=0) cache = Cache(self.__dir, 25, min_time=0)
val = b'0123456789' val = b"0123456789"
cache.set("key", val) cache.set("key", val)
self.assertEqual(cache.get_value("key"), val) self.assertEqual(cache.get_value("key"), val)
@ -106,13 +107,12 @@ class CacheTestCase(unittest.TestCase):
with cache.get_fileobj("key") as f: with cache.get_fileobj("key") as f:
self.assertEqual(f.read(), val) 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) self.assertEqual(f.read(), val)
def test_accessing_preserves(self): def test_accessing_preserves(self):
cache = Cache(self.__dir, 25, min_time=0) cache = Cache(self.__dir, 25, min_time=0)
val = b'0123456789' val = b"0123456789"
cache.set("key1", val) cache.set("key1", val)
cache.set("key2", val) cache.set("key2", val)
self.assertEqual(cache.size, 20) self.assertEqual(cache.size, 20)
@ -127,7 +127,7 @@ class CacheTestCase(unittest.TestCase):
def test_automatic_delete_oldest(self): def test_automatic_delete_oldest(self):
cache = Cache(self.__dir, 25, min_time=0) cache = Cache(self.__dir, 25, min_time=0)
val = b'0123456789' val = b"0123456789"
cache.set("key1", val) cache.set("key1", val)
self.assertTrue(cache.has("key1")) self.assertTrue(cache.has("key1"))
self.assertEqual(cache.size, 10) self.assertEqual(cache.size, 10)
@ -145,7 +145,7 @@ class CacheTestCase(unittest.TestCase):
def test_delete(self): def test_delete(self):
cache = Cache(self.__dir, 25, min_time=0) cache = Cache(self.__dir, 25, min_time=0)
val = b'0123456789' val = b"0123456789"
cache.set("key1", val) cache.set("key1", val)
self.assertTrue(cache.has("key1")) self.assertTrue(cache.has("key1"))
self.assertEqual(cache.size, 10) self.assertEqual(cache.size, 10)
@ -157,9 +157,10 @@ class CacheTestCase(unittest.TestCase):
def test_cleanup_on_error(self): def test_cleanup_on_error(self):
cache = Cache(self.__dir, 10) cache = Cache(self.__dir, 10)
def gen(): def gen():
# Cause a TypeError halfway through # 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 yield b
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
@ -171,8 +172,9 @@ class CacheTestCase(unittest.TestCase):
def test_parallel_generation(self): def test_parallel_generation(self):
cache = Cache(self.__dir, 20) cache = Cache(self.__dir, 20)
def gen(): 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 yield b
g1 = cache.set_generated("key", gen) g1 = cache.set_generated("key", gen)
@ -207,8 +209,8 @@ class CacheTestCase(unittest.TestCase):
def test_replace(self): def test_replace(self):
cache = Cache(self.__dir, 20) cache = Cache(self.__dir, 20)
val_small = b'0' val_small = b"0"
val_big = b'0123456789' val_big = b"0123456789"
cache.set("key", val_small) cache.set("key", val_small)
self.assertEqual(cache.size, 1) self.assertEqual(cache.size, 1)
@ -221,7 +223,7 @@ class CacheTestCase(unittest.TestCase):
def test_no_auto_prune(self): def test_no_auto_prune(self):
cache = Cache(self.__dir, 10, min_time=0, auto_prune=False) cache = Cache(self.__dir, 10, min_time=0, auto_prune=False)
val = b'0123456789' val = b"0123456789"
cache.set("key1", val) cache.set("key1", val)
cache.set("key2", val) cache.set("key2", val)
@ -234,7 +236,7 @@ class CacheTestCase(unittest.TestCase):
def test_min_time_clear(self): def test_min_time_clear(self):
cache = Cache(self.__dir, 40, min_time=1) cache = Cache(self.__dir, 40, min_time=1)
val = b'0123456789' val = b"0123456789"
cache.set("key1", val) cache.set("key1", val)
cache.set("key2", val) cache.set("key2", val)
@ -251,7 +253,7 @@ class CacheTestCase(unittest.TestCase):
def test_not_expired(self): def test_not_expired(self):
cache = Cache(self.__dir, 40, min_time=1) cache = Cache(self.__dir, 40, min_time=1)
val = b'0123456789' val = b"0123456789"
cache.set("key1", val) cache.set("key1", val)
with self.assertRaises(ProtectedError): with self.assertRaises(ProtectedError):
cache.delete("key1") cache.delete("key1")
@ -261,7 +263,7 @@ class CacheTestCase(unittest.TestCase):
def test_missing_cache_file(self): def test_missing_cache_file(self):
cache = Cache(self.__dir, 10, min_time=0) cache = Cache(self.__dir, 10, min_time=0)
val = b'0123456789' val = b"0123456789"
os.remove(cache.set("key", val)) os.remove(cache.set("key", val))
self.assertEqual(cache.size, 10) self.assertEqual(cache.size, 10)
@ -275,5 +277,5 @@ class CacheTestCase(unittest.TestCase):
self.assertEqual(cache.size, 0) self.assertEqual(cache.size, 0)
if __name__ == '__main__': if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -17,7 +17,7 @@ import unittest
from contextlib import contextmanager from contextlib import contextmanager
from pony.orm import db_session 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 from StringIO import StringIO
except ImportError: except ImportError:
from io import StringIO from io import StringIO
@ -27,18 +27,19 @@ from supysonic.cli import SupysonicCLI
from ..testbase import TestConfig from ..testbase import TestConfig
class CLITestCase(unittest.TestCase): class CLITestCase(unittest.TestCase):
""" Really basic tests. Some even don't check anything but are just there for coverage """ """ Really basic tests. Some even don't check anything but are just there for coverage """
def setUp(self): def setUp(self):
conf = TestConfig(False, False) conf = TestConfig(False, False)
self.__dbfile = tempfile.mkstemp()[1] self.__dbfile = tempfile.mkstemp()[1]
conf.BASE['database_uri'] = 'sqlite:///' + self.__dbfile conf.BASE["database_uri"] = "sqlite:///" + self.__dbfile
init_database(conf.BASE['database_uri']) init_database(conf.BASE["database_uri"])
self.__stdout = StringIO() self.__stdout = StringIO()
self.__stderr = 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): def tearDown(self):
self.__stdout.close() self.__stdout.close()
@ -56,7 +57,7 @@ class CLITestCase(unittest.TestCase):
def test_folder_add(self): def test_folder_add(self):
with self._tempdir() as d: with self._tempdir() as d:
self.__cli.onecmd('folder add tmpfolder ' + d) self.__cli.onecmd("folder add tmpfolder " + d)
with db_session: with db_session:
f = Folder.select().first() f = Folder.select().first()
@ -65,76 +66,76 @@ class CLITestCase(unittest.TestCase):
def test_folder_add_errors(self): def test_folder_add_errors(self):
with self._tempdir() as d: with self._tempdir() as d:
self.__cli.onecmd('folder add f1 ' + d) self.__cli.onecmd("folder add f1 " + d)
self.__cli.onecmd('folder add f2 ' + d) self.__cli.onecmd("folder add f2 " + d)
with self._tempdir() as d: with self._tempdir() as d:
self.__cli.onecmd('folder add f1 ' + d) self.__cli.onecmd("folder add f1 " + d)
self.__cli.onecmd('folder add f3 /invalid/path') self.__cli.onecmd("folder add f3 /invalid/path")
with db_session: with db_session:
self.assertEqual(Folder.select().count(), 1) self.assertEqual(Folder.select().count(), 1)
def test_folder_delete(self): def test_folder_delete(self):
with self._tempdir() as d: with self._tempdir() as d:
self.__cli.onecmd('folder add tmpfolder ' + d) self.__cli.onecmd("folder add tmpfolder " + d)
self.__cli.onecmd('folder delete randomfolder') self.__cli.onecmd("folder delete randomfolder")
self.__cli.onecmd('folder delete tmpfolder') self.__cli.onecmd("folder delete tmpfolder")
with db_session: with db_session:
self.assertEqual(Folder.select().count(), 0) self.assertEqual(Folder.select().count(), 0)
def test_folder_list(self): def test_folder_list(self):
with self._tempdir() as d: with self._tempdir() as d:
self.__cli.onecmd('folder add tmpfolder ' + d) self.__cli.onecmd("folder add tmpfolder " + d)
self.__cli.onecmd('folder list') self.__cli.onecmd("folder list")
self.assertIn('tmpfolder', self.__stdout.getvalue()) self.assertIn("tmpfolder", self.__stdout.getvalue())
self.assertIn(d, self.__stdout.getvalue()) self.assertIn(d, self.__stdout.getvalue())
def test_folder_scan(self): def test_folder_scan(self):
with self._tempdir() as d: with self._tempdir() as d:
self.__cli.onecmd('folder add tmpfolder ' + d) self.__cli.onecmd("folder add tmpfolder " + d)
with tempfile.NamedTemporaryFile(dir = d): with tempfile.NamedTemporaryFile(dir=d):
self.__cli.onecmd('folder scan') self.__cli.onecmd("folder scan")
self.__cli.onecmd('folder scan tmpfolder nonexistent') self.__cli.onecmd("folder scan tmpfolder nonexistent")
def test_user_add(self): def test_user_add(self):
self.__cli.onecmd('user add -p Alic3 alice') self.__cli.onecmd("user add -p Alic3 alice")
self.__cli.onecmd('user add -p alice alice') self.__cli.onecmd("user add -p alice alice")
with db_session: with db_session:
self.assertEqual(User.select().count(), 1) self.assertEqual(User.select().count(), 1)
def test_user_delete(self): def test_user_delete(self):
self.__cli.onecmd('user add -p Alic3 alice') self.__cli.onecmd("user add -p Alic3 alice")
self.__cli.onecmd('user delete alice') self.__cli.onecmd("user delete alice")
self.__cli.onecmd('user delete bob') self.__cli.onecmd("user delete bob")
with db_session: with db_session:
self.assertEqual(User.select().count(), 0) self.assertEqual(User.select().count(), 0)
def test_user_list(self): def test_user_list(self):
self.__cli.onecmd('user add -p Alic3 alice') self.__cli.onecmd("user add -p Alic3 alice")
self.__cli.onecmd('user list') self.__cli.onecmd("user list")
self.assertIn('alice', self.__stdout.getvalue()) self.assertIn("alice", self.__stdout.getvalue())
def test_user_setadmin(self): def test_user_setadmin(self):
self.__cli.onecmd('user add -p Alic3 alice') self.__cli.onecmd("user add -p Alic3 alice")
self.__cli.onecmd('user setadmin alice') self.__cli.onecmd("user setadmin alice")
self.__cli.onecmd('user setadmin bob') self.__cli.onecmd("user setadmin bob")
with db_session: with db_session:
self.assertTrue(User.get(name = 'alice').admin) self.assertTrue(User.get(name="alice").admin)
def test_user_changepass(self): def test_user_changepass(self):
self.__cli.onecmd('user add -p Alic3 alice') self.__cli.onecmd("user add -p Alic3 alice")
self.__cli.onecmd('user changepass alice newpass') self.__cli.onecmd("user changepass alice newpass")
self.__cli.onecmd('user changepass bob B0b') self.__cli.onecmd("user changepass bob B0b")
def test_other(self): def test_other(self):
self.assertTrue(self.__cli.do_EOF('')) self.assertTrue(self.__cli.do_EOF(""))
self.__cli.onecmd('unknown command') self.__cli.onecmd("unknown command")
self.__cli.postloop() self.__cli.postloop()
self.__cli.completedefault('user', 'user', 4, 4) self.__cli.completedefault("user", "user", 4, 4)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -13,32 +13,33 @@ import unittest
from supysonic.config import IniConfig from supysonic.config import IniConfig
from supysonic.py23 import strtype from supysonic.py23 import strtype
class ConfigTestCase(unittest.TestCase): class ConfigTestCase(unittest.TestCase):
def test_sections(self): def test_sections(self):
conf = IniConfig('tests/assets/sample.ini') conf = IniConfig("tests/assets/sample.ini")
for attr in ('TYPES', 'BOOLEANS'): for attr in ("TYPES", "BOOLEANS"):
self.assertTrue(hasattr(conf, attr)) self.assertTrue(hasattr(conf, attr))
self.assertIsInstance(getattr(conf, attr), dict) self.assertIsInstance(getattr(conf, attr), dict)
def test_types(self): 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["float"], float)
self.assertIsInstance(conf.TYPES['int'], int) self.assertIsInstance(conf.TYPES["int"], int)
self.assertIsInstance(conf.TYPES['string'], strtype) self.assertIsInstance(conf.TYPES["string"], strtype)
for t in ('bool', 'switch', 'yn'): for t in ("bool", "switch", "yn"):
self.assertIsInstance(conf.BOOLEANS[t + '_false'], bool) self.assertIsInstance(conf.BOOLEANS[t + "_false"], bool)
self.assertIsInstance(conf.BOOLEANS[t + '_true'], bool) self.assertIsInstance(conf.BOOLEANS[t + "_true"], bool)
self.assertFalse(conf.BOOLEANS[t + '_false']) self.assertFalse(conf.BOOLEANS[t + "_false"])
self.assertTrue(conf.BOOLEANS[t + '_true']) self.assertTrue(conf.BOOLEANS[t + "_true"])
def test_no_interpolation(self): 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["variable"], "value")
self.assertEqual(conf.ISSUE84['key'], 'some value with a %variable') self.assertEqual(conf.ISSUE84["key"], "some value with a %variable")
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -17,11 +17,12 @@ from pony.orm import db_session
from supysonic import db 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): class DbTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
db.init_database('sqlite:') db.init_database("sqlite:")
try: try:
self.assertRegex self.assertRegex
@ -32,100 +33,89 @@ class DbTestCase(unittest.TestCase):
db.release_database() db.release_database()
def create_some_folders(self): def create_some_folders(self):
root_folder = db.Folder( root_folder = db.Folder(root=True, name="Root folder", path="tests")
root = True,
name = 'Root folder',
path = 'tests'
)
child_folder = db.Folder( child_folder = db.Folder(
root = False, root=False,
name = 'Child folder', name="Child folder",
path = 'tests/assets', path="tests/assets",
cover_art = 'cover.jpg', cover_art="cover.jpg",
parent = root_folder parent=root_folder,
) )
child_2 = db.Folder( child_2 = db.Folder(
root = False, root=False,
name = 'Child folder (No Art)', name="Child folder (No Art)",
path = 'tests/formats', path="tests/formats",
parent = root_folder parent=root_folder,
) )
return root_folder, child_folder, child_2 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() root, child, child_2 = self.create_some_folders()
if not artist: if not artist:
artist = db.Artist(name = 'Test artist') artist = db.Artist(name="Test artist")
if not album: if not album:
album = db.Album(artist = artist, name = 'Test Album') album = db.Album(artist=artist, name="Test Album")
track1 = db.Track( track1 = db.Track(
title = 'Track Title', title="Track Title",
album = album, album=album,
artist = artist, artist=artist,
disc = 1, disc=1,
number = 1, number=1,
duration = 3, duration=3,
has_art = True, has_art=True,
bitrate = 320, bitrate=320,
path = 'tests/assets/formats/silence.ogg', path="tests/assets/formats/silence.ogg",
last_modification = 1234, last_modification=1234,
root_folder = root, root_folder=root,
folder = child folder=child,
) )
track2 = db.Track( track2 = db.Track(
title = 'One Awesome Song', title="One Awesome Song",
album = album, album=album,
artist = artist, artist=artist,
disc = 1, disc=1,
number = 2, number=2,
duration = 5, duration=5,
bitrate = 96, bitrate=96,
path = 'tests/assets/23bytes', path="tests/assets/23bytes",
last_modification = 1234, last_modification=1234,
root_folder = root, root_folder=root,
folder = child folder=child,
) )
return track1, track2 return track1, track2
def create_track_in(self, folder, root, artist = None, album = None, has_art = True): def create_track_in(self, folder, root, artist=None, album=None, has_art=True):
artist = artist or db.Artist(name = 'Snazzy Artist') artist = artist or db.Artist(name="Snazzy Artist")
album = album or db.Album(artist = artist, name = 'Rockin\' Album') album = album or db.Album(artist=artist, name="Rockin' Album")
return db.Track( return db.Track(
title = 'Nifty Number', title="Nifty Number",
album = album, album=album,
artist = artist, artist=artist,
disc = 1, disc=1,
number = 1, number=1,
duration = 5, duration=5,
has_art = has_art, has_art=has_art,
bitrate = 96, bitrate=96,
path = 'tests/assets/formats/silence.flac', path="tests/assets/formats/silence.flac",
last_modification = 1234, last_modification=1234,
root_folder = root, root_folder=root,
folder = folder folder=folder,
) )
def create_user(self, name = 'Test User'): def create_user(self, name="Test User"):
return db.User( return db.User(name=name, password="secret", salt="ABC+")
name = name,
password = 'secret',
salt = 'ABC+',
)
def create_playlist(self): def create_playlist(self):
playlist = db.Playlist( playlist = db.Playlist(user=self.create_user(), name="Playlist!")
user = self.create_user(),
name = 'Playlist!'
)
return playlist return playlist
@ -134,146 +124,134 @@ class DbTestCase(unittest.TestCase):
root_folder, child_folder, child_noart = self.create_some_folders() root_folder, child_folder, child_noart = self.create_some_folders()
track_embededart = self.create_track_in(child_noart, root_folder) track_embededart = self.create_track_in(child_noart, root_folder)
MockUser = namedtuple('User', [ 'id' ]) MockUser = namedtuple("User", ["id"])
user = MockUser(uuid.uuid4()) user = MockUser(uuid.uuid4())
root = root_folder.as_subsonic_child(user) root = root_folder.as_subsonic_child(user)
self.assertIsInstance(root, dict) self.assertIsInstance(root, dict)
self.assertIn('id', root) self.assertIn("id", root)
self.assertIn('isDir', root) self.assertIn("isDir", root)
self.assertIn('title', root) self.assertIn("title", root)
self.assertIn('album', root) self.assertIn("album", root)
self.assertIn('created', root) self.assertIn("created", root)
self.assertTrue(root['isDir']) self.assertTrue(root["isDir"])
self.assertEqual(root['title'], 'Root folder') self.assertEqual(root["title"], "Root folder")
self.assertEqual(root['album'], 'Root folder') self.assertEqual(root["album"], "Root folder")
self.assertRegex(root['created'], date_regex) self.assertRegex(root["created"], date_regex)
child = child_folder.as_subsonic_child(user) child = child_folder.as_subsonic_child(user)
self.assertIn('parent', child) self.assertIn("parent", child)
self.assertIn('artist', child) self.assertIn("artist", child)
self.assertIn('coverArt', child) self.assertIn("coverArt", child)
self.assertEqual(child['parent'], str(root_folder.id)) self.assertEqual(child["parent"], str(root_folder.id))
self.assertEqual(child['artist'], root_folder.name) self.assertEqual(child["artist"], root_folder.name)
self.assertEqual(child['coverArt'], child['id']) self.assertEqual(child["coverArt"], child["id"])
noart = child_noart.as_subsonic_child(user) noart = child_noart.as_subsonic_child(user)
self.assertIn('coverArt', noart) self.assertIn("coverArt", noart)
self.assertEqual(noart['coverArt'], str(track_embededart.id)) self.assertEqual(noart["coverArt"], str(track_embededart.id))
@db_session @db_session
def test_folder_annotation(self): def test_folder_annotation(self):
root_folder, child_folder, _ = self.create_some_folders() root_folder, child_folder, _ = self.create_some_folders()
user = self.create_user() user = self.create_user()
star = db.StarredFolder( star = db.StarredFolder(user=user, starred=root_folder)
user = user, rating_user = db.RatingFolder(user=user, rated=root_folder, rating=2)
starred = root_folder other = self.create_user("Other")
) rating_other = db.RatingFolder(user=other, rated=root_folder, rating=5)
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) root = root_folder.as_subsonic_child(user)
self.assertIn('starred', root) self.assertIn("starred", root)
self.assertIn('userRating', root) self.assertIn("userRating", root)
self.assertIn('averageRating', root) self.assertIn("averageRating", root)
self.assertRegex(root['starred'], date_regex) self.assertRegex(root["starred"], date_regex)
self.assertEqual(root['userRating'], 2) self.assertEqual(root["userRating"], 2)
self.assertEqual(root['averageRating'], 3.5) self.assertEqual(root["averageRating"], 3.5)
child = child_folder.as_subsonic_child(user) child = child_folder.as_subsonic_child(user)
self.assertNotIn('starred', child) self.assertNotIn("starred", child)
self.assertNotIn('userRating', child) self.assertNotIn("userRating", child)
@db_session @db_session
def test_artist(self): def test_artist(self):
artist = db.Artist(name = 'Test Artist') artist = db.Artist(name="Test Artist")
user = self.create_user() 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) artist_dict = artist.as_subsonic_artist(user)
self.assertIsInstance(artist_dict, dict) self.assertIsInstance(artist_dict, dict)
self.assertIn('id', artist_dict) self.assertIn("id", artist_dict)
self.assertIn('name', artist_dict) self.assertIn("name", artist_dict)
self.assertIn('albumCount', artist_dict) self.assertIn("albumCount", artist_dict)
self.assertIn('starred', artist_dict) self.assertIn("starred", artist_dict)
self.assertEqual(artist_dict['name'], 'Test Artist') self.assertEqual(artist_dict["name"], "Test Artist")
self.assertEqual(artist_dict['albumCount'], 0) self.assertEqual(artist_dict["albumCount"], 0)
self.assertRegex(artist_dict['starred'], date_regex) self.assertRegex(artist_dict["starred"], date_regex)
db.Album(name = 'Test Artist', artist = artist) # self-titled db.Album(name="Test Artist", artist=artist) # self-titled
db.Album(name = 'The Album After The First One', artist = artist) db.Album(name="The Album After The First One", artist=artist)
artist_dict = artist.as_subsonic_artist(user) artist_dict = artist.as_subsonic_artist(user)
self.assertEqual(artist_dict['albumCount'], 2) self.assertEqual(artist_dict["albumCount"], 2)
@db_session @db_session
def test_album(self): def test_album(self):
artist = db.Artist(name = 'Test Artist') artist = db.Artist(name="Test Artist")
album = db.Album(artist = artist, name = 'Test Album') album = db.Album(artist=artist, name="Test Album")
user = self.create_user() user = self.create_user()
star = db.StarredAlbum( star = db.StarredAlbum(user=user, starred=album)
user = user,
starred = album
)
# No tracks, shouldn't be stored under normal circumstances # No tracks, shouldn't be stored under normal circumstances
self.assertRaises(ValueError, album.as_subsonic_album, user) self.assertRaises(ValueError, album.as_subsonic_album, user)
root_folder, folder_art, folder_noart = self.create_some_folders() 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) album_dict = album.as_subsonic_album(user)
self.assertIsInstance(album_dict, dict) self.assertIsInstance(album_dict, dict)
self.assertIn('id', album_dict) self.assertIn("id", album_dict)
self.assertIn('name', album_dict) self.assertIn("name", album_dict)
self.assertIn('artist', album_dict) self.assertIn("artist", album_dict)
self.assertIn('artistId', album_dict) self.assertIn("artistId", album_dict)
self.assertIn('songCount', album_dict) self.assertIn("songCount", album_dict)
self.assertIn('duration', album_dict) self.assertIn("duration", album_dict)
self.assertIn('created', album_dict) self.assertIn("created", album_dict)
self.assertIn('starred', album_dict) self.assertIn("starred", album_dict)
self.assertIn('coverArt', album_dict) self.assertIn("coverArt", album_dict)
self.assertEqual(album_dict['name'], album.name) self.assertEqual(album_dict["name"], album.name)
self.assertEqual(album_dict['artist'], artist.name) self.assertEqual(album_dict["artist"], artist.name)
self.assertEqual(album_dict['artistId'], str(artist.id)) self.assertEqual(album_dict["artistId"], str(artist.id))
self.assertEqual(album_dict['songCount'], 1) self.assertEqual(album_dict["songCount"], 1)
self.assertEqual(album_dict['duration'], 5) self.assertEqual(album_dict["duration"], 5)
self.assertEqual(album_dict['coverArt'], str(track1.id)) self.assertEqual(album_dict["coverArt"], str(track1.id))
self.assertRegex(album_dict['created'], date_regex) self.assertRegex(album_dict["created"], date_regex)
self.assertRegex(album_dict['starred'], date_regex) self.assertRegex(album_dict["starred"], date_regex)
@db_session @db_session
def test_track(self): def test_track(self):
track1, track2 = self.create_some_tracks() track1, track2 = self.create_some_tracks()
# Assuming SQLite doesn't enforce foreign key constraints # Assuming SQLite doesn't enforce foreign key constraints
MockUser = namedtuple('User', [ 'id' ]) MockUser = namedtuple("User", ["id"])
user = MockUser(uuid.uuid4()) user = MockUser(uuid.uuid4())
track1_dict = track1.as_subsonic_child(user, None) track1_dict = track1.as_subsonic_child(user, None)
self.assertIsInstance(track1_dict, dict) self.assertIsInstance(track1_dict, dict)
self.assertIn('id', track1_dict) self.assertIn("id", track1_dict)
self.assertIn('parent', track1_dict) self.assertIn("parent", track1_dict)
self.assertIn('isDir', track1_dict) self.assertIn("isDir", track1_dict)
self.assertIn('title', track1_dict) self.assertIn("title", track1_dict)
self.assertFalse(track1_dict['isDir']) self.assertFalse(track1_dict["isDir"])
self.assertIn('coverArt', track1_dict) self.assertIn("coverArt", track1_dict)
self.assertEqual(track1_dict['coverArt'], track1_dict['id']) self.assertEqual(track1_dict["coverArt"], track1_dict["id"])
track2_dict = track2.as_subsonic_child(user, None) 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. # ... we'll test the rest against the API XSD.
@db_session @db_session
@ -287,15 +265,12 @@ class DbTestCase(unittest.TestCase):
def test_chat(self): def test_chat(self):
user = self.create_user() user = self.create_user()
line = db.ChatMessage( line = db.ChatMessage(user=user, message="Hello world!")
user = user,
message = 'Hello world!'
)
line_dict = line.responsize() line_dict = line.responsize()
self.assertIsInstance(line_dict, dict) self.assertIsInstance(line_dict, dict)
self.assertIn('username', line_dict) self.assertIn("username", line_dict)
self.assertEqual(line_dict['username'], user.name) self.assertEqual(line_dict["username"], user.name)
@db_session @db_session
def test_playlist(self): def test_playlist(self):
@ -310,19 +285,21 @@ class DbTestCase(unittest.TestCase):
playlist.add(track1) playlist.add(track1)
playlist.add(track2) playlist.add(track2)
self.assertSequenceEqual(playlist.get_tracks(), [ track1, track2 ]) self.assertSequenceEqual(playlist.get_tracks(), [track1, track2])
playlist.add(track2.id) playlist.add(track2.id)
playlist.add(track1.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() playlist.clear()
self.assertSequenceEqual(playlist.get_tracks(), []) self.assertSequenceEqual(playlist.get_tracks(), [])
playlist.add(str(track1.id)) 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) self.assertRaises(NameError, playlist.add, 2345)
@db_session @db_session
@ -332,18 +309,18 @@ class DbTestCase(unittest.TestCase):
playlist.add(track1) playlist.add(track1)
playlist.add(track2) playlist.add(track2)
playlist.remove_at_indexes([ 0, 2 ]) playlist.remove_at_indexes([0, 2])
self.assertSequenceEqual(playlist.get_tracks(), [ track2 ]) self.assertSequenceEqual(playlist.get_tracks(), [track2])
playlist.add(track1) playlist.add(track1)
playlist.add(track2) playlist.add(track2)
playlist.add(track2) playlist.add(track2)
playlist.remove_at_indexes([ 2, 1 ]) playlist.remove_at_indexes([2, 1])
self.assertSequenceEqual(playlist.get_tracks(), [ track2, track2 ]) self.assertSequenceEqual(playlist.get_tracks(), [track2, track2])
playlist.add(track1) playlist.add(track1)
playlist.remove_at_indexes([ 1, 1 ]) playlist.remove_at_indexes([1, 1])
self.assertSequenceEqual(playlist.get_tracks(), [ track2, track1 ]) self.assertSequenceEqual(playlist.get_tracks(), [track2, track1])
@db_session @db_session
def test_playlist_fixing(self): def test_playlist_fixing(self):
@ -353,14 +330,14 @@ class DbTestCase(unittest.TestCase):
playlist.add(track1) playlist.add(track1)
playlist.add(uuid.uuid4()) playlist.add(uuid.uuid4())
playlist.add(track2) playlist.add(track2)
self.assertSequenceEqual(playlist.get_tracks(), [ track1, track2 ]) self.assertSequenceEqual(playlist.get_tracks(), [track1, track2])
track2.delete() 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) playlist.tracks = "{0},{0},some random garbage,{0}".format(track1.id)
self.assertSequenceEqual(playlist.get_tracks(), [ track1, track1, track1 ]) self.assertSequenceEqual(playlist.get_tracks(), [track1, track1, track1])
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -13,16 +13,17 @@ import unittest
from supysonic.lastfm import LastFm from supysonic.lastfm import LastFm
class LastFmTestCase(unittest.TestCase): class LastFmTestCase(unittest.TestCase):
""" Designed only to have coverage on the most important method """ """ Designed only to have coverage on the most important method """
def test_request(self): def test_request(self):
logging.getLogger('supysonic.lastfm').addHandler(logging.NullHandler()) logging.getLogger("supysonic.lastfm").addHandler(logging.NullHandler())
lastfm = LastFm({ 'api_key': 'key', 'secret': 'secret' }, None) 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) self.assertIsInstance(rv, dict)
if __name__ == '__main__':
unittest.main()
if __name__ == "__main__":
unittest.main()

View File

@ -21,12 +21,13 @@ from supysonic import db
from supysonic.managers.folder import FolderManager from supysonic.managers.folder import FolderManager
from supysonic.scanner import Scanner from supysonic.scanner import Scanner
class ScannerTestCase(unittest.TestCase): class ScannerTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
db.init_database('sqlite:') db.init_database("sqlite:")
with db_session: 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.assertIsNotNone(folder)
self.folderid = folder.id self.folderid = folder.id
@ -38,14 +39,14 @@ class ScannerTestCase(unittest.TestCase):
@contextmanager @contextmanager
def __temporary_track_copy(self): def __temporary_track_copy(self):
track = db.Track.select().first() track = db.Track.select().first()
with tempfile.NamedTemporaryFile(dir = os.path.dirname(track.path)) as tf: with tempfile.NamedTemporaryFile(dir=os.path.dirname(track.path)) as tf:
with io.open(track.path, 'rb') as f: with io.open(track.path, "rb") as f:
tf.write(f.read()) tf.write(f.read())
yield tf yield tf
def __scan(self, force = False): def __scan(self, force=False):
self.scanner = Scanner(force) self.scanner = Scanner(force)
self.scanner.queue_folder('folder') self.scanner.queue_folder("folder")
self.scanner.run() self.scanner.run()
@db_session @db_session
@ -53,7 +54,9 @@ class ScannerTestCase(unittest.TestCase):
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
self.assertRaises(TypeError, self.scanner.queue_folder, None) 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 @db_session
def test_rescan(self): 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, None)
self.assertRaises(TypeError, self.scanner.scan_file, track) self.assertRaises(TypeError, self.scanner.scan_file, track)
self.scanner.scan_file('/some/inexistent/path') self.scanner.scan_file("/some/inexistent/path")
commit() commit()
self.assertEqual(db.Track.select().count(), 1) 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, None)
self.assertRaises(TypeError, self.scanner.remove_file, track) self.assertRaises(TypeError, self.scanner.remove_file, track)
self.scanner.remove_file('/some/inexistent/path') self.scanner.remove_file("/some/inexistent/path")
commit() commit()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
@ -97,12 +100,12 @@ class ScannerTestCase(unittest.TestCase):
@db_session @db_session
def test_move_file(self): def test_move_file(self):
track = db.Track.select().first() track = db.Track.select().first()
self.assertRaises(TypeError, self.scanner.move_file, None, 'string') 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, track, "string")
self.assertRaises(TypeError, self.scanner.move_file, 'string', None) 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, "string", track)
self.scanner.move_file('/some/inexistent/path', track.path) self.scanner.move_file("/some/inexistent/path", track.path)
commit() commit()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
@ -110,7 +113,9 @@ class ScannerTestCase(unittest.TestCase):
commit() commit()
self.assertEqual(db.Track.select().count(), 1) 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: with self.__temporary_track_copy() as tf:
self.__scan() self.__scan()
@ -121,7 +126,7 @@ class ScannerTestCase(unittest.TestCase):
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
track = db.Track.select().first() 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) self.scanner.move_file(track.path, new_path)
commit() commit()
self.assertEqual(db.Track.select().count(), 1) self.assertEqual(db.Track.select().count(), 1)
@ -137,7 +142,7 @@ class ScannerTestCase(unittest.TestCase):
self.assertEqual(db.Track.select().count(), 2) self.assertEqual(db.Track.select().count(), 2)
tf.seek(0, 0) tf.seek(0, 0)
tf.write(b'\x00' * 4096) tf.write(b"\x00" * 4096)
tf.truncate() tf.truncate()
self.__scan(True) self.__scan(True)
@ -164,20 +169,20 @@ class ScannerTestCase(unittest.TestCase):
with self.__temporary_track_copy() as tf: with self.__temporary_track_copy() as tf:
self.__scan() self.__scan()
commit() commit()
copy = db.Track.get(path = tf.name) copy = db.Track.get(path=tf.name)
self.assertEqual(copy.artist.name, 'Some artist') self.assertEqual(copy.artist.name, "Some artist")
self.assertEqual(copy.album.name, 'Awesome album') self.assertEqual(copy.album.name, "Awesome album")
tags = mutagen.File(copy.path, easy = True) tags = mutagen.File(copy.path, easy=True)
tags['artist'] = 'Renamed artist' tags["artist"] = "Renamed artist"
tags['album'] = 'Crappy album' tags["album"] = "Crappy album"
tags.save() tags.save()
self.__scan(True) self.__scan(True)
self.assertEqual(copy.artist.name, 'Renamed artist') self.assertEqual(copy.artist.name, "Renamed artist")
self.assertEqual(copy.album.name, 'Crappy album') self.assertEqual(copy.album.name, "Crappy album")
self.assertIsNotNone(db.Artist.get(name = 'Some artist')) self.assertIsNotNone(db.Artist.get(name="Some artist"))
self.assertIsNotNone(db.Album.get(name = 'Awesome album')) self.assertIsNotNone(db.Album.get(name="Awesome album"))
def test_stats(self): def test_stats(self):
stats = self.scanner.stats() stats = self.scanner.stats()
@ -188,6 +193,6 @@ class ScannerTestCase(unittest.TestCase):
self.assertEqual(stats.deleted.albums, 0) self.assertEqual(stats.deleted.albums, 0)
self.assertEqual(stats.deleted.tracks, 0) self.assertEqual(stats.deleted.tracks, 0)
if __name__ == '__main__':
unittest.main()
if __name__ == "__main__":
unittest.main()

View File

@ -18,22 +18,22 @@ from supysonic.web import create_application
from ..testbase import TestConfig from ..testbase import TestConfig
class SecretTestCase(unittest.TestCase): class SecretTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.__dbfile = tempfile.mkstemp()[1] self.__dbfile = tempfile.mkstemp()[1]
self.__dir = tempfile.mkdtemp() self.__dir = tempfile.mkdtemp()
self.config = TestConfig(False, False) self.config = TestConfig(False, False)
self.config.BASE['database_uri'] = 'sqlite:///' + self.__dbfile self.config.BASE["database_uri"] = "sqlite:///" + self.__dbfile
self.config.WEBAPP['cache_dir'] = self.__dir self.config.WEBAPP["cache_dir"] = self.__dir
init_database(self.config.BASE['database_uri']) init_database(self.config.BASE["database_uri"])
release_database() release_database()
def tearDown(self): def tearDown(self):
shutil.rmtree(self.__dir) shutil.rmtree(self.__dir)
os.remove(self.__dbfile) os.remove(self.__dbfile)
def test_key(self): def test_key(self):
app1 = create_application(self.config) app1 = create_application(self.config)
release_database() release_database()
@ -43,6 +43,6 @@ class SecretTestCase(unittest.TestCase):
self.assertEqual(app1.secret_key, app2.secret_key) self.assertEqual(app1.secret_key, app2.secret_key)
if __name__ == '__main__':
unittest.main()
if __name__ == "__main__":
unittest.main()

View File

@ -26,25 +26,23 @@ from supysonic.watcher import SupysonicWatcher
from ..testbase import TestConfig from ..testbase import TestConfig
class WatcherTestConfig(TestConfig): class WatcherTestConfig(TestConfig):
DAEMON = { DAEMON = {"wait_delay": 0.5, "log_file": "/dev/null", "log_level": "DEBUG"}
'wait_delay': 0.5,
'log_file': '/dev/null',
'log_level': 'DEBUG'
}
def __init__(self, db_uri): def __init__(self, db_uri):
super(WatcherTestConfig, self).__init__(False, False) super(WatcherTestConfig, self).__init__(False, False)
self.BASE['database_uri'] = db_uri self.BASE["database_uri"] = db_uri
class WatcherTestBase(unittest.TestCase): class WatcherTestBase(unittest.TestCase):
def setUp(self): def setUp(self):
self.__dbfile = tempfile.mkstemp()[1] self.__dbfile = tempfile.mkstemp()[1]
dburi = 'sqlite:///' + self.__dbfile dburi = "sqlite:///" + self.__dbfile
init_database(dburi) init_database(dburi)
conf = WatcherTestConfig(dburi) conf = WatcherTestConfig(dburi)
self.__sleep_time = conf.DAEMON['wait_delay'] + 1 self.__sleep_time = conf.DAEMON["wait_delay"] + 1
self.__watcher = SupysonicWatcher(conf) self.__watcher = SupysonicWatcher(conf)
@ -65,12 +63,13 @@ class WatcherTestBase(unittest.TestCase):
def _sleep(self): def _sleep(self):
time.sleep(self.__sleep_time) time.sleep(self.__sleep_time)
class WatcherTestCase(WatcherTestBase): class WatcherTestCase(WatcherTestBase):
def setUp(self): def setUp(self):
super(WatcherTestCase, self).setUp() super(WatcherTestCase, self).setUp()
self.__dir = tempfile.mkdtemp() self.__dir = tempfile.mkdtemp()
with db_session: with db_session:
FolderManager.add('Folder', self.__dir) FolderManager.add("Folder", self.__dir)
self._start() self._start()
def tearDown(self): def tearDown(self):
@ -83,25 +82,28 @@ class WatcherTestCase(WatcherTestBase):
with tempfile.NamedTemporaryFile() as f: with tempfile.NamedTemporaryFile() as f:
return os.path.basename(f.name) return os.path.basename(f.name)
def _temppath(self, suffix, depth = 0): def _temppath(self, suffix, depth=0):
if 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) os.makedirs(dirpath)
else: else:
dirpath = self.__dir dirpath = self.__dir
return os.path.join(dirpath, self._tempname() + suffix) return os.path.join(dirpath, self._tempname() + suffix)
def _addfile(self, depth = 0): def _addfile(self, depth=0):
path = self._temppath('.mp3', depth) path = self._temppath(".mp3", depth)
shutil.copyfile('tests/assets/folder/silence.mp3', path) shutil.copyfile("tests/assets/folder/silence.mp3", path)
return path return path
def _addcover(self, suffix = None, depth = 0): def _addcover(self, suffix=None, depth=0):
suffix = '.jpg' if suffix is None else (suffix + '.jpg') suffix = ".jpg" if suffix is None else (suffix + ".jpg")
path = self._temppath(suffix, depth) path = self._temppath(suffix, depth)
shutil.copyfile('tests/assets/cover.jpg', path) shutil.copyfile("tests/assets/cover.jpg", path)
return path return path
class AudioWatcherTestCase(WatcherTestCase): class AudioWatcherTestCase(WatcherTestCase):
@db_session @db_session
def assertTrackCountEqual(self, expected): def assertTrackCountEqual(self, expected):
@ -114,7 +116,7 @@ class AudioWatcherTestCase(WatcherTestCase):
self.assertTrackCountEqual(1) self.assertTrackCountEqual(1)
# This test now fails and I don't understand why # 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._addfile()
# self._stop() # self._stop()
# self.assertTrackCountEqual(1) # self.assertTrackCountEqual(1)
@ -136,18 +138,22 @@ class AudioWatcherTestCase(WatcherTestCase):
trackid = None trackid = None
with db_session: with db_session:
self.assertEqual(Track.select().count(), 1) 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 trackid = Track.select().first().id
tags = mutagen.File(path, easy = True) tags = mutagen.File(path, easy=True)
tags['artist'] = 'Renamed' tags["artist"] = "Renamed"
tags.save() tags.save()
self._sleep() self._sleep()
with db_session: with db_session:
self.assertEqual(Track.select().count(), 1) self.assertEqual(Track.select().count(), 1)
self.assertEqual(Artist.select(lambda a: a.name == 'Some artist').count(), 0) self.assertEqual(
self.assertEqual(Artist.select(lambda a: a.name == 'Renamed').count(), 1) 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) self.assertEqual(Track.select().first().id, trackid)
def test_rename(self): def test_rename(self):
@ -159,7 +165,7 @@ class AudioWatcherTestCase(WatcherTestCase):
self.assertEqual(Track.select().count(), 1) self.assertEqual(Track.select().count(), 1)
trackid = Track.select().first().id trackid = Track.select().first().id
newpath = self._temppath('.mp3') newpath = self._temppath(".mp3")
shutil.move(path, newpath) shutil.move(path, newpath)
self._sleep() self._sleep()
@ -168,14 +174,16 @@ class AudioWatcherTestCase(WatcherTestCase):
self.assertIsNotNone(track) self.assertIsNotNone(track)
self.assertNotEqual(track.path, path) self.assertNotEqual(track.path, path)
self.assertEqual(track.path, newpath) 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) self.assertEqual(track.id, trackid)
def test_move_in(self): def test_move_in(self):
filename = self._tempname() + '.mp3' filename = self._tempname() + ".mp3"
initialpath = os.path.join(tempfile.gettempdir(), filename) initialpath = os.path.join(tempfile.gettempdir(), filename)
shutil.copyfile('tests/assets/folder/silence.mp3', initialpath) shutil.copyfile("tests/assets/folder/silence.mp3", initialpath)
shutil.move(initialpath, self._temppath('.mp3')) shutil.move(initialpath, self._temppath(".mp3"))
self._sleep() self._sleep()
self.assertTrackCountEqual(1) self.assertTrackCountEqual(1)
@ -208,7 +216,7 @@ class AudioWatcherTestCase(WatcherTestCase):
def test_add_rename(self): def test_add_rename(self):
path = self._addfile() path = self._addfile()
shutil.move(path, self._temppath('.mp3')) shutil.move(path, self._temppath(".mp3"))
self._sleep() self._sleep()
self.assertTrackCountEqual(1) self.assertTrackCountEqual(1)
@ -217,7 +225,7 @@ class AudioWatcherTestCase(WatcherTestCase):
self._sleep() self._sleep()
self.assertTrackCountEqual(1) self.assertTrackCountEqual(1)
newpath = self._temppath('.mp3') newpath = self._temppath(".mp3")
shutil.move(path, newpath) shutil.move(path, newpath)
os.unlink(newpath) os.unlink(newpath)
self._sleep() self._sleep()
@ -225,7 +233,7 @@ class AudioWatcherTestCase(WatcherTestCase):
def test_add_rename_delete(self): def test_add_rename_delete(self):
path = self._addfile() path = self._addfile()
newpath = self._temppath('.mp3') newpath = self._temppath(".mp3")
shutil.move(path, newpath) shutil.move(path, newpath)
os.unlink(newpath) os.unlink(newpath)
self._sleep() self._sleep()
@ -236,13 +244,14 @@ class AudioWatcherTestCase(WatcherTestCase):
self._sleep() self._sleep()
self.assertTrackCountEqual(1) self.assertTrackCountEqual(1)
newpath = self._temppath('.mp3') newpath = self._temppath(".mp3")
finalpath = self._temppath('.mp3') finalpath = self._temppath(".mp3")
shutil.move(path, newpath) shutil.move(path, newpath)
shutil.move(newpath, finalpath) shutil.move(newpath, finalpath)
self._sleep() self._sleep()
self.assertTrackCountEqual(1) self.assertTrackCountEqual(1)
class CoverWatcherTestCase(WatcherTestCase): class CoverWatcherTestCase(WatcherTestCase):
def test_add_file_then_cover(self): def test_add_file_then_cover(self):
self._addfile() self._addfile()
@ -274,14 +283,14 @@ class CoverWatcherTestCase(WatcherTestCase):
def test_naming_add_good(self): def test_naming_add_good(self):
bad = os.path.basename(self._addcover()) bad = os.path.basename(self._addcover())
self._sleep() self._sleep()
good = os.path.basename(self._addcover('cover')) good = os.path.basename(self._addcover("cover"))
self._sleep() self._sleep()
with db_session: with db_session:
self.assertEqual(Folder.select().first().cover_art, good) self.assertEqual(Folder.select().first().cover_art, good)
def test_naming_add_bad(self): def test_naming_add_bad(self):
good = os.path.basename(self._addcover('cover')) good = os.path.basename(self._addcover("cover"))
self._sleep() self._sleep()
bad = os.path.basename(self._addcover()) bad = os.path.basename(self._addcover())
self._sleep() self._sleep()
@ -291,7 +300,7 @@ class CoverWatcherTestCase(WatcherTestCase):
def test_naming_remove_good(self): def test_naming_remove_good(self):
bad = self._addcover() bad = self._addcover()
good = self._addcover('cover') good = self._addcover("cover")
self._sleep() self._sleep()
os.unlink(good) os.unlink(good)
self._sleep() self._sleep()
@ -301,7 +310,7 @@ class CoverWatcherTestCase(WatcherTestCase):
def test_naming_remove_bad(self): def test_naming_remove_bad(self):
bad = self._addcover() bad = self._addcover()
good = self._addcover('cover') good = self._addcover("cover")
self._sleep() self._sleep()
os.unlink(bad) os.unlink(bad)
self._sleep() self._sleep()
@ -312,22 +321,24 @@ class CoverWatcherTestCase(WatcherTestCase):
def test_rename(self): def test_rename(self):
path = self._addcover() path = self._addcover()
self._sleep() self._sleep()
newpath = self._temppath('.jpg') newpath = self._temppath(".jpg")
shutil.move(path, newpath) shutil.move(path, newpath)
self._sleep() self._sleep()
with db_session: 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): def test_add_to_folder_without_track(self):
path = self._addcover(depth = 1) path = self._addcover(depth=1)
self._sleep() self._sleep()
with db_session: 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): def test_remove_from_folder_without_track(self):
path = self._addcover(depth = 1) path = self._addcover(depth=1)
self._sleep() self._sleep()
os.unlink(path) os.unlink(path)
self._sleep() self._sleep()
@ -336,6 +347,7 @@ class CoverWatcherTestCase(WatcherTestCase):
self._addfile(1) self._addfile(1)
self._sleep() self._sleep()
def suite(): def suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
@ -344,6 +356,6 @@ def suite():
return suite return suite
if __name__ == '__main__':
unittest.main()
if __name__ == "__main__":
unittest.main()

View File

@ -15,6 +15,7 @@ from .test_folder import FolderTestCase
from .test_playlist import PlaylistTestCase from .test_playlist import PlaylistTestCase
from .test_user import UserTestCase from .test_user import UserTestCase
def suite(): def suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
@ -24,4 +25,3 @@ def suite():
suite.addTest(unittest.makeSuite(UserTestCase)) suite.addTest(unittest.makeSuite(UserTestCase))
return suite return suite

View File

@ -9,6 +9,7 @@
from ..testbase import TestBase from ..testbase import TestBase
class FrontendTestBase(TestBase): class FrontendTestBase(TestBase):
__with_webui__ = True __with_webui__ = True
@ -17,8 +18,11 @@ class FrontendTestBase(TestBase):
self._patch_client() self._patch_client()
def _login(self, username, password): 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): def _logout(self):
return self.client.get('/user/logout', follow_redirects = True) return self.client.get("/user/logout", follow_redirects=True)

View File

@ -16,87 +16,85 @@ from supysonic.db import Folder
from .frontendtestbase import FrontendTestBase from .frontendtestbase import FrontendTestBase
class FolderTestCase(FrontendTestBase): class FolderTestCase(FrontendTestBase):
def test_index(self): def test_index(self):
self._login('bob', 'B0b') self._login("bob", "B0b")
rv = self.client.get('/folder', follow_redirects = True) rv = self.client.get("/folder", follow_redirects=True)
self.assertIn('There\'s nothing much to see', rv.data) self.assertIn("There's nothing much to see", rv.data)
self.assertNotIn('Music folders', rv.data) self.assertNotIn("Music folders", rv.data)
self._logout() self._logout()
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/folder') rv = self.client.get("/folder")
self.assertIn('Music folders', rv.data) self.assertIn("Music folders", rv.data)
def test_add_get(self): def test_add_get(self):
self._login('bob', 'B0b') self._login("bob", "B0b")
rv = self.client.get('/folder/add', follow_redirects = True) rv = self.client.get("/folder/add", follow_redirects=True)
self.assertIn('There\'s nothing much to see', rv.data) self.assertIn("There's nothing much to see", rv.data)
self.assertNotIn('Add Folder', rv.data) self.assertNotIn("Add Folder", rv.data)
self._logout() self._logout()
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/folder/add') rv = self.client.get("/folder/add")
self.assertIn('Add Folder', rv.data) self.assertIn("Add Folder", rv.data)
def test_add_post(self): def test_add_post(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.post('/folder/add') rv = self.client.post("/folder/add")
self.assertIn('required', rv.data) self.assertIn("required", rv.data)
rv = self.client.post('/folder/add', data = { 'name': 'name' }) rv = self.client.post("/folder/add", data={"name": "name"})
self.assertIn('required', rv.data) self.assertIn("required", rv.data)
rv = self.client.post('/folder/add', data = { 'path': 'path' }) rv = self.client.post("/folder/add", data={"path": "path"})
self.assertIn('required', rv.data) self.assertIn("required", rv.data)
rv = self.client.post('/folder/add', data = { 'name': 'name', 'path': 'path' }) rv = self.client.post("/folder/add", data={"name": "name", "path": "path"})
self.assertIn('Add Folder', rv.data) self.assertIn("Add Folder", rv.data)
rv = self.client.post('/folder/add', data = { 'name': 'name', 'path': 'tests/assets' }, follow_redirects = True) rv = self.client.post(
self.assertIn('created', rv.data) "/folder/add",
data={"name": "name", "path": "tests/assets"},
follow_redirects=True,
)
self.assertIn("created", rv.data)
with db_session: with db_session:
self.assertEqual(Folder.select().count(), 1) self.assertEqual(Folder.select().count(), 1)
def test_delete(self): def test_delete(self):
with db_session: with db_session:
folder = Folder( folder = Folder(name="folder", path="tests/assets", root=True)
name = 'folder',
path = 'tests/assets',
root = True
)
self._login('bob', 'B0b') self._login("bob", "B0b")
rv = self.client.get('/folder/del/' + str(folder.id), follow_redirects = True) rv = self.client.get("/folder/del/" + str(folder.id), follow_redirects=True)
self.assertIn('There\'s nothing much to see', rv.data) self.assertIn("There's nothing much to see", rv.data)
with db_session: with db_session:
self.assertEqual(Folder.select().count(), 1) self.assertEqual(Folder.select().count(), 1)
self._logout() self._logout()
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/folder/del/string', follow_redirects = True) rv = self.client.get("/folder/del/string", follow_redirects=True)
self.assertIn('badly formed', rv.data) self.assertIn("badly formed", rv.data)
rv = self.client.get('/folder/del/' + str(uuid.uuid4()), follow_redirects = True) rv = self.client.get("/folder/del/" + str(uuid.uuid4()), follow_redirects=True)
self.assertIn('No such folder', rv.data) self.assertIn("No such folder", rv.data)
rv = self.client.get('/folder/del/' + str(folder.id), follow_redirects = True) rv = self.client.get("/folder/del/" + str(folder.id), follow_redirects=True)
self.assertIn('Music folders', rv.data) self.assertIn("Music folders", rv.data)
with db_session: with db_session:
self.assertEqual(Folder.select().count(), 0) self.assertEqual(Folder.select().count(), 0)
def test_scan(self): def test_scan(self):
with db_session: with db_session:
folder = Folder( folder = Folder(name="folder", path="tests/assets/folder", root=True)
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) rv = self.client.get("/folder/scan/string", follow_redirects=True)
self.assertIn('badly formed', rv.data) self.assertIn("badly formed", rv.data)
rv = self.client.get('/folder/scan/' + str(uuid.uuid4()), follow_redirects = True) rv = self.client.get("/folder/scan/" + str(uuid.uuid4()), follow_redirects=True)
self.assertIn('No such folder', rv.data) self.assertIn("No such folder", rv.data)
rv = self.client.get('/folder/scan/' + str(folder.id), follow_redirects = True) rv = self.client.get("/folder/scan/" + str(folder.id), follow_redirects=True)
self.assertIn('start', rv.data) self.assertIn("start", rv.data)
rv = self.client.get('/folder/scan', follow_redirects = True) rv = self.client.get("/folder/scan", follow_redirects=True)
self.assertIn('start', rv.data) self.assertIn("start", rv.data)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -17,61 +17,62 @@ from supysonic.db import User
from .frontendtestbase import FrontendTestBase from .frontendtestbase import FrontendTestBase
class LoginTestCase(FrontendTestBase): class LoginTestCase(FrontendTestBase):
def test_unauthorized_request(self): def test_unauthorized_request(self):
# Unauthorized request # Unauthorized request
rv = self.client.get('/', follow_redirects=True) rv = self.client.get("/", follow_redirects=True)
self.assertIn('Please login', rv.data) self.assertIn("Please login", rv.data)
def test_login_with_bad_data(self): def test_login_with_bad_data(self):
# Login with not blank user or password # Login with not blank user or password
rv = self._login('', '') rv = self._login("", "")
self.assertIn('Missing user name', rv.data) self.assertIn("Missing user name", rv.data)
self.assertIn('Missing password', rv.data) self.assertIn("Missing password", rv.data)
# Login with not valid user or password # Login with not valid user or password
rv = self._login('nonexistent', 'nonexistent') rv = self._login("nonexistent", "nonexistent")
self.assertIn('Wrong username or password', rv.data) self.assertIn("Wrong username or password", rv.data)
rv = self._login('alice', 'badpassword') rv = self._login("alice", "badpassword")
self.assertIn('Wrong username or password', rv.data) self.assertIn("Wrong username or password", rv.data)
def test_login_admin(self): def test_login_admin(self):
# Login with a valid admin user # Login with a valid admin user
rv = self._login('alice', 'Alic3') rv = self._login("alice", "Alic3")
self.assertIn('Logged in', rv.data) self.assertIn("Logged in", rv.data)
self.assertIn('Users', rv.data) self.assertIn("Users", rv.data)
self.assertIn('Folders', rv.data) self.assertIn("Folders", rv.data)
def test_login_non_admin(self): def test_login_non_admin(self):
# Login with a valid non-admin user # Login with a valid non-admin user
rv = self._login('bob', 'B0b') rv = self._login("bob", "B0b")
self.assertIn('Logged in', rv.data) self.assertIn("Logged in", rv.data)
# Non-admin user cannot acces to users and folders # Non-admin user cannot acces to users and folders
self.assertNotIn('Users', rv.data) self.assertNotIn("Users", rv.data)
self.assertNotIn('Folders', rv.data) self.assertNotIn("Folders", rv.data)
def test_root_with_valid_session(self): def test_root_with_valid_session(self):
# Root with valid session # Root with valid session
with db_session: with db_session:
with self.client.session_transaction() as sess: with self.client.session_transaction() as sess:
sess['userid'] = User.get(name = 'alice').id sess["userid"] = User.get(name="alice").id
rv = self.client.get('/', follow_redirects=True) rv = self.client.get("/", follow_redirects=True)
self.assertIn('alice', rv.data) self.assertIn("alice", rv.data)
self.assertIn('Log out', rv.data) self.assertIn("Log out", rv.data)
self.assertIn('There\'s nothing much to see here.', rv.data) self.assertIn("There's nothing much to see here.", rv.data)
def test_root_with_non_valid_session(self): def test_root_with_non_valid_session(self):
# Root with a no-valid session # Root with a no-valid session
with self.client.session_transaction() as sess: with self.client.session_transaction() as sess:
sess['userid'] = uuid.uuid4() sess["userid"] = uuid.uuid4()
rv = self.client.get('/', follow_redirects=True) rv = self.client.get("/", follow_redirects=True)
self.assertIn('Please login', rv.data) self.assertIn("Please login", rv.data)
def test_multiple_login(self): def test_multiple_login(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self._login('bob', 'B0b') rv = self._login("bob", "B0b")
self.assertIn('Already logged in', rv.data) self.assertIn("Already logged in", rv.data)
self.assertIn('alice', rv.data) self.assertIn("alice", rv.data)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -16,99 +16,111 @@ from supysonic.db import Folder, Artist, Album, Track, Playlist, User
from .frontendtestbase import FrontendTestBase from .frontendtestbase import FrontendTestBase
class PlaylistTestCase(FrontendTestBase): class PlaylistTestCase(FrontendTestBase):
def setUp(self): def setUp(self):
super(PlaylistTestCase, self).setUp() super(PlaylistTestCase, self).setUp()
with db_session: with db_session:
folder = Folder(name = 'Root', path = 'tests/assets', root = True) folder = Folder(name="Root", path="tests/assets", root=True)
artist = Artist(name = 'Artist!') artist = Artist(name="Artist!")
album = Album(name = 'Album!', artist = artist) album = Album(name="Album!", artist=artist)
track = Track( track = Track(
path = 'tests/assets/23bytes', path="tests/assets/23bytes",
title = '23bytes', title="23bytes",
artist = artist, artist=artist,
album = album, album=album,
folder = folder, folder=folder,
root_folder = folder, root_folder=folder,
duration = 2, duration=2,
disc = 1, disc=1,
number = 1, number=1,
bitrate = 320, bitrate=320,
last_modification = 0 last_modification=0,
) )
playlist = Playlist( playlist = Playlist(name="Playlist!", user=User.get(name="alice"))
name = 'Playlist!',
user = User.get(name = 'alice')
)
for _ in range(4): for _ in range(4):
playlist.add(track) playlist.add(track)
self.playlistid = playlist.id self.playlistid = playlist.id
def test_index(self): def test_index(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/playlist') rv = self.client.get("/playlist")
self.assertIn('My playlists', rv.data) self.assertIn("My playlists", rv.data)
def test_details(self): def test_details(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/playlist/string', follow_redirects = True) rv = self.client.get("/playlist/string", follow_redirects=True)
self.assertIn('Invalid', rv.data) self.assertIn("Invalid", rv.data)
rv = self.client.get('/playlist/' + str(uuid.uuid4()), follow_redirects = True) rv = self.client.get("/playlist/" + str(uuid.uuid4()), follow_redirects=True)
self.assertIn('Unknown', rv.data) self.assertIn("Unknown", rv.data)
rv = self.client.get('/playlist/' + str(self.playlistid)) rv = self.client.get("/playlist/" + str(self.playlistid))
self.assertIn('Playlist!', rv.data) self.assertIn("Playlist!", rv.data)
self.assertIn('23bytes', rv.data) self.assertIn("23bytes", rv.data)
self.assertIn('Artist!', rv.data) self.assertIn("Artist!", rv.data)
self.assertIn('Album!', rv.data) self.assertIn("Album!", rv.data)
def test_update(self): def test_update(self):
self._login('bob', 'B0b') self._login("bob", "B0b")
rv = self.client.post('/playlist/string', follow_redirects = True) rv = self.client.post("/playlist/string", follow_redirects=True)
self.assertIn('Invalid', rv.data) self.assertIn("Invalid", rv.data)
rv = self.client.post('/playlist/' + str(uuid.uuid4()), follow_redirects = True) rv = self.client.post("/playlist/" + str(uuid.uuid4()), follow_redirects=True)
self.assertIn('Unknown', rv.data) self.assertIn("Unknown", rv.data)
rv = self.client.post('/playlist/' + str(self.playlistid), follow_redirects = True) rv = self.client.post(
self.assertNotIn('updated', rv.data) "/playlist/" + str(self.playlistid), follow_redirects=True
self.assertIn('not allowed', rv.data) )
self.assertNotIn("updated", rv.data)
self.assertIn("not allowed", rv.data)
self._logout() self._logout()
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.post('/playlist/' + str(self.playlistid), follow_redirects = True) rv = self.client.post(
self.assertNotIn('updated', rv.data) "/playlist/" + str(self.playlistid), follow_redirects=True
self.assertIn('Missing', rv.data) )
self.assertNotIn("updated", rv.data)
self.assertIn("Missing", rv.data)
with db_session: 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) rv = self.client.post(
self.assertIn('updated', rv.data) "/playlist/" + str(self.playlistid),
self.assertNotIn('not allowed', rv.data) data={"name": "abc", "public": True},
follow_redirects=True,
)
self.assertIn("updated", rv.data)
self.assertNotIn("not allowed", rv.data)
with db_session: with db_session:
playlist = Playlist[self.playlistid] playlist = Playlist[self.playlistid]
self.assertEqual(playlist.name, 'abc') self.assertEqual(playlist.name, "abc")
self.assertTrue(playlist.public) self.assertTrue(playlist.public)
def test_delete(self): def test_delete(self):
self._login('bob', 'B0b') self._login("bob", "B0b")
rv = self.client.get('/playlist/del/string', follow_redirects = True) rv = self.client.get("/playlist/del/string", follow_redirects=True)
self.assertIn('Invalid', rv.data) self.assertIn("Invalid", rv.data)
rv = self.client.get('/playlist/del/' + str(uuid.uuid4()), follow_redirects = True) rv = self.client.get(
self.assertIn('Unknown', rv.data) "/playlist/del/" + str(uuid.uuid4()), follow_redirects=True
rv = self.client.get('/playlist/del/' + str(self.playlistid), follow_redirects = True) )
self.assertIn('not allowed', rv.data) 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: with db_session:
self.assertEqual(Playlist.select().count(), 1) self.assertEqual(Playlist.select().count(), 1)
self._logout() self._logout()
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/playlist/del/' + str(self.playlistid), follow_redirects = True) rv = self.client.get(
self.assertIn('deleted', rv.data) "/playlist/del/" + str(self.playlistid), follow_redirects=True
)
self.assertIn("deleted", rv.data)
with db_session: with db_session:
self.assertEqual(Playlist.select().count(), 0) self.assertEqual(Playlist.select().count(), 0)
if __name__ == '__main__':
unittest.main()
if __name__ == "__main__":
unittest.main()

View File

@ -17,225 +17,258 @@ from supysonic.db import User, ClientPrefs
from .frontendtestbase import FrontendTestBase from .frontendtestbase import FrontendTestBase
class UserTestCase(FrontendTestBase): class UserTestCase(FrontendTestBase):
def setUp(self): def setUp(self):
super(UserTestCase, self).setUp() super(UserTestCase, self).setUp()
with db_session: 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): def test_index(self):
self._login('bob', 'B0b') self._login("bob", "B0b")
rv = self.client.get('/user', follow_redirects = True) rv = self.client.get("/user", follow_redirects=True)
self.assertIn('There\'s nothing much to see', rv.data) self.assertIn("There's nothing much to see", rv.data)
self.assertNotIn('Users', rv.data) self.assertNotIn("Users", rv.data)
self._logout() self._logout()
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/user') rv = self.client.get("/user")
self.assertIn('Users', rv.data) self.assertIn("Users", rv.data)
def test_details(self): def test_details(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/user/string', follow_redirects = True) rv = self.client.get("/user/string", follow_redirects=True)
self.assertIn('badly formed', rv.data) self.assertIn("badly formed", rv.data)
rv = self.client.get('/user/' + str(uuid.uuid4()), follow_redirects = True) rv = self.client.get("/user/" + str(uuid.uuid4()), follow_redirects=True)
self.assertIn('No such user', rv.data) self.assertIn("No such user", rv.data)
rv = self.client.get('/user/' + str(self.users['bob'])) rv = self.client.get("/user/" + str(self.users["bob"]))
self.assertIn('bob', rv.data) self.assertIn("bob", rv.data)
self._logout() self._logout()
with db_session: 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') self._login("bob", "B0b")
rv = self.client.get('/user/' + str(self.users['alice']), follow_redirects = True) rv = self.client.get("/user/" + str(self.users["alice"]), follow_redirects=True)
self.assertIn('There\'s nothing much to see', rv.data) self.assertIn("There's nothing much to see", rv.data)
self.assertNotIn('<h2>bob</h2>', rv.data) self.assertNotIn("<h2>bob</h2>", rv.data)
rv = self.client.get('/user/me') rv = self.client.get("/user/me")
self.assertIn('<h2>bob</h2>', rv.data) self.assertIn("<h2>bob</h2>", rv.data)
self.assertIn('tests', rv.data) self.assertIn("tests", rv.data)
def test_update_client_prefs(self): def test_update_client_prefs(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.post('/user/me') rv = self.client.post("/user/me")
self.assertIn('updated', rv.data) # does nothing, says it's updated anyway self.assertIn("updated", rv.data) # does nothing, says it's updated anyway
# error cases, silently ignored # error cases, silently ignored
self.client.post('/user/me', data = { 'garbage': 'trash' }) 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={"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={"_l": "m"})
self.client.post('/user/me', data = { 'n_': 'o' }) self.client.post("/user/me", data={"n_": "o"})
self.client.post('/user/me', data = { 'inexisting_client': 'setting' }) self.client.post("/user/me", data={"inexisting_client": "setting"})
with db_session: 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 }) rv = self.client.post(
self.assertIn('updated', rv.data) "/user/me", data={"tests_format": "mp3", "tests_bitrate": 128}
)
self.assertIn("updated", rv.data)
with db_session: with db_session:
prefs = ClientPrefs[User[self.users['alice']], 'tests'] prefs = ClientPrefs[User[self.users["alice"]], "tests"]
self.assertEqual(prefs.format, 'mp3') self.assertEqual(prefs.format, "mp3")
self.assertEqual(prefs.bitrate, 128) 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: with db_session:
self.assertEqual(ClientPrefs.select().count(), 0) self.assertEqual(ClientPrefs.select().count(), 0)
def test_change_username_get(self): def test_change_username_get(self):
self._login('bob', 'B0b') self._login("bob", "B0b")
rv = self.client.get('/user/whatever/changeusername', follow_redirects = True) rv = self.client.get("/user/whatever/changeusername", follow_redirects=True)
self.assertIn('There\'s nothing much to see', rv.data) self.assertIn("There's nothing much to see", rv.data)
self._logout() self._logout()
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/user/whatever/changeusername', follow_redirects = True) rv = self.client.get("/user/whatever/changeusername", follow_redirects=True)
self.assertIn('badly formed', rv.data) self.assertIn("badly formed", rv.data)
rv = self.client.get('/user/{}/changeusername'.format(uuid.uuid4()), follow_redirects = True) rv = self.client.get(
self.assertIn('No such user', rv.data) "/user/{}/changeusername".format(uuid.uuid4()), follow_redirects=True
self.client.get('/user/{}/changeusername'.format(self.users['bob'])) )
self.assertIn("No such user", rv.data)
self.client.get("/user/{}/changeusername".format(self.users["bob"]))
def test_change_username_post(self): def test_change_username_post(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.post('/user/whatever/changeusername', follow_redirects = True) rv = self.client.post("/user/whatever/changeusername", follow_redirects=True)
self.assertIn('badly formed', rv.data) self.assertIn("badly formed", rv.data)
rv = self.client.post('/user/{}/changeusername'.format(uuid.uuid4()), follow_redirects = True) rv = self.client.post(
self.assertIn('No such user', rv.data) "/user/{}/changeusername".format(uuid.uuid4()), follow_redirects=True
)
self.assertIn("No such user", rv.data)
path = '/user/{}/changeusername'.format(self.users['bob']) path = "/user/{}/changeusername".format(self.users["bob"])
rv = self.client.post(path, follow_redirects = True) rv = self.client.post(path, follow_redirects=True)
self.assertIn('required', rv.data) self.assertIn("required", rv.data)
rv = self.client.post(path, data = { 'user': 'bob' }, follow_redirects = True) rv = self.client.post(path, data={"user": "bob"}, follow_redirects=True)
self.assertIn('No changes', rv.data) self.assertIn("No changes", rv.data)
rv = self.client.post(path, data = { 'user': 'b0b', 'admin': 1 }, follow_redirects = True) rv = self.client.post(
self.assertIn('updated', rv.data) path, data={"user": "b0b", "admin": 1}, follow_redirects=True
self.assertIn('b0b', rv.data) )
self.assertIn("updated", rv.data)
self.assertIn("b0b", rv.data)
with db_session: with db_session:
bob = User[self.users['bob']] bob = User[self.users["bob"]]
self.assertEqual(bob.name, 'b0b') self.assertEqual(bob.name, "b0b")
self.assertTrue(bob.admin) 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: 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): def test_change_mail_get(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
self.client.get('/user/me/changemail') self.client.get("/user/me/changemail")
# whatever # whatever
def test_change_mail_post(self): def test_change_mail_post(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
self.client.post('/user/me/changemail') self.client.post("/user/me/changemail")
# whatever # whatever
def test_change_password_get(self): def test_change_password_get(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/user/me/changepass') rv = self.client.get("/user/me/changepass")
self.assertIn('Current password', rv.data) self.assertIn("Current password", rv.data)
rv = self.client.get('/user/{}/changepass'.format(self.users['bob'])) rv = self.client.get("/user/{}/changepass".format(self.users["bob"]))
self.assertNotIn('Current password', rv.data) self.assertNotIn("Current password", rv.data)
def test_change_password_post(self): def test_change_password_post(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
path = '/user/me/changepass' path = "/user/me/changepass"
rv = self.client.post(path) rv = self.client.post(path)
self.assertIn('required', rv.data) self.assertIn("required", rv.data)
rv = self.client.post(path, data = { 'current': 'alice' }) rv = self.client.post(path, data={"current": "alice"})
self.assertIn('required', rv.data) self.assertIn("required", rv.data)
rv = self.client.post(path, data = { 'new': 'alice' }) rv = self.client.post(path, data={"new": "alice"})
self.assertIn('required', rv.data) self.assertIn("required", rv.data)
rv = self.client.post(path, data = { 'current': 'alice', 'new': 'alice' }) rv = self.client.post(path, data={"current": "alice", "new": "alice"})
self.assertIn('password and its confirmation don', rv.data) self.assertIn("password and its confirmation don", rv.data)
rv = self.client.post(path, data = { 'current': 'alice', 'new': 'alice', 'confirm': 'alice' }) rv = self.client.post(
self.assertIn('Wrong password', rv.data) path, data={"current": "alice", "new": "alice", "confirm": "alice"}
)
self.assertIn("Wrong password", rv.data)
self._logout() self._logout()
rv = self._login('alice', 'Alic3') rv = self._login("alice", "Alic3")
self.assertIn('Logged in', rv.data) self.assertIn("Logged in", rv.data)
rv = self.client.post(path, data = { 'current': 'Alic3', 'new': 'alice', 'confirm': 'alice' }, follow_redirects = True) rv = self.client.post(
self.assertIn('changed', rv.data) path,
data={"current": "Alic3", "new": "alice", "confirm": "alice"},
follow_redirects=True,
)
self.assertIn("changed", rv.data)
self._logout() self._logout()
rv = self._login('alice', 'alice') rv = self._login("alice", "alice")
self.assertIn('Logged in', rv.data) 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) rv = self.client.post(path)
self.assertIn('required', rv.data) self.assertIn("required", rv.data)
rv = self.client.post(path, data = { 'new': 'alice' }) rv = self.client.post(path, data={"new": "alice"})
self.assertIn('password and its confirmation don', rv.data) self.assertIn("password and its confirmation don", rv.data)
rv = self.client.post(path, data = { 'new': 'alice', 'confirm': 'alice' }, follow_redirects = True) rv = self.client.post(
self.assertIn('changed', rv.data) path, data={"new": "alice", "confirm": "alice"}, follow_redirects=True
)
self.assertIn("changed", rv.data)
self._logout() self._logout()
rv = self._login('bob', 'alice') rv = self._login("bob", "alice")
self.assertIn('Logged in', rv.data) self.assertIn("Logged in", rv.data)
def test_add_get(self): def test_add_get(self):
self._login('bob', 'B0b') self._login("bob", "B0b")
rv = self.client.get('/user/add', follow_redirects = True) rv = self.client.get("/user/add", follow_redirects=True)
self.assertIn('There\'s nothing much to see', rv.data) self.assertIn("There's nothing much to see", rv.data)
self.assertNotIn('Add User', rv.data) self.assertNotIn("Add User", rv.data)
self._logout() self._logout()
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/user/add') rv = self.client.get("/user/add")
self.assertIn('Add User', rv.data) self.assertIn("Add User", rv.data)
def test_add_post(self): def test_add_post(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.post('/user/add') rv = self.client.post("/user/add")
self.assertIn('required', rv.data) self.assertIn("required", rv.data)
rv = self.client.post('/user/add', data = { 'user': 'user' }) rv = self.client.post("/user/add", data={"user": "user"})
self.assertIn('Please provide a password', rv.data) self.assertIn("Please provide a password", rv.data)
rv = self.client.post('/user/add', data = { 'passwd': 'passwd' }) rv = self.client.post("/user/add", data={"passwd": "passwd"})
self.assertIn('required', rv.data) self.assertIn("required", rv.data)
rv = self.client.post('/user/add', data = { 'user': 'name', 'passwd': 'passwd' }) rv = self.client.post("/user/add", data={"user": "name", "passwd": "passwd"})
self.assertIn('passwords don', rv.data) self.assertIn("passwords don", rv.data)
rv = self.client.post('/user/add', data = { 'user': 'alice', 'passwd': 'passwd', 'passwd_confirm': 'passwd' }) rv = self.client.post(
"/user/add",
data={"user": "alice", "passwd": "passwd", "passwd_confirm": "passwd"},
)
self.assertIn(escape("User 'alice' exists"), rv.data) self.assertIn(escape("User 'alice' exists"), rv.data)
with db_session: with db_session:
self.assertEqual(User.select().count(), 2) 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) rv = self.client.post(
self.assertIn('added', rv.data) "/user/add",
data={
"user": "user",
"passwd": "passwd",
"passwd_confirm": "passwd",
"admin": 1,
},
follow_redirects=True,
)
self.assertIn("added", rv.data)
with db_session: with db_session:
self.assertEqual(User.select().count(), 3) self.assertEqual(User.select().count(), 3)
self._logout() self._logout()
rv = self._login('user', 'passwd') rv = self._login("user", "passwd")
self.assertIn('Logged in', rv.data) self.assertIn("Logged in", rv.data)
def test_delete(self): def test_delete(self):
path = '/user/del/{}'.format(self.users['bob']) path = "/user/del/{}".format(self.users["bob"])
self._login('bob', 'B0b') self._login("bob", "B0b")
rv = self.client.get(path, follow_redirects = True) rv = self.client.get(path, follow_redirects=True)
self.assertIn('There\'s nothing much to see', rv.data) self.assertIn("There's nothing much to see", rv.data)
with db_session: with db_session:
self.assertEqual(User.select().count(), 2) self.assertEqual(User.select().count(), 2)
self._logout() self._logout()
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/user/del/string', follow_redirects = True) rv = self.client.get("/user/del/string", follow_redirects=True)
self.assertIn('badly formed', rv.data) self.assertIn("badly formed", rv.data)
rv = self.client.get('/user/del/' + str(uuid.uuid4()), follow_redirects = True) rv = self.client.get("/user/del/" + str(uuid.uuid4()), follow_redirects=True)
self.assertIn('No such user', rv.data) self.assertIn("No such user", rv.data)
rv = self.client.get(path, follow_redirects = True) rv = self.client.get(path, follow_redirects=True)
self.assertIn('Deleted', rv.data) self.assertIn("Deleted", rv.data)
with db_session: with db_session:
self.assertEqual(User.select().count(), 1) self.assertEqual(User.select().count(), 1)
self._logout() self._logout()
rv = self._login('bob', 'B0b') rv = self._login("bob", "B0b")
self.assertIn('Wrong username or password', rv.data) self.assertIn("Wrong username or password", rv.data)
def test_lastfm_link(self): def test_lastfm_link(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/user/me/lastfm/link', follow_redirects = True) rv = self.client.get("/user/me/lastfm/link", follow_redirects=True)
self.assertIn('Missing LastFM auth token', rv.data) self.assertIn("Missing LastFM auth token", rv.data)
rv = self.client.get('/user/me/lastfm/link', query_string = { 'token': 'abcdef' }, follow_redirects = True) rv = self.client.get(
self.assertIn('No API key set', rv.data) "/user/me/lastfm/link",
query_string={"token": "abcdef"},
follow_redirects=True,
)
self.assertIn("No API key set", rv.data)
def test_lastfm_unlink(self): def test_lastfm_unlink(self):
self._login('alice', 'Alic3') self._login("alice", "Alic3")
rv = self.client.get('/user/me/lastfm/unlink', follow_redirects = True) rv = self.client.get("/user/me/lastfm/unlink", follow_redirects=True)
self.assertIn('Unlinked', rv.data) self.assertIn("Unlinked", rv.data)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -19,37 +19,39 @@ from supysonic.db import Folder
from supysonic.managers.folder import FolderManager from supysonic.managers.folder import FolderManager
from supysonic.scanner import Scanner from supysonic.scanner import Scanner
class Issue101TestCase(unittest.TestCase): class Issue101TestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.__dir = tempfile.mkdtemp() self.__dir = tempfile.mkdtemp()
init_database('sqlite:') init_database("sqlite:")
with db_session: with db_session:
FolderManager.add('folder', self.__dir) FolderManager.add("folder", self.__dir)
def tearDown(self): def tearDown(self):
release_database() release_database()
shutil.rmtree(self.__dir) shutil.rmtree(self.__dir)
def test_issue(self): def test_issue(self):
firstsubdir = tempfile.mkdtemp(dir = self.__dir) firstsubdir = tempfile.mkdtemp(dir=self.__dir)
subdir = firstsubdir subdir = firstsubdir
for _ in range(4): for _ in range(4):
subdir = tempfile.mkdtemp(dir = subdir) subdir = tempfile.mkdtemp(dir=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")
)
with db_session: with db_session:
scanner = Scanner() scanner = Scanner()
scanner.queue_folder('folder') scanner.queue_folder("folder")
scanner.run() scanner.run()
shutil.rmtree(firstsubdir) shutil.rmtree(firstsubdir)
with db_session: with db_session:
scanner = Scanner() scanner = Scanner()
scanner.queue_folder('folder') scanner.queue_folder("folder")
scanner.run() scanner.run()
if __name__ == '__main__': if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -18,35 +18,36 @@ from supysonic.scanner import Scanner
from .testbase import TestBase from .testbase import TestBase
class Issue129TestCase(TestBase): class Issue129TestCase(TestBase):
def setUp(self): def setUp(self):
super(Issue129TestCase, self).setUp() super(Issue129TestCase, self).setUp()
with db_session: 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 = Scanner()
scanner.queue_folder('folder') scanner.queue_folder("folder")
scanner.run() scanner.run()
self.trackid = Track.select().first().id 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): def test_last_play(self):
with db_session: with db_session:
User[self.userid].last_play = Track[self.trackid] User[self.userid].last_play = Track[self.trackid]
with db_session: with db_session:
FolderManager.delete_by_name('folder') FolderManager.delete_by_name("folder")
def test_starred(self): def test_starred(self):
with db_session: with db_session:
StarredTrack(user = self.userid, starred = self.trackid) StarredTrack(user=self.userid, starred=self.trackid)
FolderManager.delete_by_name('folder') FolderManager.delete_by_name("folder")
def test_rating(self): def test_rating(self):
with db_session: with db_session:
RatingTrack(user = self.userid, rated = self.trackid, rating = 5) RatingTrack(user=self.userid, rated=self.trackid, rating=5)
FolderManager.delete_by_name('folder') FolderManager.delete_by_name("folder")
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -18,13 +18,14 @@ from supysonic.db import Folder, Track
from supysonic.managers.folder import FolderManager from supysonic.managers.folder import FolderManager
from supysonic.scanner import Scanner from supysonic.scanner import Scanner
class Issue133TestCase(unittest.TestCase): class Issue133TestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.__dir = tempfile.mkdtemp() self.__dir = tempfile.mkdtemp()
shutil.copy('tests/assets/issue133.flac', self.__dir) shutil.copy("tests/assets/issue133.flac", self.__dir)
init_database('sqlite:') init_database("sqlite:")
with db_session: with db_session:
FolderManager.add('folder', self.__dir) FolderManager.add("folder", self.__dir)
def tearDown(self): def tearDown(self):
release_database() release_database()
@ -33,13 +34,13 @@ class Issue133TestCase(unittest.TestCase):
@db_session @db_session
def test_issue133(self): def test_issue133(self):
scanner = Scanner() scanner = Scanner()
scanner.queue_folder('folder') scanner.queue_folder("folder")
scanner.run() scanner.run()
del scanner del scanner
track = Track.select().first() track = Track.select().first()
self.assertNotIn('\x00', track.title) self.assertNotIn("\x00", track.title)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -18,12 +18,13 @@ from supysonic.db import Folder, Track
from supysonic.managers.folder import FolderManager from supysonic.managers.folder import FolderManager
from supysonic.scanner import Scanner from supysonic.scanner import Scanner
class Issue139TestCase(unittest.TestCase): class Issue139TestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.__dir = tempfile.mkdtemp() self.__dir = tempfile.mkdtemp()
init_database('sqlite:') init_database("sqlite:")
with db_session: with db_session:
FolderManager.add('folder', self.__dir) FolderManager.add("folder", self.__dir)
def tearDown(self): def tearDown(self):
release_database() release_database()
@ -32,17 +33,18 @@ class Issue139TestCase(unittest.TestCase):
@db_session @db_session
def do_scan(self): def do_scan(self):
scanner = Scanner() scanner = Scanner()
scanner.queue_folder('folder') scanner.queue_folder("folder")
scanner.run() scanner.run()
del scanner del scanner
def test_null_genre(self): def test_null_genre(self):
shutil.copy('tests/assets/issue139.mp3', self.__dir) shutil.copy("tests/assets/issue139.mp3", self.__dir)
self.do_scan() self.do_scan()
def test_float_bitrate(self): def test_float_bitrate(self):
shutil.copy('tests/assets/issue139.aac', self.__dir) shutil.copy("tests/assets/issue139.aac", self.__dir)
self.do_scan() self.do_scan()
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -19,28 +19,30 @@ from supysonic.db import Folder
from supysonic.managers.folder import FolderManager from supysonic.managers.folder import FolderManager
from supysonic.scanner import Scanner from supysonic.scanner import Scanner
class Issue148TestCase(unittest.TestCase): class Issue148TestCase(unittest.TestCase):
def setUp(self): def setUp(self):
self.__dir = tempfile.mkdtemp() self.__dir = tempfile.mkdtemp()
init_database('sqlite:') init_database("sqlite:")
with db_session: with db_session:
FolderManager.add('folder', self.__dir) FolderManager.add("folder", self.__dir)
def tearDown(self): def tearDown(self):
release_database() release_database()
shutil.rmtree(self.__dir) shutil.rmtree(self.__dir)
def test_issue(self): def test_issue(self):
subdir = os.path.join(self.__dir, ' ') subdir = os.path.join(self.__dir, " ")
os.makedirs(subdir) 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 = Scanner()
scanner.queue_folder('folder') scanner.queue_folder("folder")
scanner.run() scanner.run()
del scanner del scanner
if __name__ == '__main__': if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -13,6 +13,7 @@ import unittest
from .test_manager_folder import FolderManagerTestCase from .test_manager_folder import FolderManagerTestCase
from .test_manager_user import UserManagerTestCase from .test_manager_user import UserManagerTestCase
def suite(): def suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
@ -20,4 +21,3 @@ def suite():
suite.addTest(unittest.makeSuite(UserManagerTestCase)) suite.addTest(unittest.makeSuite(UserManagerTestCase))
return suite return suite

View File

@ -20,10 +20,11 @@ import uuid
from pony.orm import db_session, ObjectNotFound from pony.orm import db_session, ObjectNotFound
class FolderManagerTestCase(unittest.TestCase): class FolderManagerTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
# Create an empty sqlite database in memory # Create an empty sqlite database in memory
db.init_database('sqlite:') db.init_database("sqlite:")
# Create some temporary directories # Create some temporary directories
self.media_dir = tempfile.mkdtemp() self.media_dir = tempfile.mkdtemp()
@ -36,31 +37,29 @@ class FolderManagerTestCase(unittest.TestCase):
def create_folders(self): def create_folders(self):
# Add test folders # Add test folders
self.assertIsNotNone(FolderManager.add('media', self.media_dir)) self.assertIsNotNone(FolderManager.add("media", self.media_dir))
self.assertIsNotNone(FolderManager.add('music', self.music_dir)) self.assertIsNotNone(FolderManager.add("music", self.music_dir))
folder = db.Folder( folder = db.Folder(
root = False, root=False, name="non-root", path=os.path.join(self.music_dir, "subfolder")
name = 'non-root',
path = os.path.join(self.music_dir, 'subfolder')
) )
artist = db.Artist(name = 'Artist') artist = db.Artist(name="Artist")
album = db.Album(name = 'Album', artist = artist) album = db.Album(name="Album", artist=artist)
root = db.Folder.get(name = 'media') root = db.Folder.get(name="media")
track = db.Track( track = db.Track(
title = 'Track', title="Track",
artist = artist, artist=artist,
album = album, album=album,
disc = 1, disc=1,
number = 1, number=1,
path = os.path.join(self.media_dir, 'somefile'), path=os.path.join(self.media_dir, "somefile"),
folder = root, folder=root,
root_folder = root, root_folder=root,
duration = 2, duration=2,
bitrate = 320, bitrate=320,
last_modification = 0 last_modification=0,
) )
@db_session @db_session
@ -68,13 +67,13 @@ class FolderManagerTestCase(unittest.TestCase):
self.create_folders() self.create_folders()
# Get existing folders # Get existing folders
for name in ['media', 'music']: for name in ["media", "music"]:
folder = db.Folder.get(name = name, root = True) folder = db.Folder.get(name=name, root=True)
self.assertEqual(FolderManager.get(folder.id), folder) self.assertEqual(FolderManager.get(folder.id), folder)
# Get with invalid UUID # Get with invalid UUID
self.assertRaises(ValueError, FolderManager.get, 'invalid-uuid') self.assertRaises(ValueError, FolderManager.get, "invalid-uuid")
self.assertRaises(ValueError, FolderManager.get, 0xdeadbeef) self.assertRaises(ValueError, FolderManager.get, 0xDEADBEEF)
# Non-existent folder # Non-existent folder
self.assertRaises(ObjectNotFound, FolderManager.get, uuid.uuid4()) self.assertRaises(ObjectNotFound, FolderManager.get, uuid.uuid4())
@ -85,27 +84,29 @@ class FolderManagerTestCase(unittest.TestCase):
self.assertEqual(db.Folder.select().count(), 3) self.assertEqual(db.Folder.select().count(), 3)
# Create duplicate # Create duplicate
self.assertRaises(ValueError, FolderManager.add, 'media', self.media_dir) self.assertRaises(ValueError, FolderManager.add, "media", self.media_dir)
self.assertEqual(db.Folder.select(lambda f: f.name == 'media').count(), 1) self.assertEqual(db.Folder.select(lambda f: f.name == "media").count(), 1)
# Duplicate path # Duplicate path
self.assertRaises(ValueError, FolderManager.add, 'new-folder', self.media_dir) 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.assertEqual(
db.Folder.select(lambda f: f.path == self.media_dir).count(), 1
)
# Invalid path # Invalid path
path = os.path.abspath('/this/not/is/valid') path = os.path.abspath("/this/not/is/valid")
self.assertRaises(ValueError, FolderManager.add, 'invalid-path', path) self.assertRaises(ValueError, FolderManager.add, "invalid-path", path)
self.assertFalse(db.Folder.exists(path = path)) self.assertFalse(db.Folder.exists(path=path))
# Subfolder of already added 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) os.mkdir(path)
self.assertRaises(ValueError, FolderManager.add, 'subfolder', path) self.assertRaises(ValueError, FolderManager.add, "subfolder", path)
self.assertEqual(db.Folder.select().count(), 3) self.assertEqual(db.Folder.select().count(), 3)
# Parent folder of an already added path # Parent folder of an already added path
path = os.path.join(self.media_dir, '..') path = os.path.join(self.media_dir, "..")
self.assertRaises(ValueError, FolderManager.add, 'parent', path) self.assertRaises(ValueError, FolderManager.add, "parent", path)
self.assertEqual(db.Folder.select().count(), 3) self.assertEqual(db.Folder.select().count(), 3)
def test_delete_folder(self): def test_delete_folder(self):
@ -114,7 +115,7 @@ class FolderManagerTestCase(unittest.TestCase):
with db_session: with db_session:
# Delete invalid UUID # Delete invalid UUID
self.assertRaises(ValueError, FolderManager.delete, 'invalid-uuid') self.assertRaises(ValueError, FolderManager.delete, "invalid-uuid")
self.assertEqual(db.Folder.select().count(), 3) self.assertEqual(db.Folder.select().count(), 3)
# Delete non-existent folder # Delete non-existent folder
@ -122,14 +123,14 @@ class FolderManagerTestCase(unittest.TestCase):
self.assertEqual(db.Folder.select().count(), 3) self.assertEqual(db.Folder.select().count(), 3)
# Delete non-root folder # 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.assertRaises(ObjectNotFound, FolderManager.delete, folder.id)
self.assertEqual(db.Folder.select().count(), 3) self.assertEqual(db.Folder.select().count(), 3)
with db_session: with db_session:
# Delete existing folders # Delete existing folders
for name in ['media', 'music']: for name in ["media", "music"]:
folder = db.Folder.get(name = name, root = True) folder = db.Folder.get(name=name, root=True)
FolderManager.delete(folder.id) FolderManager.delete(folder.id)
self.assertRaises(ObjectNotFound, db.Folder.__getitem__, folder.id) self.assertRaises(ObjectNotFound, db.Folder.__getitem__, folder.id)
@ -142,15 +143,15 @@ class FolderManagerTestCase(unittest.TestCase):
with db_session: with db_session:
# Delete non-existent folder # 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) self.assertEqual(db.Folder.select().count(), 3)
with db_session: with db_session:
# Delete existing folders # Delete existing folders
for name in ['media', 'music']: for name in ["media", "music"]:
FolderManager.delete_by_name(name) 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() unittest.main()

View File

@ -20,10 +20,11 @@ import uuid
from pony.orm import db_session, commit from pony.orm import db_session, commit
from pony.orm import ObjectNotFound from pony.orm import ObjectNotFound
class UserManagerTestCase(unittest.TestCase): class UserManagerTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
# Create an empty sqlite database in memory # Create an empty sqlite database in memory
db.init_database('sqlite:') db.init_database("sqlite:")
def tearDown(self): def tearDown(self):
db.release_database() db.release_database()
@ -31,51 +32,62 @@ class UserManagerTestCase(unittest.TestCase):
@db_session @db_session
def create_data(self): def create_data(self):
# Create some users # Create some users
self.assertIsInstance(UserManager.add('alice', 'ALICE', 'test@example.com', True), db.User) self.assertIsInstance(
self.assertIsInstance(UserManager.add('bob', 'BOB', 'bob@example.com', False), db.User) UserManager.add("alice", "ALICE", "test@example.com", True), db.User
self.assertIsInstance(UserManager.add('charlie', 'CHARLIE', 'charlie@example.com', False), 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) folder = db.Folder(name="Root", path="tests/assets", root=True)
artist = db.Artist(name = 'Artist') artist = db.Artist(name="Artist")
album = db.Album(name = 'Album', artist = artist) album = db.Album(name="Album", artist=artist)
track = db.Track( track = db.Track(
title = 'Track', title="Track",
disc = 1, disc=1,
number = 1, number=1,
duration = 1, duration=1,
artist = artist, artist=artist,
album = album, album=album,
path = 'tests/assets/empty', path="tests/assets/empty",
folder = folder, folder=folder,
root_folder = folder, root_folder=folder,
bitrate = 320, bitrate=320,
last_modification = 0 last_modification=0,
) )
playlist = db.Playlist( playlist = db.Playlist(name="Playlist", user=db.User.get(name="alice"))
name = 'Playlist',
user = db.User.get(name = 'alice')
)
playlist.add(track) playlist.add(track)
def test_encrypt_password(self): def test_encrypt_password(self):
func = UserManager._UserManager__encrypt_password func = UserManager._UserManager__encrypt_password
self.assertEqual(func('password','salt'), ('59b3e8d637cf97edbe2384cf59cb7453dfe30789', 'salt')) self.assertEqual(
self.assertEqual(func('pass-word','pepper'), ('d68c95a91ed7773aa57c7c044d2309a5bf1da2e7', 'pepper')) func("password", "salt"),
self.assertEqual(func(u'éèàïô', 'ABC+'), ('b639ba5217b89c906019d89d5816b407d8730898', 'ABC+')) ("59b3e8d637cf97edbe2384cf59cb7453dfe30789", "salt"),
)
self.assertEqual(
func("pass-word", "pepper"),
("d68c95a91ed7773aa57c7c044d2309a5bf1da2e7", "pepper"),
)
self.assertEqual(
func(u"éèàïô", "ABC+"), ("b639ba5217b89c906019d89d5816b407d8730898", "ABC+")
)
@db_session @db_session
def test_get_user(self): def test_get_user(self):
self.create_data() self.create_data()
# Get existing users # Get existing users
for name in ['alice', 'bob', 'charlie']: for name in ["alice", "bob", "charlie"]:
user = db.User.get(name = name) user = db.User.get(name=name)
self.assertEqual(UserManager.get(user.id), user) self.assertEqual(UserManager.get(user.id), user)
# Get with invalid UUID # Get with invalid UUID
self.assertRaises(ValueError, UserManager.get, 'invalid-uuid') self.assertRaises(ValueError, UserManager.get, "invalid-uuid")
self.assertRaises(ValueError, UserManager.get, 0xfee1bad) self.assertRaises(ValueError, UserManager.get, 0xFEE1BAD)
# Non-existent user # Non-existent user
self.assertRaises(ObjectNotFound, UserManager.get, uuid.uuid4()) self.assertRaises(ObjectNotFound, UserManager.get, uuid.uuid4())
@ -86,15 +98,17 @@ class UserManagerTestCase(unittest.TestCase):
self.assertEqual(db.User.select().count(), 3) self.assertEqual(db.User.select().count(), 3)
# Create duplicate # 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 @db_session
def test_delete_user(self): def test_delete_user(self):
self.create_data() self.create_data()
# Delete invalid UUID # Delete invalid UUID
self.assertRaises(ValueError, UserManager.delete, 'invalid-uuid') self.assertRaises(ValueError, UserManager.delete, "invalid-uuid")
self.assertRaises(ValueError, UserManager.delete, 0xfee1b4d) self.assertRaises(ValueError, UserManager.delete, 0xFEE1B4D)
self.assertEqual(db.User.select().count(), 3) self.assertEqual(db.User.select().count(), 3)
# Delete non-existent user # Delete non-existent user
@ -102,8 +116,8 @@ class UserManagerTestCase(unittest.TestCase):
self.assertEqual(db.User.select().count(), 3) self.assertEqual(db.User.select().count(), 3)
# Delete existing users # Delete existing users
for name in ['alice', 'bob', 'charlie']: for name in ["alice", "bob", "charlie"]:
user = db.User.get(name = name) user = db.User.get(name=name)
UserManager.delete(user.id) UserManager.delete(user.id)
self.assertRaises(ObjectNotFound, db.User.__getitem__, user.id) self.assertRaises(ObjectNotFound, db.User.__getitem__, user.id)
commit() commit()
@ -114,68 +128,84 @@ class UserManagerTestCase(unittest.TestCase):
self.create_data() self.create_data()
# Delete existing users # Delete existing users
for name in ['alice', 'bob', 'charlie']: for name in ["alice", "bob", "charlie"]:
UserManager.delete_by_name(name) UserManager.delete_by_name(name)
self.assertFalse(db.User.exists(name = name)) self.assertFalse(db.User.exists(name=name))
# Delete non-existent user # Delete non-existent user
self.assertRaises(ObjectNotFound, UserManager.delete_by_name, 'null') self.assertRaises(ObjectNotFound, UserManager.delete_by_name, "null")
@db_session @db_session
def test_try_auth(self): def test_try_auth(self):
self.create_data() self.create_data()
# Test authentication # Test authentication
for name in ['alice', 'bob', 'charlie']: for name in ["alice", "bob", "charlie"]:
user = db.User.get(name = name) user = db.User.get(name=name)
authed = UserManager.try_auth(name, name.upper()) authed = UserManager.try_auth(name, name.upper())
self.assertEqual(authed, user) self.assertEqual(authed, user)
# Wrong password # Wrong password
self.assertIsNone(UserManager.try_auth('alice', 'bad')) self.assertIsNone(UserManager.try_auth("alice", "bad"))
self.assertIsNone(UserManager.try_auth('alice', 'alice')) self.assertIsNone(UserManager.try_auth("alice", "alice"))
# Non-existent user # Non-existent user
self.assertIsNone(UserManager.try_auth('null', 'null')) self.assertIsNone(UserManager.try_auth("null", "null"))
@db_session @db_session
def test_change_password(self): def test_change_password(self):
self.create_data() self.create_data()
# With existing users # With existing users
for name in ['alice', 'bob', 'charlie']: for name in ["alice", "bob", "charlie"]:
user = db.User.get(name = name) user = db.User.get(name=name)
# Good password # Good password
UserManager.change_password(user.id, name.upper(), 'newpass') UserManager.change_password(user.id, name.upper(), "newpass")
self.assertEqual(UserManager.try_auth(name, 'newpass'), user) self.assertEqual(UserManager.try_auth(name, "newpass"), user)
# Old password # Old password
self.assertEqual(UserManager.try_auth(name, name.upper()), None) self.assertEqual(UserManager.try_auth(name, name.upper()), None)
# Wrong password # 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 # Ensure we still got the same number of users
self.assertEqual(db.User.select().count(), 3) self.assertEqual(db.User.select().count(), 3)
# With invalid UUID # 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 # 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 @db_session
def test_change_password2(self): def test_change_password2(self):
self.create_data() self.create_data()
# With existing users # With existing users
for name in ['alice', 'bob', 'charlie']: for name in ["alice", "bob", "charlie"]:
UserManager.change_password2(name, 'newpass') UserManager.change_password2(name, "newpass")
user = db.User.get(name = name) user = db.User.get(name=name)
self.assertEqual(UserManager.try_auth(name, 'newpass'), user) self.assertEqual(UserManager.try_auth(name, "newpass"), user)
self.assertEqual(UserManager.try_auth(name, name.upper()), None) self.assertEqual(UserManager.try_auth(name, name.upper()), None)
# Non-existent user # 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() unittest.main()

View File

@ -20,18 +20,16 @@ from supysonic.config import DefaultConfig
from supysonic.managers.user import UserManager from supysonic.managers.user import UserManager
from supysonic.web import create_application from supysonic.web import create_application
class TestConfig(DefaultConfig): class TestConfig(DefaultConfig):
TESTING = True TESTING = True
LOGGER_HANDLER_POLICY = 'never' LOGGER_HANDLER_POLICY = "never"
MIMETYPES = { MIMETYPES = {"mp3": "audio/mpeg", "weirdextension": "application/octet-stream"}
'mp3': 'audio/mpeg',
'weirdextension': 'application/octet-stream'
}
TRANSCODING = { TRANSCODING = {
'transcoder_mp3_mp3': 'echo -n %srcpath %outrate', "transcoder_mp3_mp3": "echo -n %srcpath %outrate",
'decoder_mp3': 'echo -n Pushing out some mp3 data...', "decoder_mp3": "echo -n Pushing out some mp3 data...",
'encoder_cat': 'cat -', "encoder_cat": "cat -",
'encoder_md5': 'md5sum' "encoder_md5": "md5sum",
} }
def __init__(self, with_webui, with_api): def __init__(self, with_webui, with_api):
@ -39,7 +37,7 @@ class TestConfig(DefaultConfig):
for cls in reversed(inspect.getmro(self.__class__)): for cls in reversed(inspect.getmro(self.__class__)):
for attr, value in cls.__dict__.items(): for attr, value in cls.__dict__.items():
if attr.startswith('_') or attr != attr.upper(): if attr.startswith("_") or attr != attr.upper():
continue continue
if isinstance(value, dict): if isinstance(value, dict):
@ -47,15 +45,13 @@ class TestConfig(DefaultConfig):
else: else:
setattr(self, attr, value) setattr(self, attr, value)
self.WEBAPP.update({ self.WEBAPP.update({"mount_webui": with_webui, "mount_api": with_api})
'mount_webui': with_webui,
'mount_api': with_api
})
class MockResponse(object): class MockResponse(object):
def __init__(self, response): def __init__(self, response):
self.__status_code = response.status_code 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 self.__mimetype = response.mimetype
@property @property
@ -70,14 +66,17 @@ class MockResponse(object):
def mimetype(self): def mimetype(self):
return self.__mimetype return self.__mimetype
def patch_method(f): def patch_method(f):
original = f original = f
def patched(*args, **kwargs): def patched(*args, **kwargs):
rv = original(*args, **kwargs) rv = original(*args, **kwargs)
return MockResponse(rv) return MockResponse(rv)
return patched return patched
class TestBase(unittest.TestCase): class TestBase(unittest.TestCase):
__with_webui__ = False __with_webui__ = False
__with_api__ = False __with_api__ = False
@ -86,18 +85,18 @@ class TestBase(unittest.TestCase):
self.__dbfile = tempfile.mkstemp()[1] self.__dbfile = tempfile.mkstemp()[1]
self.__dir = tempfile.mkdtemp() self.__dir = tempfile.mkdtemp()
config = TestConfig(self.__with_webui__, self.__with_api__) config = TestConfig(self.__with_webui__, self.__with_api__)
config.BASE['database_uri'] = 'sqlite:///' + self.__dbfile config.BASE["database_uri"] = "sqlite:///" + self.__dbfile
config.WEBAPP['cache_dir'] = self.__dir config.WEBAPP["cache_dir"] = self.__dir
init_database(config.BASE['database_uri']) init_database(config.BASE["database_uri"])
release_database() release_database()
self.__app = create_application(config) self.__app = create_application(config)
self.client = self.__app.test_client() self.client = self.__app.test_client()
with db_session: with db_session:
UserManager.add('alice', 'Alic3', 'test@example.com', True) UserManager.add("alice", "Alic3", "test@example.com", True)
UserManager.add('bob', 'B0b', 'bob@example.com', False) UserManager.add("bob", "B0b", "bob@example.com", False)
def _patch_client(self): def _patch_client(self):
self.client.get = patch_method(self.client.get) self.client.get = patch_method(self.client.get)
@ -110,4 +109,3 @@ class TestBase(unittest.TestCase):
release_database() release_database()
shutil.rmtree(self.__dir) shutil.rmtree(self.__dir)
os.remove(self.__dbfile) os.remove(self.__dbfile)

View File

@ -9,6 +9,6 @@
import binascii 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")