Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
funkwhale
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Incidents
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nykopol
funkwhale
Commits
bbd27340
Verified
Commit
bbd27340
authored
May 08, 2018
by
Agate
💬
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
See #75: initial subsonic implementation that works with
http://p.subfireplayer.net
parent
96822994
Changes
15
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
774 additions
and
48 deletions
+774
-48
api/config/api_urls.py
api/config/api_urls.py
+7
-1
api/funkwhale_api/music/factories.py
api/funkwhale_api/music/factories.py
+1
-1
api/funkwhale_api/music/models.py
api/funkwhale_api/music/models.py
+7
-1
api/funkwhale_api/music/views.py
api/funkwhale_api/music/views.py
+48
-45
api/funkwhale_api/subsonic/__init__.py
api/funkwhale_api/subsonic/__init__.py
+0
-0
api/funkwhale_api/subsonic/authentication.py
api/funkwhale_api/subsonic/authentication.py
+69
-0
api/funkwhale_api/subsonic/negotiation.py
api/funkwhale_api/subsonic/negotiation.py
+21
-0
api/funkwhale_api/subsonic/renderers.py
api/funkwhale_api/subsonic/renderers.py
+48
-0
api/funkwhale_api/subsonic/serializers.py
api/funkwhale_api/subsonic/serializers.py
+100
-0
api/funkwhale_api/subsonic/views.py
api/funkwhale_api/subsonic/views.py
+143
-0
api/tests/conftest.py
api/tests/conftest.py
+1
-0
api/tests/subsonic/test_authentication.py
api/tests/subsonic/test_authentication.py
+56
-0
api/tests/subsonic/test_renderers.py
api/tests/subsonic/test_renderers.py
+44
-0
api/tests/subsonic/test_serializers.py
api/tests/subsonic/test_serializers.py
+109
-0
api/tests/subsonic/test_views.py
api/tests/subsonic/test_views.py
+120
-0
No files found.
api/config/api_urls.py
View file @
bbd27340
from
rest_framework
import
routers
from
rest_framework.urlpatterns
import
format_suffix_patterns
from
django.conf.urls
import
include
,
url
from
funkwhale_api.activity
import
views
as
activity_views
from
funkwhale_api.instance
import
views
as
instance_views
from
funkwhale_api.music
import
views
from
funkwhale_api.playlists
import
views
as
playlists_views
from
funkwhale_api.subsonic.views
import
SubsonicViewSet
from
rest_framework_jwt
import
views
as
jwt_views
from
dynamic_preferences.api.viewsets
import
GlobalPreferencesViewSet
...
...
@@ -27,6 +29,10 @@ router.register(
'playlist-tracks'
)
v1_patterns
=
router
.
urls
subsonic_router
=
routers
.
SimpleRouter
(
trailing_slash
=
False
)
subsonic_router
.
register
(
r'subsonic/rest'
,
SubsonicViewSet
,
base_name
=
'subsonic'
)
v1_patterns
+=
[
url
(
r'^instance/'
,
include
(
...
...
@@ -68,4 +74,4 @@ v1_patterns += [
urlpatterns
=
[
url
(
r'^v1/'
,
include
((
v1_patterns
,
'v1'
),
namespace
=
'v1'
))
]
]
+
format_suffix_patterns
(
subsonic_router
.
urls
,
allowed
=
[
'view'
])
api/funkwhale_api/music/factories.py
View file @
bbd27340
...
...
@@ -26,7 +26,7 @@ class ArtistFactory(factory.django.DjangoModelFactory):
class
AlbumFactory
(
factory
.
django
.
DjangoModelFactory
):
title
=
factory
.
Faker
(
'sentence'
,
nb_words
=
3
)
mbid
=
factory
.
Faker
(
'uuid4'
)
release_date
=
factory
.
Faker
(
'date'
)
release_date
=
factory
.
Faker
(
'date
_object
'
)
cover
=
factory
.
django
.
ImageField
()
artist
=
factory
.
SubFactory
(
ArtistFactory
)
release_group_id
=
factory
.
Faker
(
'uuid4'
)
...
...
api/funkwhale_api/music/models.py
View file @
bbd27340
...
...
@@ -457,7 +457,13 @@ class TrackFile(models.Model):
def
filename
(
self
):
return
'{}{}'
.
format
(
self
.
track
.
full_name
,
os
.
path
.
splitext
(
self
.
audio_file
.
name
)[
-
1
])
self
.
extension
)
@
property
def
extension
(
self
):
if
not
self
.
audio_file
:
return
return
os
.
path
.
splitext
(
self
.
audio_file
.
name
)[
-
1
].
replace
(
'.'
,
''
,
1
)
def
save
(
self
,
**
kwargs
):
if
not
self
.
mimetype
and
self
.
audio_file
:
...
...
api/funkwhale_api/music/views.py
View file @
bbd27340
...
...
@@ -245,6 +245,53 @@ def get_file_path(audio_file):
return
path
def
handle_serve
(
track_file
):
f
=
track_file
# we update the accessed_date
f
.
accessed_date
=
timezone
.
now
()
f
.
save
(
update_fields
=
[
'accessed_date'
])
mt
=
f
.
mimetype
audio_file
=
f
.
audio_file
try
:
library_track
=
f
.
library_track
except
ObjectDoesNotExist
:
library_track
=
None
if
library_track
and
not
audio_file
:
if
not
library_track
.
audio_file
:
# we need to populate from cache
with
transaction
.
atomic
():
# why the transaction/select_for_update?
# this is because browsers may send multiple requests
# in a short time range, for partial content,
# thus resulting in multiple downloads from the remote
qs
=
LibraryTrack
.
objects
.
select_for_update
()
library_track
=
qs
.
get
(
pk
=
library_track
.
pk
)
library_track
.
download_audio
()
audio_file
=
library_track
.
audio_file
file_path
=
get_file_path
(
audio_file
)
mt
=
library_track
.
audio_mimetype
elif
audio_file
:
file_path
=
get_file_path
(
audio_file
)
elif
f
.
source
and
f
.
source
.
startswith
(
'file://'
):
file_path
=
get_file_path
(
f
.
source
.
replace
(
'file://'
,
''
,
1
))
response
=
Response
()
filename
=
f
.
filename
mapping
=
{
'nginx'
:
'X-Accel-Redirect'
,
'apache2'
:
'X-Sendfile'
,
}
file_header
=
mapping
[
settings
.
REVERSE_PROXY_TYPE
]
response
[
file_header
]
=
file_path
filename
=
"filename*=UTF-8''{}"
.
format
(
urllib
.
parse
.
quote
(
filename
))
response
[
"Content-Disposition"
]
=
"attachment; {}"
.
format
(
filename
)
if
mt
:
response
[
"Content-Type"
]
=
mt
return
response
class
TrackFileViewSet
(
viewsets
.
ReadOnlyModelViewSet
):
queryset
=
(
models
.
TrackFile
.
objects
.
all
().
order_by
(
'-id'
))
serializer_class
=
serializers
.
TrackFileSerializer
...
...
@@ -261,54 +308,10 @@ class TrackFileViewSet(viewsets.ReadOnlyModelViewSet):
'track__artist'
,
)
try
:
f
=
queryset
.
get
(
pk
=
kwargs
[
'pk'
]
)
return
handle_serve
(
queryset
.
get
(
pk
=
kwargs
[
'pk'
])
)
except
models
.
TrackFile
.
DoesNotExist
:
return
Response
(
status
=
404
)
# we update the accessed_date
f
.
accessed_date
=
timezone
.
now
()
f
.
save
(
update_fields
=
[
'accessed_date'
])
mt
=
f
.
mimetype
audio_file
=
f
.
audio_file
try
:
library_track
=
f
.
library_track
except
ObjectDoesNotExist
:
library_track
=
None
if
library_track
and
not
audio_file
:
if
not
library_track
.
audio_file
:
# we need to populate from cache
with
transaction
.
atomic
():
# why the transaction/select_for_update?
# this is because browsers may send multiple requests
# in a short time range, for partial content,
# thus resulting in multiple downloads from the remote
qs
=
LibraryTrack
.
objects
.
select_for_update
()
library_track
=
qs
.
get
(
pk
=
library_track
.
pk
)
library_track
.
download_audio
()
audio_file
=
library_track
.
audio_file
file_path
=
get_file_path
(
audio_file
)
mt
=
library_track
.
audio_mimetype
elif
audio_file
:
file_path
=
get_file_path
(
audio_file
)
elif
f
.
source
and
f
.
source
.
startswith
(
'file://'
):
file_path
=
get_file_path
(
f
.
source
.
replace
(
'file://'
,
''
,
1
))
response
=
Response
()
filename
=
f
.
filename
mapping
=
{
'nginx'
:
'X-Accel-Redirect'
,
'apache2'
:
'X-Sendfile'
,
}
file_header
=
mapping
[
settings
.
REVERSE_PROXY_TYPE
]
response
[
file_header
]
=
file_path
filename
=
"filename*=UTF-8''{}"
.
format
(
urllib
.
parse
.
quote
(
filename
))
response
[
"Content-Disposition"
]
=
"attachment; {}"
.
format
(
filename
)
if
mt
:
response
[
"Content-Type"
]
=
mt
return
response
@
list_route
(
methods
=
[
'get'
])
def
viewable
(
self
,
request
,
*
args
,
**
kwargs
):
return
Response
({},
status
=
200
)
...
...
api/funkwhale_api/subsonic/__init__.py
0 → 100644
View file @
bbd27340
api/funkwhale_api/subsonic/authentication.py
0 → 100644
View file @
bbd27340
import
binascii
import
hashlib
from
rest_framework
import
authentication
from
rest_framework
import
exceptions
from
funkwhale_api.users.models
import
User
def
get_token
(
salt
,
password
):
to_hash
=
password
+
salt
h
=
hashlib
.
md5
()
h
.
update
(
to_hash
.
encode
(
'utf-8'
))
return
h
.
hexdigest
()
def
authenticate
(
username
,
password
):
try
:
if
password
.
startswith
(
'enc:'
):
password
=
password
.
replace
(
'enc:'
,
''
,
1
)
password
=
binascii
.
unhexlify
(
password
).
decode
(
'utf-8'
)
user
=
User
.
objects
.
get
(
username
=
username
,
is_active
=
True
,
subsonic_api_token
=
password
)
except
(
User
.
DoesNotExist
,
binascii
.
Error
):
raise
exceptions
.
AuthenticationFailed
(
'Wrong username or password.'
)
return
(
user
,
None
)
def
authenticate_salt
(
username
,
salt
,
token
):
try
:
user
=
User
.
objects
.
get
(
username
=
username
,
is_active
=
True
,
subsonic_api_token__isnull
=
False
)
except
User
.
DoesNotExist
:
raise
exceptions
.
AuthenticationFailed
(
'Wrong username or password.'
)
expected
=
get_token
(
salt
,
user
.
subsonic_api_token
)
if
expected
!=
token
:
raise
exceptions
.
AuthenticationFailed
(
'Wrong username or password.'
)
return
(
user
,
None
)
class
SubsonicAuthentication
(
authentication
.
BaseAuthentication
):
def
authenticate
(
self
,
request
):
data
=
request
.
GET
or
request
.
POST
username
=
data
.
get
(
'u'
)
if
not
username
:
return
None
p
=
data
.
get
(
'p'
)
s
=
data
.
get
(
's'
)
t
=
data
.
get
(
't'
)
if
not
p
and
(
not
s
or
not
t
):
raise
exceptions
.
AuthenticationFailed
(
'Missing credentials'
)
if
p
:
return
authenticate
(
username
,
p
)
return
authenticate_salt
(
username
,
s
,
t
)
api/funkwhale_api/subsonic/negotiation.py
0 → 100644
View file @
bbd27340
from
rest_framework
import
exceptions
from
rest_framework
import
negotiation
from
.
import
renderers
MAPPING
=
{
'json'
:
(
renderers
.
SubsonicJSONRenderer
(),
'application/json'
),
'xml'
:
(
renderers
.
SubsonicXMLRenderer
(),
'text/xml'
),
}
class
SubsonicContentNegociation
(
negotiation
.
DefaultContentNegotiation
):
def
select_renderer
(
self
,
request
,
renderers
,
format_suffix
=
None
):
path
=
request
.
path
data
=
request
.
GET
or
request
.
POST
requested_format
=
data
.
get
(
'f'
,
'xml'
)
try
:
return
MAPPING
[
requested_format
]
except
KeyError
:
raise
exceptions
.
NotAcceptable
(
available_renderers
=
renderers
)
api/funkwhale_api/subsonic/renderers.py
0 → 100644
View file @
bbd27340
import
xml.etree.ElementTree
as
ET
from
rest_framework
import
renderers
class
SubsonicJSONRenderer
(
renderers
.
JSONRenderer
):
def
render
(
self
,
data
,
accepted_media_type
=
None
,
renderer_context
=
None
):
if
not
data
:
# when stream view is called, we don't have any data
return
super
().
render
(
data
,
accepted_media_type
,
renderer_context
)
final
=
{
'subsonic-response'
:
{
'status'
:
'ok'
,
'version'
:
'1.16.0'
,
}
}
final
[
'subsonic-response'
].
update
(
data
)
return
super
().
render
(
final
,
accepted_media_type
,
renderer_context
)
class
SubsonicXMLRenderer
(
renderers
.
JSONRenderer
):
media_type
=
'text/xml'
def
render
(
self
,
data
,
accepted_media_type
=
None
,
renderer_context
=
None
):
if
not
data
:
# when stream view is called, we don't have any data
return
super
().
render
(
data
,
accepted_media_type
,
renderer_context
)
final
=
{
'xmlns'
:
'http://subsonic.org/restapi'
,
'status'
:
'ok'
,
'version'
:
'1.16.0'
,
}
final
.
update
(
data
)
tree
=
dict_to_xml_tree
(
'subsonic-response'
,
final
)
return
b'<?xml version="1.0" encoding="UTF-8"?>
\n
'
+
ET
.
tostring
(
tree
,
encoding
=
'utf-8'
)
def
dict_to_xml_tree
(
root_tag
,
d
,
parent
=
None
):
root
=
ET
.
Element
(
root_tag
)
for
key
,
value
in
d
.
items
():
if
isinstance
(
value
,
dict
):
root
.
append
(
dict_to_xml_tree
(
key
,
value
,
parent
=
root
))
elif
isinstance
(
value
,
list
):
for
obj
in
value
:
root
.
append
(
dict_to_xml_tree
(
key
,
obj
,
parent
=
root
))
else
:
root
.
set
(
key
,
str
(
value
))
return
root
api/funkwhale_api/subsonic/serializers.py
0 → 100644
View file @
bbd27340
import
collections
from
django.db.models
import
functions
,
Count
from
rest_framework
import
serializers
class
GetArtistsSerializer
(
serializers
.
Serializer
):
def
to_representation
(
self
,
queryset
):
payload
=
{
'ignoredArticles'
:
''
,
'index'
:
[]
}
queryset
=
queryset
.
annotate
(
_albums_count
=
Count
(
'albums'
))
queryset
=
queryset
.
order_by
(
functions
.
Lower
(
'name'
))
values
=
queryset
.
values
(
'id'
,
'_albums_count'
,
'name'
)
first_letter_mapping
=
collections
.
defaultdict
(
list
)
for
artist
in
values
:
first_letter_mapping
[
artist
[
'name'
][
0
].
upper
()].
append
(
artist
)
for
letter
,
artists
in
sorted
(
first_letter_mapping
.
items
()):
letter_data
=
{
'name'
:
letter
,
'artist'
:
[
{
'id'
:
v
[
'id'
],
'name'
:
v
[
'name'
],
'albumCount'
:
v
[
'_albums_count'
]
}
for
v
in
artists
]
}
payload
[
'index'
].
append
(
letter_data
)
return
payload
class
GetArtistSerializer
(
serializers
.
Serializer
):
def
to_representation
(
self
,
artist
):
albums
=
artist
.
albums
.
prefetch_related
(
'tracks__files'
)
payload
=
{
'id'
:
artist
.
pk
,
'name'
:
artist
.
name
,
'albumCount'
:
len
(
albums
),
'album'
:
[],
}
for
album
in
albums
:
album_data
=
{
'id'
:
album
.
id
,
'artistId'
:
artist
.
id
,
'name'
:
album
.
title
,
'artist'
:
artist
.
name
,
'created'
:
album
.
creation_date
,
'songCount'
:
len
(
album
.
tracks
.
all
())
}
if
album
.
release_date
:
album_data
[
'year'
]
=
album
.
release_date
.
year
payload
[
'album'
].
append
(
album_data
)
return
payload
class
GetAlbumSerializer
(
serializers
.
Serializer
):
def
to_representation
(
self
,
album
):
tracks
=
album
.
tracks
.
prefetch_related
(
'files'
)
payload
=
{
'id'
:
album
.
id
,
'artistId'
:
album
.
artist
.
id
,
'name'
:
album
.
title
,
'artist'
:
album
.
artist
.
name
,
'created'
:
album
.
creation_date
,
'songCount'
:
len
(
tracks
),
'song'
:
[],
}
if
album
.
release_date
:
payload
[
'year'
]
=
album
.
release_date
.
year
for
track
in
tracks
:
try
:
tf
=
[
tf
for
tf
in
track
.
files
.
all
()][
0
]
except
IndexError
:
continue
track_data
=
{
'id'
:
track
.
pk
,
'isDir'
:
False
,
'title'
:
track
.
title
,
'album'
:
album
.
title
,
'artist'
:
album
.
artist
.
name
,
'track'
:
track
.
position
,
'contentType'
:
tf
.
mimetype
,
'suffix'
:
tf
.
extension
,
'duration'
:
tf
.
duration
,
'created'
:
track
.
creation_date
,
'albumId'
:
album
.
pk
,
'artistId'
:
album
.
artist
.
pk
,
'type'
:
'music'
,
}
if
album
.
release_date
:
track_data
[
'year'
]
=
album
.
release_date
.
year
payload
[
'song'
].
append
(
track_data
)
return
payload
api/funkwhale_api/subsonic/views.py
0 → 100644
View file @
bbd27340
from
rest_framework
import
exceptions
from
rest_framework
import
permissions
as
rest_permissions
from
rest_framework
import
response
from
rest_framework
import
viewsets
from
rest_framework.decorators
import
list_route
from
rest_framework.serializers
import
ValidationError
from
funkwhale_api.music
import
models
as
music_models
from
funkwhale_api.music
import
views
as
music_views
from
.
import
authentication
from
.
import
negotiation
from
.
import
serializers
def
find_object
(
queryset
,
model_field
=
'pk'
,
field
=
'id'
,
cast
=
int
):
def
decorator
(
func
):
def
inner
(
self
,
request
,
*
args
,
**
kwargs
):
data
=
request
.
GET
or
request
.
POST
try
:
raw_value
=
data
[
field
]
except
KeyError
:
return
response
.
Response
({
'code'
:
10
,
'message'
:
"required parameter '{}' not present"
.
format
(
field
)
})
try
:
value
=
cast
(
raw_value
)
except
(
TypeError
,
ValidationError
):
return
response
.
Response
({
'code'
:
0
,
'message'
:
'For input string "{}"'
.
format
(
raw_value
)
})
try
:
obj
=
queryset
.
get
(
**
{
model_field
:
value
})
except
queryset
.
model
.
DoesNotExist
:
return
response
.
Response
({
'code'
:
70
,
'message'
:
'{} not found'
.
format
(
queryset
.
model
.
__class__
.
__name__
)
})
kwargs
[
'obj'
]
=
obj
return
func
(
self
,
request
,
*
args
,
**
kwargs
)
return
inner
return
decorator
class
SubsonicViewSet
(
viewsets
.
GenericViewSet
):
content_negotiation_class
=
negotiation
.
SubsonicContentNegociation
authentication_classes
=
[
authentication
.
SubsonicAuthentication
]
permissions_classes
=
[
rest_permissions
.
IsAuthenticated
]
def
handle_exception
(
self
,
exc
):
# subsonic API sends 200 status code with custom error
# codes in the payload
mapping
=
{
exceptions
.
AuthenticationFailed
:
(
40
,
'Wrong username or password.'
)
}
payload
=
{
'status'
:
'failed'
}
try
:
code
,
message
=
mapping
[
exc
.
__class__
]
except
KeyError
:
return
super
().
handle_exception
(
exc
)
else
:
payload
[
'error'
]
=
{
'code'
:
code
,
'message'
:
message
}
return
response
.
Response
(
payload
,
status
=
200
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
permission_classes
=
[])
def
ping
(
self
,
request
,
*
args
,
**
kwargs
):
data
=
{
'status'
:
'ok'
,
'version'
:
'1.16.0'
}
return
response
.
Response
(
data
,
status
=
200
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'get_artists'
,
url_path
=
'getArtists'
)
def
get_artists
(
self
,
request
,
*
args
,
**
kwargs
):
artists
=
music_models
.
Artist
.
objects
.
all
()
data
=
serializers
.
GetArtistsSerializer
(
artists
).
data
payload
=
{
'artists'
:
data
}
return
response
.
Response
(
payload
,
status
=
200
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'get_artist'
,
url_path
=
'getArtist'
)
@
find_object
(
music_models
.
Artist
.
objects
.
all
())
def
get_artist
(
self
,
request
,
*
args
,
**
kwargs
):
artist
=
kwargs
.
pop
(
'obj'
)
data
=
serializers
.
GetArtistSerializer
(
artist
).
data
payload
=
{
'artist'
:
data
}
return
response
.
Response
(
payload
,
status
=
200
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'get_album'
,
url_path
=
'getAlbum'
)
@
find_object
(
music_models
.
Album
.
objects
.
select_related
(
'artist'
))
def
get_album
(
self
,
request
,
*
args
,
**
kwargs
):
album
=
kwargs
.
pop
(
'obj'
)
data
=
serializers
.
GetAlbumSerializer
(
album
).
data
payload
=
{
'album'
:
data
}
return
response
.
Response
(
payload
,
status
=
200
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'stream'
,
url_path
=
'stream'
)
@
find_object
(
music_models
.
Track
.
objects
.
all
())
def
stream
(
self
,
request
,
*
args
,
**
kwargs
):
track
=
kwargs
.
pop
(
'obj'
)
queryset
=
track
.
files
.
select_related
(
'library_track'
,
'track__album__artist'
,
'track__artist'
,
)
track_file
=
queryset
.
first
()
if
not
track_file
:
return
Response
(
status
=
404
)
return
music_views
.
handle_serve
(
track_file
)
api/tests/conftest.py
View file @
bbd27340
...
...
@@ -130,6 +130,7 @@ def logged_in_api_client(db, factories, api_client):
"""
user
=
factories
[
'users.User'
]()
assert
api_client
.
login
(
username
=
user
.
username
,
password
=
'test'
)
api_client
.
force_authenticate
(
user
=
user
)
setattr
(
api_client
,
'user'
,
user
)
yield
api_client
delattr
(
api_client
,
'user'
)
...
...
api/tests/subsonic/test_authentication.py
0 → 100644
View file @
bbd27340
import
binascii
from
funkwhale_api.subsonic
import
authentication
def
test_auth_with_salt
(
api_request
,
factories
):
salt
=
'salt'
user
=
factories
[
'users.User'
]()
user
.
subsonic_api_token
=
'password'
user
.
save
()
token
=
authentication
.
get_token
(
salt
,
'password'
)
request
=
api_request
.
get
(
'/'
,
{
't'
:
token
,
's'
:
salt
,
'u'
:
user
.
username
})
authenticator
=
authentication
.
SubsonicAuthentication
()
u
,
_
=
authenticator
.
authenticate
(
request
)
assert
user
==
u
def
test_auth_with_password_hex
(
api_request
,
factories
):
salt
=
'salt'
user
=
factories
[
'users.User'
]()
user
.
subsonic_api_token
=
'password'
user
.
save
()
token
=
authentication
.
get_token
(
salt
,
'password'
)
request
=
api_request
.
get
(
'/'
,
{
'u'
:
user
.
username
,
'p'
:
'enc:{}'
.
format
(
binascii
.
hexlify
(
user
.
subsonic_api_token
.
encode
(
'utf-8'
)).
decode
(
'utf-8'
))
})
authenticator
=
authentication
.
SubsonicAuthentication
()
u
,
_
=
authenticator
.
authenticate
(
request
)
assert
user
==
u
def
test_auth_with_password_cleartext
(
api_request
,
factories
):
salt
=
'salt'
user
=
factories
[
'users.User'
]()
user
.
subsonic_api_token
=
'password'
user
.
save
()
token
=
authentication
.
get_token
(
salt
,
'password'
)
request
=
api_request
.
get
(
'/'
,
{
'u'
:
user
.
username
,
'p'
:
'password'
,
})
authenticator
=
authentication
.
SubsonicAuthentication
()
u
,
_
=
authenticator
.
authenticate
(
request
)
assert
user
==
u
api/tests/subsonic/test_renderers.py
0 → 100644
View file @
bbd27340
import
json
import
xml.etree.ElementTree
as
ET