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
reqs = [
'flask>=0.11',
'pony>=0.7.6',
'Pillow',
'requests>=1.0.0',
'mutagen>=1.33',
'scandir<2.0.0',
'watchdog>=0.8.0',
'zipstream'
"flask>=0.11",
"pony>=0.7.6",
"Pillow",
"requests>=1.0.0",
"mutagen>=1.33",
"scandir<2.0.0",
"watchdog>=0.8.0",
"zipstream",
]
setup(
name=project.NAME,
version=project.VERSION,
description=project.DESCRIPTION,
keywords=project.KEYWORDS,
long_description=project.LONG_DESCRIPTION,
author=project.AUTHOR_NAME,
author_email=project.AUTHOR_EMAIL,
url=project.URL,
license=project.LICENSE,
packages=find_packages(exclude=['tests*']),
install_requires = reqs,
entry_points={ 'console_scripts': [
'supysonic-cli=supysonic.cli:main',
'supysonic-daemon=supysonic.daemon:main'
] },
zip_safe=False,
include_package_data=True,
test_suite='tests.suite',
tests_require = [ 'lxml' ],
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Environment :: Web Environment',
'Framework :: Flask',
'Intended Audience :: End Users/Desktop',
'Intended Audience :: System Administrators',
'License :: OSI Approved :: GNU Affero General Public License v3',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Multimedia :: Sound/Audio'
name=project.NAME,
version=project.VERSION,
description=project.DESCRIPTION,
keywords=project.KEYWORDS,
long_description=project.LONG_DESCRIPTION,
author=project.AUTHOR_NAME,
author_email=project.AUTHOR_EMAIL,
url=project.URL,
license=project.LICENSE,
packages=find_packages(exclude=["tests*"]),
install_requires=reqs,
entry_points={
"console_scripts": [
"supysonic-cli=supysonic.cli:main",
"supysonic-daemon=supysonic.daemon:main",
]
)
},
zip_safe=False,
include_package_data=True,
test_suite="tests.suite",
tests_require=["lxml"],
classifiers=[
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Environment :: Web Environment",
"Framework :: Flask",
"Intended Audience :: End Users/Desktop",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Topic :: Multimedia :: Sound/Audio",
],
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,28 +15,32 @@ from werkzeug.exceptions import BadRequestKeyError
from . import api
from .exceptions import GenericError, MissingParameter, NotFound, ServerError
@api.errorhandler(ValueError)
def value_error(e):
rollback()
return GenericError("{0.__class__.__name__}: {0}".format(e))
@api.errorhandler(BadRequestKeyError)
def key_error(e):
rollback()
return MissingParameter()
@api.errorhandler(ObjectNotFound)
def not_found(e):
rollback()
return NotFound(e.entity.__name__)
@api.errorhandler(500)
def generic_error(e): # pragma: nocover
def generic_error(e): # pragma: nocover
rollback()
return ServerError("{0.__class__.__name__}: {0}".format(e))
#@api.errorhandler(404)
@api.route('/<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 werkzeug.exceptions import HTTPException
class SubsonicAPIException(HTTPException):
code = 400
api_code = None
message = None
def get_response(self, environ = None):
def get_response(self, environ=None):
rv = request.formatter.error(self.api_code, self.message)
rv.status_code = self.code
return rv
def __str__(self):
code = self.api_code if self.api_code is not None else '??'
return '{}: {}'.format(code, self.message)
code = self.api_code if self.api_code is not None else "??"
return "{}: {}".format(code, self.message)
class GenericError(SubsonicAPIException):
api_code = 0
@ -31,14 +33,17 @@ class GenericError(SubsonicAPIException):
super(GenericError, self).__init__(*args, **kwargs)
self.message = message
class ServerError(GenericError):
code = 500
class UnsupportedParameter(GenericError):
def __init__(self, parameter, *args, **kwargs):
message = "Unsupported parameter '{}'".format(parameter)
super(UnsupportedParameter, self).__init__(message, *args, **kwargs)
class MissingParameter(SubsonicAPIException):
api_code = 10
@ -46,31 +51,39 @@ class MissingParameter(SubsonicAPIException):
super(MissingParameter, self).__init__(*args, **kwargs)
self.message = "A required parameter is missing."
class ClientMustUpgrade(SubsonicAPIException):
api_code = 20
message = 'Incompatible Subsonic REST protocol version. Client must upgrade.'
message = "Incompatible Subsonic REST protocol version. Client must upgrade."
class ServerMustUpgrade(SubsonicAPIException):
code = 501
api_code = 30
message = 'Incompatible Subsonic REST protocol version. Server must upgrade.'
message = "Incompatible Subsonic REST protocol version. Server must upgrade."
class Unauthorized(SubsonicAPIException):
code = 401
api_code = 40
message = 'Wrong username or password.'
message = "Wrong username or password."
class Forbidden(SubsonicAPIException):
code = 403
api_code = 50
message = 'User is not authorized for the given operation.'
message = "User is not authorized for the given operation."
class TrialExpired(SubsonicAPIException):
code = 402
api_code = 60
message = ("The trial period for the Supysonic server is over."
message = (
"The trial period for the Supysonic server is over."
"But since it doesn't use any licensing you shouldn't be seeing this error ever."
"So something went wrong or you got scammed.")
"So something went wrong or you got scammed."
)
class NotFound(SubsonicAPIException):
code = 404
@ -78,7 +91,8 @@ class NotFound(SubsonicAPIException):
def __init__(self, entity, *args, **kwargs):
super(NotFound, self).__init__(*args, **kwargs)
self.message = '{} not found'.format(entity)
self.message = "{} not found".format(entity)
class AggregateException(SubsonicAPIException):
def __init__(self, exceptions, *args, **kwargs):
@ -88,7 +102,7 @@ class AggregateException(SubsonicAPIException):
for exc in exceptions:
if not isinstance(exc, SubsonicAPIException):
# Try to convert regular exceptions to SubsonicAPIExceptions
handler = current_app._find_error_handler(exc) # meh
handler = current_app._find_error_handler(exc) # meh
if handler:
exc = handler(exc)
assert isinstance(exc, SubsonicAPIException)
@ -96,14 +110,17 @@ class AggregateException(SubsonicAPIException):
exc = GenericError(str(exc))
self.exceptions.append(exc)
def get_response(self, environ = None):
def get_response(self, environ=None):
if len(self.exceptions) == 1:
return self.exceptions[0].get_response()
codes = set(exc.api_code for exc in self.exceptions)
errors = [ dict(code = exc.api_code, message = exc.message) for exc in self.exceptions ]
errors = [
dict(code=exc.api_code, message=exc.message) for exc in self.exceptions
]
rv = request.formatter('error', dict(code = list(codes)[0] if len(codes) == 1 else 0, error = errors))
rv = request.formatter(
"error", dict(code=list(codes)[0] if len(codes) == 1 else 0, error=errors)
)
rv.status_code = self.code
return rv

View File

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

View File

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

View File

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

View File

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

View File

@ -12,11 +12,12 @@ from flask import request
from ..py23 import dict
from . import api
@api.route('/ping.view', methods = [ 'GET', 'POST' ])
@api.route("/ping.view", methods=["GET", "POST"])
def ping():
return request.formatter.empty
@api.route('/getLicense.view', methods = [ 'GET', 'POST' ])
def license():
return request.formatter('license', dict(valid = True))
@api.route("/getLicense.view", methods=["GET", "POST"])
def license():
return request.formatter("license", dict(valid=True))

View File

@ -11,12 +11,20 @@ from . import api
from .exceptions import GenericError
methods = (
'getVideos', 'getAvatar', 'getShares', 'createShare', 'updateShare', 'deleteShare',
"getVideos",
"getAvatar",
"getShares",
"createShare",
"updateShare",
"deleteShare",
)
def unsupported():
return GenericError('Not supported by Supysonic'), 501
return GenericError("Not supported by Supysonic"), 501
for m in methods:
api.add_url_rule('/{}.view'.format(m), 'unsupported', unsupported, methods = [ 'GET', 'POST' ])
api.add_url_rule(
"/{}.view".format(m), "unsupported", unsupported, methods=["GET", "POST"]
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,4 +6,3 @@
# Copyright (C) 2013 Alban 'spl0k' Féron
#
# 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 ..py23 import strtype
class FolderManager:
@staticmethod
def get(uid):
@ -26,26 +27,26 @@ class FolderManager:
elif isinstance(uid, uuid.UUID):
pass
else:
raise ValueError('Invalid folder id')
raise ValueError("Invalid folder id")
return Folder[uid]
@staticmethod
def add(name, path):
if Folder.get(name = name, root = True) is not None:
if Folder.get(name=name, root=True) is not None:
raise ValueError("Folder '{}' exists".format(name))
path = os.path.abspath(os.path.expanduser(path))
if not os.path.isdir(path):
raise ValueError("The path doesn't exits or isn't a directory")
if Folder.get(path = path) is not None:
raise ValueError('This path is already registered')
if Folder.get(path=path) is not None:
raise ValueError("This path is already registered")
if any(path.startswith(p) for p in select(f.path for f in Folder if f.root)):
raise ValueError('This path is already registered')
raise ValueError("This path is already registered")
if Folder.exists(lambda f: f.path.startswith(path)):
raise ValueError('This path contains a folder that is already registered')
raise ValueError("This path contains a folder that is already registered")
folder = Folder(root = True, name = name, path = path)
folder = Folder(root=True, name=name, path=path)
try:
DaemonClient().add_watched_folder(path)
except DaemonUnavailableError:
@ -66,20 +67,21 @@ class FolderManager:
for user in User.select(lambda u: u.last_play.root_folder == folder):
user.last_play = None
RatingTrack.select(lambda r: r.rated.root_folder == folder).delete(bulk = True)
StarredTrack.select(lambda s: s.starred.root_folder == folder).delete(bulk = True)
RatingTrack.select(lambda r: r.rated.root_folder == folder).delete(bulk=True)
StarredTrack.select(lambda s: s.starred.root_folder == folder).delete(bulk=True)
Track.select(lambda t: t.root_folder == folder).delete(bulk = True)
Track.select(lambda t: t.root_folder == folder).delete(bulk=True)
Album.prune()
Artist.prune()
Folder.select(lambda f: not f.root and f.path.startswith(folder.path)).delete(bulk = True)
Folder.select(lambda f: not f.root and f.path.startswith(folder.path)).delete(
bulk=True
)
folder.delete()
@staticmethod
def delete_by_name(name):
folder = Folder.get(name = name, root = True)
folder = Folder.get(name=name, root=True)
if not folder:
raise ObjectNotFound(Folder)
FolderManager.delete(folder.id)

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@
# Converts ids from hex-encoded strings to binary data
import argparse
try:
import MySQLdb as provider
except ImportError:
@ -20,17 +21,18 @@ from uuid import UUID
from warnings import filterwarnings
parser = argparse.ArgumentParser()
parser.add_argument('username')
parser.add_argument('password')
parser.add_argument('database')
parser.add_argument('-H', '--host', default = 'localhost', help = 'default: localhost')
parser.add_argument("username")
parser.add_argument("password")
parser.add_argument("database")
parser.add_argument("-H", "--host", default="localhost", help="default: localhost")
args = parser.parse_args()
def process_table(connection, table, fields, nullable_fields = ()):
to_update = { field: set() for field in fields + nullable_fields }
def process_table(connection, table, fields, nullable_fields=()):
to_update = {field: set() for field in fields + nullable_fields}
c = connection.cursor()
c.execute('SELECT {1} FROM {0}'.format(table, ','.join(fields + nullable_fields)))
c.execute("SELECT {1} FROM {0}".format(table, ",".join(fields + nullable_fields)))
for row in c:
for field, value in zip(fields + nullable_fields, row):
if value is None or not isinstance(value, basestring):
@ -40,36 +42,40 @@ def process_table(connection, table, fields, nullable_fields = ()):
for field, values in to_update.iteritems():
if not values:
continue
sql = 'UPDATE {0} SET {1}=%s WHERE {1}=%s'.format(table, field)
sql = "UPDATE {0} SET {1}=%s WHERE {1}=%s".format(table, field)
c.executemany(sql, map(lambda v: (UUID(v).bytes, v), values))
for field in fields:
sql = 'ALTER TABLE {0} MODIFY {1} BINARY(16) NOT NULL'.format(table, field)
sql = "ALTER TABLE {0} MODIFY {1} BINARY(16) NOT NULL".format(table, field)
c.execute(sql)
for field in nullable_fields:
sql = 'ALTER TABLE {0} MODIFY {1} BINARY(16)'.format(table, field)
sql = "ALTER TABLE {0} MODIFY {1} BINARY(16)".format(table, field)
c.execute(sql)
connection.commit()
filterwarnings('ignore', category = provider.Warning)
conn = provider.connect(host = args.host, user = args.username, passwd = args.password, db = args.database)
conn.cursor().execute('SET FOREIGN_KEY_CHECKS = 0')
process_table(conn, 'folder', ('id',), ('parent_id',))
process_table(conn, 'artist', ('id',))
process_table(conn, 'album', ('id', 'artist_id'))
process_table(conn, 'track', ('id', 'album_id', 'artist_id', 'root_folder_id', 'folder_id'))
process_table(conn, 'user', ('id',), ('last_play_id',))
process_table(conn, 'client_prefs', ('user_id',))
process_table(conn, 'starred_folder', ('user_id', 'starred_id'))
process_table(conn, 'starred_artist', ('user_id', 'starred_id'))
process_table(conn, 'starred_album', ('user_id', 'starred_id'))
process_table(conn, 'starred_track', ('user_id', 'starred_id'))
process_table(conn, 'rating_folder', ('user_id', 'rated_id'))
process_table(conn, 'rating_track', ('user_id', 'rated_id'))
process_table(conn, 'chat_message', ('id', 'user_id'))
process_table(conn, 'playlist', ('id', 'user_id'))
filterwarnings("ignore", category=provider.Warning)
conn = provider.connect(
host=args.host, user=args.username, passwd=args.password, db=args.database
)
conn.cursor().execute("SET FOREIGN_KEY_CHECKS = 0")
conn.cursor().execute('SET FOREIGN_KEY_CHECKS = 1')
process_table(conn, "folder", ("id",), ("parent_id",))
process_table(conn, "artist", ("id",))
process_table(conn, "album", ("id", "artist_id"))
process_table(
conn, "track", ("id", "album_id", "artist_id", "root_folder_id", "folder_id")
)
process_table(conn, "user", ("id",), ("last_play_id",))
process_table(conn, "client_prefs", ("user_id",))
process_table(conn, "starred_folder", ("user_id", "starred_id"))
process_table(conn, "starred_artist", ("user_id", "starred_id"))
process_table(conn, "starred_album", ("user_id", "starred_id"))
process_table(conn, "starred_track", ("user_id", "starred_id"))
process_table(conn, "rating_folder", ("user_id", "rated_id"))
process_table(conn, "rating_track", ("user_id", "rated_id"))
process_table(conn, "chat_message", ("id", "user_id"))
process_table(conn, "playlist", ("id", "user_id"))
conn.cursor().execute("SET FOREIGN_KEY_CHECKS = 1")
conn.close()

View File

@ -9,26 +9,36 @@ except:
pass
parser = argparse.ArgumentParser()
parser.add_argument('username')
parser.add_argument('password')
parser.add_argument('database')
parser.add_argument('-H', '--host', default = 'localhost', help = 'default: localhost')
parser.add_argument("username")
parser.add_argument("password")
parser.add_argument("database")
parser.add_argument("-H", "--host", default="localhost", help="default: localhost")
args = parser.parse_args()
def process_table(connection, table):
c = connection.cursor()
c.execute(r"ALTER TABLE {0} ADD COLUMN path_hash BYTEA NOT NULL DEFAULT E'\\0000'".format(table))
c.execute(
r"ALTER TABLE {0} ADD COLUMN path_hash BYTEA NOT NULL DEFAULT E'\\0000'".format(
table
)
)
hashes = dict()
c.execute('SELECT path FROM {0}'.format(table))
c.execute("SELECT path FROM {0}".format(table))
for row in c.fetchall():
hashes[row[0]] = hashlib.sha1(row[0].encode('utf-8')).digest()
c.executemany('UPDATE {0} SET path_hash=%s WHERE path=%s'.format(table), [ (bytes(h), p) for p, h in hashes.items() ])
hashes[row[0]] = hashlib.sha1(row[0].encode("utf-8")).digest()
c.executemany(
"UPDATE {0} SET path_hash=%s WHERE path=%s".format(table),
[(bytes(h), p) for p, h in hashes.items()],
)
c.execute('CREATE UNIQUE INDEX index_{0}_path ON {0}(path_hash)'.format(table))
c.execute("CREATE UNIQUE INDEX index_{0}_path ON {0}(path_hash)".format(table))
with psycopg2.connect(host = args.host, user = args.username, password = args.password, dbname = args.database) as conn:
process_table(conn, 'folder')
process_table(conn, 'track')
with psycopg2.connect(
host=args.host, user=args.username, password=args.password, dbname=args.database
) as conn:
process_table(conn, "folder")
process_table(conn, "track")

View File

@ -16,40 +16,43 @@ import sqlite3
from uuid import UUID
parser = argparse.ArgumentParser()
parser.add_argument('dbfile', help = 'Path to the SQLite database file')
parser.add_argument("dbfile", help="Path to the SQLite database file")
args = parser.parse_args()
def process_table(connection, table, fields):
to_update = { field: set() for field in fields }
to_update = {field: set() for field in fields}
c = connection.cursor()
for row in c.execute('SELECT {1} FROM {0}'.format(table, ','.join(fields))):
for row in c.execute("SELECT {1} FROM {0}".format(table, ",".join(fields))):
for field, value in zip(fields, row):
if value is None or not isinstance(value, basestring):
continue
to_update[field].add(value)
for field, values in to_update.iteritems():
sql = 'UPDATE {0} SET {1}=? WHERE {1}=?'.format(table, field)
sql = "UPDATE {0} SET {1}=? WHERE {1}=?".format(table, field)
c.executemany(sql, map(lambda v: (buffer(UUID(v).bytes), v), values))
connection.commit()
with sqlite3.connect(args.dbfile) as conn:
conn.cursor().execute('PRAGMA foreign_keys = OFF')
process_table(conn, 'folder', ('id', 'parent_id'))
process_table(conn, 'artist', ('id',))
process_table(conn, 'album', ('id', 'artist_id'))
process_table(conn, 'track', ('id', 'album_id', 'artist_id', 'root_folder_id', 'folder_id'))
process_table(conn, 'user', ('id', 'last_play_id'))
process_table(conn, 'client_prefs', ('user_id',))
process_table(conn, 'starred_folder', ('user_id', 'starred_id'))
process_table(conn, 'starred_artist', ('user_id', 'starred_id'))
process_table(conn, 'starred_album', ('user_id', 'starred_id'))
process_table(conn, 'starred_track', ('user_id', 'starred_id'))
process_table(conn, 'rating_folder', ('user_id', 'rated_id'))
process_table(conn, 'rating_track', ('user_id', 'rated_id'))
process_table(conn, 'chat_message', ('id', 'user_id'))
process_table(conn, 'playlist', ('id', 'user_id'))
conn.cursor().execute("PRAGMA foreign_keys = OFF")
process_table(conn, "folder", ("id", "parent_id"))
process_table(conn, "artist", ("id",))
process_table(conn, "album", ("id", "artist_id"))
process_table(
conn, "track", ("id", "album_id", "artist_id", "root_folder_id", "folder_id")
)
process_table(conn, "user", ("id", "last_play_id"))
process_table(conn, "client_prefs", ("user_id",))
process_table(conn, "starred_folder", ("user_id", "starred_id"))
process_table(conn, "starred_artist", ("user_id", "starred_id"))
process_table(conn, "starred_album", ("user_id", "starred_id"))
process_table(conn, "starred_track", ("user_id", "starred_id"))
process_table(conn, "rating_folder", ("user_id", "rated_id"))
process_table(conn, "rating_track", ("user_id", "rated_id"))
process_table(conn, "chat_message", ("id", "user_id"))
process_table(conn, "playlist", ("id", "user_id"))

View File

@ -9,23 +9,29 @@ except:
pass
parser = argparse.ArgumentParser()
parser.add_argument('dbfile', help = 'Path to the SQLite database file')
parser.add_argument("dbfile", help="Path to the SQLite database file")
args = parser.parse_args()
def process_table(connection, table):
c = connection.cursor()
c.execute('ALTER TABLE {0} ADD COLUMN path_hash BLOB NOT NULL DEFAULT ROWID'.format(table))
c.execute(
"ALTER TABLE {0} ADD COLUMN path_hash BLOB NOT NULL DEFAULT ROWID".format(table)
)
hashes = dict()
for row in c.execute('SELECT path FROM {0}'.format(table)):
hashes[row[0]] = hashlib.sha1(row[0].encode('utf-8')).digest()
c.executemany('UPDATE {0} SET path_hash=? WHERE path=?'.format(table), [ (bytes(h), p) for p, h in hashes.items() ])
for row in c.execute("SELECT path FROM {0}".format(table)):
hashes[row[0]] = hashlib.sha1(row[0].encode("utf-8")).digest()
c.executemany(
"UPDATE {0} SET path_hash=? WHERE path=?".format(table),
[(bytes(h), p) for p, h in hashes.items()],
)
c.execute("CREATE UNIQUE INDEX index_{0}_path ON {0}(path_hash)".format(table))
c.execute('CREATE UNIQUE INDEX index_{0}_path ON {0}(path_hash)'.format(table))
with sqlite3.connect(args.dbfile) as conn:
process_table(conn, 'folder')
process_table(conn, 'track')
conn.cursor().execute('VACUUM')
process_table(conn, "folder")
process_table(conn, "track")
conn.cursor().execute("VACUUM")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,10 +15,11 @@ from supysonic.py23 import strtype
from ..testbase import TestBase
path_replace_regexp = re.compile(r'/(\w+)')
path_replace_regexp = re.compile(r"/(\w+)")
NS = "http://subsonic.org/restapi"
NSMAP = {"sub": NS}
NS = 'http://subsonic.org/restapi'
NSMAP = { 'sub': NS }
class ApiTestBase(TestBase):
__with_api__ = True
@ -26,7 +27,7 @@ class ApiTestBase(TestBase):
def setUp(self):
super(ApiTestBase, self).setUp()
xsd = etree.parse('tests/assets/subsonic-rest-api-1.9.0.xsd')
xsd = etree.parse("tests/assets/subsonic-rest-api-1.9.0.xsd")
self.schema = etree.XMLSchema(xsd)
def _find(self, xml, path):
@ -34,7 +35,7 @@ class ApiTestBase(TestBase):
Helper method that insert the namespace in ElementPath 'path'
"""
path = path_replace_regexp.sub(r'/{{{}}}\1'.format(NS), path)
path = path_replace_regexp.sub(r"/{{{}}}\1".format(NS), path)
return xml.find(path)
def _xpath(self, elem, path):
@ -42,10 +43,12 @@ class ApiTestBase(TestBase):
Helper method that insert a prefix and map the namespace in XPath 'path'
"""
path = path_replace_regexp.sub(r'/sub:\1', path)
return elem.xpath(path, namespaces = NSMAP)
path = path_replace_regexp.sub(r"/sub:\1", path)
return elem.xpath(path, namespaces=NSMAP)
def _make_request(self, endpoint, args = {}, tag = None, error = None, skip_post = False, skip_xsd = False):
def _make_request(
self, endpoint, args={}, tag=None, error=None, skip_post=False, skip_xsd=False
):
"""
Makes both a GET and POST requests against the API, assert both get the same response.
If the user isn't provided with the 'u' and 'p' in 'args', the default 'alice' is used.
@ -68,31 +71,30 @@ class ApiTestBase(TestBase):
if tag and not isinstance(tag, strtype):
raise TypeError("'tag', expecting a str, got " + type(tag).__name__)
args.update({ 'c': 'tests', 'v': '1.9.0' })
if 'u' not in args:
args.update({ 'u': 'alice', 'p': 'Alic3' })
args.update({"c": "tests", "v": "1.9.0"})
if "u" not in args:
args.update({"u": "alice", "p": "Alic3"})
uri = '/rest/{}.view'.format(endpoint)
rg = self.client.get(uri, query_string = args)
uri = "/rest/{}.view".format(endpoint)
rg = self.client.get(uri, query_string=args)
if not skip_post:
rp = self.client.post(uri, data = args)
rp = self.client.post(uri, data=args)
self.assertEqual(rg.data, rp.data)
xml = etree.fromstring(rg.data)
if not skip_xsd:
self.schema.assert_(xml)
if xml.get('status') == 'ok':
if xml.get("status") == "ok":
self.assertIsNone(error)
if tag:
self.assertEqual(xml[0].tag, '{{{}}}{}'.format(NS, tag))
self.assertEqual(xml[0].tag, "{{{}}}{}".format(NS, tag))
return rg, xml[0]
else:
self.assertEqual(len(xml), 0)
return rg, None
else:
self.assertIsNone(tag)
self.assertEqual(xml[0].tag, '{{{}}}error'.format(NS))
self.assertEqual(xml[0].get('code'), str(error))
self.assertEqual(xml[0].tag, "{{{}}}error".format(NS))
self.assertEqual(xml[0].get("code"), str(error))
return rg

View File

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

View File

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

View File

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

View File

@ -18,45 +18,48 @@ from supysonic.db import Folder, Artist, Album, Track
from .apitestbase import ApiTestBase
class BrowseTestCase(ApiTestBase):
def setUp(self):
super(BrowseTestCase, self).setUp()
with db_session:
Folder(root = True, name = 'Empty root', path = '/tmp')
root = Folder(root = True, name = 'Root folder', path = 'tests/assets')
Folder(root=True, name="Empty root", path="/tmp")
root = Folder(root=True, name="Root folder", path="tests/assets")
for letter in 'ABC':
for letter in "ABC":
folder = Folder(
name = letter + 'rtist',
path = 'tests/assets/{}rtist'.format(letter),
parent = root
name=letter + "rtist",
path="tests/assets/{}rtist".format(letter),
parent=root,
)
artist = Artist(name = letter + 'rtist')
artist = Artist(name=letter + "rtist")
for lether in 'AB':
for lether in "AB":
afolder = Folder(
name = letter + lether + 'lbum',
path = 'tests/assets/{0}rtist/{0}{1}lbum'.format(letter, lether),
parent = folder
name=letter + lether + "lbum",
path="tests/assets/{0}rtist/{0}{1}lbum".format(letter, lether),
parent=folder,
)
album = Album(name = letter + lether + 'lbum', artist = artist)
album = Album(name=letter + lether + "lbum", artist=artist)
for num, song in enumerate([ 'One', 'Two', 'Three' ]):
for num, song in enumerate(["One", "Two", "Three"]):
track = Track(
disc = 1,
number = num,
title = song,
duration = 2,
album = album,
artist = artist,
bitrate = 320,
path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song),
last_modification = 0,
root_folder = root,
folder = afolder
disc=1,
number=num,
title=song,
duration=2,
album=album,
artist=artist,
bitrate=320,
path="tests/assets/{0}rtist/{0}{1}lbum/{2}".format(
letter, lether, song
),
last_modification=0,
root_folder=root,
folder=afolder,
)
self.assertEqual(Folder.select().count(), 11)
@ -68,107 +71,136 @@ class BrowseTestCase(ApiTestBase):
def test_get_music_folders(self):
# Do not validate against the XSD here, this is the only place where the API should return ids as ints
# all our ids are uuids :/
rv, child = self._make_request('getMusicFolders', tag = 'musicFolders', skip_xsd = True)
rv, child = self._make_request(
"getMusicFolders", tag="musicFolders", skip_xsd=True
)
self.assertEqual(len(child), 2)
self.assertSequenceEqual(sorted(self._xpath(child, './musicFolder/@name')), [ 'Empty root', 'Root folder' ])
self.assertSequenceEqual(
sorted(self._xpath(child, "./musicFolder/@name")),
["Empty root", "Root folder"],
)
def test_get_indexes(self):
self._make_request('getIndexes', { 'musicFolderId': 'abcdef' }, error = 0)
self._make_request('getIndexes', { 'musicFolderId': str(uuid.uuid4()) }, error = 70)
self._make_request('getIndexes', { 'ifModifiedSince': 'quoi' }, error = 0)
self._make_request("getIndexes", {"musicFolderId": "abcdef"}, error=0)
self._make_request("getIndexes", {"musicFolderId": str(uuid.uuid4())}, error=70)
self._make_request("getIndexes", {"ifModifiedSince": "quoi"}, error=0)
rv, child = self._make_request('getIndexes', { 'ifModifiedSince': int(time.time() * 1000 + 1000) }, tag = 'indexes')
rv, child = self._make_request(
"getIndexes",
{"ifModifiedSince": int(time.time() * 1000 + 1000)},
tag="indexes",
)
self.assertEqual(len(child), 0)
with db_session:
fid = Folder.get(name = 'Empty root').id
rv, child = self._make_request('getIndexes', { 'musicFolderId': str(fid) }, tag = 'indexes')
fid = Folder.get(name="Empty root").id
rv, child = self._make_request(
"getIndexes", {"musicFolderId": str(fid)}, tag="indexes"
)
self.assertEqual(len(child), 0)
rv, child = self._make_request('getIndexes', tag = 'indexes')
rv, child = self._make_request("getIndexes", tag="indexes")
self.assertEqual(len(child), 3)
for i, letter in enumerate([ 'A', 'B', 'C' ]):
self.assertEqual(child[i].get('name'), letter)
for i, letter in enumerate(["A", "B", "C"]):
self.assertEqual(child[i].get("name"), letter)
self.assertEqual(len(child[i]), 1)
self.assertEqual(child[i][0].get('name'), letter + 'rtist')
self.assertEqual(child[i][0].get("name"), letter + "rtist")
def test_get_music_directory(self):
self._make_request('getMusicDirectory', error = 10)
self._make_request('getMusicDirectory', { 'id': 'id' }, error = 0)
self._make_request('getMusicDirectory', { 'id': str(uuid.uuid4()) }, error = 70)
self._make_request("getMusicDirectory", error=10)
self._make_request("getMusicDirectory", {"id": "id"}, error=0)
self._make_request("getMusicDirectory", {"id": str(uuid.uuid4())}, error=70)
# should test with folders with both children folders and tracks. this code would break in that case
with db_session:
for f in Folder.select():
rv, child = self._make_request('getMusicDirectory', { 'id': str(f.id) }, tag = 'directory')
self.assertEqual(child.get('id'), str(f.id))
self.assertEqual(child.get('name'), f.name)
rv, child = self._make_request(
"getMusicDirectory", {"id": str(f.id)}, tag="directory"
)
self.assertEqual(child.get("id"), str(f.id))
self.assertEqual(child.get("name"), f.name)
self.assertEqual(len(child), f.children.count() + f.tracks.count())
for dbc, xmlc in zip(sorted(f.children, key = lambda c: c.name), sorted(child, key = lambda c: c.get('title'))):
self.assertEqual(dbc.name, xmlc.get('title'))
self.assertEqual(xmlc.get('artist'), f.name)
self.assertEqual(xmlc.get('parent'), str(f.id))
for t, xmlc in zip(sorted(f.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))):
self.assertEqual(t.title, xmlc.get('title'))
self.assertEqual(xmlc.get('parent'), str(f.id))
for dbc, xmlc in zip(
sorted(f.children, key=lambda c: c.name),
sorted(child, key=lambda c: c.get("title")),
):
self.assertEqual(dbc.name, xmlc.get("title"))
self.assertEqual(xmlc.get("artist"), f.name)
self.assertEqual(xmlc.get("parent"), str(f.id))
for t, xmlc in zip(
sorted(f.tracks, key=lambda t: t.title),
sorted(child, key=lambda c: c.get("title")),
):
self.assertEqual(t.title, xmlc.get("title"))
self.assertEqual(xmlc.get("parent"), str(f.id))
def test_get_artists(self):
# same as getIndexes standard case
# dataset should be improved to have a different directory structure than /root/Artist/Album/Track
rv, child = self._make_request('getArtists', tag = 'artists')
rv, child = self._make_request("getArtists", tag="artists")
self.assertEqual(len(child), 3)
for i, letter in enumerate([ 'A', 'B', 'C' ]):
self.assertEqual(child[i].get('name'), letter)
for i, letter in enumerate(["A", "B", "C"]):
self.assertEqual(child[i].get("name"), letter)
self.assertEqual(len(child[i]), 1)
self.assertEqual(child[i][0].get('name'), letter + 'rtist')
self.assertEqual(child[i][0].get("name"), letter + "rtist")
def test_get_artist(self):
# dataset should be improved to have tracks by a different artist than the album's artist
self._make_request('getArtist', error = 10)
self._make_request('getArtist', { 'id': 'artist' }, error = 0)
self._make_request('getArtist', { 'id': str(uuid.uuid4()) }, error = 70)
self._make_request("getArtist", error=10)
self._make_request("getArtist", {"id": "artist"}, error=0)
self._make_request("getArtist", {"id": str(uuid.uuid4())}, error=70)
with db_session:
for ar in Artist.select():
rv, child = self._make_request('getArtist', { 'id': str(ar.id) }, tag = 'artist')
self.assertEqual(child.get('id'), str(ar.id))
self.assertEqual(child.get('albumCount'), str(len(child)))
rv, child = self._make_request(
"getArtist", {"id": str(ar.id)}, tag="artist"
)
self.assertEqual(child.get("id"), str(ar.id))
self.assertEqual(child.get("albumCount"), str(len(child)))
self.assertEqual(len(child), ar.albums.count())
for dal, xal in zip(sorted(ar.albums, key = lambda a: a.name), sorted(child, key = lambda c: c.get('name'))):
self.assertEqual(dal.name, xal.get('name'))
self.assertEqual(xal.get('artist'), ar.name) # could break with a better dataset
self.assertEqual(xal.get('artistId'), str(ar.id)) # see above
for dal, xal in zip(
sorted(ar.albums, key=lambda a: a.name),
sorted(child, key=lambda c: c.get("name")),
):
self.assertEqual(dal.name, xal.get("name"))
self.assertEqual(
xal.get("artist"), ar.name
) # could break with a better dataset
self.assertEqual(xal.get("artistId"), str(ar.id)) # see above
def test_get_album(self):
self._make_request('getAlbum', error = 10)
self._make_request('getAlbum', { 'id': 'nastynasty' }, error = 0)
self._make_request('getAlbum', { 'id': str(uuid.uuid4()) }, error = 70)
self._make_request("getAlbum", error=10)
self._make_request("getAlbum", {"id": "nastynasty"}, error=0)
self._make_request("getAlbum", {"id": str(uuid.uuid4())}, error=70)
with db_session:
a = Album.select().first()
rv, child = self._make_request('getAlbum', { 'id': str(a.id) }, tag = 'album')
self.assertEqual(child.get('id'), str(a.id))
self.assertEqual(child.get('songCount'), str(len(child)))
rv, child = self._make_request("getAlbum", {"id": str(a.id)}, tag="album")
self.assertEqual(child.get("id"), str(a.id))
self.assertEqual(child.get("songCount"), str(len(child)))
self.assertEqual(len(child), a.tracks.count())
for dal, xal in zip(sorted(a.tracks, key = lambda t: t.title), sorted(child, key = lambda c: c.get('title'))):
self.assertEqual(dal.title, xal.get('title'))
self.assertEqual(xal.get('album'), a.name)
self.assertEqual(xal.get('albumId'), str(a.id))
for dal, xal in zip(
sorted(a.tracks, key=lambda t: t.title),
sorted(child, key=lambda c: c.get("title")),
):
self.assertEqual(dal.title, xal.get("title"))
self.assertEqual(xal.get("album"), a.name)
self.assertEqual(xal.get("albumId"), str(a.id))
def test_get_song(self):
self._make_request('getSong', error = 10)
self._make_request('getSong', { 'id': 'nastynasty' }, error = 0)
self._make_request('getSong', { 'id': str(uuid.uuid4()) }, error = 70)
self._make_request("getSong", error=10)
self._make_request("getSong", {"id": "nastynasty"}, error=0)
self._make_request("getSong", {"id": str(uuid.uuid4())}, error=70)
with db_session:
s = Track.select().first()
self._make_request('getSong', { 'id': str(s.id) }, tag = 'song')
self._make_request("getSong", {"id": str(s.id)}, tag="song")
def test_get_videos(self):
self._make_request('getVideos', error = 0)
self._make_request("getVideos", error=0)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

View File

@ -14,32 +14,41 @@ import time
from .apitestbase import ApiTestBase
class ChatTestCase(ApiTestBase):
def test_add_message(self):
self._make_request('addChatMessage', error = 10)
rv, child = self._make_request('getChatMessages', tag = 'chatMessages')
self._make_request("addChatMessage", error=10)
rv, child = self._make_request("getChatMessages", tag="chatMessages")
self.assertEqual(len(child), 0)
self._make_request('addChatMessage', { 'message': 'Heres a message' }, skip_post = True)
rv, child = self._make_request('getChatMessages', tag = 'chatMessages')
self._make_request(
"addChatMessage", {"message": "Heres a message"}, skip_post=True
)
rv, child = self._make_request("getChatMessages", tag="chatMessages")
self.assertEqual(len(child), 1)
self.assertEqual(child[0].get('username'), 'alice')
self.assertEqual(child[0].get('message'), 'Heres a message')
self.assertEqual(child[0].get("username"), "alice")
self.assertEqual(child[0].get("message"), "Heres a message")
def test_get_messages(self):
self._make_request('addChatMessage', { 'message': 'Hello' }, skip_post = True)
self._make_request("addChatMessage", {"message": "Hello"}, skip_post=True)
time.sleep(1)
self._make_request('addChatMessage', { 'message': 'Is someone there?' }, skip_post = True)
self._make_request(
"addChatMessage", {"message": "Is someone there?"}, skip_post=True
)
rv, child = self._make_request('getChatMessages', tag = 'chatMessages')
rv, child = self._make_request("getChatMessages", tag="chatMessages")
self.assertEqual(len(child), 2)
rv, child = self._make_request('getChatMessages', { 'since': int(time.time()) * 1000 - 500 }, tag = 'chatMessages')
rv, child = self._make_request(
"getChatMessages",
{"since": int(time.time()) * 1000 - 500},
tag="chatMessages",
)
self.assertEqual(len(child), 1)
self.assertEqual(child[0].get('message'), 'Is someone there?')
self.assertEqual(child[0].get("message"), "Is someone there?")
self._make_request('getChatMessages', { 'since': 'invalid timestamp' }, error = 0)
self._make_request("getChatMessages", {"since": "invalid timestamp"}, error=0)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,43 +18,46 @@ from supysonic.scanner import Scanner
from .apitestbase import ApiTestBase
class TranscodingTestCase(ApiTestBase):
def setUp(self):
super(TranscodingTestCase, self).setUp()
self._patch_client()
with db_session:
folder = FolderManager.add('Folder', 'tests/assets/folder')
folder = FolderManager.add("Folder", "tests/assets/folder")
scanner = Scanner()
scanner.queue_folder('Folder')
scanner.queue_folder("Folder")
scanner.run()
self.trackid = Track.get().id
def _stream(self, **kwargs):
kwargs.update({ 'u': 'alice', 'p': 'Alic3', 'c': 'tests', 'v': '1.9.0', 'id': self.trackid })
kwargs.update(
{"u": "alice", "p": "Alic3", "c": "tests", "v": "1.9.0", "id": self.trackid}
)
rv = self.client.get('/rest/stream.view', query_string = kwargs)
rv = self.client.get("/rest/stream.view", query_string=kwargs)
self.assertEqual(rv.status_code, 200)
self.assertFalse(rv.mimetype.startswith('text/'))
self.assertFalse(rv.mimetype.startswith("text/"))
return rv
def test_no_transcoding_available(self):
self._make_request('stream', { 'id': self.trackid, 'format': 'wat' }, error = 0)
self._make_request("stream", {"id": self.trackid, "format": "wat"}, error=0)
def test_direct_transcode(self):
rv = self._stream(maxBitRate = 96, estimateContentLength = 'true')
self.assertIn('tests/assets/folder/silence.mp3', rv.data)
self.assertTrue(rv.data.endswith('96'))
rv = self._stream(maxBitRate=96, estimateContentLength="true")
self.assertIn("tests/assets/folder/silence.mp3", rv.data)
self.assertTrue(rv.data.endswith("96"))
def test_decode_encode(self):
rv = self._stream(format = 'cat')
self.assertEqual(rv.data, 'Pushing out some mp3 data...')
rv = self._stream(format="cat")
self.assertEqual(rv.data, "Pushing out some mp3 data...")
rv = self._stream(format = 'md5')
self.assertTrue(rv.data.startswith('dbb16c0847e5d8c3b1867604828cb50b'))
rv = self._stream(format="md5")
self.assertTrue(rv.data.startswith("dbb16c0847e5d8c3b1867604828cb50b"))
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,16 +13,17 @@ import unittest
from supysonic.lastfm import LastFm
class LastFmTestCase(unittest.TestCase):
""" Designed only to have coverage on the most important method """
def test_request(self):
logging.getLogger('supysonic.lastfm').addHandler(logging.NullHandler())
lastfm = LastFm({ 'api_key': 'key', 'secret': 'secret' }, None)
logging.getLogger("supysonic.lastfm").addHandler(logging.NullHandler())
lastfm = LastFm({"api_key": "key", "secret": "secret"}, None)
rv = lastfm._LastFm__api_request(False, method = 'dummy', accents = u'àéèùö')
rv = lastfm._LastFm__api_request(False, method="dummy", accents=u"àéèùö")
self.assertIsInstance(rv, dict)
if __name__ == '__main__':
unittest.main()
if __name__ == "__main__":
unittest.main()

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@
from ..testbase import TestBase
class FrontendTestBase(TestBase):
__with_webui__ = True
@ -17,8 +18,11 @@ class FrontendTestBase(TestBase):
self._patch_client()
def _login(self, username, password):
return self.client.post('/user/login', data = { 'user': username, 'password': password }, follow_redirects = True)
return self.client.post(
"/user/login",
data={"user": username, "password": password},
follow_redirects=True,
)
def _logout(self):
return self.client.get('/user/logout', follow_redirects = True)
return self.client.get("/user/logout", follow_redirects=True)

View File

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

View File

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

View File

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

View File

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

View File

@ -19,37 +19,39 @@ from supysonic.db import Folder
from supysonic.managers.folder import FolderManager
from supysonic.scanner import Scanner
class Issue101TestCase(unittest.TestCase):
def setUp(self):
self.__dir = tempfile.mkdtemp()
init_database('sqlite:')
init_database("sqlite:")
with db_session:
FolderManager.add('folder', self.__dir)
FolderManager.add("folder", self.__dir)
def tearDown(self):
release_database()
shutil.rmtree(self.__dir)
def test_issue(self):
firstsubdir = tempfile.mkdtemp(dir = self.__dir)
firstsubdir = tempfile.mkdtemp(dir=self.__dir)
subdir = firstsubdir
for _ in range(4):
subdir = tempfile.mkdtemp(dir = subdir)
shutil.copyfile('tests/assets/folder/silence.mp3', os.path.join(subdir, 'silence.mp3'))
subdir = tempfile.mkdtemp(dir=subdir)
shutil.copyfile(
"tests/assets/folder/silence.mp3", os.path.join(subdir, "silence.mp3")
)
with db_session:
scanner = Scanner()
scanner.queue_folder('folder')
scanner.queue_folder("folder")
scanner.run()
shutil.rmtree(firstsubdir)
with db_session:
scanner = Scanner()
scanner.queue_folder('folder')
scanner.queue_folder("folder")
scanner.run()
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

View File

@ -18,35 +18,36 @@ from supysonic.scanner import Scanner
from .testbase import TestBase
class Issue129TestCase(TestBase):
def setUp(self):
super(Issue129TestCase, self).setUp()
with db_session:
folder = FolderManager.add('folder', os.path.abspath('tests/assets/folder'))
folder = FolderManager.add("folder", os.path.abspath("tests/assets/folder"))
scanner = Scanner()
scanner.queue_folder('folder')
scanner.queue_folder("folder")
scanner.run()
self.trackid = Track.select().first().id
self.userid = User.get(name = 'alice').id
self.userid = User.get(name="alice").id
def test_last_play(self):
with db_session:
User[self.userid].last_play = Track[self.trackid]
with db_session:
FolderManager.delete_by_name('folder')
FolderManager.delete_by_name("folder")
def test_starred(self):
with db_session:
StarredTrack(user = self.userid, starred = self.trackid)
FolderManager.delete_by_name('folder')
StarredTrack(user=self.userid, starred=self.trackid)
FolderManager.delete_by_name("folder")
def test_rating(self):
with db_session:
RatingTrack(user = self.userid, rated = self.trackid, rating = 5)
FolderManager.delete_by_name('folder')
RatingTrack(user=self.userid, rated=self.trackid, rating=5)
FolderManager.delete_by_name("folder")
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,6 @@
import binascii
def hexlify(s):
return binascii.hexlify(s.encode('utf-8')).decode('utf-8')
def hexlify(s):
return binascii.hexlify(s.encode("utf-8")).decode("utf-8")