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
e31099ef
Verified
Commit
e31099ef
authored
May 08, 2018
by
Agate
💬
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
See #75 more subsonic api endpoints (star, unstar, search...)
parent
40cde0cd
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
433 additions
and
43 deletions
+433
-43
api/funkwhale_api/subsonic/filters.py
api/funkwhale_api/subsonic/filters.py
+23
-0
api/funkwhale_api/subsonic/serializers.py
api/funkwhale_api/subsonic/serializers.py
+91
-39
api/funkwhale_api/subsonic/views.py
api/funkwhale_api/subsonic/views.py
+168
-1
api/tests/subsonic/test_serializers.py
api/tests/subsonic/test_serializers.py
+29
-3
api/tests/subsonic/test_views.py
api/tests/subsonic/test_views.py
+122
-0
No files found.
api/funkwhale_api/subsonic/filters.py
0 → 100644
View file @
e31099ef
from
django_filters
import
rest_framework
as
filters
from
funkwhale_api.music
import
models
as
music_models
class
AlbumList2FilterSet
(
filters
.
FilterSet
):
type
=
filters
.
CharFilter
(
name
=
'_'
,
method
=
'filter_type'
)
class
Meta
:
model
=
music_models
.
Album
fields
=
[
'type'
]
def
filter_type
(
self
,
queryset
,
name
,
value
):
ORDERING
=
{
'random'
:
'?'
,
'newest'
:
'-creation_date'
,
'alphabeticalByArtist'
:
'artist__name'
,
'alphabeticalByName'
:
'title'
,
}
if
value
not
in
ORDERING
:
return
queryset
return
queryset
.
order_by
(
ORDERING
[
value
])
api/funkwhale_api/subsonic/serializers.py
View file @
e31099ef
...
...
@@ -4,6 +4,16 @@ from django.db.models import functions, Count
from
rest_framework
import
serializers
from
funkwhale_api.music
import
models
as
music_models
def
get_artist_data
(
artist_values
):
return
{
'id'
:
artist_values
[
'id'
],
'name'
:
artist_values
[
'name'
],
'albumCount'
:
artist_values
[
'_albums_count'
]
}
class
GetArtistsSerializer
(
serializers
.
Serializer
):
def
to_representation
(
self
,
queryset
):
...
...
@@ -11,7 +21,7 @@ class GetArtistsSerializer(serializers.Serializer):
'ignoredArticles'
:
''
,
'index'
:
[]
}
queryset
=
queryset
.
annotate
(
_albums_count
=
Count
(
'albums'
)
)
queryset
=
queryset
.
with_albums_count
(
)
queryset
=
queryset
.
order_by
(
functions
.
Lower
(
'name'
))
values
=
queryset
.
values
(
'id'
,
'_albums_count'
,
'name'
)
...
...
@@ -23,11 +33,7 @@ class GetArtistsSerializer(serializers.Serializer):
letter_data
=
{
'name'
:
letter
,
'artist'
:
[
{
'id'
:
v
[
'id'
],
'name'
:
v
[
'name'
],
'albumCount'
:
v
[
'_albums_count'
]
}
get_artist_data
(
v
)
for
v
in
artists
]
}
...
...
@@ -59,42 +65,88 @@ class GetArtistSerializer(serializers.Serializer):
return
payload
def
get_track_data
(
album
,
track
,
tf
):
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
or
''
,
'duration'
:
tf
.
duration
or
0
,
'created'
:
track
.
creation_date
,
'albumId'
:
album
.
pk
,
'artistId'
:
album
.
artist
.
pk
,
'type'
:
'music'
,
}
if
album
.
release_date
:
data
[
'year'
]
=
album
.
release_date
.
year
return
data
def
get_album2_data
(
album
):
payload
=
{
'id'
:
album
.
id
,
'artistId'
:
album
.
artist
.
id
,
'name'
:
album
.
title
,
'artist'
:
album
.
artist
.
name
,
'created'
:
album
.
creation_date
,
}
try
:
payload
[
'songCount'
]
=
album
.
_tracks_count
except
AttributeError
:
payload
[
'songCount'
]
=
len
(
album
.
tracks
.
prefetch_related
(
'files'
))
return
payload
def
get_song_list_data
(
tracks
):
songs
=
[]
for
track
in
tracks
:
try
:
tf
=
[
tf
for
tf
in
track
.
files
.
all
()][
0
]
except
IndexError
:
continue
track_data
=
get_track_data
(
track
.
album
,
track
,
tf
)
songs
.
append
(
track_data
)
return
songs
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'
:
[],
}
tracks
=
album
.
tracks
.
prefetch_related
(
'files'
).
select_related
(
'album'
)
payload
=
get_album2_data
(
album
)
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
)
payload
[
'song'
]
=
get_song_list_data
(
tracks
)
return
payload
def
get_starred_tracks_data
(
favorites
):
by_track_id
=
{
f
.
track_id
:
f
for
f
in
favorites
}
tracks
=
music_models
.
Track
.
objects
.
filter
(
pk__in
=
by_track_id
.
keys
()
).
select_related
(
'album__artist'
).
prefetch_related
(
'files'
)
tracks
=
tracks
.
order_by
(
'-creation_date'
)
data
=
[]
for
t
in
tracks
:
try
:
tf
=
[
tf
for
tf
in
t
.
files
.
all
()][
0
]
except
IndexError
:
continue
td
=
get_track_data
(
t
.
album
,
t
,
tf
)
td
[
'starred'
]
=
by_track_id
[
t
.
pk
].
creation_date
data
.
append
(
td
)
return
data
def
get_album_list2_data
(
albums
):
return
[
get_album2_data
(
a
)
for
a
in
albums
]
api/funkwhale_api/subsonic/views.py
View file @
e31099ef
import
datetime
from
django.utils
import
timezone
from
rest_framework
import
exceptions
from
rest_framework
import
permissions
as
rest_permissions
from
rest_framework
import
response
...
...
@@ -5,10 +9,13 @@ from rest_framework import viewsets
from
rest_framework.decorators
import
list_route
from
rest_framework.serializers
import
ValidationError
from
funkwhale_api.favorites.models
import
TrackFavorite
from
funkwhale_api.music
import
models
as
music_models
from
funkwhale_api.music
import
utils
from
funkwhale_api.music
import
views
as
music_views
from
.
import
authentication
from
.
import
filters
from
.
import
negotiation
from
.
import
serializers
...
...
@@ -83,6 +90,24 @@ class SubsonicViewSet(viewsets.GenericViewSet):
}
return
response
.
Response
(
data
,
status
=
200
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'get_license'
,
permissions_classes
=
[],
url_path
=
'getLicense'
)
def
get_license
(
self
,
request
,
*
args
,
**
kwargs
):
now
=
timezone
.
now
()
data
=
{
'status'
:
'ok'
,
'version'
:
'1.16.0'
,
'license'
:
{
'valid'
:
'true'
,
'email'
:
'valid@valid.license'
,
'licenseExpires'
:
now
+
datetime
.
timedelta
(
days
=
365
)
}
}
return
response
.
Response
(
data
,
status
=
200
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'get_artists'
,
...
...
@@ -110,6 +135,19 @@ class SubsonicViewSet(viewsets.GenericViewSet):
return
response
.
Response
(
payload
,
status
=
200
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'get_artist_info2'
,
url_path
=
'getArtistInfo2'
)
@
find_object
(
music_models
.
Artist
.
objects
.
all
())
def
get_artist_info2
(
self
,
request
,
*
args
,
**
kwargs
):
artist
=
kwargs
.
pop
(
'obj'
)
payload
=
{
'artist-info2'
:
{}
}
return
response
.
Response
(
payload
,
status
=
200
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'get_album'
,
...
...
@@ -139,5 +177,134 @@ class SubsonicViewSet(viewsets.GenericViewSet):
)
track_file
=
queryset
.
first
()
if
not
track_file
:
return
Response
(
status
=
404
)
return
response
.
Response
(
status
=
404
)
return
music_views
.
handle_serve
(
track_file
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'star'
,
url_path
=
'star'
)
@
find_object
(
music_models
.
Track
.
objects
.
all
())
def
star
(
self
,
request
,
*
args
,
**
kwargs
):
track
=
kwargs
.
pop
(
'obj'
)
TrackFavorite
.
add
(
user
=
request
.
user
,
track
=
track
)
return
response
.
Response
({
'status'
:
'ok'
})
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'unstar'
,
url_path
=
'unstar'
)
@
find_object
(
music_models
.
Track
.
objects
.
all
())
def
unstar
(
self
,
request
,
*
args
,
**
kwargs
):
track
=
kwargs
.
pop
(
'obj'
)
request
.
user
.
track_favorites
.
filter
(
track
=
track
).
delete
()
return
response
.
Response
({
'status'
:
'ok'
})
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'get_starred2'
,
url_path
=
'getStarred2'
)
def
get_starred2
(
self
,
request
,
*
args
,
**
kwargs
):
favorites
=
request
.
user
.
track_favorites
.
all
()
data
=
{
'song'
:
serializers
.
get_starred_tracks_data
(
favorites
)
}
return
response
.
Response
(
data
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'get_album_list2'
,
url_path
=
'getAlbumList2'
)
def
get_album_list2
(
self
,
request
,
*
args
,
**
kwargs
):
queryset
=
music_models
.
Album
.
objects
.
with_tracks_count
()
data
=
request
.
GET
or
request
.
POST
filterset
=
filters
.
AlbumList2FilterSet
(
data
,
queryset
=
queryset
)
queryset
=
filterset
.
qs
try
:
offset
=
int
(
data
[
'offset'
])
except
(
TypeError
,
KeyError
,
ValueError
):
offset
=
0
try
:
size
=
int
(
data
[
'size'
])
except
(
TypeError
,
KeyError
,
ValueError
):
size
=
50
size
=
min
(
size
,
500
)
queryset
=
queryset
[
offset
:
size
]
data
=
{
'albumList2'
:
{
'album'
:
serializers
.
get_album_list2_data
(
queryset
)
}
}
return
response
.
Response
(
data
)
@
list_route
(
methods
=
[
'get'
,
'post'
],
url_name
=
'search3'
,
url_path
=
'search3'
)
def
search3
(
self
,
request
,
*
args
,
**
kwargs
):
data
=
request
.
GET
or
request
.
POST
query
=
str
(
data
.
get
(
'query'
,
''
)).
replace
(
'*'
,
''
)
conf
=
[
{
'subsonic'
:
'artist'
,
'search_fields'
:
[
'name'
],
'queryset'
:
(
music_models
.
Artist
.
objects
.
with_albums_count
()
.
values
(
'id'
,
'_albums_count'
,
'name'
)
),
'serializer'
:
lambda
qs
:
[
serializers
.
get_artist_data
(
a
)
for
a
in
qs
]
},
{
'subsonic'
:
'album'
,
'search_fields'
:
[
'title'
],
'queryset'
:
(
music_models
.
Album
.
objects
.
with_tracks_count
()
.
select_related
(
'artist'
)
),
'serializer'
:
serializers
.
get_album_list2_data
,
},
{
'subsonic'
:
'song'
,
'search_fields'
:
[
'title'
],
'queryset'
:
(
music_models
.
Track
.
objects
.
prefetch_related
(
'files'
)
.
select_related
(
'album__artist'
)
),
'serializer'
:
serializers
.
get_song_list_data
,
},
]
payload
=
{
'searchResult3'
:
{}
}
for
c
in
conf
:
offsetKey
=
'{}Offset'
.
format
(
c
[
'subsonic'
])
countKey
=
'{}Count'
.
format
(
c
[
'subsonic'
])
try
:
offset
=
int
(
data
[
offsetKey
])
except
(
TypeError
,
KeyError
,
ValueError
):
offset
=
0
try
:
size
=
int
(
data
[
countKey
])
except
(
TypeError
,
KeyError
,
ValueError
):
size
=
20
size
=
min
(
size
,
100
)
queryset
=
c
[
'queryset'
]
if
query
:
queryset
=
c
[
'queryset'
].
filter
(
utils
.
get_query
(
query
,
c
[
'search_fields'
])
)
queryset
=
queryset
[
offset
:
size
]
payload
[
'searchResult3'
][
c
[
'subsonic'
]]
=
c
[
'serializer'
](
queryset
)
return
response
.
Response
(
payload
)
api/tests/subsonic/test_serializers.py
View file @
e31099ef
from
funkwhale_api.music
import
models
as
music_models
from
funkwhale_api.subsonic
import
serializers
...
...
@@ -89,15 +90,15 @@ def test_get_album_serializer(factories):
'song'
:
[
{
'id'
:
track
.
pk
,
'isDir'
:
False
,
'isDir'
:
'false'
,
'title'
:
track
.
title
,
'album'
:
album
.
title
,
'artist'
:
artist
.
name
,
'track'
:
track
.
position
,
'year'
:
track
.
album
.
release_date
.
year
,
'contentType'
:
tf
.
mimetype
,
'suffix'
:
tf
.
extension
,
'duration'
:
tf
.
duration
,
'suffix'
:
tf
.
extension
or
''
,
'duration'
:
tf
.
duration
or
0
,
'created'
:
track
.
creation_date
,
'albumId'
:
album
.
pk
,
'artistId'
:
artist
.
pk
,
...
...
@@ -107,3 +108,28 @@ def test_get_album_serializer(factories):
}
assert
serializers
.
GetAlbumSerializer
(
album
).
data
==
expected
def
test_starred_tracks2_serializer
(
factories
):
artist
=
factories
[
'music.Artist'
]()
album
=
factories
[
'music.Album'
](
artist
=
artist
)
track
=
factories
[
'music.Track'
](
album
=
album
)
tf
=
factories
[
'music.TrackFile'
](
track
=
track
)
favorite
=
factories
[
'favorites.TrackFavorite'
](
track
=
track
)
expected
=
[
serializers
.
get_track_data
(
album
,
track
,
tf
)]
expected
[
0
][
'starred'
]
=
favorite
.
creation_date
data
=
serializers
.
get_starred_tracks_data
([
favorite
])
assert
data
==
expected
def
test_get_album_list2_serializer
(
factories
):
album1
=
factories
[
'music.Album'
]()
album2
=
factories
[
'music.Album'
]()
qs
=
music_models
.
Album
.
objects
.
with_tracks_count
().
order_by
(
'pk'
)
expected
=
[
serializers
.
get_album2_data
(
album1
),
serializers
.
get_album2_data
(
album2
),
]
data
=
serializers
.
get_album_list2_data
(
qs
)
assert
data
==
expected
api/tests/subsonic/test_views.py
View file @
e31099ef
import
datetime
import
json
import
pytest
from
django.utils
import
timezone
from
django.urls
import
reverse
from
rest_framework.response
import
Response
from
funkwhale_api.music
import
models
as
music_models
...
...
@@ -42,6 +45,26 @@ def test_exception_wrong_credentials(f, db, api_client):
assert
response
.
data
==
expected
@
pytest
.
mark
.
parametrize
(
'f'
,
[
'xml'
,
'json'
])
def
test_get_license
(
f
,
db
,
logged_in_api_client
,
mocker
):
url
=
reverse
(
'api:subsonic-get-license'
)
assert
url
.
endswith
(
'getLicense'
)
is
True
now
=
timezone
.
now
()
mocker
.
patch
(
'django.utils.timezone.now'
,
return_value
=
now
)
response
=
logged_in_api_client
.
get
(
url
,
{
'f'
:
f
})
expected
=
{
'status'
:
'ok'
,
'version'
:
'1.16.0'
,
'license'
:
{
'valid'
:
'true'
,
'email'
:
'valid@valid.license'
,
'licenseExpires'
:
now
+
datetime
.
timedelta
(
days
=
365
)
}
}
assert
response
.
status_code
==
200
assert
response
.
data
==
expected
@
pytest
.
mark
.
parametrize
(
'f'
,
[
'xml'
,
'json'
])
def
test_ping
(
f
,
db
,
api_client
):
url
=
reverse
(
'api:subsonic-ping'
)
...
...
@@ -86,6 +109,21 @@ def test_get_artist(f, db, logged_in_api_client, factories):
assert
response
.
data
==
expected
@
pytest
.
mark
.
parametrize
(
'f'
,
[
'xml'
,
'json'
])
def
test_get_artist_info2
(
f
,
db
,
logged_in_api_client
,
factories
):
url
=
reverse
(
'api:subsonic-get-artist-info2'
)
assert
url
.
endswith
(
'getArtistInfo2'
)
is
True
artist
=
factories
[
'music.Artist'
]()
expected
=
{
'artist-info2'
:
{}
}
response
=
logged_in_api_client
.
get
(
url
,
{
'id'
:
artist
.
pk
})
assert
response
.
status_code
==
200
assert
response
.
data
==
expected
@
pytest
.
mark
.
parametrize
(
'f'
,
[
'xml'
,
'json'
])
def
test_get_album
(
f
,
db
,
logged_in_api_client
,
factories
):
url
=
reverse
(
'api:subsonic-get-album'
)
...
...
@@ -118,3 +156,87 @@ def test_stream(f, db, logged_in_api_client, factories, mocker):
track_file
=
tf
)
assert
response
.
status_code
==
200
@
pytest
.
mark
.
parametrize
(
'f'
,
[
'xml'
,
'json'
])
def
test_star
(
f
,
db
,
logged_in_api_client
,
factories
):
url
=
reverse
(
'api:subsonic-star'
)
assert
url
.
endswith
(
'star'
)
is
True
track
=
factories
[
'music.Track'
]()
response
=
logged_in_api_client
.
get
(
url
,
{
'f'
:
f
,
'id'
:
track
.
pk
})
assert
response
.
status_code
==
200
assert
response
.
data
==
{
'status'
:
'ok'
}
favorite
=
logged_in_api_client
.
user
.
track_favorites
.
latest
(
'id'
)
assert
favorite
.
track
==
track
@
pytest
.
mark
.
parametrize
(
'f'
,
[
'xml'
,
'json'
])
def
test_unstar
(
f
,
db
,
logged_in_api_client
,
factories
):
url
=
reverse
(
'api:subsonic-unstar'
)
assert
url
.
endswith
(
'unstar'
)
is
True
track
=
factories
[
'music.Track'
]()
favorite
=
factories
[
'favorites.TrackFavorite'
](
track
=
track
,
user
=
logged_in_api_client
.
user
)
response
=
logged_in_api_client
.
get
(
url
,
{
'f'
:
f
,
'id'
:
track
.
pk
})
assert
response
.
status_code
==
200
assert
response
.
data
==
{
'status'
:
'ok'
}
assert
logged_in_api_client
.
user
.
track_favorites
.
count
()
==
0
@
pytest
.
mark
.
parametrize
(
'f'
,
[
'xml'
,
'json'
])
def
test_get_starred2
(
f
,
db
,
logged_in_api_client
,
factories
):
url
=
reverse
(
'api:subsonic-get-starred2'
)
assert
url
.
endswith
(
'getStarred2'
)
is
True
track
=
factories
[
'music.Track'
]()
favorite
=
factories
[
'favorites.TrackFavorite'
](
track
=
track
,
user
=
logged_in_api_client
.
user
)
response
=
logged_in_api_client
.
get
(
url
,
{
'f'
:
f
,
'id'
:
track
.
pk
})
assert
response
.
status_code
==
200
assert
response
.
data
==
{
'song'
:
serializers
.
get_starred_tracks_data
([
favorite
])
}
@
pytest
.
mark
.
parametrize
(
'f'
,
[
'xml'
,
'json'
])
def
test_get_album_list2
(
f
,
db
,
logged_in_api_client
,
factories
):
url
=
reverse
(
'api:subsonic-get-album-list2'
)
assert
url
.
endswith
(
'getAlbumList2'
)
is
True
album1
=
factories
[
'music.Album'
]()
album2
=
factories
[
'music.Album'
]()
response
=
logged_in_api_client
.
get
(
url
,
{
'f'
:
f
,
'type'
:
'newest'
})
assert
response
.
status_code
==
200
assert
response
.
data
==
{
'albumList2'
:
{
'album'
:
serializers
.
get_album_list2_data
([
album2
,
album1
])
}
}
@
pytest
.
mark
.
parametrize
(
'f'
,
[
'xml'
,
'json'
])
def
test_search3
(
f
,
db
,
logged_in_api_client
,
factories
):
url
=
reverse
(
'api:subsonic-search3'
)
assert
url
.
endswith
(
'search3'
)
is
True
artist
=
factories
[
'music.Artist'
](
name
=
'testvalue'
)
factories
[
'music.Artist'
](
name
=
'nope'
)
album
=
factories
[
'music.Album'
](
title
=
'testvalue'
)
factories
[
'music.Album'
](
title
=
'nope'
)
track
=
factories
[
'music.Track'
](
title
=
'testvalue'
)
factories
[
'music.Track'
](
title
=
'nope'
)
response
=
logged_in_api_client
.
get
(
url
,
{
'f'
:
f
,
'query'
:
'testval'
})
artist_qs
=
music_models
.
Artist
.
objects
.
with_albums_count
().
filter
(
pk
=
artist
.
pk
).
values
(
'_albums_count'
,
'id'
,
'name'
)
assert
response
.
status_code
==
200
assert
response
.
data
==
{
'searchResult3'
:
{
'artist'
:
[
serializers
.
get_artist_data
(
a
)
for
a
in
artist_qs
],
'album'
:
serializers
.
get_album_list2_data
([
album
]),
'song'
:
serializers
.
get_song_list_data
([
track
]),
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment