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:
parent
7966f767ca
commit
7c8a75d45c
60
setup.py
60
setup.py
@ -15,14 +15,14 @@ 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(
|
||||
@ -35,29 +35,31 @@ setup(
|
||||
author_email=project.AUTHOR_EMAIL,
|
||||
url=project.URL,
|
||||
license=project.LICENSE,
|
||||
packages=find_packages(exclude=['tests*']),
|
||||
packages=find_packages(exclude=["tests*"]),
|
||||
install_requires=reqs,
|
||||
entry_points={ 'console_scripts': [
|
||||
'supysonic-cli=supysonic.cli:main',
|
||||
'supysonic-daemon=supysonic.daemon:main'
|
||||
] },
|
||||
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' ],
|
||||
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'
|
||||
]
|
||||
"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",
|
||||
],
|
||||
)
|
||||
|
@ -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"""
|
||||
|
@ -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)
|
||||
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 *
|
||||
|
||||
|
@ -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
|
||||
@ -39,119 +53,195 @@ def rand_songs():
|
||||
query = query.filter(lambda t: t.genre == genre)
|
||||
if fid:
|
||||
if not Folder.exists(id=fid, root=True):
|
||||
raise NotFound('Folder')
|
||||
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(
|
||||
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(
|
||||
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(
|
||||
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 ]
|
||||
))
|
||||
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
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -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,15 +35,16 @@ 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)
|
||||
|
||||
|
||||
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:
|
||||
@ -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,7 +139,7 @@ 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]
|
||||
@ -135,18 +149,18 @@ def rate():
|
||||
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
|
||||
@ -46,7 +51,7 @@ def list_indexes():
|
||||
|
||||
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(
|
||||
return request.formatter(
|
||||
"indexes",
|
||||
dict(
|
||||
lastModified=last_modif * 1000,
|
||||
index = [ dict(
|
||||
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()) ]
|
||||
))
|
||||
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()) ]
|
||||
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(
|
||||
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()) ]
|
||||
))
|
||||
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()) ]
|
||||
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)
|
||||
)
|
||||
|
@ -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']
|
||||
msg = request.values["message"]
|
||||
ChatMessage(user=request.user, message=msg)
|
||||
|
||||
return request.formatter.empty
|
||||
|
||||
|
@ -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
|
||||
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
|
||||
|
@ -10,6 +10,7 @@
|
||||
from flask import current_app, request
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
|
||||
class SubsonicAPIException(HTTPException):
|
||||
code = 400
|
||||
api_code = None
|
||||
@ -21,8 +22,9 @@ class SubsonicAPIException(HTTPException):
|
||||
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):
|
||||
@ -101,9 +115,12 @@ class AggregateException(SubsonicAPIException):
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
else:
|
||||
dec_proc = subprocess.Popen(decoder, stdout=subprocess.PIPE)
|
||||
proc = subprocess.Popen(encoder, stdin = dec_proc.stdout, stdout = subprocess.PIPE)
|
||||
proc = subprocess.Popen(
|
||||
encoder, stdin=dec_proc.stdout, stdout=subprocess.PIPE
|
||||
)
|
||||
except OSError:
|
||||
raise ServerError('Error while running the transcoding process')
|
||||
raise ServerError("Error while running the transcoding process")
|
||||
|
||||
def transcode():
|
||||
try:
|
||||
@ -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,9 +182,10 @@ 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
|
||||
@ -163,23 +200,26 @@ def download_media():
|
||||
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)
|
||||
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))
|
||||
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())
|
||||
|
@ -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)
|
||||
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:
|
||||
@ -63,7 +75,7 @@ def create_playlist():
|
||||
elif 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
|
||||
|
||||
|
@ -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:
|
||||
@ -43,25 +55,51 @@ def old_search():
|
||||
tend = offset + count - fcount
|
||||
res = res[:] + tracks[toff:tend][:]
|
||||
|
||||
return request.formatter('searchResult', dict(
|
||||
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 ]
|
||||
))
|
||||
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(
|
||||
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] ]
|
||||
))
|
||||
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_offset = int(artist_offset) if artist_offset else 0
|
||||
@ -70,21 +108,43 @@ def new_search():
|
||||
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_offset = int(artist_offset) if artist_offset else 0
|
||||
@ -93,13 +153,22 @@ def search_id3():
|
||||
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],
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
@ -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))
|
||||
|
@ -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"]
|
||||
)
|
||||
|
@ -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)
|
||||
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
|
||||
|
||||
|
@ -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 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()]):
|
||||
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):
|
||||
|
257
supysonic/cli.py
257
supysonic/cli.py
@ -24,6 +24,7 @@ from .managers.folder import FolderManager
|
||||
from .managers.user import UserManager
|
||||
from .scanner import Scanner
|
||||
|
||||
|
||||
class TimedProgressDisplay:
|
||||
def __init__(self, stdout, interval=5):
|
||||
self.__stdout = stdout
|
||||
@ -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,7 +134,7 @@ 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 []
|
||||
|
||||
@ -125,25 +143,56 @@ class SupysonicCLI(cmd.Cmd):
|
||||
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
|
||||
password = getpass.getpass()
|
||||
confirm = getpass.getpass('Confirm password: ')
|
||||
confirm = getpass.getpass("Confirm password: ")
|
||||
if password != confirm:
|
||||
raise ValueError("Passwords don't match")
|
||||
return password
|
||||
@ -279,10 +380,12 @@ class SupysonicCLI(cmd.Cmd):
|
||||
def user_setadmin(self, name, off):
|
||||
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):
|
||||
@ -294,17 +397,19 @@ class SupysonicCLI(cmd.Cmd):
|
||||
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()
|
||||
|
@ -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):
|
||||
@ -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)
|
||||
|
||||
|
@ -12,22 +12,24 @@ 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):
|
||||
self.name = name
|
||||
@ -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
|
||||
@ -58,9 +61,10 @@ def is_valid_cover(path):
|
||||
except IOError:
|
||||
return False
|
||||
|
||||
|
||||
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):
|
||||
@ -81,4 +85,3 @@ def find_cover_in_folder(path, album_name = None):
|
||||
return candidates[0]
|
||||
|
||||
return sorted(candidates, key=lambda c: c.score, reverse=True)[0]
|
||||
|
||||
|
@ -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()
|
||||
|
@ -14,35 +14,42 @@ 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):
|
||||
self.__folders = folders
|
||||
@ -51,37 +58,42 @@ class ScannerStartCommand(ScannerCommand):
|
||||
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')
|
||||
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)
|
||||
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))
|
||||
|
||||
@ -92,6 +104,6 @@ class DaemonClient(object):
|
||||
|
||||
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))
|
||||
|
@ -7,5 +7,6 @@
|
||||
#
|
||||
# Distributed under terms of the GNU AGPLv3 license.
|
||||
|
||||
|
||||
class DaemonUnavailableError(Exception):
|
||||
pass
|
||||
|
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
|
363
supysonic/db.py
363
supysonic/db.py
@ -31,61 +31,72 @@ try:
|
||||
except ImportError:
|
||||
from urlparse import urlparse, parse_qsl
|
||||
|
||||
SCHEMA_VERSION = '20190518'
|
||||
SCHEMA_VERSION = "20190518"
|
||||
|
||||
|
||||
def now():
|
||||
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')
|
||||
_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)
|
||||
@ -96,39 +107,44 @@ class Folder(PathMixin, db.Entity):
|
||||
isDir=True,
|
||||
title=self.name,
|
||||
album=self.name,
|
||||
created = self.created.isoformat()
|
||||
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,8 +152,9 @@ class Folder(PathMixin, db.Entity):
|
||||
if not count:
|
||||
return total
|
||||
|
||||
|
||||
class Artist(db.Entity):
|
||||
_table_ = 'artist'
|
||||
_table_ = "artist"
|
||||
|
||||
id = PrimaryKey(UUID, default=uuid4)
|
||||
name = Required(str) # unique
|
||||
@ -151,27 +168,31 @@ class Artist(db.Entity):
|
||||
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)
|
||||
name = Required(str)
|
||||
artist = Required(Artist, column = 'artist_id')
|
||||
artist = Required(Artist, column="artist_id")
|
||||
tracks = Set(lambda: Track)
|
||||
|
||||
stars = Set(lambda: StarredAlbum)
|
||||
@ -184,34 +205,40 @@ class Album(db.Entity):
|
||||
artistId=str(self.artist.id),
|
||||
songCount=self.tracks.count(),
|
||||
duration=sum(self.tracks.duration),
|
||||
created = min(self.tracks.created).isoformat()
|
||||
created=min(self.tracks.created).isoformat(),
|
||||
)
|
||||
|
||||
track_with_cover = self.tracks.select(lambda t: t.folder.cover_art is not None).first()
|
||||
track_with_cover = self.tracks.select(
|
||||
lambda t: t.folder.cover_art is not None
|
||||
).first()
|
||||
if track_with_cover is not None:
|
||||
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)
|
||||
disc = Required(int)
|
||||
@ -222,21 +249,21 @@ class Track(PathMixin, db.Entity):
|
||||
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')
|
||||
_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)
|
||||
|
||||
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
|
||||
|
||||
@ -263,53 +290,68 @@ class Track(PathMixin, db.Entity):
|
||||
created=self.created.isoformat(),
|
||||
albumId=str(self.album.id),
|
||||
artistId=str(self.artist.id),
|
||||
type = 'music'
|
||||
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,17 +361,27 @@ 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'
|
||||
_table_ = "user"
|
||||
|
||||
id = PrimaryKey(UUID, default=uuid4)
|
||||
name = Required(str, 64) # unique
|
||||
@ -338,9 +390,11 @@ class User(db.Entity):
|
||||
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
|
||||
lastfm_status = Required(
|
||||
bool, default=True
|
||||
) # True: ok/unlinked, False: invalid session
|
||||
|
||||
last_play = Optional(Track, column = 'last_play_id')
|
||||
last_play = Optional(Track, column="last_play_id")
|
||||
last_play_date = Optional(datetime, precision=0)
|
||||
|
||||
clients = Set(lambda: ClientPrefs)
|
||||
@ -369,90 +423,97 @@ class User(db.Entity):
|
||||
podcastRole=False,
|
||||
streamRole=True,
|
||||
jukeboxRole=False,
|
||||
shareRole = False
|
||||
shareRole=False,
|
||||
)
|
||||
|
||||
class ClientPrefs(db.Entity):
|
||||
_table_ = 'client_prefs'
|
||||
|
||||
user = Required(User, column = 'user_id')
|
||||
class ClientPrefs(db.Entity):
|
||||
_table_ = "client_prefs"
|
||||
|
||||
user = Required(User, column="user_id")
|
||||
client_name = Required(str, 32)
|
||||
PrimaryKey(user, client_name)
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
_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')
|
||||
_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'
|
||||
_table_ = "chat_message"
|
||||
|
||||
id = PrimaryKey(UUID, default=uuid4)
|
||||
user = Required(User, column = 'user_id')
|
||||
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'
|
||||
_table_ = "playlist"
|
||||
|
||||
id = PrimaryKey(UUID, default=uuid4)
|
||||
user = Required(User, column = 'user_id')
|
||||
user = Required(User, column="user_id")
|
||||
name = Required(str)
|
||||
comment = Optional(str)
|
||||
public = Required(bool, default=False)
|
||||
@ -463,15 +524,17 @@ class Playlist(db.Entity):
|
||||
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),
|
||||
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()
|
||||
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,49 +570,66 @@ 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)
|
||||
|
||||
@ -561,28 +641,37 @@ def init_database(database_uri):
|
||||
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()
|
||||
@ -590,9 +679,9 @@ def init_database(database_uri):
|
||||
|
||||
db.generate_mapping(check_tables=False)
|
||||
|
||||
|
||||
def release_database():
|
||||
metadb.disconnect()
|
||||
db.disconnect()
|
||||
db.provider = metadb.provider = None
|
||||
db.schema = metadb.schema = None
|
||||
|
||||
|
@ -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 *
|
||||
|
||||
|
@ -21,60 +21,70 @@ 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):
|
||||
try:
|
||||
@ -82,13 +92,13 @@ def scan_folder(id = None):
|
||||
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"))
|
||||
|
@ -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"))
|
||||
|
@ -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
|
||||
@ -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)
|
||||
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)
|
||||
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"))
|
||||
|
@ -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
|
||||
|
||||
|
@ -6,4 +6,3 @@
|
||||
# Copyright (C) 2013 Alban 'spl0k' Féron
|
||||
#
|
||||
# Distributed under terms of the GNU AGPLv3 license.
|
||||
|
||||
|
@ -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,7 +27,7 @@ class FolderManager:
|
||||
elif isinstance(uid, uuid.UUID):
|
||||
pass
|
||||
else:
|
||||
raise ValueError('Invalid folder id')
|
||||
raise ValueError("Invalid folder id")
|
||||
|
||||
return Folder[uid]
|
||||
|
||||
@ -39,11 +40,11 @@ class FolderManager:
|
||||
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')
|
||||
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)
|
||||
try:
|
||||
@ -72,7 +73,9 @@ class FolderManager:
|
||||
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()
|
||||
|
||||
@ -82,4 +85,3 @@ class FolderManager:
|
||||
if not folder:
|
||||
raise ObjectNotFound(Folder)
|
||||
FolderManager.delete(folder.id)
|
||||
|
||||
|
@ -18,6 +18,7 @@ from pony.orm import ObjectNotFound
|
||||
from ..db import User
|
||||
from ..py23 import strtype
|
||||
|
||||
|
||||
class UserManager:
|
||||
@staticmethod
|
||||
def get(uid):
|
||||
@ -26,7 +27,7 @@ class UserManager:
|
||||
elif isinstance(uid, strtype):
|
||||
uid = uuid.UUID(uid)
|
||||
else:
|
||||
raise ValueError('Invalid user id')
|
||||
raise ValueError("Invalid user id")
|
||||
|
||||
return User[uid]
|
||||
|
||||
@ -37,13 +38,7 @@ class UserManager:
|
||||
|
||||
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
|
||||
|
||||
@ -73,7 +68,7 @@ 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]
|
||||
|
||||
@ -88,6 +83,8 @@ class UserManager:
|
||||
@staticmethod
|
||||
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,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
@ -107,7 +118,7 @@ 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)
|
||||
@ -125,7 +136,7 @@ class Scanner(Thread):
|
||||
|
||||
for f in entries:
|
||||
try: # test for badly encoded filenames
|
||||
f.encode('utf-8')
|
||||
f.encode("utf-8")
|
||||
except UnicodeError:
|
||||
self.__stats.errors.append(path)
|
||||
continue
|
||||
@ -155,7 +166,9 @@ class Scanner(Thread):
|
||||
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
|
||||
@ -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
|
||||
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,48 +223,63 @@ 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)
|
||||
if not tr:
|
||||
@ -261,9 +291,9 @@ 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
|
||||
@ -289,7 +319,7 @@ 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)))
|
||||
raise TypeError("Expecting string, got " + str(type(dirpath)))
|
||||
|
||||
if not os.path.exists(dirpath):
|
||||
return
|
||||
@ -309,7 +339,7 @@ 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)))
|
||||
raise TypeError("Expecting string, got " + str(type(path)))
|
||||
|
||||
folder = Folder.get(path=os.path.dirname(path))
|
||||
if folder is None:
|
||||
@ -356,19 +386,27 @@ 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 != '/':
|
||||
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
|
||||
@ -386,7 +424,7 @@ class Scanner(Thread):
|
||||
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
|
||||
|
||||
|
@ -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}
|
||||
|
||||
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()
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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}
|
||||
|
||||
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"))
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -29,12 +29,17 @@ 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:
|
||||
@ -80,6 +85,7 @@ class SupysonicWatcherEventHandler(PatternMatchingEventHandler):
|
||||
op |= FLAG_COVER
|
||||
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):
|
||||
@ -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__()
|
||||
@ -247,10 +254,11 @@ class ScannerProcessingQueue(Thread):
|
||||
|
||||
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,7 +270,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("Scheduling watcher for %s", path)
|
||||
watch = self.__observer.schedule(self.__handler, path, recursive=True)
|
||||
@ -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()
|
||||
)
|
||||
|
@ -24,61 +24,66 @@ from .utils import get_secret_key
|
||||
|
||||
logger = logging.getLogger(__package__)
|
||||
|
||||
|
||||
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
|
||||
config = IniConfig.from_common_locations()
|
||||
app.config.from_object(config)
|
||||
|
||||
# Set loglevel
|
||||
logfile = app.config['WEBAPP']['log_file']
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
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,11 +71,11 @@ 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)
|
||||
uri = "/rest/{}.view".format(endpoint)
|
||||
rg = self.client.get(uri, query_string=args)
|
||||
if not skip_post:
|
||||
rp = self.client.post(uri, data=args)
|
||||
@ -82,17 +85,16 @@ class ApiTestBase(TestBase):
|
||||
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
|
||||
|
||||
|
@ -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',
|
||||
title="Track",
|
||||
album=album,
|
||||
artist=artist,
|
||||
disc=1,
|
||||
number=1,
|
||||
path = 'tests/assets/empty',
|
||||
path="tests/assets/empty",
|
||||
folder=folder,
|
||||
root_folder=folder,
|
||||
duration=2,
|
||||
bitrate=320,
|
||||
last_modification = 0
|
||||
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()
|
||||
|
||||
|
@ -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',
|
||||
title="Track",
|
||||
album=album,
|
||||
artist=artist,
|
||||
disc=1,
|
||||
number=1,
|
||||
path = 'tests/assets/empty',
|
||||
path="tests/assets/empty",
|
||||
folder=folder,
|
||||
root_folder=root,
|
||||
duration=2,
|
||||
bitrate=320,
|
||||
last_modification = 0
|
||||
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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -18,33 +18,34 @@ 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,
|
||||
@ -53,10 +54,12 @@ class BrowseTestCase(ApiTestBase):
|
||||
album=album,
|
||||
artist=artist,
|
||||
bitrate=320,
|
||||
path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song),
|
||||
path="tests/assets/{0}rtist/{0}{1}lbum/{2}".format(
|
||||
letter, lether, song
|
||||
),
|
||||
last_modification=0,
|
||||
root_folder=root,
|
||||
folder = afolder
|
||||
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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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'),
|
||||
name="Root",
|
||||
path=os.path.abspath("tests/assets"),
|
||||
root=True,
|
||||
cover_art = 'cover.jpg'
|
||||
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',
|
||||
title="23bytes",
|
||||
number=1,
|
||||
disc=1,
|
||||
artist=artist,
|
||||
album=album,
|
||||
path = os.path.abspath('tests/assets/23bytes'),
|
||||
path=os.path.abspath("tests/assets/23bytes"),
|
||||
root_folder=folder,
|
||||
folder=folder,
|
||||
duration=2,
|
||||
bitrate=320,
|
||||
last_modification = 0
|
||||
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]',
|
||||
title="[silence]",
|
||||
number=1,
|
||||
disc=1,
|
||||
artist=artist,
|
||||
album=album,
|
||||
path = os.path.abspath('tests/assets/formats/silence.{0}'.format(self.formats[i][0])),
|
||||
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
|
||||
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()
|
||||
|
||||
|
@ -16,17 +16,18 @@ 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,
|
||||
@ -35,127 +36,176 @@ class PlaylistTestCase(ApiTestBase):
|
||||
album=album,
|
||||
artist=artist,
|
||||
bitrate=320,
|
||||
path = 'tests/assets/' + song,
|
||||
path="tests/assets/" + song,
|
||||
last_modification=0,
|
||||
root_folder=root,
|
||||
folder = root
|
||||
folder=root,
|
||||
)
|
||||
songs[song] = track
|
||||
|
||||
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)
|
||||
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()}
|
||||
|
||||
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()
|
||||
|
||||
|
@ -18,6 +18,7 @@ from supysonic.py23 import strtype
|
||||
|
||||
from ..testbase import TestBase
|
||||
|
||||
|
||||
class UnwrapperMixin(object):
|
||||
def make_response(self, elem, data):
|
||||
with self.request_context():
|
||||
@ -28,197 +29,174 @@ class UnwrapperMixin(object):
|
||||
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' }
|
||||
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'
|
||||
]
|
||||
})
|
||||
"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()}
|
||||
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]):
|
||||
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' }
|
||||
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'
|
||||
]
|
||||
})
|
||||
"list": [{"b": "B"}, {"c": "C"}, "final string"],
|
||||
}
|
||||
)
|
||||
|
||||
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()
|
||||
|
@ -17,27 +17,32 @@ 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,
|
||||
@ -46,10 +51,12 @@ class SearchTestCase(ApiTestBase):
|
||||
album=album,
|
||||
artist=artist,
|
||||
bitrate=320,
|
||||
path = 'tests/assets/{0}rtist/{0}{1}lbum/{2}'.format(letter, lether, song),
|
||||
path="tests/assets/{0}rtist/{0}{1}lbum/{2}".format(
|
||||
letter, lether, song
|
||||
),
|
||||
last_modification=0,
|
||||
root_folder=root,
|
||||
folder = afolder
|
||||
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')
|
||||
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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -27,14 +27,15 @@ 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()
|
||||
@ -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)
|
||||
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 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()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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,25 +33,21 @@ 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
|
||||
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
|
||||
name="Child folder (No Art)",
|
||||
path="tests/formats",
|
||||
parent=root_folder,
|
||||
)
|
||||
|
||||
return root_folder, child_folder, child_2
|
||||
@ -59,13 +56,13 @@ class DbTestCase(unittest.TestCase):
|
||||
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',
|
||||
title="Track Title",
|
||||
album=album,
|
||||
artist=artist,
|
||||
disc=1,
|
||||
@ -73,33 +70,33 @@ class DbTestCase(unittest.TestCase):
|
||||
duration=3,
|
||||
has_art=True,
|
||||
bitrate=320,
|
||||
path = 'tests/assets/formats/silence.ogg',
|
||||
path="tests/assets/formats/silence.ogg",
|
||||
last_modification=1234,
|
||||
root_folder=root,
|
||||
folder = child
|
||||
folder=child,
|
||||
)
|
||||
|
||||
track2 = db.Track(
|
||||
title = 'One Awesome Song',
|
||||
title="One Awesome Song",
|
||||
album=album,
|
||||
artist=artist,
|
||||
disc=1,
|
||||
number=2,
|
||||
duration=5,
|
||||
bitrate=96,
|
||||
path = 'tests/assets/23bytes',
|
||||
path="tests/assets/23bytes",
|
||||
last_modification=1234,
|
||||
root_folder=root,
|
||||
folder = child
|
||||
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')
|
||||
artist = artist or db.Artist(name="Snazzy Artist")
|
||||
album = album or db.Album(artist=artist, name="Rockin' Album")
|
||||
return db.Track(
|
||||
title = 'Nifty Number',
|
||||
title="Nifty Number",
|
||||
album=album,
|
||||
artist=artist,
|
||||
disc=1,
|
||||
@ -107,25 +104,18 @@ class DbTestCase(unittest.TestCase):
|
||||
duration=5,
|
||||
has_art=has_art,
|
||||
bitrate=96,
|
||||
path = 'tests/assets/formats/silence.flac',
|
||||
path="tests/assets/formats/silence.flac",
|
||||
last_modification=1234,
|
||||
root_folder=root,
|
||||
folder = folder
|
||||
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)
|
||||
|
||||
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):
|
||||
@ -314,7 +289,9 @@ class DbTestCase(unittest.TestCase):
|
||||
|
||||
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(), [])
|
||||
@ -322,7 +299,7 @@ class DbTestCase(unittest.TestCase):
|
||||
playlist.add(str(track1.id))
|
||||
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
|
||||
@ -358,9 +335,9 @@ class DbTestCase(unittest.TestCase):
|
||||
track2.delete()
|
||||
self.assertSequenceEqual(playlist.get_tracks(), [track1])
|
||||
|
||||
playlist.tracks = '{0},{0},some random garbage,{0}'.format(track1.id)
|
||||
playlist.tracks = "{0},{0},some random garbage,{0}".format(track1.id)
|
||||
self.assertSequenceEqual(playlist.get_tracks(), [track1, track1, track1])
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
@ -39,13 +40,13 @@ class ScannerTestCase(unittest.TestCase):
|
||||
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 io.open(track.path, "rb") as f:
|
||||
tf.write(f.read())
|
||||
yield tf
|
||||
|
||||
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)
|
||||
@ -165,19 +170,19 @@ class ScannerTestCase(unittest.TestCase):
|
||||
self.__scan()
|
||||
commit()
|
||||
copy = db.Track.get(path=tf.name)
|
||||
self.assertEqual(copy.artist.name, 'Some artist')
|
||||
self.assertEqual(copy.album.name, 'Awesome album')
|
||||
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["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()
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
@ -85,23 +84,26 @@ class WatcherTestCase(WatcherTestBase):
|
||||
|
||||
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)
|
||||
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')
|
||||
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):
|
||||
@ -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["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,12 +321,14 @@ 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)
|
||||
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -16,18 +16,19 @@ 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',
|
||||
path="tests/assets/23bytes",
|
||||
title="23bytes",
|
||||
artist=artist,
|
||||
album=album,
|
||||
folder=folder,
|
||||
@ -36,79 +37,90 @@ class PlaylistTestCase(FrontendTestBase):
|
||||
disc=1,
|
||||
number=1,
|
||||
bitrate=320,
|
||||
last_modification = 0
|
||||
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()
|
||||
|
@ -17,6 +17,7 @@ from supysonic.db import User, ClientPrefs
|
||||
|
||||
from .frontendtestbase import FrontendTestBase
|
||||
|
||||
|
||||
class UserTestCase(FrontendTestBase):
|
||||
def setUp(self):
|
||||
super(UserTestCase, self).setUp()
|
||||
@ -25,217 +26,249 @@ class UserTestCase(FrontendTestBase):
|
||||
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'])
|
||||
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)
|
||||
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')
|
||||
self._login("bob", "B0b")
|
||||
rv = self.client.get(path, follow_redirects=True)
|
||||
self.assertIn('There\'s nothing much to see', rv.data)
|
||||
self.assertIn("There's nothing much to see", rv.data)
|
||||
with db_session:
|
||||
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)
|
||||
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.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()
|
||||
|
||||
|
@ -19,12 +19,13 @@ 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()
|
||||
@ -35,21 +36,22 @@ class Issue101TestCase(unittest.TestCase):
|
||||
subdir = firstsubdir
|
||||
for _ in range(4):
|
||||
subdir = tempfile.mkdtemp(dir=subdir)
|
||||
shutil.copyfile('tests/assets/folder/silence.mp3', os.path.join(subdir, 'silence.mp3'))
|
||||
shutil.copyfile(
|
||||
"tests/assets/folder/silence.mp3", os.path.join(subdir, "silence.mp3")
|
||||
)
|
||||
|
||||
with db_session:
|
||||
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()
|
||||
|
||||
|
@ -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')
|
||||
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')
|
||||
FolderManager.delete_by_name("folder")
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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',
|
||||
title="Track",
|
||||
artist=artist,
|
||||
album=album,
|
||||
disc=1,
|
||||
number=1,
|
||||
path = os.path.join(self.media_dir, 'somefile'),
|
||||
path=os.path.join(self.media_dir, "somefile"),
|
||||
folder=root,
|
||||
root_folder=root,
|
||||
duration=2,
|
||||
bitrate=320,
|
||||
last_modification = 0
|
||||
last_modification=0,
|
||||
)
|
||||
|
||||
@db_session
|
||||
@ -68,13 +67,13 @@ class FolderManagerTestCase(unittest.TestCase):
|
||||
self.create_folders()
|
||||
|
||||
# Get existing folders
|
||||
for name in ['media', 'music']:
|
||||
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)
|
||||
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,13 +123,13 @@ 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']:
|
||||
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))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
@ -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',
|
||||
title="Track",
|
||||
disc=1,
|
||||
number=1,
|
||||
duration=1,
|
||||
artist=artist,
|
||||
album=album,
|
||||
path = 'tests/assets/empty',
|
||||
path="tests/assets/empty",
|
||||
folder=folder,
|
||||
root_folder=folder,
|
||||
bitrate=320,
|
||||
last_modification = 0
|
||||
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']:
|
||||
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,7 +116,7 @@ class UserManagerTestCase(unittest.TestCase):
|
||||
self.assertEqual(db.User.select().count(), 3)
|
||||
|
||||
# Delete existing users
|
||||
for name in ['alice', 'bob', 'charlie']:
|
||||
for name in ["alice", "bob", "charlie"]:
|
||||
user = db.User.get(name=name)
|
||||
UserManager.delete(user.id)
|
||||
self.assertRaises(ObjectNotFound, db.User.__getitem__, user.id)
|
||||
@ -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))
|
||||
|
||||
# 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']:
|
||||
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']:
|
||||
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')
|
||||
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, "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()
|
||||
|
||||
|
@ -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,10 +45,8 @@ 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):
|
||||
@ -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)
|
||||
|
||||
|
@ -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")
|
||||
|
Loading…
Reference in New Issue
Block a user