Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
jovuit
funkwhale
Commits
7b18e46f
Verified
Commit
7b18e46f
authored
May 19, 2018
by
Eliot Berriot
Browse files
Merge branch 'release/0.13'
parents
107cca7b
d299964c
Changes
80
Hide whitespace changes
Inline
Side-by-side
.env.dev
View file @
7b18e46f
...
...
@@ -9,3 +9,5 @@ FUNKWHALE_PROTOCOL=http
PYTHONDONTWRITEBYTECODE=true
WEBPACK_DEVSERVER_PORT=8080
MUSIC_DIRECTORY_PATH=/music
BROWSABLE_API_ENABLED=True
CACHEOPS_ENABLED=False
CHANGELOG
View file @
7b18e46f
...
...
@@ -10,6 +10,127 @@ This changelog is viewable on the web at https://docs.funkwhale.audio/changelog.
.. towncrier
0.13 (2018-05-19)
-----------------
Upgrade instructions are available at
https://docs.funkwhale.audio/upgrading.html
Features:
- Can now import and play flac files (#157)
- Simpler permission system (#152)
- Store file length, size and bitrate (#195)
- We now have a brand new instance settings interface in the front-end (#206)
Enhancements:
- Disabled browsable HTML API in production (#205)
- Instances can now indicate on the nodeinfo endpoint if they want to remain
private (#200)
Bugfixes:
- .well-known/nodeinfo endpoint can now answer to request with Accept:
application/json (#197)
- Fixed escaping issue of track name in playlist modal (#201)
- Fixed missing dot when downloading file (#204)
- In-place imported tracks with non-ascii characters don't break reverse-proxy
serving (#196)
- Removed Python 3.6 dependency (secrets module) (#198)
- Uplayable tracks are now properly disabled in the interface (#199)
Instance settings interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Prior to this release, the only way to update instance settings (such as
instance description, signup policy, federation configuration, etc.) was using
the admin interface provided by Django (the back-end framework which power the API).
This interface worked, but was not really-user friendly and intuitive.
Starting from this release, we now offer a dedicated interface directly
in the front-end. You can view and edit all your instance settings from here,
assuming you have the required permissions.
This interface is available at ``/manage/settings` and via link in the sidebar.
Storage of bitrate, size and length in database
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Starting with this release, when importing files, Funkwhale will store
additional information about audio files:
- Bitrate
- Size (in bytes)
- Duration
This change is not retroactive, meaning already imported files will lack those
informations. The interface and API should work as before in such case, however,
we offer a command to deal with legacy files and populate the missing values.
On docker setups:
.. code-block:: shell
docker-compose run --rm api python manage.py fix_track_files
On non-docker setups:
.. code-block:: shell
# from your activated virtualenv
python manage.py fix_track_files
.. note::
The execution time for this command is proportional to the number of
audio files stored on your instance. This is because we need to read the
files from disk to fetch the data. You can run it in the background
while Funkwhale is up.
It's also safe to interrupt this command and rerun it at a later point, or run
it multiple times.
Use the --dry-run flag to check how many files would be impacted.
Simpler permission system
^^^^^^^^^^^^^^^^^^^^^^^^^
Starting from this release, the permission system is much simpler. Up until now,
we were using Django's built-in permission system, which was working, but also
quite complex to deal with.
The new implementation relies on simpler logic, which will make integration
on the front-end in upcoming releases faster and easier.
If you have manually given permissions to users on your instance,
you can migrate those to the new system.
On docker setups:
.. code-block:: shell
docker-compose run --rm api python manage.py script django_permissions_to_user_permissions --no-input
On non-docker setups:
.. code-block:: shell
# in your virtualenv
python api/manage.py script django_permissions_to_user_permissions --no-input
There is still no dedicated interface to manage user permissions, but you
can use the admin interface at ``/api/admin/users/user/`` for that purpose in
the meantime.
0.12 (2018-05-09)
-----------------
...
...
@@ -110,9 +231,7 @@ We offer two settings to manage nodeinfo in your Funkwhale instance:
and user activity.
To make your instance fully compatible with the nodeinfo protocol, you need to
to edit your nginx configuration file:
.. code-block::
to edit your nginx configuration file::
# before
...
...
...
@@ -130,9 +249,7 @@ to edit your nginx configuration file:
}
...
You can do the same if you use apache:
.. code-block::
You can do the same if you use apache::
# before
...
...
...
api/config/settings/common.py
View file @
7b18e46f
...
...
@@ -406,8 +406,18 @@ REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS'
:
(
'rest_framework.filters.OrderingFilter'
,
'django_filters.rest_framework.DjangoFilterBackend'
,
),
'DEFAULT_RENDERER_CLASSES'
:
(
'rest_framework.renderers.JSONRenderer'
,
)
}
BROWSABLE_API_ENABLED
=
env
.
bool
(
'BROWSABLE_API_ENABLED'
,
default
=
False
)
if
BROWSABLE_API_ENABLED
:
REST_FRAMEWORK
[
'DEFAULT_RENDERER_CLASSES'
]
+=
(
'rest_framework.renderers.BrowsableAPIRenderer'
,
)
REST_AUTH_SERIALIZERS
=
{
'PASSWORD_RESET_SERIALIZER'
:
'funkwhale_api.users.serializers.PasswordResetSerializer'
# noqa
}
...
...
api/funkwhale_api/__init__.py
View file @
7b18e46f
# -*- coding: utf-8 -*-
__version__
=
'0.1
2
'
__version__
=
'0.1
3
'
__version_info__
=
tuple
([
int
(
num
)
if
num
.
isdigit
()
else
num
for
num
in
__version__
.
replace
(
'-'
,
'.'
,
1
).
split
(
'.'
)])
api/funkwhale_api/common/dynamic_preferences_registry.py
View file @
7b18e46f
...
...
@@ -16,5 +16,5 @@ class APIAutenticationRequired(
help_text
=
(
'If disabled, anonymous users will be able to query the API'
'and access music data (as well as other data exposed in the API '
'without specific permissions)'
'without specific permissions)
.
'
)
api/funkwhale_api/common/management/__init__.py
0 → 100644
View file @
7b18e46f
api/funkwhale_api/common/management/commands/__init__.py
0 → 100644
View file @
7b18e46f
api/funkwhale_api/common/management/commands/script.py
0 → 100644
View file @
7b18e46f
from
django.core.management.base
import
BaseCommand
,
CommandError
from
funkwhale_api.common
import
scripts
class
Command
(
BaseCommand
):
help
=
'Run a specific script from funkwhale_api/common/scripts/'
def
add_arguments
(
self
,
parser
):
parser
.
add_argument
(
'script_name'
,
nargs
=
'?'
,
type
=
str
)
parser
.
add_argument
(
'--noinput'
,
'--no-input'
,
action
=
'store_false'
,
dest
=
'interactive'
,
help
=
"Do NOT prompt the user for input of any kind."
,
)
def
handle
(
self
,
*
args
,
**
options
):
name
=
options
[
'script_name'
]
if
not
name
:
self
.
show_help
()
available_scripts
=
self
.
get_scripts
()
try
:
script
=
available_scripts
[
name
]
except
KeyError
:
raise
CommandError
(
'{} is not a valid script. Run python manage.py script for a '
'list of available scripts'
.
format
(
name
))
self
.
stdout
.
write
(
''
)
if
options
[
'interactive'
]:
message
=
(
'Are you sure you want to execute the script {}?
\n\n
'
"Type 'yes' to continue, or 'no' to cancel: "
).
format
(
name
)
if
input
(
''
.
join
(
message
))
!=
'yes'
:
raise
CommandError
(
"Script cancelled."
)
script
[
'entrypoint'
](
self
,
**
options
)
def
show_help
(
self
):
indentation
=
4
self
.
stdout
.
write
(
''
)
self
.
stdout
.
write
(
'Available scripts:'
)
self
.
stdout
.
write
(
'Launch with: python manage.py <script_name>'
)
available_scripts
=
self
.
get_scripts
()
for
name
,
script
in
sorted
(
available_scripts
.
items
()):
self
.
stdout
.
write
(
''
)
self
.
stdout
.
write
(
self
.
style
.
SUCCESS
(
name
))
self
.
stdout
.
write
(
''
)
for
line
in
script
[
'help'
].
splitlines
():
self
.
stdout
.
write
(
' {}'
.
format
(
line
))
self
.
stdout
.
write
(
''
)
def
get_scripts
(
self
):
available_scripts
=
[
k
for
k
in
sorted
(
scripts
.
__dict__
.
keys
())
if
not
k
.
startswith
(
'__'
)
]
data
=
{}
for
name
in
available_scripts
:
module
=
getattr
(
scripts
,
name
)
data
[
name
]
=
{
'name'
:
name
,
'help'
:
module
.
__doc__
.
strip
(),
'entrypoint'
:
module
.
main
}
return
data
api/funkwhale_api/common/permissions.py
View file @
7b18e46f
...
...
@@ -3,7 +3,7 @@ import operator
from
django.conf
import
settings
from
django.http
import
Http404
from
rest_framework.permissions
import
BasePermission
,
DjangoModelPermissions
from
rest_framework.permissions
import
BasePermission
from
funkwhale_api.common
import
preferences
...
...
@@ -16,17 +16,6 @@ class ConditionalAuthentication(BasePermission):
return
True
class
HasModelPermission
(
DjangoModelPermissions
):
"""
Same as DjangoModelPermissions, but we pin the model:
class MyModelPermission(HasModelPermission):
model = User
"""
def
get_required_permissions
(
self
,
method
,
model_cls
):
return
super
().
get_required_permissions
(
method
,
self
.
model
)
class
OwnerPermission
(
BasePermission
):
"""
Ensure the request user is the owner of the object.
...
...
api/funkwhale_api/common/scripts/__init__.py
0 → 100644
View file @
7b18e46f
from
.
import
django_permissions_to_user_permissions
from
.
import
test
api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py
0 → 100644
View file @
7b18e46f
"""
Convert django permissions to user permissions in the database,
following the work done in #152.
"""
from
django.db.models
import
Q
from
funkwhale_api.users
import
models
from
django.contrib.auth.models
import
Permission
mapping
=
{
'dynamic_preferences.change_globalpreferencemodel'
:
'settings'
,
'music.add_importbatch'
:
'library'
,
'federation.change_library'
:
'federation'
,
}
def
main
(
command
,
**
kwargs
):
for
codename
,
user_permission
in
sorted
(
mapping
.
items
()):
app_label
,
c
=
codename
.
split
(
'.'
)
p
=
Permission
.
objects
.
get
(
content_type__app_label
=
app_label
,
codename
=
c
)
users
=
models
.
User
.
objects
.
filter
(
Q
(
groups__permissions
=
p
)
|
Q
(
user_permissions
=
p
)).
distinct
()
total
=
users
.
count
()
command
.
stdout
.
write
(
'Updating {} users with {} permission...'
.
format
(
total
,
user_permission
))
users
.
update
(
**
{
'permission_{}'
.
format
(
user_permission
):
True
})
api/funkwhale_api/common/scripts/test.py
0 → 100644
View file @
7b18e46f
"""
This is a test script that does nothing.
You can launch it just to check how it works.
"""
def
main
(
command
,
**
kwargs
):
command
.
stdout
.
write
(
'Test script run successfully'
)
api/funkwhale_api/federation/dynamic_preferences_registry.py
View file @
7b18e46f
...
...
@@ -19,6 +19,9 @@ class MusicCacheDuration(types.IntPreference):
'locally? Federated files that were not listened in this interval '
'will be erased and refetched from the remote on the next listening.'
)
field_kwargs
=
{
'required'
:
False
,
}
@
global_preferences_registry
.
register
...
...
@@ -29,7 +32,7 @@ class Enabled(preferences.DefaultFromSettingMixin, types.BooleanPreference):
verbose_name
=
'Federation enabled'
help_text
=
(
'Use this setting to enable or disable federation logic and API'
' globally'
' globally
.
'
)
...
...
@@ -41,8 +44,11 @@ class CollectionPageSize(
setting
=
'FEDERATION_COLLECTION_PAGE_SIZE'
verbose_name
=
'Federation collection page size'
help_text
=
(
'How much items to display in ActivityPub collections'
'How much items to display in ActivityPub collections
.
'
)
field_kwargs
=
{
'required'
:
False
,
}
@
global_preferences_registry
.
register
...
...
@@ -54,8 +60,11 @@ class ActorFetchDelay(
verbose_name
=
'Federation actor fetch delay'
help_text
=
(
'How much minutes to wait before refetching actors on '
'request authentication'
'request authentication
.
'
)
field_kwargs
=
{
'required'
:
False
,
}
@
global_preferences_registry
.
register
...
...
@@ -66,6 +75,6 @@ class MusicNeedsApproval(
setting
=
'FEDERATION_MUSIC_NEEDS_APPROVAL'
verbose_name
=
'Federation music needs approval'
help_text
=
(
'When true, other federation actors will
require
your approval'
'When true, other federation actors will
need
your approval'
' before being able to browse your library.'
)
api/funkwhale_api/federation/factories.py
View file @
7b18e46f
...
...
@@ -233,6 +233,9 @@ class AudioMetadataFactory(factory.Factory):
release
=
factory
.
LazyAttribute
(
lambda
o
:
'https://musicbrainz.org/release/{}'
.
format
(
uuid
.
uuid4
())
)
bitrate
=
42
length
=
43
size
=
44
class
Meta
:
model
=
dict
...
...
api/funkwhale_api/federation/models.py
View file @
7b18e46f
...
...
@@ -216,3 +216,6 @@ class LibraryTrack(models.Model):
for
chunk
in
r
.
iter_content
(
chunk_size
=
512
):
tmp_file
.
write
(
chunk
)
self
.
audio_file
.
save
(
filename
,
tmp_file
)
def
get_metadata
(
self
,
key
):
return
self
.
metadata
.
get
(
key
)
api/funkwhale_api/federation/serializers.py
View file @
7b18e46f
...
...
@@ -688,6 +688,12 @@ class AudioMetadataSerializer(serializers.Serializer):
artist
=
ArtistMetadataSerializer
()
release
=
ReleaseMetadataSerializer
()
recording
=
RecordingMetadataSerializer
()
bitrate
=
serializers
.
IntegerField
(
required
=
False
,
allow_null
=
True
,
min_value
=
0
)
size
=
serializers
.
IntegerField
(
required
=
False
,
allow_null
=
True
,
min_value
=
0
)
length
=
serializers
.
IntegerField
(
required
=
False
,
allow_null
=
True
,
min_value
=
0
)
class
AudioSerializer
(
serializers
.
Serializer
):
...
...
@@ -760,6 +766,9 @@ class AudioSerializer(serializers.Serializer):
'musicbrainz_id'
:
str
(
track
.
mbid
)
if
track
.
mbid
else
None
,
'title'
:
track
.
title
,
},
'bitrate'
:
instance
.
bitrate
,
'size'
:
instance
.
size
,
'length'
:
instance
.
duration
,
},
'url'
:
{
'href'
:
utils
.
full_url
(
instance
.
path
),
...
...
api/funkwhale_api/federation/views.py
View file @
7b18e46f
...
...
@@ -15,8 +15,8 @@ from rest_framework.serializers import ValidationError
from
funkwhale_api.common
import
preferences
from
funkwhale_api.common
import
utils
as
funkwhale_utils
from
funkwhale_api.common.permissions
import
HasModelPermission
from
funkwhale_api.music.models
import
TrackFile
from
funkwhale_api.users.permissions
import
HasUserPermission
from
.
import
activity
from
.
import
actors
...
...
@@ -88,7 +88,7 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet):
class
WellKnownViewSet
(
viewsets
.
GenericViewSet
):
authentication_classes
=
[]
permission_classes
=
[]
renderer_classes
=
[
renderers
.
WebfingerRenderer
]
renderer_classes
=
[
renderers
.
JSONRenderer
,
renderers
.
WebfingerRenderer
]
@
list_route
(
methods
=
[
'get'
])
def
nodeinfo
(
self
,
request
,
*
args
,
**
kwargs
):
...
...
@@ -187,16 +187,13 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
return
response
.
Response
(
data
)
class
LibraryPermission
(
HasModelPermission
):
model
=
models
.
Library
class
LibraryViewSet
(
mixins
.
RetrieveModelMixin
,
mixins
.
UpdateModelMixin
,
mixins
.
ListModelMixin
,
viewsets
.
GenericViewSet
):
permission_classes
=
[
LibraryPermission
]
permission_classes
=
(
HasUserPermission
,)
required_permissions
=
[
'federation'
]
queryset
=
models
.
Library
.
objects
.
all
().
select_related
(
'actor'
,
'follow'
,
...
...
@@ -291,7 +288,8 @@ class LibraryViewSet(
class
LibraryTrackViewSet
(
mixins
.
ListModelMixin
,
viewsets
.
GenericViewSet
):
permission_classes
=
[
LibraryPermission
]
permission_classes
=
(
HasUserPermission
,)
required_permissions
=
[
'federation'
]
queryset
=
models
.
LibraryTrack
.
objects
.
all
().
select_related
(
'library__actor'
,
'library__follow'
,
...
...
api/funkwhale_api/instance/dynamic_preferences_registry.py
View file @
7b18e46f
...
...
@@ -13,8 +13,11 @@ class InstanceName(types.StringPreference):
section
=
instance
name
=
'name'
default
=
''
help_text
=
'Instance public name'
verbose_name
=
'The public name of your instance'
verbose_name
=
'Public name'
help_text
=
'The public name of your instance, displayed in the about page.'
field_kwargs
=
{
'required'
:
False
,
}
@
global_preferences_registry
.
register
...
...
@@ -23,7 +26,11 @@ class InstanceShortDescription(types.StringPreference):
section
=
instance
name
=
'short_description'
default
=
''
verbose_name
=
'Instance succinct description'
verbose_name
=
'Short description'
help_text
=
'Instance succinct description, displayed in the about page.'
field_kwargs
=
{
'required'
:
False
,
}
@
global_preferences_registry
.
register
...
...
@@ -31,31 +38,31 @@ class InstanceLongDescription(types.StringPreference):
show_in_api
=
True
section
=
instance
name
=
'long_description'
verbose_name
=
'Long description'
default
=
''
help_text
=
'Instance long description (markdown allowed)'
help_text
=
'Instance long description, displayed in the about page (markdown allowed).'
widget
=
widgets
.
Textarea
field_kwargs
=
{
'
widget'
:
widgets
.
Textarea
'
required'
:
False
,
}
@
global_preferences_registry
.
register
class
RavenDSN
(
types
.
StringPreference
):
show_in_api
=
True
section
=
raven
name
=
'front_dsn'
default
=
'https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4'
verbose_name
=
(
'A raven DSN key used to report front-ent errors to '
'a sentry instance'
)
verbose_name
=
'Raven DSN key (front-end)'
help_text
=
(
'Keeping the default one will report errors to funkwhale developers'
'A Raven DSN key used to report front-ent errors to '
'a sentry instance. Keeping the default one will report errors to '
'Funkwhale developers.'
)
SENTRY_HELP_TEXT
=
(
'Error reporting is disabled by default but you can enable it if'
' you want to help us improve funkwhale'
)
field_kwargs
=
{
'required'
:
False
,
}
@
global_preferences_registry
.
register
...
...
@@ -65,8 +72,7 @@ class RavenEnabled(types.BooleanPreference):
name
=
'front_enabled'
default
=
False
verbose_name
=
(
'Wether error reporting to a Sentry instance using raven is enabled'
' for front-end errors'
'Report front-end errors with Raven'
)
...
...
@@ -78,13 +84,27 @@ class InstanceNodeinfoEnabled(types.BooleanPreference):
default
=
True
verbose_name
=
'Enable nodeinfo endpoint'
help_text
=
(
'This endpoint is needed for your about page to work.'
'This endpoint is needed for your about page to work.
'
'It
\'
s also helpful for the various monitoring '
'tools that map and analyzize the fediverse, '
'but you can disable it completely if needed.'
)
@
global_preferences_registry
.
register
class
InstanceNodeinfoPrivate
(
types
.
BooleanPreference
):
show_in_api
=
False
section
=
instance
name
=
'nodeinfo_private'
default
=
False
verbose_name
=
'Private mode in nodeinfo'
help_text
=
(
'Indicate in the nodeinfo endpoint that you do not want your instance '
'to be tracked by third-party services. '
'There is no guarantee these tools will honor this setting though.'
)
@
global_preferences_registry
.
register
class
InstanceNodeinfoStatsEnabled
(
types
.
BooleanPreference
):
show_in_api
=
False
...
...
@@ -93,6 +113,6 @@ class InstanceNodeinfoStatsEnabled(types.BooleanPreference):
default
=
True
verbose_name
=
'Enable usage and library stats in nodeinfo endpoint'
help_text
=
(
'Disable this f you don
\'
t want to share usage and library statistics'
'Disable this
i
f you don
\'
t want to share usage and library statistics
'
'in the nodeinfo endpoint but don
\'
t want to disable it completely.'
)
api/funkwhale_api/instance/nodeinfo.py
View file @
7b18e46f
...
...
@@ -12,6 +12,7 @@ memo = memoize.Memoizer(store, namespace='instance:stats')
def
get
():
share_stats
=
preferences
.
get
(
'instance__nodeinfo_stats_enabled'
)
private
=
preferences
.
get
(
'instance__nodeinfo_private'
)
data
=
{
'version'
:
'2.0'
,
'software'
:
{
...
...
@@ -30,6 +31,7 @@ def get():
}
},
'metadata'
:
{
'private'
:
preferences
.
get
(
'instance__nodeinfo_private'
),
'shortDescription'
:
preferences
.
get
(
'instance__short_description'
),
'longDescription'
:
preferences
.
get
(
'instance__long_description'
),
'nodeName'
:
preferences
.
get
(
'instance__name'
),
...
...
api/funkwhale_api/instance/urls.py
View file @
7b18e46f
from
django.conf.urls
import
url
from
rest_framework
import
routers
from
.
import
views
admin_router
=
routers
.
SimpleRouter
()
admin_router
.
register
(
r
'admin/settings'
,
views
.
AdminSettings
,
'admin-settings'
)