From 62ca3bd73636da9039b32c65908ac4c900ae515d Mon Sep 17 00:00:00 2001 From: Eliot Berriot Date: Sat, 9 Jun 2018 15:36:16 +0200 Subject: [PATCH] Blacked the code --- api/config/api_urls.py | 112 +-- api/config/routing.py | 19 +- api/config/settings/common.py | 396 +++++----- api/config/settings/local.py | 52 +- api/config/settings/production.py | 76 +- api/config/urls.py | 36 +- api/demo/demo-user.py | 6 +- api/funkwhale_api/__init__.py | 9 +- api/funkwhale_api/activity/apps.py | 3 +- api/funkwhale_api/activity/record.py | 15 +- api/funkwhale_api/activity/serializers.py | 9 +- api/funkwhale_api/activity/utils.py | 41 +- api/funkwhale_api/activity/views.py | 2 +- api/funkwhale_api/common/auth.py | 11 +- api/funkwhale_api/common/authentication.py | 32 +- api/funkwhale_api/common/consumers.py | 2 +- .../common/dynamic_preferences_registry.py | 17 +- api/funkwhale_api/common/fields.py | 27 +- .../common/management/commands/script.py | 51 +- .../common/migrations/0001_initial.py | 4 +- api/funkwhale_api/common/pagination.py | 2 +- api/funkwhale_api/common/permissions.py | 22 +- api/funkwhale_api/common/preferences.py | 10 +- .../django_permissions_to_user_permissions.py | 22 +- api/funkwhale_api/common/scripts/test.py | 2 +- api/funkwhale_api/common/serializers.py | 61 +- api/funkwhale_api/common/session.py | 7 +- api/funkwhale_api/common/storage.py | 3 +- api/funkwhale_api/common/utils.py | 8 +- .../contrib/sites/migrations/0001_initial.py | 42 +- .../0002_set_site_domain_and_name.py | 19 +- .../migrations/0003_auto_20171214_2205.py | 21 +- api/funkwhale_api/downloader/downloader.py | 17 +- api/funkwhale_api/factories.py | 2 +- api/funkwhale_api/favorites/activities.py | 15 +- api/funkwhale_api/favorites/admin.py | 7 +- api/funkwhale_api/favorites/factories.py | 2 +- .../favorites/migrations/0001_initial.py | 44 +- api/funkwhale_api/favorites/models.py | 13 +- api/funkwhale_api/favorites/serializers.py | 19 +- api/funkwhale_api/favorites/urls.py | 3 +- api/funkwhale_api/favorites/views.py | 22 +- api/funkwhale_api/federation/activity.py | 95 ++- api/funkwhale_api/federation/actors.py | 276 +++---- api/funkwhale_api/federation/admin.py | 62 +- api/funkwhale_api/federation/api_urls.py | 10 +- .../federation/authentication.py | 14 +- .../dynamic_preferences_registry.py | 73 +- api/funkwhale_api/federation/exceptions.py | 2 - api/funkwhale_api/federation/factories.py | 162 ++-- api/funkwhale_api/federation/filters.py | 72 +- api/funkwhale_api/federation/keys.py | 24 +- api/funkwhale_api/federation/library.py | 117 +-- .../federation/migrations/0001_initial.py | 86 +- .../migrations/0002_auto_20180403_1620.py | 9 +- .../migrations/0003_auto_20180407_1010.py | 192 +++-- .../migrations/0004_auto_20180410_2025.py | 34 +- .../migrations/0005_auto_20180413_1723.py | 24 +- .../migrations/0006_auto_20180521_1702.py | 20 +- api/funkwhale_api/federation/models.py | 121 ++- api/funkwhale_api/federation/parsers.py | 2 +- api/funkwhale_api/federation/permissions.py | 10 +- api/funkwhale_api/federation/renderers.py | 4 +- api/funkwhale_api/federation/serializers.py | 662 +++++++--------- api/funkwhale_api/federation/signing.py | 35 +- api/funkwhale_api/federation/tasks.py | 81 +- api/funkwhale_api/federation/urls.py | 18 +- api/funkwhale_api/federation/utils.py | 17 +- api/funkwhale_api/federation/views.py | 239 +++--- api/funkwhale_api/federation/webfinger.py | 29 +- api/funkwhale_api/history/activities.py | 15 +- api/funkwhale_api/history/admin.py | 10 +- api/funkwhale_api/history/factories.py | 2 +- .../history/migrations/0001_initial.py | 52 +- .../migrations/0002_auto_20180325_1433.py | 11 +- api/funkwhale_api/history/models.py | 16 +- api/funkwhale_api/history/serializers.py | 22 +- api/funkwhale_api/history/urls.py | 3 +- api/funkwhale_api/history/views.py | 7 +- api/funkwhale_api/instance/consumers.py | 2 +- .../instance/dynamic_preferences_registry.py | 94 +-- api/funkwhale_api/instance/nodeinfo.py | 89 +-- api/funkwhale_api/instance/stats.py | 18 +- api/funkwhale_api/instance/urls.py | 7 +- api/funkwhale_api/instance/views.py | 23 +- api/funkwhale_api/manage/filters.py | 22 +- api/funkwhale_api/manage/serializers.py | 59 +- api/funkwhale_api/manage/urls.py | 6 +- api/funkwhale_api/manage/views.py | 37 +- api/funkwhale_api/music/admin.py | 82 +- api/funkwhale_api/music/factories.py | 109 ++- api/funkwhale_api/music/fake_data.py | 10 +- api/funkwhale_api/music/filters.py | 72 +- api/funkwhale_api/music/importers.py | 33 +- api/funkwhale_api/music/lyrics.py | 10 +- .../management/commands/fix_track_files.py | 80 +- api/funkwhale_api/music/metadata.py | 244 +++--- .../music/migrations/0001_initial.py | 201 +++-- .../migrations/0002_auto_20151215_1645.py | 30 +- .../migrations/0003_auto_20151222_2233.py | 10 +- .../music/migrations/0004_track_tags.py | 17 +- .../music/migrations/0005_deduplicate.py | 22 +- .../music/migrations/0006_unique_mbid.py | 29 +- .../music/migrations/0007_track_position.py | 10 +- .../migrations/0008_auto_20160529_1456.py | 16 +- .../migrations/0009_auto_20160920_1614.py | 77 +- .../migrations/0010_auto_20160920_1742.py | 10 +- .../music/migrations/0011_rename_files.py | 10 +- .../migrations/0012_auto_20161122_1905.py | 14 +- .../migrations/0013_auto_20171213_2211.py | 16 +- .../migrations/0014_importjob_track_file.py | 18 +- .../0015_bind_track_file_to_import_job.py | 14 +- .../0016_trackfile_acoustid_track_id.py | 10 +- .../migrations/0017_auto_20171227_1728.py | 26 +- .../migrations/0018_auto_20180218_1554.py | 27 +- .../migrations/0019_populate_mimetypes.py | 18 +- .../migrations/0020_importbatch_status.py | 21 +- .../migrations/0021_populate_batch_status.py | 13 +- .../0022_importbatch_import_request.py | 19 +- .../migrations/0023_auto_20180407_1010.py | 94 ++- .../music/migrations/0024_populate_uuid.py | 60 +- .../migrations/0025_auto_20180419_2023.py | 10 +- .../0026_trackfile_accessed_date.py | 10 +- .../migrations/0027_auto_20180515_1808.py | 24 +- api/funkwhale_api/music/models.py | 449 +++++------ api/funkwhale_api/music/permissions.py | 13 +- api/funkwhale_api/music/serializers.py | 203 ++--- api/funkwhale_api/music/tasks.py | 223 +++--- api/funkwhale_api/music/utils.py | 48 +- api/funkwhale_api/music/views.py | 275 +++---- api/funkwhale_api/musicbrainz/client.py | 62 +- api/funkwhale_api/musicbrainz/urls.py | 26 +- api/funkwhale_api/musicbrainz/views.py | 27 +- api/funkwhale_api/playlists/admin.py | 8 +- .../playlists/dynamic_preferences_registry.py | 12 +- api/funkwhale_api/playlists/factories.py | 6 +- api/funkwhale_api/playlists/filters.py | 11 +- .../playlists/migrations/0001_initial.py | 88 ++- .../migrations/0002_auto_20180316_2217.py | 24 +- .../migrations/0003_auto_20180319_1214.py | 43 +- .../migrations/0004_auto_20180320_1713.py | 17 +- api/funkwhale_api/playlists/models.py | 71 +- api/funkwhale_api/playlists/serializers.py | 49 +- api/funkwhale_api/playlists/views.py | 84 +- .../providers/acoustid/__init__.py | 6 +- .../acoustid/dynamic_preferences_registry.py | 14 +- .../management/commands/import_files.py | 199 ++--- .../providers/audiofile/tasks.py | 48 +- api/funkwhale_api/providers/urls.py | 18 +- api/funkwhale_api/providers/youtube/client.py | 41 +- .../youtube/dynamic_preferences_registry.py | 14 +- api/funkwhale_api/providers/youtube/urls.py | 4 +- api/funkwhale_api/providers/youtube/views.py | 20 +- api/funkwhale_api/radios/admin.py | 44 +- api/funkwhale_api/radios/factories.py | 17 +- api/funkwhale_api/radios/filters.py | 134 ++-- api/funkwhale_api/radios/filtersets.py | 5 +- .../radios/migrations/0001_initial.py | 71 +- .../0002_radiosession_session_key.py | 10 +- .../migrations/0003_auto_20160521_1708.py | 19 +- .../migrations/0004_auto_20180107_1813.py | 52 +- api/funkwhale_api/radios/models.py | 48 +- api/funkwhale_api/radios/radios.py | 67 +- api/funkwhale_api/radios/registries.py | 6 +- api/funkwhale_api/radios/serializers.py | 53 +- api/funkwhale_api/radios/urls.py | 7 +- api/funkwhale_api/radios/views.py | 65 +- api/funkwhale_api/requests/admin.py | 12 +- api/funkwhale_api/requests/api_urls.py | 6 +- api/funkwhale_api/requests/factories.py | 8 +- api/funkwhale_api/requests/filters.py | 15 +- .../requests/migrations/0001_initial.py | 54 +- api/funkwhale_api/requests/models.py | 22 +- api/funkwhale_api/requests/serializers.py | 25 +- api/funkwhale_api/requests/views.py | 18 +- api/funkwhale_api/subsonic/authentication.py | 40 +- .../subsonic/dynamic_preferences_registry.py | 16 +- api/funkwhale_api/subsonic/filters.py | 12 +- api/funkwhale_api/subsonic/negotiation.py | 6 +- api/funkwhale_api/subsonic/renderers.py | 31 +- api/funkwhale_api/subsonic/serializers.py | 212 +++-- api/funkwhale_api/subsonic/views.py | 479 +++++------ api/funkwhale_api/taskapp/celery.py | 16 +- api/funkwhale_api/users/adapters.py | 5 +- api/funkwhale_api/users/admin.py | 69 +- api/funkwhale_api/users/api_urls.py | 2 +- .../users/dynamic_preferences_registry.py | 28 +- api/funkwhale_api/users/factories.py | 24 +- .../users/migrations/0001_initial.py | 143 +++- .../migrations/0002_auto_20171214_2205.py | 23 +- .../migrations/0003_auto_20171226_1357.py | 16 +- .../migrations/0004_user_privacy_level.py | 21 +- .../0005_user_subsonic_api_token.py | 10 +- .../migrations/0006_auto_20180517_2324.py | 16 +- .../migrations/0007_auto_20180524_2009.py | 38 +- api/funkwhale_api/users/models.py | 75 +- api/funkwhale_api/users/permissions.py | 8 +- api/funkwhale_api/users/rest_auth_urls.py | 23 +- api/funkwhale_api/users/serializers.py | 44 +- api/funkwhale_api/users/views.py | 42 +- api/tests/activity/test_record.py | 15 +- api/tests/activity/test_serializers.py | 7 +- api/tests/activity/test_utils.py | 14 +- api/tests/activity/test_views.py | 11 +- api/tests/channels/test_auth.py | 22 +- api/tests/channels/test_consumers.py | 12 +- api/tests/common/test_fields.py | 17 +- api/tests/common/test_permissions.py | 26 +- api/tests/common/test_preferences.py | 39 +- api/tests/common/test_scripts.py | 32 +- api/tests/common/test_serializers.py | 89 +-- api/tests/common/test_session.py | 11 +- api/tests/conftest.py | 52 +- api/tests/data/youtube.py | 94 +-- api/tests/favorites/test_activity.py | 48 +- api/tests/favorites/test_favorites.py | 93 ++- api/tests/federation/test_activity.py | 37 +- api/tests/federation/test_actors.py | 552 ++++++------- api/tests/federation/test_authentication.py | 43 +- api/tests/federation/test_keys.py | 30 +- api/tests/federation/test_library.py | 73 +- api/tests/federation/test_models.py | 23 +- api/tests/federation/test_permissions.py | 52 +- api/tests/federation/test_serializers.py | 747 +++++++++--------- api/tests/federation/test_signing.py | 99 +-- api/tests/federation/test_tasks.py | 138 ++-- api/tests/federation/test_utils.py | 64 +- api/tests/federation/test_views.py | 416 +++++----- api/tests/federation/test_webfinger.py | 66 +- api/tests/history/test_activity.py | 48 +- api/tests/history/test_history.py | 30 +- api/tests/instance/test_nodeinfo.py | 142 ++-- api/tests/instance/test_preferences.py | 20 +- api/tests/instance/test_stats.py | 45 +- api/tests/instance/test_views.py | 33 +- api/tests/manage/test_serializers.py | 2 +- api/tests/manage/test_views.py | 21 +- api/tests/music/conftest.py | 641 ++++++++------- api/tests/music/test_activity.py | 11 +- api/tests/music/test_api.py | 294 +++---- api/tests/music/test_commands.py | 41 +- api/tests/music/test_import.py | 229 +++--- api/tests/music/test_lyrics.py | 53 +- api/tests/music/test_metadata.py | 130 +-- api/tests/music/test_models.py | 127 ++- api/tests/music/test_music.py | 151 ++-- api/tests/music/test_permissions.py | 54 +- api/tests/music/test_serializers.py | 119 ++- api/tests/music/test_tasks.py | 252 +++--- api/tests/music/test_utils.py | 36 +- api/tests/music/test_views.py | 368 ++++----- api/tests/music/test_works.py | 51 +- api/tests/musicbrainz/conftest.py | 219 +++-- api/tests/musicbrainz/test_api.py | 100 +-- api/tests/musicbrainz/test_cache.py | 12 +- api/tests/playlists/test_models.py | 61 +- api/tests/playlists/test_serializers.py | 70 +- api/tests/playlists/test_views.py | 177 ++--- api/tests/radios/test_api.py | 130 ++- api/tests/radios/test_filters.py | 153 ++-- api/tests/radios/test_radios.py | 138 ++-- api/tests/requests/test_models.py | 14 +- api/tests/requests/test_views.py | 14 +- api/tests/subsonic/test_authentication.py | 64 +- api/tests/subsonic/test_renderers.py | 22 +- api/tests/subsonic/test_serializers.py | 268 +++---- api/tests/subsonic/test_views.py | 408 +++++----- api/tests/test_acoustid.py | 45 +- api/tests/test_downloader.py | 8 +- api/tests/test_import_audio_file.py | 185 ++--- api/tests/test_jwt_querystring.py | 13 +- api/tests/test_tasks.py | 11 +- api/tests/test_youtube.py | 81 +- api/tests/users/test_activity.py | 9 +- api/tests/users/test_admin.py | 22 +- api/tests/users/test_jwt.py | 3 +- api/tests/users/test_models.py | 70 +- api/tests/users/test_permissions.py | 92 ++- api/tests/users/test_views.py | 185 ++--- 279 files changed, 8861 insertions(+), 9527 deletions(-) diff --git a/api/config/api_urls.py b/api/config/api_urls.py index 98b863a93..f3f61313e 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -12,70 +12,70 @@ from dynamic_preferences.api.viewsets import GlobalPreferencesViewSet from dynamic_preferences.users.viewsets import UserPreferencesViewSet router = routers.SimpleRouter() -router.register(r'settings', GlobalPreferencesViewSet, base_name='settings') -router.register(r'activity', activity_views.ActivityViewSet, 'activity') -router.register(r'tags', views.TagViewSet, 'tags') -router.register(r'tracks', views.TrackViewSet, 'tracks') -router.register(r'trackfiles', views.TrackFileViewSet, 'trackfiles') -router.register(r'artists', views.ArtistViewSet, 'artists') -router.register(r'albums', views.AlbumViewSet, 'albums') -router.register(r'import-batches', views.ImportBatchViewSet, 'import-batches') -router.register(r'import-jobs', views.ImportJobViewSet, 'import-jobs') -router.register(r'submit', views.SubmitViewSet, 'submit') -router.register(r'playlists', playlists_views.PlaylistViewSet, 'playlists') +router.register(r"settings", GlobalPreferencesViewSet, base_name="settings") +router.register(r"activity", activity_views.ActivityViewSet, "activity") +router.register(r"tags", views.TagViewSet, "tags") +router.register(r"tracks", views.TrackViewSet, "tracks") +router.register(r"trackfiles", views.TrackFileViewSet, "trackfiles") +router.register(r"artists", views.ArtistViewSet, "artists") +router.register(r"albums", views.AlbumViewSet, "albums") +router.register(r"import-batches", views.ImportBatchViewSet, "import-batches") +router.register(r"import-jobs", views.ImportJobViewSet, "import-jobs") +router.register(r"submit", views.SubmitViewSet, "submit") +router.register(r"playlists", playlists_views.PlaylistViewSet, "playlists") router.register( - r'playlist-tracks', - playlists_views.PlaylistTrackViewSet, - 'playlist-tracks') + r"playlist-tracks", playlists_views.PlaylistTrackViewSet, "playlist-tracks" +) v1_patterns = router.urls subsonic_router = routers.SimpleRouter(trailing_slash=False) -subsonic_router.register(r'subsonic/rest', SubsonicViewSet, base_name='subsonic') +subsonic_router.register(r"subsonic/rest", SubsonicViewSet, base_name="subsonic") v1_patterns += [ - url(r'^instance/', + url( + r"^instance/", + include(("funkwhale_api.instance.urls", "instance"), namespace="instance"), + ), + url( + r"^manage/", + include(("funkwhale_api.manage.urls", "manage"), namespace="manage"), + ), + url( + r"^federation/", include( - ('funkwhale_api.instance.urls', 'instance'), - namespace='instance')), - url(r'^manage/', - include( - ('funkwhale_api.manage.urls', 'manage'), - namespace='manage')), - url(r'^federation/', - include( - ('funkwhale_api.federation.api_urls', 'federation'), - namespace='federation')), - url(r'^providers/', - include( - ('funkwhale_api.providers.urls', 'providers'), - namespace='providers')), - url(r'^favorites/', - include( - ('funkwhale_api.favorites.urls', 'favorites'), - namespace='favorites')), - url(r'^search$', - views.Search.as_view(), name='search'), - url(r'^radios/', - include( - ('funkwhale_api.radios.urls', 'radios'), - namespace='radios')), - url(r'^history/', - include( - ('funkwhale_api.history.urls', 'history'), - namespace='history')), - url(r'^users/', - include( - ('funkwhale_api.users.api_urls', 'users'), - namespace='users')), - url(r'^requests/', - include( - ('funkwhale_api.requests.api_urls', 'requests'), - namespace='requests')), - url(r'^token/$', jwt_views.obtain_jwt_token, name='token'), - url(r'^token/refresh/$', jwt_views.refresh_jwt_token, name='token_refresh'), + ("funkwhale_api.federation.api_urls", "federation"), namespace="federation" + ), + ), + url( + r"^providers/", + include(("funkwhale_api.providers.urls", "providers"), namespace="providers"), + ), + url( + r"^favorites/", + include(("funkwhale_api.favorites.urls", "favorites"), namespace="favorites"), + ), + url(r"^search$", views.Search.as_view(), name="search"), + url( + r"^radios/", + include(("funkwhale_api.radios.urls", "radios"), namespace="radios"), + ), + url( + r"^history/", + include(("funkwhale_api.history.urls", "history"), namespace="history"), + ), + url( + r"^users/", + include(("funkwhale_api.users.api_urls", "users"), namespace="users"), + ), + url( + r"^requests/", + include(("funkwhale_api.requests.api_urls", "requests"), namespace="requests"), + ), + url(r"^token/$", jwt_views.obtain_jwt_token, name="token"), + url(r"^token/refresh/$", jwt_views.refresh_jwt_token, name="token_refresh"), ] urlpatterns = [ - url(r'^v1/', include((v1_patterns, 'v1'), namespace='v1')) -] + format_suffix_patterns(subsonic_router.urls, allowed=['view']) + url(r"^v1/", include((v1_patterns, "v1"), namespace="v1")) +] + format_suffix_patterns(subsonic_router.urls, allowed=["view"]) diff --git a/api/config/routing.py b/api/config/routing.py index 574d5a18e..b1f163759 100644 --- a/api/config/routing.py +++ b/api/config/routing.py @@ -7,12 +7,13 @@ from funkwhale_api.common.auth import TokenAuthMiddleware from funkwhale_api.instance import consumers -application = ProtocolTypeRouter({ - # Empty for now (http->django views is added by default) - "websocket": TokenAuthMiddleware( - URLRouter([ - url("^api/v1/instance/activity$", - consumers.InstanceActivityConsumer), - ]) - ), -}) +application = ProtocolTypeRouter( + { + # Empty for now (http->django views is added by default) + "websocket": TokenAuthMiddleware( + URLRouter( + [url("^api/v1/instance/activity$", consumers.InstanceActivityConsumer)] + ) + ) + } +) diff --git a/api/config/settings/common.py b/api/config/settings/common.py index 6ab2a8303..f7688a4e4 100644 --- a/api/config/settings/common.py +++ b/api/config/settings/common.py @@ -18,123 +18,117 @@ from celery.schedules import crontab from funkwhale_api import __version__ ROOT_DIR = environ.Path(__file__) - 3 # (/a/b/myfile.py - 3 = /) -APPS_DIR = ROOT_DIR.path('funkwhale_api') +APPS_DIR = ROOT_DIR.path("funkwhale_api") env = environ.Env() try: - env.read_env(ROOT_DIR.file('.env')) + env.read_env(ROOT_DIR.file(".env")) except FileNotFoundError: pass FUNKWHALE_HOSTNAME = None -FUNKWHALE_HOSTNAME_SUFFIX = env('FUNKWHALE_HOSTNAME_SUFFIX', default=None) -FUNKWHALE_HOSTNAME_PREFIX = env('FUNKWHALE_HOSTNAME_PREFIX', default=None) +FUNKWHALE_HOSTNAME_SUFFIX = env("FUNKWHALE_HOSTNAME_SUFFIX", default=None) +FUNKWHALE_HOSTNAME_PREFIX = env("FUNKWHALE_HOSTNAME_PREFIX", default=None) if FUNKWHALE_HOSTNAME_PREFIX and FUNKWHALE_HOSTNAME_SUFFIX: # We're in traefik case, in development - FUNKWHALE_HOSTNAME = '{}.{}'.format( - FUNKWHALE_HOSTNAME_PREFIX, FUNKWHALE_HOSTNAME_SUFFIX) - FUNKWHALE_PROTOCOL = env('FUNKWHALE_PROTOCOL', default='https') + FUNKWHALE_HOSTNAME = "{}.{}".format( + FUNKWHALE_HOSTNAME_PREFIX, FUNKWHALE_HOSTNAME_SUFFIX + ) + FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https") else: try: - FUNKWHALE_HOSTNAME = env('FUNKWHALE_HOSTNAME') - FUNKWHALE_PROTOCOL = env('FUNKWHALE_PROTOCOL', default='https') + FUNKWHALE_HOSTNAME = env("FUNKWHALE_HOSTNAME") + FUNKWHALE_PROTOCOL = env("FUNKWHALE_PROTOCOL", default="https") except Exception: - FUNKWHALE_URL = env('FUNKWHALE_URL') + FUNKWHALE_URL = env("FUNKWHALE_URL") _parsed = urlsplit(FUNKWHALE_URL) FUNKWHALE_HOSTNAME = _parsed.netloc FUNKWHALE_PROTOCOL = _parsed.scheme -FUNKWHALE_URL = '{}://{}'.format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME) +FUNKWHALE_URL = "{}://{}".format(FUNKWHALE_PROTOCOL, FUNKWHALE_HOSTNAME) # XXX: deprecated, see #186 -FEDERATION_ENABLED = env.bool('FEDERATION_ENABLED', default=True) -FEDERATION_HOSTNAME = env('FEDERATION_HOSTNAME', default=FUNKWHALE_HOSTNAME) +FEDERATION_ENABLED = env.bool("FEDERATION_ENABLED", default=True) +FEDERATION_HOSTNAME = env("FEDERATION_HOSTNAME", default=FUNKWHALE_HOSTNAME) # XXX: deprecated, see #186 -FEDERATION_COLLECTION_PAGE_SIZE = env.int( - 'FEDERATION_COLLECTION_PAGE_SIZE', default=50 -) +FEDERATION_COLLECTION_PAGE_SIZE = env.int("FEDERATION_COLLECTION_PAGE_SIZE", default=50) # XXX: deprecated, see #186 FEDERATION_MUSIC_NEEDS_APPROVAL = env.bool( - 'FEDERATION_MUSIC_NEEDS_APPROVAL', default=True + "FEDERATION_MUSIC_NEEDS_APPROVAL", default=True ) # XXX: deprecated, see #186 -FEDERATION_ACTOR_FETCH_DELAY = env.int( - 'FEDERATION_ACTOR_FETCH_DELAY', default=60 * 12) -ALLOWED_HOSTS = env.list('DJANGO_ALLOWED_HOSTS') +FEDERATION_ACTOR_FETCH_DELAY = env.int("FEDERATION_ACTOR_FETCH_DELAY", default=60 * 12) +ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS") # APP CONFIGURATION # ------------------------------------------------------------------------------ DJANGO_APPS = ( - 'channels', + "channels", # Default Django apps: - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.postgres', - + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.postgres", # Useful template tags: # 'django.contrib.humanize', - # Admin - 'django.contrib.admin', + "django.contrib.admin", ) THIRD_PARTY_APPS = ( # 'crispy_forms', # Form layouts - 'allauth', # registration - 'allauth.account', # registration - 'allauth.socialaccount', # registration - 'corsheaders', - 'rest_framework', - 'rest_framework.authtoken', - 'taggit', - 'rest_auth', - 'rest_auth.registration', - 'dynamic_preferences', - 'django_filters', - 'cacheops', - 'django_cleanup', + "allauth", # registration + "allauth.account", # registration + "allauth.socialaccount", # registration + "corsheaders", + "rest_framework", + "rest_framework.authtoken", + "taggit", + "rest_auth", + "rest_auth.registration", + "dynamic_preferences", + "django_filters", + "cacheops", + "django_cleanup", ) # Sentry RAVEN_ENABLED = env.bool("RAVEN_ENABLED", default=False) -RAVEN_DSN = env("RAVEN_DSN", default='') +RAVEN_DSN = env("RAVEN_DSN", default="") if RAVEN_ENABLED: RAVEN_CONFIG = { - 'dsn': RAVEN_DSN, + "dsn": RAVEN_DSN, # If you are using git, you can also automatically configure the # release based on the git info. - 'release': __version__, + "release": __version__, } - THIRD_PARTY_APPS += ( - 'raven.contrib.django.raven_compat', - ) + THIRD_PARTY_APPS += ("raven.contrib.django.raven_compat",) # Apps specific for this project go here. LOCAL_APPS = ( - 'funkwhale_api.common', - 'funkwhale_api.activity.apps.ActivityConfig', - 'funkwhale_api.users', # custom users app + "funkwhale_api.common", + "funkwhale_api.activity.apps.ActivityConfig", + "funkwhale_api.users", # custom users app # Your stuff: custom apps go here - 'funkwhale_api.instance', - 'funkwhale_api.music', - 'funkwhale_api.requests', - 'funkwhale_api.favorites', - 'funkwhale_api.federation', - 'funkwhale_api.radios', - 'funkwhale_api.history', - 'funkwhale_api.playlists', - 'funkwhale_api.providers.audiofile', - 'funkwhale_api.providers.youtube', - 'funkwhale_api.providers.acoustid', - 'funkwhale_api.subsonic', + "funkwhale_api.instance", + "funkwhale_api.music", + "funkwhale_api.requests", + "funkwhale_api.favorites", + "funkwhale_api.federation", + "funkwhale_api.radios", + "funkwhale_api.history", + "funkwhale_api.playlists", + "funkwhale_api.providers.audiofile", + "funkwhale_api.providers.youtube", + "funkwhale_api.providers.acoustid", + "funkwhale_api.subsonic", ) # See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps @@ -145,20 +139,18 @@ INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS # ------------------------------------------------------------------------------ MIDDLEWARE = ( # Make sure djangosecure.middleware.SecurityMiddleware is listed first - 'django.contrib.sessions.middleware.SessionMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ) # MIGRATIONS CONFIGURATION # ------------------------------------------------------------------------------ -MIGRATION_MODULES = { - 'sites': 'funkwhale_api.contrib.sites.migrations' -} +MIGRATION_MODULES = {"sites": "funkwhale_api.contrib.sites.migrations"} # DEBUG # ------------------------------------------------------------------------------ @@ -168,9 +160,7 @@ DEBUG = env.bool("DJANGO_DEBUG", False) # FIXTURE CONFIGURATION # ------------------------------------------------------------------------------ # See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FIXTURE_DIRS -FIXTURE_DIRS = ( - str(APPS_DIR.path('fixtures')), -) +FIXTURE_DIRS = (str(APPS_DIR.path("fixtures")),) # EMAIL CONFIGURATION # ------------------------------------------------------------------------------ @@ -178,16 +168,14 @@ FIXTURE_DIRS = ( # EMAIL # ------------------------------------------------------------------------------ DEFAULT_FROM_EMAIL = env( - 'DEFAULT_FROM_EMAIL', - default='Funkwhale '.format(FUNKWHALE_HOSTNAME)) + "DEFAULT_FROM_EMAIL", default="Funkwhale ".format(FUNKWHALE_HOSTNAME) +) -EMAIL_SUBJECT_PREFIX = env( - "EMAIL_SUBJECT_PREFIX", default='[Funkwhale] ') -SERVER_EMAIL = env('SERVER_EMAIL', default=DEFAULT_FROM_EMAIL) +EMAIL_SUBJECT_PREFIX = env("EMAIL_SUBJECT_PREFIX", default="[Funkwhale] ") +SERVER_EMAIL = env("SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) -EMAIL_CONFIG = env.email_url( - 'EMAIL_CONFIG', default='consolemail://') +EMAIL_CONFIG = env.email_url("EMAIL_CONFIG", default="consolemail://") vars().update(EMAIL_CONFIG) @@ -196,9 +184,9 @@ vars().update(EMAIL_CONFIG) # See: https://docs.djangoproject.com/en/dev/ref/settings/#databases DATABASES = { # Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ - 'default': env.db("DATABASE_URL"), + "default": env.db("DATABASE_URL") } -DATABASES['default']['ATOMIC_REQUESTS'] = True +DATABASES["default"]["ATOMIC_REQUESTS"] = True # # DATABASES = { # 'default': { @@ -212,10 +200,10 @@ DATABASES['default']['ATOMIC_REQUESTS'] = True # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. # In a Windows environment this must be set to your system time zone. -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" # See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" # See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id SITE_ID = 1 @@ -235,126 +223,120 @@ USE_TZ = True TEMPLATES = [ { # See: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-TEMPLATES-BACKEND - 'BACKEND': 'django.template.backends.django.DjangoTemplates', + "BACKEND": "django.template.backends.django.DjangoTemplates", # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs - 'DIRS': [ - str(APPS_DIR.path('templates')), - ], - 'OPTIONS': { + "DIRS": [str(APPS_DIR.path("templates"))], + "OPTIONS": { # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug - 'debug': DEBUG, + "debug": DEBUG, # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders # https://docs.djangoproject.com/en/dev/ref/templates/api/#loader-types - 'loaders': [ - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', + "loaders": [ + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", ], # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", # Your stuff: custom template context processors go here ], }, - }, + } ] # See: http://django-crispy-forms.readthedocs.org/en/latest/install.html#template-packs -CRISPY_TEMPLATE_PACK = 'bootstrap3' +CRISPY_TEMPLATE_PACK = "bootstrap3" # STATIC FILE CONFIGURATION # ------------------------------------------------------------------------------ # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root -STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR('staticfiles'))) +STATIC_ROOT = env("STATIC_ROOT", default=str(ROOT_DIR("staticfiles"))) # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url -STATIC_URL = env("STATIC_URL", default='/staticfiles/') -DEFAULT_FILE_STORAGE = 'funkwhale_api.common.storage.ASCIIFileSystemStorage' +STATIC_URL = env("STATIC_URL", default="/staticfiles/") +DEFAULT_FILE_STORAGE = "funkwhale_api.common.storage.ASCIIFileSystemStorage" # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS -STATICFILES_DIRS = ( - str(APPS_DIR.path('static')), -) +STATICFILES_DIRS = (str(APPS_DIR.path("static")),) # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) # MEDIA CONFIGURATION # ------------------------------------------------------------------------------ # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root -MEDIA_ROOT = env("MEDIA_ROOT", default=str(APPS_DIR('media'))) +MEDIA_ROOT = env("MEDIA_ROOT", default=str(APPS_DIR("media"))) # See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url -MEDIA_URL = env("MEDIA_URL", default='/media/') +MEDIA_URL = env("MEDIA_URL", default="/media/") # URL Configuration # ------------------------------------------------------------------------------ -ROOT_URLCONF = 'config.urls' +ROOT_URLCONF = "config.urls" # See: https://docs.djangoproject.com/en/dev/ref/settings/#wsgi-application -WSGI_APPLICATION = 'config.wsgi.application' +WSGI_APPLICATION = "config.wsgi.application" ASGI_APPLICATION = "config.routing.application" # This ensures that Django will be able to detect a secure connection -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # AUTHENTICATION CONFIGURATION # ------------------------------------------------------------------------------ AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'allauth.account.auth_backends.AuthenticationBackend', + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", ) SESSION_COOKIE_HTTPONLY = False # Some really nice defaults -ACCOUNT_AUTHENTICATION_METHOD = 'username_email' +ACCOUNT_AUTHENTICATION_METHOD = "username_email" ACCOUNT_EMAIL_REQUIRED = True -ACCOUNT_EMAIL_VERIFICATION = 'mandatory' +ACCOUNT_EMAIL_VERIFICATION = "mandatory" # Custom user app defaults # Select the correct user model -AUTH_USER_MODEL = 'users.User' -LOGIN_REDIRECT_URL = 'users:redirect' -LOGIN_URL = 'account_login' +AUTH_USER_MODEL = "users.User" +LOGIN_REDIRECT_URL = "users:redirect" +LOGIN_URL = "account_login" # SLUGLIFIER -AUTOSLUG_SLUGIFY_FUNCTION = 'slugify.slugify' +AUTOSLUG_SLUGIFY_FUNCTION = "slugify.slugify" CACHE_DEFAULT = "redis://127.0.0.1:6379/0" -CACHES = { - "default": env.cache_url('CACHE_URL', default=CACHE_DEFAULT) -} +CACHES = {"default": env.cache_url("CACHE_URL", default=CACHE_DEFAULT)} CACHES["default"]["BACKEND"] = "django_redis.cache.RedisCache" from urllib.parse import urlparse -cache_url = urlparse(CACHES['default']['LOCATION']) + +cache_url = urlparse(CACHES["default"]["LOCATION"]) CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", - "CONFIG": { - "hosts": [(cache_url.hostname, cache_url.port)], - }, - }, + "CONFIG": {"hosts": [(cache_url.hostname, cache_url.port)]}, + } } CACHES["default"]["OPTIONS"] = { "CLIENT_CLASS": "django_redis.client.DefaultClient", "IGNORE_EXCEPTIONS": True, # mimics memcache behavior. - # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior + # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior } ########## CELERY -INSTALLED_APPS += ('funkwhale_api.taskapp.celery.CeleryConfig',) +INSTALLED_APPS += ("funkwhale_api.taskapp.celery.CeleryConfig",) CELERY_BROKER_URL = env( - "CELERY_BROKER_URL", default=env('CACHE_URL', default=CACHE_DEFAULT)) + "CELERY_BROKER_URL", default=env("CACHE_URL", default=CACHE_DEFAULT) +) ########## END CELERY # Location of root django.contrib.admin URL, use {% url 'admin:index' %} @@ -362,25 +344,24 @@ CELERY_BROKER_URL = env( CELERY_TASK_DEFAULT_RATE_LIMIT = 1 CELERY_TASK_TIME_LIMIT = 300 CELERYBEAT_SCHEDULE = { - 'federation.clean_music_cache': { - 'task': 'funkwhale_api.federation.tasks.clean_music_cache', - 'schedule': crontab(hour='*/2'), - 'options': { - 'expires': 60 * 2, - }, + "federation.clean_music_cache": { + "task": "funkwhale_api.federation.tasks.clean_music_cache", + "schedule": crontab(hour="*/2"), + "options": {"expires": 60 * 2}, } } import datetime + JWT_AUTH = { - 'JWT_ALLOW_REFRESH': True, - 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), - 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=30), - 'JWT_AUTH_HEADER_PREFIX': 'JWT', - 'JWT_GET_USER_SECRET_KEY': lambda user: user.secret_key + "JWT_ALLOW_REFRESH": True, + "JWT_EXPIRATION_DELTA": datetime.timedelta(days=7), + "JWT_REFRESH_EXPIRATION_DELTA": datetime.timedelta(days=30), + "JWT_AUTH_HEADER_PREFIX": "JWT", + "JWT_GET_USER_SECRET_KEY": lambda user: user.secret_key, } OLD_PASSWORD_FIELD_ENABLED = True -ACCOUNT_ADAPTER = 'funkwhale_api.users.adapters.FunkwhaleAccountAdapter' +ACCOUNT_ADAPTER = "funkwhale_api.users.adapters.FunkwhaleAccountAdapter" CORS_ORIGIN_ALLOW_ALL = True # CORS_ORIGIN_WHITELIST = ( # 'localhost', @@ -389,41 +370,37 @@ CORS_ORIGIN_ALLOW_ALL = True CORS_ALLOW_CREDENTIALS = True REST_FRAMEWORK = { - 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAuthenticated', + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), + "DEFAULT_PAGINATION_CLASS": "funkwhale_api.common.pagination.FunkwhalePagination", + "PAGE_SIZE": 25, + "DEFAULT_PARSER_CLASSES": ( + "rest_framework.parsers.JSONParser", + "rest_framework.parsers.FormParser", + "rest_framework.parsers.MultiPartParser", + "funkwhale_api.federation.parsers.ActivityParser", ), - 'DEFAULT_PAGINATION_CLASS': 'funkwhale_api.common.pagination.FunkwhalePagination', - 'PAGE_SIZE': 25, - 'DEFAULT_PARSER_CLASSES': ( - 'rest_framework.parsers.JSONParser', - 'rest_framework.parsers.FormParser', - 'rest_framework.parsers.MultiPartParser', - 'funkwhale_api.federation.parsers.ActivityParser', + "DEFAULT_AUTHENTICATION_CLASSES": ( + "funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS", + "funkwhale_api.common.authentication.BearerTokenHeaderAuth", + "rest_framework_jwt.authentication.JSONWebTokenAuthentication", + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.BasicAuthentication", ), - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'funkwhale_api.common.authentication.JSONWebTokenAuthenticationQS', - 'funkwhale_api.common.authentication.BearerTokenHeaderAuth', - 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', - 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.BasicAuthentication', + "DEFAULT_FILTER_BACKENDS": ( + "rest_framework.filters.OrderingFilter", + "django_filters.rest_framework.DjangoFilterBackend", ), - 'DEFAULT_FILTER_BACKENDS': ( - 'rest_framework.filters.OrderingFilter', - 'django_filters.rest_framework.DjangoFilterBackend', - ), - 'DEFAULT_RENDERER_CLASSES': ( - 'rest_framework.renderers.JSONRenderer', - ) + "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), } -BROWSABLE_API_ENABLED = env.bool('BROWSABLE_API_ENABLED', default=False) +BROWSABLE_API_ENABLED = env.bool("BROWSABLE_API_ENABLED", default=False) if BROWSABLE_API_ENABLED: - REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'] += ( - 'rest_framework.renderers.BrowsableAPIRenderer', + REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] += ( + "rest_framework.renderers.BrowsableAPIRenderer", ) REST_AUTH_SERIALIZERS = { - 'PASSWORD_RESET_SERIALIZER': 'funkwhale_api.users.serializers.PasswordResetSerializer' # noqa + "PASSWORD_RESET_SERIALIZER": "funkwhale_api.users.serializers.PasswordResetSerializer" # noqa } REST_SESSION_LOGIN = False REST_USE_JWT = True @@ -434,60 +411,55 @@ USE_X_FORWARDED_PORT = True # Wether we should use Apache, Nginx (or other) headers when serving audio files # Default to Nginx -REVERSE_PROXY_TYPE = env('REVERSE_PROXY_TYPE', default='nginx') -assert REVERSE_PROXY_TYPE in ['apache2', 'nginx'], 'Unsupported REVERSE_PROXY_TYPE' +REVERSE_PROXY_TYPE = env("REVERSE_PROXY_TYPE", default="nginx") +assert REVERSE_PROXY_TYPE in ["apache2", "nginx"], "Unsupported REVERSE_PROXY_TYPE" # Which path will be used to process the internal redirection # **DO NOT** put a slash at the end -PROTECT_FILES_PATH = env('PROTECT_FILES_PATH', default='/_protected') +PROTECT_FILES_PATH = env("PROTECT_FILES_PATH", default="/_protected") # use this setting to tweak for how long you want to cache # musicbrainz results. (value is in seconds) -MUSICBRAINZ_CACHE_DURATION = env.int( - 'MUSICBRAINZ_CACHE_DURATION', - default=300 -) -CACHEOPS_REDIS = env('CACHE_URL', default=CACHE_DEFAULT) -CACHEOPS_ENABLED = env.bool('CACHEOPS_ENABLED', default=True) +MUSICBRAINZ_CACHE_DURATION = env.int("MUSICBRAINZ_CACHE_DURATION", default=300) +CACHEOPS_REDIS = env("CACHE_URL", default=CACHE_DEFAULT) +CACHEOPS_ENABLED = env.bool("CACHEOPS_ENABLED", default=True) CACHEOPS = { - 'music.artist': {'ops': 'all', 'timeout': 60 * 60}, - 'music.album': {'ops': 'all', 'timeout': 60 * 60}, - 'music.track': {'ops': 'all', 'timeout': 60 * 60}, - 'music.trackfile': {'ops': 'all', 'timeout': 60 * 60}, - 'taggit.tag': {'ops': 'all', 'timeout': 60 * 60}, + "music.artist": {"ops": "all", "timeout": 60 * 60}, + "music.album": {"ops": "all", "timeout": 60 * 60}, + "music.track": {"ops": "all", "timeout": 60 * 60}, + "music.trackfile": {"ops": "all", "timeout": 60 * 60}, + "taggit.tag": {"ops": "all", "timeout": 60 * 60}, } # Custom Admin URL, use {% url 'admin:index' %} -ADMIN_URL = env('DJANGO_ADMIN_URL', default='^api/admin/') +ADMIN_URL = env("DJANGO_ADMIN_URL", default="^api/admin/") CSRF_USE_SESSIONS = True # Playlist settings # XXX: deprecated, see #186 -PLAYLISTS_MAX_TRACKS = env.int('PLAYLISTS_MAX_TRACKS', default=250) +PLAYLISTS_MAX_TRACKS = env.int("PLAYLISTS_MAX_TRACKS", default=250) ACCOUNT_USERNAME_BLACKLIST = [ - 'funkwhale', - 'library', - 'test', - 'status', - 'root', - 'admin', - 'owner', - 'superuser', - 'staff', - 'service', -] + env.list('ACCOUNT_USERNAME_BLACKLIST', default=[]) + "funkwhale", + "library", + "test", + "status", + "root", + "admin", + "owner", + "superuser", + "staff", + "service", +] + env.list("ACCOUNT_USERNAME_BLACKLIST", default=[]) -EXTERNAL_REQUESTS_VERIFY_SSL = env.bool( - 'EXTERNAL_REQUESTS_VERIFY_SSL', - default=True -) +EXTERNAL_REQUESTS_VERIFY_SSL = env.bool("EXTERNAL_REQUESTS_VERIFY_SSL", default=True) # XXX: deprecated, see #186 API_AUTHENTICATION_REQUIRED = env.bool("API_AUTHENTICATION_REQUIRED", True) -MUSIC_DIRECTORY_PATH = env('MUSIC_DIRECTORY_PATH', default=None) +MUSIC_DIRECTORY_PATH = env("MUSIC_DIRECTORY_PATH", default=None) # on Docker setup, the music directory may not match the host path, # and we need to know it for it to serve stuff properly MUSIC_DIRECTORY_SERVE_PATH = env( - 'MUSIC_DIRECTORY_SERVE_PATH', default=MUSIC_DIRECTORY_PATH) + "MUSIC_DIRECTORY_SERVE_PATH", default=MUSIC_DIRECTORY_PATH +) diff --git a/api/config/settings/local.py b/api/config/settings/local.py index df14945cc..51e793476 100644 --- a/api/config/settings/local.py +++ b/api/config/settings/local.py @@ -1,53 +1,53 @@ # -*- coding: utf-8 -*- -''' +""" Local settings - Run in Debug mode - Use console backend for emails - Add Django Debug Toolbar - Add django-extensions as app -''' +""" from .common import * # noqa # DEBUG # ------------------------------------------------------------------------------ -DEBUG = env.bool('DJANGO_DEBUG', default=True) -TEMPLATES[0]['OPTIONS']['debug'] = DEBUG +DEBUG = env.bool("DJANGO_DEBUG", default=True) +TEMPLATES[0]["OPTIONS"]["debug"] = DEBUG # SECRET CONFIGURATION # ------------------------------------------------------------------------------ # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key # Note: This key only used for development and testing. -SECRET_KEY = env("DJANGO_SECRET_KEY", default='mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0sw@r5sua*1zp4fmxc') +SECRET_KEY = env( + "DJANGO_SECRET_KEY", default="mc$&b=5j#6^bv7tld1gyjp2&+^-qrdy=0sw@r5sua*1zp4fmxc" +) # Mail settings # ------------------------------------------------------------------------------ -EMAIL_HOST = 'localhost' +EMAIL_HOST = "localhost" EMAIL_PORT = 1025 # django-debug-toolbar # ------------------------------------------------------------------------------ -MIDDLEWARE += ('debug_toolbar.middleware.DebugToolbarMiddleware',) +MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",) # INTERNAL_IPS = ('127.0.0.1', '10.0.2.2',) DEBUG_TOOLBAR_CONFIG = { - 'DISABLE_PANELS': [ - 'debug_toolbar.panels.redirects.RedirectsPanel', - ], - 'SHOW_TEMPLATE_CONTEXT': True, - 'SHOW_TOOLBAR_CALLBACK': lambda request: True, + "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], + "SHOW_TEMPLATE_CONTEXT": True, + "SHOW_TOOLBAR_CALLBACK": lambda request: True, } # django-extensions # ------------------------------------------------------------------------------ # INSTALLED_APPS += ('django_extensions', ) -INSTALLED_APPS += ('debug_toolbar', ) +INSTALLED_APPS += ("debug_toolbar",) # TESTING # ------------------------------------------------------------------------------ -TEST_RUNNER = 'django.test.runner.DiscoverRunner' +TEST_RUNNER = "django.test.runner.DiscoverRunner" ########## CELERY # In development, all tasks will be executed locally by blocking until the task returns @@ -57,23 +57,15 @@ CELERY_TASK_ALWAYS_EAGER = False # Your local stuff: Below this line define 3rd party library settings LOGGING = { - 'version': 1, - 'handlers': { - 'console':{ - 'level':'DEBUG', - 'class':'logging.StreamHandler', - }, - }, - 'loggers': { - 'django.request': { - 'handlers':['console'], - 'propagate': True, - 'level':'DEBUG', - }, - '': { - 'level': 'DEBUG', - 'handlers': ['console'], + "version": 1, + "handlers": {"console": {"level": "DEBUG", "class": "logging.StreamHandler"}}, + "loggers": { + "django.request": { + "handlers": ["console"], + "propagate": True, + "level": "DEBUG", }, + "": {"level": "DEBUG", "handlers": ["console"]}, }, } CSRF_TRUSTED_ORIGINS = [o for o in ALLOWED_HOSTS] diff --git a/api/config/settings/production.py b/api/config/settings/production.py index 39be40dc3..0c79ee2ef 100644 --- a/api/config/settings/production.py +++ b/api/config/settings/production.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -''' +""" Production Configurations - Use djangosecure @@ -8,7 +8,7 @@ Production Configurations - Use Redis on Heroku -''' +""" from __future__ import absolute_import, unicode_literals from django.utils import six @@ -58,19 +58,24 @@ CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS # ------------------------------------------------------------------------------ # Uploaded Media Files # ------------------------ -DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' +DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage" # Static Assets # ------------------------ -STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage" # TEMPLATE CONFIGURATION # ------------------------------------------------------------------------------ # See: # https://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.loaders.cached.Loader -TEMPLATES[0]['OPTIONS']['loaders'] = [ - ('django.template.loaders.cached.Loader', [ - 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', ]), +TEMPLATES[0]["OPTIONS"]["loaders"] = [ + ( + "django.template.loaders.cached.Loader", + [ + "django.template.loaders.filesystem.Loader", + "django.template.loaders.app_directories.Loader", + ], + ) ] # CACHING @@ -78,7 +83,6 @@ TEMPLATES[0]['OPTIONS']['loaders'] = [ # Heroku URL does not pass the DB number, so we parse it in - # LOGGING CONFIGURATION # ------------------------------------------------------------------------------ # See: https://docs.djangoproject.com/en/dev/ref/settings/#logging @@ -88,43 +92,39 @@ TEMPLATES[0]['OPTIONS']['loaders'] = [ # See http://docs.djangoproject.com/en/dev/topics/logging for # more details on how to customize your logging configuration. LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' + "version": 1, + "disable_existing_loggers": False, + "filters": {"require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}}, + "formatters": { + "verbose": { + "format": "%(levelname)s %(asctime)s %(module)s " + "%(process)d %(thread)d %(message)s" } }, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s %(asctime)s %(module)s ' - '%(process)d %(thread)d %(message)s' + "handlers": { + "mail_admins": { + "level": "ERROR", + "filters": ["require_debug_false"], + "class": "django.utils.log.AdminEmailHandler", + }, + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "verbose", }, }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' + "loggers": { + "django.request": { + "handlers": ["mail_admins"], + "level": "ERROR", + "propagate": True, }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'verbose', + "django.security.DisallowedHost": { + "level": "ERROR", + "handlers": ["console", "mail_admins"], + "propagate": True, }, }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True - }, - 'django.security.DisallowedHost': { - 'level': 'ERROR', - 'handlers': ['console', 'mail_admins'], - 'propagate': True - } - } } diff --git a/api/config/urls.py b/api/config/urls.py index 90598ea84..60753294c 100644 --- a/api/config/urls.py +++ b/api/config/urls.py @@ -11,32 +11,30 @@ from django.views import defaults as default_views urlpatterns = [ # Django Admin, use {% url 'admin:index' %} url(settings.ADMIN_URL, admin.site.urls), - - url(r'^api/', include(("config.api_urls", 'api'), namespace="api")), - url(r'^', include( - ('funkwhale_api.federation.urls', 'federation'), - namespace="federation")), - url(r'^api/v1/auth/', include('rest_auth.urls')), - url(r'^api/v1/auth/registration/', include('funkwhale_api.users.rest_auth_urls')), - url(r'^accounts/', include('allauth.urls')), - + url(r"^api/", include(("config.api_urls", "api"), namespace="api")), + url( + r"^", + include( + ("funkwhale_api.federation.urls", "federation"), namespace="federation" + ), + ), + url(r"^api/v1/auth/", include("rest_auth.urls")), + url(r"^api/v1/auth/registration/", include("funkwhale_api.users.rest_auth_urls")), + url(r"^accounts/", include("allauth.urls")), # Your stuff: custom urls includes go here - - ] if settings.DEBUG: # This allows the error pages to be debugged during development, just visit # these url in browser to see how these error pages look like. urlpatterns += [ - url(r'^400/$', default_views.bad_request), - url(r'^403/$', default_views.permission_denied), - url(r'^404/$', default_views.page_not_found), - url(r'^500/$', default_views.server_error), + url(r"^400/$", default_views.bad_request), + url(r"^403/$", default_views.permission_denied), + url(r"^404/$", default_views.page_not_found), + url(r"^500/$", default_views.server_error), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - if 'debug_toolbar' in settings.INSTALLED_APPS: + if "debug_toolbar" in settings.INSTALLED_APPS: import debug_toolbar - urlpatterns += [ - url(r'^__debug__/', include(debug_toolbar.urls)), - ] + + urlpatterns += [url(r"^__debug__/", include(debug_toolbar.urls))] diff --git a/api/demo/demo-user.py b/api/demo/demo-user.py index 4f8648fb3..94757d2fa 100644 --- a/api/demo/demo-user.py +++ b/api/demo/demo-user.py @@ -1,7 +1,7 @@ from funkwhale_api.users.models import User -u = User.objects.create(email='demo@demo.com', username='demo', is_staff=True) -u.set_password('demo') -u.subsonic_api_token = 'demo' +u = User.objects.create(email="demo@demo.com", username="demo", is_staff=True) +u.set_password("demo") +u.subsonic_api_token = "demo" u.save() diff --git a/api/funkwhale_api/__init__.py b/api/funkwhale_api/__init__.py index 8b5b81ad4..0162c2317 100644 --- a/api/funkwhale_api/__init__.py +++ b/api/funkwhale_api/__init__.py @@ -1,3 +1,8 @@ # -*- coding: utf-8 -*- -__version__ = '0.14.1' -__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace('-', '.', 1).split('.')]) +__version__ = "0.14.1" +__version_info__ = tuple( + [ + int(num) if num.isdigit() else num + for num in __version__.replace("-", ".", 1).split(".") + ] +) diff --git a/api/funkwhale_api/activity/apps.py b/api/funkwhale_api/activity/apps.py index 0c66cbf50..b70f65c57 100644 --- a/api/funkwhale_api/activity/apps.py +++ b/api/funkwhale_api/activity/apps.py @@ -2,8 +2,9 @@ from django.apps import AppConfig, apps from . import record + class ActivityConfig(AppConfig): - name = 'funkwhale_api.activity' + name = "funkwhale_api.activity" def ready(self): super(ActivityConfig, self).ready() diff --git a/api/funkwhale_api/activity/record.py b/api/funkwhale_api/activity/record.py index fa55c0e85..3e34b1027 100644 --- a/api/funkwhale_api/activity/record.py +++ b/api/funkwhale_api/activity/record.py @@ -2,37 +2,36 @@ import persisting_theory class ActivityRegistry(persisting_theory.Registry): - look_into = 'activities' + look_into = "activities" def _register_for_model(self, model, attr, value): key = model._meta.label - d = self.setdefault(key, {'consumers': []}) + d = self.setdefault(key, {"consumers": []}) d[attr] = value def register_serializer(self, serializer_class): model = serializer_class.Meta.model - self._register_for_model(model, 'serializer', serializer_class) + self._register_for_model(model, "serializer", serializer_class) return serializer_class def register_consumer(self, label): def decorator(func): - consumers = self[label]['consumers'] + consumers = self[label]["consumers"] if func not in consumers: consumers.append(func) return func + return decorator registry = ActivityRegistry() - - def send(obj): conf = registry[obj.__class__._meta.label] - consumers = conf['consumers'] + consumers = conf["consumers"] if not consumers: return - serializer = conf['serializer'](obj) + serializer = conf["serializer"](obj) for consumer in consumers: consumer(data=serializer.data, obj=obj) diff --git a/api/funkwhale_api/activity/serializers.py b/api/funkwhale_api/activity/serializers.py index fd9b185cf..6df3a5870 100644 --- a/api/funkwhale_api/activity/serializers.py +++ b/api/funkwhale_api/activity/serializers.py @@ -4,8 +4,8 @@ from funkwhale_api.activity import record class ModelSerializer(serializers.ModelSerializer): - id = serializers.CharField(source='get_activity_url') - local_id = serializers.IntegerField(source='id') + id = serializers.CharField(source="get_activity_url") + local_id = serializers.IntegerField(source="id") # url = serializers.SerializerMethodField() def get_url(self, obj): @@ -17,8 +17,7 @@ class AutoSerializer(serializers.Serializer): A serializer that will automatically use registered activity serializers to serialize an henerogeneous list of objects (favorites, listenings, etc.) """ + def to_representation(self, instance): - serializer = record.registry[instance._meta.label]['serializer']( - instance - ) + serializer = record.registry[instance._meta.label]["serializer"](instance) return serializer.data diff --git a/api/funkwhale_api/activity/utils.py b/api/funkwhale_api/activity/utils.py index 46336930e..236d23d88 100644 --- a/api/funkwhale_api/activity/utils.py +++ b/api/funkwhale_api/activity/utils.py @@ -6,31 +6,25 @@ from funkwhale_api.history.models import Listening def combined_recent(limit, **kwargs): - datetime_field = kwargs.pop('datetime_field', 'creation_date') - source_querysets = { - qs.model._meta.label: qs for qs in kwargs.pop('querysets') - } + datetime_field = kwargs.pop("datetime_field", "creation_date") + source_querysets = {qs.model._meta.label: qs for qs in kwargs.pop("querysets")} querysets = { k: qs.annotate( - __type=models.Value( - qs.model._meta.label, output_field=models.CharField() - ) - ).values('pk', datetime_field, '__type') + __type=models.Value(qs.model._meta.label, output_field=models.CharField()) + ).values("pk", datetime_field, "__type") for k, qs in source_querysets.items() } _qs_list = list(querysets.values()) union_qs = _qs_list[0].union(*_qs_list[1:]) records = [] - for row in union_qs.order_by('-{}'.format(datetime_field))[:limit]: - records.append({ - 'type': row['__type'], - 'when': row[datetime_field], - 'pk': row['pk'] - }) + for row in union_qs.order_by("-{}".format(datetime_field))[:limit]: + records.append( + {"type": row["__type"], "when": row[datetime_field], "pk": row["pk"]} + ) # Now we bulk-load each object type in turn to_load = {} for record in records: - to_load.setdefault(record['type'], []).append(record['pk']) + to_load.setdefault(record["type"], []).append(record["pk"]) fetched = {} for key, pks in to_load.items(): @@ -39,26 +33,19 @@ def combined_recent(limit, **kwargs): # Annotate 'records' with loaded objects for record in records: - record['object'] = fetched[(record['type'], record['pk'])] + record["object"] = fetched[(record["type"], record["pk"])] return records def get_activity(user, limit=20): - query = fields.privacy_level_query( - user, lookup_field='user__privacy_level') + query = fields.privacy_level_query(user, lookup_field="user__privacy_level") querysets = [ Listening.objects.filter(query).select_related( - 'track', - 'user', - 'track__artist', - 'track__album__artist', + "track", "user", "track__artist", "track__album__artist" ), TrackFavorite.objects.filter(query).select_related( - 'track', - 'user', - 'track__artist', - 'track__album__artist', + "track", "user", "track__artist", "track__album__artist" ), ] records = combined_recent(limit=limit, querysets=querysets) - return [r['object'] for r in records] + return [r["object"] for r in records] diff --git a/api/funkwhale_api/activity/views.py b/api/funkwhale_api/activity/views.py index e66de1ccf..f2f747d4d 100644 --- a/api/funkwhale_api/activity/views.py +++ b/api/funkwhale_api/activity/views.py @@ -17,4 +17,4 @@ class ActivityViewSet(viewsets.GenericViewSet): def list(self, request, *args, **kwargs): activity = utils.get_activity(user=request.user) serializer = self.serializer_class(activity, many=True) - return Response({'results': serializer.data}, status=200) + return Response({"results": serializer.data}, status=200) diff --git a/api/funkwhale_api/common/auth.py b/api/funkwhale_api/common/auth.py index faf13571d..88010e798 100644 --- a/api/funkwhale_api/common/auth.py +++ b/api/funkwhale_api/common/auth.py @@ -16,20 +16,19 @@ class TokenHeaderAuth(BaseJSONWebTokenAuthentication): def get_jwt_value(self, request): try: - qs = request.get('query_string', b'').decode('utf-8') + qs = request.get("query_string", b"").decode("utf-8") parsed = parse_qs(qs) - token = parsed['token'][0] + token = parsed["token"][0] except KeyError: - raise exceptions.AuthenticationFailed('No token') + raise exceptions.AuthenticationFailed("No token") if not token: - raise exceptions.AuthenticationFailed('Empty token') + raise exceptions.AuthenticationFailed("Empty token") return token class TokenAuthMiddleware: - def __init__(self, inner): # Store the ASGI application we were passed self.inner = inner @@ -41,5 +40,5 @@ class TokenAuthMiddleware: except (User.DoesNotExist, exceptions.AuthenticationFailed): user = AnonymousUser() - scope['user'] = user + scope["user"] = user return self.inner(scope) diff --git a/api/funkwhale_api/common/authentication.py b/api/funkwhale_api/common/authentication.py index c7566eac8..9ddc06a3b 100644 --- a/api/funkwhale_api/common/authentication.py +++ b/api/funkwhale_api/common/authentication.py @@ -6,34 +6,34 @@ from rest_framework_jwt import authentication from rest_framework_jwt.settings import api_settings -class JSONWebTokenAuthenticationQS( - authentication.BaseJSONWebTokenAuthentication): +class JSONWebTokenAuthenticationQS(authentication.BaseJSONWebTokenAuthentication): - www_authenticate_realm = 'api' + www_authenticate_realm = "api" def get_jwt_value(self, request): - token = request.query_params.get('jwt') - if 'jwt' in request.query_params and not token: - msg = _('Invalid Authorization header. No credentials provided.') + token = request.query_params.get("jwt") + if "jwt" in request.query_params and not token: + msg = _("Invalid Authorization header. No credentials provided.") raise exceptions.AuthenticationFailed(msg) return token def authenticate_header(self, request): return '{0} realm="{1}"'.format( - api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm) + api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm + ) -class BearerTokenHeaderAuth( - authentication.BaseJSONWebTokenAuthentication): +class BearerTokenHeaderAuth(authentication.BaseJSONWebTokenAuthentication): """ For backward compatibility purpose, we used Authorization: JWT but Authorization: Bearer is probably better. """ - www_authenticate_realm = 'api' + + www_authenticate_realm = "api" def get_jwt_value(self, request): auth = authentication.get_authorization_header(request).split() - auth_header_prefix = 'bearer' + auth_header_prefix = "bearer" if not auth: if api_settings.JWT_AUTH_COOKIE: @@ -44,14 +44,16 @@ class BearerTokenHeaderAuth( return None if len(auth) == 1: - msg = _('Invalid Authorization header. No credentials provided.') + msg = _("Invalid Authorization header. No credentials provided.") raise exceptions.AuthenticationFailed(msg) elif len(auth) > 2: - msg = _('Invalid Authorization header. Credentials string ' - 'should not contain spaces.') + msg = _( + "Invalid Authorization header. Credentials string " + "should not contain spaces." + ) raise exceptions.AuthenticationFailed(msg) return auth[1] def authenticate_header(self, request): - return '{0} realm="{1}"'.format('Bearer', self.www_authenticate_realm) + return '{0} realm="{1}"'.format("Bearer", self.www_authenticate_realm) diff --git a/api/funkwhale_api/common/consumers.py b/api/funkwhale_api/common/consumers.py index 300ce5e26..32a8876dc 100644 --- a/api/funkwhale_api/common/consumers.py +++ b/api/funkwhale_api/common/consumers.py @@ -5,7 +5,7 @@ from funkwhale_api.common import channels class JsonAuthConsumer(JsonWebsocketConsumer): def connect(self): try: - assert self.scope['user'].pk is not None + assert self.scope["user"].pk is not None except (AssertionError, AttributeError, KeyError): return self.close() diff --git a/api/funkwhale_api/common/dynamic_preferences_registry.py b/api/funkwhale_api/common/dynamic_preferences_registry.py index 15b182671..d6dfed783 100644 --- a/api/funkwhale_api/common/dynamic_preferences_registry.py +++ b/api/funkwhale_api/common/dynamic_preferences_registry.py @@ -3,18 +3,19 @@ from dynamic_preferences.registries import global_preferences_registry from funkwhale_api.common import preferences -common = types.Section('common') +common = types.Section("common") @global_preferences_registry.register class APIAutenticationRequired( - preferences.DefaultFromSettingMixin, types.BooleanPreference): + preferences.DefaultFromSettingMixin, types.BooleanPreference +): section = common - name = 'api_authentication_required' - verbose_name = 'API Requires authentication' - setting = 'API_AUTHENTICATION_REQUIRED' + name = "api_authentication_required" + verbose_name = "API Requires authentication" + setting = "API_AUTHENTICATION_REQUIRED" 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).' + "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)." ) diff --git a/api/funkwhale_api/common/fields.py b/api/funkwhale_api/common/fields.py index 98e971662..de3ee37e5 100644 --- a/api/funkwhale_api/common/fields.py +++ b/api/funkwhale_api/common/fields.py @@ -6,34 +6,31 @@ from funkwhale_api.music import utils PRIVACY_LEVEL_CHOICES = [ - ('me', 'Only me'), - ('followers', 'Me and my followers'), - ('instance', 'Everyone on my instance, and my followers'), - ('everyone', 'Everyone, including people on other instances'), + ("me", "Only me"), + ("followers", "Me and my followers"), + ("instance", "Everyone on my instance, and my followers"), + ("everyone", "Everyone, including people on other instances"), ] def get_privacy_field(): return models.CharField( - max_length=30, choices=PRIVACY_LEVEL_CHOICES, default='instance') + max_length=30, choices=PRIVACY_LEVEL_CHOICES, default="instance" + ) -def privacy_level_query(user, lookup_field='privacy_level'): +def privacy_level_query(user, lookup_field="privacy_level"): if user.is_anonymous: - return models.Q(**{ - lookup_field: 'everyone', - }) + return models.Q(**{lookup_field: "everyone"}) - return models.Q(**{ - '{}__in'.format(lookup_field): [ - 'followers', 'instance', 'everyone' - ] - }) + return models.Q( + **{"{}__in".format(lookup_field): ["followers", "instance", "everyone"]} + ) class SearchFilter(django_filters.CharFilter): def __init__(self, *args, **kwargs): - self.search_fields = kwargs.pop('search_fields') + self.search_fields = kwargs.pop("search_fields") super().__init__(*args, **kwargs) def filter(self, qs, value): diff --git a/api/funkwhale_api/common/management/commands/script.py b/api/funkwhale_api/common/management/commands/script.py index 9d26a5836..03e32d5dd 100644 --- a/api/funkwhale_api/common/management/commands/script.py +++ b/api/funkwhale_api/common/management/commands/script.py @@ -4,17 +4,20 @@ from funkwhale_api.common import scripts class Command(BaseCommand): - help = 'Run a specific script from funkwhale_api/common/scripts/' + 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("script_name", nargs="?", type=str) parser.add_argument( - '--noinput', '--no-input', action='store_false', dest='interactive', + "--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'] + name = options["script_name"] if not name: self.show_help() @@ -23,44 +26,44 @@ class Command(BaseCommand): 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)) + "{} is not a valid script. Run python manage.py script for a " + "list of available scripts".format(name) + ) - self.stdout.write('') - if options['interactive']: + self.stdout.write("") + if options["interactive"]: message = ( - 'Are you sure you want to execute the script {}?\n\n' + "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': + if input("".join(message)) != "yes": raise CommandError("Script cancelled.") - script['entrypoint'](self, **options) + 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 ') + self.stdout.write("") + self.stdout.write("Available scripts:") + self.stdout.write("Launch with: python manage.py ") available_scripts = self.get_scripts() for name, script in sorted(available_scripts.items()): - self.stdout.write('') + 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('') + 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('__') + 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 + "name": name, + "help": module.__doc__.strip(), + "entrypoint": module.main, } return data diff --git a/api/funkwhale_api/common/migrations/0001_initial.py b/api/funkwhale_api/common/migrations/0001_initial.py index e95cc11e9..a362855b8 100644 --- a/api/funkwhale_api/common/migrations/0001_initial.py +++ b/api/funkwhale_api/common/migrations/0001_initial.py @@ -7,6 +7,4 @@ class Migration(migrations.Migration): dependencies = [] - operations = [ - UnaccentExtension() - ] + operations = [UnaccentExtension()] diff --git a/api/funkwhale_api/common/pagination.py b/api/funkwhale_api/common/pagination.py index 20efcb741..e5068bce2 100644 --- a/api/funkwhale_api/common/pagination.py +++ b/api/funkwhale_api/common/pagination.py @@ -2,5 +2,5 @@ from rest_framework.pagination import PageNumberPagination class FunkwhalePagination(PageNumberPagination): - page_size_query_param = 'page_size' + page_size_query_param = "page_size" max_page_size = 50 diff --git a/api/funkwhale_api/common/permissions.py b/api/funkwhale_api/common/permissions.py index e9e8b8819..82da2c577 100644 --- a/api/funkwhale_api/common/permissions.py +++ b/api/funkwhale_api/common/permissions.py @@ -9,9 +9,8 @@ from funkwhale_api.common import preferences class ConditionalAuthentication(BasePermission): - def has_permission(self, request, view): - if preferences.get('common__api_authentication_required'): + if preferences.get("common__api_authentication_required"): return request.user and request.user.is_authenticated return True @@ -28,24 +27,25 @@ class OwnerPermission(BasePermission): owner_field = 'owner' owner_checks = ['read', 'write'] """ + perms_map = { - 'GET': 'read', - 'OPTIONS': 'read', - 'HEAD': 'read', - 'POST': 'write', - 'PUT': 'write', - 'PATCH': 'write', - 'DELETE': 'write', + "GET": "read", + "OPTIONS": "read", + "HEAD": "read", + "POST": "write", + "PUT": "write", + "PATCH": "write", + "DELETE": "write", } def has_object_permission(self, request, view, obj): method_check = self.perms_map[request.method] - owner_checks = getattr(view, 'owner_checks', ['read', 'write']) + owner_checks = getattr(view, "owner_checks", ["read", "write"]) if method_check not in owner_checks: # check not enabled return True - owner_field = getattr(view, 'owner_field', 'user') + owner_field = getattr(view, "owner_field", "user") owner = operator.attrgetter(owner_field)(obj) if owner != request.user: raise Http404 diff --git a/api/funkwhale_api/common/preferences.py b/api/funkwhale_api/common/preferences.py index a2d3f04b7..9b8f12e85 100644 --- a/api/funkwhale_api/common/preferences.py +++ b/api/funkwhale_api/common/preferences.py @@ -17,7 +17,7 @@ def get(pref): class StringListSerializer(serializers.BaseSerializer): - separator = ',' + separator = "," sort = True @classmethod @@ -27,8 +27,8 @@ class StringListSerializer(serializers.BaseSerializer): if type(value) not in [list, tuple]: raise cls.exception( - "Cannot serialize, value {} is not a list or a tuple".format( - value)) + "Cannot serialize, value {} is not a list or a tuple".format(value) + ) if cls.sort: value = sorted(value) @@ -38,7 +38,7 @@ class StringListSerializer(serializers.BaseSerializer): def to_python(cls, value, **kwargs): if not value: return [] - return value.split(',') + return value.split(",") class StringListPreference(types.BasePreferenceType): @@ -47,5 +47,5 @@ class StringListPreference(types.BasePreferenceType): def get_api_additional_data(self): d = super(StringListPreference, self).get_api_additional_data() - d['choices'] = self.get('choices') + d["choices"] = self.get("choices") return d diff --git a/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py b/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py index 1bc971f80..1c2072385 100644 --- a/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py +++ b/api/funkwhale_api/common/scripts/django_permissions_to_user_permissions.py @@ -8,22 +8,22 @@ 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', + "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) + 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() + 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}) + command.stdout.write( + "Updating {} users with {} permission...".format(total, user_permission) + ) + users.update(**{"permission_{}".format(user_permission): True}) diff --git a/api/funkwhale_api/common/scripts/test.py b/api/funkwhale_api/common/scripts/test.py index ab401dca4..b3a27f402 100644 --- a/api/funkwhale_api/common/scripts/test.py +++ b/api/funkwhale_api/common/scripts/test.py @@ -5,4 +5,4 @@ You can launch it just to check how it works. def main(command, **kwargs): - command.stdout.write('Test script run successfully') + command.stdout.write("Test script run successfully") diff --git a/api/funkwhale_api/common/serializers.py b/api/funkwhale_api/common/serializers.py index a995cc360..8cc85ec0c 100644 --- a/api/funkwhale_api/common/serializers.py +++ b/api/funkwhale_api/common/serializers.py @@ -17,67 +17,68 @@ class ActionSerializer(serializers.Serializer): dangerous_actions = [] def __init__(self, *args, **kwargs): - self.queryset = kwargs.pop('queryset') + self.queryset = kwargs.pop("queryset") if self.actions is None: raise ValueError( - 'You must declare a list of actions on ' - 'the serializer class') + "You must declare a list of actions on " "the serializer class" + ) for action in self.actions: - handler_name = 'handle_{}'.format(action) - assert hasattr(self, handler_name), ( - '{} miss a {} method'.format( - self.__class__.__name__, handler_name) + handler_name = "handle_{}".format(action) + assert hasattr(self, handler_name), "{} miss a {} method".format( + self.__class__.__name__, handler_name ) super().__init__(self, *args, **kwargs) def validate_action(self, value): if value not in self.actions: raise serializers.ValidationError( - '{} is not a valid action. Pick one of {}.'.format( - value, ', '.join(self.actions) + "{} is not a valid action. Pick one of {}.".format( + value, ", ".join(self.actions) ) ) return value def validate_objects(self, value): qs = None - if value == 'all': - return self.queryset.all().order_by('id') + if value == "all": + return self.queryset.all().order_by("id") if type(value) in [list, tuple]: - return self.queryset.filter(pk__in=value).order_by('id') + return self.queryset.filter(pk__in=value).order_by("id") raise serializers.ValidationError( - '{} is not a valid value for objects. You must provide either a ' - 'list of identifiers or the string "all".'.format(value)) + "{} is not a valid value for objects. You must provide either a " + 'list of identifiers or the string "all".'.format(value) + ) def validate(self, data): - dangerous = data['action'] in self.dangerous_actions - if dangerous and self.initial_data['objects'] == 'all': + dangerous = data["action"] in self.dangerous_actions + if dangerous and self.initial_data["objects"] == "all": raise serializers.ValidationError( - 'This action is to dangerous to be applied to all objects') - if self.filterset_class and 'filters' in data: + "This action is to dangerous to be applied to all objects" + ) + if self.filterset_class and "filters" in data: qs_filterset = self.filterset_class( - data['filters'], queryset=data['objects']) + data["filters"], queryset=data["objects"] + ) try: assert qs_filterset.form.is_valid() except (AssertionError, TypeError): - raise serializers.ValidationError('Invalid filters') - data['objects'] = qs_filterset.qs + raise serializers.ValidationError("Invalid filters") + data["objects"] = qs_filterset.qs - data['count'] = data['objects'].count() - if data['count'] < 1: - raise serializers.ValidationError( - 'No object matching your request') + data["count"] = data["objects"].count() + if data["count"] < 1: + raise serializers.ValidationError("No object matching your request") return data def save(self): - handler_name = 'handle_{}'.format(self.validated_data['action']) + handler_name = "handle_{}".format(self.validated_data["action"]) handler = getattr(self, handler_name) - result = handler(self.validated_data['objects']) + result = handler(self.validated_data["objects"]) payload = { - 'updated': self.validated_data['count'], - 'action': self.validated_data['action'], - 'result': result, + "updated": self.validated_data["count"], + "action": self.validated_data["action"], + "result": result, } return payload diff --git a/api/funkwhale_api/common/session.py b/api/funkwhale_api/common/session.py index 7f5584bd1..871fc4c21 100644 --- a/api/funkwhale_api/common/session.py +++ b/api/funkwhale_api/common/session.py @@ -6,13 +6,12 @@ import funkwhale_api def get_user_agent(): - return 'python-requests (funkwhale/{}; +{})'.format( - funkwhale_api.__version__, - settings.FUNKWHALE_URL + return "python-requests (funkwhale/{}; +{})".format( + funkwhale_api.__version__, settings.FUNKWHALE_URL ) def get_session(): s = requests.Session() - s.headers['User-Agent'] = get_user_agent() + s.headers["User-Agent"] = get_user_agent() return s diff --git a/api/funkwhale_api/common/storage.py b/api/funkwhale_api/common/storage.py index 658ce795a..c5651693f 100644 --- a/api/funkwhale_api/common/storage.py +++ b/api/funkwhale_api/common/storage.py @@ -7,6 +7,7 @@ class ASCIIFileSystemStorage(FileSystemStorage): """ Convert unicode characters in name to ASCII characters. """ + def get_valid_name(self, name): - name = unicodedata.normalize('NFKD', name).encode('ascii', 'ignore') + name = unicodedata.normalize("NFKD", name).encode("ascii", "ignore") return super().get_valid_name(name) diff --git a/api/funkwhale_api/common/utils.py b/api/funkwhale_api/common/utils.py index 2d7641bf5..f2ea52a8c 100644 --- a/api/funkwhale_api/common/utils.py +++ b/api/funkwhale_api/common/utils.py @@ -9,13 +9,13 @@ def rename_file(instance, field_name, new_name, allow_missing_file=False): field = getattr(instance, field_name) current_name, extension = os.path.splitext(field.name) - new_name_with_extension = '{}{}'.format(new_name, extension) + new_name_with_extension = "{}{}".format(new_name, extension) try: shutil.move(field.path, new_name_with_extension) except FileNotFoundError: if not allow_missing_file: raise - print('Skipped missing file', field.path) + print("Skipped missing file", field.path) initial_path = os.path.dirname(field.name) field.name = os.path.join(initial_path, new_name_with_extension) instance.save() @@ -23,9 +23,7 @@ def rename_file(instance, field_name, new_name, allow_missing_file=False): def on_commit(f, *args, **kwargs): - return transaction.on_commit( - lambda: f(*args, **kwargs) - ) + return transaction.on_commit(lambda: f(*args, **kwargs)) def set_query_parameter(url, **kwargs): diff --git a/api/funkwhale_api/contrib/sites/migrations/0001_initial.py b/api/funkwhale_api/contrib/sites/migrations/0001_initial.py index cf95cec65..8b7ec088c 100644 --- a/api/funkwhale_api/contrib/sites/migrations/0001_initial.py +++ b/api/funkwhale_api/contrib/sites/migrations/0001_initial.py @@ -7,25 +7,39 @@ import django.contrib.sites.models class Migration(migrations.Migration): - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Site', + name="Site", fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('domain', models.CharField(verbose_name='domain name', max_length=100, validators=[django.contrib.sites.models._simple_domain_name_validator])), - ('name', models.CharField(verbose_name='display name', max_length=50)), + ( + "id", + models.AutoField( + verbose_name="ID", + primary_key=True, + serialize=False, + auto_created=True, + ), + ), + ( + "domain", + models.CharField( + verbose_name="domain name", + max_length=100, + validators=[ + django.contrib.sites.models._simple_domain_name_validator + ], + ), + ), + ("name", models.CharField(verbose_name="display name", max_length=50)), ], options={ - 'verbose_name_plural': 'sites', - 'verbose_name': 'site', - 'db_table': 'django_site', - 'ordering': ('domain',), + "verbose_name_plural": "sites", + "verbose_name": "site", + "db_table": "django_site", + "ordering": ("domain",), }, - managers=[ - ('objects', django.contrib.sites.models.SiteManager()), - ], - ), + managers=[("objects", django.contrib.sites.models.SiteManager())], + ) ] diff --git a/api/funkwhale_api/contrib/sites/migrations/0002_set_site_domain_and_name.py b/api/funkwhale_api/contrib/sites/migrations/0002_set_site_domain_and_name.py index e92c8c338..7b091708c 100644 --- a/api/funkwhale_api/contrib/sites/migrations/0002_set_site_domain_and_name.py +++ b/api/funkwhale_api/contrib/sites/migrations/0002_set_site_domain_and_name.py @@ -10,10 +10,7 @@ def update_site_forward(apps, schema_editor): Site = apps.get_model("sites", "Site") Site.objects.update_or_create( id=settings.SITE_ID, - defaults={ - "domain": "funkwhale.io", - "name": "funkwhale_api" - } + defaults={"domain": "funkwhale.io", "name": "funkwhale_api"}, ) @@ -21,20 +18,12 @@ def update_site_backward(apps, schema_editor): """Revert site domain and name to default.""" Site = apps.get_model("sites", "Site") Site.objects.update_or_create( - id=settings.SITE_ID, - defaults={ - "domain": "example.com", - "name": "example.com" - } + id=settings.SITE_ID, defaults={"domain": "example.com", "name": "example.com"} ) class Migration(migrations.Migration): - dependencies = [ - ('sites', '0001_initial'), - ] + dependencies = [("sites", "0001_initial")] - operations = [ - migrations.RunPython(update_site_forward, update_site_backward), - ] + operations = [migrations.RunPython(update_site_forward, update_site_backward)] diff --git a/api/funkwhale_api/contrib/sites/migrations/0003_auto_20171214_2205.py b/api/funkwhale_api/contrib/sites/migrations/0003_auto_20171214_2205.py index 14a9ec1a8..5a903b8d1 100644 --- a/api/funkwhale_api/contrib/sites/migrations/0003_auto_20171214_2205.py +++ b/api/funkwhale_api/contrib/sites/migrations/0003_auto_20171214_2205.py @@ -8,20 +8,21 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('sites', '0002_set_site_domain_and_name'), - ] + dependencies = [("sites", "0002_set_site_domain_and_name")] operations = [ migrations.AlterModelManagers( - name='site', - managers=[ - ('objects', django.contrib.sites.models.SiteManager()), - ], + name="site", + managers=[("objects", django.contrib.sites.models.SiteManager())], ), migrations.AlterField( - model_name='site', - name='domain', - field=models.CharField(max_length=100, unique=True, validators=[django.contrib.sites.models._simple_domain_name_validator], verbose_name='domain name'), + model_name="site", + name="domain", + field=models.CharField( + max_length=100, + unique=True, + validators=[django.contrib.sites.models._simple_domain_name_validator], + verbose_name="domain name", + ), ), ] diff --git a/api/funkwhale_api/downloader/downloader.py b/api/funkwhale_api/downloader/downloader.py index 7fc237b08..3599b86ef 100644 --- a/api/funkwhale_api/downloader/downloader.py +++ b/api/funkwhale_api/downloader/downloader.py @@ -7,20 +7,15 @@ import glob def download( - url, - target_directory=settings.MEDIA_ROOT, - name="%(id)s.%(ext)s", - bitrate=192): + url, target_directory=settings.MEDIA_ROOT, name="%(id)s.%(ext)s", bitrate=192 +): target_path = os.path.join(target_directory, name) ydl_opts = { - 'quiet': True, - 'outtmpl': target_path, - 'postprocessors': [{ - 'key': 'FFmpegExtractAudio', - 'preferredcodec': 'vorbis', - }], + "quiet": True, + "outtmpl": target_path, + "postprocessors": [{"key": "FFmpegExtractAudio", "preferredcodec": "vorbis"}], } _downloader = youtube_dl.YoutubeDL(ydl_opts) info = _downloader.extract_info(url) - info['audio_file_path'] = target_path % {'id': info['id'], 'ext': 'ogg'} + info["audio_file_path"] = target_path % {"id": info["id"], "ext": "ogg"} return info diff --git a/api/funkwhale_api/factories.py b/api/funkwhale_api/factories.py index 6fed66edb..602037a06 100644 --- a/api/funkwhale_api/factories.py +++ b/api/funkwhale_api/factories.py @@ -3,7 +3,7 @@ import persisting_theory class FactoriesRegistry(persisting_theory.Registry): - look_into = 'factories' + look_into = "factories" def prepare_name(self, data, name=None): return name or data._meta.model._meta.label diff --git a/api/funkwhale_api/favorites/activities.py b/api/funkwhale_api/favorites/activities.py index a2dbc4e2f..d9d546335 100644 --- a/api/funkwhale_api/favorites/activities.py +++ b/api/funkwhale_api/favorites/activities.py @@ -3,17 +3,14 @@ from funkwhale_api.activity import record from . import serializers -record.registry.register_serializer( - serializers.TrackFavoriteActivitySerializer) +record.registry.register_serializer(serializers.TrackFavoriteActivitySerializer) -@record.registry.register_consumer('favorites.TrackFavorite') +@record.registry.register_consumer("favorites.TrackFavorite") def broadcast_track_favorite_to_instance_activity(data, obj): - if obj.user.privacy_level not in ['instance', 'everyone']: + if obj.user.privacy_level not in ["instance", "everyone"]: return - channels.group_send('instance_activity', { - 'type': 'event.send', - 'text': '', - 'data': data - }) + channels.group_send( + "instance_activity", {"type": "event.send", "text": "", "data": data} + ) diff --git a/api/funkwhale_api/favorites/admin.py b/api/funkwhale_api/favorites/admin.py index e8f29fac4..f56980e8c 100644 --- a/api/funkwhale_api/favorites/admin.py +++ b/api/funkwhale_api/favorites/admin.py @@ -5,8 +5,5 @@ from . import models @admin.register(models.TrackFavorite) class TrackFavoriteAdmin(admin.ModelAdmin): - list_display = ['user', 'track', 'creation_date'] - list_select_related = [ - 'user', - 'track' - ] + list_display = ["user", "track", "creation_date"] + list_select_related = ["user", "track"] diff --git a/api/funkwhale_api/favorites/factories.py b/api/funkwhale_api/favorites/factories.py index 233dd049c..797c135ef 100644 --- a/api/funkwhale_api/favorites/factories.py +++ b/api/funkwhale_api/favorites/factories.py @@ -12,4 +12,4 @@ class TrackFavorite(factory.django.DjangoModelFactory): user = factory.SubFactory(UserFactory) class Meta: - model = 'favorites.TrackFavorite' + model = "favorites.TrackFavorite" diff --git a/api/funkwhale_api/favorites/migrations/0001_initial.py b/api/funkwhale_api/favorites/migrations/0001_initial.py index c2bd03182..17a66462e 100644 --- a/api/funkwhale_api/favorites/migrations/0001_initial.py +++ b/api/funkwhale_api/favorites/migrations/0001_initial.py @@ -9,25 +9,47 @@ from django.conf import settings class Migration(migrations.Migration): dependencies = [ - ('music', '0003_auto_20151222_2233'), + ("music", "0003_auto_20151222_2233"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='TrackFavorite', + name="TrackFavorite", fields=[ - ('id', models.AutoField(serialize=False, auto_created=True, verbose_name='ID', primary_key=True)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('track', models.ForeignKey(related_name='track_favorites', to='music.Track', on_delete=models.CASCADE)), - ('user', models.ForeignKey(related_name='track_favorites', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + serialize=False, + auto_created=True, + verbose_name="ID", + primary_key=True, + ), + ), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "track", + models.ForeignKey( + related_name="track_favorites", + to="music.Track", + on_delete=models.CASCADE, + ), + ), + ( + "user", + models.ForeignKey( + related_name="track_favorites", + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ), + ), ], - options={ - 'ordering': ('-creation_date',), - }, + options={"ordering": ("-creation_date",)}, ), migrations.AlterUniqueTogether( - name='trackfavorite', - unique_together=set([('track', 'user')]), + name="trackfavorite", unique_together=set([("track", "user")]) ), ] diff --git a/api/funkwhale_api/favorites/models.py b/api/funkwhale_api/favorites/models.py index 0c6a6b11c..dd2b8f3ec 100644 --- a/api/funkwhale_api/favorites/models.py +++ b/api/funkwhale_api/favorites/models.py @@ -8,13 +8,15 @@ from funkwhale_api.music.models import Track class TrackFavorite(models.Model): creation_date = models.DateTimeField(default=timezone.now) user = models.ForeignKey( - 'users.User', related_name='track_favorites', on_delete=models.CASCADE) + "users.User", related_name="track_favorites", on_delete=models.CASCADE + ) track = models.ForeignKey( - Track, related_name='track_favorites', on_delete=models.CASCADE) + Track, related_name="track_favorites", on_delete=models.CASCADE + ) class Meta: - unique_together = ('track', 'user') - ordering = ('-creation_date',) + unique_together = ("track", "user") + ordering = ("-creation_date",) @classmethod def add(cls, track, user): @@ -22,5 +24,4 @@ class TrackFavorite(models.Model): return favorite def get_activity_url(self): - return '{}/favorites/tracks/{}'.format( - self.user.get_activity_url(), self.pk) + return "{}/favorites/tracks/{}".format(self.user.get_activity_url(), self.pk) diff --git a/api/funkwhale_api/favorites/serializers.py b/api/funkwhale_api/favorites/serializers.py index bb4538b2d..40260c2aa 100644 --- a/api/funkwhale_api/favorites/serializers.py +++ b/api/funkwhale_api/favorites/serializers.py @@ -11,29 +11,22 @@ from . import models class TrackFavoriteActivitySerializer(activity_serializers.ModelSerializer): type = serializers.SerializerMethodField() - object = TrackActivitySerializer(source='track') - actor = UserActivitySerializer(source='user') - published = serializers.DateTimeField(source='creation_date') + object = TrackActivitySerializer(source="track") + actor = UserActivitySerializer(source="user") + published = serializers.DateTimeField(source="creation_date") class Meta: model = models.TrackFavorite - fields = [ - 'id', - 'local_id', - 'object', - 'type', - 'actor', - 'published' - ] + fields = ["id", "local_id", "object", "type", "actor", "published"] def get_actor(self, obj): return UserActivitySerializer(obj.user).data def get_type(self, obj): - return 'Like' + return "Like" class UserTrackFavoriteSerializer(serializers.ModelSerializer): class Meta: model = models.TrackFavorite - fields = ('id', 'track', 'creation_date') + fields = ("id", "track", "creation_date") diff --git a/api/funkwhale_api/favorites/urls.py b/api/funkwhale_api/favorites/urls.py index 6a9b12a81..b85023ade 100644 --- a/api/funkwhale_api/favorites/urls.py +++ b/api/funkwhale_api/favorites/urls.py @@ -2,7 +2,8 @@ from django.conf.urls import include, url from . import views from rest_framework import routers + router = routers.SimpleRouter() -router.register(r'tracks', views.TrackFavoriteViewSet, 'tracks') +router.register(r"tracks", views.TrackFavoriteViewSet, "tracks") urlpatterns = router.urls diff --git a/api/funkwhale_api/favorites/views.py b/api/funkwhale_api/favorites/views.py index cd2aa3b61..7deae7a98 100644 --- a/api/funkwhale_api/favorites/views.py +++ b/api/funkwhale_api/favorites/views.py @@ -12,13 +12,15 @@ from . import models from . import serializers -class TrackFavoriteViewSet(mixins.CreateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): +class TrackFavoriteViewSet( + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): serializer_class = serializers.UserTrackFavoriteSerializer - queryset = (models.TrackFavorite.objects.all()) + queryset = models.TrackFavorite.objects.all() permission_classes = [ConditionalAuthentication] def create(self, request, *args, **kwargs): @@ -28,20 +30,22 @@ class TrackFavoriteViewSet(mixins.CreateModelMixin, serializer = self.get_serializer(instance=instance) headers = self.get_success_headers(serializer.data) record.send(instance) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + return Response( + serializer.data, status=status.HTTP_201_CREATED, headers=headers + ) def get_queryset(self): return self.queryset.filter(user=self.request.user) def perform_create(self, serializer): - track = Track.objects.get(pk=serializer.data['track']) + track = Track.objects.get(pk=serializer.data["track"]) favorite = models.TrackFavorite.add(track=track, user=self.request.user) return favorite - @list_route(methods=['delete', 'post']) + @list_route(methods=["delete", "post"]) def remove(self, request, *args, **kwargs): try: - pk = int(request.data['track']) + pk = int(request.data["track"]) favorite = request.user.track_favorites.get(track__pk=pk) except (AttributeError, ValueError, models.TrackFavorite.DoesNotExist): return Response({}, status=400) diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index becf6c96f..6dd26a87d 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -2,66 +2,59 @@ from . import serializers from . import tasks ACTIVITY_TYPES = [ - 'Accept', - 'Add', - 'Announce', - 'Arrive', - 'Block', - 'Create', - 'Delete', - 'Dislike', - 'Flag', - 'Follow', - 'Ignore', - 'Invite', - 'Join', - 'Leave', - 'Like', - 'Listen', - 'Move', - 'Offer', - 'Question', - 'Reject', - 'Read', - 'Remove', - 'TentativeReject', - 'TentativeAccept', - 'Travel', - 'Undo', - 'Update', - 'View', + "Accept", + "Add", + "Announce", + "Arrive", + "Block", + "Create", + "Delete", + "Dislike", + "Flag", + "Follow", + "Ignore", + "Invite", + "Join", + "Leave", + "Like", + "Listen", + "Move", + "Offer", + "Question", + "Reject", + "Read", + "Remove", + "TentativeReject", + "TentativeAccept", + "Travel", + "Undo", + "Update", + "View", ] OBJECT_TYPES = [ - 'Article', - 'Audio', - 'Collection', - 'Document', - 'Event', - 'Image', - 'Note', - 'OrderedCollection', - 'Page', - 'Place', - 'Profile', - 'Relationship', - 'Tombstone', - 'Video', + "Article", + "Audio", + "Collection", + "Document", + "Event", + "Image", + "Note", + "OrderedCollection", + "Page", + "Place", + "Profile", + "Relationship", + "Tombstone", + "Video", ] + ACTIVITY_TYPES def deliver(activity, on_behalf_of, to=[]): - return tasks.send.delay( - activity=activity, - actor_id=on_behalf_of.pk, - to=to - ) + return tasks.send.delay(activity=activity, actor_id=on_behalf_of.pk, to=to) def accept_follow(follow): serializer = serializers.AcceptFollowSerializer(follow) - return deliver( - serializer.data, - to=[follow.actor.url], - on_behalf_of=follow.target) + return deliver(serializer.data, to=[follow.actor.url], on_behalf_of=follow.target) diff --git a/api/funkwhale_api/federation/actors.py b/api/funkwhale_api/federation/actors.py index 7a209b1ff..32b545656 100644 --- a/api/funkwhale_api/federation/actors.py +++ b/api/funkwhale_api/federation/actors.py @@ -29,8 +29,10 @@ logger = logging.getLogger(__name__) def remove_tags(text): - logger.debug('Removing tags from %s', text) - return ''.join(xml.etree.ElementTree.fromstring('
{}
'.format(text)).itertext()) + logger.debug("Removing tags from %s", text) + return "".join( + xml.etree.ElementTree.fromstring("
{}
".format(text)).itertext() + ) def get_actor_data(actor_url): @@ -38,16 +40,13 @@ def get_actor_data(actor_url): actor_url, timeout=5, verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, - headers={ - 'Accept': 'application/activity+json', - } + headers={"Accept": "application/activity+json"}, ) response.raise_for_status() try: return response.json() except: - raise ValueError( - 'Invalid actor payload: {}'.format(response.text)) + raise ValueError("Invalid actor payload: {}".format(response.text)) def get_actor(actor_url): @@ -56,7 +55,8 @@ def get_actor(actor_url): except models.Actor.DoesNotExist: actor = None fetch_delta = datetime.timedelta( - minutes=preferences.get('federation__actor_fetch_delay')) + minutes=preferences.get("federation__actor_fetch_delay") + ) if actor and actor.last_fetch_date > timezone.now() - fetch_delta: # cache is hot, we can return as is return actor @@ -73,8 +73,7 @@ class SystemActor(object): def get_request_auth(self): actor = self.get_actor_instance() - return signing.get_auth( - actor.private_key, actor.private_key_id) + return signing.get_auth(actor.private_key, actor.private_key_id) def serialize(self): actor = self.get_actor_instance() @@ -88,42 +87,35 @@ class SystemActor(object): pass private, public = keys.get_key_pair() args = self.get_instance_argument( - self.id, - name=self.name, - summary=self.summary, - **self.additional_attributes + self.id, name=self.name, summary=self.summary, **self.additional_attributes ) - args['private_key'] = private.decode('utf-8') - args['public_key'] = public.decode('utf-8') + args["private_key"] = private.decode("utf-8") + args["public_key"] = public.decode("utf-8") return models.Actor.objects.create(**args) def get_actor_url(self): return utils.full_url( - reverse( - 'federation:instance-actors-detail', - kwargs={'actor': self.id})) + reverse("federation:instance-actors-detail", kwargs={"actor": self.id}) + ) def get_instance_argument(self, id, name, summary, **kwargs): p = { - 'preferred_username': id, - 'domain': settings.FEDERATION_HOSTNAME, - 'type': 'Person', - 'name': name.format(host=settings.FEDERATION_HOSTNAME), - 'manually_approves_followers': True, - 'url': self.get_actor_url(), - 'shared_inbox_url': utils.full_url( - reverse( - 'federation:instance-actors-inbox', - kwargs={'actor': id})), - 'inbox_url': utils.full_url( - reverse( - 'federation:instance-actors-inbox', - kwargs={'actor': id})), - 'outbox_url': utils.full_url( - reverse( - 'federation:instance-actors-outbox', - kwargs={'actor': id})), - 'summary': summary.format(host=settings.FEDERATION_HOSTNAME) + "preferred_username": id, + "domain": settings.FEDERATION_HOSTNAME, + "type": "Person", + "name": name.format(host=settings.FEDERATION_HOSTNAME), + "manually_approves_followers": True, + "url": self.get_actor_url(), + "shared_inbox_url": utils.full_url( + reverse("federation:instance-actors-inbox", kwargs={"actor": id}) + ), + "inbox_url": utils.full_url( + reverse("federation:instance-actors-inbox", kwargs={"actor": id}) + ), + "outbox_url": utils.full_url( + reverse("federation:instance-actors-outbox", kwargs={"actor": id}) + ), + "summary": summary.format(host=settings.FEDERATION_HOSTNAME), } p.update(kwargs) return p @@ -145,22 +137,19 @@ class SystemActor(object): Main entrypoint for handling activities posted to the actor's inbox """ - logger.info('Received activity on %s inbox', self.id) + logger.info("Received activity on %s inbox", self.id) if actor is None: - raise PermissionDenied('Actor not authenticated') + raise PermissionDenied("Actor not authenticated") - serializer = serializers.ActivitySerializer( - data=data, context={'actor': actor}) + serializer = serializers.ActivitySerializer(data=data, context={"actor": actor}) serializer.is_valid(raise_exception=True) ac = serializer.data try: - handler = getattr( - self, 'handle_{}'.format(ac['type'].lower())) + handler = getattr(self, "handle_{}".format(ac["type"].lower())) except (KeyError, AttributeError): - logger.debug( - 'No handler for activity %s', ac['type']) + logger.debug("No handler for activity %s", ac["type"]) return return handler(data, actor) @@ -168,9 +157,10 @@ class SystemActor(object): def handle_follow(self, ac, sender): system_actor = self.get_actor_instance() serializer = serializers.FollowSerializer( - data=ac, context={'follow_actor': sender}) + data=ac, context={"follow_actor": sender} + ) if not serializer.is_valid(): - return logger.info('Invalid follow payload') + return logger.info("Invalid follow payload") approved = True if not self.manually_approves_followers else None follow = serializer.save(approved=approved) if follow.approved: @@ -179,26 +169,27 @@ class SystemActor(object): def handle_accept(self, ac, sender): system_actor = self.get_actor_instance() serializer = serializers.AcceptFollowSerializer( - data=ac, - context={'follow_target': sender, 'follow_actor': system_actor}) + data=ac, context={"follow_target": sender, "follow_actor": system_actor} + ) if not serializer.is_valid(raise_exception=True): - return logger.info('Received invalid payload') + return logger.info("Received invalid payload") return serializer.save() def handle_undo_follow(self, ac, sender): system_actor = self.get_actor_instance() serializer = serializers.UndoFollowSerializer( - data=ac, context={'actor': sender, 'target': system_actor}) + data=ac, context={"actor": sender, "target": system_actor} + ) if not serializer.is_valid(): - return logger.info('Received invalid payload') + return logger.info("Received invalid payload") serializer.save() def handle_undo(self, ac, sender): - if ac['object']['type'] != 'Follow': + if ac["object"]["type"] != "Follow": return - if ac['object']['actor'] != sender.url: + if ac["object"]["actor"] != sender.url: # not the same actor, permission issue return @@ -206,55 +197,52 @@ class SystemActor(object): class LibraryActor(SystemActor): - id = 'library' - name = '{host}\'s library' - summary = 'Bot account to federate with {host}\'s library' - additional_attributes = { - 'manually_approves_followers': True - } + id = "library" + name = "{host}'s library" + summary = "Bot account to federate with {host}'s library" + additional_attributes = {"manually_approves_followers": True} def serialize(self): data = super().serialize() - urls = data.setdefault('url', []) - urls.append({ - 'type': 'Link', - 'mediaType': 'application/activity+json', - 'name': 'library', - 'href': utils.full_url(reverse('federation:music:files-list')) - }) + urls = data.setdefault("url", []) + urls.append( + { + "type": "Link", + "mediaType": "application/activity+json", + "name": "library", + "href": utils.full_url(reverse("federation:music:files-list")), + } + ) return data @property def manually_approves_followers(self): - return preferences.get('federation__music_needs_approval') + return preferences.get("federation__music_needs_approval") @transaction.atomic def handle_create(self, ac, sender): try: remote_library = models.Library.objects.get( - actor=sender, - federation_enabled=True, + actor=sender, federation_enabled=True ) except models.Library.DoesNotExist: - logger.info( - 'Skipping import, we\'re not following %s', sender.url) + logger.info("Skipping import, we're not following %s", sender.url) return - if ac['object']['type'] != 'Collection': + if ac["object"]["type"] != "Collection": return - if ac['object']['totalItems'] <= 0: + if ac["object"]["totalItems"] <= 0: return try: - items = ac['object']['items'] + items = ac["object"]["items"] except KeyError: - logger.warning('No items in collection!') + logger.warning("No items in collection!") return item_serializers = [ - serializers.AudioSerializer( - data=i, context={'library': remote_library}) + serializers.AudioSerializer(data=i, context={"library": remote_library}) for i in items ] now = timezone.now() @@ -263,27 +251,21 @@ class LibraryActor(SystemActor): if s.is_valid(): valid_serializers.append(s) else: - logger.debug( - 'Skipping invalid item %s, %s', s.initial_data, s.errors) + logger.debug("Skipping invalid item %s, %s", s.initial_data, s.errors) lts = [] for s in valid_serializers: lts.append(s.save()) if remote_library.autoimport: - batch = music_models.ImportBatch.objects.create( - source='federation', - ) + batch = music_models.ImportBatch.objects.create(source="federation") for lt in lts: if lt.creation_date < now: # track was already in the library, we do not trigger # an import continue job = music_models.ImportJob.objects.create( - batch=batch, - library_track=lt, - mbid=lt.mbid, - source=lt.url, + batch=batch, library_track=lt, mbid=lt.mbid, source=lt.url ) funkwhale_utils.on_commit( music_tasks.import_job_run.delay, @@ -293,15 +275,13 @@ class LibraryActor(SystemActor): class TestActor(SystemActor): - id = 'test' - name = '{host}\'s test account' + id = "test" + name = "{host}'s test account" summary = ( - 'Bot account to test federation with {host}. ' - 'Send me /ping and I\'ll answer you.' + "Bot account to test federation with {host}. " + "Send me /ping and I'll answer you." ) - additional_attributes = { - 'manually_approves_followers': False - } + additional_attributes = {"manually_approves_followers": False} manually_approves_followers = False def get_outbox(self, data, actor=None): @@ -309,15 +289,14 @@ class TestActor(SystemActor): "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", - {} + {}, ], "id": utils.full_url( - reverse( - 'federation:instance-actors-outbox', - kwargs={'actor': self.id})), + reverse("federation:instance-actors-outbox", kwargs={"actor": self.id}) + ), "type": "OrderedCollection", "totalItems": 0, - "orderedItems": [] + "orderedItems": [], } def parse_command(self, message): @@ -327,99 +306,86 @@ class TestActor(SystemActor): """ raw = remove_tags(message) try: - return raw.split('/')[1] + return raw.split("/")[1] except IndexError: return def handle_create(self, ac, sender): - if ac['object']['type'] != 'Note': + if ac["object"]["type"] != "Note": return # we received a toot \o/ - command = self.parse_command(ac['object']['content']) - logger.debug('Parsed command: %s', command) - if command != 'ping': + command = self.parse_command(ac["object"]["content"]) + logger.debug("Parsed command: %s", command) + if command != "ping": return now = timezone.now() test_actor = self.get_actor_instance() - reply_url = 'https://{}/activities/note/{}'.format( + reply_url = "https://{}/activities/note/{}".format( settings.FEDERATION_HOSTNAME, now.timestamp() ) - reply_content = '{} Pong!'.format( - sender.mention_username - ) + reply_content = "{} Pong!".format(sender.mention_username) reply_activity = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", - {} + {}, ], - 'type': 'Create', - 'actor': test_actor.url, - 'id': '{}/activity'.format(reply_url), - 'published': now.isoformat(), - 'to': ac['actor'], - 'cc': [], - 'object': { - 'type': 'Note', - 'content': 'Pong!', - 'summary': None, - 'published': now.isoformat(), - 'id': reply_url, - 'inReplyTo': ac['object']['id'], - 'sensitive': False, - 'url': reply_url, - 'to': [ac['actor']], - 'attributedTo': test_actor.url, - 'cc': [], - 'attachment': [], - 'tag': [{ - "type": "Mention", - "href": ac['actor'], - "name": sender.mention_username - }] - } + "type": "Create", + "actor": test_actor.url, + "id": "{}/activity".format(reply_url), + "published": now.isoformat(), + "to": ac["actor"], + "cc": [], + "object": { + "type": "Note", + "content": "Pong!", + "summary": None, + "published": now.isoformat(), + "id": reply_url, + "inReplyTo": ac["object"]["id"], + "sensitive": False, + "url": reply_url, + "to": [ac["actor"]], + "attributedTo": test_actor.url, + "cc": [], + "attachment": [], + "tag": [ + { + "type": "Mention", + "href": ac["actor"], + "name": sender.mention_username, + } + ], + }, } - activity.deliver( - reply_activity, - to=[ac['actor']], - on_behalf_of=test_actor) + activity.deliver(reply_activity, to=[ac["actor"]], on_behalf_of=test_actor) def handle_follow(self, ac, sender): super().handle_follow(ac, sender) # also, we follow back test_actor = self.get_actor_instance() follow_back = models.Follow.objects.get_or_create( - actor=test_actor, - target=sender, - approved=None, + actor=test_actor, target=sender, approved=None )[0] activity.deliver( serializers.FollowSerializer(follow_back).data, to=[follow_back.target.url], - on_behalf_of=follow_back.actor) + on_behalf_of=follow_back.actor, + ) def handle_undo_follow(self, ac, sender): super().handle_undo_follow(ac, sender) actor = self.get_actor_instance() # we also unfollow the sender, if possible try: - follow = models.Follow.objects.get( - target=sender, - actor=actor, - ) + follow = models.Follow.objects.get(target=sender, actor=actor) except models.Follow.DoesNotExist: return undo = serializers.UndoFollowSerializer(follow).data follow.delete() - activity.deliver( - undo, - to=[sender.url], - on_behalf_of=actor) + activity.deliver(undo, to=[sender.url], on_behalf_of=actor) -SYSTEM_ACTORS = { - 'library': LibraryActor(), - 'test': TestActor(), -} +SYSTEM_ACTORS = {"library": LibraryActor(), "test": TestActor()} diff --git a/api/funkwhale_api/federation/admin.py b/api/funkwhale_api/federation/admin.py index 6a097174b..a82e9aaf2 100644 --- a/api/funkwhale_api/federation/admin.py +++ b/api/funkwhale_api/federation/admin.py @@ -6,61 +6,43 @@ from . import models @admin.register(models.Actor) class ActorAdmin(admin.ModelAdmin): list_display = [ - 'url', - 'domain', - 'preferred_username', - 'type', - 'creation_date', - 'last_fetch_date'] - search_fields = ['url', 'domain', 'preferred_username'] - list_filter = [ - 'type' + "url", + "domain", + "preferred_username", + "type", + "creation_date", + "last_fetch_date", ] + search_fields = ["url", "domain", "preferred_username"] + list_filter = ["type"] @admin.register(models.Follow) class FollowAdmin(admin.ModelAdmin): - list_display = [ - 'actor', - 'target', - 'approved', - 'creation_date' - ] - list_filter = [ - 'approved' - ] - search_fields = ['actor__url', 'target__url'] + list_display = ["actor", "target", "approved", "creation_date"] + list_filter = ["approved"] + search_fields = ["actor__url", "target__url"] list_select_related = True @admin.register(models.Library) class LibraryAdmin(admin.ModelAdmin): - list_display = [ - 'actor', - 'url', - 'creation_date', - 'fetched_date', - 'tracks_count'] - search_fields = ['actor__url', 'url'] - list_filter = [ - 'federation_enabled', - 'download_files', - 'autoimport', - ] + list_display = ["actor", "url", "creation_date", "fetched_date", "tracks_count"] + search_fields = ["actor__url", "url"] + list_filter = ["federation_enabled", "download_files", "autoimport"] list_select_related = True @admin.register(models.LibraryTrack) class LibraryTrackAdmin(admin.ModelAdmin): list_display = [ - 'title', - 'artist_name', - 'album_title', - 'url', - 'library', - 'creation_date', - 'published_date', + "title", + "artist_name", + "album_title", + "url", + "library", + "creation_date", + "published_date", ] - search_fields = [ - 'library__url', 'url', 'artist_name', 'title', 'album_title'] + search_fields = ["library__url", "url", "artist_name", "title", "album_title"] list_select_related = True diff --git a/api/funkwhale_api/federation/api_urls.py b/api/funkwhale_api/federation/api_urls.py index 41dd1c0f9..625043bf6 100644 --- a/api/funkwhale_api/federation/api_urls.py +++ b/api/funkwhale_api/federation/api_urls.py @@ -3,13 +3,7 @@ from rest_framework import routers from . import views router = routers.SimpleRouter() -router.register( - r'libraries', - views.LibraryViewSet, - 'libraries') -router.register( - r'library-tracks', - views.LibraryTrackViewSet, - 'library-tracks') +router.register(r"libraries", views.LibraryViewSet, "libraries") +router.register(r"library-tracks", views.LibraryTrackViewSet, "library-tracks") urlpatterns = router.urls diff --git a/api/funkwhale_api/federation/authentication.py b/api/funkwhale_api/federation/authentication.py index bfd46084c..b669b4004 100644 --- a/api/funkwhale_api/federation/authentication.py +++ b/api/funkwhale_api/federation/authentication.py @@ -17,7 +17,7 @@ class SignatureAuthentication(authentication.BaseAuthentication): def authenticate_actor(self, request): headers = utils.clean_wsgi_headers(request.META) try: - signature = headers['Signature'] + signature = headers["Signature"] key_id = keys.get_key_id_from_signature_header(signature) except KeyError: return @@ -25,25 +25,25 @@ class SignatureAuthentication(authentication.BaseAuthentication): raise exceptions.AuthenticationFailed(str(e)) try: - actor = actors.get_actor(key_id.split('#')[0]) + actor = actors.get_actor(key_id.split("#")[0]) except Exception as e: raise exceptions.AuthenticationFailed(str(e)) if not actor.public_key: - raise exceptions.AuthenticationFailed('No public key found') + raise exceptions.AuthenticationFailed("No public key found") try: - signing.verify_django(request, actor.public_key.encode('utf-8')) + signing.verify_django(request, actor.public_key.encode("utf-8")) except cryptography.exceptions.InvalidSignature: - raise exceptions.AuthenticationFailed('Invalid signature') + raise exceptions.AuthenticationFailed("Invalid signature") return actor def authenticate(self, request): - setattr(request, 'actor', None) + setattr(request, "actor", None) actor = self.authenticate_actor(request) if not actor: return user = AnonymousUser() - setattr(request, 'actor', actor) + setattr(request, "actor", actor) return (user, None) diff --git a/api/funkwhale_api/federation/dynamic_preferences_registry.py b/api/funkwhale_api/federation/dynamic_preferences_registry.py index 8b1b2b03f..21f4076ea 100644 --- a/api/funkwhale_api/federation/dynamic_preferences_registry.py +++ b/api/funkwhale_api/federation/dynamic_preferences_registry.py @@ -4,77 +4,66 @@ from dynamic_preferences import types from dynamic_preferences.registries import global_preferences_registry from funkwhale_api.common import preferences -federation = types.Section('federation') + +federation = types.Section("federation") @global_preferences_registry.register class MusicCacheDuration(types.IntPreference): show_in_api = True section = federation - name = 'music_cache_duration' + name = "music_cache_duration" default = 60 * 24 * 2 - verbose_name = 'Music cache duration' + verbose_name = "Music cache duration" help_text = ( - 'How much minutes do you want to keep a copy of federated tracks' - 'locally? Federated files that were not listened in this interval ' - 'will be erased and refetched from the remote on the next listening.' + "How much minutes do you want to keep a copy of federated tracks" + "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, - } + field_kwargs = {"required": False} @global_preferences_registry.register class Enabled(preferences.DefaultFromSettingMixin, types.BooleanPreference): section = federation - name = 'enabled' - setting = 'FEDERATION_ENABLED' - verbose_name = 'Federation enabled' + name = "enabled" + setting = "FEDERATION_ENABLED" + verbose_name = "Federation enabled" help_text = ( - 'Use this setting to enable or disable federation logic and API' - ' globally.' + "Use this setting to enable or disable federation logic and API" " globally." ) @global_preferences_registry.register -class CollectionPageSize( - preferences.DefaultFromSettingMixin, types.IntPreference): +class CollectionPageSize(preferences.DefaultFromSettingMixin, types.IntPreference): section = federation - name = 'collection_page_size' - setting = 'FEDERATION_COLLECTION_PAGE_SIZE' - verbose_name = 'Federation collection page size' - help_text = ( - 'How much items to display in ActivityPub collections.' - ) - field_kwargs = { - 'required': False, - } + name = "collection_page_size" + setting = "FEDERATION_COLLECTION_PAGE_SIZE" + verbose_name = "Federation collection page size" + help_text = "How much items to display in ActivityPub collections." + field_kwargs = {"required": False} @global_preferences_registry.register -class ActorFetchDelay( - preferences.DefaultFromSettingMixin, types.IntPreference): +class ActorFetchDelay(preferences.DefaultFromSettingMixin, types.IntPreference): section = federation - name = 'actor_fetch_delay' - setting = 'FEDERATION_ACTOR_FETCH_DELAY' - verbose_name = 'Federation actor fetch delay' + name = "actor_fetch_delay" + setting = "FEDERATION_ACTOR_FETCH_DELAY" + verbose_name = "Federation actor fetch delay" help_text = ( - 'How much minutes to wait before refetching actors on ' - 'request authentication.' + "How much minutes to wait before refetching actors on " + "request authentication." ) - field_kwargs = { - 'required': False, - } + field_kwargs = {"required": False} @global_preferences_registry.register -class MusicNeedsApproval( - preferences.DefaultFromSettingMixin, types.BooleanPreference): +class MusicNeedsApproval(preferences.DefaultFromSettingMixin, types.BooleanPreference): section = federation - name = 'music_needs_approval' - setting = 'FEDERATION_MUSIC_NEEDS_APPROVAL' - verbose_name = 'Federation music needs approval' + name = "music_needs_approval" + setting = "FEDERATION_MUSIC_NEEDS_APPROVAL" + verbose_name = "Federation music needs approval" help_text = ( - 'When true, other federation actors will need your approval' - ' before being able to browse your library.' + "When true, other federation actors will need your approval" + " before being able to browse your library." ) diff --git a/api/funkwhale_api/federation/exceptions.py b/api/funkwhale_api/federation/exceptions.py index 31d864b36..b3fb73ab8 100644 --- a/api/funkwhale_api/federation/exceptions.py +++ b/api/funkwhale_api/federation/exceptions.py @@ -1,5 +1,3 @@ - - class MalformedPayload(ValueError): pass diff --git a/api/funkwhale_api/federation/factories.py b/api/funkwhale_api/federation/factories.py index 891609cba..52a5e8b54 100644 --- a/api/funkwhale_api/federation/factories.py +++ b/api/funkwhale_api/federation/factories.py @@ -12,29 +12,25 @@ from . import keys from . import models -registry.register(keys.get_key_pair, name='federation.KeyPair') +registry.register(keys.get_key_pair, name="federation.KeyPair") -@registry.register(name='federation.SignatureAuth') +@registry.register(name="federation.SignatureAuth") class SignatureAuthFactory(factory.Factory): - algorithm = 'rsa-sha256' + algorithm = "rsa-sha256" key = factory.LazyFunction(lambda: keys.get_key_pair()[0]) - key_id = factory.Faker('url') + key_id = factory.Faker("url") use_auth_header = False - headers = [ - '(request-target)', - 'user-agent', - 'host', - 'date', - 'content-type',] + headers = ["(request-target)", "user-agent", "host", "date", "content-type"] + class Meta: model = requests_http_signature.HTTPSignatureAuth -@registry.register(name='federation.SignedRequest') +@registry.register(name="federation.SignedRequest") class SignedRequestFactory(factory.Factory): - url = factory.Faker('url') - method = 'get' + url = factory.Faker("url") + method = "get" auth = factory.SubFactory(SignatureAuthFactory) class Meta: @@ -43,59 +39,62 @@ class SignedRequestFactory(factory.Factory): @factory.post_generation def headers(self, create, extracted, **kwargs): default_headers = { - 'User-Agent': 'Test', - 'Host': 'test.host', - 'Date': 'Right now', - 'Content-Type': 'application/activity+json' + "User-Agent": "Test", + "Host": "test.host", + "Date": "Right now", + "Content-Type": "application/activity+json", } if extracted: default_headers.update(extracted) self.headers.update(default_headers) -@registry.register(name='federation.Link') +@registry.register(name="federation.Link") class LinkFactory(factory.Factory): - type = 'Link' - href = factory.Faker('url') - mediaType = 'text/html' + type = "Link" + href = factory.Faker("url") + mediaType = "text/html" class Meta: model = dict class Params: - audio = factory.Trait( - mediaType=factory.Iterator(['audio/mp3', 'audio/ogg']) - ) + audio = factory.Trait(mediaType=factory.Iterator(["audio/mp3", "audio/ogg"])) @registry.register class ActorFactory(factory.DjangoModelFactory): public_key = None private_key = None - preferred_username = factory.Faker('user_name') - summary = factory.Faker('paragraph') - domain = factory.Faker('domain_name') - url = factory.LazyAttribute(lambda o: 'https://{}/users/{}'.format(o.domain, o.preferred_username)) - inbox_url = factory.LazyAttribute(lambda o: 'https://{}/users/{}/inbox'.format(o.domain, o.preferred_username)) - outbox_url = factory.LazyAttribute(lambda o: 'https://{}/users/{}/outbox'.format(o.domain, o.preferred_username)) + preferred_username = factory.Faker("user_name") + summary = factory.Faker("paragraph") + domain = factory.Faker("domain_name") + url = factory.LazyAttribute( + lambda o: "https://{}/users/{}".format(o.domain, o.preferred_username) + ) + inbox_url = factory.LazyAttribute( + lambda o: "https://{}/users/{}/inbox".format(o.domain, o.preferred_username) + ) + outbox_url = factory.LazyAttribute( + lambda o: "https://{}/users/{}/outbox".format(o.domain, o.preferred_username) + ) class Meta: model = models.Actor class Params: local = factory.Trait( - domain=factory.LazyAttribute( - lambda o: settings.FEDERATION_HOSTNAME) + domain=factory.LazyAttribute(lambda o: settings.FEDERATION_HOSTNAME) ) @classmethod def _generate(cls, create, attrs): - has_public = attrs.get('public_key') is not None - has_private = attrs.get('private_key') is not None + has_public = attrs.get("public_key") is not None + has_private = attrs.get("private_key") is not None if not has_public and not has_private: private, public = keys.get_key_pair() - attrs['private_key'] = private.decode('utf-8') - attrs['public_key'] = public.decode('utf-8') + attrs["private_key"] = private.decode("utf-8") + attrs["public_key"] = public.decode("utf-8") return super()._generate(create, attrs) @@ -108,15 +107,13 @@ class FollowFactory(factory.DjangoModelFactory): model = models.Follow class Params: - local = factory.Trait( - actor=factory.SubFactory(ActorFactory, local=True) - ) + local = factory.Trait(actor=factory.SubFactory(ActorFactory, local=True)) @registry.register class LibraryFactory(factory.DjangoModelFactory): actor = factory.SubFactory(ActorFactory) - url = factory.Faker('url') + url = factory.Faker("url") federation_enabled = True download_files = False autoimport = False @@ -126,42 +123,36 @@ class LibraryFactory(factory.DjangoModelFactory): class ArtistMetadataFactory(factory.Factory): - name = factory.Faker('name') + name = factory.Faker("name") class Meta: model = dict class Params: - musicbrainz = factory.Trait( - musicbrainz_id=factory.Faker('uuid4') - ) + musicbrainz = factory.Trait(musicbrainz_id=factory.Faker("uuid4")) class ReleaseMetadataFactory(factory.Factory): - title = factory.Faker('sentence') + title = factory.Faker("sentence") class Meta: model = dict class Params: - musicbrainz = factory.Trait( - musicbrainz_id=factory.Faker('uuid4') - ) + musicbrainz = factory.Trait(musicbrainz_id=factory.Faker("uuid4")) class RecordingMetadataFactory(factory.Factory): - title = factory.Faker('sentence') + title = factory.Faker("sentence") class Meta: model = dict class Params: - musicbrainz = factory.Trait( - musicbrainz_id=factory.Faker('uuid4') - ) + musicbrainz = factory.Trait(musicbrainz_id=factory.Faker("uuid4")) -@registry.register(name='federation.LibraryTrackMetadata') +@registry.register(name="federation.LibraryTrackMetadata") class LibraryTrackMetadataFactory(factory.Factory): artist = factory.SubFactory(ArtistMetadataFactory) recording = factory.SubFactory(RecordingMetadataFactory) @@ -174,64 +165,59 @@ class LibraryTrackMetadataFactory(factory.Factory): @registry.register class LibraryTrackFactory(factory.DjangoModelFactory): library = factory.SubFactory(LibraryFactory) - url = factory.Faker('url') - title = factory.Faker('sentence') - artist_name = factory.Faker('sentence') - album_title = factory.Faker('sentence') - audio_url = factory.Faker('url') - audio_mimetype = 'audio/ogg' + url = factory.Faker("url") + title = factory.Faker("sentence") + artist_name = factory.Faker("sentence") + album_title = factory.Faker("sentence") + audio_url = factory.Faker("url") + audio_mimetype = "audio/ogg" metadata = factory.SubFactory(LibraryTrackMetadataFactory) class Meta: model = models.LibraryTrack class Params: - with_audio_file = factory.Trait( - audio_file=factory.django.FileField() - ) + with_audio_file = factory.Trait(audio_file=factory.django.FileField()) -@registry.register(name='federation.Note') +@registry.register(name="federation.Note") class NoteFactory(factory.Factory): - type = 'Note' - id = factory.Faker('url') - published = factory.LazyFunction( - lambda: timezone.now().isoformat() - ) + type = "Note" + id = factory.Faker("url") + published = factory.LazyFunction(lambda: timezone.now().isoformat()) inReplyTo = None - content = factory.Faker('sentence') + content = factory.Faker("sentence") class Meta: model = dict -@registry.register(name='federation.Activity') +@registry.register(name="federation.Activity") class ActivityFactory(factory.Factory): - type = 'Create' - id = factory.Faker('url') - published = factory.LazyFunction( - lambda: timezone.now().isoformat() - ) - actor = factory.Faker('url') + type = "Create" + id = factory.Faker("url") + published = factory.LazyFunction(lambda: timezone.now().isoformat()) + actor = factory.Faker("url") object = factory.SubFactory( NoteFactory, - actor=factory.SelfAttribute('..actor'), - published=factory.SelfAttribute('..published')) + actor=factory.SelfAttribute("..actor"), + published=factory.SelfAttribute("..published"), + ) class Meta: model = dict -@registry.register(name='federation.AudioMetadata') +@registry.register(name="federation.AudioMetadata") class AudioMetadataFactory(factory.Factory): recording = factory.LazyAttribute( - lambda o: 'https://musicbrainz.org/recording/{}'.format(uuid.uuid4()) + lambda o: "https://musicbrainz.org/recording/{}".format(uuid.uuid4()) ) artist = factory.LazyAttribute( - lambda o: 'https://musicbrainz.org/artist/{}'.format(uuid.uuid4()) + lambda o: "https://musicbrainz.org/artist/{}".format(uuid.uuid4()) ) release = factory.LazyAttribute( - lambda o: 'https://musicbrainz.org/release/{}'.format(uuid.uuid4()) + lambda o: "https://musicbrainz.org/release/{}".format(uuid.uuid4()) ) bitrate = 42 length = 43 @@ -241,14 +227,12 @@ class AudioMetadataFactory(factory.Factory): model = dict -@registry.register(name='federation.Audio') +@registry.register(name="federation.Audio") class AudioFactory(factory.Factory): - type = 'Audio' - id = factory.Faker('url') - published = factory.LazyFunction( - lambda: timezone.now().isoformat() - ) - actor = factory.Faker('url') + type = "Audio" + id = factory.Faker("url") + published = factory.LazyFunction(lambda: timezone.now().isoformat()) + actor = factory.Faker("url") url = factory.SubFactory(LinkFactory, audio=True) metadata = factory.SubFactory(LibraryTrackMetadataFactory) diff --git a/api/funkwhale_api/federation/filters.py b/api/funkwhale_api/federation/filters.py index 1d93f68b9..3b5bfd739 100644 --- a/api/funkwhale_api/federation/filters.py +++ b/api/funkwhale_api/federation/filters.py @@ -6,73 +6,67 @@ from . import models class LibraryFilter(django_filters.FilterSet): - approved = django_filters.BooleanFilter('following__approved') - q = fields.SearchFilter(search_fields=[ - 'actor__domain', - ]) + approved = django_filters.BooleanFilter("following__approved") + q = fields.SearchFilter(search_fields=["actor__domain"]) class Meta: model = models.Library fields = { - 'approved': ['exact'], - 'federation_enabled': ['exact'], - 'download_files': ['exact'], - 'autoimport': ['exact'], - 'tracks_count': ['exact'], + "approved": ["exact"], + "federation_enabled": ["exact"], + "download_files": ["exact"], + "autoimport": ["exact"], + "tracks_count": ["exact"], } class LibraryTrackFilter(django_filters.FilterSet): - library = django_filters.CharFilter('library__uuid') - status = django_filters.CharFilter(method='filter_status') - q = fields.SearchFilter(search_fields=[ - 'artist_name', - 'title', - 'album_title', - 'library__actor__domain', - ]) + library = django_filters.CharFilter("library__uuid") + status = django_filters.CharFilter(method="filter_status") + q = fields.SearchFilter( + search_fields=["artist_name", "title", "album_title", "library__actor__domain"] + ) def filter_status(self, queryset, field_name, value): - if value == 'imported': + if value == "imported": return queryset.filter(local_track_file__isnull=False) - elif value == 'not_imported': - return queryset.filter( - local_track_file__isnull=True - ).exclude(import_jobs__status='pending') - elif value == 'import_pending': - return queryset.filter(import_jobs__status='pending') + elif value == "not_imported": + return queryset.filter(local_track_file__isnull=True).exclude( + import_jobs__status="pending" + ) + elif value == "import_pending": + return queryset.filter(import_jobs__status="pending") return queryset class Meta: model = models.LibraryTrack fields = { - 'library': ['exact'], - 'artist_name': ['exact', 'icontains'], - 'title': ['exact', 'icontains'], - 'album_title': ['exact', 'icontains'], - 'audio_mimetype': ['exact', 'icontains'], + "library": ["exact"], + "artist_name": ["exact", "icontains"], + "title": ["exact", "icontains"], + "album_title": ["exact", "icontains"], + "audio_mimetype": ["exact", "icontains"], } class FollowFilter(django_filters.FilterSet): - pending = django_filters.CharFilter(method='filter_pending') + pending = django_filters.CharFilter(method="filter_pending") ordering = django_filters.OrderingFilter( # tuple-mapping retains order fields=( - ('creation_date', 'creation_date'), - ('modification_date', 'modification_date'), - ), + ("creation_date", "creation_date"), + ("modification_date", "modification_date"), + ) + ) + q = fields.SearchFilter( + search_fields=["actor__domain", "actor__preferred_username"] ) - q = fields.SearchFilter(search_fields=[ - 'actor__domain', - 'actor__preferred_username', - ]) class Meta: model = models.Follow - fields = ['approved', 'pending', 'q'] + fields = ["approved", "pending", "q"] def filter_pending(self, queryset, field_name, value): - if value.lower() in ['true', '1', 'yes']: + if value.lower() in ["true", "1", "yes"]: queryset = queryset.filter(approved__isnull=True) return queryset diff --git a/api/funkwhale_api/federation/keys.py b/api/funkwhale_api/federation/keys.py index 7e9d316c2..fb6f0748c 100644 --- a/api/funkwhale_api/federation/keys.py +++ b/api/funkwhale_api/federation/keys.py @@ -7,42 +7,40 @@ import urllib.parse from . import exceptions -KEY_ID_REGEX = re.compile(r'keyId=\"(?P.*)\"') +KEY_ID_REGEX = re.compile(r"keyId=\"(?P.*)\"") def get_key_pair(size=2048): key = rsa.generate_private_key( - backend=crypto_default_backend(), - public_exponent=65537, - key_size=size + backend=crypto_default_backend(), public_exponent=65537, key_size=size ) private_key = key.private_bytes( crypto_serialization.Encoding.PEM, crypto_serialization.PrivateFormat.PKCS8, - crypto_serialization.NoEncryption()) + crypto_serialization.NoEncryption(), + ) public_key = key.public_key().public_bytes( - crypto_serialization.Encoding.PEM, - crypto_serialization.PublicFormat.PKCS1 + crypto_serialization.Encoding.PEM, crypto_serialization.PublicFormat.PKCS1 ) return private_key, public_key def get_key_id_from_signature_header(header_string): - parts = header_string.split(',') + parts = header_string.split(",") try: raw_key_id = [p for p in parts if p.startswith('keyId="')][0] except IndexError: - raise ValueError('Missing key id') + raise ValueError("Missing key id") match = KEY_ID_REGEX.match(raw_key_id) if not match: - raise ValueError('Invalid key id') + raise ValueError("Invalid key id") key_id = match.groups()[0] url = urllib.parse.urlparse(key_id) if not url.scheme or not url.netloc: - raise ValueError('Invalid url') - if url.scheme not in ['http', 'https']: - raise ValueError('Invalid shceme') + raise ValueError("Invalid url") + if url.scheme not in ["http", "https"]: + raise ValueError("Invalid shceme") return key_id diff --git a/api/funkwhale_api/federation/library.py b/api/funkwhale_api/federation/library.py index c53ce5430..28bb9a762 100644 --- a/api/funkwhale_api/federation/library.py +++ b/api/funkwhale_api/federation/library.py @@ -24,87 +24,66 @@ def scan_from_account_name(account_name): """ data = {} try: - username, domain = webfinger.clean_acct( - account_name, ensure_local=False) + username, domain = webfinger.clean_acct(account_name, ensure_local=False) except serializers.ValidationError: - return { - 'webfinger': { - 'errors': ['Invalid account string'] - } - } - system_library = actors.SYSTEM_ACTORS['library'].get_actor_instance() - library = models.Library.objects.filter( - actor__domain=domain, - actor__preferred_username=username - ).select_related('actor').first() - data['local'] = { - 'following': False, - 'awaiting_approval': False, - } + return {"webfinger": {"errors": ["Invalid account string"]}} + system_library = actors.SYSTEM_ACTORS["library"].get_actor_instance() + library = ( + models.Library.objects.filter( + actor__domain=domain, actor__preferred_username=username + ) + .select_related("actor") + .first() + ) + data["local"] = {"following": False, "awaiting_approval": False} try: follow = models.Follow.objects.get( target__preferred_username=username, target__domain=username, actor=system_library, ) - data['local']['awaiting_approval'] = not bool(follow.approved) - data['local']['following'] = True + data["local"]["awaiting_approval"] = not bool(follow.approved) + data["local"]["following"] = True except models.Follow.DoesNotExist: pass try: - data['webfinger'] = webfinger.get_resource( - 'acct:{}'.format(account_name)) + data["webfinger"] = webfinger.get_resource("acct:{}".format(account_name)) except requests.ConnectionError: - return { - 'webfinger': { - 'errors': ['This webfinger resource is not reachable'] - } - } + return {"webfinger": {"errors": ["This webfinger resource is not reachable"]}} except requests.HTTPError as e: return { - 'webfinger': { - 'errors': [ - 'Error {} during webfinger request'.format( - e.response.status_code)] + "webfinger": { + "errors": [ + "Error {} during webfinger request".format(e.response.status_code) + ] } } except json.JSONDecodeError as e: - return { - 'webfinger': { - 'errors': ['Could not process webfinger response'] - } - } + return {"webfinger": {"errors": ["Could not process webfinger response"]}} try: - data['actor'] = actors.get_actor_data(data['webfinger']['actor_url']) + data["actor"] = actors.get_actor_data(data["webfinger"]["actor_url"]) except requests.ConnectionError: - data['actor'] = { - 'errors': ['This actor is not reachable'] - } + data["actor"] = {"errors": ["This actor is not reachable"]} return data except requests.HTTPError as e: - data['actor'] = { - 'errors': [ - 'Error {} during actor request'.format( - e.response.status_code)] + data["actor"] = { + "errors": ["Error {} during actor request".format(e.response.status_code)] } return data - serializer = serializers.LibraryActorSerializer(data=data['actor']) + serializer = serializers.LibraryActorSerializer(data=data["actor"]) if not serializer.is_valid(): - data['actor'] = { - 'errors': ['Invalid ActivityPub actor'] - } + data["actor"] = {"errors": ["Invalid ActivityPub actor"]} return data - data['library'] = get_library_data( - serializer.validated_data['library_url']) + data["library"] = get_library_data(serializer.validated_data["library_url"]) return data def get_library_data(library_url): - actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() auth = signing.get_auth(actor.private_key, actor.private_key_id) try: response = session.get_session().get( @@ -112,55 +91,37 @@ def get_library_data(library_url): auth=auth, timeout=5, verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, - headers={ - 'Content-Type': 'application/activity+json' - } + headers={"Content-Type": "application/activity+json"}, ) except requests.ConnectionError: - return { - 'errors': ['This library is not reachable'] - } + return {"errors": ["This library is not reachable"]} scode = response.status_code if scode == 401: - return { - 'errors': ['This library requires authentication'] - } + return {"errors": ["This library requires authentication"]} elif scode == 403: - return { - 'errors': ['Permission denied while scanning library'] - } + return {"errors": ["Permission denied while scanning library"]} elif scode >= 400: - return { - 'errors': ['Error {} while fetching the library'.format(scode)] - } - serializer = serializers.PaginatedCollectionSerializer( - data=response.json(), - ) + return {"errors": ["Error {} while fetching the library".format(scode)]} + serializer = serializers.PaginatedCollectionSerializer(data=response.json()) if not serializer.is_valid(): - return { - 'errors': [ - 'Invalid ActivityPub response from remote library'] - } + return {"errors": ["Invalid ActivityPub response from remote library"]} return serializer.validated_data def get_library_page(library, page_url): - actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() auth = signing.get_auth(actor.private_key, actor.private_key_id) response = session.get_session().get( page_url, auth=auth, timeout=5, verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, - headers={ - 'Content-Type': 'application/activity+json' - } + headers={"Content-Type": "application/activity+json"}, ) serializer = serializers.CollectionPageSerializer( data=response.json(), - context={ - 'library': library, - 'item_serializer': serializers.AudioSerializer}) + context={"library": library, "item_serializer": serializers.AudioSerializer}, + ) serializer.is_valid(raise_exception=True) return serializer.validated_data diff --git a/api/funkwhale_api/federation/migrations/0001_initial.py b/api/funkwhale_api/federation/migrations/0001_initial.py index a9157e57e..a4c641b4e 100644 --- a/api/funkwhale_api/federation/migrations/0001_initial.py +++ b/api/funkwhale_api/federation/migrations/0001_initial.py @@ -8,30 +8,74 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='Actor', + name="Actor", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('url', models.URLField(db_index=True, max_length=500, unique=True)), - ('outbox_url', models.URLField(max_length=500)), - ('inbox_url', models.URLField(max_length=500)), - ('following_url', models.URLField(blank=True, max_length=500, null=True)), - ('followers_url', models.URLField(blank=True, max_length=500, null=True)), - ('shared_inbox_url', models.URLField(blank=True, max_length=500, null=True)), - ('type', models.CharField(choices=[('Person', 'Person'), ('Application', 'Application'), ('Group', 'Group'), ('Organization', 'Organization'), ('Service', 'Service')], default='Person', max_length=25)), - ('name', models.CharField(blank=True, max_length=200, null=True)), - ('domain', models.CharField(max_length=1000)), - ('summary', models.CharField(blank=True, max_length=500, null=True)), - ('preferred_username', models.CharField(blank=True, max_length=200, null=True)), - ('public_key', models.CharField(blank=True, max_length=5000, null=True)), - ('private_key', models.CharField(blank=True, max_length=5000, null=True)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('last_fetch_date', models.DateTimeField(default=django.utils.timezone.now)), - ('manually_approves_followers', models.NullBooleanField(default=None)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("url", models.URLField(db_index=True, max_length=500, unique=True)), + ("outbox_url", models.URLField(max_length=500)), + ("inbox_url", models.URLField(max_length=500)), + ( + "following_url", + models.URLField(blank=True, max_length=500, null=True), + ), + ( + "followers_url", + models.URLField(blank=True, max_length=500, null=True), + ), + ( + "shared_inbox_url", + models.URLField(blank=True, max_length=500, null=True), + ), + ( + "type", + models.CharField( + choices=[ + ("Person", "Person"), + ("Application", "Application"), + ("Group", "Group"), + ("Organization", "Organization"), + ("Service", "Service"), + ], + default="Person", + max_length=25, + ), + ), + ("name", models.CharField(blank=True, max_length=200, null=True)), + ("domain", models.CharField(max_length=1000)), + ("summary", models.CharField(blank=True, max_length=500, null=True)), + ( + "preferred_username", + models.CharField(blank=True, max_length=200, null=True), + ), + ( + "public_key", + models.CharField(blank=True, max_length=5000, null=True), + ), + ( + "private_key", + models.CharField(blank=True, max_length=5000, null=True), + ), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "last_fetch_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("manually_approves_followers", models.NullBooleanField(default=None)), ], - ), + ) ] diff --git a/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py b/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py index 2200424d8..9c848ac58 100644 --- a/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py +++ b/api/funkwhale_api/federation/migrations/0002_auto_20180403_1620.py @@ -5,13 +5,10 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('federation', '0001_initial'), - ] + dependencies = [("federation", "0001_initial")] operations = [ migrations.AlterUniqueTogether( - name='actor', - unique_together={('domain', 'preferred_username')}, - ), + name="actor", unique_together={("domain", "preferred_username")} + ) ] diff --git a/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py index 12e3d73fe..021b2ad1c 100644 --- a/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py +++ b/api/funkwhale_api/federation/migrations/0003_auto_20180407_1010.py @@ -10,7 +10,7 @@ import uuid def delete_system_actors(apps, schema_editor): """Revert site domain and name to default.""" Actor = apps.get_model("federation", "Actor") - Actor.objects.filter(preferred_username__in=['test', 'library']).delete() + Actor.objects.filter(preferred_username__in=["test", "library"]).delete() def backward(apps, schema_editor): @@ -19,76 +19,168 @@ def backward(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('federation', '0002_auto_20180403_1620'), - ] + dependencies = [("federation", "0002_auto_20180403_1620")] operations = [ migrations.RunPython(delete_system_actors, backward), migrations.CreateModel( - name='Follow', + name="Follow", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('modification_date', models.DateTimeField(auto_now=True)), - ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emitted_follows', to='federation.Actor')), - ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follows', to='federation.Actor')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("uuid", models.UUIDField(default=uuid.uuid4, unique=True)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("modification_date", models.DateTimeField(auto_now=True)), + ( + "actor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="emitted_follows", + to="federation.Actor", + ), + ), + ( + "target", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="received_follows", + to="federation.Actor", + ), + ), ], ), migrations.CreateModel( - name='FollowRequest', + name="FollowRequest", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('modification_date', models.DateTimeField(auto_now=True)), - ('approved', models.NullBooleanField(default=None)), - ('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emmited_follow_requests', to='federation.Actor')), - ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_follow_requests', to='federation.Actor')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("uuid", models.UUIDField(default=uuid.uuid4, unique=True)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("modification_date", models.DateTimeField(auto_now=True)), + ("approved", models.NullBooleanField(default=None)), + ( + "actor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="emmited_follow_requests", + to="federation.Actor", + ), + ), + ( + "target", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="received_follow_requests", + to="federation.Actor", + ), + ), ], ), migrations.CreateModel( - name='Library', + name="Library", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('modification_date', models.DateTimeField(auto_now=True)), - ('fetched_date', models.DateTimeField(blank=True, null=True)), - ('uuid', models.UUIDField(default=uuid.uuid4)), - ('url', models.URLField()), - ('federation_enabled', models.BooleanField()), - ('download_files', models.BooleanField()), - ('autoimport', models.BooleanField()), - ('tracks_count', models.PositiveIntegerField(blank=True, null=True)), - ('actor', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='library', to='federation.Actor')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("modification_date", models.DateTimeField(auto_now=True)), + ("fetched_date", models.DateTimeField(blank=True, null=True)), + ("uuid", models.UUIDField(default=uuid.uuid4)), + ("url", models.URLField()), + ("federation_enabled", models.BooleanField()), + ("download_files", models.BooleanField()), + ("autoimport", models.BooleanField()), + ("tracks_count", models.PositiveIntegerField(blank=True, null=True)), + ( + "actor", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="library", + to="federation.Actor", + ), + ), ], ), migrations.CreateModel( - name='LibraryTrack', + name="LibraryTrack", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('url', models.URLField(unique=True)), - ('audio_url', models.URLField()), - ('audio_mimetype', models.CharField(max_length=200)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('modification_date', models.DateTimeField(auto_now=True)), - ('fetched_date', models.DateTimeField(blank=True, null=True)), - ('published_date', models.DateTimeField(blank=True, null=True)), - ('artist_name', models.CharField(max_length=500)), - ('album_title', models.CharField(max_length=500)), - ('title', models.CharField(max_length=500)), - ('metadata', django.contrib.postgres.fields.jsonb.JSONField(default={}, max_length=10000)), - ('library', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracks', to='federation.Library')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("url", models.URLField(unique=True)), + ("audio_url", models.URLField()), + ("audio_mimetype", models.CharField(max_length=200)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("modification_date", models.DateTimeField(auto_now=True)), + ("fetched_date", models.DateTimeField(blank=True, null=True)), + ("published_date", models.DateTimeField(blank=True, null=True)), + ("artist_name", models.CharField(max_length=500)), + ("album_title", models.CharField(max_length=500)), + ("title", models.CharField(max_length=500)), + ( + "metadata", + django.contrib.postgres.fields.jsonb.JSONField( + default={}, max_length=10000 + ), + ), + ( + "library", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="tracks", + to="federation.Library", + ), + ), ], ), migrations.AddField( - model_name='actor', - name='followers', - field=models.ManyToManyField(related_name='following', through='federation.Follow', to='federation.Actor'), + model_name="actor", + name="followers", + field=models.ManyToManyField( + related_name="following", + through="federation.Follow", + to="federation.Actor", + ), ), migrations.AlterUniqueTogether( - name='follow', - unique_together={('actor', 'target')}, + name="follow", unique_together={("actor", "target")} ), ] diff --git a/api/funkwhale_api/federation/migrations/0004_auto_20180410_2025.py b/api/funkwhale_api/federation/migrations/0004_auto_20180410_2025.py index bea4d14ae..f0e5cf1d6 100644 --- a/api/funkwhale_api/federation/migrations/0004_auto_20180410_2025.py +++ b/api/funkwhale_api/federation/migrations/0004_auto_20180410_2025.py @@ -6,30 +6,26 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('federation', '0003_auto_20180407_1010'), - ] + dependencies = [("federation", "0003_auto_20180407_1010")] operations = [ - migrations.RemoveField( - model_name='followrequest', - name='actor', - ), - migrations.RemoveField( - model_name='followrequest', - name='target', - ), + migrations.RemoveField(model_name="followrequest", name="actor"), + migrations.RemoveField(model_name="followrequest", name="target"), migrations.AddField( - model_name='follow', - name='approved', + model_name="follow", + name="approved", field=models.NullBooleanField(default=None), ), migrations.AddField( - model_name='library', - name='follow', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='library', to='federation.Follow'), - ), - migrations.DeleteModel( - name='FollowRequest', + model_name="library", + name="follow", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="library", + to="federation.Follow", + ), ), + migrations.DeleteModel(name="FollowRequest"), ] diff --git a/api/funkwhale_api/federation/migrations/0005_auto_20180413_1723.py b/api/funkwhale_api/federation/migrations/0005_auto_20180413_1723.py index 00ba5c83d..0b2029e95 100644 --- a/api/funkwhale_api/federation/migrations/0005_auto_20180413_1723.py +++ b/api/funkwhale_api/federation/migrations/0005_auto_20180413_1723.py @@ -8,19 +8,25 @@ import funkwhale_api.federation.models class Migration(migrations.Migration): - dependencies = [ - ('federation', '0004_auto_20180410_2025'), - ] + dependencies = [("federation", "0004_auto_20180410_2025")] operations = [ migrations.AddField( - model_name='librarytrack', - name='audio_file', - field=models.FileField(blank=True, null=True, upload_to=funkwhale_api.federation.models.get_file_path), + model_name="librarytrack", + name="audio_file", + field=models.FileField( + blank=True, + null=True, + upload_to=funkwhale_api.federation.models.get_file_path, + ), ), migrations.AlterField( - model_name='librarytrack', - name='metadata', - field=django.contrib.postgres.fields.jsonb.JSONField(default={}, encoder=django.core.serializers.json.DjangoJSONEncoder, max_length=10000), + model_name="librarytrack", + name="metadata", + field=django.contrib.postgres.fields.jsonb.JSONField( + default={}, + encoder=django.core.serializers.json.DjangoJSONEncoder, + max_length=10000, + ), ), ] diff --git a/api/funkwhale_api/federation/migrations/0006_auto_20180521_1702.py b/api/funkwhale_api/federation/migrations/0006_auto_20180521_1702.py index 7dcf85670..eb731f0aa 100644 --- a/api/funkwhale_api/federation/migrations/0006_auto_20180521_1702.py +++ b/api/funkwhale_api/federation/migrations/0006_auto_20180521_1702.py @@ -5,24 +5,20 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('federation', '0005_auto_20180413_1723'), - ] + dependencies = [("federation", "0005_auto_20180413_1723")] operations = [ migrations.AlterField( - model_name='library', - name='url', + model_name="library", name="url", field=models.URLField(max_length=500) + ), + migrations.AlterField( + model_name="librarytrack", + name="audio_url", field=models.URLField(max_length=500), ), migrations.AlterField( - model_name='librarytrack', - name='audio_url', - field=models.URLField(max_length=500), - ), - migrations.AlterField( - model_name='librarytrack', - name='url', + model_name="librarytrack", + name="url", field=models.URLField(max_length=500, unique=True), ), ] diff --git a/api/funkwhale_api/federation/models.py b/api/funkwhale_api/federation/models.py index 8b4f28475..398ab23c4 100644 --- a/api/funkwhale_api/federation/models.py +++ b/api/funkwhale_api/federation/models.py @@ -12,16 +12,16 @@ from funkwhale_api.common import session from funkwhale_api.music import utils as music_utils TYPE_CHOICES = [ - ('Person', 'Person'), - ('Application', 'Application'), - ('Group', 'Group'), - ('Organization', 'Organization'), - ('Service', 'Service'), + ("Person", "Person"), + ("Application", "Application"), + ("Group", "Group"), + ("Organization", "Organization"), + ("Service", "Service"), ] class Actor(models.Model): - ap_type = 'Actor' + ap_type = "Actor" url = models.URLField(unique=True, max_length=500, db_index=True) outbox_url = models.URLField(max_length=500) @@ -29,49 +29,41 @@ class Actor(models.Model): following_url = models.URLField(max_length=500, null=True, blank=True) followers_url = models.URLField(max_length=500, null=True, blank=True) shared_inbox_url = models.URLField(max_length=500, null=True, blank=True) - type = models.CharField( - choices=TYPE_CHOICES, default='Person', max_length=25) + type = models.CharField(choices=TYPE_CHOICES, default="Person", max_length=25) name = models.CharField(max_length=200, null=True, blank=True) domain = models.CharField(max_length=1000) summary = models.CharField(max_length=500, null=True, blank=True) - preferred_username = models.CharField( - max_length=200, null=True, blank=True) + preferred_username = models.CharField(max_length=200, null=True, blank=True) public_key = models.CharField(max_length=5000, null=True, blank=True) private_key = models.CharField(max_length=5000, null=True, blank=True) creation_date = models.DateTimeField(default=timezone.now) - last_fetch_date = models.DateTimeField( - default=timezone.now) + last_fetch_date = models.DateTimeField(default=timezone.now) manually_approves_followers = models.NullBooleanField(default=None) followers = models.ManyToManyField( - to='self', + to="self", symmetrical=False, - through='Follow', - through_fields=('target', 'actor'), - related_name='following', + through="Follow", + through_fields=("target", "actor"), + related_name="following", ) class Meta: - unique_together = ['domain', 'preferred_username'] + unique_together = ["domain", "preferred_username"] @property def webfinger_subject(self): - return '{}@{}'.format( - self.preferred_username, - settings.FEDERATION_HOSTNAME, - ) + return "{}@{}".format(self.preferred_username, settings.FEDERATION_HOSTNAME) @property def private_key_id(self): - return '{}#main-key'.format(self.url) + return "{}#main-key".format(self.url) @property def mention_username(self): - return '@{}@{}'.format(self.preferred_username, self.domain) + return "@{}@{}".format(self.preferred_username, self.domain) def save(self, **kwargs): - lowercase_fields = [ - 'domain', - ] + lowercase_fields = ["domain"] for field in lowercase_fields: v = getattr(self, field, None) if v: @@ -86,58 +78,54 @@ class Actor(models.Model): @property def is_system(self): from . import actors - return all([ - settings.FEDERATION_HOSTNAME == self.domain, - self.preferred_username in actors.SYSTEM_ACTORS - ]) + + return all( + [ + settings.FEDERATION_HOSTNAME == self.domain, + self.preferred_username in actors.SYSTEM_ACTORS, + ] + ) @property def system_conf(self): from . import actors + if self.is_system: return actors.SYSTEM_ACTORS[self.preferred_username] def get_approved_followers(self): follows = self.received_follows.filter(approved=True) - return self.followers.filter( - pk__in=follows.values_list('actor', flat=True)) + return self.followers.filter(pk__in=follows.values_list("actor", flat=True)) class Follow(models.Model): - ap_type = 'Follow' + ap_type = "Follow" uuid = models.UUIDField(default=uuid.uuid4, unique=True) actor = models.ForeignKey( - Actor, - related_name='emitted_follows', - on_delete=models.CASCADE, + Actor, related_name="emitted_follows", on_delete=models.CASCADE ) target = models.ForeignKey( - Actor, - related_name='received_follows', - on_delete=models.CASCADE, + Actor, related_name="received_follows", on_delete=models.CASCADE ) creation_date = models.DateTimeField(default=timezone.now) - modification_date = models.DateTimeField( - auto_now=True) + modification_date = models.DateTimeField(auto_now=True) approved = models.NullBooleanField(default=None) class Meta: - unique_together = ['actor', 'target'] + unique_together = ["actor", "target"] def get_federation_url(self): - return '{}#follows/{}'.format(self.actor.url, self.uuid) + return "{}#follows/{}".format(self.actor.url, self.uuid) class Library(models.Model): creation_date = models.DateTimeField(default=timezone.now) - modification_date = models.DateTimeField( - auto_now=True) + modification_date = models.DateTimeField(auto_now=True) fetched_date = models.DateTimeField(null=True, blank=True) actor = models.OneToOneField( - Actor, - on_delete=models.CASCADE, - related_name='library') + Actor, on_delete=models.CASCADE, related_name="library" + ) uuid = models.UUIDField(default=uuid.uuid4) url = models.URLField(max_length=500) @@ -149,69 +137,60 @@ class Library(models.Model): autoimport = models.BooleanField() tracks_count = models.PositiveIntegerField(null=True, blank=True) follow = models.OneToOneField( - Follow, - related_name='library', - null=True, - blank=True, - on_delete=models.SET_NULL, + Follow, related_name="library", null=True, blank=True, on_delete=models.SET_NULL ) def get_file_path(instance, filename): uid = str(uuid.uuid4()) chunk_size = 2 - chunks = [uid[i:i+chunk_size] for i in range(0, len(uid), chunk_size)] + chunks = [uid[i : i + chunk_size] for i in range(0, len(uid), chunk_size)] parts = chunks[:3] + [filename] - return os.path.join('federation_cache', *parts) + return os.path.join("federation_cache", *parts) class LibraryTrack(models.Model): url = models.URLField(unique=True, max_length=500) audio_url = models.URLField(max_length=500) audio_mimetype = models.CharField(max_length=200) - audio_file = models.FileField( - upload_to=get_file_path, - null=True, - blank=True) + audio_file = models.FileField(upload_to=get_file_path, null=True, blank=True) creation_date = models.DateTimeField(default=timezone.now) - modification_date = models.DateTimeField( - auto_now=True) + modification_date = models.DateTimeField(auto_now=True) fetched_date = models.DateTimeField(null=True, blank=True) published_date = models.DateTimeField(null=True, blank=True) library = models.ForeignKey( - Library, related_name='tracks', on_delete=models.CASCADE) + Library, related_name="tracks", on_delete=models.CASCADE + ) artist_name = models.CharField(max_length=500) album_title = models.CharField(max_length=500) title = models.CharField(max_length=500) - metadata = JSONField( - default={}, max_length=10000, encoder=DjangoJSONEncoder) + metadata = JSONField(default={}, max_length=10000, encoder=DjangoJSONEncoder) @property def mbid(self): try: - return self.metadata['recording']['musicbrainz_id'] + return self.metadata["recording"]["musicbrainz_id"] except KeyError: pass def download_audio(self): from . import actors - auth = actors.SYSTEM_ACTORS['library'].get_request_auth() + + auth = actors.SYSTEM_ACTORS["library"].get_request_auth() remote_response = session.get_session().get( self.audio_url, auth=auth, stream=True, timeout=20, verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, - headers={ - 'Content-Type': 'application/activity+json' - } + headers={"Content-Type": "application/activity+json"}, ) with remote_response as r: remote_response.raise_for_status() extension = music_utils.get_ext_from_type(self.audio_mimetype) - title = ' - '.join([self.title, self.album_title, self.artist_name]) - filename = '{}.{}'.format(title, extension) + title = " - ".join([self.title, self.album_title, self.artist_name]) + filename = "{}.{}".format(title, extension) tmp_file = tempfile.TemporaryFile() for chunk in r.iter_content(chunk_size=512): tmp_file.write(chunk) diff --git a/api/funkwhale_api/federation/parsers.py b/api/funkwhale_api/federation/parsers.py index 874d808f9..8afe21a23 100644 --- a/api/funkwhale_api/federation/parsers.py +++ b/api/funkwhale_api/federation/parsers.py @@ -2,4 +2,4 @@ from rest_framework import parsers class ActivityParser(parsers.JSONParser): - media_type = 'application/activity+json' + media_type = "application/activity+json" diff --git a/api/funkwhale_api/federation/permissions.py b/api/funkwhale_api/federation/permissions.py index 438b675cb..ccffb1014 100644 --- a/api/funkwhale_api/federation/permissions.py +++ b/api/funkwhale_api/federation/permissions.py @@ -7,15 +7,13 @@ from . import actors class LibraryFollower(BasePermission): - def has_permission(self, request, view): - if not preferences.get('federation__music_needs_approval'): + if not preferences.get("federation__music_needs_approval"): return True - actor = getattr(request, 'actor', None) + actor = getattr(request, "actor", None) if actor is None: return False - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() - return library.received_follows.filter( - approved=True, actor=actor).exists() + library = actors.SYSTEM_ACTORS["library"].get_actor_instance() + return library.received_follows.filter(approved=True, actor=actor).exists() diff --git a/api/funkwhale_api/federation/renderers.py b/api/funkwhale_api/federation/renderers.py index 642b63462..d72c4c06a 100644 --- a/api/funkwhale_api/federation/renderers.py +++ b/api/funkwhale_api/federation/renderers.py @@ -2,8 +2,8 @@ from rest_framework.renderers import JSONRenderer class ActivityPubRenderer(JSONRenderer): - media_type = 'application/activity+json' + media_type = "application/activity+json" class WebfingerRenderer(JSONRenderer): - media_type = 'application/jrd+json' + media_type = "application/jrd+json" diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 6ffffaa9a..367ca9929 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -20,8 +20,8 @@ from . import utils AP_CONTEXT = [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", {}, ] @@ -43,58 +43,58 @@ class ActorSerializer(serializers.Serializer): def to_representation(self, instance): ret = { - 'id': instance.url, - 'outbox': instance.outbox_url, - 'inbox': instance.inbox_url, - 'preferredUsername': instance.preferred_username, - 'type': instance.type, + "id": instance.url, + "outbox": instance.outbox_url, + "inbox": instance.inbox_url, + "preferredUsername": instance.preferred_username, + "type": instance.type, } if instance.name: - ret['name'] = instance.name + ret["name"] = instance.name if instance.followers_url: - ret['followers'] = instance.followers_url + ret["followers"] = instance.followers_url if instance.following_url: - ret['following'] = instance.following_url + ret["following"] = instance.following_url if instance.summary: - ret['summary'] = instance.summary + ret["summary"] = instance.summary if instance.manually_approves_followers is not None: - ret['manuallyApprovesFollowers'] = instance.manually_approves_followers + ret["manuallyApprovesFollowers"] = instance.manually_approves_followers - ret['@context'] = AP_CONTEXT + ret["@context"] = AP_CONTEXT if instance.public_key: - ret['publicKey'] = { - 'owner': instance.url, - 'publicKeyPem': instance.public_key, - 'id': '{}#main-key'.format(instance.url) + ret["publicKey"] = { + "owner": instance.url, + "publicKeyPem": instance.public_key, + "id": "{}#main-key".format(instance.url), } - ret['endpoints'] = {} + ret["endpoints"] = {} if instance.shared_inbox_url: - ret['endpoints']['sharedInbox'] = instance.shared_inbox_url + ret["endpoints"]["sharedInbox"] = instance.shared_inbox_url return ret def prepare_missing_fields(self): kwargs = { - 'url': self.validated_data['id'], - 'outbox_url': self.validated_data['outbox'], - 'inbox_url': self.validated_data['inbox'], - 'following_url': self.validated_data.get('following'), - 'followers_url': self.validated_data.get('followers'), - 'summary': self.validated_data.get('summary'), - 'type': self.validated_data['type'], - 'name': self.validated_data.get('name'), - 'preferred_username': self.validated_data['preferredUsername'], + "url": self.validated_data["id"], + "outbox_url": self.validated_data["outbox"], + "inbox_url": self.validated_data["inbox"], + "following_url": self.validated_data.get("following"), + "followers_url": self.validated_data.get("followers"), + "summary": self.validated_data.get("summary"), + "type": self.validated_data["type"], + "name": self.validated_data.get("name"), + "preferred_username": self.validated_data["preferredUsername"], } - maf = self.validated_data.get('manuallyApprovesFollowers') + maf = self.validated_data.get("manuallyApprovesFollowers") if maf is not None: - kwargs['manually_approves_followers'] = maf - domain = urllib.parse.urlparse(kwargs['url']).netloc - kwargs['domain'] = domain - for endpoint, url in self.initial_data.get('endpoints', {}).items(): - if endpoint == 'sharedInbox': - kwargs['shared_inbox_url'] = url + kwargs["manually_approves_followers"] = maf + domain = urllib.parse.urlparse(kwargs["url"]).netloc + kwargs["domain"] = domain + for endpoint, url in self.initial_data.get("endpoints", {}).items(): + if endpoint == "sharedInbox": + kwargs["shared_inbox_url"] = url break try: - kwargs['public_key'] = self.initial_data['publicKey']['publicKeyPem'] + kwargs["public_key"] = self.initial_data["publicKey"]["publicKeyPem"] except KeyError: pass return kwargs @@ -106,10 +106,7 @@ class ActorSerializer(serializers.Serializer): def save(self, **kwargs): d = self.prepare_missing_fields() d.update(kwargs) - return models.Actor.objects.update_or_create( - url=d['url'], - defaults=d, - )[0] + return models.Actor.objects.update_or_create(url=d["url"], defaults=d)[0] def validate_summary(self, value): if value: @@ -120,35 +117,33 @@ class APIActorSerializer(serializers.ModelSerializer): class Meta: model = models.Actor fields = [ - 'id', - 'url', - 'creation_date', - 'summary', - 'preferred_username', - 'name', - 'last_fetch_date', - 'domain', - 'type', - 'manually_approves_followers', - + "id", + "url", + "creation_date", + "summary", + "preferred_username", + "name", + "last_fetch_date", + "domain", + "type", + "manually_approves_followers", ] class LibraryActorSerializer(ActorSerializer): - url = serializers.ListField( - child=serializers.JSONField()) + url = serializers.ListField(child=serializers.JSONField()) def validate(self, validated_data): try: - urls = validated_data['url'] + urls = validated_data["url"] except KeyError: - raise serializers.ValidationError('Missing URL field') + raise serializers.ValidationError("Missing URL field") for u in urls: try: - if u['name'] != 'library': + if u["name"] != "library": continue - validated_data['library_url'] = u['href'] + validated_data["library_url"] = u["href"] break except KeyError: continue @@ -160,12 +155,12 @@ class APIFollowSerializer(serializers.ModelSerializer): class Meta: model = models.Follow fields = [ - 'uuid', - 'actor', - 'target', - 'approved', - 'creation_date', - 'modification_date', + "uuid", + "actor", + "target", + "approved", + "creation_date", + "modification_date", ] @@ -177,19 +172,19 @@ class APILibrarySerializer(serializers.ModelSerializer): model = models.Library read_only_fields = [ - 'actor', - 'uuid', - 'url', - 'tracks_count', - 'follow', - 'fetched_date', - 'modification_date', - 'creation_date', + "actor", + "uuid", + "url", + "tracks_count", + "follow", + "fetched_date", + "modification_date", + "creation_date", ] fields = [ - 'autoimport', - 'federation_enabled', - 'download_files', + "autoimport", + "federation_enabled", + "download_files", ] + read_only_fields @@ -203,24 +198,22 @@ class APILibraryFollowUpdateSerializer(serializers.Serializer): def validate_follow(self, value): from . import actors - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - qs = models.Follow.objects.filter( - pk=value, - target=library_actor, - ) + + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + qs = models.Follow.objects.filter(pk=value, target=library_actor) try: return qs.get() except models.Follow.DoesNotExist: - raise serializers.ValidationError('Invalid follow') + raise serializers.ValidationError("Invalid follow") def save(self): - new_status = self.validated_data['approved'] - follow = self.validated_data['follow'] + new_status = self.validated_data["approved"] + follow = self.validated_data["follow"] if new_status == follow.approved: return follow follow.approved = new_status - follow.save(update_fields=['approved', 'modification_date']) + follow.save(update_fields=["approved", "modification_date"]) if new_status: activity.accept_follow(follow) return follow @@ -233,19 +226,13 @@ class APILibraryCreateSerializer(serializers.ModelSerializer): class Meta: model = models.Library - fields = [ - 'uuid', - 'actor', - 'autoimport', - 'federation_enabled', - 'download_files', - ] + fields = ["uuid", "actor", "autoimport", "federation_enabled", "download_files"] def validate(self, validated_data): from . import actors from . import library - actor_url = validated_data['actor'] + actor_url = validated_data["actor"] actor_data = actors.get_actor_data(actor_url) acs = LibraryActorSerializer(data=actor_data) acs.is_valid(raise_exception=True) @@ -253,43 +240,39 @@ class APILibraryCreateSerializer(serializers.ModelSerializer): actor = models.Actor.objects.get(url=actor_url) except models.Actor.DoesNotExist: actor = acs.save() - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - validated_data['follow'] = models.Follow.objects.get_or_create( - actor=library_actor, - target=actor, + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + validated_data["follow"] = models.Follow.objects.get_or_create( + actor=library_actor, target=actor )[0] - if validated_data['follow'].approved is None: + if validated_data["follow"].approved is None: funkwhale_utils.on_commit( activity.deliver, - FollowSerializer(validated_data['follow']).data, - on_behalf_of=validated_data['follow'].actor, - to=[validated_data['follow'].target.url], + FollowSerializer(validated_data["follow"]).data, + on_behalf_of=validated_data["follow"].actor, + to=[validated_data["follow"].target.url], ) - library_data = library.get_library_data( - acs.validated_data['library_url']) - if 'errors' in library_data: + library_data = library.get_library_data(acs.validated_data["library_url"]) + if "errors" in library_data: # we pass silently because it may means we require permission # before scanning pass - validated_data['library'] = library_data - validated_data['library'].setdefault( - 'id', acs.validated_data['library_url'] - ) - validated_data['actor'] = actor + validated_data["library"] = library_data + validated_data["library"].setdefault("id", acs.validated_data["library_url"]) + validated_data["actor"] = actor return validated_data def create(self, validated_data): library = models.Library.objects.update_or_create( - url=validated_data['library']['id'], + url=validated_data["library"]["id"], defaults={ - 'actor': validated_data['actor'], - 'follow': validated_data['follow'], - 'tracks_count': validated_data['library'].get('totalItems'), - 'federation_enabled': validated_data['federation_enabled'], - 'autoimport': validated_data['autoimport'], - 'download_files': validated_data['download_files'], - } + "actor": validated_data["actor"], + "follow": validated_data["follow"], + "tracks_count": validated_data["library"].get("totalItems"), + "federation_enabled": validated_data["federation_enabled"], + "autoimport": validated_data["autoimport"], + "download_files": validated_data["download_files"], + }, )[0] return library @@ -301,73 +284,73 @@ class APILibraryTrackSerializer(serializers.ModelSerializer): class Meta: model = models.LibraryTrack fields = [ - 'id', - 'url', - 'audio_url', - 'audio_mimetype', - 'creation_date', - 'modification_date', - 'fetched_date', - 'published_date', - 'metadata', - 'artist_name', - 'album_title', - 'title', - 'library', - 'local_track_file', - 'status', + "id", + "url", + "audio_url", + "audio_mimetype", + "creation_date", + "modification_date", + "fetched_date", + "published_date", + "metadata", + "artist_name", + "album_title", + "title", + "library", + "local_track_file", + "status", ] def get_status(self, o): try: if o.local_track_file is not None: - return 'imported' + return "imported" except music_models.TrackFile.DoesNotExist: pass for job in o.import_jobs.all(): - if job.status == 'pending': - return 'import_pending' - return 'not_imported' + if job.status == "pending": + return "import_pending" + return "not_imported" class FollowSerializer(serializers.Serializer): id = serializers.URLField(max_length=500) object = serializers.URLField(max_length=500) actor = serializers.URLField(max_length=500) - type = serializers.ChoiceField(choices=['Follow']) + type = serializers.ChoiceField(choices=["Follow"]) def validate_object(self, v): - expected = self.context.get('follow_target') + expected = self.context.get("follow_target") if expected and expected.url != v: - raise serializers.ValidationError('Invalid target') + raise serializers.ValidationError("Invalid target") try: return models.Actor.objects.get(url=v) except models.Actor.DoesNotExist: - raise serializers.ValidationError('Target not found') + raise serializers.ValidationError("Target not found") def validate_actor(self, v): - expected = self.context.get('follow_actor') + expected = self.context.get("follow_actor") if expected and expected.url != v: - raise serializers.ValidationError('Invalid actor') + raise serializers.ValidationError("Invalid actor") try: return models.Actor.objects.get(url=v) except models.Actor.DoesNotExist: - raise serializers.ValidationError('Actor not found') + raise serializers.ValidationError("Actor not found") def save(self, **kwargs): return models.Follow.objects.get_or_create( - actor=self.validated_data['actor'], - target=self.validated_data['object'], + actor=self.validated_data["actor"], + target=self.validated_data["object"], **kwargs, )[0] def to_representation(self, instance): return { - '@context': AP_CONTEXT, - 'actor': instance.actor.url, - 'id': instance.get_federation_url(), - 'object': instance.target.url, - 'type': 'Follow' + "@context": AP_CONTEXT, + "actor": instance.actor.url, + "id": instance.get_federation_url(), + "object": instance.target.url, + "type": "Follow", } return ret @@ -379,13 +362,13 @@ class APIFollowSerializer(serializers.ModelSerializer): class Meta: model = models.Follow fields = [ - 'uuid', - 'id', - 'approved', - 'creation_date', - 'modification_date', - 'actor', - 'target', + "uuid", + "id", + "approved", + "creation_date", + "modification_date", + "actor", + "target", ] @@ -393,84 +376,87 @@ class AcceptFollowSerializer(serializers.Serializer): id = serializers.URLField(max_length=500) actor = serializers.URLField(max_length=500) object = FollowSerializer() - type = serializers.ChoiceField(choices=['Accept']) + type = serializers.ChoiceField(choices=["Accept"]) def validate_actor(self, v): - expected = self.context.get('follow_target') + expected = self.context.get("follow_target") if expected and expected.url != v: - raise serializers.ValidationError('Invalid actor') + raise serializers.ValidationError("Invalid actor") try: return models.Actor.objects.get(url=v) except models.Actor.DoesNotExist: - raise serializers.ValidationError('Actor not found') + raise serializers.ValidationError("Actor not found") def validate(self, validated_data): # we ensure the accept actor actually match the follow target - if validated_data['actor'] != validated_data['object']['object']: - raise serializers.ValidationError('Actor mismatch') + if validated_data["actor"] != validated_data["object"]["object"]: + raise serializers.ValidationError("Actor mismatch") try: - validated_data['follow'] = models.Follow.objects.filter( - target=validated_data['actor'], - actor=validated_data['object']['actor'] - ).exclude(approved=True).get() + validated_data["follow"] = ( + models.Follow.objects.filter( + target=validated_data["actor"], + actor=validated_data["object"]["actor"], + ) + .exclude(approved=True) + .get() + ) except models.Follow.DoesNotExist: - raise serializers.ValidationError('No follow to accept') + raise serializers.ValidationError("No follow to accept") return validated_data def to_representation(self, instance): return { "@context": AP_CONTEXT, - "id": instance.get_federation_url() + '/accept', + "id": instance.get_federation_url() + "/accept", "type": "Accept", "actor": instance.target.url, - "object": FollowSerializer(instance).data + "object": FollowSerializer(instance).data, } def save(self): - self.validated_data['follow'].approved = True - self.validated_data['follow'].save() - return self.validated_data['follow'] + self.validated_data["follow"].approved = True + self.validated_data["follow"].save() + return self.validated_data["follow"] class UndoFollowSerializer(serializers.Serializer): id = serializers.URLField(max_length=500) actor = serializers.URLField(max_length=500) object = FollowSerializer() - type = serializers.ChoiceField(choices=['Undo']) + type = serializers.ChoiceField(choices=["Undo"]) def validate_actor(self, v): - expected = self.context.get('follow_target') + expected = self.context.get("follow_target") if expected and expected.url != v: - raise serializers.ValidationError('Invalid actor') + raise serializers.ValidationError("Invalid actor") try: return models.Actor.objects.get(url=v) except models.Actor.DoesNotExist: - raise serializers.ValidationError('Actor not found') + raise serializers.ValidationError("Actor not found") def validate(self, validated_data): # we ensure the accept actor actually match the follow actor - if validated_data['actor'] != validated_data['object']['actor']: - raise serializers.ValidationError('Actor mismatch') + if validated_data["actor"] != validated_data["object"]["actor"]: + raise serializers.ValidationError("Actor mismatch") try: - validated_data['follow'] = models.Follow.objects.filter( - actor=validated_data['actor'], - target=validated_data['object']['object'] + validated_data["follow"] = models.Follow.objects.filter( + actor=validated_data["actor"], target=validated_data["object"]["object"] ).get() except models.Follow.DoesNotExist: - raise serializers.ValidationError('No follow to remove') + raise serializers.ValidationError("No follow to remove") return validated_data def to_representation(self, instance): return { "@context": AP_CONTEXT, - "id": instance.get_federation_url() + '/undo', + "id": instance.get_federation_url() + "/undo", "type": "Undo", "actor": instance.actor.url, - "object": FollowSerializer(instance).data + "object": FollowSerializer(instance).data, } def save(self): - return self.validated_data['follow'].delete() + return self.validated_data["follow"].delete() class ActorWebfingerSerializer(serializers.Serializer): @@ -480,68 +466,59 @@ class ActorWebfingerSerializer(serializers.Serializer): actor_url = serializers.URLField(max_length=500, required=False) def validate(self, validated_data): - validated_data['actor_url'] = None - for l in validated_data['links']: + validated_data["actor_url"] = None + for l in validated_data["links"]: try: - if not l['rel'] == 'self': + if not l["rel"] == "self": continue - if not l['type'] == 'application/activity+json': + if not l["type"] == "application/activity+json": continue - validated_data['actor_url'] = l['href'] + validated_data["actor_url"] = l["href"] break except KeyError: pass - if validated_data['actor_url'] is None: - raise serializers.ValidationError('No valid actor url found') + if validated_data["actor_url"] is None: + raise serializers.ValidationError("No valid actor url found") return validated_data def to_representation(self, instance): data = {} - data['subject'] = 'acct:{}'.format(instance.webfinger_subject) - data['links'] = [ - { - 'rel': 'self', - 'href': instance.url, - 'type': 'application/activity+json' - } - ] - data['aliases'] = [ - instance.url + data["subject"] = "acct:{}".format(instance.webfinger_subject) + data["links"] = [ + {"rel": "self", "href": instance.url, "type": "application/activity+json"} ] + data["aliases"] = [instance.url] return data class ActivitySerializer(serializers.Serializer): actor = serializers.URLField(max_length=500) id = serializers.URLField(max_length=500, required=False) - type = serializers.ChoiceField( - choices=[(c, c) for c in activity.ACTIVITY_TYPES]) + type = serializers.ChoiceField(choices=[(c, c) for c in activity.ACTIVITY_TYPES]) object = serializers.JSONField() def validate_object(self, value): try: - type = value['type'] + type = value["type"] except KeyError: - raise serializers.ValidationError('Missing object type') + raise serializers.ValidationError("Missing object type") except TypeError: # probably a URL return value try: object_serializer = OBJECT_SERIALIZERS[type] except KeyError: - raise serializers.ValidationError( - 'Unsupported type {}'.format(type)) + raise serializers.ValidationError("Unsupported type {}".format(type)) serializer = object_serializer(data=value) serializer.is_valid(raise_exception=True) return serializer.data def validate_actor(self, value): - request_actor = self.context.get('actor') + request_actor = self.context.get("actor") if request_actor and request_actor.url != value: raise serializers.ValidationError( - 'The actor making the request do not match' - ' the activity actor' + "The actor making the request do not match" " the activity actor" ) return value @@ -549,47 +526,39 @@ class ActivitySerializer(serializers.Serializer): d = {} d.update(conf) - if self.context.get('include_ap_context', True): - d['@context'] = AP_CONTEXT + if self.context.get("include_ap_context", True): + d["@context"] = AP_CONTEXT return d class ObjectSerializer(serializers.Serializer): id = serializers.URLField(max_length=500) url = serializers.URLField(max_length=500, required=False, allow_null=True) - type = serializers.ChoiceField( - choices=[(c, c) for c in activity.OBJECT_TYPES]) - content = serializers.CharField( - required=False, allow_null=True) - summary = serializers.CharField( - required=False, allow_null=True) - name = serializers.CharField( - required=False, allow_null=True) - published = serializers.DateTimeField( - required=False, allow_null=True) - updated = serializers.DateTimeField( - required=False, allow_null=True) + type = serializers.ChoiceField(choices=[(c, c) for c in activity.OBJECT_TYPES]) + content = serializers.CharField(required=False, allow_null=True) + summary = serializers.CharField(required=False, allow_null=True) + name = serializers.CharField(required=False, allow_null=True) + published = serializers.DateTimeField(required=False, allow_null=True) + updated = serializers.DateTimeField(required=False, allow_null=True) to = serializers.ListField( - child=serializers.URLField(max_length=500), - required=False, allow_null=True) + child=serializers.URLField(max_length=500), required=False, allow_null=True + ) cc = serializers.ListField( - child=serializers.URLField(max_length=500), - required=False, allow_null=True) + child=serializers.URLField(max_length=500), required=False, allow_null=True + ) bto = serializers.ListField( - child=serializers.URLField(max_length=500), - required=False, allow_null=True) + child=serializers.URLField(max_length=500), required=False, allow_null=True + ) bcc = serializers.ListField( - child=serializers.URLField(max_length=500), - required=False, allow_null=True) + child=serializers.URLField(max_length=500), required=False, allow_null=True + ) -OBJECT_SERIALIZERS = { - t: ObjectSerializer - for t in activity.OBJECT_TYPES -} + +OBJECT_SERIALIZERS = {t: ObjectSerializer for t in activity.OBJECT_TYPES} class PaginatedCollectionSerializer(serializers.Serializer): - type = serializers.ChoiceField(choices=['Collection']) + type = serializers.ChoiceField(choices=["Collection"]) totalItems = serializers.IntegerField(min_value=0) actor = serializers.URLField(max_length=500) id = serializers.URLField(max_length=500) @@ -597,30 +566,26 @@ class PaginatedCollectionSerializer(serializers.Serializer): last = serializers.URLField(max_length=500) def to_representation(self, conf): - paginator = Paginator( - conf['items'], - conf.get('page_size', 20) - ) - first = funkwhale_utils.set_query_parameter(conf['id'], page=1) + paginator = Paginator(conf["items"], conf.get("page_size", 20)) + first = funkwhale_utils.set_query_parameter(conf["id"], page=1) current = first - last = funkwhale_utils.set_query_parameter( - conf['id'], page=paginator.num_pages) + last = funkwhale_utils.set_query_parameter(conf["id"], page=paginator.num_pages) d = { - 'id': conf['id'], - 'actor': conf['actor'].url, - 'totalItems': paginator.count, - 'type': 'Collection', - 'current': current, - 'first': first, - 'last': last, + "id": conf["id"], + "actor": conf["actor"].url, + "totalItems": paginator.count, + "type": "Collection", + "current": current, + "first": first, + "last": last, } - if self.context.get('include_ap_context', True): - d['@context'] = AP_CONTEXT + if self.context.get("include_ap_context", True): + d["@context"] = AP_CONTEXT return d class CollectionPageSerializer(serializers.Serializer): - type = serializers.ChoiceField(choices=['CollectionPage']) + type = serializers.ChoiceField(choices=["CollectionPage"]) totalItems = serializers.IntegerField(min_value=0) items = serializers.ListField() actor = serializers.URLField(max_length=500) @@ -632,7 +597,7 @@ class CollectionPageSerializer(serializers.Serializer): partOf = serializers.URLField(max_length=500) def validate_items(self, v): - item_serializer = self.context.get('item_serializer') + item_serializer = self.context.get("item_serializer") if not item_serializer: return v raw_items = [item_serializer(data=i, context=self.context) for i in v] @@ -641,47 +606,45 @@ class CollectionPageSerializer(serializers.Serializer): if i.is_valid(): valid_items.append(i) else: - logger.debug('Invalid item %s: %s', i.data, i.errors) + logger.debug("Invalid item %s: %s", i.data, i.errors) return valid_items def to_representation(self, conf): - page = conf['page'] - first = funkwhale_utils.set_query_parameter( - conf['id'], page=1) + page = conf["page"] + first = funkwhale_utils.set_query_parameter(conf["id"], page=1) last = funkwhale_utils.set_query_parameter( - conf['id'], page=page.paginator.num_pages) - id = funkwhale_utils.set_query_parameter( - conf['id'], page=page.number) + conf["id"], page=page.paginator.num_pages + ) + id = funkwhale_utils.set_query_parameter(conf["id"], page=page.number) d = { - 'id': id, - 'partOf': conf['id'], - 'actor': conf['actor'].url, - 'totalItems': page.paginator.count, - 'type': 'CollectionPage', - 'first': first, - 'last': last, - 'items': [ - conf['item_serializer']( - i, - context={ - 'actor': conf['actor'], - 'include_ap_context': False} + "id": id, + "partOf": conf["id"], + "actor": conf["actor"].url, + "totalItems": page.paginator.count, + "type": "CollectionPage", + "first": first, + "last": last, + "items": [ + conf["item_serializer"]( + i, context={"actor": conf["actor"], "include_ap_context": False} ).data for i in page.object_list - ] + ], } if page.has_previous(): - d['prev'] = funkwhale_utils.set_query_parameter( - conf['id'], page=page.previous_page_number()) + d["prev"] = funkwhale_utils.set_query_parameter( + conf["id"], page=page.previous_page_number() + ) if page.has_next(): - d['next'] = funkwhale_utils.set_query_parameter( - conf['id'], page=page.next_page_number()) + d["next"] = funkwhale_utils.set_query_parameter( + conf["id"], page=page.next_page_number() + ) - if self.context.get('include_ap_context', True): - d['@context'] = AP_CONTEXT + if self.context.get("include_ap_context", True): + d["@context"] = AP_CONTEXT return d @@ -704,12 +667,9 @@ 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) + 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): @@ -721,41 +681,39 @@ class AudioSerializer(serializers.Serializer): metadata = AudioMetadataSerializer() def validate_type(self, v): - if v != 'Audio': - raise serializers.ValidationError('Invalid type for audio') + if v != "Audio": + raise serializers.ValidationError("Invalid type for audio") return v def validate_url(self, v): try: - url = v['href'] + url = v["href"] except (KeyError, TypeError): - raise serializers.ValidationError('Missing href') + raise serializers.ValidationError("Missing href") try: - media_type = v['mediaType'] + media_type = v["mediaType"] except (KeyError, TypeError): - raise serializers.ValidationError('Missing mediaType') + raise serializers.ValidationError("Missing mediaType") - if not media_type or not media_type.startswith('audio/'): - raise serializers.ValidationError('Invalid mediaType') + if not media_type or not media_type.startswith("audio/"): + raise serializers.ValidationError("Invalid mediaType") return v def create(self, validated_data): defaults = { - 'audio_mimetype': validated_data['url']['mediaType'], - 'audio_url': validated_data['url']['href'], - 'metadata': validated_data['metadata'], - 'artist_name': validated_data['metadata']['artist']['name'], - 'album_title': validated_data['metadata']['release']['title'], - 'title': validated_data['metadata']['recording']['title'], - 'published_date': validated_data['published'], - 'modification_date': validated_data.get('updated'), + "audio_mimetype": validated_data["url"]["mediaType"], + "audio_url": validated_data["url"]["href"], + "metadata": validated_data["metadata"], + "artist_name": validated_data["metadata"]["artist"]["name"], + "album_title": validated_data["metadata"]["release"]["title"], + "title": validated_data["metadata"]["recording"]["title"], + "published_date": validated_data["published"], + "modification_date": validated_data.get("updated"), } return models.LibraryTrack.objects.get_or_create( - library=self.context['library'], - url=validated_data['id'], - defaults=defaults + library=self.context["library"], url=validated_data["id"], defaults=defaults )[0] def to_representation(self, instance): @@ -764,87 +722,77 @@ class AudioSerializer(serializers.Serializer): artist = instance.track.artist d = { - 'type': 'Audio', - 'id': instance.get_federation_url(), - 'name': instance.track.full_name, - 'published': instance.creation_date.isoformat(), - 'updated': instance.modification_date.isoformat(), - 'metadata': { - 'artist': { - 'musicbrainz_id': str(artist.mbid) if artist.mbid else None, - 'name': artist.name, + "type": "Audio", + "id": instance.get_federation_url(), + "name": instance.track.full_name, + "published": instance.creation_date.isoformat(), + "updated": instance.modification_date.isoformat(), + "metadata": { + "artist": { + "musicbrainz_id": str(artist.mbid) if artist.mbid else None, + "name": artist.name, }, - 'release': { - 'musicbrainz_id': str(album.mbid) if album.mbid else None, - 'title': album.title, + "release": { + "musicbrainz_id": str(album.mbid) if album.mbid else None, + "title": album.title, }, - 'recording': { - 'musicbrainz_id': str(track.mbid) if track.mbid else None, - 'title': track.title, + "recording": { + "musicbrainz_id": str(track.mbid) if track.mbid else None, + "title": track.title, }, - 'bitrate': instance.bitrate, - 'size': instance.size, - 'length': instance.duration, + "bitrate": instance.bitrate, + "size": instance.size, + "length": instance.duration, }, - 'url': { - 'href': utils.full_url(instance.path), - 'type': 'Link', - 'mediaType': instance.mimetype + "url": { + "href": utils.full_url(instance.path), + "type": "Link", + "mediaType": instance.mimetype, }, - 'attributedTo': [ - self.context['actor'].url - ] + "attributedTo": [self.context["actor"].url], } - if self.context.get('include_ap_context', True): - d['@context'] = AP_CONTEXT + if self.context.get("include_ap_context", True): + d["@context"] = AP_CONTEXT return d class CollectionSerializer(serializers.Serializer): - def to_representation(self, conf): d = { - 'id': conf['id'], - 'actor': conf['actor'].url, - 'totalItems': len(conf['items']), - 'type': 'Collection', - 'items': [ - conf['item_serializer']( - i, - context={ - 'actor': conf['actor'], - 'include_ap_context': False} + "id": conf["id"], + "actor": conf["actor"].url, + "totalItems": len(conf["items"]), + "type": "Collection", + "items": [ + conf["item_serializer"]( + i, context={"actor": conf["actor"], "include_ap_context": False} ).data - for i in conf['items'] - ] + for i in conf["items"] + ], } - if self.context.get('include_ap_context', True): - d['@context'] = AP_CONTEXT + if self.context.get("include_ap_context", True): + d["@context"] = AP_CONTEXT return d class LibraryTrackActionSerializer(common_serializers.ActionSerializer): - actions = ['import'] + actions = ["import"] filterset_class = filters.LibraryTrackFilter @transaction.atomic def handle_import(self, objects): batch = music_models.ImportBatch.objects.create( - source='federation', - submitted_by=self.context['submitted_by'] + source="federation", submitted_by=self.context["submitted_by"] ) jobs = [] for lt in objects: job = music_models.ImportJob( - batch=batch, - library_track=lt, - mbid=lt.mbid, - source=lt.url, + batch=batch, library_track=lt, mbid=lt.mbid, source=lt.url ) jobs.append(job) music_models.ImportJob.objects.bulk_create(jobs) music_tasks.import_batch_run.delay(import_batch_id=batch.pk) - return {'batch': {'id': batch.pk}} + return {"batch": {"id": batch.pk}} diff --git a/api/funkwhale_api/federation/signing.py b/api/funkwhale_api/federation/signing.py index 8d984d3ff..46e7ae55a 100644 --- a/api/funkwhale_api/federation/signing.py +++ b/api/funkwhale_api/federation/signing.py @@ -10,9 +10,7 @@ logger = logging.getLogger(__name__) def verify(request, public_key): return requests_http_signature.HTTPSignatureAuth.verify( - request, - key_resolver=lambda **kwargs: public_key, - use_auth_header=False, + request, key_resolver=lambda **kwargs: public_key, use_auth_header=False ) @@ -27,26 +25,24 @@ def verify_django(django_request, public_key): # with requests_http_signature headers[h.lower()] = v try: - signature = headers['Signature'] + signature = headers["Signature"] except KeyError: raise exceptions.MissingSignature - url = 'http://noop{}'.format(django_request.path) - query = django_request.META['QUERY_STRING'] + url = "http://noop{}".format(django_request.path) + query = django_request.META["QUERY_STRING"] if query: - url += '?{}'.format(query) + url += "?{}".format(query) signature_headers = signature.split('headers="')[1].split('",')[0] - expected = signature_headers.split(' ') - logger.debug('Signature expected headers: %s', expected) + expected = signature_headers.split(" ") + logger.debug("Signature expected headers: %s", expected) for header in expected: try: headers[header] except KeyError: - logger.debug('Missing header: %s', header) + logger.debug("Missing header: %s", header) request = requests.Request( - method=django_request.method, - url=url, - data=django_request.body, - headers=headers) + method=django_request.method, url=url, data=django_request.body, headers=headers + ) for h in request.headers.keys(): v = request.headers[h] if v: @@ -58,13 +54,8 @@ def verify_django(django_request, public_key): def get_auth(private_key, private_key_id): return requests_http_signature.HTTPSignatureAuth( use_auth_header=False, - headers=[ - '(request-target)', - 'user-agent', - 'host', - 'date', - 'content-type'], - algorithm='rsa-sha256', - key=private_key.encode('utf-8'), + headers=["(request-target)", "user-agent", "host", "date", "content-type"], + algorithm="rsa-sha256", + key=private_key.encode("utf-8"), key_id=private_key_id, ) diff --git a/api/funkwhale_api/federation/tasks.py b/api/funkwhale_api/federation/tasks.py index 8f931b0ed..0e6ee1e60 100644 --- a/api/funkwhale_api/federation/tasks.py +++ b/api/funkwhale_api/federation/tasks.py @@ -24,96 +24,100 @@ logger = logging.getLogger(__name__) @celery.app.task( - name='federation.send', + name="federation.send", autoretry_for=[RequestException], retry_backoff=30, - max_retries=5) -@celery.require_instance(models.Actor, 'actor') + max_retries=5, +) +@celery.require_instance(models.Actor, "actor") def send(activity, actor, to): - logger.info('Preparing activity delivery to %s', to) - auth = signing.get_auth( - actor.private_key, actor.private_key_id) + logger.info("Preparing activity delivery to %s", to) + auth = signing.get_auth(actor.private_key, actor.private_key_id) for url in to: recipient_actor = actors.get_actor(url) - logger.debug('delivering to %s', recipient_actor.inbox_url) - logger.debug('activity content: %s', json.dumps(activity)) + logger.debug("delivering to %s", recipient_actor.inbox_url) + logger.debug("activity content: %s", json.dumps(activity)) response = session.get_session().post( auth=auth, json=activity, url=recipient_actor.inbox_url, timeout=5, verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, - headers={ - 'Content-Type': 'application/activity+json' - } + headers={"Content-Type": "application/activity+json"}, ) response.raise_for_status() - logger.debug('Remote answered with %s', response.status_code) + logger.debug("Remote answered with %s", response.status_code) @celery.app.task( - name='federation.scan_library', + name="federation.scan_library", autoretry_for=[RequestException], retry_backoff=30, - max_retries=5) -@celery.require_instance(models.Library, 'library') + max_retries=5, +) +@celery.require_instance(models.Library, "library") def scan_library(library, until=None): if not library.federation_enabled: return data = lb.get_library_data(library.url) - scan_library_page.delay( - library_id=library.id, page_url=data['first'], until=until) + scan_library_page.delay(library_id=library.id, page_url=data["first"], until=until) library.fetched_date = timezone.now() - library.tracks_count = data['totalItems'] - library.save(update_fields=['fetched_date', 'tracks_count']) + library.tracks_count = data["totalItems"] + library.save(update_fields=["fetched_date", "tracks_count"]) @celery.app.task( - name='federation.scan_library_page', + name="federation.scan_library_page", autoretry_for=[RequestException], retry_backoff=30, - max_retries=5) -@celery.require_instance(models.Library, 'library') + max_retries=5, +) +@celery.require_instance(models.Library, "library") def scan_library_page(library, page_url, until=None): if not library.federation_enabled: return data = lb.get_library_page(library, page_url) lts = [] - for item_serializer in data['items']: - item_date = item_serializer.validated_data['published'] + for item_serializer in data["items"]: + item_date = item_serializer.validated_data["published"] if until and item_date < until: return lts.append(item_serializer.save()) - next_page = data.get('next') + next_page = data.get("next") if next_page and next_page != page_url: scan_library_page.delay(library_id=library.id, page_url=next_page) -@celery.app.task(name='federation.clean_music_cache') +@celery.app.task(name="federation.clean_music_cache") def clean_music_cache(): preferences = global_preferences_registry.manager() - delay = preferences['federation__music_cache_duration'] + delay = preferences["federation__music_cache_duration"] if delay < 1: return # cache clearing disabled limit = timezone.now() - datetime.timedelta(minutes=delay) - candidates = models.LibraryTrack.objects.filter( - Q(audio_file__isnull=False) & ( - Q(local_track_file__accessed_date__lt=limit) | - Q(local_track_file__accessed_date=None) + candidates = ( + models.LibraryTrack.objects.filter( + Q(audio_file__isnull=False) + & ( + Q(local_track_file__accessed_date__lt=limit) + | Q(local_track_file__accessed_date=None) + ) ) - ).exclude(audio_file='').only('audio_file', 'id') + .exclude(audio_file="") + .only("audio_file", "id") + ) for lt in candidates: lt.audio_file.delete() # we also delete orphaned files, if any - storage = models.LibraryTrack._meta.get_field('audio_file').storage - files = get_files(storage, 'federation_cache') + storage = models.LibraryTrack._meta.get_field("audio_file").storage + files = get_files(storage, "federation_cache") existing = models.LibraryTrack.objects.filter(audio_file__in=files) - missing = set(files) - set(existing.values_list('audio_file', flat=True)) + missing = set(files) - set(existing.values_list("audio_file", flat=True)) for m in missing: storage.delete(m) @@ -124,12 +128,9 @@ def get_files(storage, *parts): in a given directory using django's storage. """ if not parts: - raise ValueError('Missing path') + raise ValueError("Missing path") dirs, files = storage.listdir(os.path.join(*parts)) for dir in dirs: files += get_files(storage, *(list(parts) + [dir])) - return [ - os.path.join(parts[-1], path) - for path in files - ] + return [os.path.join(parts[-1], path) for path in files] diff --git a/api/funkwhale_api/federation/urls.py b/api/funkwhale_api/federation/urls.py index 2c24b5257..3cd4cb732 100644 --- a/api/funkwhale_api/federation/urls.py +++ b/api/funkwhale_api/federation/urls.py @@ -6,19 +6,11 @@ from . import views router = routers.SimpleRouter(trailing_slash=False) music_router = routers.SimpleRouter(trailing_slash=False) router.register( - r'federation/instance/actors', - views.InstanceActorViewSet, - 'instance-actors') -router.register( - r'.well-known', - views.WellKnownViewSet, - 'well-known') - -music_router.register( - r'files', - views.MusicFilesViewSet, - 'files', + r"federation/instance/actors", views.InstanceActorViewSet, "instance-actors" ) +router.register(r".well-known", views.WellKnownViewSet, "well-known") + +music_router.register(r"files", views.MusicFilesViewSet, "files") urlpatterns = router.urls + [ - url('federation/music/', include((music_router.urls, 'music'), namespace='music')) + url("federation/music/", include((music_router.urls, "music"), namespace="music")) ] diff --git a/api/funkwhale_api/federation/utils.py b/api/funkwhale_api/federation/utils.py index df093add8..e09870223 100644 --- a/api/funkwhale_api/federation/utils.py +++ b/api/funkwhale_api/federation/utils.py @@ -6,10 +6,10 @@ def full_url(path): Given a relative path, return a full url usable for federation purpose """ root = settings.FUNKWHALE_URL - if path.startswith('/') and root.endswith('/'): + if path.startswith("/") and root.endswith("/"): return root + path[1:] - elif not path.startswith('/') and not root.endswith('/'): - return root + '/' + path + elif not path.startswith("/") and not root.endswith("/"): + return root + "/" + path else: return root + path @@ -19,17 +19,14 @@ def clean_wsgi_headers(raw_headers): Convert WSGI headers from CONTENT_TYPE to Content-Type notation """ cleaned = {} - non_prefixed = [ - 'content_type', - 'content_length', - ] + non_prefixed = ["content_type", "content_length"] for raw_header, value in raw_headers.items(): h = raw_header.lower() - if not h.startswith('http_') and h not in non_prefixed: + if not h.startswith("http_") and h not in non_prefixed: continue - words = h.replace('http_', '', 1).split('_') - cleaned_header = '-'.join([w.capitalize() for w in words]) + words = h.replace("http_", "", 1).split("_") + cleaned_header = "-".join([w.capitalize() for w in words]) cleaned[cleaned_header] = value return cleaned diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py index 1350ec731..36bf1a444 100644 --- a/api/funkwhale_api/federation/views.py +++ b/api/funkwhale_api/federation/views.py @@ -34,22 +34,21 @@ from . import webfinger class FederationMixin(object): def dispatch(self, request, *args, **kwargs): - if not preferences.get('federation__enabled'): + if not preferences.get("federation__enabled"): return HttpResponse(status=405) return super().dispatch(request, *args, **kwargs) class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet): - lookup_field = 'actor' - lookup_value_regex = '[a-z]*' - authentication_classes = [ - authentication.SignatureAuthentication] + lookup_field = "actor" + lookup_value_regex = "[a-z]*" + authentication_classes = [authentication.SignatureAuthentication] permission_classes = [] renderer_classes = [renderers.ActivityPubRenderer] def get_object(self): try: - return actors.SYSTEM_ACTORS[self.kwargs['actor']] + return actors.SYSTEM_ACTORS[self.kwargs["actor"]] except KeyError: raise Http404 @@ -59,12 +58,10 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet): data = actor.system_conf.serialize() return response.Response(data, status=200) - @detail_route(methods=['get', 'post']) + @detail_route(methods=["get", "post"]) def inbox(self, request, *args, **kwargs): system_actor = self.get_object() - handler = getattr(system_actor, '{}_inbox'.format( - request.method.lower() - )) + handler = getattr(system_actor, "{}_inbox".format(request.method.lower())) try: data = handler(request.data, actor=request.actor) @@ -72,12 +69,10 @@ class InstanceActorViewSet(FederationMixin, viewsets.GenericViewSet): return response.Response(status=405) return response.Response({}, status=200) - @detail_route(methods=['get', 'post']) + @detail_route(methods=["get", "post"]) def outbox(self, request, *args, **kwargs): system_actor = self.get_object() - handler = getattr(system_actor, '{}_outbox'.format( - request.method.lower() - )) + handler = getattr(system_actor, "{}_outbox".format(request.method.lower())) try: data = handler(request.data, actor=request.actor) except NotImplementedError: @@ -90,45 +85,36 @@ class WellKnownViewSet(viewsets.GenericViewSet): permission_classes = [] renderer_classes = [renderers.JSONRenderer, renderers.WebfingerRenderer] - @list_route(methods=['get']) + @list_route(methods=["get"]) def nodeinfo(self, request, *args, **kwargs): - if not preferences.get('instance__nodeinfo_enabled'): + if not preferences.get("instance__nodeinfo_enabled"): return HttpResponse(status=404) data = { - 'links': [ + "links": [ { - 'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0', - 'href': utils.full_url( - reverse('api:v1:instance:nodeinfo-2.0') - ) + "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", + "href": utils.full_url(reverse("api:v1:instance:nodeinfo-2.0")), } ] } return response.Response(data) - @list_route(methods=['get']) + @list_route(methods=["get"]) def webfinger(self, request, *args, **kwargs): - if not preferences.get('federation__enabled'): + if not preferences.get("federation__enabled"): return HttpResponse(status=405) try: - resource_type, resource = webfinger.clean_resource( - request.GET['resource']) - cleaner = getattr(webfinger, 'clean_{}'.format(resource_type)) + resource_type, resource = webfinger.clean_resource(request.GET["resource"]) + cleaner = getattr(webfinger, "clean_{}".format(resource_type)) result = cleaner(resource) except forms.ValidationError as e: - return response.Response({ - 'errors': { - 'resource': e.message - } - }, status=400) + return response.Response({"errors": {"resource": e.message}}, status=400) except KeyError: - return response.Response({ - 'errors': { - 'resource': 'This field is required', - } - }, status=400) + return response.Response( + {"errors": {"resource": "This field is required"}}, status=400 + ) - handler = getattr(self, 'handler_{}'.format(resource_type)) + handler = getattr(self, "handler_{}".format(resource_type)) data = handler(result) return response.Response(data) @@ -140,28 +126,25 @@ class WellKnownViewSet(viewsets.GenericViewSet): class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet): - authentication_classes = [ - authentication.SignatureAuthentication] + authentication_classes = [authentication.SignatureAuthentication] permission_classes = [permissions.LibraryFollower] renderer_classes = [renderers.ActivityPubRenderer] def list(self, request, *args, **kwargs): - page = request.GET.get('page') - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() - qs = music_models.TrackFile.objects.order_by( - '-creation_date' - ).select_related( - 'track__artist', - 'track__album__artist' - ).filter(library_track__isnull=True) + page = request.GET.get("page") + library = actors.SYSTEM_ACTORS["library"].get_actor_instance() + qs = ( + music_models.TrackFile.objects.order_by("-creation_date") + .select_related("track__artist", "track__album__artist") + .filter(library_track__isnull=True) + ) if page is None: conf = { - 'id': utils.full_url(reverse('federation:music:files-list')), - 'page_size': preferences.get( - 'federation__collection_page_size'), - 'items': qs, - 'item_serializer': serializers.AudioSerializer, - 'actor': library, + "id": utils.full_url(reverse("federation:music:files-list")), + "page_size": preferences.get("federation__collection_page_size"), + "items": qs, + "item_serializer": serializers.AudioSerializer, + "actor": library, } serializer = serializers.PaginatedCollectionSerializer(conf) data = serializer.data @@ -169,17 +152,17 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet): try: page_number = int(page) except: - return response.Response( - {'page': ['Invalid page number']}, status=400) + return response.Response({"page": ["Invalid page number"]}, status=400) p = paginator.Paginator( - qs, preferences.get('federation__collection_page_size')) + qs, preferences.get("federation__collection_page_size") + ) try: page = p.page(page_number) conf = { - 'id': utils.full_url(reverse('federation:music:files-list')), - 'page': page, - 'item_serializer': serializers.AudioSerializer, - 'actor': library, + "id": utils.full_url(reverse("federation:music:files-list")), + "page": page, + "item_serializer": serializers.AudioSerializer, + "actor": library, } serializer = serializers.CollectionPageSerializer(conf) data = serializer.data @@ -190,93 +173,76 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet): class LibraryViewSet( - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): permission_classes = (HasUserPermission,) - required_permissions = ['federation'] - queryset = models.Library.objects.all().select_related( - 'actor', - 'follow', - ) - lookup_field = 'uuid' + required_permissions = ["federation"] + queryset = models.Library.objects.all().select_related("actor", "follow") + lookup_field = "uuid" filter_class = filters.LibraryFilter serializer_class = serializers.APILibrarySerializer ordering_fields = ( - 'id', - 'creation_date', - 'fetched_date', - 'actor__domain', - 'tracks_count', + "id", + "creation_date", + "fetched_date", + "actor__domain", + "tracks_count", ) - @list_route(methods=['get']) + @list_route(methods=["get"]) def fetch(self, request, *args, **kwargs): - account = request.GET.get('account') + account = request.GET.get("account") if not account: - return response.Response( - {'account': 'This field is mandatory'}, status=400) + return response.Response({"account": "This field is mandatory"}, status=400) data = library.scan_from_account_name(account) return response.Response(data) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) def scan(self, request, *args, **kwargs): library = self.get_object() - serializer = serializers.APILibraryScanSerializer( - data=request.data - ) + serializer = serializers.APILibraryScanSerializer(data=request.data) serializer.is_valid(raise_exception=True) result = tasks.scan_library.delay( - library_id=library.pk, - until=serializer.validated_data.get('until') + library_id=library.pk, until=serializer.validated_data.get("until") ) - return response.Response({'task': result.id}) + return response.Response({"task": result.id}) - @list_route(methods=['get']) + @list_route(methods=["get"]) def following(self, request, *args, **kwargs): - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - queryset = models.Follow.objects.filter( - actor=library_actor - ).select_related( - 'actor', - 'target', - ).order_by('-creation_date') + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + queryset = ( + models.Follow.objects.filter(actor=library_actor) + .select_related("actor", "target") + .order_by("-creation_date") + ) filterset = filters.FollowFilter(request.GET, queryset=queryset) final_qs = filterset.qs serializer = serializers.APIFollowSerializer(final_qs, many=True) - data = { - 'results': serializer.data, - 'count': len(final_qs), - } + data = {"results": serializer.data, "count": len(final_qs)} return response.Response(data) - @list_route(methods=['get', 'patch']) + @list_route(methods=["get", "patch"]) def followers(self, request, *args, **kwargs): - if request.method.lower() == 'patch': - serializer = serializers.APILibraryFollowUpdateSerializer( - data=request.data) + if request.method.lower() == "patch": + serializer = serializers.APILibraryFollowUpdateSerializer(data=request.data) serializer.is_valid(raise_exception=True) follow = serializer.save() - return response.Response( - serializers.APIFollowSerializer(follow).data - ) + return response.Response(serializers.APIFollowSerializer(follow).data) - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - queryset = models.Follow.objects.filter( - target=library_actor - ).select_related( - 'actor', - 'target', - ).order_by('-creation_date') + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + queryset = ( + models.Follow.objects.filter(target=library_actor) + .select_related("actor", "target") + .order_by("-creation_date") + ) filterset = filters.FollowFilter(request.GET, queryset=queryset) final_qs = filterset.qs serializer = serializers.APIFollowSerializer(final_qs, many=True) - data = { - 'results': serializer.data, - 'count': len(final_qs), - } + data = {"results": serializer.data, "count": len(final_qs)} return response.Response(data) @transaction.atomic @@ -287,37 +253,32 @@ class LibraryViewSet( return response.Response(serializer.data, status=201) -class LibraryTrackViewSet( - mixins.ListModelMixin, - viewsets.GenericViewSet): +class LibraryTrackViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): permission_classes = (HasUserPermission,) - required_permissions = ['federation'] - queryset = models.LibraryTrack.objects.all().select_related( - 'library__actor', - 'library__follow', - 'local_track_file', - ).prefetch_related('import_jobs') + required_permissions = ["federation"] + queryset = ( + models.LibraryTrack.objects.all() + .select_related("library__actor", "library__follow", "local_track_file") + .prefetch_related("import_jobs") + ) filter_class = filters.LibraryTrackFilter serializer_class = serializers.APILibraryTrackSerializer ordering_fields = ( - 'id', - 'artist_name', - 'title', - 'album_title', - 'creation_date', - 'modification_date', - 'fetched_date', - 'published_date', + "id", + "artist_name", + "title", + "album_title", + "creation_date", + "modification_date", + "fetched_date", + "published_date", ) - @list_route(methods=['post']) + @list_route(methods=["post"]) def action(self, request, *args, **kwargs): - queryset = models.LibraryTrack.objects.filter( - local_track_file__isnull=True) + queryset = models.LibraryTrack.objects.filter(local_track_file__isnull=True) serializer = serializers.LibraryTrackActionSerializer( - request.data, - queryset=queryset, - context={'submitted_by': request.user} + request.data, queryset=queryset, context={"submitted_by": request.user} ) serializer.is_valid(raise_exception=True) result = serializer.save() diff --git a/api/funkwhale_api/federation/webfinger.py b/api/funkwhale_api/federation/webfinger.py index f5cb99635..b37f02e8a 100644 --- a/api/funkwhale_api/federation/webfinger.py +++ b/api/funkwhale_api/federation/webfinger.py @@ -8,36 +8,35 @@ from . import actors from . import utils from . import serializers -VALID_RESOURCE_TYPES = ['acct'] +VALID_RESOURCE_TYPES = ["acct"] def clean_resource(resource_string): if not resource_string: - raise forms.ValidationError('Invalid resource string') + raise forms.ValidationError("Invalid resource string") try: - resource_type, resource = resource_string.split(':', 1) + resource_type, resource = resource_string.split(":", 1) except ValueError: - raise forms.ValidationError('Missing webfinger resource type') + raise forms.ValidationError("Missing webfinger resource type") if resource_type not in VALID_RESOURCE_TYPES: - raise forms.ValidationError('Invalid webfinger resource type') + raise forms.ValidationError("Invalid webfinger resource type") return resource_type, resource def clean_acct(acct_string, ensure_local=True): try: - username, hostname = acct_string.split('@') + username, hostname = acct_string.split("@") except ValueError: - raise forms.ValidationError('Invalid format') + raise forms.ValidationError("Invalid format") if ensure_local and hostname.lower() != settings.FEDERATION_HOSTNAME: - raise forms.ValidationError( - 'Invalid hostname {}'.format(hostname)) + raise forms.ValidationError("Invalid hostname {}".format(hostname)) if ensure_local and username not in actors.SYSTEM_ACTORS: - raise forms.ValidationError('Invalid username') + raise forms.ValidationError("Invalid username") return username, hostname @@ -45,12 +44,12 @@ def clean_acct(acct_string, ensure_local=True): def get_resource(resource_string): resource_type, resource = clean_resource(resource_string) username, hostname = clean_acct(resource, ensure_local=False) - url = 'https://{}/.well-known/webfinger?resource={}'.format( - hostname, resource_string) + url = "https://{}/.well-known/webfinger?resource={}".format( + hostname, resource_string + ) response = session.get_session().get( - url, - verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, - timeout=5) + url, verify=settings.EXTERNAL_REQUESTS_VERIFY_SSL, timeout=5 + ) response.raise_for_status() serializer = serializers.ActorWebfingerSerializer(data=response.json()) serializer.is_valid(raise_exception=True) diff --git a/api/funkwhale_api/history/activities.py b/api/funkwhale_api/history/activities.py index e478f9b7f..3e8556ce5 100644 --- a/api/funkwhale_api/history/activities.py +++ b/api/funkwhale_api/history/activities.py @@ -3,17 +3,14 @@ from funkwhale_api.activity import record from . import serializers -record.registry.register_serializer( - serializers.ListeningActivitySerializer) +record.registry.register_serializer(serializers.ListeningActivitySerializer) -@record.registry.register_consumer('history.Listening') +@record.registry.register_consumer("history.Listening") def broadcast_listening_to_instance_activity(data, obj): - if obj.user.privacy_level not in ['instance', 'everyone']: + if obj.user.privacy_level not in ["instance", "everyone"]: return - channels.group_send('instance_activity', { - 'type': 'event.send', - 'text': '', - 'data': data - }) + channels.group_send( + "instance_activity", {"type": "event.send", "text": "", "data": data} + ) diff --git a/api/funkwhale_api/history/admin.py b/api/funkwhale_api/history/admin.py index 5ddfb8998..cbc7f89dd 100644 --- a/api/funkwhale_api/history/admin.py +++ b/api/funkwhale_api/history/admin.py @@ -2,11 +2,9 @@ from django.contrib import admin from . import models + @admin.register(models.Listening) class ListeningAdmin(admin.ModelAdmin): - list_display = ['track', 'creation_date', 'user', 'session_key'] - search_fields = ['track__name', 'user__username'] - list_select_related = [ - 'user', - 'track' - ] + list_display = ["track", "creation_date", "user", "session_key"] + search_fields = ["track__name", "user__username"] + list_select_related = ["user", "track"] diff --git a/api/funkwhale_api/history/factories.py b/api/funkwhale_api/history/factories.py index 86fea64d2..0524eff19 100644 --- a/api/funkwhale_api/history/factories.py +++ b/api/funkwhale_api/history/factories.py @@ -11,4 +11,4 @@ class ListeningFactory(factory.django.DjangoModelFactory): track = factory.SubFactory(factories.TrackFactory) class Meta: - model = 'history.Listening' + model = "history.Listening" diff --git a/api/funkwhale_api/history/migrations/0001_initial.py b/api/funkwhale_api/history/migrations/0001_initial.py index 7b6f950ed..cd2777230 100644 --- a/api/funkwhale_api/history/migrations/0001_initial.py +++ b/api/funkwhale_api/history/migrations/0001_initial.py @@ -9,22 +9,52 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('music', '0008_auto_20160529_1456'), + ("music", "0008_auto_20160529_1456"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Listening', + name="Listening", fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('end_date', models.DateTimeField(null=True, blank=True, default=django.utils.timezone.now)), - ('session_key', models.CharField(null=True, blank=True, max_length=100)), - ('track', models.ForeignKey(related_name='listenings', to='music.Track', on_delete=models.CASCADE)), - ('user', models.ForeignKey(blank=True, null=True, related_name='listenings', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + primary_key=True, + serialize=False, + auto_created=True, + ), + ), + ( + "end_date", + models.DateTimeField( + null=True, blank=True, default=django.utils.timezone.now + ), + ), + ( + "session_key", + models.CharField(null=True, blank=True, max_length=100), + ), + ( + "track", + models.ForeignKey( + related_name="listenings", + to="music.Track", + on_delete=models.CASCADE, + ), + ), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + related_name="listenings", + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ), + ), ], - options={ - 'ordering': ('-end_date',), - }, - ), + options={"ordering": ("-end_date",)}, + ) ] diff --git a/api/funkwhale_api/history/migrations/0002_auto_20180325_1433.py b/api/funkwhale_api/history/migrations/0002_auto_20180325_1433.py index d83dbb0a4..efc020925 100644 --- a/api/funkwhale_api/history/migrations/0002_auto_20180325_1433.py +++ b/api/funkwhale_api/history/migrations/0002_auto_20180325_1433.py @@ -5,18 +5,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('history', '0001_initial'), - ] + dependencies = [("history", "0001_initial")] operations = [ migrations.AlterModelOptions( - name='listening', - options={'ordering': ('-creation_date',)}, + name="listening", options={"ordering": ("-creation_date",)} ), migrations.RenameField( - model_name='listening', - old_name='end_date', - new_name='creation_date', + model_name="listening", old_name="end_date", new_name="creation_date" ), ] diff --git a/api/funkwhale_api/history/models.py b/api/funkwhale_api/history/models.py index 480461d35..2edc98fbf 100644 --- a/api/funkwhale_api/history/models.py +++ b/api/funkwhale_api/history/models.py @@ -6,21 +6,21 @@ from funkwhale_api.music.models import Track class Listening(models.Model): - creation_date = models.DateTimeField( - default=timezone.now, null=True, blank=True) + creation_date = models.DateTimeField(default=timezone.now, null=True, blank=True) track = models.ForeignKey( - Track, related_name="listenings", on_delete=models.CASCADE) + Track, related_name="listenings", on_delete=models.CASCADE + ) user = models.ForeignKey( - 'users.User', + "users.User", related_name="listenings", null=True, blank=True, - on_delete=models.CASCADE) + on_delete=models.CASCADE, + ) session_key = models.CharField(max_length=100, null=True, blank=True) class Meta: - ordering = ('-creation_date',) + ordering = ("-creation_date",) def get_activity_url(self): - return '{}/listenings/tracks/{}'.format( - self.user.get_activity_url(), self.pk) + return "{}/listenings/tracks/{}".format(self.user.get_activity_url(), self.pk) diff --git a/api/funkwhale_api/history/serializers.py b/api/funkwhale_api/history/serializers.py index 572787ae0..e49322798 100644 --- a/api/funkwhale_api/history/serializers.py +++ b/api/funkwhale_api/history/serializers.py @@ -9,35 +9,27 @@ from . import models class ListeningActivitySerializer(activity_serializers.ModelSerializer): type = serializers.SerializerMethodField() - object = TrackActivitySerializer(source='track') - actor = UserActivitySerializer(source='user') - published = serializers.DateTimeField(source='creation_date') + object = TrackActivitySerializer(source="track") + actor = UserActivitySerializer(source="user") + published = serializers.DateTimeField(source="creation_date") class Meta: model = models.Listening - fields = [ - 'id', - 'local_id', - 'object', - 'type', - 'actor', - 'published' - ] + fields = ["id", "local_id", "object", "type", "actor", "published"] def get_actor(self, obj): return UserActivitySerializer(obj.user).data def get_type(self, obj): - return 'Listen' + return "Listen" class ListeningSerializer(serializers.ModelSerializer): - class Meta: model = models.Listening - fields = ('id', 'user', 'track', 'creation_date') + fields = ("id", "user", "track", "creation_date") def create(self, validated_data): - validated_data['user'] = self.context['user'] + validated_data["user"] = self.context["user"] return super().create(validated_data) diff --git a/api/funkwhale_api/history/urls.py b/api/funkwhale_api/history/urls.py index 6bd72a8a2..30e0105c0 100644 --- a/api/funkwhale_api/history/urls.py +++ b/api/funkwhale_api/history/urls.py @@ -2,7 +2,8 @@ from django.conf.urls import include, url from . import views from rest_framework import routers + router = routers.SimpleRouter() -router.register(r'listenings', views.ListeningViewSet, 'listenings') +router.register(r"listenings", views.ListeningViewSet, "listenings") urlpatterns = router.urls diff --git a/api/funkwhale_api/history/views.py b/api/funkwhale_api/history/views.py index 3da8b2a38..057e6b323 100644 --- a/api/funkwhale_api/history/views.py +++ b/api/funkwhale_api/history/views.py @@ -12,9 +12,8 @@ from . import serializers class ListeningViewSet( - mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - viewsets.GenericViewSet): + mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet +): serializer_class = serializers.ListeningSerializer queryset = models.Listening.objects.all() @@ -31,5 +30,5 @@ class ListeningViewSet( def get_serializer_context(self): context = super().get_serializer_context() - context['user'] = self.request.user + context["user"] = self.request.user return context diff --git a/api/funkwhale_api/instance/consumers.py b/api/funkwhale_api/instance/consumers.py index eee5f7f0e..bb213a001 100644 --- a/api/funkwhale_api/instance/consumers.py +++ b/api/funkwhale_api/instance/consumers.py @@ -5,4 +5,4 @@ class InstanceActivityConsumer(JsonAuthConsumer): groups = ["instance_activity"] def event_send(self, message): - self.send_json(message['data']) + self.send_json(message["data"]) diff --git a/api/funkwhale_api/instance/dynamic_preferences_registry.py b/api/funkwhale_api/instance/dynamic_preferences_registry.py index 8ccf80dd9..618bd8ea4 100644 --- a/api/funkwhale_api/instance/dynamic_preferences_registry.py +++ b/api/funkwhale_api/instance/dynamic_preferences_registry.py @@ -3,91 +3,83 @@ from django.forms import widgets from dynamic_preferences import types from dynamic_preferences.registries import global_preferences_registry -raven = types.Section('raven') -instance = types.Section('instance') +raven = types.Section("raven") +instance = types.Section("instance") @global_preferences_registry.register class InstanceName(types.StringPreference): show_in_api = True section = instance - name = 'name' - default = '' - verbose_name = 'Public name' - help_text = 'The public name of your instance, displayed in the about page.' - field_kwargs = { - 'required': False, - } + name = "name" + default = "" + 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 class InstanceShortDescription(types.StringPreference): show_in_api = True section = instance - name = 'short_description' - default = '' - verbose_name = 'Short description' - help_text = 'Instance succinct description, displayed in the about page.' - field_kwargs = { - 'required': False, - } + name = "short_description" + default = "" + verbose_name = "Short description" + help_text = "Instance succinct description, displayed in the about page." + field_kwargs = {"required": False} @global_preferences_registry.register class InstanceLongDescription(types.StringPreference): show_in_api = True section = instance - name = 'long_description' - verbose_name = 'Long description' - default = '' - help_text = 'Instance long description, displayed in the about page (markdown allowed).' + name = "long_description" + verbose_name = "Long description" + default = "" + help_text = ( + "Instance long description, displayed in the about page (markdown allowed)." + ) widget = widgets.Textarea - field_kwargs = { - 'required': False, - } + field_kwargs = {"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 = 'Raven DSN key (front-end)' + name = "front_dsn" + default = "https://9e0562d46b09442bb8f6844e50cbca2b@sentry.eliotberriot.com/4" + verbose_name = "Raven DSN key (front-end)" help_text = ( - 'A Raven DSN key used to report front-ent errors to ' - 'a sentry instance. 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." ) - field_kwargs = { - 'required': False, - } + field_kwargs = {"required": False} @global_preferences_registry.register class RavenEnabled(types.BooleanPreference): show_in_api = True section = raven - name = 'front_enabled' + name = "front_enabled" default = False - verbose_name = ( - 'Report front-end errors with Raven' - ) + verbose_name = "Report front-end errors with Raven" @global_preferences_registry.register class InstanceNodeinfoEnabled(types.BooleanPreference): show_in_api = False section = instance - name = 'nodeinfo_enabled' + name = "nodeinfo_enabled" default = True - verbose_name = 'Enable nodeinfo endpoint' + verbose_name = "Enable nodeinfo endpoint" help_text = ( - '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.' + "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." ) @@ -95,13 +87,13 @@ class InstanceNodeinfoEnabled(types.BooleanPreference): class InstanceNodeinfoPrivate(types.BooleanPreference): show_in_api = False section = instance - name = 'nodeinfo_private' + name = "nodeinfo_private" default = False - verbose_name = 'Private mode in nodeinfo' + 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.' + "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." ) @@ -109,10 +101,10 @@ class InstanceNodeinfoPrivate(types.BooleanPreference): class InstanceNodeinfoStatsEnabled(types.BooleanPreference): show_in_api = False section = instance - name = 'nodeinfo_stats_enabled' + name = "nodeinfo_stats_enabled" default = True - verbose_name = 'Enable usage and library stats in nodeinfo endpoint' + verbose_name = "Enable usage and library stats in nodeinfo endpoint" help_text = ( - 'Disable this if you don\'t want to share usage and library statistics ' - 'in the nodeinfo endpoint but don\'t want to disable it completely.' + "Disable this if you don't want to share usage and library statistics " + "in the nodeinfo endpoint but don't want to disable it completely." ) diff --git a/api/funkwhale_api/instance/nodeinfo.py b/api/funkwhale_api/instance/nodeinfo.py index dbc005af7..ea982a63f 100644 --- a/api/funkwhale_api/instance/nodeinfo.py +++ b/api/funkwhale_api/instance/nodeinfo.py @@ -6,70 +6,47 @@ from funkwhale_api.common import preferences from . import stats -store = memoize.djangocache.Cache('default') -memo = memoize.Memoizer(store, namespace='instance:stats') +store = memoize.djangocache.Cache("default") +memo = memoize.Memoizer(store, namespace="instance:stats") def get(): - share_stats = preferences.get('instance__nodeinfo_stats_enabled') - private = preferences.get('instance__nodeinfo_private') + share_stats = preferences.get("instance__nodeinfo_stats_enabled") + private = preferences.get("instance__nodeinfo_private") data = { - 'version': '2.0', - 'software': { - 'name': 'funkwhale', - 'version': funkwhale_api.__version__ - }, - 'protocols': ['activitypub'], - 'services': { - 'inbound': [], - 'outbound': [] - }, - 'openRegistrations': preferences.get('users__registration_enabled'), - 'usage': { - 'users': { - 'total': 0, - } - }, - 'metadata': { - 'private': preferences.get('instance__nodeinfo_private'), - 'shortDescription': preferences.get('instance__short_description'), - 'longDescription': preferences.get('instance__long_description'), - 'nodeName': preferences.get('instance__name'), - 'library': { - 'federationEnabled': preferences.get('federation__enabled'), - 'federationNeedsApproval': preferences.get('federation__music_needs_approval'), - 'anonymousCanListen': preferences.get('common__api_authentication_required'), + "version": "2.0", + "software": {"name": "funkwhale", "version": funkwhale_api.__version__}, + "protocols": ["activitypub"], + "services": {"inbound": [], "outbound": []}, + "openRegistrations": preferences.get("users__registration_enabled"), + "usage": {"users": {"total": 0}}, + "metadata": { + "private": preferences.get("instance__nodeinfo_private"), + "shortDescription": preferences.get("instance__short_description"), + "longDescription": preferences.get("instance__long_description"), + "nodeName": preferences.get("instance__name"), + "library": { + "federationEnabled": preferences.get("federation__enabled"), + "federationNeedsApproval": preferences.get( + "federation__music_needs_approval" + ), + "anonymousCanListen": preferences.get( + "common__api_authentication_required" + ), }, - } + }, } if share_stats: - getter = memo( - lambda: stats.get(), - max_age=600 - ) + getter = memo(lambda: stats.get(), max_age=600) statistics = getter() - data['usage']['users']['total'] = statistics['users'] - data['metadata']['library']['tracks'] = { - 'total': statistics['tracks'], - } - data['metadata']['library']['artists'] = { - 'total': statistics['artists'], - } - data['metadata']['library']['albums'] = { - 'total': statistics['albums'], - } - data['metadata']['library']['music'] = { - 'hours': statistics['music_duration'] - } + data["usage"]["users"]["total"] = statistics["users"] + data["metadata"]["library"]["tracks"] = {"total": statistics["tracks"]} + data["metadata"]["library"]["artists"] = {"total": statistics["artists"]} + data["metadata"]["library"]["albums"] = {"total": statistics["albums"]} + data["metadata"]["library"]["music"] = {"hours": statistics["music_duration"]} - data['metadata']['usage'] = { - 'favorites': { - 'tracks': { - 'total': statistics['track_favorites'], - } - }, - 'listenings': { - 'total': statistics['listenings'] - } + data["metadata"]["usage"] = { + "favorites": {"tracks": {"total": statistics["track_favorites"]}}, + "listenings": {"total": statistics["listenings"]}, } return data diff --git a/api/funkwhale_api/instance/stats.py b/api/funkwhale_api/instance/stats.py index 167b333d6..061aade75 100644 --- a/api/funkwhale_api/instance/stats.py +++ b/api/funkwhale_api/instance/stats.py @@ -8,13 +8,13 @@ from funkwhale_api.users.models import User def get(): return { - 'users': get_users(), - 'tracks': get_tracks(), - 'albums': get_albums(), - 'artists': get_artists(), - 'track_favorites': get_track_favorites(), - 'listenings': get_listenings(), - 'music_duration': get_music_duration(), + "users": get_users(), + "tracks": get_tracks(), + "albums": get_albums(), + "artists": get_artists(), + "track_favorites": get_track_favorites(), + "listenings": get_listenings(), + "music_duration": get_music_duration(), } @@ -43,9 +43,7 @@ def get_artists(): def get_music_duration(): - seconds = models.TrackFile.objects.aggregate( - d=Sum('duration'), - )['d'] + seconds = models.TrackFile.objects.aggregate(d=Sum("duration"))["d"] if seconds: return seconds / 3600 return 0 diff --git a/api/funkwhale_api/instance/urls.py b/api/funkwhale_api/instance/urls.py index 7992842c0..05682b1e7 100644 --- a/api/funkwhale_api/instance/urls.py +++ b/api/funkwhale_api/instance/urls.py @@ -2,10 +2,11 @@ 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') +admin_router.register(r"admin/settings", views.AdminSettings, "admin-settings") urlpatterns = [ - url(r'^nodeinfo/2.0/$', views.NodeInfo.as_view(), name='nodeinfo-2.0'), - url(r'^settings/$', views.InstanceSettings.as_view(), name='settings'), + url(r"^nodeinfo/2.0/$", views.NodeInfo.as_view(), name="nodeinfo-2.0"), + url(r"^settings/$", views.InstanceSettings.as_view(), name="settings"), ] + admin_router.urls diff --git a/api/funkwhale_api/instance/views.py b/api/funkwhale_api/instance/views.py index b905acd3e..69663c125 100644 --- a/api/funkwhale_api/instance/views.py +++ b/api/funkwhale_api/instance/views.py @@ -12,15 +12,14 @@ from . import nodeinfo from . import stats -NODEINFO_2_CONTENT_TYPE = ( - 'application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8' # noqa -) +NODEINFO_2_CONTENT_TYPE = "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8" # noqa class AdminSettings(preferences_viewsets.GlobalPreferencesViewSet): pagination_class = None permission_classes = (HasUserPermission,) - required_permissions = ['settings'] + required_permissions = ["settings"] + class InstanceSettings(views.APIView): permission_classes = [] @@ -29,16 +28,11 @@ class InstanceSettings(views.APIView): def get(self, request, *args, **kwargs): manager = global_preferences_registry.manager() manager.all() - all_preferences = manager.model.objects.all().order_by( - 'section', 'name' - ) + all_preferences = manager.model.objects.all().order_by("section", "name") api_preferences = [ - p - for p in all_preferences - if getattr(p.preference, 'show_in_api', False) + p for p in all_preferences if getattr(p.preference, "show_in_api", False) ] - data = serializers.GlobalPreferenceSerializer( - api_preferences, many=True).data + data = serializers.GlobalPreferenceSerializer(api_preferences, many=True).data return Response(data, status=200) @@ -47,8 +41,7 @@ class NodeInfo(views.APIView): authentication_classes = [] def get(self, request, *args, **kwargs): - if not preferences.get('instance__nodeinfo_enabled'): + if not preferences.get("instance__nodeinfo_enabled"): return Response(status=404) data = nodeinfo.get() - return Response( - data, status=200, content_type=NODEINFO_2_CONTENT_TYPE) + return Response(data, status=200, content_type=NODEINFO_2_CONTENT_TYPE) diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py index 9853b7a61..6bb8a1ccc 100644 --- a/api/funkwhale_api/manage/filters.py +++ b/api/funkwhale_api/manage/filters.py @@ -7,19 +7,15 @@ from funkwhale_api.music import models as music_models class ManageTrackFileFilterSet(filters.FilterSet): - q = fields.SearchFilter(search_fields=[ - 'track__title', - 'track__album__title', - 'track__artist__name', - 'source', - ]) + q = fields.SearchFilter( + search_fields=[ + "track__title", + "track__album__title", + "track__artist__name", + "source", + ] + ) class Meta: model = music_models.TrackFile - fields = [ - 'q', - 'track__album', - 'track__artist', - 'track', - 'library_track' - ] + fields = ["q", "track__album", "track__artist", "track", "library_track"] diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py index 02300ec06..1c94cf553 100644 --- a/api/funkwhale_api/manage/serializers.py +++ b/api/funkwhale_api/manage/serializers.py @@ -10,12 +10,7 @@ from . import filters class ManageTrackFileArtistSerializer(serializers.ModelSerializer): class Meta: model = music_models.Artist - fields = [ - 'id', - 'mbid', - 'creation_date', - 'name', - ] + fields = ["id", "mbid", "creation_date", "name"] class ManageTrackFileAlbumSerializer(serializers.ModelSerializer): @@ -24,13 +19,13 @@ class ManageTrackFileAlbumSerializer(serializers.ModelSerializer): class Meta: model = music_models.Album fields = ( - 'id', - 'mbid', - 'title', - 'artist', - 'release_date', - 'cover', - 'creation_date', + "id", + "mbid", + "title", + "artist", + "release_date", + "cover", + "creation_date", ) @@ -40,15 +35,7 @@ class ManageTrackFileTrackSerializer(serializers.ModelSerializer): class Meta: model = music_models.Track - fields = ( - 'id', - 'mbid', - 'title', - 'album', - 'artist', - 'creation_date', - 'position', - ) + fields = ("id", "mbid", "title", "album", "artist", "creation_date", "position") class ManageTrackFileSerializer(serializers.ModelSerializer): @@ -57,24 +44,24 @@ class ManageTrackFileSerializer(serializers.ModelSerializer): class Meta: model = music_models.TrackFile fields = ( - 'id', - 'path', - 'source', - 'filename', - 'mimetype', - 'track', - 'duration', - 'mimetype', - 'bitrate', - 'size', - 'path', - 'library_track', + "id", + "path", + "source", + "filename", + "mimetype", + "track", + "duration", + "mimetype", + "bitrate", + "size", + "path", + "library_track", ) class ManageTrackFileActionSerializer(common_serializers.ActionSerializer): - actions = ['delete'] - dangerous_actions = ['delete'] + actions = ["delete"] + dangerous_actions = ["delete"] filterset_class = filters.ManageTrackFileFilterSet @transaction.atomic diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py index c434581ec..38ed7d1c9 100644 --- a/api/funkwhale_api/manage/urls.py +++ b/api/funkwhale_api/manage/urls.py @@ -2,10 +2,10 @@ from django.conf.urls import include, url from . import views from rest_framework import routers + library_router = routers.SimpleRouter() -library_router.register(r'track-files', views.ManageTrackFileViewSet, 'track-files') +library_router.register(r"track-files", views.ManageTrackFileViewSet, "track-files") urlpatterns = [ - url(r'^library/', - include((library_router.urls, 'instance'), namespace='library')), + url(r"^library/", include((library_router.urls, "instance"), namespace="library")) ] diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py index 74059caa1..77581acf0 100644 --- a/api/funkwhale_api/manage/views.py +++ b/api/funkwhale_api/manage/views.py @@ -11,38 +11,35 @@ from . import serializers class ManageTrackFileViewSet( - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.DestroyModelMixin, - viewsets.GenericViewSet): + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): queryset = ( music_models.TrackFile.objects.all() - .select_related( - 'track__artist', - 'track__album__artist', - 'library_track') - .order_by('-id') + .select_related("track__artist", "track__album__artist", "library_track") + .order_by("-id") ) serializer_class = serializers.ManageTrackFileSerializer filter_class = filters.ManageTrackFileFilterSet permission_classes = (HasUserPermission,) - required_permissions = ['library'] + required_permissions = ["library"] ordering_fields = [ - 'accessed_date', - 'modification_date', - 'creation_date', - 'track__artist__name', - 'bitrate', - 'size', - 'duration', + "accessed_date", + "modification_date", + "creation_date", + "track__artist__name", + "bitrate", + "size", + "duration", ] - @list_route(methods=['post']) + @list_route(methods=["post"]) def action(self, request, *args, **kwargs): queryset = self.get_queryset() serializer = serializers.ManageTrackFileActionSerializer( - request.data, - queryset=queryset, + request.data, queryset=queryset ) serializer.is_valid(raise_exception=True) result = serializer.save() diff --git a/api/funkwhale_api/music/admin.py b/api/funkwhale_api/music/admin.py index 1654428ba..a5775acd6 100644 --- a/api/funkwhale_api/music/admin.py +++ b/api/funkwhale_api/music/admin.py @@ -5,85 +5,73 @@ from . import models @admin.register(models.Artist) class ArtistAdmin(admin.ModelAdmin): - list_display = ['name', 'mbid', 'creation_date'] - search_fields = ['name', 'mbid'] + list_display = ["name", "mbid", "creation_date"] + search_fields = ["name", "mbid"] @admin.register(models.Album) class AlbumAdmin(admin.ModelAdmin): - list_display = ['title', 'artist', 'mbid', 'release_date', 'creation_date'] - search_fields = ['title', 'artist__name', 'mbid'] + list_display = ["title", "artist", "mbid", "release_date", "creation_date"] + search_fields = ["title", "artist__name", "mbid"] list_select_related = True @admin.register(models.Track) class TrackAdmin(admin.ModelAdmin): - list_display = ['title', 'artist', 'album', 'mbid'] - search_fields = ['title', 'artist__name', 'album__title', 'mbid'] + list_display = ["title", "artist", "album", "mbid"] + search_fields = ["title", "artist__name", "album__title", "mbid"] list_select_related = True @admin.register(models.ImportBatch) class ImportBatchAdmin(admin.ModelAdmin): - list_display = [ - 'submitted_by', - 'creation_date', - 'import_request', - 'status'] - list_select_related = [ - 'submitted_by', - 'import_request', - ] - list_filter = ['status'] - search_fields = [ - 'import_request__name', 'source', 'batch__pk', 'mbid'] + list_display = ["submitted_by", "creation_date", "import_request", "status"] + list_select_related = ["submitted_by", "import_request"] + list_filter = ["status"] + search_fields = ["import_request__name", "source", "batch__pk", "mbid"] @admin.register(models.ImportJob) class ImportJobAdmin(admin.ModelAdmin): - list_display = ['source', 'batch', 'track_file', 'status', 'mbid'] - list_select_related = [ - 'track_file', - 'batch', - ] - search_fields = ['source', 'batch__pk', 'mbid'] - list_filter = ['status'] + list_display = ["source", "batch", "track_file", "status", "mbid"] + list_select_related = ["track_file", "batch"] + search_fields = ["source", "batch__pk", "mbid"] + list_filter = ["status"] @admin.register(models.Work) class WorkAdmin(admin.ModelAdmin): - list_display = ['title', 'mbid', 'language', 'nature'] + list_display = ["title", "mbid", "language", "nature"] list_select_related = True - search_fields = ['title'] - list_filter = ['language', 'nature'] + search_fields = ["title"] + list_filter = ["language", "nature"] @admin.register(models.Lyrics) class LyricsAdmin(admin.ModelAdmin): - list_display = ['url', 'id', 'url'] + list_display = ["url", "id", "url"] list_select_related = True - search_fields = ['url', 'work__title'] - list_filter = ['work__language'] + search_fields = ["url", "work__title"] + list_filter = ["work__language"] @admin.register(models.TrackFile) class TrackFileAdmin(admin.ModelAdmin): list_display = [ - 'track', - 'audio_file', - 'source', - 'duration', - 'mimetype', - 'size', - 'bitrate' - ] - list_select_related = [ - 'track' + "track", + "audio_file", + "source", + "duration", + "mimetype", + "size", + "bitrate", ] + list_select_related = ["track"] search_fields = [ - 'source', - 'acoustid_track_id', - 'track__title', - 'track__album__title', - 'track__artist__name'] - list_filter = ['mimetype'] + "source", + "acoustid_track_id", + "track__title", + "track__album__title", + "track__artist__name", + ] + list_filter = ["mimetype"] diff --git a/api/funkwhale_api/music/factories.py b/api/funkwhale_api/music/factories.py index 11423f5b0..6e2e6a115 100644 --- a/api/funkwhale_api/music/factories.py +++ b/api/funkwhale_api/music/factories.py @@ -2,78 +2,72 @@ import factory import os from funkwhale_api.factories import registry, ManyToManyFromList -from funkwhale_api.federation.factories import ( - LibraryTrackFactory, -) +from funkwhale_api.federation.factories import LibraryTrackFactory from funkwhale_api.users.factories import UserFactory SAMPLES_PATH = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), - 'tests', 'music' + "tests", + "music", ) @registry.register class ArtistFactory(factory.django.DjangoModelFactory): - name = factory.Faker('name') - mbid = factory.Faker('uuid4') + name = factory.Faker("name") + mbid = factory.Faker("uuid4") class Meta: - model = 'music.Artist' + model = "music.Artist" @registry.register class AlbumFactory(factory.django.DjangoModelFactory): - title = factory.Faker('sentence', nb_words=3) - mbid = factory.Faker('uuid4') - release_date = factory.Faker('date_object') + title = factory.Faker("sentence", nb_words=3) + mbid = factory.Faker("uuid4") + release_date = factory.Faker("date_object") cover = factory.django.ImageField() artist = factory.SubFactory(ArtistFactory) - release_group_id = factory.Faker('uuid4') + release_group_id = factory.Faker("uuid4") class Meta: - model = 'music.Album' + model = "music.Album" @registry.register class TrackFactory(factory.django.DjangoModelFactory): - title = factory.Faker('sentence', nb_words=3) - mbid = factory.Faker('uuid4') + title = factory.Faker("sentence", nb_words=3) + mbid = factory.Faker("uuid4") album = factory.SubFactory(AlbumFactory) - artist = factory.SelfAttribute('album.artist') + artist = factory.SelfAttribute("album.artist") position = 1 - tags = ManyToManyFromList('tags') + tags = ManyToManyFromList("tags") class Meta: - model = 'music.Track' + model = "music.Track" @registry.register class TrackFileFactory(factory.django.DjangoModelFactory): track = factory.SubFactory(TrackFactory) audio_file = factory.django.FileField( - from_path=os.path.join(SAMPLES_PATH, 'test.ogg')) + from_path=os.path.join(SAMPLES_PATH, "test.ogg") + ) bitrate = None size = None duration = None class Meta: - model = 'music.TrackFile' + model = "music.TrackFile" class Params: - in_place = factory.Trait( - audio_file=None, - ) + in_place = factory.Trait(audio_file=None) federation = factory.Trait( audio_file=None, library_track=factory.SubFactory(LibraryTrackFactory), - mimetype=factory.LazyAttribute( - lambda o: o.library_track.audio_mimetype - ), - source=factory.LazyAttribute( - lambda o: o.library_track.audio_url - ), + mimetype=factory.LazyAttribute(lambda o: o.library_track.audio_mimetype), + source=factory.LazyAttribute(lambda o: o.library_track.audio_url), ) @@ -82,26 +76,21 @@ class ImportBatchFactory(factory.django.DjangoModelFactory): submitted_by = factory.SubFactory(UserFactory) class Meta: - model = 'music.ImportBatch' + model = "music.ImportBatch" class Params: - federation = factory.Trait( - submitted_by=None, - source='federation', - ) - finished = factory.Trait( - status='finished', - ) + federation = factory.Trait(submitted_by=None, source="federation") + finished = factory.Trait(status="finished") @registry.register class ImportJobFactory(factory.django.DjangoModelFactory): batch = factory.SubFactory(ImportBatchFactory) - source = factory.Faker('url') - mbid = factory.Faker('uuid4') + source = factory.Faker("url") + mbid = factory.Faker("uuid4") class Meta: - model = 'music.ImportJob' + model = "music.ImportJob" class Params: federation = factory.Trait( @@ -110,53 +99,51 @@ class ImportJobFactory(factory.django.DjangoModelFactory): batch=factory.SubFactory(ImportBatchFactory, federation=True), ) finished = factory.Trait( - status='finished', - track_file=factory.SubFactory(TrackFileFactory), - ) - in_place = factory.Trait( - status='finished', - audio_file=None, + status="finished", track_file=factory.SubFactory(TrackFileFactory) ) + in_place = factory.Trait(status="finished", audio_file=None) with_audio_file = factory.Trait( - status='finished', + status="finished", audio_file=factory.django.FileField( - from_path=os.path.join(SAMPLES_PATH, 'test.ogg')), + from_path=os.path.join(SAMPLES_PATH, "test.ogg") + ), ) -@registry.register(name='music.FileImportJob') +@registry.register(name="music.FileImportJob") class FileImportJobFactory(ImportJobFactory): - source = 'file://' + source = "file://" mbid = None audio_file = factory.django.FileField( - from_path=os.path.join(SAMPLES_PATH, 'test.ogg')) + from_path=os.path.join(SAMPLES_PATH, "test.ogg") + ) @registry.register class WorkFactory(factory.django.DjangoModelFactory): - mbid = factory.Faker('uuid4') - language = 'eng' - nature = 'song' - title = factory.Faker('sentence', nb_words=3) + mbid = factory.Faker("uuid4") + language = "eng" + nature = "song" + title = factory.Faker("sentence", nb_words=3) class Meta: - model = 'music.Work' + model = "music.Work" @registry.register class LyricsFactory(factory.django.DjangoModelFactory): work = factory.SubFactory(WorkFactory) - url = factory.Faker('url') - content = factory.Faker('paragraphs', nb=4) + url = factory.Faker("url") + content = factory.Faker("paragraphs", nb=4) class Meta: - model = 'music.Lyrics' + model = "music.Lyrics" @registry.register class TagFactory(factory.django.DjangoModelFactory): - name = factory.SelfAttribute('slug') - slug = factory.Faker('slug') + name = factory.SelfAttribute("slug") + slug = factory.Faker("slug") class Meta: - model = 'taggit.Tag' + model = "taggit.Tag" diff --git a/api/funkwhale_api/music/fake_data.py b/api/funkwhale_api/music/fake_data.py index 892b784ca..de7b61ef5 100644 --- a/api/funkwhale_api/music/fake_data.py +++ b/api/funkwhale_api/music/fake_data.py @@ -10,13 +10,15 @@ from funkwhale_api.music import factories def create_data(count=25): artists = factories.ArtistFactory.create_batch(size=count) for artist in artists: - print('Creating data for', artist) + print("Creating data for", artist) albums = factories.AlbumFactory.create_batch( - artist=artist, size=random.randint(1, 5)) + artist=artist, size=random.randint(1, 5) + ) for album in albums: factories.TrackFileFactory.create_batch( - track__album=album, size=random.randint(3, 18)) + track__album=album, size=random.randint(3, 18) + ) -if __name__ == '__main__': +if __name__ == "__main__": create_data() diff --git a/api/funkwhale_api/music/filters.py b/api/funkwhale_api/music/filters.py index dc7aafc21..6e1e4df09 100644 --- a/api/funkwhale_api/music/filters.py +++ b/api/funkwhale_api/music/filters.py @@ -7,12 +7,10 @@ from . import models class ListenableMixin(filters.FilterSet): - listenable = filters.BooleanFilter(name='_', method='filter_listenable') + listenable = filters.BooleanFilter(name="_", method="filter_listenable") def filter_listenable(self, queryset, name, value): - queryset = queryset.annotate( - files_count=Count('tracks__files') - ) + queryset = queryset.annotate(files_count=Count("tracks__files")) if value: return queryset.filter(files_count__gt=0) else: @@ -20,39 +18,31 @@ class ListenableMixin(filters.FilterSet): class ArtistFilter(ListenableMixin): - q = fields.SearchFilter(search_fields=[ - 'name', - ]) + q = fields.SearchFilter(search_fields=["name"]) class Meta: model = models.Artist fields = { - 'name': ['exact', 'iexact', 'startswith', 'icontains'], - 'listenable': 'exact', + "name": ["exact", "iexact", "startswith", "icontains"], + "listenable": "exact", } class TrackFilter(filters.FilterSet): - q = fields.SearchFilter(search_fields=[ - 'title', - 'album__title', - 'artist__name', - ]) - listenable = filters.BooleanFilter(name='_', method='filter_listenable') + q = fields.SearchFilter(search_fields=["title", "album__title", "artist__name"]) + listenable = filters.BooleanFilter(name="_", method="filter_listenable") class Meta: model = models.Track fields = { - 'title': ['exact', 'iexact', 'startswith', 'icontains'], - 'listenable': ['exact'], - 'artist': ['exact'], - 'album': ['exact'], + "title": ["exact", "iexact", "startswith", "icontains"], + "listenable": ["exact"], + "artist": ["exact"], + "album": ["exact"], } def filter_listenable(self, queryset, name, value): - queryset = queryset.annotate( - files_count=Count('files') - ) + queryset = queryset.annotate(files_count=Count("files")) if value: return queryset.filter(files_count__gt=0) else: @@ -60,46 +50,32 @@ class TrackFilter(filters.FilterSet): class ImportBatchFilter(filters.FilterSet): - q = fields.SearchFilter(search_fields=[ - 'submitted_by__username', - 'source', - ]) + q = fields.SearchFilter(search_fields=["submitted_by__username", "source"]) class Meta: model = models.ImportBatch - fields = { - 'status': ['exact'], - 'source': ['exact'], - 'submitted_by': ['exact'], - } + fields = {"status": ["exact"], "source": ["exact"], "submitted_by": ["exact"]} class ImportJobFilter(filters.FilterSet): - q = fields.SearchFilter(search_fields=[ - 'batch__submitted_by__username', - 'source', - ]) + q = fields.SearchFilter(search_fields=["batch__submitted_by__username", "source"]) class Meta: model = models.ImportJob fields = { - 'batch': ['exact'], - 'batch__status': ['exact'], - 'batch__source': ['exact'], - 'batch__submitted_by': ['exact'], - 'status': ['exact'], - 'source': ['exact'], + "batch": ["exact"], + "batch__status": ["exact"], + "batch__source": ["exact"], + "batch__submitted_by": ["exact"], + "status": ["exact"], + "source": ["exact"], } class AlbumFilter(ListenableMixin): - listenable = filters.BooleanFilter(name='_', method='filter_listenable') - q = fields.SearchFilter(search_fields=[ - 'title', - 'artist__name' - 'source', - ]) + listenable = filters.BooleanFilter(name="_", method="filter_listenable") + q = fields.SearchFilter(search_fields=["title", "artist__name" "source"]) class Meta: model = models.Album - fields = ['listenable', 'q', 'artist'] + fields = ["listenable", "q", "artist"] diff --git a/api/funkwhale_api/music/importers.py b/api/funkwhale_api/music/importers.py index 7e26fe968..ce7ded02b 100644 --- a/api/funkwhale_api/music/importers.py +++ b/api/funkwhale_api/music/importers.py @@ -1,42 +1,43 @@ - - def load(model, *args, **kwargs): importer = registry[model.__name__](model=model) return importer.load(*args, **kwargs) + class Importer(object): def __init__(self, model): self.model = model def load(self, cleaned_data, raw_data, import_hooks): - mbid = cleaned_data.pop('mbid') + mbid = cleaned_data.pop("mbid") m = self.model.objects.update_or_create(mbid=mbid, defaults=cleaned_data)[0] for hook in import_hooks: hook(m, cleaned_data, raw_data) return m + class Mapping(object): """Cast musicbrainz data to funkwhale data and vice-versa""" + def __init__(self, musicbrainz_mapping): self.musicbrainz_mapping = musicbrainz_mapping self._from_musicbrainz = {} self._to_musicbrainz = {} for field_name, conf in self.musicbrainz_mapping.items(): - self._from_musicbrainz[conf['musicbrainz_field_name']] = { - 'field_name': field_name, - 'converter': conf.get('converter', lambda v: v) + self._from_musicbrainz[conf["musicbrainz_field_name"]] = { + "field_name": field_name, + "converter": conf.get("converter", lambda v: v), } self._to_musicbrainz[field_name] = { - 'field_name': conf['musicbrainz_field_name'], - 'converter': conf.get('converter', lambda v: v) + "field_name": conf["musicbrainz_field_name"], + "converter": conf.get("converter", lambda v: v), } - def from_musicbrainz(self, key, value): - return self._from_musicbrainz[key]['field_name'], self._from_musicbrainz[key]['converter'](value) -registry = { - 'Artist': Importer, - 'Track': Importer, - 'Album': Importer, - 'Work': Importer, -} + def from_musicbrainz(self, key, value): + return ( + self._from_musicbrainz[key]["field_name"], + self._from_musicbrainz[key]["converter"](value), + ) + + +registry = {"Artist": Importer, "Track": Importer, "Album": Importer, "Work": Importer} diff --git a/api/funkwhale_api/music/lyrics.py b/api/funkwhale_api/music/lyrics.py index 1ad69ce25..4fd31d92c 100644 --- a/api/funkwhale_api/music/lyrics.py +++ b/api/funkwhale_api/music/lyrics.py @@ -6,22 +6,22 @@ from bs4 import BeautifulSoup def _get_html(url): with urllib.request.urlopen(url) as response: html = response.read() - return html.decode('utf-8') + return html.decode("utf-8") def extract_content(html): soup = BeautifulSoup(html, "html.parser") - return soup.find_all("div", class_='lyricbox')[0].contents + return soup.find_all("div", class_="lyricbox")[0].contents def clean_content(contents): final_content = "" for e in contents: - if e == '\n': + if e == "\n": continue - if e.name == 'script': + if e.name == "script": continue - if e.name == 'br': + if e.name == "br": final_content += "\n" continue try: diff --git a/api/funkwhale_api/music/management/commands/fix_track_files.py b/api/funkwhale_api/music/management/commands/fix_track_files.py index c18e2b255..94dd4fb9f 100644 --- a/api/funkwhale_api/music/management/commands/fix_track_files.py +++ b/api/funkwhale_api/music/management/commands/fix_track_files.py @@ -10,20 +10,20 @@ from funkwhale_api.music import models, utils class Command(BaseCommand): - help = 'Run common checks and fix against imported tracks' + help = "Run common checks and fix against imported tracks" def add_arguments(self, parser): parser.add_argument( - '--dry-run', - action='store_true', - dest='dry_run', + "--dry-run", + action="store_true", + dest="dry_run", default=False, - help='Do not execute anything' + help="Do not execute anything", ) def handle(self, *args, **options): - if options['dry_run']: - self.stdout.write('Dry-run on, will not commit anything') + if options["dry_run"]: + self.stdout.write("Dry-run on, will not commit anything") self.fix_mimetypes(**options) self.fix_file_data(**options) self.fix_file_size(**options) @@ -31,75 +31,73 @@ class Command(BaseCommand): @transaction.atomic def fix_mimetypes(self, dry_run, **kwargs): - self.stdout.write('Fixing missing mimetypes...') + self.stdout.write("Fixing missing mimetypes...") matching = models.TrackFile.objects.filter( - source__startswith='file://').exclude(mimetype__startswith='audio/') + source__startswith="file://" + ).exclude(mimetype__startswith="audio/") self.stdout.write( - '[mimetypes] {} entries found with bad or no mimetype'.format( - matching.count())) + "[mimetypes] {} entries found with bad or no mimetype".format( + matching.count() + ) + ) for extension, mimetype in utils.EXTENSION_TO_MIMETYPE.items(): - qs = matching.filter(source__endswith='.{}'.format(extension)) + qs = matching.filter(source__endswith=".{}".format(extension)) self.stdout.write( - '[mimetypes] setting {} {} files to {}'.format( + "[mimetypes] setting {} {} files to {}".format( qs.count(), extension, mimetype - )) + ) + ) if not dry_run: - self.stdout.write('[mimetypes] commiting...') + self.stdout.write("[mimetypes] commiting...") qs.update(mimetype=mimetype) def fix_file_data(self, dry_run, **kwargs): - self.stdout.write('Fixing missing bitrate or length...') + self.stdout.write("Fixing missing bitrate or length...") matching = models.TrackFile.objects.filter( - Q(bitrate__isnull=True) | Q(duration__isnull=True)) + Q(bitrate__isnull=True) | Q(duration__isnull=True) + ) total = matching.count() self.stdout.write( - '[bitrate/length] {} entries found with missing values'.format( - total)) + "[bitrate/length] {} entries found with missing values".format(total) + ) if dry_run: return - for i, tf in enumerate(matching.only('audio_file')): + for i, tf in enumerate(matching.only("audio_file")): self.stdout.write( - '[bitrate/length] {}/{} fixing file #{}'.format( - i+1, total, tf.pk - )) + "[bitrate/length] {}/{} fixing file #{}".format(i + 1, total, tf.pk) + ) try: audio_file = tf.get_audio_file() if audio_file: with audio_file as f: data = utils.get_audio_file_data(audio_file) - tf.bitrate = data['bitrate'] - tf.duration = data['length'] - tf.save(update_fields=['duration', 'bitrate']) + tf.bitrate = data["bitrate"] + tf.duration = data["length"] + tf.save(update_fields=["duration", "bitrate"]) else: - self.stderr.write('[bitrate/length] no file found') + self.stderr.write("[bitrate/length] no file found") except Exception as e: self.stderr.write( - '[bitrate/length] error with file #{}: {}'.format( - tf.pk, str(e) - ) + "[bitrate/length] error with file #{}: {}".format(tf.pk, str(e)) ) def fix_file_size(self, dry_run, **kwargs): - self.stdout.write('Fixing missing size...') + self.stdout.write("Fixing missing size...") matching = models.TrackFile.objects.filter(size__isnull=True) total = matching.count() - self.stdout.write( - '[size] {} entries found with missing values'.format(total)) + self.stdout.write("[size] {} entries found with missing values".format(total)) if dry_run: return - for i, tf in enumerate(matching.only('size')): + for i, tf in enumerate(matching.only("size")): self.stdout.write( - '[size] {}/{} fixing file #{}'.format( - i+1, total, tf.pk - )) + "[size] {}/{} fixing file #{}".format(i + 1, total, tf.pk) + ) try: tf.size = tf.get_file_size() - tf.save(update_fields=['size']) + tf.save(update_fields=["size"]) except Exception as e: self.stderr.write( - '[size] error with file #{}: {}'.format( - tf.pk, str(e) - ) + "[size] error with file #{}: {}".format(tf.pk, str(e)) ) diff --git a/api/funkwhale_api/music/metadata.py b/api/funkwhale_api/music/metadata.py index 4c17c42c0..3a43521c5 100644 --- a/api/funkwhale_api/music/metadata.py +++ b/api/funkwhale_api/music/metadata.py @@ -14,21 +14,17 @@ class UnsupportedTag(KeyError): def get_id3_tag(f, k): - if k == 'pictures': - return f.tags.getall('APIC') + if k == "pictures": + return f.tags.getall("APIC") # First we try to grab the standard key try: return f.tags[k].text[0] except KeyError: pass # then we fallback on parsing non standard tags - all_tags = f.tags.getall('TXXX') + all_tags = f.tags.getall("TXXX") try: - matches = [ - t - for t in all_tags - if t.desc.lower() == k.lower() - ] + matches = [t for t in all_tags if t.desc.lower() == k.lower()] return matches[0].text[0] except (KeyError, IndexError): raise TagNotFound(k) @@ -37,17 +33,19 @@ def get_id3_tag(f, k): def clean_id3_pictures(apic): pictures = [] for p in list(apic): - pictures.append({ - 'mimetype': p.mime, - 'content': p.data, - 'description': p.desc, - 'type': p.type.real, - }) + pictures.append( + { + "mimetype": p.mime, + "content": p.data, + "description": p.desc, + "type": p.type.real, + } + ) return pictures def get_flac_tag(f, k): - if k == 'pictures': + if k == "pictures": return f.pictures try: return f.get(k, [])[0] @@ -58,22 +56,22 @@ def get_flac_tag(f, k): def clean_flac_pictures(apic): pictures = [] for p in list(apic): - pictures.append({ - 'mimetype': p.mime, - 'content': p.data, - 'description': p.desc, - 'type': p.type.real, - }) + pictures.append( + { + "mimetype": p.mime, + "content": p.data, + "description": p.desc, + "type": p.type.real, + } + ) return pictures def get_mp3_recording_id(f, k): try: - return [ - t - for t in f.tags.getall('UFID') - if 'musicbrainz.org' in t.owner - ][0].data.decode('utf-8') + return [t for t in f.tags.getall("UFID") if "musicbrainz.org" in t.owner][ + 0 + ].data.decode("utf-8") except IndexError: raise TagNotFound(k) @@ -86,18 +84,17 @@ def convert_track_number(v): pass try: - return int(v.split('/')[0]) + return int(v.split("/")[0]) except (ValueError, AttributeError, IndexError): pass - class FirstUUIDField(forms.UUIDField): def to_python(self, value): try: # sometimes, Picard leaves to uuids in the field, separated # by a slash - value = value.split('/')[0] + value = value.split("/")[0] except (AttributeError, IndexError, TypeError): pass @@ -105,150 +102,119 @@ class FirstUUIDField(forms.UUIDField): VALIDATION = { - 'musicbrainz_artistid': FirstUUIDField(), - 'musicbrainz_albumid': FirstUUIDField(), - 'musicbrainz_recordingid': FirstUUIDField(), + "musicbrainz_artistid": FirstUUIDField(), + "musicbrainz_albumid": FirstUUIDField(), + "musicbrainz_recordingid": FirstUUIDField(), } CONF = { - 'OggVorbis': { - 'getter': lambda f, k: f[k][0], - 'fields': { - 'track_number': { - 'field': 'TRACKNUMBER', - 'to_application': convert_track_number + "OggVorbis": { + "getter": lambda f, k: f[k][0], + "fields": { + "track_number": { + "field": "TRACKNUMBER", + "to_application": convert_track_number, }, - 'title': {}, - 'artist': {}, - 'album': {}, - 'date': { - 'field': 'date', - 'to_application': lambda v: arrow.get(v).date() - }, - 'musicbrainz_albumid': {}, - 'musicbrainz_artistid': {}, - 'musicbrainz_recordingid': { - 'field': 'musicbrainz_trackid' - }, - } + "title": {}, + "artist": {}, + "album": {}, + "date": {"field": "date", "to_application": lambda v: arrow.get(v).date()}, + "musicbrainz_albumid": {}, + "musicbrainz_artistid": {}, + "musicbrainz_recordingid": {"field": "musicbrainz_trackid"}, + }, }, - 'OggTheora': { - 'getter': lambda f, k: f[k][0], - 'fields': { - 'track_number': { - 'field': 'TRACKNUMBER', - 'to_application': convert_track_number + "OggTheora": { + "getter": lambda f, k: f[k][0], + "fields": { + "track_number": { + "field": "TRACKNUMBER", + "to_application": convert_track_number, }, - 'title': {}, - 'artist': {}, - 'album': {}, - 'date': { - 'field': 'date', - 'to_application': lambda v: arrow.get(v).date() - }, - 'musicbrainz_albumid': { - 'field': 'MusicBrainz Album Id' - }, - 'musicbrainz_artistid': { - 'field': 'MusicBrainz Artist Id' - }, - 'musicbrainz_recordingid': { - 'field': 'MusicBrainz Track Id' - }, - } + "title": {}, + "artist": {}, + "album": {}, + "date": {"field": "date", "to_application": lambda v: arrow.get(v).date()}, + "musicbrainz_albumid": {"field": "MusicBrainz Album Id"}, + "musicbrainz_artistid": {"field": "MusicBrainz Artist Id"}, + "musicbrainz_recordingid": {"field": "MusicBrainz Track Id"}, + }, }, - 'MP3': { - 'getter': get_id3_tag, - 'clean_pictures': clean_id3_pictures, - 'fields': { - 'track_number': { - 'field': 'TRCK', - 'to_application': convert_track_number + "MP3": { + "getter": get_id3_tag, + "clean_pictures": clean_id3_pictures, + "fields": { + "track_number": {"field": "TRCK", "to_application": convert_track_number}, + "title": {"field": "TIT2"}, + "artist": {"field": "TPE1"}, + "album": {"field": "TALB"}, + "date": { + "field": "TDRC", + "to_application": lambda v: arrow.get(str(v)).date(), }, - 'title': { - 'field': 'TIT2' + "musicbrainz_albumid": {"field": "MusicBrainz Album Id"}, + "musicbrainz_artistid": {"field": "MusicBrainz Artist Id"}, + "musicbrainz_recordingid": { + "field": "UFID", + "getter": get_mp3_recording_id, }, - 'artist': { - 'field': 'TPE1' - }, - 'album': { - 'field': 'TALB' - }, - 'date': { - 'field': 'TDRC', - 'to_application': lambda v: arrow.get(str(v)).date() - }, - 'musicbrainz_albumid': { - 'field': 'MusicBrainz Album Id' - }, - 'musicbrainz_artistid': { - 'field': 'MusicBrainz Artist Id' - }, - 'musicbrainz_recordingid': { - 'field': 'UFID', - 'getter': get_mp3_recording_id, - }, - 'pictures': {}, - } + "pictures": {}, + }, }, - 'FLAC': { - 'getter': get_flac_tag, - 'clean_pictures': clean_flac_pictures, - 'fields': { - 'track_number': { - 'field': 'tracknumber', - 'to_application': convert_track_number + "FLAC": { + "getter": get_flac_tag, + "clean_pictures": clean_flac_pictures, + "fields": { + "track_number": { + "field": "tracknumber", + "to_application": convert_track_number, }, - 'title': {}, - 'artist': {}, - 'album': {}, - 'date': { - 'field': 'date', - 'to_application': lambda v: arrow.get(str(v)).date() + "title": {}, + "artist": {}, + "album": {}, + "date": { + "field": "date", + "to_application": lambda v: arrow.get(str(v)).date(), }, - 'musicbrainz_albumid': {}, - 'musicbrainz_artistid': {}, - 'musicbrainz_recordingid': { - 'field': 'musicbrainz_trackid' - }, - 'test': {}, - 'pictures': {}, - } + "musicbrainz_albumid": {}, + "musicbrainz_artistid": {}, + "musicbrainz_recordingid": {"field": "musicbrainz_trackid"}, + "test": {}, + "pictures": {}, + }, }, } class Metadata(object): - def __init__(self, path): self._file = mutagen.File(path) if self._file is None: - raise ValueError('Cannot parse metadata from {}'.format(path)) + raise ValueError("Cannot parse metadata from {}".format(path)) ft = self.get_file_type(self._file) try: self._conf = CONF[ft] except KeyError: - raise ValueError('Unsupported format {}'.format(ft)) + raise ValueError("Unsupported format {}".format(ft)) def get_file_type(self, f): return f.__class__.__name__ def get(self, key, default=NODEFAULT): try: - field_conf = self._conf['fields'][key] + field_conf = self._conf["fields"][key] except KeyError: - raise UnsupportedTag( - '{} is not supported for this file format'.format(key)) - real_key = field_conf.get('field', key) + raise UnsupportedTag("{} is not supported for this file format".format(key)) + real_key = field_conf.get("field", key) try: - getter = field_conf.get('getter', self._conf['getter']) + getter = field_conf.get("getter", self._conf["getter"]) v = getter(self._file, real_key) except KeyError: if default == NODEFAULT: raise TagNotFound(real_key) return default - converter = field_conf.get('to_application') + converter = field_conf.get("to_application") if converter: v = converter(v) field = VALIDATION.get(key) @@ -256,15 +222,15 @@ class Metadata(object): v = field.to_python(v) return v - def get_picture(self, picture_type='cover_front'): + def get_picture(self, picture_type="cover_front"): ptype = getattr(mutagen.id3.PictureType, picture_type.upper()) try: - pictures = self.get('pictures') + pictures = self.get("pictures") except (UnsupportedTag, TagNotFound): return - cleaner = self._conf.get('clean_pictures', lambda v: v) + cleaner = self._conf.get("clean_pictures", lambda v: v) pictures = cleaner(pictures) for p in pictures: - if p['type'] == ptype: + if p["type"] == ptype: return p diff --git a/api/funkwhale_api/music/migrations/0001_initial.py b/api/funkwhale_api/music/migrations/0001_initial.py index 265b81577..0bb12342d 100644 --- a/api/funkwhale_api/music/migrations/0001_initial.py +++ b/api/funkwhale_api/music/migrations/0001_initial.py @@ -8,82 +8,183 @@ import django.utils.timezone class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] operations = [ migrations.CreateModel( - name='Album', + name="Album", fields=[ - ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')), - ('mbid', models.UUIDField(editable=False, blank=True, null=True)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('title', models.CharField(max_length=255)), - ('release_date', models.DateField()), - ('type', models.CharField(default='album', choices=[('album', 'Album')], max_length=30)), + ( + "id", + models.AutoField( + primary_key=True, + auto_created=True, + serialize=False, + verbose_name="ID", + ), + ), + ("mbid", models.UUIDField(editable=False, blank=True, null=True)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("title", models.CharField(max_length=255)), + ("release_date", models.DateField()), + ( + "type", + models.CharField( + default="album", choices=[("album", "Album")], max_length=30 + ), + ), ], - options={ - 'abstract': False, - }, + options={"abstract": False}, ), migrations.CreateModel( - name='Artist', + name="Artist", fields=[ - ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')), - ('mbid', models.UUIDField(editable=False, blank=True, null=True)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('name', models.CharField(max_length=255)), + ( + "id", + models.AutoField( + primary_key=True, + auto_created=True, + serialize=False, + verbose_name="ID", + ), + ), + ("mbid", models.UUIDField(editable=False, blank=True, null=True)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("name", models.CharField(max_length=255)), ], - options={ - 'abstract': False, - }, + options={"abstract": False}, ), migrations.CreateModel( - name='ImportBatch', + name="ImportBatch", fields=[ - ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('submitted_by', models.ForeignKey(related_name='imports', to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + primary_key=True, + auto_created=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "submitted_by", + models.ForeignKey( + related_name="imports", + to=settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ), + ), ], ), migrations.CreateModel( - name='ImportJob', + name="ImportJob", fields=[ - ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')), - ('source', models.URLField()), - ('mbid', models.UUIDField(editable=False)), - ('status', models.CharField(default='pending', choices=[('pending', 'Pending'), ('finished', 'finished')], max_length=30)), - ('batch', models.ForeignKey(related_name='jobs', to='music.ImportBatch', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + primary_key=True, + auto_created=True, + serialize=False, + verbose_name="ID", + ), + ), + ("source", models.URLField()), + ("mbid", models.UUIDField(editable=False)), + ( + "status", + models.CharField( + default="pending", + choices=[("pending", "Pending"), ("finished", "finished")], + max_length=30, + ), + ), + ( + "batch", + models.ForeignKey( + related_name="jobs", + to="music.ImportBatch", + on_delete=models.CASCADE, + ), + ), ], ), migrations.CreateModel( - name='Track', + name="Track", fields=[ - ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')), - ('mbid', models.UUIDField(editable=False, blank=True, null=True)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('title', models.CharField(max_length=255)), - ('album', models.ForeignKey(related_name='tracks', blank=True, null=True, to='music.Album', on_delete=models.CASCADE)), - ('artist', models.ForeignKey(related_name='tracks', to='music.Artist', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + primary_key=True, + auto_created=True, + serialize=False, + verbose_name="ID", + ), + ), + ("mbid", models.UUIDField(editable=False, blank=True, null=True)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("title", models.CharField(max_length=255)), + ( + "album", + models.ForeignKey( + related_name="tracks", + blank=True, + null=True, + to="music.Album", + on_delete=models.CASCADE, + ), + ), + ( + "artist", + models.ForeignKey( + related_name="tracks", + to="music.Artist", + on_delete=models.CASCADE, + ), + ), ], - options={ - 'abstract': False, - }, + options={"abstract": False}, ), migrations.CreateModel( - name='TrackFile', + name="TrackFile", fields=[ - ('id', models.AutoField(primary_key=True, auto_created=True, serialize=False, verbose_name='ID')), - ('audio_file', models.FileField(upload_to='tracks')), - ('source', models.URLField(blank=True, null=True)), - ('duration', models.IntegerField(blank=True, null=True)), - ('track', models.ForeignKey(related_name='files', to='music.Track', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + primary_key=True, + auto_created=True, + serialize=False, + verbose_name="ID", + ), + ), + ("audio_file", models.FileField(upload_to="tracks")), + ("source", models.URLField(blank=True, null=True)), + ("duration", models.IntegerField(blank=True, null=True)), + ( + "track", + models.ForeignKey( + related_name="files", to="music.Track", on_delete=models.CASCADE + ), + ), ], ), migrations.AddField( - model_name='album', - name='artist', - field=models.ForeignKey(related_name='albums', to='music.Artist', on_delete=models.CASCADE), + model_name="album", + name="artist", + field=models.ForeignKey( + related_name="albums", to="music.Artist", on_delete=models.CASCADE + ), ), ] diff --git a/api/funkwhale_api/music/migrations/0002_auto_20151215_1645.py b/api/funkwhale_api/music/migrations/0002_auto_20151215_1645.py index 1b54a5cfc..094c679da 100644 --- a/api/funkwhale_api/music/migrations/0002_auto_20151215_1645.py +++ b/api/funkwhale_api/music/migrations/0002_auto_20151215_1645.py @@ -6,35 +6,31 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0001_initial'), - ] + dependencies = [("music", "0001_initial")] operations = [ migrations.AlterModelOptions( - name='album', - options={'ordering': ['-creation_date']}, + name="album", options={"ordering": ["-creation_date"]} ), migrations.AlterModelOptions( - name='artist', - options={'ordering': ['-creation_date']}, + name="artist", options={"ordering": ["-creation_date"]} ), migrations.AlterModelOptions( - name='importbatch', - options={'ordering': ['-creation_date']}, + name="importbatch", options={"ordering": ["-creation_date"]} ), migrations.AlterModelOptions( - name='track', - options={'ordering': ['-creation_date']}, + name="track", options={"ordering": ["-creation_date"]} ), migrations.AddField( - model_name='album', - name='cover', - field=models.ImageField(upload_to='albums/covers/%Y/%m/%d', null=True, blank=True), + model_name="album", + name="cover", + field=models.ImageField( + upload_to="albums/covers/%Y/%m/%d", null=True, blank=True + ), ), migrations.AlterField( - model_name='trackfile', - name='audio_file', - field=models.FileField(upload_to='tracks/%Y/%m/%d'), + model_name="trackfile", + name="audio_file", + field=models.FileField(upload_to="tracks/%Y/%m/%d"), ), ] diff --git a/api/funkwhale_api/music/migrations/0003_auto_20151222_2233.py b/api/funkwhale_api/music/migrations/0003_auto_20151222_2233.py index 060957dc1..d8337a781 100644 --- a/api/funkwhale_api/music/migrations/0003_auto_20151222_2233.py +++ b/api/funkwhale_api/music/migrations/0003_auto_20151222_2233.py @@ -6,14 +6,10 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0002_auto_20151215_1645'), - ] + dependencies = [("music", "0002_auto_20151215_1645")] operations = [ migrations.AlterField( - model_name='album', - name='release_date', - field=models.DateField(null=True), - ), + model_name="album", name="release_date", field=models.DateField(null=True) + ) ] diff --git a/api/funkwhale_api/music/migrations/0004_track_tags.py b/api/funkwhale_api/music/migrations/0004_track_tags.py index f95b08b0e..c00f5edfe 100644 --- a/api/funkwhale_api/music/migrations/0004_track_tags.py +++ b/api/funkwhale_api/music/migrations/0004_track_tags.py @@ -8,14 +8,19 @@ import taggit.managers class Migration(migrations.Migration): dependencies = [ - ('taggit', '0002_auto_20150616_2121'), - ('music', '0003_auto_20151222_2233'), + ("taggit", "0002_auto_20150616_2121"), + ("music", "0003_auto_20151222_2233"), ] operations = [ migrations.AddField( - model_name='track', - name='tags', - field=taggit.managers.TaggableManager(verbose_name='Tags', help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag'), - ), + model_name="track", + name="tags", + field=taggit.managers.TaggableManager( + verbose_name="Tags", + help_text="A comma-separated list of tags.", + through="taggit.TaggedItem", + to="taggit.Tag", + ), + ) ] diff --git a/api/funkwhale_api/music/migrations/0005_deduplicate.py b/api/funkwhale_api/music/migrations/0005_deduplicate.py index 82dca0caa..0dfdd78f4 100644 --- a/api/funkwhale_api/music/migrations/0005_deduplicate.py +++ b/api/funkwhale_api/music/migrations/0005_deduplicate.py @@ -5,7 +5,14 @@ from django.db import migrations, models def get_duplicates(model): - return [i['mbid'] for i in model.objects.values('mbid').annotate(idcount=models.Count('mbid')).order_by('-idcount') if i['idcount'] > 1] + return [ + i["mbid"] + for i in model.objects.values("mbid") + .annotate(idcount=models.Count("mbid")) + .order_by("-idcount") + if i["idcount"] > 1 + ] + def deduplicate(apps, schema_editor): Artist = apps.get_model("music", "Artist") @@ -13,28 +20,25 @@ def deduplicate(apps, schema_editor): Track = apps.get_model("music", "Track") for mbid in get_duplicates(Artist): - ref = Artist.objects.filter(mbid=mbid).order_by('pk').first() + ref = Artist.objects.filter(mbid=mbid).order_by("pk").first() duplicates = Artist.objects.filter(mbid=mbid).exclude(pk=ref.pk) Album.objects.filter(artist__in=duplicates).update(artist=ref) Track.objects.filter(artist__in=duplicates).update(artist=ref) duplicates.delete() for mbid in get_duplicates(Album): - ref = Album.objects.filter(mbid=mbid).order_by('pk').first() + ref = Album.objects.filter(mbid=mbid).order_by("pk").first() duplicates = Album.objects.filter(mbid=mbid).exclude(pk=ref.pk) Track.objects.filter(album__in=duplicates).update(album=ref) duplicates.delete() + def rewind(*args, **kwargs): pass class Migration(migrations.Migration): - dependencies = [ - ('music', '0004_track_tags'), - ] + dependencies = [("music", "0004_track_tags")] - operations = [ - migrations.RunPython(deduplicate, rewind), - ] + operations = [migrations.RunPython(deduplicate, rewind)] diff --git a/api/funkwhale_api/music/migrations/0006_unique_mbid.py b/api/funkwhale_api/music/migrations/0006_unique_mbid.py index e13e3a743..7d926e373 100644 --- a/api/funkwhale_api/music/migrations/0006_unique_mbid.py +++ b/api/funkwhale_api/music/migrations/0006_unique_mbid.py @@ -3,26 +3,31 @@ from __future__ import unicode_literals from django.db import migrations, models + class Migration(migrations.Migration): - dependencies = [ - ('music', '0005_deduplicate'), - ] + dependencies = [("music", "0005_deduplicate")] operations = [ migrations.AlterField( - model_name='album', - name='mbid', - field=models.UUIDField(null=True, editable=False, unique=True, blank=True, db_index=True), + model_name="album", + name="mbid", + field=models.UUIDField( + null=True, editable=False, unique=True, blank=True, db_index=True + ), ), migrations.AlterField( - model_name='artist', - name='mbid', - field=models.UUIDField(null=True, editable=False, unique=True, blank=True, db_index=True), + model_name="artist", + name="mbid", + field=models.UUIDField( + null=True, editable=False, unique=True, blank=True, db_index=True + ), ), migrations.AlterField( - model_name='track', - name='mbid', - field=models.UUIDField(null=True, editable=False, unique=True, blank=True, db_index=True), + model_name="track", + name="mbid", + field=models.UUIDField( + null=True, editable=False, unique=True, blank=True, db_index=True + ), ), ] diff --git a/api/funkwhale_api/music/migrations/0007_track_position.py b/api/funkwhale_api/music/migrations/0007_track_position.py index 089e0128f..d43dcaea3 100644 --- a/api/funkwhale_api/music/migrations/0007_track_position.py +++ b/api/funkwhale_api/music/migrations/0007_track_position.py @@ -6,14 +6,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0006_unique_mbid'), - ] + dependencies = [("music", "0006_unique_mbid")] operations = [ migrations.AddField( - model_name='track', - name='position', + model_name="track", + name="position", field=models.PositiveIntegerField(blank=True, null=True), - ), + ) ] diff --git a/api/funkwhale_api/music/migrations/0008_auto_20160529_1456.py b/api/funkwhale_api/music/migrations/0008_auto_20160529_1456.py index e7fa5c8f4..8812c65a4 100644 --- a/api/funkwhale_api/music/migrations/0008_auto_20160529_1456.py +++ b/api/funkwhale_api/music/migrations/0008_auto_20160529_1456.py @@ -6,24 +6,22 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0007_track_position'), - ] + dependencies = [("music", "0007_track_position")] operations = [ migrations.AlterField( - model_name='album', - name='mbid', + model_name="album", + name="mbid", field=models.UUIDField(null=True, db_index=True, unique=True, blank=True), ), migrations.AlterField( - model_name='artist', - name='mbid', + model_name="artist", + name="mbid", field=models.UUIDField(null=True, db_index=True, unique=True, blank=True), ), migrations.AlterField( - model_name='track', - name='mbid', + model_name="track", + name="mbid", field=models.UUIDField(null=True, db_index=True, unique=True, blank=True), ), ] diff --git a/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py b/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py index 3a3d93989..2999df60c 100644 --- a/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py +++ b/api/funkwhale_api/music/migrations/0009_auto_20160920_1614.py @@ -8,42 +8,71 @@ import versatileimagefield.fields class Migration(migrations.Migration): - dependencies = [ - ('music', '0008_auto_20160529_1456'), - ] + dependencies = [("music", "0008_auto_20160529_1456")] operations = [ migrations.CreateModel( - name='Lyrics', + name="Lyrics", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)), - ('url', models.URLField()), - ('content', models.TextField(null=True, blank=True)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + verbose_name="ID", + serialize=False, + ), + ), + ("url", models.URLField()), + ("content", models.TextField(null=True, blank=True)), ], ), migrations.CreateModel( - name='Work', + name="Work", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)), - ('mbid', models.UUIDField(unique=True, null=True, db_index=True, blank=True)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('language', models.CharField(max_length=20)), - ('nature', models.CharField(max_length=50)), - ('title', models.CharField(max_length=255)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + verbose_name="ID", + serialize=False, + ), + ), + ( + "mbid", + models.UUIDField(unique=True, null=True, db_index=True, blank=True), + ), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("language", models.CharField(max_length=20)), + ("nature", models.CharField(max_length=50)), + ("title", models.CharField(max_length=255)), ], - options={ - 'ordering': ['-creation_date'], - 'abstract': False, - }, + options={"ordering": ["-creation_date"], "abstract": False}, ), migrations.AddField( - model_name='lyrics', - name='work', - field=models.ForeignKey(related_name='lyrics', to='music.Work', blank=True, null=True, on_delete=models.CASCADE), + model_name="lyrics", + name="work", + field=models.ForeignKey( + related_name="lyrics", + to="music.Work", + blank=True, + null=True, + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='track', - name='work', - field=models.ForeignKey(related_name='tracks', to='music.Work', blank=True, null=True, on_delete=models.CASCADE), + model_name="track", + name="work", + field=models.ForeignKey( + related_name="tracks", + to="music.Work", + blank=True, + null=True, + on_delete=models.CASCADE, + ), ), ] diff --git a/api/funkwhale_api/music/migrations/0010_auto_20160920_1742.py b/api/funkwhale_api/music/migrations/0010_auto_20160920_1742.py index 03ac05793..dcb4f7950 100644 --- a/api/funkwhale_api/music/migrations/0010_auto_20160920_1742.py +++ b/api/funkwhale_api/music/migrations/0010_auto_20160920_1742.py @@ -7,14 +7,10 @@ import versatileimagefield.fields class Migration(migrations.Migration): - dependencies = [ - ('music', '0009_auto_20160920_1614'), - ] + dependencies = [("music", "0009_auto_20160920_1614")] operations = [ migrations.AlterField( - model_name='lyrics', - name='url', - field=models.URLField(unique=True), - ), + model_name="lyrics", name="url", field=models.URLField(unique=True) + ) ] diff --git a/api/funkwhale_api/music/migrations/0011_rename_files.py b/api/funkwhale_api/music/migrations/0011_rename_files.py index 1c59535f5..7152cca3b 100644 --- a/api/funkwhale_api/music/migrations/0011_rename_files.py +++ b/api/funkwhale_api/music/migrations/0011_rename_files.py @@ -47,15 +47,13 @@ def rewind(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('music', '0010_auto_20160920_1742'), - ] + dependencies = [("music", "0010_auto_20160920_1742")] operations = [ migrations.AlterField( - model_name='trackfile', - name='audio_file', - field=models.FileField(upload_to='tracks/%Y/%m/%d', max_length=255), + model_name="trackfile", + name="audio_file", + field=models.FileField(upload_to="tracks/%Y/%m/%d", max_length=255), ), migrations.RunPython(rename_files, rewind), ] diff --git a/api/funkwhale_api/music/migrations/0012_auto_20161122_1905.py b/api/funkwhale_api/music/migrations/0012_auto_20161122_1905.py index 8d7e25246..016d1c110 100644 --- a/api/funkwhale_api/music/migrations/0012_auto_20161122_1905.py +++ b/api/funkwhale_api/music/migrations/0012_auto_20161122_1905.py @@ -7,14 +7,14 @@ import versatileimagefield.fields class Migration(migrations.Migration): - dependencies = [ - ('music', '0011_rename_files'), - ] + dependencies = [("music", "0011_rename_files")] operations = [ migrations.AlterField( - model_name='album', - name='cover', - field=versatileimagefield.fields.VersatileImageField(null=True, blank=True, upload_to='albums/covers/%Y/%m/%d'), - ), + model_name="album", + name="cover", + field=versatileimagefield.fields.VersatileImageField( + null=True, blank=True, upload_to="albums/covers/%Y/%m/%d" + ), + ) ] diff --git a/api/funkwhale_api/music/migrations/0013_auto_20171213_2211.py b/api/funkwhale_api/music/migrations/0013_auto_20171213_2211.py index 00ccbb621..2874aa81f 100644 --- a/api/funkwhale_api/music/migrations/0013_auto_20171213_2211.py +++ b/api/funkwhale_api/music/migrations/0013_auto_20171213_2211.py @@ -7,22 +7,16 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0012_auto_20161122_1905'), - ] + dependencies = [("music", "0012_auto_20161122_1905")] operations = [ + migrations.AlterModelOptions(name="importjob", options={"ordering": ("id",)}), migrations.AlterModelOptions( - name='importjob', - options={'ordering': ('id',)}, - ), - migrations.AlterModelOptions( - name='track', - options={'ordering': ['album', 'position']}, + name="track", options={"ordering": ["album", "position"]} ), migrations.AddField( - model_name='album', - name='release_group_id', + model_name="album", + name="release_group_id", field=models.UUIDField(blank=True, null=True), ), ] diff --git a/api/funkwhale_api/music/migrations/0014_importjob_track_file.py b/api/funkwhale_api/music/migrations/0014_importjob_track_file.py index 6950fd3c1..004e247ea 100644 --- a/api/funkwhale_api/music/migrations/0014_importjob_track_file.py +++ b/api/funkwhale_api/music/migrations/0014_importjob_track_file.py @@ -8,14 +8,18 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('music', '0013_auto_20171213_2211'), - ] + dependencies = [("music", "0013_auto_20171213_2211")] operations = [ migrations.AddField( - model_name='importjob', - name='track_file', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to='music.TrackFile'), - ), + model_name="importjob", + name="track_file", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="jobs", + to="music.TrackFile", + ), + ) ] diff --git a/api/funkwhale_api/music/migrations/0015_bind_track_file_to_import_job.py b/api/funkwhale_api/music/migrations/0015_bind_track_file_to_import_job.py index edb5e6470..9762864f1 100644 --- a/api/funkwhale_api/music/migrations/0015_bind_track_file_to_import_job.py +++ b/api/funkwhale_api/music/migrations/0015_bind_track_file_to_import_job.py @@ -10,13 +10,13 @@ def bind_jobs(apps, schema_editor): TrackFile = apps.get_model("music", "TrackFile") ImportJob = apps.get_model("music", "ImportJob") - for job in ImportJob.objects.all().only('mbid'): + for job in ImportJob.objects.all().only("mbid"): f = TrackFile.objects.filter(track__mbid=job.mbid).first() if not f: - print('No file for mbid {}'.format(job.mbid)) + print("No file for mbid {}".format(job.mbid)) continue job.track_file = f - job.save(update_fields=['track_file']) + job.save(update_fields=["track_file"]) def rewind(apps, schema_editor): @@ -25,10 +25,6 @@ def rewind(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('music', '0014_importjob_track_file'), - ] + dependencies = [("music", "0014_importjob_track_file")] - operations = [ - migrations.RunPython(bind_jobs, rewind), - ] + operations = [migrations.RunPython(bind_jobs, rewind)] diff --git a/api/funkwhale_api/music/migrations/0016_trackfile_acoustid_track_id.py b/api/funkwhale_api/music/migrations/0016_trackfile_acoustid_track_id.py index 21d8ce8ea..467fb0eef 100644 --- a/api/funkwhale_api/music/migrations/0016_trackfile_acoustid_track_id.py +++ b/api/funkwhale_api/music/migrations/0016_trackfile_acoustid_track_id.py @@ -5,14 +5,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0015_bind_track_file_to_import_job'), - ] + dependencies = [("music", "0015_bind_track_file_to_import_job")] operations = [ migrations.AddField( - model_name='trackfile', - name='acoustid_track_id', + model_name="trackfile", + name="acoustid_track_id", field=models.UUIDField(blank=True, null=True), - ), + ) ] diff --git a/api/funkwhale_api/music/migrations/0017_auto_20171227_1728.py b/api/funkwhale_api/music/migrations/0017_auto_20171227_1728.py index dfca66437..10a8ed1e8 100644 --- a/api/funkwhale_api/music/migrations/0017_auto_20171227_1728.py +++ b/api/funkwhale_api/music/migrations/0017_auto_20171227_1728.py @@ -5,24 +5,28 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0016_trackfile_acoustid_track_id'), - ] + dependencies = [("music", "0016_trackfile_acoustid_track_id")] operations = [ migrations.AddField( - model_name='importbatch', - name='source', - field=models.CharField(choices=[('api', 'api'), ('shell', 'shell')], default='api', max_length=30), + model_name="importbatch", + name="source", + field=models.CharField( + choices=[("api", "api"), ("shell", "shell")], + default="api", + max_length=30, + ), ), migrations.AddField( - model_name='importjob', - name='audio_file', - field=models.FileField(blank=True, max_length=255, null=True, upload_to='imports/%Y/%m/%d'), + model_name="importjob", + name="audio_file", + field=models.FileField( + blank=True, max_length=255, null=True, upload_to="imports/%Y/%m/%d" + ), ), migrations.AlterField( - model_name='importjob', - name='mbid', + model_name="importjob", + name="mbid", field=models.UUIDField(blank=True, editable=False, null=True), ), ] diff --git a/api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py b/api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py index c45298798..bfc26b011 100644 --- a/api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py +++ b/api/funkwhale_api/music/migrations/0018_auto_20180218_1554.py @@ -5,24 +5,31 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0017_auto_20171227_1728'), - ] + dependencies = [("music", "0017_auto_20171227_1728")] operations = [ migrations.AddField( - model_name='trackfile', - name='mimetype', + model_name="trackfile", + name="mimetype", field=models.CharField(blank=True, max_length=200, null=True), ), migrations.AlterField( - model_name='importjob', - name='source', + model_name="importjob", + name="source", field=models.CharField(max_length=500), ), migrations.AlterField( - model_name='importjob', - name='status', - field=models.CharField(choices=[('pending', 'Pending'), ('finished', 'Finished'), ('errored', 'Errored'), ('skipped', 'Skipped')], default='pending', max_length=30), + model_name="importjob", + name="status", + field=models.CharField( + choices=[ + ("pending", "Pending"), + ("finished", "Finished"), + ("errored", "Errored"), + ("skipped", "Skipped"), + ], + default="pending", + max_length=30, + ), ), ] diff --git a/api/funkwhale_api/music/migrations/0019_populate_mimetypes.py b/api/funkwhale_api/music/migrations/0019_populate_mimetypes.py index 127aa5e69..55f9a127a 100644 --- a/api/funkwhale_api/music/migrations/0019_populate_mimetypes.py +++ b/api/funkwhale_api/music/migrations/0019_populate_mimetypes.py @@ -9,14 +9,16 @@ from funkwhale_api.music.utils import guess_mimetype def populate_mimetype(apps, schema_editor): TrackFile = apps.get_model("music", "TrackFile") - for tf in TrackFile.objects.filter(audio_file__isnull=False, mimetype__isnull=True).only('audio_file'): + for tf in TrackFile.objects.filter( + audio_file__isnull=False, mimetype__isnull=True + ).only("audio_file"): try: tf.mimetype = guess_mimetype(tf.audio_file) except Exception as e: - print('Error on track file {}: {}'.format(tf.pk, e)) + print("Error on track file {}: {}".format(tf.pk, e)) continue - print('Track file {}: {}'.format(tf.pk, tf.mimetype)) - tf.save(update_fields=['mimetype']) + print("Track file {}: {}".format(tf.pk, tf.mimetype)) + tf.save(update_fields=["mimetype"]) def rewind(apps, schema_editor): @@ -25,10 +27,6 @@ def rewind(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('music', '0018_auto_20180218_1554'), - ] + dependencies = [("music", "0018_auto_20180218_1554")] - operations = [ - migrations.RunPython(populate_mimetype, rewind), - ] + operations = [migrations.RunPython(populate_mimetype, rewind)] diff --git a/api/funkwhale_api/music/migrations/0020_importbatch_status.py b/api/funkwhale_api/music/migrations/0020_importbatch_status.py index 265d1ba5d..e02aa0859 100644 --- a/api/funkwhale_api/music/migrations/0020_importbatch_status.py +++ b/api/funkwhale_api/music/migrations/0020_importbatch_status.py @@ -5,14 +5,21 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0019_populate_mimetypes'), - ] + dependencies = [("music", "0019_populate_mimetypes")] operations = [ migrations.AddField( - model_name='importbatch', - name='status', - field=models.CharField(choices=[('pending', 'Pending'), ('finished', 'Finished'), ('errored', 'Errored'), ('skipped', 'Skipped')], default='pending', max_length=30), - ), + model_name="importbatch", + name="status", + field=models.CharField( + choices=[ + ("pending", "Pending"), + ("finished", "Finished"), + ("errored", "Errored"), + ("skipped", "Skipped"), + ], + default="pending", + max_length=30, + ), + ) ] diff --git a/api/funkwhale_api/music/migrations/0021_populate_batch_status.py b/api/funkwhale_api/music/migrations/0021_populate_batch_status.py index 061d649b0..5bda0edfb 100644 --- a/api/funkwhale_api/music/migrations/0021_populate_batch_status.py +++ b/api/funkwhale_api/music/migrations/0021_populate_batch_status.py @@ -7,11 +7,12 @@ from django.db import migrations, models def populate_status(apps, schema_editor): from funkwhale_api.music.utils import compute_status + ImportBatch = apps.get_model("music", "ImportBatch") - for ib in ImportBatch.objects.prefetch_related('jobs'): + for ib in ImportBatch.objects.prefetch_related("jobs"): ib.status = compute_status(ib.jobs.all()) - ib.save(update_fields=['status']) + ib.save(update_fields=["status"]) def rewind(apps, schema_editor): @@ -20,10 +21,6 @@ def rewind(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('music', '0020_importbatch_status'), - ] + dependencies = [("music", "0020_importbatch_status")] - operations = [ - migrations.RunPython(populate_status, rewind), - ] + operations = [migrations.RunPython(populate_status, rewind)] diff --git a/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py b/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py index d9f6f01d9..89fca02d6 100644 --- a/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py +++ b/api/funkwhale_api/music/migrations/0022_importbatch_import_request.py @@ -6,15 +6,18 @@ import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ - ('requests', '__first__'), - ('music', '0021_populate_batch_status'), - ] + dependencies = [("requests", "__first__"), ("music", "0021_populate_batch_status")] operations = [ migrations.AddField( - model_name='importbatch', - name='import_request', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='import_batches', to='requests.ImportRequest'), - ), + model_name="importbatch", + name="import_request", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="import_batches", + to="requests.ImportRequest", + ), + ) ] diff --git a/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py b/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py index ed7404ac4..8c6537d85 100644 --- a/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py +++ b/api/funkwhale_api/music/migrations/0023_auto_20180407_1010.py @@ -9,79 +9,105 @@ import django.utils.timezone class Migration(migrations.Migration): dependencies = [ - ('federation', '0003_auto_20180407_1010'), - ('music', '0022_importbatch_import_request'), + ("federation", "0003_auto_20180407_1010"), + ("music", "0022_importbatch_import_request"), ] operations = [ migrations.AddField( - model_name='album', - name='uuid', + model_name="album", + name="uuid", field=models.UUIDField(db_index=True, null=True, unique=True), ), migrations.AddField( - model_name='artist', - name='uuid', + model_name="artist", + name="uuid", field=models.UUIDField(db_index=True, null=True, unique=True), ), migrations.AddField( - model_name='importbatch', - name='uuid', + model_name="importbatch", + name="uuid", field=models.UUIDField(db_index=True, null=True, unique=True), ), migrations.AddField( - model_name='importjob', - name='library_track', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='import_jobs', to='federation.LibraryTrack'), + model_name="importjob", + name="library_track", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="import_jobs", + to="federation.LibraryTrack", + ), ), migrations.AddField( - model_name='importjob', - name='uuid', + model_name="importjob", + name="uuid", field=models.UUIDField(db_index=True, null=True, unique=True), ), migrations.AddField( - model_name='lyrics', - name='uuid', + model_name="lyrics", + name="uuid", field=models.UUIDField(db_index=True, null=True, unique=True), ), migrations.AddField( - model_name='track', - name='uuid', + model_name="track", + name="uuid", field=models.UUIDField(db_index=True, null=True, unique=True), ), migrations.AddField( - model_name='trackfile', - name='creation_date', + model_name="trackfile", + name="creation_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AddField( - model_name='trackfile', - name='library_track', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='local_track_file', to='federation.LibraryTrack'), + model_name="trackfile", + name="library_track", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="local_track_file", + to="federation.LibraryTrack", + ), ), migrations.AddField( - model_name='trackfile', - name='modification_date', + model_name="trackfile", + name="modification_date", field=models.DateTimeField(auto_now=True), ), migrations.AddField( - model_name='trackfile', - name='uuid', + model_name="trackfile", + name="uuid", field=models.UUIDField(db_index=True, null=True, unique=True), ), migrations.AddField( - model_name='work', - name='uuid', + model_name="work", + name="uuid", field=models.UUIDField(db_index=True, null=True, unique=True), ), migrations.AlterField( - model_name='importbatch', - name='source', - field=models.CharField(choices=[('api', 'api'), ('shell', 'shell'), ('federation', 'federation')], default='api', max_length=30), + model_name="importbatch", + name="source", + field=models.CharField( + choices=[ + ("api", "api"), + ("shell", "shell"), + ("federation", "federation"), + ], + default="api", + max_length=30, + ), ), migrations.AlterField( - model_name='importbatch', - name='submitted_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='imports', to=settings.AUTH_USER_MODEL), + model_name="importbatch", + name="submitted_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="imports", + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/api/funkwhale_api/music/migrations/0024_populate_uuid.py b/api/funkwhale_api/music/migrations/0024_populate_uuid.py index 10c78a3db..349a901fc 100644 --- a/api/funkwhale_api/music/migrations/0024_populate_uuid.py +++ b/api/funkwhale_api/music/migrations/0024_populate_uuid.py @@ -7,22 +7,22 @@ from django.db import migrations, models def populate_uuids(apps, schema_editor): models = [ - 'Album', - 'Artist', - 'Importbatch', - 'Importjob', - 'Lyrics', - 'Track', - 'Trackfile', - 'Work', + "Album", + "Artist", + "Importbatch", + "Importjob", + "Lyrics", + "Track", + "Trackfile", + "Work", ] for m in models: - kls = apps.get_model('music', m) - qs = kls.objects.filter(uuid__isnull=True).only('id') - print('Setting uuids for {} ({} objects)'.format(m, len(qs))) + kls = apps.get_model("music", m) + qs = kls.objects.filter(uuid__isnull=True).only("id") + print("Setting uuids for {} ({} objects)".format(m, len(qs))) for o in qs: o.uuid = uuid.uuid4() - o.save(update_fields=['uuid']) + o.save(update_fields=["uuid"]) def rewind(apps, schema_editor): @@ -31,50 +31,48 @@ def rewind(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('music', '0023_auto_20180407_1010'), - ] + dependencies = [("music", "0023_auto_20180407_1010")] operations = [ migrations.RunPython(populate_uuids, rewind), migrations.AlterField( - model_name='album', - name='uuid', + model_name="album", + name="uuid", field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), ), migrations.AlterField( - model_name='artist', - name='uuid', + model_name="artist", + name="uuid", field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), ), migrations.AlterField( - model_name='importbatch', - name='uuid', + model_name="importbatch", + name="uuid", field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), ), migrations.AlterField( - model_name='importjob', - name='uuid', + model_name="importjob", + name="uuid", field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), ), migrations.AlterField( - model_name='lyrics', - name='uuid', + model_name="lyrics", + name="uuid", field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), ), migrations.AlterField( - model_name='track', - name='uuid', + model_name="track", + name="uuid", field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), ), migrations.AlterField( - model_name='trackfile', - name='uuid', + model_name="trackfile", + name="uuid", field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), ), migrations.AlterField( - model_name='work', - name='uuid', + model_name="work", + name="uuid", field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), ), ] diff --git a/api/funkwhale_api/music/migrations/0025_auto_20180419_2023.py b/api/funkwhale_api/music/migrations/0025_auto_20180419_2023.py index 6b0230d50..be685f1fe 100644 --- a/api/funkwhale_api/music/migrations/0025_auto_20180419_2023.py +++ b/api/funkwhale_api/music/migrations/0025_auto_20180419_2023.py @@ -5,14 +5,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0024_populate_uuid'), - ] + dependencies = [("music", "0024_populate_uuid")] operations = [ migrations.AlterField( - model_name='trackfile', - name='source', + model_name="trackfile", + name="source", field=models.URLField(blank=True, max_length=500, null=True), - ), + ) ] diff --git a/api/funkwhale_api/music/migrations/0026_trackfile_accessed_date.py b/api/funkwhale_api/music/migrations/0026_trackfile_accessed_date.py index 1d5327d93..f7f46f35a 100644 --- a/api/funkwhale_api/music/migrations/0026_trackfile_accessed_date.py +++ b/api/funkwhale_api/music/migrations/0026_trackfile_accessed_date.py @@ -5,14 +5,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('music', '0025_auto_20180419_2023'), - ] + dependencies = [("music", "0025_auto_20180419_2023")] operations = [ migrations.AddField( - model_name='trackfile', - name='accessed_date', + model_name="trackfile", + name="accessed_date", field=models.DateTimeField(blank=True, null=True), - ), + ) ] diff --git a/api/funkwhale_api/music/migrations/0027_auto_20180515_1808.py b/api/funkwhale_api/music/migrations/0027_auto_20180515_1808.py index 835e115a6..1e3949da4 100644 --- a/api/funkwhale_api/music/migrations/0027_auto_20180515_1808.py +++ b/api/funkwhale_api/music/migrations/0027_auto_20180515_1808.py @@ -6,24 +6,28 @@ import taggit.managers class Migration(migrations.Migration): - dependencies = [ - ('music', '0026_trackfile_accessed_date'), - ] + dependencies = [("music", "0026_trackfile_accessed_date")] operations = [ migrations.AddField( - model_name='trackfile', - name='bitrate', + model_name="trackfile", + name="bitrate", field=models.IntegerField(blank=True, null=True), ), migrations.AddField( - model_name='trackfile', - name='size', + model_name="trackfile", + name="size", field=models.IntegerField(blank=True, null=True), ), migrations.AlterField( - model_name='track', - name='tags', - field=taggit.managers.TaggableManager(blank=True, help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags'), + model_name="track", + name="tags", + field=taggit.managers.TaggableManager( + blank=True, + help_text="A comma-separated list of tags.", + through="taggit.TaggedItem", + to="taggit.Tag", + verbose_name="Tags", + ), ), ] diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index bf3f9e12c..d4625a435 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -29,15 +29,14 @@ from . import utils class APIModelMixin(models.Model): mbid = models.UUIDField(unique=True, db_index=True, null=True, blank=True) - uuid = models.UUIDField( - unique=True, db_index=True, default=uuid.uuid4) + uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4) api_includes = [] creation_date = models.DateTimeField(default=timezone.now) import_hooks = [] class Meta: abstract = True - ordering = ['-creation_date'] + ordering = ["-creation_date"] @classmethod def get_or_create_from_api(cls, mbid): @@ -47,14 +46,20 @@ class APIModelMixin(models.Model): return cls.create_from_api(id=mbid), True def get_api_data(self): - return self.__class__.api.get(id=self.mbid, includes=self.api_includes)[self.musicbrainz_model] + return self.__class__.api.get(id=self.mbid, includes=self.api_includes)[ + self.musicbrainz_model + ] @classmethod def create_from_api(cls, **kwargs): - if kwargs.get('id'): - raw_data = cls.api.get(id=kwargs['id'], includes=cls.api_includes)[cls.musicbrainz_model] + if kwargs.get("id"): + raw_data = cls.api.get(id=kwargs["id"], includes=cls.api_includes)[ + cls.musicbrainz_model + ] else: - raw_data = cls.api.search(**kwargs)['{0}-list'.format(cls.musicbrainz_model)][0] + raw_data = cls.api.search(**kwargs)[ + "{0}-list".format(cls.musicbrainz_model) + ][0] cleaned_data = cls.clean_musicbrainz_data(raw_data) return importers.load(cls, cleaned_data, raw_data, cls.import_hooks) @@ -73,32 +78,28 @@ class APIModelMixin(models.Model): @property def musicbrainz_url(self): if self.mbid: - return 'https://musicbrainz.org/{}/{}'.format( - self.musicbrainz_model, self.mbid) + return "https://musicbrainz.org/{}/{}".format( + self.musicbrainz_model, self.mbid + ) class ArtistQuerySet(models.QuerySet): def with_albums_count(self): - return self.annotate(_albums_count=models.Count('albums')) + return self.annotate(_albums_count=models.Count("albums")) def with_albums(self): return self.prefetch_related( - models.Prefetch( - 'albums', queryset=Album.objects.with_tracks_count()) + models.Prefetch("albums", queryset=Album.objects.with_tracks_count()) ) class Artist(APIModelMixin): name = models.CharField(max_length=255) - musicbrainz_model = 'artist' + musicbrainz_model = "artist" musicbrainz_mapping = { - 'mbid': { - 'musicbrainz_field_name': 'id' - }, - 'name': { - 'musicbrainz_field_name': 'name' - } + "mbid": {"musicbrainz_field_name": "id"}, + "name": {"musicbrainz_field_name": "name"}, } api = musicbrainz.api.artists objects = ArtistQuerySet.as_manager() @@ -116,14 +117,12 @@ class Artist(APIModelMixin): @classmethod def get_or_create_from_name(cls, name, **kwargs): - kwargs.update({'name': name}) - return cls.objects.get_or_create( - name__iexact=name, - defaults=kwargs) + kwargs.update({"name": name}) + return cls.objects.get_or_create(name__iexact=name, defaults=kwargs) def import_artist(v): - a = Artist.get_or_create_from_api(mbid=v[0]['artist']['id'])[0] + a = Artist.get_or_create_from_api(mbid=v[0]["artist"]["id"])[0] return a @@ -135,78 +134,64 @@ def parse_date(v): def import_tracks(instance, cleaned_data, raw_data): - for track_data in raw_data['medium-list'][0]['track-list']: - track_cleaned_data = Track.clean_musicbrainz_data(track_data['recording']) - track_cleaned_data['album'] = instance - track_cleaned_data['position'] = int(track_data['position']) - track = importers.load(Track, track_cleaned_data, track_data, Track.import_hooks) + for track_data in raw_data["medium-list"][0]["track-list"]: + track_cleaned_data = Track.clean_musicbrainz_data(track_data["recording"]) + track_cleaned_data["album"] = instance + track_cleaned_data["position"] = int(track_data["position"]) + track = importers.load( + Track, track_cleaned_data, track_data, Track.import_hooks + ) class AlbumQuerySet(models.QuerySet): def with_tracks_count(self): - return self.annotate(_tracks_count=models.Count('tracks')) + return self.annotate(_tracks_count=models.Count("tracks")) class Album(APIModelMixin): title = models.CharField(max_length=255) - artist = models.ForeignKey( - Artist, related_name='albums', on_delete=models.CASCADE) + artist = models.ForeignKey(Artist, related_name="albums", on_delete=models.CASCADE) release_date = models.DateField(null=True) release_group_id = models.UUIDField(null=True, blank=True) - cover = VersatileImageField(upload_to='albums/covers/%Y/%m/%d', null=True, blank=True) - TYPE_CHOICES = ( - ('album', 'Album'), + cover = VersatileImageField( + upload_to="albums/covers/%Y/%m/%d", null=True, blank=True ) - type = models.CharField(choices=TYPE_CHOICES, max_length=30, default='album') + TYPE_CHOICES = (("album", "Album"),) + type = models.CharField(choices=TYPE_CHOICES, max_length=30, default="album") - api_includes = ['artist-credits', 'recordings', 'media', 'release-groups'] + api_includes = ["artist-credits", "recordings", "media", "release-groups"] api = musicbrainz.api.releases - musicbrainz_model = 'release' + musicbrainz_model = "release" musicbrainz_mapping = { - 'mbid': { - 'musicbrainz_field_name': 'id', + "mbid": {"musicbrainz_field_name": "id"}, + "position": { + "musicbrainz_field_name": "release-list", + "converter": lambda v: int(v[0]["medium-list"][0]["position"]), }, - 'position': { - 'musicbrainz_field_name': 'release-list', - 'converter': lambda v: int(v[0]['medium-list'][0]['position']), + "release_group_id": { + "musicbrainz_field_name": "release-group", + "converter": lambda v: v["id"], }, - 'release_group_id': { - 'musicbrainz_field_name': 'release-group', - 'converter': lambda v: v['id'], + "title": {"musicbrainz_field_name": "title"}, + "release_date": {"musicbrainz_field_name": "date", "converter": parse_date}, + "type": {"musicbrainz_field_name": "type", "converter": lambda v: v.lower()}, + "artist": { + "musicbrainz_field_name": "artist-credit", + "converter": import_artist, }, - 'title': { - 'musicbrainz_field_name': 'title', - }, - 'release_date': { - 'musicbrainz_field_name': 'date', - 'converter': parse_date, - - }, - 'type': { - 'musicbrainz_field_name': 'type', - 'converter': lambda v: v.lower(), - }, - 'artist': { - 'musicbrainz_field_name': 'artist-credit', - 'converter': import_artist, - } } objects = AlbumQuerySet.as_manager() def get_image(self, data=None): if data: - f = ContentFile(data['content']) - extensions = { - 'image/jpeg': 'jpg', - 'image/png': 'png', - 'image/gif': 'gif', - } - extension = extensions.get(data['mimetype'], 'jpg') - self.cover.save('{}.{}'.format(self.uuid, extension), f) + f = ContentFile(data["content"]) + extensions = {"image/jpeg": "jpg", "image/png": "png", "image/gif": "gif"} + extension = extensions.get(data["mimetype"], "jpg") + self.cover.save("{}.{}".format(self.uuid, extension), f) else: - image_data = musicbrainz.api.images.get_front(str(self.mbid)) + image_data = musicbrainz.api.images.get_front(str(self.mbid)) f = ContentFile(image_data) - self.cover.save('{0}.jpg'.format(self.mbid), f) + self.cover.save("{0}.jpg".format(self.mbid), f) return self.cover.file def __str__(self): @@ -222,35 +207,30 @@ class Album(APIModelMixin): @classmethod def get_or_create_from_title(cls, title, **kwargs): - kwargs.update({'title': title}) - return cls.objects.get_or_create( - title__iexact=title, - defaults=kwargs) + kwargs.update({"title": title}) + return cls.objects.get_or_create(title__iexact=title, defaults=kwargs) def import_tags(instance, cleaned_data, raw_data): MINIMUM_COUNT = 2 tags_to_add = [] - for tag_data in raw_data.get('tag-list', []): + for tag_data in raw_data.get("tag-list", []): try: - if int(tag_data['count']) < MINIMUM_COUNT: + if int(tag_data["count"]) < MINIMUM_COUNT: continue except ValueError: continue - tags_to_add.append(tag_data['name']) + tags_to_add.append(tag_data["name"]) instance.tags.add(*tags_to_add) def import_album(v): - a = Album.get_or_create_from_api(mbid=v[0]['id'])[0] + a = Album.get_or_create_from_api(mbid=v[0]["id"])[0] return a def link_recordings(instance, cleaned_data, raw_data): - tracks = [ - r['target'] - for r in raw_data['recording-relation-list'] - ] + tracks = [r["target"] for r in raw_data["recording-relation-list"]] Track.objects.filter(mbid__in=tracks).update(work=instance) @@ -258,9 +238,9 @@ def import_lyrics(instance, cleaned_data, raw_data): try: url = [ url_data - for url_data in raw_data['url-relation-list'] - if url_data['type'] == 'lyrics' - ][0]['target'] + for url_data in raw_data["url-relation-list"] + if url_data["type"] == "lyrics" + ][0]["target"] except (IndexError, KeyError): return l, _ = Lyrics.objects.get_or_create(work=instance, url=url) @@ -274,47 +254,31 @@ class Work(APIModelMixin): title = models.CharField(max_length=255) api = musicbrainz.api.works - api_includes = ['url-rels', 'recording-rels'] - musicbrainz_model = 'work' + api_includes = ["url-rels", "recording-rels"] + musicbrainz_model = "work" musicbrainz_mapping = { - 'mbid': { - 'musicbrainz_field_name': 'id' - }, - 'title': { - 'musicbrainz_field_name': 'title' - }, - 'language': { - 'musicbrainz_field_name': 'language', - }, - 'nature': { - 'musicbrainz_field_name': 'type', - 'converter': lambda v: v.lower(), - }, + "mbid": {"musicbrainz_field_name": "id"}, + "title": {"musicbrainz_field_name": "title"}, + "language": {"musicbrainz_field_name": "language"}, + "nature": {"musicbrainz_field_name": "type", "converter": lambda v: v.lower()}, } - import_hooks = [ - import_lyrics, - link_recordings - ] + import_hooks = [import_lyrics, link_recordings] def fetch_lyrics(self): l = self.lyrics.first() if l: return l - data = self.api.get(self.mbid, includes=['url-rels'])['work'] + data = self.api.get(self.mbid, includes=["url-rels"])["work"] l = import_lyrics(self, {}, data) return l class Lyrics(models.Model): - uuid = models.UUIDField( - unique=True, db_index=True, default=uuid.uuid4) + uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4) work = models.ForeignKey( - Work, - related_name='lyrics', - null=True, - blank=True, - on_delete=models.CASCADE) + Work, related_name="lyrics", null=True, blank=True, on_delete=models.CASCADE + ) url = models.URLField(unique=True) content = models.TextField(null=True, blank=True) @@ -324,67 +288,55 @@ class Lyrics(models.Model): self.content, safe_mode=True, enable_attributes=False, - extensions=['markdown.extensions.nl2br']) + extensions=["markdown.extensions.nl2br"], + ) class TrackQuerySet(models.QuerySet): def for_nested_serialization(self): - return (self.select_related() - .select_related('album__artist', 'artist') - .prefetch_related('files')) + return ( + self.select_related() + .select_related("album__artist", "artist") + .prefetch_related("files") + ) def get_artist(release_list): return Artist.get_or_create_from_api( - mbid=release_list[0]['artist-credits'][0]['artists']['id'])[0] + mbid=release_list[0]["artist-credits"][0]["artists"]["id"] + )[0] class Track(APIModelMixin): title = models.CharField(max_length=255) - artist = models.ForeignKey( - Artist, related_name='tracks', on_delete=models.CASCADE) + artist = models.ForeignKey(Artist, related_name="tracks", on_delete=models.CASCADE) position = models.PositiveIntegerField(null=True, blank=True) album = models.ForeignKey( - Album, - related_name='tracks', - null=True, - blank=True, - on_delete=models.CASCADE) + Album, related_name="tracks", null=True, blank=True, on_delete=models.CASCADE + ) work = models.ForeignKey( - Work, - related_name='tracks', - null=True, - blank=True, - on_delete=models.CASCADE) + Work, related_name="tracks", null=True, blank=True, on_delete=models.CASCADE + ) - musicbrainz_model = 'recording' + musicbrainz_model = "recording" api = musicbrainz.api.recordings - api_includes = ['artist-credits', 'releases', 'media', 'tags', 'work-rels'] + api_includes = ["artist-credits", "releases", "media", "tags", "work-rels"] musicbrainz_mapping = { - 'mbid': { - 'musicbrainz_field_name': 'id' - }, - 'title': { - 'musicbrainz_field_name': 'title' - }, - 'artist': { + "mbid": {"musicbrainz_field_name": "id"}, + "title": {"musicbrainz_field_name": "title"}, + "artist": { # we use the artist from the release to avoid #237 - 'musicbrainz_field_name': 'release-list', - 'converter': get_artist, - }, - 'album': { - 'musicbrainz_field_name': 'release-list', - 'converter': import_album, + "musicbrainz_field_name": "release-list", + "converter": get_artist, }, + "album": {"musicbrainz_field_name": "release-list", "converter": import_album}, } - import_hooks = [ - import_tags - ] + import_hooks = [import_tags] objects = TrackQuerySet.as_manager() tags = TaggableManager(blank=True) class Meta: - ordering = ['album', 'position'] + ordering = ["album", "position"] def __str__(self): return self.title @@ -399,43 +351,33 @@ class Track(APIModelMixin): def get_work(self): if self.work: return self.work - data = self.api.get(self.mbid, includes=['work-rels']) + data = self.api.get(self.mbid, includes=["work-rels"]) try: - work_data = data['recording']['work-relation-list'][0]['work'] + work_data = data["recording"]["work-relation-list"][0]["work"] except (IndexError, KeyError): return - work, _ = Work.get_or_create_from_api(mbid=work_data['id']) + work, _ = Work.get_or_create_from_api(mbid=work_data["id"]) return work def get_lyrics_url(self): - return reverse('api:v1:tracks-lyrics', kwargs={'pk': self.pk}) + return reverse("api:v1:tracks-lyrics", kwargs={"pk": self.pk}) @property def full_name(self): try: - return '{} - {} - {}'.format( - self.artist.name, - self.album.title, - self.title, - ) + return "{} - {} - {}".format(self.artist.name, self.album.title, self.title) except AttributeError: - return '{} - {}'.format( - self.artist.name, - self.title, - ) + return "{} - {}".format(self.artist.name, self.title) def get_activity_url(self): if self.mbid: - return 'https://musicbrainz.org/recording/{}'.format( - self.mbid) - return settings.FUNKWHALE_URL + '/tracks/{}'.format(self.pk) + return "https://musicbrainz.org/recording/{}".format(self.mbid) + return settings.FUNKWHALE_URL + "/tracks/{}".format(self.pk) @classmethod def get_or_create_from_title(cls, title, **kwargs): - kwargs.update({'title': title}) - return cls.objects.get_or_create( - title__iexact=title, - defaults=kwargs) + kwargs.update({"title": title}) + return cls.objects.get_or_create(title__iexact=title, defaults=kwargs) @classmethod def get_or_create_from_release(cls, release_mbid, mbid): @@ -448,35 +390,32 @@ class Track(APIModelMixin): album = Album.get_or_create_from_api(release_mbid)[0] data = musicbrainz.client.api.releases.get( - str(album.mbid), includes=Album.api_includes) - tracks = [ - t - for m in data['release']['medium-list'] - for t in m['track-list'] - ] + str(album.mbid), includes=Album.api_includes + ) + tracks = [t for m in data["release"]["medium-list"] for t in m["track-list"]] track_data = None for track in tracks: - if track['recording']['id'] == mbid: + if track["recording"]["id"] == mbid: track_data = track break if not track_data: - raise ValueError('No track found matching this ID') + raise ValueError("No track found matching this ID") return cls.objects.update_or_create( mbid=mbid, defaults={ - 'position': int(track['position']), - 'title': track['recording']['title'], - 'album': album, - 'artist': album.artist, - } + "position": int(track["position"]), + "title": track["recording"]["title"], + "album": album, + "artist": album.artist, + }, ) + + class TrackFile(models.Model): - uuid = models.UUIDField( - unique=True, db_index=True, default=uuid.uuid4) - track = models.ForeignKey( - Track, related_name='files', on_delete=models.CASCADE) - audio_file = models.FileField(upload_to='tracks/%Y/%m/%d', max_length=255) + uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4) + track = models.ForeignKey(Track, related_name="files", on_delete=models.CASCADE) + audio_file = models.FileField(upload_to="tracks/%Y/%m/%d", max_length=255) source = models.URLField(null=True, blank=True, max_length=500) creation_date = models.DateTimeField(default=timezone.now) modification_date = models.DateTimeField(auto_now=True) @@ -488,8 +427,8 @@ class TrackFile(models.Model): mimetype = models.CharField(null=True, blank=True, max_length=200) library_track = models.OneToOneField( - 'federation.LibraryTrack', - related_name='local_track_file', + "federation.LibraryTrack", + related_name="local_track_file", on_delete=models.CASCADE, null=True, blank=True, @@ -499,45 +438,38 @@ class TrackFile(models.Model): # import the track file, since there is not any # we create a tmp dir for the download tmp_dir = tempfile.mkdtemp() - data = downloader.download( - self.source, - target_directory=tmp_dir) - self.duration = data.get('duration', None) + data = downloader.download(self.source, target_directory=tmp_dir) + self.duration = data.get("duration", None) self.audio_file.save( - os.path.basename(data['audio_file_path']), - File(open(data['audio_file_path'], 'rb')) + os.path.basename(data["audio_file_path"]), + File(open(data["audio_file_path"], "rb")), ) shutil.rmtree(tmp_dir) return self.audio_file def get_federation_url(self): - return federation_utils.full_url( - '/federation/music/file/{}'.format(self.uuid) - ) + return federation_utils.full_url("/federation/music/file/{}".format(self.uuid)) @property def path(self): - return reverse( - 'api:v1:trackfiles-serve', kwargs={'pk': self.pk}) + return reverse("api:v1:trackfiles-serve", kwargs={"pk": self.pk}) @property def filename(self): - return '{}.{}'.format( - self.track.full_name, - self.extension) + return "{}.{}".format(self.track.full_name, self.extension) @property def extension(self): if not self.audio_file: return - return os.path.splitext(self.audio_file.name)[-1].replace('.', '', 1) + return os.path.splitext(self.audio_file.name)[-1].replace(".", "", 1) def get_file_size(self): if self.audio_file: return self.audio_file.size - if self.source.startswith('file://'): - return os.path.getsize(self.source.replace('file://', '', 1)) + if self.source.startswith("file://"): + return os.path.getsize(self.source.replace("file://", "", 1)) if self.library_track and self.library_track.audio_file: return self.library_track.audio_file.size @@ -545,8 +477,8 @@ class TrackFile(models.Model): def get_audio_file(self): if self.audio_file: return self.audio_file.open() - if self.source.startswith('file://'): - return open(self.source.replace('file://', '', 1), 'rb') + if self.source.startswith("file://"): + return open(self.source.replace("file://", "", 1), "rb") if self.library_track and self.library_track.audio_file: return self.library_track.audio_file.open() @@ -557,15 +489,15 @@ class TrackFile(models.Model): audio_data = utils.get_audio_file_data(f) if not audio_data: return - self.duration = int(audio_data['length']) - self.bitrate = audio_data['bitrate'] + self.duration = int(audio_data["length"]) + self.bitrate = audio_data["bitrate"] self.size = self.get_file_size() else: lt = self.library_track if lt: - self.duration = lt.get_metadata('length') - self.size = lt.get_metadata('size') - self.bitrate = lt.get_metadata('bitrate') + self.duration = lt.get_metadata("length") + self.size = lt.get_metadata("size") + self.bitrate = lt.get_metadata("bitrate") def save(self, **kwargs): if not self.mimetype and self.audio_file: @@ -580,41 +512,44 @@ class TrackFile(models.Model): IMPORT_STATUS_CHOICES = ( - ('pending', 'Pending'), - ('finished', 'Finished'), - ('errored', 'Errored'), - ('skipped', 'Skipped'), + ("pending", "Pending"), + ("finished", "Finished"), + ("errored", "Errored"), + ("skipped", "Skipped"), ) class ImportBatch(models.Model): - uuid = models.UUIDField( - unique=True, db_index=True, default=uuid.uuid4) + uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4) IMPORT_BATCH_SOURCES = [ - ('api', 'api'), - ('shell', 'shell'), - ('federation', 'federation'), + ("api", "api"), + ("shell", "shell"), + ("federation", "federation"), ] source = models.CharField( - max_length=30, default='api', choices=IMPORT_BATCH_SOURCES) + max_length=30, default="api", choices=IMPORT_BATCH_SOURCES + ) creation_date = models.DateTimeField(default=timezone.now) submitted_by = models.ForeignKey( - 'users.User', - related_name='imports', + "users.User", + related_name="imports", null=True, blank=True, - on_delete=models.CASCADE) + on_delete=models.CASCADE, + ) status = models.CharField( - choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30) + choices=IMPORT_STATUS_CHOICES, default="pending", max_length=30 + ) import_request = models.ForeignKey( - 'requests.ImportRequest', - related_name='import_batches', + "requests.ImportRequest", + related_name="import_batches", null=True, blank=True, - on_delete=models.CASCADE) + on_delete=models.CASCADE, + ) class Meta: - ordering = ['-creation_date'] + ordering = ["-creation_date"] def __str__(self): return str(self.pk) @@ -624,46 +559,46 @@ class ImportBatch(models.Model): self.status = utils.compute_status(self.jobs.all()) if self.status == old_status: return - self.save(update_fields=['status']) - if self.status != old_status and self.status == 'finished': + self.save(update_fields=["status"]) + if self.status != old_status and self.status == "finished": from . import tasks + tasks.import_batch_notify_followers.delay(import_batch_id=self.pk) def get_federation_url(self): return federation_utils.full_url( - '/federation/music/import/batch/{}'.format(self.uuid) + "/federation/music/import/batch/{}".format(self.uuid) ) class ImportJob(models.Model): - uuid = models.UUIDField( - unique=True, db_index=True, default=uuid.uuid4) + uuid = models.UUIDField(unique=True, db_index=True, default=uuid.uuid4) batch = models.ForeignKey( - ImportBatch, related_name='jobs', on_delete=models.CASCADE) + ImportBatch, related_name="jobs", on_delete=models.CASCADE + ) track_file = models.ForeignKey( - TrackFile, - related_name='jobs', - null=True, - blank=True, - on_delete=models.CASCADE) + TrackFile, related_name="jobs", null=True, blank=True, on_delete=models.CASCADE + ) source = models.CharField(max_length=500) mbid = models.UUIDField(editable=False, null=True, blank=True) status = models.CharField( - choices=IMPORT_STATUS_CHOICES, default='pending', max_length=30) + choices=IMPORT_STATUS_CHOICES, default="pending", max_length=30 + ) audio_file = models.FileField( - upload_to='imports/%Y/%m/%d', max_length=255, null=True, blank=True) + upload_to="imports/%Y/%m/%d", max_length=255, null=True, blank=True + ) library_track = models.ForeignKey( - 'federation.LibraryTrack', - related_name='import_jobs', + "federation.LibraryTrack", + related_name="import_jobs", on_delete=models.SET_NULL, null=True, - blank=True + blank=True, ) class Meta: - ordering = ('id', ) + ordering = ("id",) @receiver(post_save, sender=ImportJob) @@ -673,22 +608,22 @@ def update_batch_status(sender, instance, **kwargs): @receiver(post_save, sender=ImportBatch) def update_request_status(sender, instance, created, **kwargs): - update_fields = kwargs.get('update_fields', []) or [] + update_fields = kwargs.get("update_fields", []) or [] if not instance.import_request: return - if not created and not 'status' in update_fields: + if not created and not "status" in update_fields: return r_status = instance.import_request.status status = instance.status - if status == 'pending' and r_status == 'pending': + if status == "pending" and r_status == "pending": # let's mark the request as accepted since we started an import - instance.import_request.status = 'accepted' - return instance.import_request.save(update_fields=['status']) + instance.import_request.status = "accepted" + return instance.import_request.save(update_fields=["status"]) - if status == 'finished' and r_status == 'accepted': + if status == "finished" and r_status == "accepted": # let's mark the request as imported since the import is over - instance.import_request.status = 'imported' - return instance.import_request.save(update_fields=['status']) + instance.import_request.status = "imported" + return instance.import_request.save(update_fields=["status"]) diff --git a/api/funkwhale_api/music/permissions.py b/api/funkwhale_api/music/permissions.py index d31e1c5d5..a85e5a9a6 100644 --- a/api/funkwhale_api/music/permissions.py +++ b/api/funkwhale_api/music/permissions.py @@ -8,22 +8,19 @@ from funkwhale_api.federation import models class Listen(BasePermission): - def has_permission(self, request, view): - if not preferences.get('common__api_authentication_required'): + if not preferences.get("common__api_authentication_required"): return True - user = getattr(request, 'user', None) + user = getattr(request, "user", None) if user and user.is_authenticated: return True - actor = getattr(request, 'actor', None) + actor = getattr(request, "actor", None) if actor is None: return False - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + library = actors.SYSTEM_ACTORS["library"].get_actor_instance() return models.Follow.objects.filter( - target=library, - actor=actor, - approved=True + target=library, actor=actor, approved=True ).exists() diff --git a/api/funkwhale_api/music/serializers.py b/api/funkwhale_api/music/serializers.py index b72bb8c4a..7aa8603e2 100644 --- a/api/funkwhale_api/music/serializers.py +++ b/api/funkwhale_api/music/serializers.py @@ -19,14 +19,14 @@ class ArtistAlbumSerializer(serializers.ModelSerializer): class Meta: model = models.Album fields = ( - 'id', - 'mbid', - 'title', - 'artist', - 'release_date', - 'cover', - 'creation_date', - 'tracks_count', + "id", + "mbid", + "title", + "artist", + "release_date", + "cover", + "creation_date", + "tracks_count", ) def get_tracks_count(self, o): @@ -38,13 +38,7 @@ class ArtistWithAlbumsSerializer(serializers.ModelSerializer): class Meta: model = models.Artist - fields = ( - 'id', - 'mbid', - 'name', - 'creation_date', - 'albums', - ) + fields = ("id", "mbid", "name", "creation_date", "albums") class TrackFileSerializer(serializers.ModelSerializer): @@ -53,23 +47,18 @@ class TrackFileSerializer(serializers.ModelSerializer): class Meta: model = models.TrackFile fields = ( - 'id', - 'path', - 'source', - 'filename', - 'mimetype', - 'track', - 'duration', - 'mimetype', - 'bitrate', - 'size', + "id", + "path", + "source", + "filename", + "mimetype", + "track", + "duration", + "mimetype", + "bitrate", + "size", ) - read_only_fields = [ - 'duration', - 'mimetype', - 'bitrate', - 'size', - ] + read_only_fields = ["duration", "mimetype", "bitrate", "size"] def get_path(self, o): url = o.path @@ -82,26 +71,21 @@ class AlbumTrackSerializer(serializers.ModelSerializer): class Meta: model = models.Track fields = ( - 'id', - 'mbid', - 'title', - 'album', - 'artist', - 'creation_date', - 'files', - 'position', + "id", + "mbid", + "title", + "album", + "artist", + "creation_date", + "files", + "position", ) class ArtistSimpleSerializer(serializers.ModelSerializer): class Meta: model = models.Artist - fields = ( - 'id', - 'mbid', - 'name', - 'creation_date', - ) + fields = ("id", "mbid", "name", "creation_date") class AlbumSerializer(serializers.ModelSerializer): @@ -111,20 +95,20 @@ class AlbumSerializer(serializers.ModelSerializer): class Meta: model = models.Album fields = ( - 'id', - 'mbid', - 'title', - 'artist', - 'tracks', - 'release_date', - 'cover', - 'creation_date', + "id", + "mbid", + "title", + "artist", + "tracks", + "release_date", + "cover", + "creation_date", ) def get_tracks(self, o): ordered_tracks = sorted( o.tracks.all(), - key=lambda v: (v.position, v.title) if v.position else (99999, v.title) + key=lambda v: (v.position, v.title) if v.position else (99999, v.title), ) return AlbumTrackSerializer(ordered_tracks, many=True).data @@ -135,13 +119,13 @@ class TrackAlbumSerializer(serializers.ModelSerializer): class Meta: model = models.Album fields = ( - 'id', - 'mbid', - 'title', - 'artist', - 'release_date', - 'cover', - 'creation_date', + "id", + "mbid", + "title", + "artist", + "release_date", + "cover", + "creation_date", ) @@ -154,15 +138,15 @@ class TrackSerializer(serializers.ModelSerializer): class Meta: model = models.Track fields = ( - 'id', - 'mbid', - 'title', - 'album', - 'artist', - 'creation_date', - 'files', - 'position', - 'lyrics', + "id", + "mbid", + "title", + "album", + "artist", + "creation_date", + "files", + "position", + "lyrics", ) def get_lyrics(self, obj): @@ -172,20 +156,19 @@ class TrackSerializer(serializers.ModelSerializer): class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag - fields = ('id', 'name', 'slug') + fields = ("id", "name", "slug") class SimpleAlbumSerializer(serializers.ModelSerializer): - class Meta: model = models.Album - fields = ('id', 'mbid', 'title', 'release_date', 'cover') + fields = ("id", "mbid", "title", "release_date", "cover") class LyricsSerializer(serializers.ModelSerializer): class Meta: model = models.Lyrics - fields = ('id', 'work', 'content', 'content_rendered') + fields = ("id", "work", "content", "content_rendered") class ImportJobSerializer(serializers.ModelSerializer): @@ -193,15 +176,8 @@ class ImportJobSerializer(serializers.ModelSerializer): class Meta: model = models.ImportJob - fields = ( - 'id', - 'mbid', - 'batch', - 'source', - 'status', - 'track_file', - 'audio_file') - read_only_fields = ('status', 'track_file') + fields = ("id", "mbid", "batch", "source", "status", "track_file", "audio_file") + read_only_fields = ("status", "track_file") class ImportBatchSerializer(serializers.ModelSerializer): @@ -210,19 +186,19 @@ class ImportBatchSerializer(serializers.ModelSerializer): class Meta: model = models.ImportBatch fields = ( - 'id', - 'submitted_by', - 'source', - 'status', - 'creation_date', - 'import_request') - read_only_fields = ( - 'creation_date', 'submitted_by', 'source') + "id", + "submitted_by", + "source", + "status", + "creation_date", + "import_request", + ) + read_only_fields = ("creation_date", "submitted_by", "source") def to_representation(self, instance): repr = super().to_representation(instance) try: - repr['job_count'] = instance.job_count + repr["job_count"] = instance.job_count except AttributeError: # Queryset was not annotated pass @@ -231,50 +207,43 @@ class ImportBatchSerializer(serializers.ModelSerializer): class TrackActivitySerializer(activity_serializers.ModelSerializer): type = serializers.SerializerMethodField() - name = serializers.CharField(source='title') - artist = serializers.CharField(source='artist.name') - album = serializers.CharField(source='album.title') + name = serializers.CharField(source="title") + artist = serializers.CharField(source="artist.name") + album = serializers.CharField(source="album.title") class Meta: model = models.Track - fields = [ - 'id', - 'local_id', - 'name', - 'type', - 'artist', - 'album', - ] + fields = ["id", "local_id", "name", "type", "artist", "album"] def get_type(self, obj): - return 'Audio' + return "Audio" class ImportJobRunSerializer(serializers.Serializer): jobs = serializers.PrimaryKeyRelatedField( many=True, - queryset=models.ImportJob.objects.filter( - status__in=['pending', 'errored'] - ) + queryset=models.ImportJob.objects.filter(status__in=["pending", "errored"]), ) batches = serializers.PrimaryKeyRelatedField( - many=True, - queryset=models.ImportBatch.objects.all() + many=True, queryset=models.ImportBatch.objects.all() ) def validate(self, validated_data): - jobs = validated_data['jobs'] - batches_ids = [b.pk for b in validated_data['batches']] + jobs = validated_data["jobs"] + batches_ids = [b.pk for b in validated_data["batches"]] query = Q(batch__pk__in=batches_ids) query |= Q(pk__in=[j.id for j in jobs]) - queryset = models.ImportJob.objects.filter(query).filter( - status__in=['pending', 'errored']).distinct() - validated_data['_jobs'] = queryset + queryset = ( + models.ImportJob.objects.filter(query) + .filter(status__in=["pending", "errored"]) + .distinct() + ) + validated_data["_jobs"] = queryset return validated_data def create(self, validated_data): - ids = validated_data['_jobs'].values_list('id', flat=True) - validated_data['_jobs'].update(status='pending') + ids = validated_data["_jobs"].values_list("id", flat=True) + validated_data["_jobs"].update(status="pending") for id in ids: tasks.import_job_run.delay(import_job_id=id) - return {'jobs': list(ids)} + return {"jobs": list(ids)} diff --git a/api/funkwhale_api/music/tasks.py b/api/funkwhale_api/music/tasks.py index 7b1b48981..9dcaceec4 100644 --- a/api/funkwhale_api/music/tasks.py +++ b/api/funkwhale_api/music/tasks.py @@ -22,24 +22,25 @@ from . import utils as music_utils logger = logging.getLogger(__name__) -@celery.app.task(name='acoustid.set_on_track_file') -@celery.require_instance(models.TrackFile, 'track_file') +@celery.app.task(name="acoustid.set_on_track_file") +@celery.require_instance(models.TrackFile, "track_file") def set_acoustid_on_track_file(track_file): client = get_acoustid_client() result = client.get_best_match(track_file.audio_file.path) def update(id): track_file.acoustid_track_id = id - track_file.save(update_fields=['acoustid_track_id']) + track_file.save(update_fields=["acoustid_track_id"]) return id + if result: - return update(result['id']) + return update(result["id"]) def import_track_from_remote(library_track): metadata = library_track.metadata try: - track_mbid = metadata['recording']['musicbrainz_id'] + track_mbid = metadata["recording"]["musicbrainz_id"] assert track_mbid # for null/empty values except (KeyError, AssertionError): pass @@ -47,39 +48,43 @@ def import_track_from_remote(library_track): return models.Track.get_or_create_from_api(mbid=track_mbid)[0] try: - album_mbid = metadata['release']['musicbrainz_id'] + album_mbid = metadata["release"]["musicbrainz_id"] assert album_mbid # for null/empty values except (KeyError, AssertionError): pass else: album, _ = models.Album.get_or_create_from_api(mbid=album_mbid) return models.Track.get_or_create_from_title( - library_track.title, artist=album.artist, album=album)[0] + library_track.title, artist=album.artist, album=album + )[0] try: - artist_mbid = metadata['artist']['musicbrainz_id'] + artist_mbid = metadata["artist"]["musicbrainz_id"] assert artist_mbid # for null/empty values except (KeyError, AssertionError): pass else: artist, _ = models.Artist.get_or_create_from_api(mbid=artist_mbid) album, _ = models.Album.get_or_create_from_title( - library_track.album_title, artist=artist) + library_track.album_title, artist=artist + ) return models.Track.get_or_create_from_title( - library_track.title, artist=artist, album=album)[0] + library_track.title, artist=artist, album=album + )[0] # worst case scenario, we have absolutely no way to link to a # musicbrainz resource, we rely on the name/titles - artist, _ = models.Artist.get_or_create_from_name( - library_track.artist_name) + artist, _ = models.Artist.get_or_create_from_name(library_track.artist_name) album, _ = models.Album.get_or_create_from_title( - library_track.album_title, artist=artist) + library_track.album_title, artist=artist + ) return models.Track.get_or_create_from_title( - library_track.title, artist=artist, album=album)[0] + library_track.title, artist=artist, album=album + )[0] def _do_import(import_job, replace=False, use_acoustid=False): - logger.info('[Import Job %s] starting job', import_job.pk) + logger.info("[Import Job %s] starting job", import_job.pk) from_file = bool(import_job.audio_file) mbid = import_job.mbid acoustid_track_id = None @@ -93,58 +98,60 @@ def _do_import(import_job, replace=False, use_acoustid=False): client = get_acoustid_client() match = client.get_best_match(import_job.audio_file.path) if match: - duration = match['recordings'][0]['duration'] - mbid = match['recordings'][0]['id'] - acoustid_track_id = match['id'] + duration = match["recordings"][0]["duration"] + mbid = match["recordings"][0]["id"] + acoustid_track_id = match["id"] if mbid: logger.info( - '[Import Job %s] importing track from musicbrainz recording %s', + "[Import Job %s] importing track from musicbrainz recording %s", import_job.pk, - str(mbid)) + str(mbid), + ) track, _ = models.Track.get_or_create_from_api(mbid=mbid) elif import_job.audio_file: logger.info( - '[Import Job %s] importing track from uploaded track data at %s', + "[Import Job %s] importing track from uploaded track data at %s", import_job.pk, - import_job.audio_file.path) - track = audiofile_tasks.import_track_data_from_path( - import_job.audio_file.path) + import_job.audio_file.path, + ) + track = audiofile_tasks.import_track_data_from_path(import_job.audio_file.path) elif import_job.library_track: logger.info( - '[Import Job %s] importing track from federated library track %s', + "[Import Job %s] importing track from federated library track %s", import_job.pk, - import_job.library_track.pk) + import_job.library_track.pk, + ) track = import_track_from_remote(import_job.library_track) - elif import_job.source.startswith('file://'): - tf_path = import_job.source.replace('file://', '', 1) + elif import_job.source.startswith("file://"): + tf_path = import_job.source.replace("file://", "", 1) logger.info( - '[Import Job %s] importing track from local track data at %s', + "[Import Job %s] importing track from local track data at %s", import_job.pk, - tf_path) - track = audiofile_tasks.import_track_data_from_path( - tf_path) + tf_path, + ) + track = audiofile_tasks.import_track_data_from_path(tf_path) else: raise ValueError( - 'Not enough data to process import, ' - 'add a mbid, an audio file or a library track') + "Not enough data to process import, " + "add a mbid, an audio file or a library track" + ) track_file = None if replace: - logger.info( - '[Import Job %s] replacing existing audio file', import_job.pk) + logger.info("[Import Job %s] replacing existing audio file", import_job.pk) track_file = track.files.first() elif track.files.count() > 0: logger.info( - '[Import Job %s] skipping, we already have a file for this track', - import_job.pk) + "[Import Job %s] skipping, we already have a file for this track", + import_job.pk, + ) if import_job.audio_file: import_job.audio_file.delete() - import_job.status = 'skipped' + import_job.status = "skipped" import_job.save() return - track_file = track_file or models.TrackFile( - track=track, source=import_job.source) + track_file = track_file or models.TrackFile(track=track, source=import_job.source) track_file.acoustid_track_id = acoustid_track_id if from_file: track_file.audio_file = ContentFile(import_job.audio_file.read()) @@ -158,13 +165,11 @@ def _do_import(import_job, replace=False, use_acoustid=False): else: # no downloading, we hotlink pass - elif not import_job.audio_file and not import_job.source.startswith('file://'): + elif not import_job.audio_file and not import_job.source.startswith("file://"): # not an implace import, and we have a source, so let's download it - logger.info( - '[Import Job %s] downloading audio file from remote', - import_job.pk) + logger.info("[Import Job %s] downloading audio file from remote", import_job.pk) track_file.download_file() - elif not import_job.audio_file and import_job.source.startswith('file://'): + elif not import_job.audio_file and import_job.source.startswith("file://"): # in place import, we set mimetype from extension path, ext = os.path.splitext(import_job.source) track_file.mimetype = music_utils.get_type_from_ext(ext) @@ -172,19 +177,15 @@ def _do_import(import_job, replace=False, use_acoustid=False): track_file.save() # if no cover is set on track album, we try to update it as well: if not track.album.cover: - logger.info( - '[Import Job %s] retrieving album cover', - import_job.pk) + logger.info("[Import Job %s] retrieving album cover", import_job.pk) update_album_cover(track.album, track_file) - import_job.status = 'finished' + import_job.status = "finished" import_job.track_file = track_file if import_job.audio_file: # it's imported on the track, we don't need it anymore import_job.audio_file.delete() import_job.save() - logger.info( - '[Import Job %s] job finished', - import_job.pk) + logger.info("[Import Job %s] job finished", import_job.pk) return track_file @@ -199,20 +200,15 @@ def update_album_cover(album, track_file, replace=False): except FileNotFoundError: metadata = None if metadata: - cover = metadata.get_picture('cover_front') + cover = metadata.get_picture("cover_front") if cover: # best case scenario, cover is embedded in the track - logger.info( - '[Album %s] Using cover embedded in file', - album.pk) + logger.info("[Album %s] Using cover embedded in file", album.pk) return album.get_image(data=cover) - if track_file.source and track_file.source.startswith('file://'): + if track_file.source and track_file.source.startswith("file://"): # let's look for a cover in the same directory - path = os.path.dirname(track_file.source.replace('file://', '', 1)) - logger.info( - '[Album %s] scanning covers from %s', - album.pk, - path) + path = os.path.dirname(track_file.source.replace("file://", "", 1)) + logger.info("[Album %s] scanning covers from %s", album.pk, path) cover = get_cover_from_fs(path) if cover: return album.get_image(data=cover) @@ -220,50 +216,41 @@ def update_album_cover(album, track_file, replace=False): return try: logger.info( - '[Album %s] Fetching cover from musicbrainz release %s', + "[Album %s] Fetching cover from musicbrainz release %s", album.pk, - str(album.mbid)) + str(album.mbid), + ) return album.get_image() except ResponseError as exc: logger.warning( - '[Album %s] cannot fetch cover from musicbrainz: %s', - album.pk, - str(exc)) + "[Album %s] cannot fetch cover from musicbrainz: %s", album.pk, str(exc) + ) -IMAGE_TYPES = [ - ('jpg', 'image/jpeg'), - ('png', 'image/png'), -] +IMAGE_TYPES = [("jpg", "image/jpeg"), ("png", "image/png")] + def get_cover_from_fs(dir_path): if os.path.exists(dir_path): for e, m in IMAGE_TYPES: - cover_path = os.path.join(dir_path, 'cover.{}'.format(e)) + cover_path = os.path.join(dir_path, "cover.{}".format(e)) if not os.path.exists(cover_path): - logger.debug('Cover %s does not exists', cover_path) + logger.debug("Cover %s does not exists", cover_path) continue - with open(cover_path, 'rb') as c: - logger.info('Found cover at %s', cover_path) - return { - 'mimetype': m, - 'content': c.read(), - } + with open(cover_path, "rb") as c: + logger.info("Found cover at %s", cover_path) + return {"mimetype": m, "content": c.read()} - -@celery.app.task(name='ImportJob.run', bind=True) +@celery.app.task(name="ImportJob.run", bind=True) @celery.require_instance( - models.ImportJob.objects.filter( - status__in=['pending', 'errored']), - 'import_job') + models.ImportJob.objects.filter(status__in=["pending", "errored"]), "import_job" +) def import_job_run(self, import_job, replace=False, use_acoustid=False): def mark_errored(exc): - logger.error( - '[Import Job %s] Error during import: %s', - import_job.pk, str(exc)) - import_job.status = 'errored' - import_job.save(update_fields=['status']) + logger.error("[Import Job %s] Error during import: %s", import_job.pk, str(exc)) + import_job.status = "errored" + import_job.save(update_fields=["status"]) try: tf = _do_import(import_job, replace, use_acoustid=use_acoustid) @@ -279,58 +266,56 @@ def import_job_run(self, import_job, replace=False, use_acoustid=False): raise -@celery.app.task(name='ImportBatch.run') -@celery.require_instance(models.ImportBatch, 'import_batch') +@celery.app.task(name="ImportBatch.run") +@celery.require_instance(models.ImportBatch, "import_batch") def import_batch_run(import_batch): - for job_id in import_batch.jobs.order_by('id').values_list('id', flat=True): + for job_id in import_batch.jobs.order_by("id").values_list("id", flat=True): import_job_run.delay(import_job_id=job_id) -@celery.app.task(name='Lyrics.fetch_content') -@celery.require_instance(models.Lyrics, 'lyrics') +@celery.app.task(name="Lyrics.fetch_content") +@celery.require_instance(models.Lyrics, "lyrics") def fetch_content(lyrics): html = lyrics_utils._get_html(lyrics.url) content = lyrics_utils.extract_content(html) cleaned_content = lyrics_utils.clean_content(content) lyrics.content = cleaned_content - lyrics.save(update_fields=['content']) + lyrics.save(update_fields=["content"]) -@celery.app.task(name='music.import_batch_notify_followers') +@celery.app.task(name="music.import_batch_notify_followers") @celery.require_instance( - models.ImportBatch.objects.filter(status='finished'), 'import_batch') + models.ImportBatch.objects.filter(status="finished"), "import_batch" +) def import_batch_notify_followers(import_batch): - if not preferences.get('federation__enabled'): + if not preferences.get("federation__enabled"): return - if import_batch.source == 'federation': + if import_batch.source == "federation": return - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() followers = library_actor.get_approved_followers() jobs = import_batch.jobs.filter( - status='finished', - library_track__isnull=True, - track_file__isnull=False, - ).select_related( - 'track_file__track__artist', - 'track_file__track__album__artist', - ) + status="finished", library_track__isnull=True, track_file__isnull=False + ).select_related("track_file__track__artist", "track_file__track__album__artist") track_files = [job.track_file for job in jobs] - collection = federation_serializers.CollectionSerializer({ - 'actor': library_actor, - 'id': import_batch.get_federation_url(), - 'items': track_files, - 'item_serializer': federation_serializers.AudioSerializer - }).data + collection = federation_serializers.CollectionSerializer( + { + "actor": library_actor, + "id": import_batch.get_federation_url(), + "items": track_files, + "item_serializer": federation_serializers.AudioSerializer, + } + ).data for f in followers: create = federation_serializers.ActivitySerializer( { - 'type': 'Create', - 'id': collection['id'], - 'object': collection, - 'actor': library_actor.url, - 'to': [f.url], + "type": "Create", + "id": collection["id"], + "object": collection, + "actor": library_actor.url, + "to": [f.url], } ).data diff --git a/api/funkwhale_api/music/utils.py b/api/funkwhale_api/music/utils.py index 3b9fbb214..9b300ac2a 100644 --- a/api/funkwhale_api/music/utils.py +++ b/api/funkwhale_api/music/utils.py @@ -6,29 +6,31 @@ import re from django.db.models import Q -def normalize_query(query_string, - findterms=re.compile(r'"([^"]+)"|(\S+)').findall, - normspace=re.compile(r'\s{2,}').sub): - ''' Splits the query string in invidual keywords, getting rid of unecessary spaces +def normalize_query( + query_string, + findterms=re.compile(r'"([^"]+)"|(\S+)').findall, + normspace=re.compile(r"\s{2,}").sub, +): + """ Splits the query string in invidual keywords, getting rid of unecessary spaces and grouping quoted words together. Example: >>> normalize_query(' some random words "with quotes " and spaces') ['some', 'random', 'words', 'with quotes', 'and', 'spaces'] - ''' - return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)] + """ + return [normspace(" ", (t[0] or t[1]).strip()) for t in findterms(query_string)] def get_query(query_string, search_fields): - ''' Returns a query, that is a combination of Q objects. That combination + """ Returns a query, that is a combination of Q objects. That combination aims to search keywords within a model by testing the given search fields. - ''' - query = None # Query to search for every search term + """ + query = None # Query to search for every search term terms = normalize_query(query_string) for term in terms: - or_query = None # Query to search for a given term in each field + or_query = None # Query to search for a given term in each field for field_name in search_fields: q = Q(**{"%s__icontains" % field_name: term}) if or_query is None: @@ -45,7 +47,7 @@ def get_query(query_string, search_fields): def guess_mimetype(f): b = min(1000000, f.size) t = magic.from_buffer(f.read(b), mime=True) - if not t.startswith('audio/'): + if not t.startswith("audio/"): # failure, we try guessing by extension mt, _ = mimetypes.guess_type(f.path) if mt: @@ -54,20 +56,20 @@ def guess_mimetype(f): def compute_status(jobs): - statuses = jobs.order_by().values_list('status', flat=True).distinct() - errored = any([status == 'errored' for status in statuses]) + statuses = jobs.order_by().values_list("status", flat=True).distinct() + errored = any([status == "errored" for status in statuses]) if errored: - return 'errored' - pending = any([status == 'pending' for status in statuses]) + return "errored" + pending = any([status == "pending" for status in statuses]) if pending: - return 'pending' - return 'finished' + return "pending" + return "finished" AUDIO_EXTENSIONS_AND_MIMETYPE = [ - ('ogg', 'audio/ogg'), - ('mp3', 'audio/mpeg'), - ('flac', 'audio/x-flac'), + ("ogg", "audio/ogg"), + ("mp3", "audio/mpeg"), + ("flac", "audio/x-flac"), ] EXTENSION_TO_MIMETYPE = {ext: mt for ext, mt in AUDIO_EXTENSIONS_AND_MIMETYPE} @@ -79,7 +81,7 @@ def get_ext_from_type(mimetype): def get_type_from_ext(extension): - if extension.startswith('.'): + if extension.startswith("."): # we remove leading dot extension = extension[1:] return EXTENSION_TO_MIMETYPE.get(extension) @@ -90,7 +92,7 @@ def get_audio_file_data(f): if not data: return d = {} - d['bitrate'] = data.info.bitrate - d['length'] = data.info.length + d["bitrate"] = data.info.bitrate + d["length"] = data.info.length return d diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index 2850c0770..15421fa38 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -46,10 +46,9 @@ logger = logging.getLogger(__name__) class TagViewSetMixin(object): - def get_queryset(self): queryset = super().get_queryset() - tag = self.request.query_params.get('tag') + tag = self.request.query_params.get("tag") if tag: queryset = queryset.filter(tags__pk=tag) return queryset @@ -60,38 +59,37 @@ class ArtistViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = serializers.ArtistWithAlbumsSerializer permission_classes = [ConditionalAuthentication] filter_class = filters.ArtistFilter - ordering_fields = ('id', 'name', 'creation_date') + ordering_fields = ("id", "name", "creation_date") class AlbumViewSet(viewsets.ReadOnlyModelViewSet): queryset = ( models.Album.objects.all() - .order_by('artist', 'release_date') - .select_related() - .prefetch_related( - 'tracks__artist', - 'tracks__files')) + .order_by("artist", "release_date") + .select_related() + .prefetch_related("tracks__artist", "tracks__files") + ) serializer_class = serializers.AlbumSerializer permission_classes = [ConditionalAuthentication] - ordering_fields = ('creation_date', 'release_date', 'title') + ordering_fields = ("creation_date", "release_date", "title") filter_class = filters.AlbumFilter class ImportBatchViewSet( - mixins.CreateModelMixin, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - viewsets.GenericViewSet): + mixins.CreateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet, +): queryset = ( - models.ImportBatch.objects - .select_related() - .order_by('-creation_date') - .annotate(job_count=Count('jobs')) + models.ImportBatch.objects.select_related() + .order_by("-creation_date") + .annotate(job_count=Count("jobs")) ) serializer_class = serializers.ImportBatchSerializer permission_classes = (HasUserPermission,) - required_permissions = ['library', 'upload'] - permission_operator = 'or' + required_permissions = ["library", "upload"] + permission_operator = "or" filter_class = filters.ImportBatchFilter def perform_create(self, serializer): @@ -101,51 +99,50 @@ class ImportBatchViewSet( qs = super().get_queryset() # if user do not have library permission, we limit to their # own jobs - if not self.request.user.has_permissions('library'): + if not self.request.user.has_permissions("library"): qs = qs.filter(submitted_by=self.request.user) return qs class ImportJobViewSet( - mixins.CreateModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): - queryset = (models.ImportJob.objects.all().select_related()) + mixins.CreateModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet +): + queryset = models.ImportJob.objects.all().select_related() serializer_class = serializers.ImportJobSerializer permission_classes = (HasUserPermission,) - required_permissions = ['library', 'upload'] - permission_operator = 'or' + required_permissions = ["library", "upload"] + permission_operator = "or" filter_class = filters.ImportJobFilter def get_queryset(self): qs = super().get_queryset() # if user do not have library permission, we limit to their # own jobs - if not self.request.user.has_permissions('library'): + if not self.request.user.has_permissions("library"): qs = qs.filter(batch__submitted_by=self.request.user) return qs - @list_route(methods=['get']) + @list_route(methods=["get"]) def stats(self, request, *args, **kwargs): - if not request.user.has_permissions('library'): + if not request.user.has_permissions("library"): return Response(status=403) qs = models.ImportJob.objects.all() filterset = filters.ImportJobFilter(request.GET, queryset=qs) qs = filterset.qs - qs = qs.values('status').order_by('status') - qs = qs.annotate(status_count=Count('status')) + qs = qs.values("status").order_by("status") + qs = qs.annotate(status_count=Count("status")) data = {} for row in qs: - data[row['status']] = row['status_count'] + data[row["status"]] = row["status_count"] for s, _ in models.IMPORT_STATUS_CHOICES: data.setdefault(s, 0) - data['count'] = sum([v for v in data.values()]) + data["count"] = sum([v for v in data.values()]) return Response(data) - @list_route(methods=['post']) + @list_route(methods=["post"]) def run(self, request, *args, **kwargs): serializer = serializers.ImportJobRunSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -154,11 +151,10 @@ class ImportJobViewSet( return Response(payload) def perform_create(self, serializer): - source = 'file://' + serializer.validated_data['audio_file'].name + source = "file://" + serializer.validated_data["audio_file"].name serializer.save(source=source) funkwhale_utils.on_commit( - tasks.import_job_run.delay, - import_job_id=serializer.instance.pk + tasks.import_job_run.delay, import_job_id=serializer.instance.pk ) @@ -166,33 +162,34 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet): """ A simple ViewSet for viewing and editing accounts. """ - queryset = (models.Track.objects.all().for_nested_serialization()) + + queryset = models.Track.objects.all().for_nested_serialization() serializer_class = serializers.TrackSerializer permission_classes = [ConditionalAuthentication] filter_class = filters.TrackFilter ordering_fields = ( - 'creation_date', - 'title', - 'album__title', - 'album__release_date', - 'position', - 'artist__name', + "creation_date", + "title", + "album__title", + "album__release_date", + "position", + "artist__name", ) def get_queryset(self): queryset = super().get_queryset() - filter_favorites = self.request.GET.get('favorites', None) + filter_favorites = self.request.GET.get("favorites", None) user = self.request.user - if user.is_authenticated and filter_favorites == 'true': + if user.is_authenticated and filter_favorites == "true": queryset = queryset.filter(track_favorites__user=user) return queryset - @detail_route(methods=['get']) + @detail_route(methods=["get"]) @transaction.non_atomic_requests def lyrics(self, request, *args, **kwargs): try: - track = models.Track.objects.get(pk=kwargs['pk']) + track = models.Track.objects.get(pk=kwargs["pk"]) except models.Track.DoesNotExist: return Response(status=404) @@ -201,7 +198,7 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet): work = track.get_work() if not work: - return Response({'error': 'unavailable work '}, status=404) + return Response({"error": "unavailable work "}, status=404) lyrics = work.fetch_lyrics() try: @@ -209,7 +206,7 @@ class TrackViewSet(TagViewSetMixin, viewsets.ReadOnlyModelViewSet): tasks.fetch_content(lyrics_id=lyrics.pk) lyrics.refresh_from_db() except AttributeError: - return Response({'error': 'unavailable lyrics'}, status=404) + return Response({"error": "unavailable lyrics"}, status=404) serializer = serializers.LyricsSerializer(lyrics) return Response(serializer.data) @@ -218,7 +215,7 @@ def get_file_path(audio_file): serve_path = settings.MUSIC_DIRECTORY_SERVE_PATH prefix = settings.MUSIC_DIRECTORY_PATH t = settings.REVERSE_PROXY_TYPE - if t == 'nginx': + if t == "nginx": # we have to use the internal locations try: path = audio_file.url @@ -226,30 +223,30 @@ def get_file_path(audio_file): # a path was given if not serve_path or not prefix: raise ValueError( - 'You need to specify MUSIC_DIRECTORY_SERVE_PATH and ' - 'MUSIC_DIRECTORY_PATH to serve in-place imported files' + "You need to specify MUSIC_DIRECTORY_SERVE_PATH and " + "MUSIC_DIRECTORY_PATH to serve in-place imported files" ) - path = '/music' + audio_file.replace(prefix, '', 1) - return (settings.PROTECT_FILES_PATH + path).encode('utf-8') - if t == 'apache2': + path = "/music" + audio_file.replace(prefix, "", 1) + return (settings.PROTECT_FILES_PATH + path).encode("utf-8") + if t == "apache2": try: path = audio_file.path except AttributeError: # a path was given if not serve_path or not prefix: raise ValueError( - 'You need to specify MUSIC_DIRECTORY_SERVE_PATH and ' - 'MUSIC_DIRECTORY_PATH to serve in-place imported files' + "You need to specify MUSIC_DIRECTORY_SERVE_PATH and " + "MUSIC_DIRECTORY_PATH to serve in-place imported files" ) path = audio_file.replace(prefix, serve_path, 1) - return path.encode('utf-8') + return path.encode("utf-8") def handle_serve(track_file): f = track_file # we update the accessed_date f.accessed_date = timezone.now() - f.save(update_fields=['accessed_date']) + f.save(update_fields=["accessed_date"]) mt = f.mimetype audio_file = f.audio_file @@ -270,28 +267,24 @@ def handle_serve(track_file): library_track.download_audio() track_file.library_track = library_track track_file.set_audio_data() - track_file.save(update_fields=['bitrate', 'duration', 'size']) + track_file.save(update_fields=["bitrate", "duration", "size"]) 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)) + elif f.source and f.source.startswith("file://"): + file_path = get_file_path(f.source.replace("file://", "", 1)) if mt: response = Response(content_type=mt) else: response = Response() filename = f.filename - mapping = { - 'nginx': 'X-Accel-Redirect', - 'apache2': 'X-Sendfile', - } + 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)) + filename = "filename*=UTF-8''{}".format(urllib.parse.quote(filename)) response["Content-Disposition"] = "attachment; {}".format(filename) if mt: response["Content-Type"] = mt @@ -302,30 +295,29 @@ def handle_serve(track_file): class TrackFileViewSet(viewsets.ReadOnlyModelViewSet): queryset = ( models.TrackFile.objects.all() - .select_related('track__artist', 'track__album') - .order_by('-id') + .select_related("track__artist", "track__album") + .order_by("-id") ) serializer_class = serializers.TrackFileSerializer - authentication_classes = rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES + [ - SignatureAuthentication - ] + authentication_classes = ( + rest_settings.api_settings.DEFAULT_AUTHENTICATION_CLASSES + + [SignatureAuthentication] + ) permission_classes = [music_permissions.Listen] - @detail_route(methods=['get']) + @detail_route(methods=["get"]) def serve(self, request, *args, **kwargs): queryset = models.TrackFile.objects.select_related( - 'library_track', - 'track__album__artist', - 'track__artist', + "library_track", "track__album__artist", "track__artist" ) try: - return handle_serve(queryset.get(pk=kwargs['pk'])) + return handle_serve(queryset.get(pk=kwargs["pk"])) except models.TrackFile.DoesNotExist: return Response(status=404) class TagViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Tag.objects.all().order_by('name') + queryset = Tag.objects.all().order_by("name") serializer_class = serializers.TagSerializer permission_classes = [ConditionalAuthentication] @@ -335,85 +327,91 @@ class Search(views.APIView): permission_classes = [ConditionalAuthentication] def get(self, request, *args, **kwargs): - query = request.GET['query'] + query = request.GET["query"] results = { # 'tags': serializers.TagSerializer(self.get_tags(query), many=True).data, - 'artists': serializers.ArtistWithAlbumsSerializer(self.get_artists(query), many=True).data, - 'tracks': serializers.TrackSerializer(self.get_tracks(query), many=True).data, - 'albums': serializers.AlbumSerializer(self.get_albums(query), many=True).data, + "artists": serializers.ArtistWithAlbumsSerializer( + self.get_artists(query), many=True + ).data, + "tracks": serializers.TrackSerializer( + self.get_tracks(query), many=True + ).data, + "albums": serializers.AlbumSerializer( + self.get_albums(query), many=True + ).data, } return Response(results, status=200) def get_tracks(self, query): search_fields = [ - 'mbid', - 'title__unaccent', - 'album__title__unaccent', - 'artist__name__unaccent'] + "mbid", + "title__unaccent", + "album__title__unaccent", + "artist__name__unaccent", + ] query_obj = utils.get_query(query, search_fields) return ( models.Track.objects.all() - .filter(query_obj) - .select_related('artist', 'album__artist') - .prefetch_related('files') - )[:self.max_results] + .filter(query_obj) + .select_related("artist", "album__artist") + .prefetch_related("files") + )[: self.max_results] def get_albums(self, query): - search_fields = [ - 'mbid', - 'title__unaccent', - 'artist__name__unaccent'] + search_fields = ["mbid", "title__unaccent", "artist__name__unaccent"] query_obj = utils.get_query(query, search_fields) return ( models.Album.objects.all() - .filter(query_obj) - .select_related() - .prefetch_related( - 'tracks__files', - ) - )[:self.max_results] + .filter(query_obj) + .select_related() + .prefetch_related("tracks__files") + )[: self.max_results] def get_artists(self, query): - search_fields = ['mbid', 'name__unaccent'] + search_fields = ["mbid", "name__unaccent"] query_obj = utils.get_query(query, search_fields) - return ( - models.Artist.objects.all() - .filter(query_obj) - .with_albums() - )[:self.max_results] + return (models.Artist.objects.all().filter(query_obj).with_albums())[ + : self.max_results + ] def get_tags(self, query): - search_fields = ['slug', 'name__unaccent'] + search_fields = ["slug", "name__unaccent"] query_obj = utils.get_query(query, search_fields) # We want the shortest tag first - qs = Tag.objects.all().annotate(slug_length=Length('slug')).order_by('slug_length') + qs = ( + Tag.objects.all() + .annotate(slug_length=Length("slug")) + .order_by("slug_length") + ) - return qs.filter(query_obj)[:self.max_results] + return qs.filter(query_obj)[: self.max_results] class SubmitViewSet(viewsets.ViewSet): queryset = models.ImportBatch.objects.none() permission_classes = (HasUserPermission,) - required_permissions = ['library'] + required_permissions = ["library"] - @list_route(methods=['post']) + @list_route(methods=["post"]) @transaction.non_atomic_requests def single(self, request, *args, **kwargs): try: - models.Track.objects.get(mbid=request.POST['mbid']) + models.Track.objects.get(mbid=request.POST["mbid"]) return Response({}) except models.Track.DoesNotExist: pass batch = models.ImportBatch.objects.create(submitted_by=request.user) - job = models.ImportJob.objects.create(mbid=request.POST['mbid'], batch=batch, source=request.POST['import_url']) + job = models.ImportJob.objects.create( + mbid=request.POST["mbid"], batch=batch, source=request.POST["import_url"] + ) tasks.import_job_run.delay(import_job_id=job.pk) serializer = serializers.ImportBatchSerializer(batch) return Response(serializer.data, status=201) def get_import_request(self, data): try: - raw = data['importRequest'] + raw = data["importRequest"] except KeyError: return @@ -423,57 +421,66 @@ class SubmitViewSet(viewsets.ViewSet): except ImportRequest.DoesNotExist: pass - @list_route(methods=['post']) + @list_route(methods=["post"]) @transaction.non_atomic_requests def album(self, request, *args, **kwargs): - data = json.loads(request.body.decode('utf-8')) + data = json.loads(request.body.decode("utf-8")) import_request = self.get_import_request(data) import_data, batch = self._import_album( - data, request, batch=None, import_request=import_request) + data, request, batch=None, import_request=import_request + ) return Response(import_data) @transaction.atomic def _import_album(self, data, request, batch=None, import_request=None): # we import the whole album here to prevent race conditions that occurs # when using get_or_create_from_api in tasks - album_data = api.releases.get(id=data['releaseId'], includes=models.Album.api_includes)['release'] + album_data = api.releases.get( + id=data["releaseId"], includes=models.Album.api_includes + )["release"] cleaned_data = models.Album.clean_musicbrainz_data(album_data) - album = importers.load(models.Album, cleaned_data, album_data, import_hooks=[models.import_tracks]) + album = importers.load( + models.Album, cleaned_data, album_data, import_hooks=[models.import_tracks] + ) try: album.get_image() except ResponseError: pass if not batch: batch = models.ImportBatch.objects.create( - submitted_by=request.user, - import_request=import_request) - for row in data['tracks']: + submitted_by=request.user, import_request=import_request + ) + for row in data["tracks"]: try: - models.TrackFile.objects.get(track__mbid=row['mbid']) + models.TrackFile.objects.get(track__mbid=row["mbid"]) except models.TrackFile.DoesNotExist: - job = models.ImportJob.objects.create(mbid=row['mbid'], batch=batch, source=row['source']) + job = models.ImportJob.objects.create( + mbid=row["mbid"], batch=batch, source=row["source"] + ) funkwhale_utils.on_commit( - tasks.import_job_run.delay, - import_job_id=job.pk + tasks.import_job_run.delay, import_job_id=job.pk ) serializer = serializers.ImportBatchSerializer(batch) return serializer.data, batch - @list_route(methods=['post']) + @list_route(methods=["post"]) @transaction.non_atomic_requests def artist(self, request, *args, **kwargs): - data = json.loads(request.body.decode('utf-8')) + data = json.loads(request.body.decode("utf-8")) import_request = self.get_import_request(data) - artist_data = api.artists.get(id=data['artistId'])['artist'] + artist_data = api.artists.get(id=data["artistId"])["artist"] cleaned_data = models.Artist.clean_musicbrainz_data(artist_data) - artist = importers.load(models.Artist, cleaned_data, artist_data, import_hooks=[]) + artist = importers.load( + models.Artist, cleaned_data, artist_data, import_hooks=[] + ) import_data = [] batch = None - for row in data['albums']: + for row in data["albums"]: row_data, batch = self._import_album( - row, request, batch=batch, import_request=import_request) + row, request, batch=batch, import_request=import_request + ) import_data.append(row_data) return Response(import_data[0]) diff --git a/api/funkwhale_api/musicbrainz/client.py b/api/funkwhale_api/musicbrainz/client.py index 8e7076a78..b3749a93b 100644 --- a/api/funkwhale_api/musicbrainz/client.py +++ b/api/funkwhale_api/musicbrainz/client.py @@ -5,17 +5,17 @@ from django.conf import settings from funkwhale_api import __version__ _api = musicbrainzngs -_api.set_useragent('funkwhale', str(__version__), settings.FUNKWHALE_URL) +_api.set_useragent("funkwhale", str(__version__), settings.FUNKWHALE_URL) -store = memoize.djangocache.Cache('default') -memo = memoize.Memoizer(store, namespace='memoize:musicbrainz') +store = memoize.djangocache.Cache("default") +memo = memoize.Memoizer(store, namespace="memoize:musicbrainz") def clean_artist_search(query, **kwargs): cleaned_kwargs = {} - if kwargs.get('name'): - cleaned_kwargs['artist'] = kwargs.get('name') + if kwargs.get("name"): + cleaned_kwargs["artist"] = kwargs.get("name") return _api.search_artists(query, **cleaned_kwargs) @@ -23,55 +23,43 @@ class API(object): _api = _api class artists(object): - search = memo( - clean_artist_search, max_age=settings.MUSICBRAINZ_CACHE_DURATION) - get = memo( - _api.get_artist_by_id, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) + search = memo(clean_artist_search, max_age=settings.MUSICBRAINZ_CACHE_DURATION) + get = memo(_api.get_artist_by_id, max_age=settings.MUSICBRAINZ_CACHE_DURATION) class images(object): get_front = memo( - _api.get_image_front, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) + _api.get_image_front, max_age=settings.MUSICBRAINZ_CACHE_DURATION + ) class recordings(object): search = memo( - _api.search_recordings, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) + _api.search_recordings, max_age=settings.MUSICBRAINZ_CACHE_DURATION + ) get = memo( - _api.get_recording_by_id, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) + _api.get_recording_by_id, max_age=settings.MUSICBRAINZ_CACHE_DURATION + ) class works(object): - search = memo( - _api.search_works, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) - get = memo( - _api.get_work_by_id, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) + search = memo(_api.search_works, max_age=settings.MUSICBRAINZ_CACHE_DURATION) + get = memo(_api.get_work_by_id, max_age=settings.MUSICBRAINZ_CACHE_DURATION) class releases(object): - search = memo( - _api.search_releases, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) - get = memo( - _api.get_release_by_id, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) - browse = memo( - _api.browse_releases, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) + search = memo(_api.search_releases, max_age=settings.MUSICBRAINZ_CACHE_DURATION) + get = memo(_api.get_release_by_id, max_age=settings.MUSICBRAINZ_CACHE_DURATION) + browse = memo(_api.browse_releases, max_age=settings.MUSICBRAINZ_CACHE_DURATION) # get_image_front = _api.get_image_front class release_groups(object): search = memo( - _api.search_release_groups, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) + _api.search_release_groups, max_age=settings.MUSICBRAINZ_CACHE_DURATION + ) get = memo( - _api.get_release_group_by_id, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) + _api.get_release_group_by_id, max_age=settings.MUSICBRAINZ_CACHE_DURATION + ) browse = memo( - _api.browse_release_groups, - max_age=settings.MUSICBRAINZ_CACHE_DURATION) + _api.browse_release_groups, max_age=settings.MUSICBRAINZ_CACHE_DURATION + ) # get_image_front = _api.get_image_front + api = API() diff --git a/api/funkwhale_api/musicbrainz/urls.py b/api/funkwhale_api/musicbrainz/urls.py index 7befe49ab..edb421202 100644 --- a/api/funkwhale_api/musicbrainz/urls.py +++ b/api/funkwhale_api/musicbrainz/urls.py @@ -4,20 +4,28 @@ from rest_framework import routers from . import views router = routers.SimpleRouter() -router.register(r'search', views.SearchViewSet, 'search') +router.register(r"search", views.SearchViewSet, "search") urlpatterns = [ - url('releases/(?P[0-9a-z-]+)/$', + url( + "releases/(?P[0-9a-z-]+)/$", views.ReleaseDetail.as_view(), - name='release-detail'), - url('artists/(?P[0-9a-z-]+)/$', + name="release-detail", + ), + url( + "artists/(?P[0-9a-z-]+)/$", views.ArtistDetail.as_view(), - name='artist-detail'), - url('release-groups/browse/(?P[0-9a-z-]+)/$', + name="artist-detail", + ), + url( + "release-groups/browse/(?P[0-9a-z-]+)/$", views.ReleaseGroupBrowse.as_view(), - name='release-group-browse'), - url('releases/browse/(?P[0-9a-z-]+)/$', + name="release-group-browse", + ), + url( + "releases/browse/(?P[0-9a-z-]+)/$", views.ReleaseBrowse.as_view(), - name='release-browse'), + name="release-browse", + ), # url('release-groups/(?P[0-9a-z-]+)/$', # views.ReleaseGroupDetail.as_view(), # name='release-group-detail'), diff --git a/api/funkwhale_api/musicbrainz/views.py b/api/funkwhale_api/musicbrainz/views.py index 5c101b161..64d4ee152 100644 --- a/api/funkwhale_api/musicbrainz/views.py +++ b/api/funkwhale_api/musicbrainz/views.py @@ -14,8 +14,7 @@ class ReleaseDetail(APIView): permission_classes = [ConditionalAuthentication] def get(self, request, *args, **kwargs): - result = api.releases.get( - id=kwargs['uuid'], includes=['artists', 'recordings']) + result = api.releases.get(id=kwargs["uuid"], includes=["artists", "recordings"]) return Response(result) @@ -23,9 +22,7 @@ class ArtistDetail(APIView): permission_classes = [ConditionalAuthentication] def get(self, request, *args, **kwargs): - result = api.artists.get( - id=kwargs['uuid'], - includes=['release-groups']) + result = api.artists.get(id=kwargs["uuid"], includes=["release-groups"]) # import json; print(json.dumps(result, indent=4)) return Response(result) @@ -34,8 +31,7 @@ class ReleaseGroupBrowse(APIView): permission_classes = [ConditionalAuthentication] def get(self, request, *args, **kwargs): - result = api.release_groups.browse( - artist=kwargs['artist_uuid']) + result = api.release_groups.browse(artist=kwargs["artist_uuid"]) return Response(result) @@ -44,29 +40,30 @@ class ReleaseBrowse(APIView): def get(self, request, *args, **kwargs): result = api.releases.browse( - release_group=kwargs['release_group_uuid'], - includes=['recordings', 'artist-credits']) + release_group=kwargs["release_group_uuid"], + includes=["recordings", "artist-credits"], + ) return Response(result) class SearchViewSet(viewsets.ViewSet): permission_classes = [ConditionalAuthentication] - @list_route(methods=['get']) + @list_route(methods=["get"]) def recordings(self, request, *args, **kwargs): - query = request.GET['query'] + query = request.GET["query"] results = api.recordings.search(query) return Response(results) - @list_route(methods=['get']) + @list_route(methods=["get"]) def releases(self, request, *args, **kwargs): - query = request.GET['query'] + query = request.GET["query"] results = api.releases.search(query) return Response(results) - @list_route(methods=['get']) + @list_route(methods=["get"]) def artists(self, request, *args, **kwargs): - query = request.GET['query'] + query = request.GET["query"] results = api.artists.search(query) # results = musicbrainzngs.search_artists(query) return Response(results) diff --git a/api/funkwhale_api/playlists/admin.py b/api/funkwhale_api/playlists/admin.py index 68e447f38..98ced232e 100644 --- a/api/funkwhale_api/playlists/admin.py +++ b/api/funkwhale_api/playlists/admin.py @@ -5,13 +5,13 @@ from . import models @admin.register(models.Playlist) class PlaylistAdmin(admin.ModelAdmin): - list_display = ['name', 'user', 'privacy_level', 'creation_date'] - search_fields = ['name', ] + list_display = ["name", "user", "privacy_level", "creation_date"] + search_fields = ["name"] list_select_related = True @admin.register(models.PlaylistTrack) class PlaylistTrackAdmin(admin.ModelAdmin): - list_display = ['playlist', 'track', 'index'] - search_fields = ['track__name', 'playlist__name'] + list_display = ["playlist", "track", "index"] + search_fields = ["track__name", "playlist__name"] list_select_related = True diff --git a/api/funkwhale_api/playlists/dynamic_preferences_registry.py b/api/funkwhale_api/playlists/dynamic_preferences_registry.py index b717177a2..5a2043452 100644 --- a/api/funkwhale_api/playlists/dynamic_preferences_registry.py +++ b/api/funkwhale_api/playlists/dynamic_preferences_registry.py @@ -3,16 +3,14 @@ from dynamic_preferences.registries import global_preferences_registry from funkwhale_api.common import preferences -playlists = types.Section('playlists') +playlists = types.Section("playlists") @global_preferences_registry.register class MaxTracks(preferences.DefaultFromSettingMixin, types.IntegerPreference): show_in_api = True section = playlists - name = 'max_tracks' - verbose_name = 'Max tracks per playlist' - setting = 'PLAYLISTS_MAX_TRACKS' - field_kwargs = { - 'required': False, - } + name = "max_tracks" + verbose_name = "Max tracks per playlist" + setting = "PLAYLISTS_MAX_TRACKS" + field_kwargs = {"required": False} diff --git a/api/funkwhale_api/playlists/factories.py b/api/funkwhale_api/playlists/factories.py index cddea6002..ff031945a 100644 --- a/api/funkwhale_api/playlists/factories.py +++ b/api/funkwhale_api/playlists/factories.py @@ -7,11 +7,11 @@ from funkwhale_api.users.factories import UserFactory @registry.register class PlaylistFactory(factory.django.DjangoModelFactory): - name = factory.Faker('name') + name = factory.Faker("name") user = factory.SubFactory(UserFactory) class Meta: - model = 'playlists.Playlist' + model = "playlists.Playlist" @registry.register @@ -20,4 +20,4 @@ class PlaylistTrackFactory(factory.django.DjangoModelFactory): track = factory.SubFactory(TrackFactory) class Meta: - model = 'playlists.PlaylistTrack' + model = "playlists.PlaylistTrack" diff --git a/api/funkwhale_api/playlists/filters.py b/api/funkwhale_api/playlists/filters.py index bc4941510..ae9f0226f 100644 --- a/api/funkwhale_api/playlists/filters.py +++ b/api/funkwhale_api/playlists/filters.py @@ -5,18 +5,13 @@ from funkwhale_api.music import utils from . import models - class PlaylistFilter(filters.FilterSet): - q = filters.CharFilter(name='_', method='filter_q') + q = filters.CharFilter(name="_", method="filter_q") class Meta: model = models.Playlist - fields = { - 'user': ['exact'], - 'name': ['exact', 'icontains'], - 'q': 'exact', - } + fields = {"user": ["exact"], "name": ["exact", "icontains"], "q": "exact"} def filter_q(self, queryset, name, value): - query = utils.get_query(value, ['name', 'user__username']) + query = utils.get_query(value, ["name", "user__username"]) return queryset.filter(query) diff --git a/api/funkwhale_api/playlists/migrations/0001_initial.py b/api/funkwhale_api/playlists/migrations/0001_initial.py index 987b2f9cf..68e66d763 100644 --- a/api/funkwhale_api/playlists/migrations/0001_initial.py +++ b/api/funkwhale_api/playlists/migrations/0001_initial.py @@ -10,34 +10,84 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('music', '0012_auto_20161122_1905'), + ("music", "0012_auto_20161122_1905"), ] operations = [ migrations.CreateModel( - name='Playlist', + name="Playlist", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('name', models.CharField(max_length=50)), - ('is_public', models.BooleanField(default=False)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='playlists', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ("name", models.CharField(max_length=50)), + ("is_public", models.BooleanField(default=False)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "user", + models.ForeignKey( + to=settings.AUTH_USER_MODEL, + related_name="playlists", + on_delete=models.CASCADE, + ), + ), ], ), migrations.CreateModel( - name='PlaylistTrack', + name="PlaylistTrack", fields=[ - ('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)), - ('lft', models.PositiveIntegerField(db_index=True, editable=False)), - ('rght', models.PositiveIntegerField(db_index=True, editable=False)), - ('tree_id', models.PositiveIntegerField(db_index=True, editable=False)), - ('position', models.PositiveIntegerField(db_index=True, editable=False)), - ('playlist', models.ForeignKey(to='playlists.Playlist', related_name='playlist_tracks', on_delete=models.CASCADE)), - ('previous', models.OneToOneField(null=True, to='playlists.PlaylistTrack', related_name='next', blank=True, on_delete=models.CASCADE)), - ('track', models.ForeignKey(to='music.Track', related_name='playlist_tracks', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + auto_created=True, + verbose_name="ID", + primary_key=True, + serialize=False, + ), + ), + ("lft", models.PositiveIntegerField(db_index=True, editable=False)), + ("rght", models.PositiveIntegerField(db_index=True, editable=False)), + ("tree_id", models.PositiveIntegerField(db_index=True, editable=False)), + ( + "position", + models.PositiveIntegerField(db_index=True, editable=False), + ), + ( + "playlist", + models.ForeignKey( + to="playlists.Playlist", + related_name="playlist_tracks", + on_delete=models.CASCADE, + ), + ), + ( + "previous", + models.OneToOneField( + null=True, + to="playlists.PlaylistTrack", + related_name="next", + blank=True, + on_delete=models.CASCADE, + ), + ), + ( + "track", + models.ForeignKey( + to="music.Track", + related_name="playlist_tracks", + on_delete=models.CASCADE, + ), + ), ], - options={ - 'ordering': ('-playlist', 'position'), - }, + options={"ordering": ("-playlist", "position")}, ), ] diff --git a/api/funkwhale_api/playlists/migrations/0002_auto_20180316_2217.py b/api/funkwhale_api/playlists/migrations/0002_auto_20180316_2217.py index 23d0a8eab..8245797bf 100644 --- a/api/funkwhale_api/playlists/migrations/0002_auto_20180316_2217.py +++ b/api/funkwhale_api/playlists/migrations/0002_auto_20180316_2217.py @@ -5,18 +5,22 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('playlists', '0001_initial'), - ] + dependencies = [("playlists", "0001_initial")] operations = [ - migrations.RemoveField( - model_name='playlist', - name='is_public', - ), + migrations.RemoveField(model_name="playlist", name="is_public"), migrations.AddField( - model_name='playlist', - name='privacy_level', - field=models.CharField(choices=[('me', 'Only me'), ('followers', 'Me and my followers'), ('instance', 'Everyone on my instance, and my followers'), ('everyone', 'Everyone, including people on other instances')], default='instance', max_length=30), + model_name="playlist", + name="privacy_level", + field=models.CharField( + choices=[ + ("me", "Only me"), + ("followers", "Me and my followers"), + ("instance", "Everyone on my instance, and my followers"), + ("everyone", "Everyone, including people on other instances"), + ], + default="instance", + max_length=30, + ), ), ] diff --git a/api/funkwhale_api/playlists/migrations/0003_auto_20180319_1214.py b/api/funkwhale_api/playlists/migrations/0003_auto_20180319_1214.py index 0284f8f2c..d4d28b9e0 100644 --- a/api/funkwhale_api/playlists/migrations/0003_auto_20180319_1214.py +++ b/api/funkwhale_api/playlists/migrations/0003_auto_20180319_1214.py @@ -6,47 +6,28 @@ import django.utils.timezone class Migration(migrations.Migration): - dependencies = [ - ('playlists', '0002_auto_20180316_2217'), - ] + dependencies = [("playlists", "0002_auto_20180316_2217")] operations = [ migrations.AlterModelOptions( - name='playlisttrack', - options={'ordering': ('-playlist', 'index')}, + name="playlisttrack", options={"ordering": ("-playlist", "index")} ), migrations.AddField( - model_name='playlisttrack', - name='creation_date', + model_name="playlisttrack", + name="creation_date", field=models.DateTimeField(default=django.utils.timezone.now), ), migrations.AddField( - model_name='playlisttrack', - name='index', + model_name="playlisttrack", + name="index", field=models.PositiveIntegerField(null=True), ), - migrations.RemoveField( - model_name='playlisttrack', - name='lft', - ), - migrations.RemoveField( - model_name='playlisttrack', - name='position', - ), - migrations.RemoveField( - model_name='playlisttrack', - name='previous', - ), - migrations.RemoveField( - model_name='playlisttrack', - name='rght', - ), - migrations.RemoveField( - model_name='playlisttrack', - name='tree_id', - ), + migrations.RemoveField(model_name="playlisttrack", name="lft"), + migrations.RemoveField(model_name="playlisttrack", name="position"), + migrations.RemoveField(model_name="playlisttrack", name="previous"), + migrations.RemoveField(model_name="playlisttrack", name="rght"), + migrations.RemoveField(model_name="playlisttrack", name="tree_id"), migrations.AlterUniqueTogether( - name='playlisttrack', - unique_together={('playlist', 'index')}, + name="playlisttrack", unique_together={("playlist", "index")} ), ] diff --git a/api/funkwhale_api/playlists/migrations/0004_auto_20180320_1713.py b/api/funkwhale_api/playlists/migrations/0004_auto_20180320_1713.py index 415b53612..75c42a5c0 100644 --- a/api/funkwhale_api/playlists/migrations/0004_auto_20180320_1713.py +++ b/api/funkwhale_api/playlists/migrations/0004_auto_20180320_1713.py @@ -5,23 +5,18 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('playlists', '0003_auto_20180319_1214'), - ] + dependencies = [("playlists", "0003_auto_20180319_1214")] operations = [ migrations.AddField( - model_name='playlist', - name='modification_date', + model_name="playlist", + name="modification_date", field=models.DateTimeField(auto_now=True), ), migrations.AlterField( - model_name='playlisttrack', - name='index', + model_name="playlisttrack", + name="index", field=models.PositiveIntegerField(blank=True, null=True), ), - migrations.AlterUniqueTogether( - name='playlisttrack', - unique_together=set(), - ), + migrations.AlterUniqueTogether(name="playlisttrack", unique_together=set()), ] diff --git a/api/funkwhale_api/playlists/models.py b/api/funkwhale_api/playlists/models.py index f5132e12d..ef787d276 100644 --- a/api/funkwhale_api/playlists/models.py +++ b/api/funkwhale_api/playlists/models.py @@ -11,17 +11,16 @@ from funkwhale_api.common import preferences class PlaylistQuerySet(models.QuerySet): def with_tracks_count(self): - return self.annotate( - _tracks_count=models.Count('playlist_tracks')) + return self.annotate(_tracks_count=models.Count("playlist_tracks")) class Playlist(models.Model): name = models.CharField(max_length=50) user = models.ForeignKey( - 'users.User', related_name="playlists", on_delete=models.CASCADE) + "users.User", related_name="playlists", on_delete=models.CASCADE + ) creation_date = models.DateTimeField(default=timezone.now) - modification_date = models.DateTimeField( - auto_now=True) + modification_date = models.DateTimeField(auto_now=True) privacy_level = fields.get_privacy_field() objects = PlaylistQuerySet.as_manager() @@ -51,89 +50,91 @@ class Playlist(models.Model): index = total if index > total: - raise exceptions.ValidationError('Index is not continuous') + raise exceptions.ValidationError("Index is not continuous") if index < 0: - raise exceptions.ValidationError('Index must be zero or positive') + raise exceptions.ValidationError("Index must be zero or positive") if move: # we remove the index temporarily, to avoid integrity errors plt.index = None - plt.save(update_fields=['index']) + plt.save(update_fields=["index"]) if index > old_index: # new index is higher than current, we decrement previous tracks - to_update = existing.filter( - index__gt=old_index, index__lte=index) - to_update.update(index=models.F('index') - 1) + to_update = existing.filter(index__gt=old_index, index__lte=index) + to_update.update(index=models.F("index") - 1) if index < old_index: # new index is lower than current, we increment next tracks to_update = existing.filter(index__lt=old_index, index__gte=index) - to_update.update(index=models.F('index') + 1) + to_update.update(index=models.F("index") + 1) else: to_update = existing.filter(index__gte=index) - to_update.update(index=models.F('index') + 1) + to_update.update(index=models.F("index") + 1) plt.index = index - plt.save(update_fields=['index']) - self.save(update_fields=['modification_date']) + plt.save(update_fields=["index"]) + self.save(update_fields=["modification_date"]) return index @transaction.atomic def remove(self, index): existing = self.playlist_tracks.select_for_update() - self.save(update_fields=['modification_date']) + self.save(update_fields=["modification_date"]) to_update = existing.filter(index__gt=index) - return to_update.update(index=models.F('index') - 1) + return to_update.update(index=models.F("index") - 1) @transaction.atomic def insert_many(self, tracks): existing = self.playlist_tracks.select_for_update() now = timezone.now() total = existing.filter(index__isnull=False).count() - max_tracks = preferences.get('playlists__max_tracks') + max_tracks = preferences.get("playlists__max_tracks") if existing.count() + len(tracks) > max_tracks: raise exceptions.ValidationError( - 'Playlist would reach the maximum of {} tracks'.format( - max_tracks)) - self.save(update_fields=['modification_date']) + "Playlist would reach the maximum of {} tracks".format(max_tracks) + ) + self.save(update_fields=["modification_date"]) start = total plts = [ PlaylistTrack( - creation_date=now, playlist=self, track=track, index=start+i) + creation_date=now, playlist=self, track=track, index=start + i + ) for i, track in enumerate(tracks) ] return PlaylistTrack.objects.bulk_create(plts) + class PlaylistTrackQuerySet(models.QuerySet): def for_nested_serialization(self): - return (self.select_related() - .select_related('track__album__artist') - .prefetch_related( - 'track__tags', - 'track__files', - 'track__artist__albums__tracks__tags')) + return ( + self.select_related() + .select_related("track__album__artist") + .prefetch_related( + "track__tags", "track__files", "track__artist__albums__tracks__tags" + ) + ) class PlaylistTrack(models.Model): track = models.ForeignKey( - 'music.Track', - related_name='playlist_tracks', - on_delete=models.CASCADE) + "music.Track", related_name="playlist_tracks", on_delete=models.CASCADE + ) index = models.PositiveIntegerField(null=True, blank=True) playlist = models.ForeignKey( - Playlist, related_name='playlist_tracks', on_delete=models.CASCADE) + Playlist, related_name="playlist_tracks", on_delete=models.CASCADE + ) creation_date = models.DateTimeField(default=timezone.now) objects = PlaylistTrackQuerySet.as_manager() class Meta: - ordering = ('-playlist', 'index') - unique_together = ('playlist', 'index') + ordering = ("-playlist", "index") + unique_together = ("playlist", "index") def delete(self, *args, **kwargs): playlist = self.playlist index = self.index - update_indexes = kwargs.pop('update_indexes', False) + update_indexes = kwargs.pop("update_indexes", False) r = super().delete(*args, **kwargs) if index is not None and update_indexes: playlist.remove(index) diff --git a/api/funkwhale_api/playlists/serializers.py b/api/funkwhale_api/playlists/serializers.py index 3f01fd689..3fac68b0e 100644 --- a/api/funkwhale_api/playlists/serializers.py +++ b/api/funkwhale_api/playlists/serializers.py @@ -15,42 +15,42 @@ class PlaylistTrackSerializer(serializers.ModelSerializer): class Meta: model = models.PlaylistTrack - fields = ('id', 'track', 'playlist', 'index', 'creation_date') + fields = ("id", "track", "playlist", "index", "creation_date") class PlaylistTrackWriteSerializer(serializers.ModelSerializer): - index = serializers.IntegerField( - required=False, min_value=0, allow_null=True) + index = serializers.IntegerField(required=False, min_value=0, allow_null=True) class Meta: model = models.PlaylistTrack - fields = ('id', 'track', 'playlist', 'index') + fields = ("id", "track", "playlist", "index") def validate_playlist(self, value): - if self.context.get('request'): + if self.context.get("request"): # validate proper ownership on the playlist - if self.context['request'].user != value.user: + if self.context["request"].user != value.user: raise serializers.ValidationError( - 'You do not have the permission to edit this playlist') + "You do not have the permission to edit this playlist" + ) existing = value.playlist_tracks.count() - max_tracks = preferences.get('playlists__max_tracks') + max_tracks = preferences.get("playlists__max_tracks") if existing >= max_tracks: raise serializers.ValidationError( - 'Playlist has reached the maximum of {} tracks'.format( - max_tracks)) + "Playlist has reached the maximum of {} tracks".format(max_tracks) + ) return value @transaction.atomic def create(self, validated_data): - index = validated_data.pop('index', None) + index = validated_data.pop("index", None) instance = super().create(validated_data) instance.playlist.insert(instance, index) return instance @transaction.atomic def update(self, instance, validated_data): - update_index = 'index' in validated_data - index = validated_data.pop('index', None) + update_index = "index" in validated_data + index = validated_data.pop("index", None) super().update(instance, validated_data) if update_index: instance.playlist.insert(instance, index) @@ -71,17 +71,15 @@ class PlaylistSerializer(serializers.ModelSerializer): class Meta: model = models.Playlist fields = ( - 'id', - 'name', - 'tracks_count', - 'user', - 'modification_date', - 'creation_date', - 'privacy_level',) - read_only_fields = [ - 'id', - 'modification_date', - 'creation_date',] + "id", + "name", + "tracks_count", + "user", + "modification_date", + "creation_date", + "privacy_level", + ) + read_only_fields = ["id", "modification_date", "creation_date"] def get_tracks_count(self, obj): try: @@ -93,4 +91,5 @@ class PlaylistSerializer(serializers.ModelSerializer): class PlaylistAddManySerializer(serializers.Serializer): tracks = serializers.PrimaryKeyRelatedField( - many=True, queryset=Track.objects.for_nested_serialization()) + many=True, queryset=Track.objects.for_nested_serialization() + ) diff --git a/api/funkwhale_api/playlists/views.py b/api/funkwhale_api/playlists/views.py index 683f90388..f45d8b272 100644 --- a/api/funkwhale_api/playlists/views.py +++ b/api/funkwhale_api/playlists/views.py @@ -16,108 +16,110 @@ from . import filters from . import models from . import serializers + class PlaylistViewSet( - mixins.RetrieveModelMixin, - mixins.CreateModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): serializer_class = serializers.PlaylistSerializer queryset = ( - models.Playlist.objects.all().select_related('user') - .annotate(tracks_count=Count('playlist_tracks')) + models.Playlist.objects.all() + .select_related("user") + .annotate(tracks_count=Count("playlist_tracks")) ) permission_classes = [ permissions.ConditionalAuthentication, permissions.OwnerPermission, IsAuthenticatedOrReadOnly, ] - owner_checks = ['write'] + owner_checks = ["write"] filter_class = filters.PlaylistFilter - ordering_fields = ('id', 'name', 'creation_date', 'modification_date') + ordering_fields = ("id", "name", "creation_date", "modification_date") - @detail_route(methods=['get']) + @detail_route(methods=["get"]) def tracks(self, request, *args, **kwargs): playlist = self.get_object() plts = playlist.playlist_tracks.all().for_nested_serialization() serializer = serializers.PlaylistTrackSerializer(plts, many=True) - data = { - 'count': len(plts), - 'results': serializer.data - } + data = {"count": len(plts), "results": serializer.data} return Response(data, status=200) - @detail_route(methods=['post']) + @detail_route(methods=["post"]) @transaction.atomic def add(self, request, *args, **kwargs): playlist = self.get_object() serializer = serializers.PlaylistAddManySerializer(data=request.data) serializer.is_valid(raise_exception=True) try: - plts = playlist.insert_many(serializer.validated_data['tracks']) + plts = playlist.insert_many(serializer.validated_data["tracks"]) except exceptions.ValidationError as e: - payload = {'playlist': e.detail} + payload = {"playlist": e.detail} return Response(payload, status=400) ids = [p.id for p in plts] - plts = models.PlaylistTrack.objects.filter( - pk__in=ids).order_by('index').for_nested_serialization() + plts = ( + models.PlaylistTrack.objects.filter(pk__in=ids) + .order_by("index") + .for_nested_serialization() + ) serializer = serializers.PlaylistTrackSerializer(plts, many=True) - data = { - 'count': len(plts), - 'results': serializer.data - } + data = {"count": len(plts), "results": serializer.data} return Response(data, status=201) - @detail_route(methods=['delete']) + @detail_route(methods=["delete"]) @transaction.atomic def clear(self, request, *args, **kwargs): playlist = self.get_object() playlist.playlist_tracks.all().delete() - playlist.save(update_fields=['modification_date']) + playlist.save(update_fields=["modification_date"]) return Response(status=204) def get_queryset(self): - return self.queryset.filter( - fields.privacy_level_query(self.request.user)) + return self.queryset.filter(fields.privacy_level_query(self.request.user)) def perform_create(self, serializer): return serializer.save( user=self.request.user, privacy_level=serializer.validated_data.get( - 'privacy_level', self.request.user.privacy_level) + "privacy_level", self.request.user.privacy_level + ), ) class PlaylistTrackViewSet( - mixins.RetrieveModelMixin, - mixins.CreateModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): + mixins.RetrieveModelMixin, + mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): serializer_class = serializers.PlaylistTrackSerializer - queryset = (models.PlaylistTrack.objects.all().for_nested_serialization()) + queryset = models.PlaylistTrack.objects.all().for_nested_serialization() permission_classes = [ permissions.ConditionalAuthentication, permissions.OwnerPermission, IsAuthenticatedOrReadOnly, ] - owner_field = 'playlist.user' - owner_checks = ['write'] + owner_field = "playlist.user" + owner_checks = ["write"] def get_serializer_class(self): - if self.request.method in ['PUT', 'PATCH', 'DELETE', 'POST']: + if self.request.method in ["PUT", "PATCH", "DELETE", "POST"]: return serializers.PlaylistTrackWriteSerializer return self.serializer_class def get_queryset(self): return self.queryset.filter( fields.privacy_level_query( - self.request.user, - lookup_field='playlist__privacy_level')) + self.request.user, lookup_field="playlist__privacy_level" + ) + ) def perform_destroy(self, instance): instance.delete(update_indexes=True) diff --git a/api/funkwhale_api/providers/acoustid/__init__.py b/api/funkwhale_api/providers/acoustid/__init__.py index 69fe058b3..558a95bb8 100644 --- a/api/funkwhale_api/providers/acoustid/__init__.py +++ b/api/funkwhale_api/providers/acoustid/__init__.py @@ -14,14 +14,14 @@ class Client(object): results = self.match(file_path=file_path) MIN_SCORE_FOR_MATCH = 0.8 try: - rows = results['results'] + rows = results["results"] except KeyError: return for row in rows: - if row['score'] >= MIN_SCORE_FOR_MATCH: + if row["score"] >= MIN_SCORE_FOR_MATCH: return row def get_acoustid_client(): manager = global_preferences_registry.manager() - return Client(api_key=manager['providers_acoustid__api_key']) + return Client(api_key=manager["providers_acoustid__api_key"]) diff --git a/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py b/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py index 33c9643b0..2e5a35f0d 100644 --- a/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py +++ b/api/funkwhale_api/providers/acoustid/dynamic_preferences_registry.py @@ -3,17 +3,15 @@ from django import forms from dynamic_preferences.types import StringPreference, Section from dynamic_preferences.registries import global_preferences_registry -acoustid = Section('providers_acoustid') +acoustid = Section("providers_acoustid") @global_preferences_registry.register class APIKey(StringPreference): section = acoustid - name = 'api_key' - default = '' - verbose_name = 'Acoustid API key' - help_text = 'The API key used to query AcoustID. Get one at https://acoustid.org/new-application.' + name = "api_key" + default = "" + verbose_name = "Acoustid API key" + help_text = "The API key used to query AcoustID. Get one at https://acoustid.org/new-application." widget = forms.PasswordInput - field_kwargs = { - 'required': False, - } + field_kwargs = {"required": False} diff --git a/api/funkwhale_api/providers/audiofile/management/commands/import_files.py b/api/funkwhale_api/providers/audiofile/management/commands/import_files.py index 70ff90ffa..9114375e0 100644 --- a/api/funkwhale_api/providers/audiofile/management/commands/import_files.py +++ b/api/funkwhale_api/providers/audiofile/management/commands/import_files.py @@ -11,186 +11,195 @@ from funkwhale_api.users.models import User class Command(BaseCommand): - help = 'Import audio files mathinc given glob pattern' + help = "Import audio files mathinc given glob pattern" def add_arguments(self, parser): - parser.add_argument('path', type=str) + parser.add_argument("path", type=str) parser.add_argument( - '--recursive', - action='store_true', - dest='recursive', + "--recursive", + action="store_true", + dest="recursive", default=False, - help='Will match the pattern recursively (including subdirectories)', + help="Will match the pattern recursively (including subdirectories)", ) parser.add_argument( - '--username', - dest='username', - help='The username of the user you want to be bound to the import', + "--username", + dest="username", + help="The username of the user you want to be bound to the import", ) parser.add_argument( - '--async', - action='store_true', - dest='async', + "--async", + action="store_true", + dest="async", default=False, - help='Will launch celery tasks for each file to import instead of doing it synchronously and block the CLI', + help="Will launch celery tasks for each file to import instead of doing it synchronously and block the CLI", ) parser.add_argument( - '--exit', '-x', - action='store_true', - dest='exit_on_failure', + "--exit", + "-x", + action="store_true", + dest="exit_on_failure", default=False, - help='Use this flag to disable error catching', + help="Use this flag to disable error catching", ) parser.add_argument( - '--in-place', '-i', - action='store_true', - dest='in_place', + "--in-place", + "-i", + action="store_true", + dest="in_place", default=False, help=( - 'Import files without duplicating them into the media directory.' - 'For in-place import to work, the music files must be readable' - 'by the web-server and funkwhale api and celeryworker processes.' - 'You may want to use this if you have a big music library to ' - 'import and not much disk space available.' - ) + "Import files without duplicating them into the media directory." + "For in-place import to work, the music files must be readable" + "by the web-server and funkwhale api and celeryworker processes." + "You may want to use this if you have a big music library to " + "import and not much disk space available." + ), ) parser.add_argument( - '--noinput', '--no-input', action='store_false', dest='interactive', + "--noinput", + "--no-input", + action="store_false", + dest="interactive", help="Do NOT prompt the user for input of any kind.", ) def handle(self, *args, **options): glob_kwargs = {} - if options['recursive']: - glob_kwargs['recursive'] = True + if options["recursive"]: + glob_kwargs["recursive"] = True try: - matching = sorted(glob.glob(options['path'], **glob_kwargs)) + matching = sorted(glob.glob(options["path"], **glob_kwargs)) except TypeError: - raise Exception('You need Python 3.5 to use the --recursive flag') + raise Exception("You need Python 3.5 to use the --recursive flag") - if options['in_place']: + if options["in_place"]: self.stdout.write( - 'Checking imported paths against settings.MUSIC_DIRECTORY_PATH') + "Checking imported paths against settings.MUSIC_DIRECTORY_PATH" + ) p = settings.MUSIC_DIRECTORY_PATH if not p: raise CommandError( - 'Importing in-place requires setting the ' - 'MUSIC_DIRECTORY_PATH variable') + "Importing in-place requires setting the " + "MUSIC_DIRECTORY_PATH variable" + ) for m in matching: if not m.startswith(p): raise CommandError( - 'Importing in-place only works if importing' - 'from {} (MUSIC_DIRECTORY_PATH), as this directory' - 'needs to be accessible by the webserver.' - 'Culprit: {}'.format(p, m)) + "Importing in-place only works if importing" + "from {} (MUSIC_DIRECTORY_PATH), as this directory" + "needs to be accessible by the webserver." + "Culprit: {}".format(p, m) + ) if not matching: - raise CommandError('No file matching pattern, aborting') + raise CommandError("No file matching pattern, aborting") user = None - if options['username']: + if options["username"]: try: - user = User.objects.get(username=options['username']) + user = User.objects.get(username=options["username"]) except User.DoesNotExist: - raise CommandError('Invalid username') + raise CommandError("Invalid username") else: # we bind the import to the first registered superuser try: - user = User.objects.filter(is_superuser=True).order_by('pk').first() + user = User.objects.filter(is_superuser=True).order_by("pk").first() assert user is not None except AssertionError: raise CommandError( - 'No superuser available, please provide a --username') + "No superuser available, please provide a --username" + ) filtered = self.filter_matching(matching, options) - self.stdout.write('Import summary:') - self.stdout.write('- {} files found matching this pattern: {}'.format( - len(matching), options['path'])) - self.stdout.write('- {} files already found in database'.format( - len(filtered['skipped']))) - self.stdout.write('- {} new files'.format( - len(filtered['new']))) + self.stdout.write("Import summary:") + self.stdout.write( + "- {} files found matching this pattern: {}".format( + len(matching), options["path"] + ) + ) + self.stdout.write( + "- {} files already found in database".format(len(filtered["skipped"])) + ) + self.stdout.write("- {} new files".format(len(filtered["new"]))) - self.stdout.write('Selected options: {}'.format(', '.join([ - 'in place' if options['in_place'] else 'copy music files', - ]))) - if len(filtered['new']) == 0: - self.stdout.write('Nothing new to import, exiting') + self.stdout.write( + "Selected options: {}".format( + ", ".join(["in place" if options["in_place"] else "copy music files"]) + ) + ) + if len(filtered["new"]) == 0: + self.stdout.write("Nothing new to import, exiting") return - if options['interactive']: + if options["interactive"]: message = ( - 'Are you sure you want to do this?\n\n' + "Are you sure you want to do this?\n\n" "Type 'yes' to continue, or 'no' to cancel: " ) - if input(''.join(message)) != 'yes': + if input("".join(message)) != "yes": raise CommandError("Import cancelled.") - batch, errors = self.do_import( - filtered['new'], user=user, options=options) - message = 'Successfully imported {} tracks' - if options['async']: - message = 'Successfully launched import for {} tracks' + batch, errors = self.do_import(filtered["new"], user=user, options=options) + message = "Successfully imported {} tracks" + if options["async"]: + message = "Successfully launched import for {} tracks" - self.stdout.write(message.format(len(filtered['new']))) + self.stdout.write(message.format(len(filtered["new"]))) if len(errors) > 0: - self.stderr.write( - '{} tracks could not be imported:'.format(len(errors))) + self.stderr.write("{} tracks could not be imported:".format(len(errors))) for path, error in errors: - self.stderr.write('- {}: {}'.format(path, error)) + self.stderr.write("- {}: {}".format(path, error)) self.stdout.write( - "For details, please refer to import batch #{}".format(batch.pk)) + "For details, please refer to import batch #{}".format(batch.pk) + ) def filter_matching(self, matching, options): - sources = ['file://{}'.format(p) for p in matching] + sources = ["file://{}".format(p) for p in matching] # we skip reimport for path that are already found # as a TrackFile.source existing = models.TrackFile.objects.filter(source__in=sources) - existing = existing.values_list('source', flat=True) - existing = set([p.replace('file://', '', 1) for p in existing]) + existing = existing.values_list("source", flat=True) + existing = set([p.replace("file://", "", 1) for p in existing]) skipped = set(matching) & existing result = { - 'initial': matching, - 'skipped': list(sorted(skipped)), - 'new': list(sorted(set(matching) - skipped)), + "initial": matching, + "skipped": list(sorted(skipped)), + "new": list(sorted(set(matching) - skipped)), } return result def do_import(self, paths, user, options): - message = '{i}/{total} Importing {path}...' - if options['async']: - message = '{i}/{total} Launching import for {path}...' + message = "{i}/{total} Importing {path}..." + if options["async"]: + message = "{i}/{total} Launching import for {path}..." # we create an import batch binded to the user - async = options['async'] + async = options["async"] import_handler = tasks.import_job_run.delay if async else tasks.import_job_run - batch = user.imports.create(source='shell') + batch = user.imports.create(source="shell") total = len(paths) errors = [] for i, path in list(enumerate(paths)): try: - self.stdout.write( - message.format(path=path, i=i+1, total=len(paths))) + self.stdout.write(message.format(path=path, i=i + 1, total=len(paths))) self.import_file(path, batch, import_handler, options) except Exception as e: - if options['exit_on_failure']: + if options["exit_on_failure"]: raise - m = 'Error while importing {}: {} {}'.format( - path, e.__class__.__name__, e) + m = "Error while importing {}: {} {}".format( + path, e.__class__.__name__, e + ) self.stderr.write(m) - errors.append((path, '{} {}'.format(e.__class__.__name__, e))) + errors.append((path, "{} {}".format(e.__class__.__name__, e))) return batch, errors def import_file(self, path, batch, import_handler, options): - job = batch.jobs.create( - source='file://' + path, - ) - if not options['in_place']: + job = batch.jobs.create(source="file://" + path) + if not options["in_place"]: name = os.path.basename(path) - with open(path, 'rb') as f: + with open(path, "rb") as f: job.audio_file.save(name, File(f)) job.save() - import_handler( - import_job_id=job.pk, - use_acoustid=False) + import_handler(import_job_id=job.pk, use_acoustid=False) diff --git a/api/funkwhale_api/providers/audiofile/tasks.py b/api/funkwhale_api/providers/audiofile/tasks.py index fb6306735..afd54dc24 100644 --- a/api/funkwhale_api/providers/audiofile/tasks.py +++ b/api/funkwhale_api/providers/audiofile/tasks.py @@ -13,50 +13,40 @@ from funkwhale_api.music import models, metadata def import_track_data_from_path(path): data = metadata.Metadata(path) album = None - track_mbid = data.get('musicbrainz_recordingid', None) - album_mbid = data.get('musicbrainz_albumid', None) + track_mbid = data.get("musicbrainz_recordingid", None) + album_mbid = data.get("musicbrainz_albumid", None) if album_mbid and track_mbid: # to gain performance and avoid additional mb lookups, # we import from the release data, which is already cached - return models.Track.get_or_create_from_release( - album_mbid, track_mbid)[0] + return models.Track.get_or_create_from_release(album_mbid, track_mbid)[0] elif track_mbid: return models.Track.get_or_create_from_api(track_mbid)[0] elif album_mbid: album = models.Album.get_or_create_from_api(album_mbid)[0] artist = album.artist if album else None - artist_mbid = data.get('musicbrainz_artistid', None) + artist_mbid = data.get("musicbrainz_artistid", None) if not artist: if artist_mbid: artist = models.Artist.get_or_create_from_api(artist_mbid)[0] else: artist = models.Artist.objects.get_or_create( - name__iexact=data.get('artist'), - defaults={ - 'name': data.get('artist'), - }, + name__iexact=data.get("artist"), defaults={"name": data.get("artist")} )[0] - release_date = data.get('date', default=None) + release_date = data.get("date", default=None) if not album: album = models.Album.objects.get_or_create( - title__iexact=data.get('album'), + title__iexact=data.get("album"), artist=artist, - defaults={ - 'title': data.get('album'), - 'release_date': release_date, - }, + defaults={"title": data.get("album"), "release_date": release_date}, )[0] - position = data.get('track_number', default=None) + position = data.get("track_number", default=None) track = models.Track.objects.get_or_create( - title__iexact=data.get('title'), + title__iexact=data.get("title"), album=album, - defaults={ - 'title': data.get('title'), - 'position': position, - }, + defaults={"title": data.get("title"), "position": position}, )[0] return track @@ -65,31 +55,27 @@ def import_metadata_with_musicbrainz(path): pass -@celery.app.task(name='audiofile.from_path') +@celery.app.task(name="audiofile.from_path") def from_path(path): acoustid_track_id = None try: client = get_acoustid_client() result = client.get_best_match(path) - acoustid_track_id = result['id'] + acoustid_track_id = result["id"] except acoustid.WebServiceError: track = import_track_data_from_path(path) except (TypeError, KeyError): track = import_metadata_without_musicbrainz(path) else: track, created = models.Track.get_or_create_from_api( - mbid=result['recordings'][0]['id'] + mbid=result["recordings"][0]["id"] ) if track.files.count() > 0: - raise ValueError('File already exists for track {}'.format(track.pk)) + raise ValueError("File already exists for track {}".format(track.pk)) - track_file = models.TrackFile( - track=track, acoustid_track_id=acoustid_track_id) - track_file.audio_file.save( - os.path.basename(path), - File(open(path, 'rb')) - ) + track_file = models.TrackFile(track=track, acoustid_track_id=acoustid_track_id) + track_file.audio_file.save(os.path.basename(path), File(open(path, "rb"))) track_file.save() return track_file diff --git a/api/funkwhale_api/providers/urls.py b/api/funkwhale_api/providers/urls.py index 10975da53..6d001ed9d 100644 --- a/api/funkwhale_api/providers/urls.py +++ b/api/funkwhale_api/providers/urls.py @@ -2,10 +2,16 @@ from django.conf.urls import include, url from funkwhale_api.music import views urlpatterns = [ - url(r'^youtube/', include( - ('funkwhale_api.providers.youtube.urls', 'youtube'), - namespace='youtube')), - url(r'^musicbrainz/', include( - ('funkwhale_api.musicbrainz.urls', 'musicbrainz'), - namespace='musicbrainz')), + url( + r"^youtube/", + include( + ("funkwhale_api.providers.youtube.urls", "youtube"), namespace="youtube" + ), + ), + url( + r"^musicbrainz/", + include( + ("funkwhale_api.musicbrainz.urls", "musicbrainz"), namespace="musicbrainz" + ), + ), ] diff --git a/api/funkwhale_api/providers/youtube/client.py b/api/funkwhale_api/providers/youtube/client.py index 792e501d7..fbc32efa4 100644 --- a/api/funkwhale_api/providers/youtube/client.py +++ b/api/funkwhale_api/providers/youtube/client.py @@ -4,12 +4,11 @@ from apiclient.discovery import build from apiclient.errors import HttpError from oauth2client.tools import argparser -from dynamic_preferences.registries import ( - global_preferences_registry as registry) +from dynamic_preferences.registries import global_preferences_registry as registry YOUTUBE_API_SERVICE_NAME = "youtube" YOUTUBE_API_VERSION = "v3" -VIDEO_BASE_URL = 'https://www.youtube.com/watch?v={0}' +VIDEO_BASE_URL = "https://www.youtube.com/watch?v={0}" def _do_search(query): @@ -17,23 +16,21 @@ def _do_search(query): youtube = build( YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, - developerKey=manager['providers_youtube__api_key']) + developerKey=manager["providers_youtube__api_key"], + ) - return youtube.search().list( - q=query, - part="id,snippet", - maxResults=25 - ).execute() + return youtube.search().list(q=query, part="id,snippet", maxResults=25).execute() class Client(object): - def search(self, query): search_response = _do_search(query) videos = [] for search_result in search_response.get("items", []): if search_result["id"]["kind"] == "youtube#video": - search_result['full_url'] = VIDEO_BASE_URL.format(search_result["id"]['videoId']) + search_result["full_url"] = VIDEO_BASE_URL.format( + search_result["id"]["videoId"] + ) videos.append(search_result) return videos @@ -44,7 +41,7 @@ class Client(object): results[key] = self.search(query) threads = [ - threading.Thread(target=search, args=(key, query,)) + threading.Thread(target=search, args=(key, query)) for key, query in queries.items() ] for thread in threads: @@ -71,16 +68,16 @@ class Client(object): } """ return { - 'id': result['id']['videoId'], - 'url': 'https://www.youtube.com/watch?v={}'.format( - result['id']['videoId']), - 'type': result['id']['kind'], - 'title': result['snippet']['title'], - 'description': result['snippet']['description'], - 'channelId': result['snippet']['channelId'], - 'channelTitle': result['snippet']['channelTitle'], - 'publishedAt': result['snippet']['publishedAt'], - 'cover': result['snippet']['thumbnails']['high']['url'], + "id": result["id"]["videoId"], + "url": "https://www.youtube.com/watch?v={}".format(result["id"]["videoId"]), + "type": result["id"]["kind"], + "title": result["snippet"]["title"], + "description": result["snippet"]["description"], + "channelId": result["snippet"]["channelId"], + "channelTitle": result["snippet"]["channelTitle"], + "publishedAt": result["snippet"]["publishedAt"], + "cover": result["snippet"]["thumbnails"]["high"]["url"], } + client = Client() diff --git a/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py b/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py index ac5fc4bde..42b54b535 100644 --- a/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py +++ b/api/funkwhale_api/providers/youtube/dynamic_preferences_registry.py @@ -3,17 +3,15 @@ from django import forms from dynamic_preferences.types import StringPreference, Section from dynamic_preferences.registries import global_preferences_registry -youtube = Section('providers_youtube') +youtube = Section("providers_youtube") @global_preferences_registry.register class APIKey(StringPreference): section = youtube - name = 'api_key' - default = 'CHANGEME' - verbose_name = 'YouTube API key' - help_text = 'The API key used to query YouTube. Get one at https://console.developers.google.com/.' + name = "api_key" + default = "CHANGEME" + verbose_name = "YouTube API key" + help_text = "The API key used to query YouTube. Get one at https://console.developers.google.com/." widget = forms.PasswordInput - field_kwargs = { - 'required': False, - } + field_kwargs = {"required": False} diff --git a/api/funkwhale_api/providers/youtube/urls.py b/api/funkwhale_api/providers/youtube/urls.py index 243d2b852..c496a2c69 100644 --- a/api/funkwhale_api/providers/youtube/urls.py +++ b/api/funkwhale_api/providers/youtube/urls.py @@ -3,6 +3,6 @@ from .views import APISearch, APISearchs urlpatterns = [ - url(r'^search/$', APISearch.as_view(), name='search'), - url(r'^searchs/$', APISearchs.as_view(), name='searchs'), + url(r"^search/$", APISearch.as_view(), name="search"), + url(r"^searchs/$", APISearchs.as_view(), name="searchs"), ] diff --git a/api/funkwhale_api/providers/youtube/views.py b/api/funkwhale_api/providers/youtube/views.py index 989b33090..012b1825d 100644 --- a/api/funkwhale_api/providers/youtube/views.py +++ b/api/funkwhale_api/providers/youtube/views.py @@ -9,11 +9,8 @@ class APISearch(APIView): permission_classes = [ConditionalAuthentication] def get(self, request, *args, **kwargs): - results = client.search(request.GET['query']) - return Response([ - client.to_funkwhale(result) - for result in results - ]) + results = client.search(request.GET["query"]) + return Response([client.to_funkwhale(result) for result in results]) class APISearchs(APIView): @@ -21,10 +18,9 @@ class APISearchs(APIView): def post(self, request, *args, **kwargs): results = client.search_multiple(request.data) - return Response({ - key: [ - client.to_funkwhale(result) - for result in group - ] - for key, group in results.items() - }) + return Response( + { + key: [client.to_funkwhale(result) for result in group] + for key, group in results.items() + } + ) diff --git a/api/funkwhale_api/radios/admin.py b/api/funkwhale_api/radios/admin.py index 6d5abadaf..187950aeb 100644 --- a/api/funkwhale_api/radios/admin.py +++ b/api/funkwhale_api/radios/admin.py @@ -5,44 +5,28 @@ from . import models @admin.register(models.Radio) class RadioAdmin(admin.ModelAdmin): - list_display = [ - 'user', 'name', 'is_public', 'creation_date', 'config'] - list_select_related = [ - 'user', - ] - list_filter = [ - 'is_public', - ] - search_fields = ['name', 'description'] + list_display = ["user", "name", "is_public", "creation_date", "config"] + list_select_related = ["user"] + list_filter = ["is_public"] + search_fields = ["name", "description"] @admin.register(models.RadioSession) class RadioSessionAdmin(admin.ModelAdmin): list_display = [ - 'user', - 'custom_radio', - 'radio_type', - 'creation_date', - 'related_object'] + "user", + "custom_radio", + "radio_type", + "creation_date", + "related_object", + ] - list_select_related = [ - 'user', - 'custom_radio' - ] - list_filter = [ - 'radio_type', - ] + list_select_related = ["user", "custom_radio"] + list_filter = ["radio_type"] @admin.register(models.RadioSessionTrack) class RadioSessionTrackAdmin(admin.ModelAdmin): - list_display = [ - 'id', - 'session', - 'position', - 'track',] + list_display = ["id", "session", "position", "track"] - list_select_related = [ - 'track', - 'session' - ] + list_select_related = ["track", "session"] diff --git a/api/funkwhale_api/radios/factories.py b/api/funkwhale_api/radios/factories.py index 6a80323be..69d6ab6e8 100644 --- a/api/funkwhale_api/radios/factories.py +++ b/api/funkwhale_api/radios/factories.py @@ -6,13 +6,13 @@ from funkwhale_api.users.factories import UserFactory @registry.register class RadioFactory(factory.django.DjangoModelFactory): - name = factory.Faker('name') - description = factory.Faker('paragraphs') + name = factory.Faker("name") + description = factory.Faker("paragraphs") user = factory.SubFactory(UserFactory) config = [] class Meta: - model = 'radios.Radio' + model = "radios.Radio" @registry.register @@ -20,15 +20,16 @@ class RadioSessionFactory(factory.django.DjangoModelFactory): user = factory.SubFactory(UserFactory) class Meta: - model = 'radios.RadioSession' + model = "radios.RadioSession" -@registry.register(name='radios.CustomRadioSession') +@registry.register(name="radios.CustomRadioSession") class RadioSessionFactory(factory.django.DjangoModelFactory): user = factory.SubFactory(UserFactory) - radio_type = 'custom' + radio_type = "custom" custom_radio = factory.SubFactory( - RadioFactory, user=factory.SelfAttribute('..user')) + RadioFactory, user=factory.SelfAttribute("..user") + ) class Meta: - model = 'radios.RadioSession' + model = "radios.RadioSession" diff --git a/api/funkwhale_api/radios/filters.py b/api/funkwhale_api/radios/filters.py index d0d338d66..cb08e46ed 100644 --- a/api/funkwhale_api/radios/filters.py +++ b/api/funkwhale_api/radios/filters.py @@ -11,7 +11,6 @@ from funkwhale_api.taskapp.celery import require_instance class RadioFilterRegistry(persisting_theory.Registry): - def prepare_data(self, data): return data() @@ -20,31 +19,27 @@ class RadioFilterRegistry(persisting_theory.Registry): @property def exposed_filters(self): - return [ - f for f in self.values() if f.expose_in_api - ] + return [f for f in self.values() if f.expose_in_api] registry = RadioFilterRegistry() def run(filters, **kwargs): - candidates = kwargs.pop('candidates', models.Track.objects.all()) + candidates = kwargs.pop("candidates", models.Track.objects.all()) final_query = None - final_query = registry['group'].get_query( - candidates, filters=filters, **kwargs) + final_query = registry["group"].get_query(candidates, filters=filters, **kwargs) if final_query: candidates = candidates.filter(final_query) - return candidates.order_by('pk') + return candidates.order_by("pk") def validate(filter_config): try: - f = registry[filter_config['type']] + f = registry[filter_config["type"]] except KeyError: - raise ValidationError( - 'Invalid type "{}"'.format(filter_config['type'])) + raise ValidationError('Invalid type "{}"'.format(filter_config["type"])) f.validate(filter_config) return True @@ -53,28 +48,22 @@ def test(filter_config, **kwargs): """ Run validation and also gather the candidates for the given config """ - data = { - 'errors': [], - 'candidates': { - 'count': None, - 'sample': None, - } - } + data = {"errors": [], "candidates": {"count": None, "sample": None}} try: validate(filter_config) except ValidationError as e: - data['errors'] = [e.message] + data["errors"] = [e.message] return data candidates = run([filter_config], **kwargs) - data['candidates']['count'] = candidates.count() - data['candidates']['sample'] = candidates[:10] + data["candidates"]["count"] = candidates.count() + data["candidates"]["sample"] = candidates[:10] return data def clean_config(filter_config): - f = registry[filter_config['type']] + f = registry[filter_config["type"]] return f.clean_config(filter_config) @@ -91,74 +80,75 @@ class RadioFilter(object): return filter_config def validate(self, config): - operator = config.get('operator', 'and') + operator = config.get("operator", "and") try: - assert operator in ['or', 'and'] + assert operator in ["or", "and"] except AssertionError: - raise ValidationError( - 'Invalid operator "{}"'.format(config['operator'])) + raise ValidationError('Invalid operator "{}"'.format(config["operator"])) @registry.register class GroupFilter(RadioFilter): - code = 'group' + code = "group" expose_in_api = False + def get_query(self, candidates, filters, **kwargs): if not filters: return final_query = None for filter_config in filters: - f = registry[filter_config['type']] + f = registry[filter_config["type"]] conf = collections.ChainMap(filter_config, kwargs) query = f.get_query(candidates, **conf) - if filter_config.get('not', False): + if filter_config.get("not", False): query = ~query if not final_query: final_query = query else: - operator = filter_config.get('operator', 'and') - if operator == 'and': + operator = filter_config.get("operator", "and") + if operator == "and": final_query &= query - elif operator == 'or': + elif operator == "or": final_query |= query else: - raise ValueError( - 'Invalid query operator "{}"'.format(operator)) + raise ValueError('Invalid query operator "{}"'.format(operator)) return final_query def validate(self, config): super().validate(config) - for fc in config['filters']: - registry[fc['type']].validate(fc) + for fc in config["filters"]: + registry[fc["type"]].validate(fc) @registry.register class ArtistFilter(RadioFilter): - code = 'artist' - label = 'Artist' - help_text = 'Select tracks for a given artist' + code = "artist" + label = "Artist" + help_text = "Select tracks for a given artist" fields = [ { - 'name': 'ids', - 'type': 'list', - 'subtype': 'number', - 'autocomplete': reverse_lazy('api:v1:artists-list'), - 'autocomplete_qs': 'q={query}', - 'autocomplete_fields': {'name': 'name', 'value': 'id'}, - 'label': 'Artist', - 'placeholder': 'Select artists' + "name": "ids", + "type": "list", + "subtype": "number", + "autocomplete": reverse_lazy("api:v1:artists-list"), + "autocomplete_qs": "q={query}", + "autocomplete_fields": {"name": "name", "value": "id"}, + "label": "Artist", + "placeholder": "Select artists", } ] def clean_config(self, filter_config): filter_config = super().clean_config(filter_config) - filter_config['ids'] = sorted(filter_config['ids']) - names = models.Artist.objects.filter( - pk__in=filter_config['ids'] - ).order_by('id').values_list('name', flat=True) - filter_config['names'] = list(names) + filter_config["ids"] = sorted(filter_config["ids"]) + names = ( + models.Artist.objects.filter(pk__in=filter_config["ids"]) + .order_by("id") + .values_list("name", flat=True) + ) + filter_config["names"] = list(names) return filter_config def get_query(self, candidates, ids, **kwargs): @@ -167,35 +157,39 @@ class ArtistFilter(RadioFilter): def validate(self, config): super().validate(config) try: - pks = models.Artist.objects.filter( - pk__in=config['ids']).values_list('pk', flat=True) - diff = set(config['ids']) - set(pks) + pks = models.Artist.objects.filter(pk__in=config["ids"]).values_list( + "pk", flat=True + ) + diff = set(config["ids"]) - set(pks) assert len(diff) == 0 except KeyError: - raise ValidationError('You must provide an id') + raise ValidationError("You must provide an id") except AssertionError: - raise ValidationError( - 'No artist matching ids "{}"'.format(diff)) + raise ValidationError('No artist matching ids "{}"'.format(diff)) @registry.register class TagFilter(RadioFilter): - code = 'tag' + code = "tag" fields = [ { - 'name': 'names', - 'type': 'list', - 'subtype': 'string', - 'autocomplete': reverse_lazy('api:v1:tags-list'), - 'autocomplete_qs': '', - 'autocomplete_fields': {'remoteValues': 'results', 'name': 'name', 'value': 'slug'}, - 'autocomplete_qs': 'query={query}', - 'label': 'Tags', - 'placeholder': 'Select tags' + "name": "names", + "type": "list", + "subtype": "string", + "autocomplete": reverse_lazy("api:v1:tags-list"), + "autocomplete_qs": "", + "autocomplete_fields": { + "remoteValues": "results", + "name": "name", + "value": "slug", + }, + "autocomplete_qs": "query={query}", + "label": "Tags", + "placeholder": "Select tags", } ] - help_text = 'Select tracks with a given tag' - label = 'Tag' + help_text = "Select tracks with a given tag" + label = "Tag" def get_query(self, candidates, names, **kwargs): return Q(tags__slug__in=names) diff --git a/api/funkwhale_api/radios/filtersets.py b/api/funkwhale_api/radios/filtersets.py index 49f471373..d8d7c9ed0 100644 --- a/api/funkwhale_api/radios/filtersets.py +++ b/api/funkwhale_api/radios/filtersets.py @@ -4,9 +4,6 @@ from . import models class RadioFilter(django_filters.FilterSet): - class Meta: model = models.Radio - fields = { - 'name': ['exact', 'iexact', 'startswith', 'icontains'] - } + fields = {"name": ["exact", "iexact", "startswith", "icontains"]} diff --git a/api/funkwhale_api/radios/migrations/0001_initial.py b/api/funkwhale_api/radios/migrations/0001_initial.py index 46faf749e..912da7be3 100644 --- a/api/funkwhale_api/radios/migrations/0001_initial.py +++ b/api/funkwhale_api/radios/migrations/0001_initial.py @@ -10,33 +10,72 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('music', '0004_track_tags'), + ("music", "0004_track_tags"), ] operations = [ migrations.CreateModel( - name='RadioSession', + name="RadioSession", fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('radio_type', models.CharField(max_length=50)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('user', models.ForeignKey(related_name='radio_sessions', blank=True, to=settings.AUTH_USER_MODEL, null=True, on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + primary_key=True, + serialize=False, + auto_created=True, + ), + ), + ("radio_type", models.CharField(max_length=50)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "user", + models.ForeignKey( + related_name="radio_sessions", + blank=True, + to=settings.AUTH_USER_MODEL, + null=True, + on_delete=models.CASCADE, + ), + ), ], ), migrations.CreateModel( - name='RadioSessionTrack', + name="RadioSessionTrack", fields=[ - ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, auto_created=True)), - ('position', models.IntegerField(default=1)), - ('session', models.ForeignKey(to='radios.RadioSession', related_name='session_tracks', on_delete=models.CASCADE)), - ('track', models.ForeignKey(to='music.Track', related_name='radio_session_tracks', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + primary_key=True, + serialize=False, + auto_created=True, + ), + ), + ("position", models.IntegerField(default=1)), + ( + "session", + models.ForeignKey( + to="radios.RadioSession", + related_name="session_tracks", + on_delete=models.CASCADE, + ), + ), + ( + "track", + models.ForeignKey( + to="music.Track", + related_name="radio_session_tracks", + on_delete=models.CASCADE, + ), + ), ], - options={ - 'ordering': ('session', 'position'), - }, + options={"ordering": ("session", "position")}, ), migrations.AlterUniqueTogether( - name='radiosessiontrack', - unique_together=set([('session', 'position')]), + name="radiosessiontrack", unique_together=set([("session", "position")]) ), ] diff --git a/api/funkwhale_api/radios/migrations/0002_radiosession_session_key.py b/api/funkwhale_api/radios/migrations/0002_radiosession_session_key.py index a903ae3ea..6c206aa62 100644 --- a/api/funkwhale_api/radios/migrations/0002_radiosession_session_key.py +++ b/api/funkwhale_api/radios/migrations/0002_radiosession_session_key.py @@ -6,14 +6,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('radios', '0001_initial'), - ] + dependencies = [("radios", "0001_initial")] operations = [ migrations.AddField( - model_name='radiosession', - name='session_key', + model_name="radiosession", + name="session_key", field=models.CharField(null=True, blank=True, max_length=100), - ), + ) ] diff --git a/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py b/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py index 7c70abc2e..2af084a87 100644 --- a/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py +++ b/api/funkwhale_api/radios/migrations/0003_auto_20160521_1708.py @@ -7,19 +7,24 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('radios', '0002_radiosession_session_key'), + ("contenttypes", "0002_remove_content_type_name"), + ("radios", "0002_radiosession_session_key"), ] operations = [ migrations.AddField( - model_name='radiosession', - name='related_object_content_type', - field=models.ForeignKey(null=True, to='contenttypes.ContentType', blank=True, on_delete=models.CASCADE), + model_name="radiosession", + name="related_object_content_type", + field=models.ForeignKey( + null=True, + to="contenttypes.ContentType", + blank=True, + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='radiosession', - name='related_object_id', + model_name="radiosession", + name="related_object_id", field=models.PositiveIntegerField(blank=True, null=True), ), ] diff --git a/api/funkwhale_api/radios/migrations/0004_auto_20180107_1813.py b/api/funkwhale_api/radios/migrations/0004_auto_20180107_1813.py index fc768b303..72f2a7d31 100644 --- a/api/funkwhale_api/radios/migrations/0004_auto_20180107_1813.py +++ b/api/funkwhale_api/radios/migrations/0004_auto_20180107_1813.py @@ -11,26 +11,52 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('radios', '0003_auto_20160521_1708'), + ("radios", "0003_auto_20160521_1708"), ] operations = [ migrations.CreateModel( - name='Radio', + name="Radio", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('description', models.TextField(blank=True)), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('is_public', models.BooleanField(default=False)), - ('version', models.PositiveIntegerField(default=0)), - ('config', django.contrib.postgres.fields.jsonb.JSONField()), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='radios', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=100)), + ("description", models.TextField(blank=True)), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("is_public", models.BooleanField(default=False)), + ("version", models.PositiveIntegerField(default=0)), + ("config", django.contrib.postgres.fields.jsonb.JSONField()), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="radios", + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.AddField( - model_name='radiosession', - name='custom_radio', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sessions', to='radios.Radio'), + model_name="radiosession", + name="custom_radio", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="sessions", + to="radios.Radio", + ), ), ] diff --git a/api/funkwhale_api/radios/models.py b/api/funkwhale_api/radios/models.py index 8758abc61..af0fa26da 100644 --- a/api/funkwhale_api/radios/models.py +++ b/api/funkwhale_api/radios/models.py @@ -14,11 +14,12 @@ from . import filters class Radio(models.Model): CONFIG_VERSION = 0 user = models.ForeignKey( - 'users.User', - related_name='radios', + "users.User", + related_name="radios", null=True, blank=True, - on_delete=models.CASCADE) + on_delete=models.CASCADE, + ) name = models.CharField(max_length=100) description = models.TextField(blank=True) creation_date = models.DateTimeField(default=timezone.now) @@ -32,27 +33,25 @@ class Radio(models.Model): class RadioSession(models.Model): user = models.ForeignKey( - 'users.User', - related_name='radio_sessions', + "users.User", + related_name="radio_sessions", null=True, blank=True, - on_delete=models.CASCADE) + on_delete=models.CASCADE, + ) session_key = models.CharField(max_length=100, null=True, blank=True) radio_type = models.CharField(max_length=50) custom_radio = models.ForeignKey( - Radio, - related_name='sessions', - null=True, - blank=True, - on_delete=models.CASCADE) + Radio, related_name="sessions", null=True, blank=True, on_delete=models.CASCADE + ) creation_date = models.DateTimeField(default=timezone.now) related_object_content_type = models.ForeignKey( - ContentType, - blank=True, - null=True, - on_delete=models.CASCADE) + ContentType, blank=True, null=True, on_delete=models.CASCADE + ) related_object_id = models.PositiveIntegerField(blank=True, null=True) - related_object = GenericForeignKey('related_object_content_type', 'related_object_id') + related_object = GenericForeignKey( + "related_object_content_type", "related_object_id" + ) def save(self, **kwargs): self.radio.clean(self) @@ -62,14 +61,16 @@ class RadioSession(models.Model): def next_position(self): next_position = 1 - last_session_track = self.session_tracks.all().order_by('-position').first() + last_session_track = self.session_tracks.all().order_by("-position").first() if last_session_track: next_position = last_session_track.position + 1 return next_position def add(self, track): - new_session_track = RadioSessionTrack.objects.create(track=track, session=self, position=self.next_position) + new_session_track = RadioSessionTrack.objects.create( + track=track, session=self, position=self.next_position + ) return new_session_track @@ -77,16 +78,19 @@ class RadioSession(models.Model): def radio(self): from .registries import registry from . import radios + return registry[self.radio_type](session=self) class RadioSessionTrack(models.Model): session = models.ForeignKey( - RadioSession, related_name='session_tracks', on_delete=models.CASCADE) + RadioSession, related_name="session_tracks", on_delete=models.CASCADE + ) position = models.IntegerField(default=1) track = models.ForeignKey( - Track, related_name='radio_session_tracks', on_delete=models.CASCADE) + Track, related_name="radio_session_tracks", on_delete=models.CASCADE + ) class Meta: - ordering = ('session', 'position') - unique_together = ('session', 'position') + ordering = ("session", "position") + unique_together = ("session", "position") diff --git a/api/funkwhale_api/radios/radios.py b/api/funkwhale_api/radios/radios.py index 0d045ea4d..f876d03b2 100644 --- a/api/funkwhale_api/radios/radios.py +++ b/api/funkwhale_api/radios/radios.py @@ -12,7 +12,6 @@ from .registries import registry class SimpleRadio(object): - def clean(self, instance): return @@ -37,13 +36,13 @@ class SessionRadio(SimpleRadio): self.session = session def start_session(self, user, **kwargs): - self.session = models.RadioSession.objects.create(user=user, radio_type=self.radio_type, **kwargs) + self.session = models.RadioSession.objects.create( + user=user, radio_type=self.radio_type, **kwargs + ) return self.session def get_queryset(self, **kwargs): - qs = Track.objects.annotate( - files_count=Count('files') - ) + qs = Track.objects.annotate(files_count=Count("files")) return qs.filter(files_count__gt=0) def get_queryset_kwargs(self): @@ -57,7 +56,9 @@ class SessionRadio(SimpleRadio): return queryset def filter_from_session(self, queryset): - already_played = self.session.session_tracks.all().values_list('track', flat=True) + already_played = self.session.session_tracks.all().values_list( + "track", flat=True + ) queryset = queryset.exclude(pk__in=already_played) return queryset @@ -76,60 +77,51 @@ class SessionRadio(SimpleRadio): return data -@registry.register(name='random') +@registry.register(name="random") class RandomRadio(SessionRadio): def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs) - return qs.order_by('?') + return qs.order_by("?") -@registry.register(name='favorites') +@registry.register(name="favorites") class FavoritesRadio(SessionRadio): - def get_queryset_kwargs(self): kwargs = super().get_queryset_kwargs() if self.session: - kwargs['user'] = self.session.user + kwargs["user"] = self.session.user return kwargs def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs) - track_ids = kwargs['user'].track_favorites.all().values_list('track', flat=True) + track_ids = kwargs["user"].track_favorites.all().values_list("track", flat=True) return qs.filter(pk__in=track_ids) -@registry.register(name='custom') +@registry.register(name="custom") class CustomRadio(SessionRadio): - def get_queryset_kwargs(self): kwargs = super().get_queryset_kwargs() - kwargs['user'] = self.session.user - kwargs['custom_radio'] = self.session.custom_radio + kwargs["user"] = self.session.user + kwargs["custom_radio"] = self.session.custom_radio return kwargs def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs) - return filters.run( - kwargs['custom_radio'].config, - candidates=qs, - ) + return filters.run(kwargs["custom_radio"].config, candidates=qs) def validate_session(self, data, **context): data = super().validate_session(data, **context) try: - user = data['user'] + user = data["user"] except KeyError: - user = context['user'] + user = context["user"] try: - assert ( - data['custom_radio'].user == user or - data['custom_radio'].is_public) + assert data["custom_radio"].user == user or data["custom_radio"].is_public except KeyError: - raise serializers.ValidationError( - 'You must provide a custom radio') + raise serializers.ValidationError("You must provide a custom radio") except AssertionError: - raise serializers.ValidationError( - "You don't have access to this radio") + raise serializers.ValidationError("You don't have access to this radio") return data @@ -139,15 +131,17 @@ class RelatedObjectRadio(SessionRadio): def clean(self, instance): super().clean(instance) if not instance.related_object: - raise ValidationError('Cannot start RelatedObjectRadio without related object') + raise ValidationError( + "Cannot start RelatedObjectRadio without related object" + ) if not isinstance(instance.related_object, self.model): - raise ValidationError('Trying to start radio with bad related object') + raise ValidationError("Trying to start radio with bad related object") def get_related_object(self, pk): return self.model.objects.get(pk=pk) -@registry.register(name='tag') +@registry.register(name="tag") class TagRadio(RelatedObjectRadio): model = Tag @@ -155,7 +149,8 @@ class TagRadio(RelatedObjectRadio): qs = super().get_queryset(**kwargs) return Track.objects.filter(tags__in=[self.session.related_object]) -@registry.register(name='artist') + +@registry.register(name="artist") class ArtistRadio(RelatedObjectRadio): model = Artist @@ -164,7 +159,7 @@ class ArtistRadio(RelatedObjectRadio): return qs.filter(artist=self.session.related_object) -@registry.register(name='less-listened') +@registry.register(name="less-listened") class LessListenedRadio(RelatedObjectRadio): model = User @@ -174,5 +169,5 @@ class LessListenedRadio(RelatedObjectRadio): def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs) - listened = self.session.user.listenings.all().values_list('track', flat=True) - return qs.exclude(pk__in=listened).order_by('?') + listened = self.session.user.listenings.all().values_list("track", flat=True) + return qs.exclude(pk__in=listened).order_by("?") diff --git a/api/funkwhale_api/radios/registries.py b/api/funkwhale_api/radios/registries.py index eec223539..4a30102b7 100644 --- a/api/funkwhale_api/radios/registries.py +++ b/api/funkwhale_api/radios/registries.py @@ -1,8 +1,10 @@ import persisting_theory + class RadioRegistry(persisting_theory.Registry): def prepare_name(self, data, name=None): - setattr(data, 'radio_type', name) + setattr(data, "radio_type", name) return name -registry = RadioRegistry() + +registry = RadioRegistry() diff --git a/api/funkwhale_api/radios/serializers.py b/api/funkwhale_api/radios/serializers.py index 8c59f8715..be60bbc3e 100644 --- a/api/funkwhale_api/radios/serializers.py +++ b/api/funkwhale_api/radios/serializers.py @@ -9,7 +9,7 @@ from .radios import registry class FilterSerializer(serializers.Serializer): - type = serializers.CharField(source='code') + type = serializers.CharField(source="code") label = serializers.CharField() help_text = serializers.CharField() fields = serializers.ReadOnlyField() @@ -21,19 +21,20 @@ class RadioSerializer(serializers.ModelSerializer): class Meta: model = models.Radio fields = ( - 'id', - 'is_public', - 'name', - 'creation_date', - 'user', - 'config', - 'description') - read_only_fields = ('user', 'creation_date') + "id", + "is_public", + "name", + "creation_date", + "user", + "config", + "description", + ) + read_only_fields = ("user", "creation_date") def save(self, **kwargs): - kwargs['config'] = [ - filters.registry[f['type']].clean_config(f) - for f in self.validated_data['config'] + kwargs["config"] = [ + filters.registry[f["type"]].clean_config(f) + for f in self.validated_data["config"] ] return super().save(**kwargs) @@ -42,7 +43,7 @@ class RadioSerializer(serializers.ModelSerializer): class RadioSessionTrackSerializerCreate(serializers.ModelSerializer): class Meta: model = models.RadioSessionTrack - fields = ('session',) + fields = ("session",) class RadioSessionTrackSerializer(serializers.ModelSerializer): @@ -50,28 +51,30 @@ class RadioSessionTrackSerializer(serializers.ModelSerializer): class Meta: model = models.RadioSessionTrack - fields = ('id', 'session', 'position', 'track') + fields = ("id", "session", "position", "track") class RadioSessionSerializer(serializers.ModelSerializer): class Meta: model = models.RadioSession fields = ( - 'id', - 'radio_type', - 'related_object_id', - 'user', - 'creation_date', - 'custom_radio', + "id", + "radio_type", + "related_object_id", + "user", + "creation_date", + "custom_radio", ) def validate(self, data): - registry[data['radio_type']]().validate_session(data, **self.context) + registry[data["radio_type"]]().validate_session(data, **self.context) return data def create(self, validated_data): - validated_data['user'] = self.context['user'] - if validated_data.get('related_object_id'): - radio = registry[validated_data['radio_type']]() - validated_data['related_object'] = radio.get_related_object(validated_data['related_object_id']) + validated_data["user"] = self.context["user"] + if validated_data.get("related_object_id"): + radio = registry[validated_data["radio_type"]]() + validated_data["related_object"] = radio.get_related_object( + validated_data["related_object_id"] + ) return super().create(validated_data) diff --git a/api/funkwhale_api/radios/urls.py b/api/funkwhale_api/radios/urls.py index d84615ca5..3f48455b0 100644 --- a/api/funkwhale_api/radios/urls.py +++ b/api/funkwhale_api/radios/urls.py @@ -2,10 +2,11 @@ from django.conf.urls import include, url from . import views from rest_framework import routers + router = routers.SimpleRouter() -router.register(r'sessions', views.RadioSessionViewSet, 'sessions') -router.register(r'radios', views.RadioViewSet, 'radios') -router.register(r'tracks', views.RadioSessionTrackViewSet, 'tracks') +router.register(r"sessions", views.RadioSessionViewSet, "sessions") +router.register(r"radios", views.RadioViewSet, "radios") +router.register(r"tracks", views.RadioSessionTrackViewSet, "tracks") urlpatterns = router.urls diff --git a/api/funkwhale_api/radios/views.py b/api/funkwhale_api/radios/views.py index ca510b82c..1d933a293 100644 --- a/api/funkwhale_api/radios/views.py +++ b/api/funkwhale_api/radios/views.py @@ -17,12 +17,13 @@ from . import serializers class RadioViewSet( - mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.ListModelMixin, - mixins.DestroyModelMixin, - viewsets.GenericViewSet): + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.ListModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): serializer_class = serializers.RadioSerializer permission_classes = [permissions.IsAuthenticated] @@ -42,7 +43,7 @@ class RadioViewSet( raise Http404 return serializer.save(user=self.request.user) - @detail_route(methods=['get']) + @detail_route(methods=["get"]) def tracks(self, request, *args, **kwargs): radio = self.get_object() tracks = radio.get_candidates().for_nested_serialization() @@ -52,36 +53,33 @@ class RadioViewSet( serializer = TrackSerializer(page, many=True) return self.get_paginated_response(serializer.data) - @list_route(methods=['get']) + @list_route(methods=["get"]) def filters(self, request, *args, **kwargs): serializer = serializers.FilterSerializer( - filters.registry.exposed_filters, many=True) + filters.registry.exposed_filters, many=True + ) return Response(serializer.data) - @list_route(methods=['post']) + @list_route(methods=["post"]) def validate(self, request, *args, **kwargs): try: - f_list = request.data['filters'] + f_list = request.data["filters"] except KeyError: - return Response( - {'error': 'You must provide a filters list'}, status=400) - data = { - 'filters': [] - } + return Response({"error": "You must provide a filters list"}, status=400) + data = {"filters": []} for f in f_list: results = filters.test(f) - if results['candidates']['sample']: - qs = results['candidates']['sample'].for_nested_serialization() - results['candidates']['sample'] = TrackSerializer( - qs, many=True).data - data['filters'].append(results) + if results["candidates"]["sample"]: + qs = results["candidates"]["sample"].for_nested_serialization() + results["candidates"]["sample"] = TrackSerializer(qs, many=True).data + data["filters"].append(results) return Response(data) -class RadioSessionViewSet(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - viewsets.GenericViewSet): +class RadioSessionViewSet( + mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet +): serializer_class = serializers.RadioSessionSerializer queryset = models.RadioSession.objects.all() @@ -93,12 +91,11 @@ class RadioSessionViewSet(mixins.CreateModelMixin, def get_serializer_context(self): context = super().get_serializer_context() - context['user'] = self.request.user + context["user"] = self.request.user return context -class RadioSessionTrackViewSet(mixins.CreateModelMixin, - viewsets.GenericViewSet): +class RadioSessionTrackViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): serializer_class = serializers.RadioSessionTrackSerializer queryset = models.RadioSessionTrack.objects.all() permission_classes = [permissions.IsAuthenticated] @@ -106,20 +103,24 @@ class RadioSessionTrackViewSet(mixins.CreateModelMixin, def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - session = serializer.validated_data['session'] + session = serializer.validated_data["session"] try: assert request.user == session.user except AssertionError: return Response(status=status.HTTP_403_FORBIDDEN) track = session.radio.pick() - session_track = session.session_tracks.all().latest('id') + session_track = session.session_tracks.all().latest("id") # self.perform_create(serializer) # dirty override here, since we use a different serializer for creation and detail - serializer = self.serializer_class(instance=session_track, context=self.get_serializer_context()) + serializer = self.serializer_class( + instance=session_track, context=self.get_serializer_context() + ) headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + return Response( + serializer.data, status=status.HTTP_201_CREATED, headers=headers + ) def get_serializer_class(self, *args, **kwargs): - if self.action == 'create': + if self.action == "create": return serializers.RadioSessionTrackSerializerCreate return super().get_serializer_class(*args, **kwargs) diff --git a/api/funkwhale_api/requests/admin.py b/api/funkwhale_api/requests/admin.py index 8ca008a03..b0f1a7990 100644 --- a/api/funkwhale_api/requests/admin.py +++ b/api/funkwhale_api/requests/admin.py @@ -5,11 +5,7 @@ from . import models @admin.register(models.ImportRequest) class ImportRequestAdmin(admin.ModelAdmin): - list_display = ['artist_name', 'user', 'status', 'creation_date'] - list_select_related = [ - 'user' - ] - list_filter = [ - 'status', - ] - search_fields = ['artist_name', 'comment', 'albums'] + list_display = ["artist_name", "user", "status", "creation_date"] + list_select_related = ["user"] + list_filter = ["status"] + search_fields = ["artist_name", "comment", "albums"] diff --git a/api/funkwhale_api/requests/api_urls.py b/api/funkwhale_api/requests/api_urls.py index 37459a664..c7c1a103b 100644 --- a/api/funkwhale_api/requests/api_urls.py +++ b/api/funkwhale_api/requests/api_urls.py @@ -2,10 +2,8 @@ from django.conf.urls import include, url from . import views from rest_framework import routers + router = routers.SimpleRouter() -router.register( - r'import-requests', - views.ImportRequestViewSet, - 'import-requests') +router.register(r"import-requests", views.ImportRequestViewSet, "import-requests") urlpatterns = router.urls diff --git a/api/funkwhale_api/requests/factories.py b/api/funkwhale_api/requests/factories.py index 2bcdeb6a9..d6673aebd 100644 --- a/api/funkwhale_api/requests/factories.py +++ b/api/funkwhale_api/requests/factories.py @@ -6,10 +6,10 @@ from funkwhale_api.users.factories import UserFactory @registry.register class ImportRequestFactory(factory.django.DjangoModelFactory): - artist_name = factory.Faker('name') - albums = factory.Faker('sentence') + artist_name = factory.Faker("name") + albums = factory.Faker("sentence") user = factory.SubFactory(UserFactory) - comment = factory.Faker('paragraph') + comment = factory.Faker("paragraph") class Meta: - model = 'requests.ImportRequest' + model = "requests.ImportRequest" diff --git a/api/funkwhale_api/requests/filters.py b/api/funkwhale_api/requests/filters.py index 7d0603362..ad8b000d2 100644 --- a/api/funkwhale_api/requests/filters.py +++ b/api/funkwhale_api/requests/filters.py @@ -6,17 +6,14 @@ from . import models class ImportRequestFilter(django_filters.FilterSet): - q = fields.SearchFilter(search_fields=[ - 'artist_name', - 'user__username', - 'albums', - 'comment', - ]) + q = fields.SearchFilter( + search_fields=["artist_name", "user__username", "albums", "comment"] + ) class Meta: model = models.ImportRequest fields = { - 'artist_name': ['exact', 'iexact', 'startswith', 'icontains'], - 'status': ['exact'], - 'user__username': ['exact'], + "artist_name": ["exact", "iexact", "startswith", "icontains"], + "status": ["exact"], + "user__username": ["exact"], } diff --git a/api/funkwhale_api/requests/migrations/0001_initial.py b/api/funkwhale_api/requests/migrations/0001_initial.py index 7c239b3c0..ab9b619ef 100644 --- a/api/funkwhale_api/requests/migrations/0001_initial.py +++ b/api/funkwhale_api/requests/migrations/0001_initial.py @@ -10,22 +10,50 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] + dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] operations = [ migrations.CreateModel( - name='ImportRequest', + name="ImportRequest", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creation_date', models.DateTimeField(default=django.utils.timezone.now)), - ('imported_date', models.DateTimeField(blank=True, null=True)), - ('artist_name', models.CharField(max_length=250)), - ('albums', models.CharField(blank=True, max_length=3000, null=True)), - ('status', models.CharField(choices=[('pending', 'pending'), ('accepted', 'accepted'), ('imported', 'imported'), ('closed', 'closed')], default='pending', max_length=50)), - ('comment', models.TextField(blank=True, max_length=3000, null=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='import_requests', to=settings.AUTH_USER_MODEL)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "creation_date", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("imported_date", models.DateTimeField(blank=True, null=True)), + ("artist_name", models.CharField(max_length=250)), + ("albums", models.CharField(blank=True, max_length=3000, null=True)), + ( + "status", + models.CharField( + choices=[ + ("pending", "pending"), + ("accepted", "accepted"), + ("imported", "imported"), + ("closed", "closed"), + ], + default="pending", + max_length=50, + ), + ), + ("comment", models.TextField(blank=True, max_length=3000, null=True)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="import_requests", + to=settings.AUTH_USER_MODEL, + ), + ), ], - ), + ) ] diff --git a/api/funkwhale_api/requests/models.py b/api/funkwhale_api/requests/models.py index d08dd4004..650d50240 100644 --- a/api/funkwhale_api/requests/models.py +++ b/api/funkwhale_api/requests/models.py @@ -2,17 +2,13 @@ from django.db import models from django.utils import timezone -NATURE_CHOICES = [ - ('artist', 'artist'), - ('album', 'album'), - ('track', 'track'), -] +NATURE_CHOICES = [("artist", "artist"), ("album", "album"), ("track", "track")] STATUS_CHOICES = [ - ('pending', 'pending'), - ('accepted', 'accepted'), - ('imported', 'imported'), - ('closed', 'closed'), + ("pending", "pending"), + ("accepted", "accepted"), + ("imported", "imported"), + ("closed", "closed"), ] @@ -20,11 +16,9 @@ class ImportRequest(models.Model): creation_date = models.DateTimeField(default=timezone.now) imported_date = models.DateTimeField(null=True, blank=True) user = models.ForeignKey( - 'users.User', - related_name='import_requests', - on_delete=models.CASCADE) + "users.User", related_name="import_requests", on_delete=models.CASCADE + ) artist_name = models.CharField(max_length=250) albums = models.CharField(max_length=3000, null=True, blank=True) - status = models.CharField( - choices=STATUS_CHOICES, max_length=50, default='pending') + status = models.CharField(choices=STATUS_CHOICES, max_length=50, default="pending") comment = models.TextField(null=True, blank=True, max_length=3000) diff --git a/api/funkwhale_api/requests/serializers.py b/api/funkwhale_api/requests/serializers.py index 51a709514..2a810a999 100644 --- a/api/funkwhale_api/requests/serializers.py +++ b/api/funkwhale_api/requests/serializers.py @@ -11,20 +11,17 @@ class ImportRequestSerializer(serializers.ModelSerializer): class Meta: model = models.ImportRequest fields = ( - 'id', - 'status', - 'albums', - 'artist_name', - 'user', - 'creation_date', - 'imported_date', - 'comment') - read_only_fields = ( - 'creation_date', - 'imported_date', - 'user', - 'status') + "id", + "status", + "albums", + "artist_name", + "user", + "creation_date", + "imported_date", + "comment", + ) + read_only_fields = ("creation_date", "imported_date", "user", "status") def create(self, validated_data): - validated_data['user'] = self.context['user'] + validated_data["user"] = self.context["user"] return super().create(validated_data) diff --git a/api/funkwhale_api/requests/views.py b/api/funkwhale_api/requests/views.py index 6553f3316..4b68555a8 100644 --- a/api/funkwhale_api/requests/views.py +++ b/api/funkwhale_api/requests/views.py @@ -9,18 +9,18 @@ from . import serializers class ImportRequestViewSet( - mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet, +): serializer_class = serializers.ImportRequestSerializer queryset = ( - models.ImportRequest.objects.all() - .select_related() - .order_by('-creation_date')) + models.ImportRequest.objects.all().select_related().order_by("-creation_date") + ) filter_class = filters.ImportRequestFilter - ordering_fields = ('id', 'artist_name', 'creation_date', 'status') + ordering_fields = ("id", "artist_name", "creation_date", "status") def perform_create(self, serializer): return serializer.save(user=self.request.user) @@ -28,5 +28,5 @@ class ImportRequestViewSet( def get_serializer_context(self): context = super().get_serializer_context() if self.request.user.is_authenticated: - context['user'] = self.request.user + context["user"] = self.request.user return context diff --git a/api/funkwhale_api/subsonic/authentication.py b/api/funkwhale_api/subsonic/authentication.py index fe9b08dc8..808e5e5ec 100644 --- a/api/funkwhale_api/subsonic/authentication.py +++ b/api/funkwhale_api/subsonic/authentication.py @@ -10,23 +10,20 @@ 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')) + 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') + 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.' + username=username, is_active=True, subsonic_api_token=password ) + except (User.DoesNotExist, binascii.Error): + raise exceptions.AuthenticationFailed("Wrong username or password.") return (user, None) @@ -34,18 +31,13 @@ def authenticate(username, password): 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.' + 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.' - ) + raise exceptions.AuthenticationFailed("Wrong username or password.") return (user, None) @@ -53,15 +45,15 @@ def authenticate_salt(username, salt, token): class SubsonicAuthentication(authentication.BaseAuthentication): def authenticate(self, request): data = request.GET or request.POST - username = data.get('u') + username = data.get("u") if not username: return None - p = data.get('p') - s = data.get('s') - t = data.get('t') + 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') + raise exceptions.AuthenticationFailed("Missing credentials") if p: return authenticate(username, p) diff --git a/api/funkwhale_api/subsonic/dynamic_preferences_registry.py b/api/funkwhale_api/subsonic/dynamic_preferences_registry.py index 93482702f..0916b0a3e 100644 --- a/api/funkwhale_api/subsonic/dynamic_preferences_registry.py +++ b/api/funkwhale_api/subsonic/dynamic_preferences_registry.py @@ -3,20 +3,20 @@ from dynamic_preferences.registries import global_preferences_registry from funkwhale_api.common import preferences -subsonic = types.Section('subsonic') +subsonic = types.Section("subsonic") @global_preferences_registry.register class APIAutenticationRequired(types.BooleanPreference): section = subsonic show_in_api = True - name = 'enabled' + name = "enabled" default = True - verbose_name = 'Enabled Subsonic API' + verbose_name = "Enabled Subsonic API" help_text = ( - 'Funkwhale supports a subset of the Subsonic API, that makes ' - 'it compatible with existing clients such as DSub for Android ' - 'or Clementine for desktop. However, Subsonic protocol is less ' - 'than ideal in terms of security and you can disable this feature ' - 'completely using this flag.' + "Funkwhale supports a subset of the Subsonic API, that makes " + "it compatible with existing clients such as DSub for Android " + "or Clementine for desktop. However, Subsonic protocol is less " + "than ideal in terms of security and you can disable this feature " + "completely using this flag." ) diff --git a/api/funkwhale_api/subsonic/filters.py b/api/funkwhale_api/subsonic/filters.py index b7b639fac..a354e23f1 100644 --- a/api/funkwhale_api/subsonic/filters.py +++ b/api/funkwhale_api/subsonic/filters.py @@ -4,18 +4,18 @@ from funkwhale_api.music import models as music_models class AlbumList2FilterSet(filters.FilterSet): - type = filters.CharFilter(name='_', method='filter_type') + type = filters.CharFilter(name="_", method="filter_type") class Meta: model = music_models.Album - fields = ['type'] + fields = ["type"] def filter_type(self, queryset, name, value): ORDERING = { - 'random': '?', - 'newest': '-creation_date', - 'alphabeticalByArtist': 'artist__name', - 'alphabeticalByName': 'title', + "random": "?", + "newest": "-creation_date", + "alphabeticalByArtist": "artist__name", + "alphabeticalByName": "title", } if value not in ORDERING: return queryset diff --git a/api/funkwhale_api/subsonic/negotiation.py b/api/funkwhale_api/subsonic/negotiation.py index 3335fda45..2b46b1753 100644 --- a/api/funkwhale_api/subsonic/negotiation.py +++ b/api/funkwhale_api/subsonic/negotiation.py @@ -5,8 +5,8 @@ from . import renderers MAPPING = { - 'json': (renderers.SubsonicJSONRenderer(), 'application/json'), - 'xml': (renderers.SubsonicXMLRenderer(), 'text/xml'), + "json": (renderers.SubsonicJSONRenderer(), "application/json"), + "xml": (renderers.SubsonicXMLRenderer(), "text/xml"), } @@ -14,7 +14,7 @@ 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') + requested_format = data.get("f", "xml") try: return MAPPING[requested_format] except KeyError: diff --git a/api/funkwhale_api/subsonic/renderers.py b/api/funkwhale_api/subsonic/renderers.py index 3a5664501..fac12d6c1 100644 --- a/api/funkwhale_api/subsonic/renderers.py +++ b/api/funkwhale_api/subsonic/renderers.py @@ -8,37 +8,34 @@ class SubsonicJSONRenderer(renderers.JSONRenderer): 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) - if 'error' in final: + final = {"subsonic-response": {"status": "ok", "version": "1.16.0"}} + final["subsonic-response"].update(data) + if "error" in final: # an error was returned - final['subsonic-response']['status'] = 'failed' + final["subsonic-response"]["status"] = "failed" return super().render(final, accepted_media_type, renderer_context) class SubsonicXMLRenderer(renderers.JSONRenderer): - media_type = 'text/xml' + 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', + "xmlns": "http://subsonic.org/restapi", + "status": "ok", + "version": "1.16.0", } final.update(data) - if 'error' in final: + if "error" in final: # an error was returned - final['status'] = 'failed' - tree = dict_to_xml_tree('subsonic-response', final) - return b'\n' + ET.tostring(tree, encoding='utf-8') + final["status"] = "failed" + tree = dict_to_xml_tree("subsonic-response", final) + return b'\n' + ET.tostring( + tree, encoding="utf-8" + ) def dict_to_xml_tree(root_tag, d, parent=None): diff --git a/api/funkwhale_api/subsonic/serializers.py b/api/funkwhale_api/subsonic/serializers.py index 97cdbcfc6..7735b1a78 100644 --- a/api/funkwhale_api/subsonic/serializers.py +++ b/api/funkwhale_api/subsonic/serializers.py @@ -10,106 +10,100 @@ 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'] + "id": artist_values["id"], + "name": artist_values["name"], + "albumCount": artist_values["_albums_count"], } class GetArtistsSerializer(serializers.Serializer): def to_representation(self, queryset): - payload = { - 'ignoredArticles': '', - 'index': [] - } + payload = {"ignoredArticles": "", "index": []} queryset = queryset.with_albums_count() - queryset = queryset.order_by(functions.Lower('name')) - values = queryset.values('id', '_albums_count', 'name') + 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) + first_letter_mapping[artist["name"][0].upper()].append(artist) for letter, artists in sorted(first_letter_mapping.items()): letter_data = { - 'name': letter, - 'artist': [ - get_artist_data(v) - for v in artists - ] + "name": letter, + "artist": [get_artist_data(v) for v in artists], } - payload['index'].append(letter_data) + payload["index"].append(letter_data) return payload class GetArtistSerializer(serializers.Serializer): def to_representation(self, artist): - albums = artist.albums.prefetch_related('tracks__files') + albums = artist.albums.prefetch_related("tracks__files") payload = { - 'id': artist.pk, - 'name': artist.name, - 'albumCount': len(albums), - 'album': [], + "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()), + "id": album.id, + "artistId": artist.id, + "name": album.title, + "artist": artist.name, + "created": album.creation_date, + "songCount": len(album.tracks.all()), } if album.cover: - album_data['coverArt'] = 'al-{}'.format(album.id) + album_data["coverArt"] = "al-{}".format(album.id) if album.release_date: - album_data['year'] = album.release_date.year - payload['album'].append(album_data) + album_data["year"] = album.release_date.year + payload["album"].append(album_data) 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 or 1, - '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', + "id": track.pk, + "isDir": "false", + "title": track.title, + "album": album.title, + "artist": album.artist.name, + "track": track.position or 1, + "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 track.album.cover: - data['coverArt'] = 'al-{}'.format(track.album.id) + data["coverArt"] = "al-{}".format(track.album.id) if tf.bitrate: - data['bitrate'] = int(tf.bitrate/1000) + data["bitrate"] = int(tf.bitrate / 1000) if tf.size: - data['size'] = tf.size + data["size"] = tf.size if album.release_date: - data['year'] = album.release_date.year + 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, + "id": album.id, + "artistId": album.artist.id, + "name": album.title, + "artist": album.artist.name, + "created": album.creation_date, } if album.cover: - payload['coverArt'] = 'al-{}'.format(album.id) + payload["coverArt"] = "al-{}".format(album.id) try: - payload['songCount'] = album._tracks_count + payload["songCount"] = album._tracks_count except AttributeError: - payload['songCount'] = len(album.tracks.prefetch_related('files')) + payload["songCount"] = len(album.tracks.prefetch_related("files")) return payload @@ -127,24 +121,23 @@ def get_song_list_data(tracks): class GetAlbumSerializer(serializers.Serializer): def to_representation(self, album): - tracks = album.tracks.prefetch_related('files').select_related('album') + tracks = album.tracks.prefetch_related("files").select_related("album") payload = get_album2_data(album) if album.release_date: - payload['year'] = album.release_date.year + payload["year"] = album.release_date.year - payload['song'] = get_song_list_data(tracks) + 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') + 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: @@ -152,54 +145,48 @@ def get_starred_tracks_data(favorites): except IndexError: continue td = get_track_data(t.album, t, tf) - td['starred'] = by_track_id[t.pk].creation_date + 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 - ] + return [get_album2_data(a) for a in albums] def get_playlist_data(playlist): return { - 'id': playlist.pk, - 'name': playlist.name, - 'owner': playlist.user.username, - 'public': 'false', - 'songCount': playlist._tracks_count, - 'duration': 0, - 'created': playlist.creation_date, + "id": playlist.pk, + "name": playlist.name, + "owner": playlist.user.username, + "public": "false", + "songCount": playlist._tracks_count, + "duration": 0, + "created": playlist.creation_date, } def get_playlist_detail_data(playlist): data = get_playlist_data(playlist) - qs = playlist.playlist_tracks.select_related( - 'track__album__artist' - ).prefetch_related('track__files').order_by('index') - data['entry'] = [] + qs = ( + playlist.playlist_tracks.select_related("track__album__artist") + .prefetch_related("track__files") + .order_by("index") + ) + data["entry"] = [] for plt in qs: try: tf = [tf for tf in plt.track.files.all()][0] except IndexError: continue td = get_track_data(plt.track.album, plt.track, tf) - data['entry'].append(td) + data["entry"].append(td) return data def get_music_directory_data(artist): - tracks = artist.tracks.select_related('album').prefetch_related('files') - data = { - 'id': artist.pk, - 'parent': 1, - 'name': artist.name, - 'child': [] - } + tracks = artist.tracks.select_related("album").prefetch_related("files") + data = {"id": artist.pk, "parent": 1, "name": artist.name, "child": []} for track in tracks: try: tf = [tf for tf in track.files.all()][0] @@ -207,40 +194,39 @@ def get_music_directory_data(artist): continue album = track.album td = { - 'id': track.pk, - 'isDir': 'false', - 'title': track.title, - 'album': album.title, - 'artist': artist.name, - 'track': track.position or 1, - 'year': track.album.release_date.year if track.album.release_date else 0, - 'contentType': tf.mimetype, - 'suffix': tf.extension or '', - 'duration': tf.duration or 0, - 'created': track.creation_date, - 'albumId': album.pk, - 'artistId': artist.pk, - 'parent': artist.id, - 'type': 'music', + "id": track.pk, + "isDir": "false", + "title": track.title, + "album": album.title, + "artist": artist.name, + "track": track.position or 1, + "year": track.album.release_date.year if track.album.release_date else 0, + "contentType": tf.mimetype, + "suffix": tf.extension or "", + "duration": tf.duration or 0, + "created": track.creation_date, + "albumId": album.pk, + "artistId": artist.pk, + "parent": artist.id, + "type": "music", } if tf.bitrate: - td['bitrate'] = int(tf.bitrate/1000) + td["bitrate"] = int(tf.bitrate / 1000) if tf.size: - td['size'] = tf.size - data['child'].append(td) + td["size"] = tf.size + data["child"].append(td) return data class ScrobbleSerializer(serializers.Serializer): submission = serializers.BooleanField(default=True, required=False) id = serializers.PrimaryKeyRelatedField( - queryset=music_models.Track.objects.annotate( - files_count=Count('files') - ).filter(files_count__gt=0) + queryset=music_models.Track.objects.annotate(files_count=Count("files")).filter( + files_count__gt=0 + ) ) def create(self, data): return history_models.Listening.objects.create( - user=self.context['user'], - track=data['id'], + user=self.context["user"], track=data["id"] ) diff --git a/api/funkwhale_api/subsonic/views.py b/api/funkwhale_api/subsonic/views.py index cc75b5279..85d120a08 100644 --- a/api/funkwhale_api/subsonic/views.py +++ b/api/funkwhale_api/subsonic/views.py @@ -25,44 +25,55 @@ from . import negotiation from . import serializers -def find_object(queryset, model_field='pk', field='id', cast=int): +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({ - 'error': { - 'code': 10, - 'message': "required parameter '{}' not present".format(field) + return response.Response( + { + "error": { + "code": 10, + "message": "required parameter '{}' not present".format( + field + ), + } } - }) + ) try: value = cast(raw_value) except (TypeError, ValidationError): - return response.Response({ - 'error': { - 'code': 0, - 'message': 'For input string "{}"'.format(raw_value) + return response.Response( + { + "error": { + "code": 0, + "message": 'For input string "{}"'.format(raw_value), + } } - }) + ) qs = queryset - if hasattr(qs, '__call__'): + if hasattr(qs, "__call__"): qs = qs(request) try: obj = qs.get(**{model_field: value}) except qs.model.DoesNotExist: - return response.Response({ - 'error': { - 'code': 70, - 'message': '{} not found'.format( - qs.model.__class__.__name__) + return response.Response( + { + "error": { + "code": 70, + "message": "{} not found".format( + qs.model.__class__.__name__ + ), + } } - }) - kwargs['obj'] = obj + ) + kwargs["obj"] = obj return func(self, request, *args, **kwargs) + return inner + return decorator @@ -72,10 +83,10 @@ class SubsonicViewSet(viewsets.GenericViewSet): permissions_classes = [rest_permissions.IsAuthenticated] def dispatch(self, request, *args, **kwargs): - if not preferences.get('subsonic__enabled'): + if not preferences.get("subsonic__enabled"): r = response.Response({}, status=405) r.accepted_renderer = renderers.JSONRenderer() - r.accepted_media_type = 'application/json' + r.accepted_media_type = "application/json" r.renderer_context = {} return r return super().dispatch(request, *args, **kwargs) @@ -83,261 +94,186 @@ class SubsonicViewSet(viewsets.GenericViewSet): 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' - } + mapping = {exceptions.AuthenticationFailed: (40, "Wrong username or password.")} + payload = {"status": "failed"} if exc.__class__ in mapping: code, message = mapping[exc.__class__] else: return super().handle_exception(exc) - payload['error'] = { - 'code': code, - 'message': message - } + payload["error"] = {"code": code, "message": message} return response.Response(payload, status=200) - @list_route( - methods=['get', 'post'], - permission_classes=[]) + @list_route(methods=["get", "post"], permission_classes=[]) def ping(self, request, *args, **kwargs): - data = { - 'status': 'ok', - 'version': '1.16.0' - } + data = {"status": "ok", "version": "1.16.0"} return response.Response(data, status=200) @list_route( - methods=['get', 'post'], - url_name='get_license', + methods=["get", "post"], + url_name="get_license", permissions_classes=[], - url_path='getLicense') + 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) - } + "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', - url_path='getArtists') + @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 - } + payload = {"artists": data} return response.Response(payload, status=200) - @list_route( - methods=['get', 'post'], - url_name='get_indexes', - url_path='getIndexes') + @list_route(methods=["get", "post"], url_name="get_indexes", url_path="getIndexes") def get_indexes(self, request, *args, **kwargs): artists = music_models.Artist.objects.all() data = serializers.GetArtistsSerializer(artists).data - payload = { - 'indexes': data - } + payload = {"indexes": data} return response.Response(payload, status=200) - @list_route( - methods=['get', 'post'], - url_name='get_artist', - url_path='getArtist') + @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') + artist = kwargs.pop("obj") data = serializers.GetArtistSerializer(artist).data - payload = { - 'artist': data - } + payload = {"artist": data} return response.Response(payload, status=200) @list_route( - methods=['get', 'post'], - url_name='get_artist_info2', - url_path='getArtistInfo2') + 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': {} - } + artist = kwargs.pop("obj") + payload = {"artist-info2": {}} 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')) + @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') + album = kwargs.pop("obj") data = serializers.GetAlbumSerializer(album).data - payload = { - '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()) + @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') + track = kwargs.pop("obj") queryset = track.files.select_related( - 'library_track', - 'track__album__artist', - 'track__artist', + "library_track", "track__album__artist", "track__artist" ) track_file = queryset.first() if not track_file: 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()) + @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') + track = kwargs.pop("obj") TrackFavorite.add(user=request.user, track=track) - return response.Response({'status': 'ok'}) + return response.Response({"status": "ok"}) - @list_route( - methods=['get', 'post'], - url_name='unstar', - url_path='unstar') - @find_object( - music_models.Track.objects.all()) + @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') + track = kwargs.pop("obj") request.user.track_favorites.filter(track=track).delete() - return response.Response({'status': 'ok'}) + return response.Response({"status": "ok"}) @list_route( - methods=['get', 'post'], - url_name='get_starred2', - url_path='getStarred2') + methods=["get", "post"], url_name="get_starred2", url_path="getStarred2" + ) def get_starred2(self, request, *args, **kwargs): favorites = request.user.track_favorites.all() - data = { - 'starred2': { - 'song': serializers.get_starred_tracks_data(favorites) - } - } + data = {"starred2": {"song": serializers.get_starred_tracks_data(favorites)}} return response.Response(data) - @list_route( - methods=['get', 'post'], - url_name='get_starred', - url_path='getStarred') + @list_route(methods=["get", "post"], url_name="get_starred", url_path="getStarred") def get_starred(self, request, *args, **kwargs): favorites = request.user.track_favorites.all() - data = { - 'starred': { - 'song': serializers.get_starred_tracks_data(favorites) - } - } + data = {"starred": {"song": serializers.get_starred_tracks_data(favorites)}} return response.Response(data) @list_route( - methods=['get', 'post'], - url_name='get_album_list2', - url_path='getAlbumList2') + 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']) + offset = int(data["offset"]) except (TypeError, KeyError, ValueError): offset = 0 try: - size = int(data['size']) + 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) - } - } + data = {"albumList2": {"album": serializers.get_album_list2_data(queryset)}} return response.Response(data) - @list_route( - methods=['get', 'post'], - url_name='search3', - url_path='search3') + @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('*', '') + 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') + "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 - ] + "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') + "subsonic": "album", + "search_fields": ["title"], + "queryset": ( + music_models.Album.objects.with_tracks_count().select_related( + "artist" + ) ), - 'serializer': serializers.get_album_list2_data, + "serializer": serializers.get_album_list2_data, }, { - 'subsonic': 'song', - 'search_fields': ['title'], - 'queryset': ( - music_models.Track.objects - .prefetch_related('files') - .select_related('album__artist') + "subsonic": "song", + "search_fields": ["title"], + "queryset": ( + music_models.Track.objects.prefetch_related("files").select_related( + "album__artist" + ) ), - 'serializer': serializers.get_song_list_data, + "serializer": serializers.get_song_list_data, }, ] - payload = { - 'searchResult3': {} - } + payload = {"searchResult3": {}} for c in conf: - offsetKey = '{}Offset'.format(c['subsonic']) - countKey = '{}Count'.format(c['subsonic']) + offsetKey = "{}Offset".format(c["subsonic"]) + countKey = "{}Count".format(c["subsonic"]) try: offset = int(data[offsetKey]) except (TypeError, KeyError, ValueError): @@ -349,60 +285,49 @@ class SubsonicViewSet(viewsets.GenericViewSet): size = 20 size = min(size, 100) - queryset = c['queryset'] + queryset = c["queryset"] if query: - queryset = c['queryset'].filter( - utils.get_query(query, c['search_fields']) + queryset = c["queryset"].filter( + utils.get_query(query, c["search_fields"]) ) queryset = queryset[offset:size] - payload['searchResult3'][c['subsonic']] = c['serializer'](queryset) + payload["searchResult3"][c["subsonic"]] = c["serializer"](queryset) return response.Response(payload) @list_route( - methods=['get', 'post'], - url_name='get_playlists', - url_path='getPlaylists') + methods=["get", "post"], url_name="get_playlists", url_path="getPlaylists" + ) def get_playlists(self, request, *args, **kwargs): - playlists = request.user.playlists.with_tracks_count().select_related( - 'user' - ) + playlists = request.user.playlists.with_tracks_count().select_related("user") data = { - 'playlists': { - 'playlist': [ - serializers.get_playlist_data(p) for p in playlists] + "playlists": { + "playlist": [serializers.get_playlist_data(p) for p in playlists] } } return response.Response(data) @list_route( - methods=['get', 'post'], - url_name='get_playlist', - url_path='getPlaylist') - @find_object( - playlists_models.Playlist.objects.with_tracks_count()) + methods=["get", "post"], url_name="get_playlist", url_path="getPlaylist" + ) + @find_object(playlists_models.Playlist.objects.with_tracks_count()) def get_playlist(self, request, *args, **kwargs): - playlist = kwargs.pop('obj') - data = { - 'playlist': serializers.get_playlist_detail_data(playlist) - } + playlist = kwargs.pop("obj") + data = {"playlist": serializers.get_playlist_detail_data(playlist)} return response.Response(data) @list_route( - methods=['get', 'post'], - url_name='update_playlist', - url_path='updatePlaylist') - @find_object( - lambda request: request.user.playlists.all(), - field='playlistId') + methods=["get", "post"], url_name="update_playlist", url_path="updatePlaylist" + ) + @find_object(lambda request: request.user.playlists.all(), field="playlistId") def update_playlist(self, request, *args, **kwargs): - playlist = kwargs.pop('obj') + playlist = kwargs.pop("obj") data = request.GET or request.POST - new_name = data.get('name', '') + new_name = data.get("name", "") if new_name: playlist.name = new_name - playlist.save(update_fields=['name', 'modification_date']) + playlist.save(update_fields=["name", "modification_date"]) try: - to_remove = int(data['songIndexToRemove']) + to_remove = int(data["songIndexToRemove"]) plt = playlist.playlist_tracks.get(index=to_remove) except (TypeError, ValueError, KeyError): pass @@ -412,7 +337,7 @@ class SubsonicViewSet(viewsets.GenericViewSet): plt.delete(update_indexes=True) ids = [] - for i in data.getlist('songIdToAdd'): + for i in data.getlist("songIdToAdd"): try: ids.append(int(i)) except (TypeError, ValueError): @@ -429,45 +354,38 @@ class SubsonicViewSet(viewsets.GenericViewSet): if sorted_tracks: playlist.insert_many(sorted_tracks) - data = { - 'status': 'ok' - } + data = {"status": "ok"} return response.Response(data) @list_route( - methods=['get', 'post'], - url_name='delete_playlist', - url_path='deletePlaylist') - @find_object( - lambda request: request.user.playlists.all()) + methods=["get", "post"], url_name="delete_playlist", url_path="deletePlaylist" + ) + @find_object(lambda request: request.user.playlists.all()) def delete_playlist(self, request, *args, **kwargs): - playlist = kwargs.pop('obj') + playlist = kwargs.pop("obj") playlist.delete() - data = { - 'status': 'ok' - } + data = {"status": "ok"} return response.Response(data) @list_route( - methods=['get', 'post'], - url_name='create_playlist', - url_path='createPlaylist') + methods=["get", "post"], url_name="create_playlist", url_path="createPlaylist" + ) def create_playlist(self, request, *args, **kwargs): data = request.GET or request.POST - name = data.get('name', '') + name = data.get("name", "") if not name: - return response.Response({ - 'error': { - 'code': 10, - 'message': 'Playlist ID or name must be specified.' + return response.Response( + { + "error": { + "code": 10, + "message": "Playlist ID or name must be specified.", + } } - }) + ) - playlist = request.user.playlists.create( - name=name - ) + playlist = request.user.playlists.create(name=name) ids = [] - for i in data.getlist('songId'): + for i in data.getlist("songId"): try: ids.append(int(i)) except (TypeError, ValueError): @@ -484,92 +402,67 @@ class SubsonicViewSet(viewsets.GenericViewSet): pass if sorted_tracks: playlist.insert_many(sorted_tracks) - playlist = request.user.playlists.with_tracks_count().get( - pk=playlist.pk) - data = { - 'playlist': serializers.get_playlist_detail_data(playlist) - } + playlist = request.user.playlists.with_tracks_count().get(pk=playlist.pk) + data = {"playlist": serializers.get_playlist_detail_data(playlist)} return response.Response(data) @list_route( - methods=['get', 'post'], - url_name='get_music_folders', - url_path='getMusicFolders') + methods=["get", "post"], + url_name="get_music_folders", + url_path="getMusicFolders", + ) def get_music_folders(self, request, *args, **kwargs): - data = { - 'musicFolders': { - 'musicFolder': [{ - 'id': 1, - 'name': 'Music' - }] - } - } + data = {"musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]}} return response.Response(data) @list_route( - methods=['get', 'post'], - url_name='get_cover_art', - url_path='getCoverArt') + methods=["get", "post"], url_name="get_cover_art", url_path="getCoverArt" + ) def get_cover_art(self, request, *args, **kwargs): data = request.GET or request.POST - id = data.get('id', '') + id = data.get("id", "") if not id: - return response.Response({ - 'error': { - 'code': 10, - 'message': 'cover art ID must be specified.' - } - }) + return response.Response( + {"error": {"code": 10, "message": "cover art ID must be specified."}} + ) - if id.startswith('al-'): + if id.startswith("al-"): try: - album_id = int(id.replace('al-', '')) - album = music_models.Album.objects.exclude( - cover__isnull=True - ).exclude(cover='').get(pk=album_id) + album_id = int(id.replace("al-", "")) + album = ( + music_models.Album.objects.exclude(cover__isnull=True) + .exclude(cover="") + .get(pk=album_id) + ) except (TypeError, ValueError, music_models.Album.DoesNotExist): - return response.Response({ - 'error': { - 'code': 70, - 'message': 'cover art not found.' - } - }) + return response.Response( + {"error": {"code": 70, "message": "cover art not found."}} + ) cover = album.cover else: - return response.Response({ - 'error': { - 'code': 70, - 'message': 'cover art not found.' - } - }) + return response.Response( + {"error": {"code": 70, "message": "cover art not found."}} + ) - mapping = { - 'nginx': 'X-Accel-Redirect', - 'apache2': 'X-Sendfile', - } + mapping = {"nginx": "X-Accel-Redirect", "apache2": "X-Sendfile"} path = music_views.get_file_path(cover) file_header = mapping[settings.REVERSE_PROXY_TYPE] # let the proxy set the content-type - r = response.Response({}, content_type='') + r = response.Response({}, content_type="") r[file_header] = path return r - @list_route( - methods=['get', 'post'], - url_name='scrobble', - url_path='scrobble') + @list_route(methods=["get", "post"], url_name="scrobble", url_path="scrobble") def scrobble(self, request, *args, **kwargs): data = request.GET or request.POST serializer = serializers.ScrobbleSerializer( - data=data, context={'user': request.user}) + data=data, context={"user": request.user} + ) if not serializer.is_valid(): - return response.Response({ - 'error': { - 'code': 0, - 'message': 'Invalid payload' - } - }) - if serializer.validated_data['submission']: + return response.Response( + {"error": {"code": 0, "message": "Invalid payload"}} + ) + if serializer.validated_data["submission"]: l = serializer.save() record.send(l) return response.Response({}) diff --git a/api/funkwhale_api/taskapp/celery.py b/api/funkwhale_api/taskapp/celery.py index 60b09bece..80fb1a60e 100644 --- a/api/funkwhale_api/taskapp/celery.py +++ b/api/funkwhale_api/taskapp/celery.py @@ -10,20 +10,22 @@ from django.conf import settings if not settings.configured: # set the default Django settings module for the 'celery' program. - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") # pragma: no cover + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "config.settings.local" + ) # pragma: no cover -app = Celery('funkwhale_api') +app = Celery("funkwhale_api") class CeleryConfig(AppConfig): - name = 'funkwhale_api.taskapp' - verbose_name = 'Celery Config' + name = "funkwhale_api.taskapp" + verbose_name = "Celery Config" def ready(self): # Using a string here means the worker will not have to # pickle the object when using Windows. - app.config_from_object('django.conf:settings', namespace='CELERY') + app.config_from_object("django.conf:settings", namespace="CELERY") app.autodiscover_tasks(lambda: settings.INSTALLED_APPS, force=True) @@ -31,7 +33,7 @@ def require_instance(model_or_qs, parameter_name, id_kwarg_name=None): def decorator(function): @functools.wraps(function) def inner(*args, **kwargs): - kw = id_kwarg_name or '_'.join([parameter_name, 'id']) + kw = id_kwarg_name or "_".join([parameter_name, "id"]) pk = kwargs.pop(kw) try: instance = model_or_qs.get(pk=pk) @@ -39,5 +41,7 @@ def require_instance(model_or_qs, parameter_name, id_kwarg_name=None): instance = model_or_qs.objects.get(pk=pk) kwargs[parameter_name] = instance return function(*args, **kwargs) + return inner + return decorator diff --git a/api/funkwhale_api/users/adapters.py b/api/funkwhale_api/users/adapters.py index 7bd341d14..691697570 100644 --- a/api/funkwhale_api/users/adapters.py +++ b/api/funkwhale_api/users/adapters.py @@ -5,11 +5,10 @@ from dynamic_preferences.registries import global_preferences_registry class FunkwhaleAccountAdapter(DefaultAccountAdapter): - def is_open_for_signup(self, request): manager = global_preferences_registry.manager() - return manager['users__registration_enabled'] + return manager["users__registration_enabled"] def send_mail(self, template_prefix, email, context): - context['funkwhale_url'] = settings.FUNKWHALE_URL + context["funkwhale_url"] = settings.FUNKWHALE_URL return super().send_mail(template_prefix, email, context) diff --git a/api/funkwhale_api/users/admin.py b/api/funkwhale_api/users/admin.py index cb74abf0e..5c694ab0e 100644 --- a/api/funkwhale_api/users/admin.py +++ b/api/funkwhale_api/users/admin.py @@ -17,9 +17,9 @@ class MyUserChangeForm(UserChangeForm): class MyUserCreationForm(UserCreationForm): - error_message = UserCreationForm.error_messages.update({ - 'duplicate_username': 'This username has already been taken.' - }) + error_message = UserCreationForm.error_messages.update( + {"duplicate_username": "This username has already been taken."} + ) class Meta(UserCreationForm.Meta): model = User @@ -30,7 +30,7 @@ class MyUserCreationForm(UserCreationForm): User.objects.get(username=username) except User.DoesNotExist: return username - raise forms.ValidationError(self.error_messages['duplicate_username']) + raise forms.ValidationError(self.error_messages["duplicate_username"]) @admin.register(User) @@ -38,38 +38,39 @@ class UserAdmin(AuthUserAdmin): form = MyUserChangeForm add_form = MyUserCreationForm list_display = [ - 'username', - 'email', - 'date_joined', - 'last_login', - 'is_staff', - 'is_superuser', + "username", + "email", + "date_joined", + "last_login", + "is_staff", + "is_superuser", ] list_filter = [ - 'is_superuser', - 'is_staff', - 'privacy_level', - 'permission_settings', - 'permission_library', - 'permission_federation', + "is_superuser", + "is_staff", + "privacy_level", + "permission_settings", + "permission_library", + "permission_federation", ] fieldsets = ( - (None, {'fields': ('username', 'password', 'privacy_level')}), - (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}), - (_('Permissions'), { - 'fields': ( - 'is_active', - 'is_staff', - 'is_superuser', - 'permission_upload', - 'permission_library', - 'permission_settings', - 'permission_federation')}), - (_('Important dates'), {'fields': ('last_login', 'date_joined')}), - (_('Useless fields'), { - 'fields': ( - 'user_permissions', - 'groups', - )}) - ) + (None, {"fields": ("username", "password", "privacy_level")}), + (_("Personal info"), {"fields": ("first_name", "last_name", "email")}), + ( + _("Permissions"), + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + "permission_upload", + "permission_library", + "permission_settings", + "permission_federation", + ) + }, + ), + (_("Important dates"), {"fields": ("last_login", "date_joined")}), + (_("Useless fields"), {"fields": ("user_permissions", "groups")}), + ) diff --git a/api/funkwhale_api/users/api_urls.py b/api/funkwhale_api/users/api_urls.py index 8aba7f1a8..dacb091f1 100644 --- a/api/funkwhale_api/users/api_urls.py +++ b/api/funkwhale_api/users/api_urls.py @@ -2,6 +2,6 @@ from rest_framework import routers from . import views router = routers.SimpleRouter() -router.register(r'users', views.UserViewSet, 'users') +router.register(r"users", views.UserViewSet, "users") urlpatterns = router.urls diff --git a/api/funkwhale_api/users/dynamic_preferences_registry.py b/api/funkwhale_api/users/dynamic_preferences_registry.py index 7108360b9..08f5730a8 100644 --- a/api/funkwhale_api/users/dynamic_preferences_registry.py +++ b/api/funkwhale_api/users/dynamic_preferences_registry.py @@ -5,36 +5,26 @@ from funkwhale_api.common import preferences as common_preferences from . import models -users = types.Section('users') +users = types.Section("users") @global_preferences_registry.register class RegistrationEnabled(types.BooleanPreference): show_in_api = True section = users - name = 'registration_enabled' + name = "registration_enabled" default = False - verbose_name = 'Open registrations to new users' - help_text = ( - 'When enabled, new users will be able to register on this instance.' - ) + verbose_name = "Open registrations to new users" + help_text = "When enabled, new users will be able to register on this instance." @global_preferences_registry.register class DefaultPermissions(common_preferences.StringListPreference): show_in_api = True section = users - name = 'default_permissions' + name = "default_permissions" default = [] - verbose_name = 'Default permissions' - help_text = ( - 'A list of default preferences to give to all registered users.' - ) - choices = [ - (k, c['label']) - for k, c in models.PERMISSIONS_CONFIGURATION.items() - ] - field_kwargs = { - 'choices': choices, - 'required': False, - } + verbose_name = "Default permissions" + help_text = "A list of default preferences to give to all registered users." + choices = [(k, c["label"]) for k, c in models.PERMISSIONS_CONFIGURATION.items()] + field_kwargs = {"choices": choices, "required": False} diff --git a/api/funkwhale_api/users/factories.py b/api/funkwhale_api/users/factories.py index cd28f4407..949bfdfe4 100644 --- a/api/funkwhale_api/users/factories.py +++ b/api/funkwhale_api/users/factories.py @@ -6,10 +6,10 @@ from django.contrib.auth.models import Permission @registry.register class GroupFactory(factory.django.DjangoModelFactory): - name = factory.Sequence(lambda n: 'group-{0}'.format(n)) + name = factory.Sequence(lambda n: "group-{0}".format(n)) class Meta: - model = 'auth.Group' + model = "auth.Group" @factory.post_generation def perms(self, create, extracted, **kwargs): @@ -20,8 +20,7 @@ class GroupFactory(factory.django.DjangoModelFactory): if extracted: perms = [ Permission.objects.get( - content_type__app_label=p.split('.')[0], - codename=p.split('.')[1], + content_type__app_label=p.split(".")[0], codename=p.split(".")[1] ) for p in extracted ] @@ -31,15 +30,15 @@ class GroupFactory(factory.django.DjangoModelFactory): @registry.register class UserFactory(factory.django.DjangoModelFactory): - username = factory.Sequence(lambda n: 'user-{0}'.format(n)) - email = factory.Sequence(lambda n: 'user-{0}@example.com'.format(n)) - password = factory.PostGenerationMethodCall('set_password', 'test') + username = factory.Sequence(lambda n: "user-{0}".format(n)) + email = factory.Sequence(lambda n: "user-{0}@example.com".format(n)) + password = factory.PostGenerationMethodCall("set_password", "test") subsonic_api_token = None - groups = ManyToManyFromList('groups') + groups = ManyToManyFromList("groups") class Meta: - model = 'users.User' - django_get_or_create = ('username', ) + model = "users.User" + django_get_or_create = ("username",) @factory.post_generation def perms(self, create, extracted, **kwargs): @@ -50,8 +49,7 @@ class UserFactory(factory.django.DjangoModelFactory): if extracted: perms = [ Permission.objects.get( - content_type__app_label=p.split('.')[0], - codename=p.split('.')[1], + content_type__app_label=p.split(".")[0], codename=p.split(".")[1] ) for p in extracted ] @@ -59,7 +57,7 @@ class UserFactory(factory.django.DjangoModelFactory): self.user_permissions.add(*perms) -@registry.register(name='users.SuperUser') +@registry.register(name="users.SuperUser") class SuperUserFactory(UserFactory): is_staff = True is_superuser = True diff --git a/api/funkwhale_api/users/migrations/0001_initial.py b/api/funkwhale_api/users/migrations/0001_initial.py index ef9240c91..cc8307c88 100644 --- a/api/funkwhale_api/users/migrations/0001_initial.py +++ b/api/funkwhale_api/users/migrations/0001_initial.py @@ -9,36 +9,129 @@ import django.core.validators class Migration(migrations.Migration): - dependencies = [ - ('auth', '0006_require_contenttypes_0002'), - ] + dependencies = [("auth", "0006_require_contenttypes_0002")] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.AutoField(primary_key=True, verbose_name='ID', serialize=False, auto_created=True)), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(null=True, verbose_name='last login', blank=True)), - ('is_superuser', models.BooleanField(help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status', default=False)), - ('username', models.CharField(max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], verbose_name='username', error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True)), - ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)), - ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)), - ('email', models.EmailField(max_length=254, verbose_name='email address', blank=True)), - ('is_staff', models.BooleanField(help_text='Designates whether the user can log into this admin site.', verbose_name='staff status', default=False)), - ('is_active', models.BooleanField(help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active', default=True)), - ('date_joined', models.DateTimeField(verbose_name='date joined', default=django.utils.timezone.now)), - ('groups', models.ManyToManyField(related_name='user_set', blank=True, verbose_name='groups', to='auth.Group', help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_query_name='user')), - ('user_permissions', models.ManyToManyField(related_name='user_set', blank=True, verbose_name='user permissions', to='auth.Permission', help_text='Specific permissions for this user.', related_query_name='user')), - ('name', models.CharField(max_length=255, verbose_name='Name of User', blank=True)), + ( + "id", + models.AutoField( + primary_key=True, + verbose_name="ID", + serialize=False, + auto_created=True, + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + null=True, verbose_name="last login", blank=True + ), + ), + ( + "is_superuser", + models.BooleanField( + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + default=False, + ), + ), + ( + "username", + models.CharField( + max_length=30, + validators=[ + django.core.validators.RegexValidator( + "^[\\w.@+-]+$", + "Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.", + "invalid", + ) + ], + verbose_name="username", + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.", + unique=True, + ), + ), + ( + "first_name", + models.CharField( + max_length=30, verbose_name="first name", blank=True + ), + ), + ( + "last_name", + models.CharField( + max_length=30, verbose_name="last name", blank=True + ), + ), + ( + "email", + models.EmailField( + max_length=254, verbose_name="email address", blank=True + ), + ), + ( + "is_staff", + models.BooleanField( + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + default=False, + ), + ), + ( + "is_active", + models.BooleanField( + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + default=True, + ), + ), + ( + "date_joined", + models.DateTimeField( + verbose_name="date joined", default=django.utils.timezone.now + ), + ), + ( + "groups", + models.ManyToManyField( + related_name="user_set", + blank=True, + verbose_name="groups", + to="auth.Group", + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_query_name="user", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + related_name="user_set", + blank=True, + verbose_name="user permissions", + to="auth.Permission", + help_text="Specific permissions for this user.", + related_query_name="user", + ), + ), + ( + "name", + models.CharField( + max_length=255, verbose_name="Name of User", blank=True + ), + ), ], options={ - 'verbose_name': 'user', - 'abstract': False, - 'verbose_name_plural': 'users', + "verbose_name": "user", + "abstract": False, + "verbose_name_plural": "users", }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), + managers=[("objects", django.contrib.auth.models.UserManager())], + ) ] diff --git a/api/funkwhale_api/users/migrations/0002_auto_20171214_2205.py b/api/funkwhale_api/users/migrations/0002_auto_20171214_2205.py index 4bbbaa62b..75fc22035 100644 --- a/api/funkwhale_api/users/migrations/0002_auto_20171214_2205.py +++ b/api/funkwhale_api/users/migrations/0002_auto_20171214_2205.py @@ -9,20 +9,23 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0001_initial'), - ] + dependencies = [("users", "0001_initial")] operations = [ migrations.AlterModelManagers( - name='user', - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], + name="user", + managers=[("objects", django.contrib.auth.models.UserManager())], ), migrations.AlterField( - model_name='user', - name='username', - field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username'), + model_name="user", + name="username", + field=models.CharField( + error_messages={"unique": "A user with that username already exists."}, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], + verbose_name="username", + ), ), ] diff --git a/api/funkwhale_api/users/migrations/0003_auto_20171226_1357.py b/api/funkwhale_api/users/migrations/0003_auto_20171226_1357.py index fd75795d3..62c038b7a 100644 --- a/api/funkwhale_api/users/migrations/0003_auto_20171226_1357.py +++ b/api/funkwhale_api/users/migrations/0003_auto_20171226_1357.py @@ -6,19 +6,19 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('users', '0002_auto_20171214_2205'), - ] + dependencies = [("users", "0002_auto_20171214_2205")] operations = [ migrations.AddField( - model_name='user', - name='secret_key', + model_name="user", + name="secret_key", field=models.UUIDField(default=uuid.uuid4, null=True), ), migrations.AlterField( - model_name='user', - name='last_name', - field=models.CharField(blank=True, max_length=150, verbose_name='last name'), + model_name="user", + name="last_name", + field=models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), ), ] diff --git a/api/funkwhale_api/users/migrations/0004_user_privacy_level.py b/api/funkwhale_api/users/migrations/0004_user_privacy_level.py index 81891eb0f..86b2c7581 100644 --- a/api/funkwhale_api/users/migrations/0004_user_privacy_level.py +++ b/api/funkwhale_api/users/migrations/0004_user_privacy_level.py @@ -5,14 +5,21 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0003_auto_20171226_1357'), - ] + dependencies = [("users", "0003_auto_20171226_1357")] operations = [ migrations.AddField( - model_name='user', - name='privacy_level', - field=models.CharField(choices=[('me', 'Only me'), ('followers', 'Me and my followers'), ('instance', 'Everyone on my instance, and my followers'), ('everyone', 'Everyone, including people on other instances')], default='instance', max_length=30), - ), + model_name="user", + name="privacy_level", + field=models.CharField( + choices=[ + ("me", "Only me"), + ("followers", "Me and my followers"), + ("instance", "Everyone on my instance, and my followers"), + ("everyone", "Everyone, including people on other instances"), + ], + default="instance", + max_length=30, + ), + ) ] diff --git a/api/funkwhale_api/users/migrations/0005_user_subsonic_api_token.py b/api/funkwhale_api/users/migrations/0005_user_subsonic_api_token.py index 689b3ef77..65a1f1935 100644 --- a/api/funkwhale_api/users/migrations/0005_user_subsonic_api_token.py +++ b/api/funkwhale_api/users/migrations/0005_user_subsonic_api_token.py @@ -5,14 +5,12 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0004_user_privacy_level'), - ] + dependencies = [("users", "0004_user_privacy_level")] operations = [ migrations.AddField( - model_name='user', - name='subsonic_api_token', + model_name="user", + name="subsonic_api_token", field=models.CharField(blank=True, max_length=255, null=True), - ), + ) ] diff --git a/api/funkwhale_api/users/migrations/0006_auto_20180517_2324.py b/api/funkwhale_api/users/migrations/0006_auto_20180517_2324.py index 7c9ab0fad..d5f6d911b 100644 --- a/api/funkwhale_api/users/migrations/0006_auto_20180517_2324.py +++ b/api/funkwhale_api/users/migrations/0006_auto_20180517_2324.py @@ -5,24 +5,22 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0005_user_subsonic_api_token'), - ] + dependencies = [("users", "0005_user_subsonic_api_token")] operations = [ migrations.AddField( - model_name='user', - name='permission_federation', + model_name="user", + name="permission_federation", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='user', - name='permission_library', + model_name="user", + name="permission_library", field=models.BooleanField(default=False), ), migrations.AddField( - model_name='user', - name='permission_settings', + model_name="user", + name="permission_settings", field=models.BooleanField(default=False), ), ] diff --git a/api/funkwhale_api/users/migrations/0007_auto_20180524_2009.py b/api/funkwhale_api/users/migrations/0007_auto_20180524_2009.py index e3d582c53..218aa7e48 100644 --- a/api/funkwhale_api/users/migrations/0007_auto_20180524_2009.py +++ b/api/funkwhale_api/users/migrations/0007_auto_20180524_2009.py @@ -5,29 +5,37 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('users', '0006_auto_20180517_2324'), - ] + dependencies = [("users", "0006_auto_20180517_2324")] operations = [ migrations.AddField( - model_name='user', - name='permission_upload', - field=models.BooleanField(default=False, verbose_name='Upload new content to the library'), + model_name="user", + name="permission_upload", + field=models.BooleanField( + default=False, verbose_name="Upload new content to the library" + ), ), migrations.AlterField( - model_name='user', - name='permission_federation', - field=models.BooleanField(default=False, help_text='Follow other instances, accept/deny library follow requests...', verbose_name='Manage library federation'), + model_name="user", + name="permission_federation", + field=models.BooleanField( + default=False, + help_text="Follow other instances, accept/deny library follow requests...", + verbose_name="Manage library federation", + ), ), migrations.AlterField( - model_name='user', - name='permission_library', - field=models.BooleanField(default=False, help_text='Manage library', verbose_name='Manage library'), + model_name="user", + name="permission_library", + field=models.BooleanField( + default=False, help_text="Manage library", verbose_name="Manage library" + ), ), migrations.AlterField( - model_name='user', - name='permission_settings', - field=models.BooleanField(default=False, verbose_name='Manage instance-level settings'), + model_name="user", + name="permission_settings", + field=models.BooleanField( + default=False, verbose_name="Manage instance-level settings" + ), ), ] diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py index fcf78d047..281fc4fe8 100644 --- a/api/funkwhale_api/users/models.py +++ b/api/funkwhale_api/users/models.py @@ -17,26 +17,20 @@ from funkwhale_api.common import preferences def get_token(): - return binascii.b2a_hex(os.urandom(15)).decode('utf-8') + return binascii.b2a_hex(os.urandom(15)).decode("utf-8") PERMISSIONS_CONFIGURATION = { - 'federation': { - 'label': 'Manage library federation', - 'help_text': 'Follow other instances, accept/deny library follow requests...', + "federation": { + "label": "Manage library federation", + "help_text": "Follow other instances, accept/deny library follow requests...", }, - 'library': { - 'label': 'Manage library', - 'help_text': 'Manage library, delete files, tracks, artists, albums...', - }, - 'settings': { - 'label': 'Manage instance-level settings', - 'help_text': '', - }, - 'upload': { - 'label': 'Upload new content to the library', - 'help_text': '', + "library": { + "label": "Manage library", + "help_text": "Manage library, delete files, tracks, artists, albums...", }, + "settings": {"label": "Manage instance-level settings", "help_text": ""}, + "upload": {"label": "Upload new content to the library", "help_text": ""}, } PERMISSIONS = sorted(PERMISSIONS_CONFIGURATION.keys()) @@ -58,51 +52,54 @@ class User(AbstractUser): # anyway since django use stronger schemes for storing passwords. # Users that want to use the subsonic API from external client # should set this token and use it as their password in such clients - subsonic_api_token = models.CharField( - blank=True, null=True, max_length=255) + subsonic_api_token = models.CharField(blank=True, null=True, max_length=255) # permissions permission_federation = models.BooleanField( - PERMISSIONS_CONFIGURATION['federation']['label'], - help_text=PERMISSIONS_CONFIGURATION['federation']['help_text'], - default=False) + PERMISSIONS_CONFIGURATION["federation"]["label"], + help_text=PERMISSIONS_CONFIGURATION["federation"]["help_text"], + default=False, + ) permission_library = models.BooleanField( - PERMISSIONS_CONFIGURATION['library']['label'], - help_text=PERMISSIONS_CONFIGURATION['library']['help_text'], - default=False) + PERMISSIONS_CONFIGURATION["library"]["label"], + help_text=PERMISSIONS_CONFIGURATION["library"]["help_text"], + default=False, + ) permission_settings = models.BooleanField( - PERMISSIONS_CONFIGURATION['settings']['label'], - help_text=PERMISSIONS_CONFIGURATION['settings']['help_text'], - default=False) + PERMISSIONS_CONFIGURATION["settings"]["label"], + help_text=PERMISSIONS_CONFIGURATION["settings"]["help_text"], + default=False, + ) permission_upload = models.BooleanField( - PERMISSIONS_CONFIGURATION['upload']['label'], - help_text=PERMISSIONS_CONFIGURATION['upload']['help_text'], - default=False) + PERMISSIONS_CONFIGURATION["upload"]["label"], + help_text=PERMISSIONS_CONFIGURATION["upload"]["help_text"], + default=False, + ) def __str__(self): return self.username def get_permissions(self): - defaults = preferences.get('users__default_permissions') + defaults = preferences.get("users__default_permissions") perms = {} for p in PERMISSIONS: v = ( - self.is_superuser or - getattr(self, 'permission_{}'.format(p)) or - p in defaults + self.is_superuser + or getattr(self, "permission_{}".format(p)) + or p in defaults ) perms[p] = v return perms - def has_permissions(self, *perms, operator='and'): - if operator not in ['and', 'or']: - raise ValueError('Invalid operator {}'.format(operator)) + def has_permissions(self, *perms, operator="and"): + if operator not in ["and", "or"]: + raise ValueError("Invalid operator {}".format(operator)) permissions = self.get_permissions() - checker = all if operator == 'and' else any + checker = all if operator == "and" else any return checker([permissions[p] for p in perms]) def get_absolute_url(self): - return reverse('users:detail', kwargs={'username': self.username}) + return reverse("users:detail", kwargs={"username": self.username}) def update_secret_key(self): self.secret_key = uuid.uuid4() @@ -119,4 +116,4 @@ class User(AbstractUser): self.update_subsonic_api_token() def get_activity_url(self): - return settings.FUNKWHALE_URL + '/@{}'.format(self.username) + return settings.FUNKWHALE_URL + "/@{}".format(self.username) diff --git a/api/funkwhale_api/users/permissions.py b/api/funkwhale_api/users/permissions.py index 146bc5e1c..02c1198e8 100644 --- a/api/funkwhale_api/users/permissions.py +++ b/api/funkwhale_api/users/permissions.py @@ -11,11 +11,13 @@ class HasUserPermission(BasePermission): permission_classes = [HasUserPermission] required_permissions = ['federation'] """ + def has_permission(self, request, view): - if not hasattr(request, 'user') or not request.user: + if not hasattr(request, "user") or not request.user: return False if request.user.is_anonymous: return False - operator = getattr(view, 'permission_operator', 'and') + operator = getattr(view, "permission_operator", "and") return request.user.has_permissions( - *view.required_permissions, operator=operator) + *view.required_permissions, operator=operator + ) diff --git a/api/funkwhale_api/users/rest_auth_urls.py b/api/funkwhale_api/users/rest_auth_urls.py index fa6c425cc..f5015ecbd 100644 --- a/api/funkwhale_api/users/rest_auth_urls.py +++ b/api/funkwhale_api/users/rest_auth_urls.py @@ -8,25 +8,30 @@ from . import views urlpatterns = [ - url(r'^$', views.RegisterView.as_view(), name='rest_register'), - url(r'^verify-email/$', + url(r"^$", views.RegisterView.as_view(), name="rest_register"), + url( + r"^verify-email/$", registration_views.VerifyEmailView.as_view(), - name='rest_verify_email'), - url(r'^change-password/$', + name="rest_verify_email", + ), + url( + r"^change-password/$", rest_auth_views.PasswordChangeView.as_view(), - name='change_password'), - + name="change_password", + ), # This url is used by django-allauth and empty TemplateView is # defined just to allow reverse() call inside app, for example when email # with verification link is being sent, then it's required to render email # content. - # account_confirm_email - You should override this view to handle it in # your API client somehow and then, send post to /verify-email/ endpoint # with proper key. # If you don't want to use API on that step, then just use ConfirmEmailView # view from: # djang-allauth https://github.com/pennersr/django-allauth/blob/master/allauth/account/views.py#L190 - url(r'^account-confirm-email/(?P\w+)/$', TemplateView.as_view(), - name='account_confirm_email'), + url( + r"^account-confirm-email/(?P\w+)/$", + TemplateView.as_view(), + name="account_confirm_email", + ), ] diff --git a/api/funkwhale_api/users/serializers.py b/api/funkwhale_api/users/serializers.py index 3a095e78a..ab289bc51 100644 --- a/api/funkwhale_api/users/serializers.py +++ b/api/funkwhale_api/users/serializers.py @@ -9,35 +9,27 @@ from . import models class UserActivitySerializer(activity_serializers.ModelSerializer): type = serializers.SerializerMethodField() - name = serializers.CharField(source='username') - local_id = serializers.CharField(source='username') + name = serializers.CharField(source="username") + local_id = serializers.CharField(source="username") class Meta: model = models.User - fields = [ - 'id', - 'local_id', - 'name', - 'type' - ] + fields = ["id", "local_id", "name", "type"] def get_type(self, obj): - return 'Person' + return "Person" class UserBasicSerializer(serializers.ModelSerializer): class Meta: model = models.User - fields = ['id', 'username', 'name', 'date_joined'] + fields = ["id", "username", "name", "date_joined"] class UserWriteSerializer(serializers.ModelSerializer): class Meta: model = models.User - fields = [ - 'name', - 'privacy_level' - ] + fields = ["name", "privacy_level"] class UserReadSerializer(serializers.ModelSerializer): @@ -47,15 +39,15 @@ class UserReadSerializer(serializers.ModelSerializer): class Meta: model = models.User fields = [ - 'id', - 'username', - 'name', - 'email', - 'is_staff', - 'is_superuser', - 'permissions', - 'date_joined', - 'privacy_level', + "id", + "username", + "name", + "email", + "is_staff", + "is_superuser", + "permissions", + "date_joined", + "privacy_level", ] def get_permissions(self, o): @@ -64,8 +56,4 @@ class UserReadSerializer(serializers.ModelSerializer): class PasswordResetSerializer(PRS): def get_email_options(self): - return { - 'extra_email_context': { - 'funkwhale_url': settings.FUNKWHALE_URL - } - } + return {"extra_email_context": {"funkwhale_url": settings.FUNKWHALE_URL}} diff --git a/api/funkwhale_api/users/views.py b/api/funkwhale_api/users/views.py index 0cc317889..dfb71dd05 100644 --- a/api/funkwhale_api/users/views.py +++ b/api/funkwhale_api/users/views.py @@ -13,12 +13,9 @@ from . import serializers class RegisterView(BaseRegisterView): - def create(self, request, *args, **kwargs): if not self.is_open_for_signup(request): - r = { - 'detail': 'Registration has been disabled', - } + r = {"detail": "Registration has been disabled"} return Response(r, status=403) return super().create(request, *args, **kwargs) @@ -26,47 +23,42 @@ class RegisterView(BaseRegisterView): return get_adapter().is_open_for_signup(request) -class UserViewSet( - mixins.UpdateModelMixin, - viewsets.GenericViewSet): +class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet): queryset = models.User.objects.all() serializer_class = serializers.UserWriteSerializer - lookup_field = 'username' + lookup_field = "username" - @list_route(methods=['get']) + @list_route(methods=["get"]) def me(self, request, *args, **kwargs): """Return information about the current user""" serializer = serializers.UserReadSerializer(request.user) return Response(serializer.data) - @detail_route( - methods=['get', 'post', 'delete'], url_path='subsonic-token') + @detail_route(methods=["get", "post", "delete"], url_path="subsonic-token") def subsonic_token(self, request, *args, **kwargs): - if not self.request.user.username == kwargs.get('username'): + if not self.request.user.username == kwargs.get("username"): return Response(status=403) - if not preferences.get('subsonic__enabled'): + if not preferences.get("subsonic__enabled"): return Response(status=405) - if request.method.lower() == 'get': - return Response({ - 'subsonic_api_token': self.request.user.subsonic_api_token - }) - if request.method.lower() == 'delete': + if request.method.lower() == "get": + return Response( + {"subsonic_api_token": self.request.user.subsonic_api_token} + ) + if request.method.lower() == "delete": self.request.user.subsonic_api_token = None - self.request.user.save(update_fields=['subsonic_api_token']) + self.request.user.save(update_fields=["subsonic_api_token"]) return Response(status=204) self.request.user.update_subsonic_api_token() - self.request.user.save(update_fields=['subsonic_api_token']) - data = { - 'subsonic_api_token': self.request.user.subsonic_api_token - } + self.request.user.save(update_fields=["subsonic_api_token"]) + data = {"subsonic_api_token": self.request.user.subsonic_api_token} return Response(data) def update(self, request, *args, **kwargs): - if not self.request.user.username == kwargs.get('username'): + if not self.request.user.username == kwargs.get("username"): return Response(status=403) return super().update(request, *args, **kwargs) def partial_update(self, request, *args, **kwargs): - if not self.request.user.username == kwargs.get('username'): + if not self.request.user.username == kwargs.get("username"): return Response(status=403) return super().partial_update(request, *args, **kwargs) diff --git a/api/tests/activity/test_record.py b/api/tests/activity/test_record.py index 41846ba6f..98cefaba2 100644 --- a/api/tests/activity/test_record.py +++ b/api/tests/activity/test_record.py @@ -8,36 +8,35 @@ from funkwhale_api.activity import record class FakeModel(models.Model): class Meta: - app_label = 'tests' + app_label = "tests" class FakeSerializer(serializers.ModelSerializer): class Meta: model = FakeModel - fields = ['id'] - - + fields = ["id"] def test_can_bind_serializer_to_model(activity_registry): activity_registry.register_serializer(FakeSerializer) - assert activity_registry['tests.FakeModel']['serializer'] == FakeSerializer + assert activity_registry["tests.FakeModel"]["serializer"] == FakeSerializer def test_can_bind_consumer_to_model(activity_registry): activity_registry.register_serializer(FakeSerializer) - @activity_registry.register_consumer('tests.FakeModel') + + @activity_registry.register_consumer("tests.FakeModel") def propagate(data, obj): return True - assert activity_registry['tests.FakeModel']['consumers'] == [propagate] + assert activity_registry["tests.FakeModel"]["consumers"] == [propagate] def test_record_object_calls_consumer(activity_registry, mocker): activity_registry.register_serializer(FakeSerializer) stub = mocker.stub() - activity_registry.register_consumer('tests.FakeModel')(stub) + activity_registry.register_consumer("tests.FakeModel")(stub) o = FakeModel(id=1) data = FakeSerializer(o).data record.send(o) diff --git a/api/tests/activity/test_serializers.py b/api/tests/activity/test_serializers.py index 792fa74b9..2561b5c8c 100644 --- a/api/tests/activity/test_serializers.py +++ b/api/tests/activity/test_serializers.py @@ -1,12 +1,11 @@ from funkwhale_api.activity import serializers from funkwhale_api.favorites.serializers import TrackFavoriteActivitySerializer -from funkwhale_api.history.serializers import \ - ListeningActivitySerializer +from funkwhale_api.history.serializers import ListeningActivitySerializer def test_autoserializer(factories): - favorite = factories['favorites.TrackFavorite']() - listening = factories['history.Listening']() + favorite = factories["favorites.TrackFavorite"]() + listening = factories["history.Listening"]() objects = [favorite, listening] serializer = serializers.AutoSerializer(objects, many=True) expected = [ diff --git a/api/tests/activity/test_utils.py b/api/tests/activity/test_utils.py index 43bb45df8..b12e3d0cb 100644 --- a/api/tests/activity/test_utils.py +++ b/api/tests/activity/test_utils.py @@ -2,20 +2,18 @@ from funkwhale_api.activity import utils def test_get_activity(factories): - user = factories['users.User']() - listening = factories['history.Listening']() - favorite = factories['favorites.TrackFavorite']() + user = factories["users.User"]() + listening = factories["history.Listening"]() + favorite = factories["favorites.TrackFavorite"]() objects = list(utils.get_activity(user)) assert objects == [favorite, listening] def test_get_activity_honors_privacy_level(factories, anonymous_user): - listening = factories['history.Listening'](user__privacy_level='me') - favorite1 = factories['favorites.TrackFavorite']( - user__privacy_level='everyone') - favorite2 = factories['favorites.TrackFavorite']( - user__privacy_level='instance') + listening = factories["history.Listening"](user__privacy_level="me") + favorite1 = factories["favorites.TrackFavorite"](user__privacy_level="everyone") + favorite2 = factories["favorites.TrackFavorite"](user__privacy_level="instance") objects = list(utils.get_activity(anonymous_user)) assert objects == [favorite1] diff --git a/api/tests/activity/test_views.py b/api/tests/activity/test_views.py index 9b24f3ad3..188039e66 100644 --- a/api/tests/activity/test_views.py +++ b/api/tests/activity/test_views.py @@ -5,14 +5,13 @@ from funkwhale_api.activity import utils def test_activity_view(factories, api_client, preferences, anonymous_user): - preferences['common__api_authentication_required'] = False - favorite = factories['favorites.TrackFavorite']( - user__privacy_level='everyone') - listening = factories['history.Listening']() - url = reverse('api:v1:activity-list') + preferences["common__api_authentication_required"] = False + favorite = factories["favorites.TrackFavorite"](user__privacy_level="everyone") + listening = factories["history.Listening"]() + url = reverse("api:v1:activity-list") objects = utils.get_activity(anonymous_user) serializer = serializers.AutoSerializer(objects, many=True) response = api_client.get(url) assert response.status_code == 200 - assert response.data['results'] == serializer.data + assert response.data["results"] == serializer.data diff --git a/api/tests/channels/test_auth.py b/api/tests/channels/test_auth.py index a2b7eaf0c..19bdebe68 100644 --- a/api/tests/channels/test_auth.py +++ b/api/tests/channels/test_auth.py @@ -8,30 +8,24 @@ jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER -@pytest.mark.parametrize('query_string', [ - b'token=wrong', - b'', -]) +@pytest.mark.parametrize("query_string", [b"token=wrong", b""]) def test_header_anonymous(query_string, factories): def callback(scope): - assert scope['user'].is_anonymous + assert scope["user"].is_anonymous - scope = { - 'query_string': query_string - } + scope = {"query_string": query_string} consumer = TokenAuthMiddleware(callback) consumer(scope) def test_header_correct_token(factories): - user = factories['users.User']() + user = factories["users.User"]() payload = jwt_payload_handler(user) token = jwt_encode_handler(payload) - def callback(scope): - assert scope['user'] == user - scope = { - 'query_string': 'token={}'.format(token).encode('utf-8') - } + def callback(scope): + assert scope["user"] == user + + scope = {"query_string": "token={}".format(token).encode("utf-8")} consumer = TokenAuthMiddleware(callback) consumer(scope) diff --git a/api/tests/channels/test_consumers.py b/api/tests/channels/test_consumers.py index f1648efb3..f4f5f3ad6 100644 --- a/api/tests/channels/test_consumers.py +++ b/api/tests/channels/test_consumers.py @@ -2,15 +2,15 @@ from funkwhale_api.common import consumers def test_auth_consumer_requires_valid_user(mocker): - m = mocker.patch('funkwhale_api.common.consumers.JsonAuthConsumer.close') - scope = {'user': None} + m = mocker.patch("funkwhale_api.common.consumers.JsonAuthConsumer.close") + scope = {"user": None} consumer = consumers.JsonAuthConsumer(scope=scope) consumer.connect() m.assert_called_once_with() def test_auth_consumer_requires_user_in_scope(mocker): - m = mocker.patch('funkwhale_api.common.consumers.JsonAuthConsumer.close') + m = mocker.patch("funkwhale_api.common.consumers.JsonAuthConsumer.close") scope = {} consumer = consumers.JsonAuthConsumer(scope=scope) consumer.connect() @@ -18,9 +18,9 @@ def test_auth_consumer_requires_user_in_scope(mocker): def test_auth_consumer_accepts_connection(mocker, factories): - user = factories['users.User']() - m = mocker.patch('funkwhale_api.common.consumers.JsonAuthConsumer.accept') - scope = {'user': user} + user = factories["users.User"]() + m = mocker.patch("funkwhale_api.common.consumers.JsonAuthConsumer.accept") + scope = {"user": user} consumer = consumers.JsonAuthConsumer(scope=scope) consumer.connect() m.assert_called_once_with() diff --git a/api/tests/common/test_fields.py b/api/tests/common/test_fields.py index 29a8fb05c..bd836f840 100644 --- a/api/tests/common/test_fields.py +++ b/api/tests/common/test_fields.py @@ -7,11 +7,16 @@ from funkwhale_api.common import fields from funkwhale_api.users.factories import UserFactory -@pytest.mark.parametrize('user,expected', [ - (AnonymousUser(), Q(privacy_level='everyone')), - (UserFactory.build(pk=1), - Q(privacy_level__in=['followers', 'instance', 'everyone'])), -]) -def test_privacy_level_query(user,expected): +@pytest.mark.parametrize( + "user,expected", + [ + (AnonymousUser(), Q(privacy_level="everyone")), + ( + UserFactory.build(pk=1), + Q(privacy_level__in=["followers", "instance", "everyone"]), + ), + ], +) +def test_privacy_level_query(user, expected): query = fields.privacy_level_query(user) assert query == expected diff --git a/api/tests/common/test_permissions.py b/api/tests/common/test_permissions.py index f04f12e0b..952ec589e 100644 --- a/api/tests/common/test_permissions.py +++ b/api/tests/common/test_permissions.py @@ -8,36 +8,36 @@ from funkwhale_api.common import permissions def test_owner_permission_owner_field_ok(nodb_factories, api_request): - playlist = nodb_factories['playlists.Playlist']() + playlist = nodb_factories["playlists.Playlist"]() view = APIView.as_view() permission = permissions.OwnerPermission() - request = api_request.get('/') - setattr(request, 'user', playlist.user) + request = api_request.get("/") + setattr(request, "user", playlist.user) check = permission.has_object_permission(request, view, playlist) assert check is True def test_owner_permission_owner_field_not_ok( - anonymous_user, nodb_factories, api_request): - playlist = nodb_factories['playlists.Playlist']() + anonymous_user, nodb_factories, api_request +): + playlist = nodb_factories["playlists.Playlist"]() view = APIView.as_view() permission = permissions.OwnerPermission() - request = api_request.get('/') - setattr(request, 'user', anonymous_user) + request = api_request.get("/") + setattr(request, "user", anonymous_user) with pytest.raises(Http404): permission.has_object_permission(request, view, playlist) -def test_owner_permission_read_only( - anonymous_user, nodb_factories, api_request): - playlist = nodb_factories['playlists.Playlist']() +def test_owner_permission_read_only(anonymous_user, nodb_factories, api_request): + playlist = nodb_factories["playlists.Playlist"]() view = APIView.as_view() - setattr(view, 'owner_checks', ['write']) + setattr(view, "owner_checks", ["write"]) permission = permissions.OwnerPermission() - request = api_request.get('/') - setattr(request, 'user', anonymous_user) + request = api_request.get("/") + setattr(request, "user", anonymous_user) check = permission.has_object_permission(request, view, playlist) assert check is True diff --git a/api/tests/common/test_preferences.py b/api/tests/common/test_preferences.py index 475610a93..5ea398c87 100644 --- a/api/tests/common/test_preferences.py +++ b/api/tests/common/test_preferences.py @@ -6,39 +6,40 @@ from funkwhale_api.common import preferences as common_preferences @pytest.fixture def string_list_pref(preferences): - @global_preferences_registry.register class P(common_preferences.StringListPreference): - default = ['hello'] - section = 'test' - name = 'string_list' + default = ["hello"] + section = "test" + name = "string_list" + yield - del global_preferences_registry['test']['string_list'] + del global_preferences_registry["test"]["string_list"] -@pytest.mark.parametrize('input,output', [ - (['a', 'b', 'c'], 'a,b,c'), - (['a', 'c', 'b'], 'a,b,c'), - (('a', 'c', 'b'), 'a,b,c'), - ([], None), -]) +@pytest.mark.parametrize( + "input,output", + [ + (["a", "b", "c"], "a,b,c"), + (["a", "c", "b"], "a,b,c"), + (("a", "c", "b"), "a,b,c"), + ([], None), + ], +) def test_string_list_serializer_to_db(input, output): s = common_preferences.StringListSerializer.to_db(input) == output -@pytest.mark.parametrize('input,output', [ - ('a,b,c', ['a', 'b', 'c'], ), - (None, []), - ('', []), -]) +@pytest.mark.parametrize( + "input,output", [("a,b,c", ["a", "b", "c"]), (None, []), ("", [])] +) def test_string_list_serializer_to_python(input, output): s = common_preferences.StringListSerializer.to_python(input) == output def test_string_list_pref_default(string_list_pref, preferences): - assert preferences['test__string_list'] == ['hello'] + assert preferences["test__string_list"] == ["hello"] def test_string_list_pref_set(string_list_pref, preferences): - preferences['test__string_list'] = ['world', 'hello'] - assert preferences['test__string_list'] == ['hello', 'world'] + preferences["test__string_list"] = ["world", "hello"] + assert preferences["test__string_list"] == ["hello", "world"] diff --git a/api/tests/common/test_scripts.py b/api/tests/common/test_scripts.py index ce478ba04..afedc3e3a 100644 --- a/api/tests/common/test_scripts.py +++ b/api/tests/common/test_scripts.py @@ -9,38 +9,26 @@ def command(): return script.Command() -@pytest.mark.parametrize('script_name', [ - 'django_permissions_to_user_permissions', - 'test', -]) +@pytest.mark.parametrize( + "script_name", ["django_permissions_to_user_permissions", "test"] +) def test_script_command_list(command, script_name, mocker): - mocked = mocker.patch( - 'funkwhale_api.common.scripts.{}.main'.format(script_name)) + mocked = mocker.patch("funkwhale_api.common.scripts.{}.main".format(script_name)) command.handle(script_name=script_name, interactive=False) - mocked.assert_called_once_with( - command, script_name=script_name, interactive=False) + mocked.assert_called_once_with(command, script_name=script_name, interactive=False) def test_django_permissions_to_user_permissions(factories, command): - group = factories['auth.Group']( + group = factories["auth.Group"](perms=["federation.change_library"]) + user1 = factories["users.User"]( perms=[ - 'federation.change_library' + "dynamic_preferences.change_globalpreferencemodel", + "music.add_importbatch", ] ) - user1 = factories['users.User']( - perms=[ - 'dynamic_preferences.change_globalpreferencemodel', - 'music.add_importbatch', - ] - ) - user2 = factories['users.User']( - perms=[ - 'music.add_importbatch', - ], - groups=[group] - ) + user2 = factories["users.User"](perms=["music.add_importbatch"], groups=[group]) scripts.django_permissions_to_user_permissions.main(command) diff --git a/api/tests/common/test_serializers.py b/api/tests/common/test_serializers.py index f0f5fb7e6..1bf13b1e0 100644 --- a/api/tests/common/test_serializers.py +++ b/api/tests/common/test_serializers.py @@ -7,20 +7,20 @@ from funkwhale_api.users import models class TestActionFilterSet(django_filters.FilterSet): class Meta: model = models.User - fields = ['is_active'] + fields = ["is_active"] class TestSerializer(serializers.ActionSerializer): - actions = ['test'] + actions = ["test"] filterset_class = TestActionFilterSet def handle_test(self, objects): - return {'hello': 'world'} + return {"hello": "world"} class TestDangerousSerializer(serializers.ActionSerializer): - actions = ['test', 'test_dangerous'] - dangerous_actions = ['test_dangerous'] + actions = ["test", "test_dangerous"] + dangerous_actions = ["test_dangerous"] def handle_test(self, objects): pass @@ -30,107 +30,88 @@ class TestDangerousSerializer(serializers.ActionSerializer): def test_action_serializer_validates_action(): - data = {'objects': 'all', 'action': 'nope'} + data = {"objects": "all", "action": "nope"} serializer = TestSerializer(data, queryset=models.User.objects.none()) assert serializer.is_valid() is False - assert 'action' in serializer.errors + assert "action" in serializer.errors def test_action_serializer_validates_objects(): - data = {'objects': 'nope', 'action': 'test'} + data = {"objects": "nope", "action": "test"} serializer = TestSerializer(data, queryset=models.User.objects.none()) assert serializer.is_valid() is False - assert 'objects' in serializer.errors + assert "objects" in serializer.errors def test_action_serializers_objects_clean_ids(factories): - user1 = factories['users.User']() - user2 = factories['users.User']() + user1 = factories["users.User"]() + user2 = factories["users.User"]() - data = {'objects': [user1.pk], 'action': 'test'} + data = {"objects": [user1.pk], "action": "test"} serializer = TestSerializer(data, queryset=models.User.objects.all()) assert serializer.is_valid() is True - assert list(serializer.validated_data['objects']) == [user1] + assert list(serializer.validated_data["objects"]) == [user1] def test_action_serializers_objects_clean_all(factories): - user1 = factories['users.User']() - user2 = factories['users.User']() + user1 = factories["users.User"]() + user2 = factories["users.User"]() - data = {'objects': 'all', 'action': 'test'} + data = {"objects": "all", "action": "test"} serializer = TestSerializer(data, queryset=models.User.objects.all()) assert serializer.is_valid() is True - assert list(serializer.validated_data['objects']) == [user1, user2] + assert list(serializer.validated_data["objects"]) == [user1, user2] def test_action_serializers_save(factories, mocker): - handler = mocker.spy(TestSerializer, 'handle_test') - user1 = factories['users.User']() - user2 = factories['users.User']() + handler = mocker.spy(TestSerializer, "handle_test") + user1 = factories["users.User"]() + user2 = factories["users.User"]() - data = {'objects': 'all', 'action': 'test'} + data = {"objects": "all", "action": "test"} serializer = TestSerializer(data, queryset=models.User.objects.all()) assert serializer.is_valid() is True result = serializer.save() - assert result == { - 'updated': 2, - 'action': 'test', - 'result': {'hello': 'world'}, - } + assert result == {"updated": 2, "action": "test", "result": {"hello": "world"}} handler.assert_called_once() def test_action_serializers_filterset(factories): - user1 = factories['users.User'](is_active=False) - user2 = factories['users.User'](is_active=True) + user1 = factories["users.User"](is_active=False) + user2 = factories["users.User"](is_active=True) - data = { - 'objects': 'all', - 'action': 'test', - 'filters': {'is_active': True}, - } + data = {"objects": "all", "action": "test", "filters": {"is_active": True}} serializer = TestSerializer(data, queryset=models.User.objects.all()) assert serializer.is_valid() is True - assert list(serializer.validated_data['objects']) == [user2] + assert list(serializer.validated_data["objects"]) == [user2] def test_action_serializers_validates_at_least_one_object(): - data = { - 'objects': 'all', - 'action': 'test', - } + data = {"objects": "all", "action": "test"} serializer = TestSerializer(data, queryset=models.User.objects.none()) assert serializer.is_valid() is False - assert 'non_field_errors' in serializer.errors + assert "non_field_errors" in serializer.errors def test_dangerous_actions_refuses_all(factories): - factories['users.User']() - data = { - 'objects': 'all', - 'action': 'test_dangerous', - } - serializer = TestDangerousSerializer( - data, queryset=models.User.objects.all()) + factories["users.User"]() + data = {"objects": "all", "action": "test_dangerous"} + serializer = TestDangerousSerializer(data, queryset=models.User.objects.all()) assert serializer.is_valid() is False - assert 'non_field_errors' in serializer.errors + assert "non_field_errors" in serializer.errors def test_dangerous_actions_refuses_not_listed(factories): - factories['users.User']() - data = { - 'objects': 'all', - 'action': 'test', - } - serializer = TestDangerousSerializer( - data, queryset=models.User.objects.all()) + factories["users.User"]() + data = {"objects": "all", "action": "test"} + serializer = TestDangerousSerializer(data, queryset=models.User.objects.all()) assert serializer.is_valid() is True diff --git a/api/tests/common/test_session.py b/api/tests/common/test_session.py index 7ff1e660b..eba26dae0 100644 --- a/api/tests/common/test_session.py +++ b/api/tests/common/test_session.py @@ -4,15 +4,14 @@ from funkwhale_api.common import session def test_get_user_agent(settings): - settings.FUNKWHALE_URL = 'https://test.com' - 'http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)' - expected = 'python-requests (funkwhale/{}; +{})'.format( - funkwhale_api.__version__, - settings.FUNKWHALE_URL + settings.FUNKWHALE_URL = "https://test.com" + "http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)" + expected = "python-requests (funkwhale/{}; +{})".format( + funkwhale_api.__version__, settings.FUNKWHALE_URL ) assert session.get_user_agent() == expected def test_get_session(): expected = session.get_user_agent() - assert session.get_session().headers['User-Agent'] == expected + assert session.get_session().headers["User-Agent"] == expected diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 7caff2009..080e1a927 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -24,6 +24,7 @@ from funkwhale_api.taskapp import celery def factories_autodiscover(): from django.apps import apps from funkwhale_api import factories + app_names = [app.name for app in apps.app_configs.values()] factories.registry.autodiscover(app_names) @@ -44,6 +45,7 @@ def factories(db): users.User or music.Track """ from funkwhale_api import factories + for v in factories.registry.values(): try: v._meta.strategy = factory.CREATE_STRATEGY @@ -60,6 +62,7 @@ def nodb_factories(): that does not require access to the database """ from funkwhale_api import factories + for v in factories.registry.values(): try: v._meta.strategy = factory.BUILD_STRATEGY @@ -104,11 +107,11 @@ def logged_in_client(db, factories, client): Returns a logged-in, non-API client with an authenticated ``User`` stored in the ``user`` attribute """ - user = factories['users.User']() - assert client.login(username=user.username, password='test') - setattr(client, 'user', user) + user = factories["users.User"]() + assert client.login(username=user.username, password="test") + setattr(client, "user", user) yield client - delattr(client, 'user') + delattr(client, "user") @pytest.fixture @@ -131,12 +134,12 @@ def logged_in_api_client(db, factories, api_client): Return a logged-in API client with an authenticated ``User`` stored in the ``user`` attribute """ - user = factories['users.User']() - assert api_client.login(username=user.username, password='test') + user = factories["users.User"]() + assert api_client.login(username=user.username, password="test") api_client.force_authenticate(user=user) - setattr(api_client, 'user', user) + setattr(api_client, "user", user) yield api_client - delattr(api_client, 'user') + delattr(api_client, "user") @pytest.fixture @@ -145,11 +148,11 @@ def superuser_api_client(db, factories, api_client): Return a logged-in API client with an authenticated superuser stored in the ``user`` attribute """ - user = factories['users.SuperUser']() - assert api_client.login(username=user.username, password='test') - setattr(api_client, 'user', user) + user = factories["users.SuperUser"]() + assert api_client.login(username=user.username, password="test") + setattr(api_client, "user", user) yield api_client - delattr(api_client, 'user') + delattr(api_client, "user") @pytest.fixture @@ -158,11 +161,11 @@ def superuser_client(db, factories, client): Return a logged-in, non-API client with an authenticated ``User`` stored in the ``user`` attribute """ - user = factories['users.SuperUser']() - assert client.login(username=user.username, password='test') - setattr(client, 'user', user) + user = factories["users.SuperUser"]() + assert client.login(username=user.username, password="test") + setattr(client, "user", user) yield client - delattr(client, 'user') + delattr(client, "user") @pytest.fixture @@ -193,7 +196,7 @@ def activity_registry(): @pytest.fixture def activity_muted(activity_registry, mocker): - yield mocker.patch.object(record, 'send') + yield mocker.patch.object(record, "send") @pytest.fixture(autouse=True) @@ -222,19 +225,21 @@ def authenticated_actor(factories, mocker): """ Returns an authenticated ActivityPub actor """ - actor = factories['federation.Actor']() + actor = factories["federation.Actor"]() mocker.patch( - 'funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor', - return_value=actor) + "funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor", + return_value=actor, + ) yield actor @pytest.fixture def assert_user_permission(): - def inner(view, permissions, operator='and'): + def inner(view, permissions, operator="and"): assert HasUserPermission in view.permission_classes - assert getattr(view, 'permission_operator', 'and') == operator + assert getattr(view, "permission_operator", "and") == operator assert set(view.required_permissions) == set(permissions) + return inner @@ -247,5 +252,6 @@ def to_api_date(): if isinstance(value, datetime.date): f = rest_fields.DateField() return f.to_representation(value) - raise ValueError('Invalid value: {}'.format(value)) + raise ValueError("Invalid value: {}".format(value)) + return inner diff --git a/api/tests/data/youtube.py b/api/tests/data/youtube.py index a8372d4c9..d3303d2ad 100644 --- a/api/tests/data/youtube.py +++ b/api/tests/data/youtube.py @@ -3,20 +3,14 @@ search = {} -search['8 bit adventure'] = { - "pageInfo": { - "totalResults": 1000000, - "resultsPerPage": 25 - }, +search["8 bit adventure"] = { + "pageInfo": {"totalResults": 1000000, "resultsPerPage": 25}, "nextPageToken": "CBkQAA", - "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/1L34zetsKWv-raAFiz0MuT0SsfQ\"", + "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/1L34zetsKWv-raAFiz0MuT0SsfQ"', "items": [ { - "id": { - "videoId": "0HxZn6CzOIo", - "kind": "youtube#video" - }, - "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/GxK-wHBWUYfrJsd1dijBPTufrVE\"", + "id": {"videoId": "0HxZn6CzOIo", "kind": "youtube#video"}, + "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/GxK-wHBWUYfrJsd1dijBPTufrVE"', "snippet": { "liveBroadcastContent": "none", "description": "Make sure to apply adhesive evenly before use. GET IT HERE: http://adhesivewombat.bandcamp.com/album/marsupial-madness Facebook: ...", @@ -28,28 +22,25 @@ search['8 bit adventure'] = { "medium": { "url": "https://i.ytimg.com/vi/0HxZn6CzOIo/mqdefault.jpg", "height": 180, - "width": 320 + "width": 320, }, "high": { "url": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg", "height": 360, - "width": 480 + "width": 480, }, "default": { "url": "https://i.ytimg.com/vi/0HxZn6CzOIo/default.jpg", "height": 90, - "width": 120 - } - } + "width": 120, + }, + }, }, - "kind": "youtube#searchResult" + "kind": "youtube#searchResult", }, { - "id": { - "videoId": "n4A_F5SXmgo", - "kind": "youtube#video" - }, - "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/aRVESw24jlgiErDgJKxNrazKRDc\"", + "id": {"videoId": "n4A_F5SXmgo", "kind": "youtube#video"}, + "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/aRVESw24jlgiErDgJKxNrazKRDc"', "snippet": { "liveBroadcastContent": "none", "description": "Free Download: http://bit.ly/1fZ1pMJ I don't post 8 bit'ish music much but damn I must admit this is goood! Enjoy \u2665 \u25bbSpikedGrin: ...", @@ -61,34 +52,31 @@ search['8 bit adventure'] = { "medium": { "url": "https://i.ytimg.com/vi/n4A_F5SXmgo/mqdefault.jpg", "height": 180, - "width": 320 + "width": 320, }, "high": { "url": "https://i.ytimg.com/vi/n4A_F5SXmgo/hqdefault.jpg", "height": 360, - "width": 480 + "width": 480, }, "default": { "url": "https://i.ytimg.com/vi/n4A_F5SXmgo/default.jpg", "height": 90, - "width": 120 - } - } + "width": 120, + }, + }, }, - "kind": "youtube#searchResult" + "kind": "youtube#searchResult", }, ], "regionCode": "FR", - "kind": "youtube#searchListResponse" + "kind": "youtube#searchListResponse", } -search['system of a down toxicity'] = { +search["system of a down toxicity"] = { "items": [ { - "id": { - "kind": "youtube#video", - "videoId": "BorYwGi2SJc" - }, + "id": {"kind": "youtube#video", "videoId": "BorYwGi2SJc"}, "kind": "youtube#searchResult", "snippet": { "title": "System of a Down: Toxicity", @@ -98,30 +86,27 @@ search['system of a down toxicity'] = { "default": { "height": 90, "width": 120, - "url": "https://i.ytimg.com/vi/BorYwGi2SJc/default.jpg" + "url": "https://i.ytimg.com/vi/BorYwGi2SJc/default.jpg", }, "high": { "height": 360, "width": 480, - "url": "https://i.ytimg.com/vi/BorYwGi2SJc/hqdefault.jpg" + "url": "https://i.ytimg.com/vi/BorYwGi2SJc/hqdefault.jpg", }, "medium": { "height": 180, "width": 320, - "url": "https://i.ytimg.com/vi/BorYwGi2SJc/mqdefault.jpg" - } + "url": "https://i.ytimg.com/vi/BorYwGi2SJc/mqdefault.jpg", + }, }, "publishedAt": "2007-12-17T12:39:54.000Z", "description": "http://www.vedrescsaba.uw.hu The System of a Down song Toxicity arranged for a classical piano quintet, played by Vedres Csaba and the Kairosz quartet.", - "liveBroadcastContent": "none" + "liveBroadcastContent": "none", }, - "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/UwR8H6P6kbijNZmBNkYd2jAzDnI\"" + "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/UwR8H6P6kbijNZmBNkYd2jAzDnI"', }, { - "id": { - "kind": "youtube#video", - "videoId": "ENBv2i88g6Y" - }, + "id": {"kind": "youtube#video", "videoId": "ENBv2i88g6Y"}, "kind": "youtube#searchResult", "snippet": { "title": "System Of A Down - Question!", @@ -131,32 +116,29 @@ search['system of a down toxicity'] = { "default": { "height": 90, "width": 120, - "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/default.jpg" + "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/default.jpg", }, "high": { "height": 360, "width": 480, - "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/hqdefault.jpg" + "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/hqdefault.jpg", }, "medium": { "height": 180, "width": 320, - "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/mqdefault.jpg" - } + "url": "https://i.ytimg.com/vi/ENBv2i88g6Y/mqdefault.jpg", + }, }, "publishedAt": "2009-10-03T04:49:03.000Z", "description": "System of a Down's official music video for 'Question!'. Click to listen to System of a Down on Spotify: http://smarturl.it/SystemSpotify?IQid=SystemQu As featured ...", - "liveBroadcastContent": "none" + "liveBroadcastContent": "none", }, - "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/dB-M0N9mB4xE-k4yAF_4d8aU0I4\"" + "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/dB-M0N9mB4xE-k4yAF_4d8aU0I4"', }, ], - "etag": "\"gMxXHe-zinKdE9lTnzKu8vjcmDI/yhLQgSpeObNnybd5JqSzlGiJ8Ew\"", + "etag": '"gMxXHe-zinKdE9lTnzKu8vjcmDI/yhLQgSpeObNnybd5JqSzlGiJ8Ew"', "nextPageToken": "CBkQAA", - "pageInfo": { - "resultsPerPage": 25, - "totalResults": 26825 - }, + "pageInfo": {"resultsPerPage": 25, "totalResults": 26825}, "kind": "youtube#searchListResponse", - "regionCode": "FR" + "regionCode": "FR", } diff --git a/api/tests/favorites/test_activity.py b/api/tests/favorites/test_activity.py index 63174f9e2..a6eff9991 100644 --- a/api/tests/favorites/test_activity.py +++ b/api/tests/favorites/test_activity.py @@ -5,15 +5,14 @@ from funkwhale_api.favorites import activities def test_get_favorite_activity_url(settings, factories): - favorite = factories['favorites.TrackFavorite']() + favorite = factories["favorites.TrackFavorite"]() user_url = favorite.user.get_activity_url() - expected = '{}/favorites/tracks/{}'.format( - user_url, favorite.pk) + expected = "{}/favorites/tracks/{}".format(user_url, favorite.pk) assert favorite.get_activity_url() == expected def test_activity_favorite_serializer(factories): - favorite = factories['favorites.TrackFavorite']() + favorite = factories["favorites.TrackFavorite"]() actor = UserActivitySerializer(favorite.user).data field = serializers.serializers.DateTimeField() @@ -32,44 +31,31 @@ def test_activity_favorite_serializer(factories): def test_track_favorite_serializer_is_connected(activity_registry): - conf = activity_registry['favorites.TrackFavorite'] - assert conf['serializer'] == serializers.TrackFavoriteActivitySerializer + conf = activity_registry["favorites.TrackFavorite"] + assert conf["serializer"] == serializers.TrackFavoriteActivitySerializer -def test_track_favorite_serializer_instance_activity_consumer( - activity_registry): - conf = activity_registry['favorites.TrackFavorite'] +def test_track_favorite_serializer_instance_activity_consumer(activity_registry): + conf = activity_registry["favorites.TrackFavorite"] consumer = activities.broadcast_track_favorite_to_instance_activity - assert consumer in conf['consumers'] + assert consumer in conf["consumers"] -def test_broadcast_track_favorite_to_instance_activity( - factories, mocker): - p = mocker.patch('funkwhale_api.common.channels.group_send') - favorite = factories['favorites.TrackFavorite']() +def test_broadcast_track_favorite_to_instance_activity(factories, mocker): + p = mocker.patch("funkwhale_api.common.channels.group_send") + favorite = factories["favorites.TrackFavorite"]() data = serializers.TrackFavoriteActivitySerializer(favorite).data consumer = activities.broadcast_track_favorite_to_instance_activity - message = { - "type": 'event.send', - "text": '', - "data": data - } + message = {"type": "event.send", "text": "", "data": data} consumer(data=data, obj=favorite) - p.assert_called_once_with('instance_activity', message) + p.assert_called_once_with("instance_activity", message) -def test_broadcast_track_favorite_to_instance_activity_private( - factories, mocker): - p = mocker.patch('funkwhale_api.common.channels.group_send') - favorite = factories['favorites.TrackFavorite']( - user__privacy_level='me' - ) +def test_broadcast_track_favorite_to_instance_activity_private(factories, mocker): + p = mocker.patch("funkwhale_api.common.channels.group_send") + favorite = factories["favorites.TrackFavorite"](user__privacy_level="me") data = serializers.TrackFavoriteActivitySerializer(favorite).data consumer = activities.broadcast_track_favorite_to_instance_activity - message = { - "type": 'event.send', - "text": '', - "data": data - } + message = {"type": "event.send", "text": "", "data": data} consumer(data=data, obj=favorite) p.assert_not_called() diff --git a/api/tests/favorites/test_favorites.py b/api/tests/favorites/test_favorites.py index 591fe7c9c..345c24bde 100644 --- a/api/tests/favorites/test_favorites.py +++ b/api/tests/favorites/test_favorites.py @@ -6,10 +6,9 @@ from funkwhale_api.music.models import Track, Artist from funkwhale_api.favorites.models import TrackFavorite - def test_user_can_add_favorite(factories): - track = factories['music.Track']() - user = factories['users.User']() + track = factories["music.Track"]() + user = factories["users.User"]() f = TrackFavorite.add(track, user) assert f.track == track @@ -17,35 +16,34 @@ def test_user_can_add_favorite(factories): def test_user_can_get_his_favorites(factories, logged_in_client, client): - favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user) - url = reverse('api:v1:favorites:tracks-list') + favorite = factories["favorites.TrackFavorite"](user=logged_in_client.user) + url = reverse("api:v1:favorites:tracks-list") response = logged_in_client.get(url) expected = [ { - 'track': favorite.track.pk, - 'id': favorite.id, - 'creation_date': favorite.creation_date.isoformat().replace('+00:00', 'Z'), + "track": favorite.track.pk, + "id": favorite.id, + "creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"), } ] - parsed_json = json.loads(response.content.decode('utf-8')) + parsed_json = json.loads(response.content.decode("utf-8")) - assert expected == parsed_json['results'] + assert expected == parsed_json["results"] -def test_user_can_add_favorite_via_api( - factories, logged_in_client, activity_muted): - track = factories['music.Track']() - url = reverse('api:v1:favorites:tracks-list') - response = logged_in_client.post(url, {'track': track.pk}) +def test_user_can_add_favorite_via_api(factories, logged_in_client, activity_muted): + track = factories["music.Track"]() + url = reverse("api:v1:favorites:tracks-list") + response = logged_in_client.post(url, {"track": track.pk}) - favorite = TrackFavorite.objects.latest('id') + favorite = TrackFavorite.objects.latest("id") expected = { - 'track': track.pk, - 'id': favorite.id, - 'creation_date': favorite.creation_date.isoformat().replace('+00:00', 'Z'), + "track": track.pk, + "id": favorite.id, + "creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"), } - parsed_json = json.loads(response.content.decode('utf-8')) + parsed_json = json.loads(response.content.decode("utf-8")) assert expected == parsed_json assert favorite.track == track @@ -53,18 +51,19 @@ def test_user_can_add_favorite_via_api( def test_adding_favorites_calls_activity_record( - factories, logged_in_client, activity_muted): - track = factories['music.Track']() - url = reverse('api:v1:favorites:tracks-list') - response = logged_in_client.post(url, {'track': track.pk}) + factories, logged_in_client, activity_muted +): + track = factories["music.Track"]() + url = reverse("api:v1:favorites:tracks-list") + response = logged_in_client.post(url, {"track": track.pk}) - favorite = TrackFavorite.objects.latest('id') + favorite = TrackFavorite.objects.latest("id") expected = { - 'track': track.pk, - 'id': favorite.id, - 'creation_date': favorite.creation_date.isoformat().replace('+00:00', 'Z'), + "track": track.pk, + "id": favorite.id, + "creation_date": favorite.creation_date.isoformat().replace("+00:00", "Z"), } - parsed_json = json.loads(response.content.decode('utf-8')) + parsed_json = json.loads(response.content.decode("utf-8")) assert expected == parsed_json assert favorite.track == track @@ -74,44 +73,42 @@ def test_adding_favorites_calls_activity_record( def test_user_can_remove_favorite_via_api(logged_in_client, factories, client): - favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user) - url = reverse('api:v1:favorites:tracks-detail', kwargs={'pk': favorite.pk}) - response = client.delete(url, {'track': favorite.track.pk}) + favorite = factories["favorites.TrackFavorite"](user=logged_in_client.user) + url = reverse("api:v1:favorites:tracks-detail", kwargs={"pk": favorite.pk}) + response = client.delete(url, {"track": favorite.track.pk}) assert response.status_code == 204 assert TrackFavorite.objects.count() == 0 -@pytest.mark.parametrize('method', ['delete', 'post']) +@pytest.mark.parametrize("method", ["delete", "post"]) def test_user_can_remove_favorite_via_api_using_track_id( - method, factories, logged_in_client): - favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user) + method, factories, logged_in_client +): + favorite = factories["favorites.TrackFavorite"](user=logged_in_client.user) - url = reverse('api:v1:favorites:tracks-remove') + url = reverse("api:v1:favorites:tracks-remove") response = getattr(logged_in_client, method)( - url, json.dumps({'track': favorite.track.pk}), - content_type='application/json' + url, json.dumps({"track": favorite.track.pk}), content_type="application/json" ) assert response.status_code == 204 assert TrackFavorite.objects.count() == 0 -@pytest.mark.parametrize('url,method', [ - ('api:v1:favorites:tracks-list', 'get'), -]) +@pytest.mark.parametrize("url,method", [("api:v1:favorites:tracks-list", "get")]) def test_url_require_auth(url, method, db, preferences, client): - preferences['common__api_authentication_required'] = True + preferences["common__api_authentication_required"] = True url = reverse(url) response = getattr(client, method)(url) assert response.status_code == 401 def test_can_filter_tracks_by_favorites(factories, logged_in_client): - favorite = factories['favorites.TrackFavorite'](user=logged_in_client.user) + favorite = factories["favorites.TrackFavorite"](user=logged_in_client.user) - url = reverse('api:v1:tracks-list') - response = logged_in_client.get(url, data={'favorites': True}) + url = reverse("api:v1:tracks-list") + response = logged_in_client.get(url, data={"favorites": True}) - parsed_json = json.loads(response.content.decode('utf-8')) - assert parsed_json['count'] == 1 - assert parsed_json['results'][0]['id'] == favorite.track.id + parsed_json = json.loads(response.content.decode("utf-8")) + assert parsed_json["count"] == 1 + assert parsed_json["results"][0]["id"] == favorite.track.id diff --git a/api/tests/federation/test_activity.py b/api/tests/federation/test_activity.py index c2673ff3b..8e9000ce0 100644 --- a/api/tests/federation/test_activity.py +++ b/api/tests/federation/test_activity.py @@ -6,41 +6,34 @@ from funkwhale_api.federation import serializers def test_deliver(factories, r_mock, mocker, settings): settings.CELERY_TASK_ALWAYS_EAGER = True - to = factories['federation.Actor']() - mocker.patch( - 'funkwhale_api.federation.actors.get_actor', - return_value=to) - sender = factories['federation.Actor']() + to = factories["federation.Actor"]() + mocker.patch("funkwhale_api.federation.actors.get_actor", return_value=to) + sender = factories["federation.Actor"]() ac = { - 'id': 'http://test.federation/activity', - 'type': 'Create', - 'actor': sender.url, - 'object': { - 'id': 'http://test.federation/note', - 'type': 'Note', - 'content': 'Hello', - } + "id": "http://test.federation/activity", + "type": "Create", + "actor": sender.url, + "object": { + "id": "http://test.federation/note", + "type": "Note", + "content": "Hello", + }, } r_mock.post(to.inbox_url) - activity.deliver( - ac, - to=[to.url], - on_behalf_of=sender, - ) + activity.deliver(ac, to=[to.url], on_behalf_of=sender) request = r_mock.request_history[0] assert r_mock.called is True assert r_mock.call_count == 1 assert request.url == to.inbox_url - assert request.headers['content-type'] == 'application/activity+json' + assert request.headers["content-type"] == "application/activity+json" def test_accept_follow(mocker, factories): - deliver = mocker.patch( - 'funkwhale_api.federation.activity.deliver') - follow = factories['federation.Follow'](approved=None) + deliver = mocker.patch("funkwhale_api.federation.activity.deliver") + follow = factories["federation.Follow"](approved=None) expected_accept = serializers.AcceptFollowSerializer(follow).data activity.accept_follow(follow) deliver.assert_called_once_with( diff --git a/api/tests/federation/test_actors.py b/api/tests/federation/test_actors.py index 6f73a9b9b..99c71b348 100644 --- a/api/tests/federation/test_actors.py +++ b/api/tests/federation/test_actors.py @@ -18,11 +18,11 @@ from funkwhale_api.music import tasks as music_tasks def test_actor_fetching(r_mock): payload = { - 'id': 'https://actor.mock/users/actor#main-key', - 'owner': 'test', - 'publicKeyPem': 'test_pem', + "id": "https://actor.mock/users/actor#main-key", + "owner": "test", + "publicKeyPem": "test_pem", } - actor_url = 'https://actor.mock/' + actor_url = "https://actor.mock/" r_mock.get(actor_url, json=payload) r = actors.get_actor_data(actor_url) @@ -30,7 +30,7 @@ def test_actor_fetching(r_mock): def test_get_actor(factories, r_mock): - actor = factories['federation.Actor'].build() + actor = factories["federation.Actor"].build() payload = serializers.ActorSerializer(actor).data r_mock.get(actor.url, json=payload) new_actor = actors.get_actor(actor.url) @@ -40,9 +40,9 @@ def test_get_actor(factories, r_mock): def test_get_actor_use_existing(factories, preferences, mocker): - preferences['federation__actor_fetch_delay'] = 60 - actor = factories['federation.Actor']() - get_data = mocker.patch('funkwhale_api.federation.actors.get_actor_data') + preferences["federation__actor_fetch_delay"] = 60 + actor = factories["federation.Actor"]() + get_data = mocker.patch("funkwhale_api.federation.actors.get_actor_data") new_actor = actors.get_actor(actor.url) assert new_actor == actor @@ -50,87 +50,83 @@ def test_get_actor_use_existing(factories, preferences, mocker): def test_get_actor_refresh(factories, preferences, mocker): - preferences['federation__actor_fetch_delay'] = 0 - actor = factories['federation.Actor']() + preferences["federation__actor_fetch_delay"] = 0 + actor = factories["federation.Actor"]() payload = serializers.ActorSerializer(actor).data # actor changed their username in the meantime - payload['preferredUsername'] = 'New me' + payload["preferredUsername"] = "New me" get_data = mocker.patch( - 'funkwhale_api.federation.actors.get_actor_data', - return_value=payload) + "funkwhale_api.federation.actors.get_actor_data", return_value=payload + ) new_actor = actors.get_actor(actor.url) assert new_actor == actor assert new_actor.last_fetch_date > actor.last_fetch_date - assert new_actor.preferred_username == 'New me' + assert new_actor.preferred_username == "New me" def test_get_library(db, settings, mocker): get_key_pair = mocker.patch( - 'funkwhale_api.federation.keys.get_key_pair', - return_value=(b'private', b'public')) + "funkwhale_api.federation.keys.get_key_pair", + return_value=(b"private", b"public"), + ) expected = { - 'preferred_username': 'library', - 'domain': settings.FEDERATION_HOSTNAME, - 'type': 'Person', - 'name': '{}\'s library'.format(settings.FEDERATION_HOSTNAME), - 'manually_approves_followers': True, - 'public_key': 'public', - 'url': utils.full_url( - reverse( - 'federation:instance-actors-detail', - kwargs={'actor': 'library'})), - 'shared_inbox_url': utils.full_url( - reverse( - 'federation:instance-actors-inbox', - kwargs={'actor': 'library'})), - 'inbox_url': utils.full_url( - reverse( - 'federation:instance-actors-inbox', - kwargs={'actor': 'library'})), - 'outbox_url': utils.full_url( - reverse( - 'federation:instance-actors-outbox', - kwargs={'actor': 'library'})), - 'summary': 'Bot account to federate with {}\'s library'.format( - settings.FEDERATION_HOSTNAME), + "preferred_username": "library", + "domain": settings.FEDERATION_HOSTNAME, + "type": "Person", + "name": "{}'s library".format(settings.FEDERATION_HOSTNAME), + "manually_approves_followers": True, + "public_key": "public", + "url": utils.full_url( + reverse("federation:instance-actors-detail", kwargs={"actor": "library"}) + ), + "shared_inbox_url": utils.full_url( + reverse("federation:instance-actors-inbox", kwargs={"actor": "library"}) + ), + "inbox_url": utils.full_url( + reverse("federation:instance-actors-inbox", kwargs={"actor": "library"}) + ), + "outbox_url": utils.full_url( + reverse("federation:instance-actors-outbox", kwargs={"actor": "library"}) + ), + "summary": "Bot account to federate with {}'s library".format( + settings.FEDERATION_HOSTNAME + ), } - actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() for key, value in expected.items(): assert getattr(actor, key) == value def test_get_test(db, mocker, settings): get_key_pair = mocker.patch( - 'funkwhale_api.federation.keys.get_key_pair', - return_value=(b'private', b'public')) + "funkwhale_api.federation.keys.get_key_pair", + return_value=(b"private", b"public"), + ) expected = { - 'preferred_username': 'test', - 'domain': settings.FEDERATION_HOSTNAME, - 'type': 'Person', - 'name': '{}\'s test account'.format(settings.FEDERATION_HOSTNAME), - 'manually_approves_followers': False, - 'public_key': 'public', - 'url': utils.full_url( - reverse( - 'federation:instance-actors-detail', - kwargs={'actor': 'test'})), - 'shared_inbox_url': utils.full_url( - reverse( - 'federation:instance-actors-inbox', - kwargs={'actor': 'test'})), - 'inbox_url': utils.full_url( - reverse( - 'federation:instance-actors-inbox', - kwargs={'actor': 'test'})), - 'outbox_url': utils.full_url( - reverse( - 'federation:instance-actors-outbox', - kwargs={'actor': 'test'})), - 'summary': 'Bot account to test federation with {}. Send me /ping and I\'ll answer you.'.format( - settings.FEDERATION_HOSTNAME), + "preferred_username": "test", + "domain": settings.FEDERATION_HOSTNAME, + "type": "Person", + "name": "{}'s test account".format(settings.FEDERATION_HOSTNAME), + "manually_approves_followers": False, + "public_key": "public", + "url": utils.full_url( + reverse("federation:instance-actors-detail", kwargs={"actor": "test"}) + ), + "shared_inbox_url": utils.full_url( + reverse("federation:instance-actors-inbox", kwargs={"actor": "test"}) + ), + "inbox_url": utils.full_url( + reverse("federation:instance-actors-inbox", kwargs={"actor": "test"}) + ), + "outbox_url": utils.full_url( + reverse("federation:instance-actors-outbox", kwargs={"actor": "test"}) + ), + "summary": "Bot account to test federation with {}. Send me /ping and I'll answer you.".format( + settings.FEDERATION_HOSTNAME + ), } - actor = actors.SYSTEM_ACTORS['test'].get_actor_instance() + actor = actors.SYSTEM_ACTORS["test"].get_actor_instance() for key, value in expected.items(): assert getattr(actor, key) == value @@ -140,233 +136,208 @@ def test_test_get_outbox(): "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", - {} + {}, ], "id": utils.full_url( - reverse( - 'federation:instance-actors-outbox', - kwargs={'actor': 'test'})), + reverse("federation:instance-actors-outbox", kwargs={"actor": "test"}) + ), "type": "OrderedCollection", "totalItems": 0, - "orderedItems": [] + "orderedItems": [], } - data = actors.SYSTEM_ACTORS['test'].get_outbox({}, actor=None) + data = actors.SYSTEM_ACTORS["test"].get_outbox({}, actor=None) assert data == expected def test_test_post_inbox_requires_authenticated_actor(): with pytest.raises(exceptions.PermissionDenied): - actors.SYSTEM_ACTORS['test'].post_inbox({}, actor=None) + actors.SYSTEM_ACTORS["test"].post_inbox({}, actor=None) def test_test_post_outbox_validates_actor(nodb_factories): - actor = nodb_factories['federation.Actor']() - data = { - 'actor': 'noop' - } + actor = nodb_factories["federation.Actor"]() + data = {"actor": "noop"} with pytest.raises(exceptions.ValidationError) as exc_info: - actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor) - msg = 'The actor making the request do not match' + actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor) + msg = "The actor making the request do not match" assert msg in exc_info.value -def test_test_post_inbox_handles_create_note( - settings, mocker, factories): - deliver = mocker.patch( - 'funkwhale_api.federation.activity.deliver') - actor = factories['federation.Actor']() +def test_test_post_inbox_handles_create_note(settings, mocker, factories): + deliver = mocker.patch("funkwhale_api.federation.activity.deliver") + actor = factories["federation.Actor"]() now = timezone.now() - mocker.patch('django.utils.timezone.now', return_value=now) + mocker.patch("django.utils.timezone.now", return_value=now) data = { - 'actor': actor.url, - 'type': 'Create', - 'id': 'http://test.federation/activity', - 'object': { - 'type': 'Note', - 'id': 'http://test.federation/object', - 'content': '

@mention /ping

' - } + "actor": actor.url, + "type": "Create", + "id": "http://test.federation/activity", + "object": { + "type": "Note", + "id": "http://test.federation/object", + "content": "

@mention /ping

", + }, } - test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance() - expected_note = factories['federation.Note']( - id='https://test.federation/activities/note/{}'.format( - now.timestamp() - ), - content='Pong!', + test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance() + expected_note = factories["federation.Note"]( + id="https://test.federation/activities/note/{}".format(now.timestamp()), + content="Pong!", published=now.isoformat(), - inReplyTo=data['object']['id'], + inReplyTo=data["object"]["id"], cc=[], summary=None, sensitive=False, attributedTo=test_actor.url, attachment=[], to=[actor.url], - url='https://{}/activities/note/{}'.format( + url="https://{}/activities/note/{}".format( settings.FEDERATION_HOSTNAME, now.timestamp() ), - tag=[{ - 'href': actor.url, - 'name': actor.mention_username, - 'type': 'Mention', - }] + tag=[{"href": actor.url, "name": actor.mention_username, "type": "Mention"}], ) expected_activity = { - '@context': serializers.AP_CONTEXT, - 'actor': test_actor.url, - 'id': 'https://{}/activities/note/{}/activity'.format( + "@context": serializers.AP_CONTEXT, + "actor": test_actor.url, + "id": "https://{}/activities/note/{}/activity".format( settings.FEDERATION_HOSTNAME, now.timestamp() ), - 'to': actor.url, - 'type': 'Create', - 'published': now.isoformat(), - 'object': expected_note, - 'cc': [], + "to": actor.url, + "type": "Create", + "published": now.isoformat(), + "object": expected_note, + "cc": [], } - actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor) + actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor) deliver.assert_called_once_with( expected_activity, to=[actor.url], - on_behalf_of=actors.SYSTEM_ACTORS['test'].get_actor_instance() + on_behalf_of=actors.SYSTEM_ACTORS["test"].get_actor_instance(), ) def test_getting_actor_instance_persists_in_db(db): - test = actors.SYSTEM_ACTORS['test'].get_actor_instance() + test = actors.SYSTEM_ACTORS["test"].get_actor_instance() from_db = models.Actor.objects.get(url=test.url) for f in test._meta.fields: assert getattr(from_db, f.name) == getattr(test, f.name) -@pytest.mark.parametrize('username,domain,expected', [ - ('test', 'wrongdomain.com', False), - ('notsystem', '', False), - ('test', '', True), -]) -def test_actor_is_system( - username, domain, expected, nodb_factories, settings): +@pytest.mark.parametrize( + "username,domain,expected", + [("test", "wrongdomain.com", False), ("notsystem", "", False), ("test", "", True)], +) +def test_actor_is_system(username, domain, expected, nodb_factories, settings): if not domain: domain = settings.FEDERATION_HOSTNAME - actor = nodb_factories['federation.Actor']( - preferred_username=username, - domain=domain, + actor = nodb_factories["federation.Actor"]( + preferred_username=username, domain=domain ) assert actor.is_system is expected -@pytest.mark.parametrize('username,domain,expected', [ - ('test', 'wrongdomain.com', None), - ('notsystem', '', None), - ('test', '', actors.SYSTEM_ACTORS['test']), -]) -def test_actor_is_system( - username, domain, expected, nodb_factories, settings): +@pytest.mark.parametrize( + "username,domain,expected", + [ + ("test", "wrongdomain.com", None), + ("notsystem", "", None), + ("test", "", actors.SYSTEM_ACTORS["test"]), + ], +) +def test_actor_is_system(username, domain, expected, nodb_factories, settings): if not domain: domain = settings.FEDERATION_HOSTNAME - actor = nodb_factories['federation.Actor']( - preferred_username=username, - domain=domain, + actor = nodb_factories["federation.Actor"]( + preferred_username=username, domain=domain ) assert actor.system_conf == expected -@pytest.mark.parametrize('value', [False, True]) -def test_library_actor_manually_approves_based_on_preference( - value, preferences): - preferences['federation__music_needs_approval'] = value - library_conf = actors.SYSTEM_ACTORS['library'] +@pytest.mark.parametrize("value", [False, True]) +def test_library_actor_manually_approves_based_on_preference(value, preferences): + preferences["federation__music_needs_approval"] = value + library_conf = actors.SYSTEM_ACTORS["library"] assert library_conf.manually_approves_followers is value def test_system_actor_handle(mocker, nodb_factories): - handler = mocker.patch( - 'funkwhale_api.federation.actors.TestActor.handle_create') - actor = nodb_factories['federation.Actor']() - activity = nodb_factories['federation.Activity']( - type='Create', actor=actor.url) - serializer = serializers.ActivitySerializer( - data=activity - ) + handler = mocker.patch("funkwhale_api.federation.actors.TestActor.handle_create") + actor = nodb_factories["federation.Actor"]() + activity = nodb_factories["federation.Activity"](type="Create", actor=actor.url) + serializer = serializers.ActivitySerializer(data=activity) assert serializer.is_valid() - actors.SYSTEM_ACTORS['test'].handle(activity, actor) + actors.SYSTEM_ACTORS["test"].handle(activity, actor) handler.assert_called_once_with(activity, actor) -def test_test_actor_handles_follow( - settings, mocker, factories): - deliver = mocker.patch( - 'funkwhale_api.federation.activity.deliver') - actor = factories['federation.Actor']() - accept_follow = mocker.patch( - 'funkwhale_api.federation.activity.accept_follow') - test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance() +def test_test_actor_handles_follow(settings, mocker, factories): + deliver = mocker.patch("funkwhale_api.federation.activity.deliver") + actor = factories["federation.Actor"]() + accept_follow = mocker.patch("funkwhale_api.federation.activity.accept_follow") + test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance() data = { - 'actor': actor.url, - 'type': 'Follow', - 'id': 'http://test.federation/user#follows/267', - 'object': test_actor.url, + "actor": actor.url, + "type": "Follow", + "id": "http://test.federation/user#follows/267", + "object": test_actor.url, } - actors.SYSTEM_ACTORS['test'].post_inbox(data, actor=actor) + actors.SYSTEM_ACTORS["test"].post_inbox(data, actor=actor) follow = models.Follow.objects.get(target=test_actor, approved=True) follow_back = models.Follow.objects.get(actor=test_actor, approved=None) accept_follow.assert_called_once_with(follow) deliver.assert_called_once_with( serializers.FollowSerializer(follow_back).data, on_behalf_of=test_actor, - to=[actor.url] + to=[actor.url], ) -def test_test_actor_handles_undo_follow( - settings, mocker, factories): - deliver = mocker.patch( - 'funkwhale_api.federation.activity.deliver') - test_actor = actors.SYSTEM_ACTORS['test'].get_actor_instance() - follow = factories['federation.Follow'](target=test_actor) - reverse_follow = factories['federation.Follow']( - actor=test_actor, target=follow.actor) +def test_test_actor_handles_undo_follow(settings, mocker, factories): + deliver = mocker.patch("funkwhale_api.federation.activity.deliver") + test_actor = actors.SYSTEM_ACTORS["test"].get_actor_instance() + follow = factories["federation.Follow"](target=test_actor) + reverse_follow = factories["federation.Follow"]( + actor=test_actor, target=follow.actor + ) follow_serializer = serializers.FollowSerializer(follow) - reverse_follow_serializer = serializers.FollowSerializer( - reverse_follow) + reverse_follow_serializer = serializers.FollowSerializer(reverse_follow) undo = { - '@context': serializers.AP_CONTEXT, - 'type': 'Undo', - 'id': follow_serializer.data['id'] + '/undo', - 'actor': follow.actor.url, - 'object': follow_serializer.data, + "@context": serializers.AP_CONTEXT, + "type": "Undo", + "id": follow_serializer.data["id"] + "/undo", + "actor": follow.actor.url, + "object": follow_serializer.data, } expected_undo = { - '@context': serializers.AP_CONTEXT, - 'type': 'Undo', - 'id': reverse_follow_serializer.data['id'] + '/undo', - 'actor': reverse_follow.actor.url, - 'object': reverse_follow_serializer.data, + "@context": serializers.AP_CONTEXT, + "type": "Undo", + "id": reverse_follow_serializer.data["id"] + "/undo", + "actor": reverse_follow.actor.url, + "object": reverse_follow_serializer.data, } - actors.SYSTEM_ACTORS['test'].post_inbox(undo, actor=follow.actor) + actors.SYSTEM_ACTORS["test"].post_inbox(undo, actor=follow.actor) deliver.assert_called_once_with( - expected_undo, - to=[follow.actor.url], - on_behalf_of=test_actor,) + expected_undo, to=[follow.actor.url], on_behalf_of=test_actor + ) assert models.Follow.objects.count() == 0 -def test_library_actor_handles_follow_manual_approval( - preferences, mocker, factories): - preferences['federation__music_needs_approval'] = True - actor = factories['federation.Actor']() +def test_library_actor_handles_follow_manual_approval(preferences, mocker, factories): + preferences["federation__music_needs_approval"] = True + actor = factories["federation.Actor"]() now = timezone.now() - mocker.patch('django.utils.timezone.now', return_value=now) - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + mocker.patch("django.utils.timezone.now", return_value=now) + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() data = { - 'actor': actor.url, - 'type': 'Follow', - 'id': 'http://test.federation/user#follows/267', - 'object': library_actor.url, + "actor": actor.url, + "type": "Follow", + "id": "http://test.federation/user#follows/267", + "object": library_actor.url, } library_actor.system_conf.post_inbox(data, actor=actor) @@ -376,18 +347,16 @@ def test_library_actor_handles_follow_manual_approval( assert follow.approved is None -def test_library_actor_handles_follow_auto_approval( - preferences, mocker, factories): - preferences['federation__music_needs_approval'] = False - actor = factories['federation.Actor']() - accept_follow = mocker.patch( - 'funkwhale_api.federation.activity.accept_follow') - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() +def test_library_actor_handles_follow_auto_approval(preferences, mocker, factories): + preferences["federation__music_needs_approval"] = False + actor = factories["federation.Actor"]() + accept_follow = mocker.patch("funkwhale_api.federation.activity.accept_follow") + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() data = { - 'actor': actor.url, - 'type': 'Follow', - 'id': 'http://test.federation/user#follows/267', - 'object': library_actor.url, + "actor": actor.url, + "type": "Follow", + "id": "http://test.federation/user#follows/267", + "object": library_actor.url, } library_actor.system_conf.post_inbox(data, actor=actor) @@ -397,14 +366,11 @@ def test_library_actor_handles_follow_auto_approval( assert follow.approved is True -def test_library_actor_handles_accept( - mocker, factories): - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - actor = factories['federation.Actor']() - pending_follow = factories['federation.Follow']( - actor=library_actor, - target=actor, - approved=None, +def test_library_actor_handles_accept(mocker, factories): + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + actor = factories["federation.Actor"]() + pending_follow = factories["federation.Follow"]( + actor=library_actor, target=actor, approved=None ) serializer = serializers.AcceptFollowSerializer(pending_follow) library_actor.system_conf.post_inbox(serializer.data, actor=actor) @@ -418,19 +384,19 @@ def test_library_actor_handle_create_audio_no_library(mocker, factories): # when we receive inbox create audio, we should not do anything # if we don't have a configured library matching the sender mocked_create = mocker.patch( - 'funkwhale_api.federation.serializers.AudioSerializer.create' + "funkwhale_api.federation.serializers.AudioSerializer.create" ) - actor = factories['federation.Actor']() - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + actor = factories["federation.Actor"]() + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() data = { - 'actor': actor.url, - 'type': 'Create', - 'id': 'http://test.federation/audio/create', - 'object': { - 'id': 'https://batch.import', - 'type': 'Collection', - 'totalItems': 2, - 'items': factories['federation.Audio'].create_batch(size=2) + "actor": actor.url, + "type": "Create", + "id": "http://test.federation/audio/create", + "object": { + "id": "https://batch.import", + "type": "Collection", + "totalItems": 2, + "items": factories["federation.Audio"].create_batch(size=2), }, } library_actor.system_conf.post_inbox(data, actor=actor) @@ -439,26 +405,24 @@ def test_library_actor_handle_create_audio_no_library(mocker, factories): models.LibraryTrack.objects.count() == 0 -def test_library_actor_handle_create_audio_no_library_enabled( - mocker, factories): +def test_library_actor_handle_create_audio_no_library_enabled(mocker, factories): # when we receive inbox create audio, we should not do anything # if we don't have an enabled library mocked_create = mocker.patch( - 'funkwhale_api.federation.serializers.AudioSerializer.create' + "funkwhale_api.federation.serializers.AudioSerializer.create" ) - disabled_library = factories['federation.Library']( - federation_enabled=False) + disabled_library = factories["federation.Library"](federation_enabled=False) actor = disabled_library.actor - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() data = { - 'actor': actor.url, - 'type': 'Create', - 'id': 'http://test.federation/audio/create', - 'object': { - 'id': 'https://batch.import', - 'type': 'Collection', - 'totalItems': 2, - 'items': factories['federation.Audio'].create_batch(size=2) + "actor": actor.url, + "type": "Create", + "id": "http://test.federation/audio/create", + "object": { + "id": "https://batch.import", + "type": "Collection", + "totalItems": 2, + "items": factories["federation.Audio"].create_batch(size=2), }, } library_actor.system_conf.post_inbox(data, actor=actor) @@ -468,97 +432,91 @@ def test_library_actor_handle_create_audio_no_library_enabled( def test_library_actor_handle_create_audio(mocker, factories): - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - remote_library = factories['federation.Library']( - federation_enabled=True - ) + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + remote_library = factories["federation.Library"](federation_enabled=True) data = { - 'actor': remote_library.actor.url, - 'type': 'Create', - 'id': 'http://test.federation/audio/create', - 'object': { - 'id': 'https://batch.import', - 'type': 'Collection', - 'totalItems': 2, - 'items': factories['federation.Audio'].create_batch(size=2) + "actor": remote_library.actor.url, + "type": "Create", + "id": "http://test.federation/audio/create", + "object": { + "id": "https://batch.import", + "type": "Collection", + "totalItems": 2, + "items": factories["federation.Audio"].create_batch(size=2), }, } library_actor.system_conf.post_inbox(data, actor=remote_library.actor) - lts = list(remote_library.tracks.order_by('id')) + lts = list(remote_library.tracks.order_by("id")) assert len(lts) == 2 - for i, a in enumerate(data['object']['items']): + for i, a in enumerate(data["object"]["items"]): lt = lts[i] assert lt.pk is not None - assert lt.url == a['id'] + assert lt.url == a["id"] assert lt.library == remote_library - assert lt.audio_url == a['url']['href'] - assert lt.audio_mimetype == a['url']['mediaType'] - assert lt.metadata == a['metadata'] - assert lt.title == a['metadata']['recording']['title'] - assert lt.artist_name == a['metadata']['artist']['name'] - assert lt.album_title == a['metadata']['release']['title'] - assert lt.published_date == arrow.get(a['published']) + assert lt.audio_url == a["url"]["href"] + assert lt.audio_mimetype == a["url"]["mediaType"] + assert lt.metadata == a["metadata"] + assert lt.title == a["metadata"]["recording"]["title"] + assert lt.artist_name == a["metadata"]["artist"]["name"] + assert lt.album_title == a["metadata"]["release"]["title"] + assert lt.published_date == arrow.get(a["published"]) def test_library_actor_handle_create_audio_autoimport(mocker, factories): - mocked_import = mocker.patch( - 'funkwhale_api.common.utils.on_commit') - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - remote_library = factories['federation.Library']( - federation_enabled=True, - autoimport=True, + mocked_import = mocker.patch("funkwhale_api.common.utils.on_commit") + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + remote_library = factories["federation.Library"]( + federation_enabled=True, autoimport=True ) data = { - 'actor': remote_library.actor.url, - 'type': 'Create', - 'id': 'http://test.federation/audio/create', - 'object': { - 'id': 'https://batch.import', - 'type': 'Collection', - 'totalItems': 2, - 'items': factories['federation.Audio'].create_batch(size=2) + "actor": remote_library.actor.url, + "type": "Create", + "id": "http://test.federation/audio/create", + "object": { + "id": "https://batch.import", + "type": "Collection", + "totalItems": 2, + "items": factories["federation.Audio"].create_batch(size=2), }, } library_actor.system_conf.post_inbox(data, actor=remote_library.actor) - lts = list(remote_library.tracks.order_by('id')) + lts = list(remote_library.tracks.order_by("id")) assert len(lts) == 2 - for i, a in enumerate(data['object']['items']): + for i, a in enumerate(data["object"]["items"]): lt = lts[i] assert lt.pk is not None - assert lt.url == a['id'] + assert lt.url == a["id"] assert lt.library == remote_library - assert lt.audio_url == a['url']['href'] - assert lt.audio_mimetype == a['url']['mediaType'] - assert lt.metadata == a['metadata'] - assert lt.title == a['metadata']['recording']['title'] - assert lt.artist_name == a['metadata']['artist']['name'] - assert lt.album_title == a['metadata']['release']['title'] - assert lt.published_date == arrow.get(a['published']) + assert lt.audio_url == a["url"]["href"] + assert lt.audio_mimetype == a["url"]["mediaType"] + assert lt.metadata == a["metadata"] + assert lt.title == a["metadata"]["recording"]["title"] + assert lt.artist_name == a["metadata"]["artist"]["name"] + assert lt.album_title == a["metadata"]["release"]["title"] + assert lt.published_date == arrow.get(a["published"]) - batch = music_models.ImportBatch.objects.latest('id') + batch = music_models.ImportBatch.objects.latest("id") assert batch.jobs.count() == len(lts) - assert batch.source == 'federation' + assert batch.source == "federation" assert batch.submitted_by is None - for i, job in enumerate(batch.jobs.order_by('id')): + for i, job in enumerate(batch.jobs.order_by("id")): lt = lts[i] assert job.library_track == lt assert job.mbid == lt.mbid assert job.source == lt.url mocked_import.assert_any_call( - music_tasks.import_job_run.delay, - import_job_id=job.pk, - use_acoustid=False, + music_tasks.import_job_run.delay, import_job_id=job.pk, use_acoustid=False ) diff --git a/api/tests/federation/test_authentication.py b/api/tests/federation/test_authentication.py index 2f69e4d4f..566a3368d 100644 --- a/api/tests/federation/test_authentication.py +++ b/api/tests/federation/test_authentication.py @@ -5,34 +5,31 @@ from funkwhale_api.federation import signing def test_authenticate(factories, mocker, api_request): private, public = keys.get_key_pair() - actor_url = 'https://test.federation/actor' + actor_url = "https://test.federation/actor" mocker.patch( - 'funkwhale_api.federation.actors.get_actor_data', + "funkwhale_api.federation.actors.get_actor_data", return_value={ - 'id': actor_url, - 'type': 'Person', - 'outbox': 'https://test.com', - 'inbox': 'https://test.com', - 'preferredUsername': 'test', - 'publicKey': { - 'publicKeyPem': public.decode('utf-8'), - 'owner': actor_url, - 'id': actor_url + '#main-key', - } - }) - signed_request = factories['federation.SignedRequest']( - auth__key=private, - auth__key_id=actor_url + '#main-key', - auth__headers=[ - 'date', - ] + "id": actor_url, + "type": "Person", + "outbox": "https://test.com", + "inbox": "https://test.com", + "preferredUsername": "test", + "publicKey": { + "publicKeyPem": public.decode("utf-8"), + "owner": actor_url, + "id": actor_url + "#main-key", + }, + }, + ) + signed_request = factories["federation.SignedRequest"]( + auth__key=private, auth__key_id=actor_url + "#main-key", auth__headers=["date"] ) prepared = signed_request.prepare() django_request = api_request.get( - '/', + "/", **{ - 'HTTP_DATE': prepared.headers['date'], - 'HTTP_SIGNATURE': prepared.headers['signature'], + "HTTP_DATE": prepared.headers["date"], + "HTTP_SIGNATURE": prepared.headers["signature"], } ) authenticator = authentication.SignatureAuthentication() @@ -40,5 +37,5 @@ def test_authenticate(factories, mocker, api_request): actor = django_request.actor assert user.is_anonymous is True - assert actor.public_key == public.decode('utf-8') + assert actor.public_key == public.decode("utf-8") assert actor.url == actor_url diff --git a/api/tests/federation/test_keys.py b/api/tests/federation/test_keys.py index 9dd71be09..0f6158680 100644 --- a/api/tests/federation/test_keys.py +++ b/api/tests/federation/test_keys.py @@ -3,23 +3,29 @@ import pytest from funkwhale_api.federation import keys -@pytest.mark.parametrize('raw, expected', [ - ('algorithm="test",keyId="https://test.com"', 'https://test.com'), - ('keyId="https://test.com",algorithm="test"', 'https://test.com'), -]) +@pytest.mark.parametrize( + "raw, expected", + [ + ('algorithm="test",keyId="https://test.com"', "https://test.com"), + ('keyId="https://test.com",algorithm="test"', "https://test.com"), + ], +) def test_get_key_from_header(raw, expected): r = keys.get_key_id_from_signature_header(raw) assert r == expected -@pytest.mark.parametrize('raw', [ - 'algorithm="test",keyid="badCase"', - 'algorithm="test",wrong="wrong"', - 'keyId = "wrong"', - 'keyId=\'wrong\'', - 'keyId="notanurl"', - 'keyId="wrong://test.com"', -]) +@pytest.mark.parametrize( + "raw", + [ + 'algorithm="test",keyid="badCase"', + 'algorithm="test",wrong="wrong"', + 'keyId = "wrong"', + "keyId='wrong'", + 'keyId="notanurl"', + 'keyId="wrong://test.com"', + ], +) def test_get_key_from_header_invalid(raw): with pytest.raises(ValueError): keys.get_key_id_from_signature_header(raw) diff --git a/api/tests/federation/test_library.py b/api/tests/federation/test_library.py index 7a3abf5d8..8b2b18500 100644 --- a/api/tests/federation/test_library.py +++ b/api/tests/federation/test_library.py @@ -3,68 +3,63 @@ from funkwhale_api.federation import serializers def test_library_scan_from_account_name(mocker, factories): - actor = factories['federation.Actor']( - preferred_username='library', - domain='test.library' + actor = factories["federation.Actor"]( + preferred_username="library", domain="test.library" ) - get_resource_result = {'actor_url': actor.url} + get_resource_result = {"actor_url": actor.url} get_resource = mocker.patch( - 'funkwhale_api.federation.webfinger.get_resource', - return_value=get_resource_result) + "funkwhale_api.federation.webfinger.get_resource", + return_value=get_resource_result, + ) actor_data = serializers.ActorSerializer(actor).data - actor_data['manuallyApprovesFollowers'] = False - actor_data['url'] = [{ - 'type': 'Link', - 'name': 'library', - 'mediaType': 'application/activity+json', - 'href': 'https://test.library' - }] + actor_data["manuallyApprovesFollowers"] = False + actor_data["url"] = [ + { + "type": "Link", + "name": "library", + "mediaType": "application/activity+json", + "href": "https://test.library", + } + ] get_actor_data = mocker.patch( - 'funkwhale_api.federation.actors.get_actor_data', - return_value=actor_data) + "funkwhale_api.federation.actors.get_actor_data", return_value=actor_data + ) - get_library_data_result = {'test': 'test'} + get_library_data_result = {"test": "test"} get_library_data = mocker.patch( - 'funkwhale_api.federation.library.get_library_data', - return_value=get_library_data_result) + "funkwhale_api.federation.library.get_library_data", + return_value=get_library_data_result, + ) - result = library.scan_from_account_name('library@test.actor') + result = library.scan_from_account_name("library@test.actor") - get_resource.assert_called_once_with('acct:library@test.actor') + get_resource.assert_called_once_with("acct:library@test.actor") get_actor_data.assert_called_once_with(actor.url) - get_library_data.assert_called_once_with(actor_data['url'][0]['href']) + get_library_data.assert_called_once_with(actor_data["url"][0]["href"]) assert result == { - 'webfinger': get_resource_result, - 'actor': actor_data, - 'library': get_library_data_result, - 'local': { - 'following': False, - 'awaiting_approval': False, - }, + "webfinger": get_resource_result, + "actor": actor_data, + "library": get_library_data_result, + "local": {"following": False, "awaiting_approval": False}, } def test_get_library_data(r_mock, factories): - actor = factories['federation.Actor']() - url = 'https://test.library' - conf = { - 'id': url, - 'items': [], - 'actor': actor, - 'page_size': 5, - } + actor = factories["federation.Actor"]() + url = "https://test.library" + conf = {"id": url, "items": [], "actor": actor, "page_size": 5} data = serializers.PaginatedCollectionSerializer(conf).data r_mock.get(url, json=data) result = library.get_library_data(url) - for f in ['totalItems', 'actor', 'id', 'type']: + for f in ["totalItems", "actor", "id", "type"]: assert result[f] == data[f] def test_get_library_data_requires_authentication(r_mock, factories): - url = 'https://test.library' + url = "https://test.library" r_mock.get(url, status_code=403) result = library.get_library_data(url) - assert result['errors'] == ['Permission denied while scanning library'] + assert result["errors"] == ["Permission denied while scanning library"] diff --git a/api/tests/federation/test_models.py b/api/tests/federation/test_models.py index ae158e659..be353cb67 100644 --- a/api/tests/federation/test_models.py +++ b/api/tests/federation/test_models.py @@ -8,34 +8,29 @@ from funkwhale_api.federation import serializers def test_cannot_duplicate_actor(factories): - actor = factories['federation.Actor']() + actor = factories["federation.Actor"]() with pytest.raises(db.IntegrityError): - factories['federation.Actor']( - domain=actor.domain, - preferred_username=actor.preferred_username, + factories["federation.Actor"]( + domain=actor.domain, preferred_username=actor.preferred_username ) def test_cannot_duplicate_follow(factories): - follow = factories['federation.Follow']() + follow = factories["federation.Follow"]() with pytest.raises(db.IntegrityError): - factories['federation.Follow']( - target=follow.target, - actor=follow.actor, - ) + factories["federation.Follow"](target=follow.target, actor=follow.actor) def test_follow_federation_url(factories): - follow = factories['federation.Follow'](local=True) - expected = '{}#follows/{}'.format( - follow.actor.url, follow.uuid) + follow = factories["federation.Follow"](local=True) + expected = "{}#follows/{}".format(follow.actor.url, follow.uuid) assert follow.get_federation_url() == expected def test_library_model_unique_per_actor(factories): - library = factories['federation.Library']() + library = factories["federation.Library"]() with pytest.raises(db.IntegrityError): - factories['federation.Library'](actor=library.actor) + factories["federation.Library"](actor=library.actor) diff --git a/api/tests/federation/test_permissions.py b/api/tests/federation/test_permissions.py index a87f26f1b..15990ca91 100644 --- a/api/tests/federation/test_permissions.py +++ b/api/tests/federation/test_permissions.py @@ -4,57 +4,59 @@ from funkwhale_api.federation import actors from funkwhale_api.federation import permissions -def test_library_follower( - factories, api_request, anonymous_user, preferences): - preferences['federation__music_needs_approval'] = True +def test_library_follower(factories, api_request, anonymous_user, preferences): + preferences["federation__music_needs_approval"] = True view = APIView.as_view() permission = permissions.LibraryFollower() - request = api_request.get('/') - setattr(request, 'user', anonymous_user) + request = api_request.get("/") + setattr(request, "user", anonymous_user) check = permission.has_permission(request, view) assert check is False def test_library_follower_actor_non_follower( - factories, api_request, anonymous_user, preferences): - preferences['federation__music_needs_approval'] = True - actor = factories['federation.Actor']() + factories, api_request, anonymous_user, preferences +): + preferences["federation__music_needs_approval"] = True + actor = factories["federation.Actor"]() view = APIView.as_view() permission = permissions.LibraryFollower() - request = api_request.get('/') - setattr(request, 'user', anonymous_user) - setattr(request, 'actor', actor) + request = api_request.get("/") + setattr(request, "user", anonymous_user) + setattr(request, "actor", actor) check = permission.has_permission(request, view) assert check is False def test_library_follower_actor_follower_not_approved( - factories, api_request, anonymous_user, preferences): - preferences['federation__music_needs_approval'] = True - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() - follow = factories['federation.Follow'](target=library, approved=False) + factories, api_request, anonymous_user, preferences +): + preferences["federation__music_needs_approval"] = True + library = actors.SYSTEM_ACTORS["library"].get_actor_instance() + follow = factories["federation.Follow"](target=library, approved=False) view = APIView.as_view() permission = permissions.LibraryFollower() - request = api_request.get('/') - setattr(request, 'user', anonymous_user) - setattr(request, 'actor', follow.actor) + request = api_request.get("/") + setattr(request, "user", anonymous_user) + setattr(request, "actor", follow.actor) check = permission.has_permission(request, view) assert check is False def test_library_follower_actor_follower( - factories, api_request, anonymous_user, preferences): - preferences['federation__music_needs_approval'] = True - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() - follow = factories['federation.Follow'](target=library, approved=True) + factories, api_request, anonymous_user, preferences +): + preferences["federation__music_needs_approval"] = True + library = actors.SYSTEM_ACTORS["library"].get_actor_instance() + follow = factories["federation.Follow"](target=library, approved=True) view = APIView.as_view() permission = permissions.LibraryFollower() - request = api_request.get('/') - setattr(request, 'user', anonymous_user) - setattr(request, 'actor', follow.actor) + request = api_request.get("/") + setattr(request, "user", anonymous_user) + setattr(request, "actor", follow.actor) check = permission.has_permission(request, view) assert check is True diff --git a/api/tests/federation/test_serializers.py b/api/tests/federation/test_serializers.py index fcf2ba1b6..ffcbd441d 100644 --- a/api/tests/federation/test_serializers.py +++ b/api/tests/federation/test_serializers.py @@ -13,25 +13,23 @@ from funkwhale_api.federation import utils def test_actor_serializer_from_ap(db): payload = { - 'id': 'https://test.federation/user', - 'type': 'Person', - 'following': 'https://test.federation/user/following', - 'followers': 'https://test.federation/user/followers', - 'inbox': 'https://test.federation/user/inbox', - 'outbox': 'https://test.federation/user/outbox', - 'preferredUsername': 'user', - 'name': 'Real User', - 'summary': 'Hello world', - 'url': 'https://test.federation/@user', - 'manuallyApprovesFollowers': False, - 'publicKey': { - 'id': 'https://test.federation/user#main-key', - 'owner': 'https://test.federation/user', - 'publicKeyPem': 'yolo' - }, - 'endpoints': { - 'sharedInbox': 'https://test.federation/inbox' + "id": "https://test.federation/user", + "type": "Person", + "following": "https://test.federation/user/following", + "followers": "https://test.federation/user/followers", + "inbox": "https://test.federation/user/inbox", + "outbox": "https://test.federation/user/outbox", + "preferredUsername": "user", + "name": "Real User", + "summary": "Hello world", + "url": "https://test.federation/@user", + "manuallyApprovesFollowers": False, + "publicKey": { + "id": "https://test.federation/user#main-key", + "owner": "https://test.federation/user", + "publicKeyPem": "yolo", }, + "endpoints": {"sharedInbox": "https://test.federation/inbox"}, } serializer = serializers.ActorSerializer(data=payload) @@ -39,30 +37,30 @@ def test_actor_serializer_from_ap(db): actor = serializer.build() - assert actor.url == payload['id'] - assert actor.inbox_url == payload['inbox'] - assert actor.outbox_url == payload['outbox'] - assert actor.shared_inbox_url == payload['endpoints']['sharedInbox'] - assert actor.followers_url == payload['followers'] - assert actor.following_url == payload['following'] - assert actor.public_key == payload['publicKey']['publicKeyPem'] - assert actor.preferred_username == payload['preferredUsername'] - assert actor.name == payload['name'] - assert actor.domain == 'test.federation' - assert actor.summary == payload['summary'] - assert actor.type == 'Person' - assert actor.manually_approves_followers == payload['manuallyApprovesFollowers'] + assert actor.url == payload["id"] + assert actor.inbox_url == payload["inbox"] + assert actor.outbox_url == payload["outbox"] + assert actor.shared_inbox_url == payload["endpoints"]["sharedInbox"] + assert actor.followers_url == payload["followers"] + assert actor.following_url == payload["following"] + assert actor.public_key == payload["publicKey"]["publicKeyPem"] + assert actor.preferred_username == payload["preferredUsername"] + assert actor.name == payload["name"] + assert actor.domain == "test.federation" + assert actor.summary == payload["summary"] + assert actor.type == "Person" + assert actor.manually_approves_followers == payload["manuallyApprovesFollowers"] def test_actor_serializer_only_mandatory_field_from_ap(db): payload = { - 'id': 'https://test.federation/user', - 'type': 'Person', - 'following': 'https://test.federation/user/following', - 'followers': 'https://test.federation/user/followers', - 'inbox': 'https://test.federation/user/inbox', - 'outbox': 'https://test.federation/user/outbox', - 'preferredUsername': 'user', + "id": "https://test.federation/user", + "type": "Person", + "following": "https://test.federation/user/following", + "followers": "https://test.federation/user/followers", + "inbox": "https://test.federation/user/inbox", + "outbox": "https://test.federation/user/outbox", + "preferredUsername": "user", } serializer = serializers.ActorSerializer(data=payload) @@ -70,58 +68,55 @@ def test_actor_serializer_only_mandatory_field_from_ap(db): actor = serializer.build() - assert actor.url == payload['id'] - assert actor.inbox_url == payload['inbox'] - assert actor.outbox_url == payload['outbox'] - assert actor.followers_url == payload['followers'] - assert actor.following_url == payload['following'] - assert actor.preferred_username == payload['preferredUsername'] - assert actor.domain == 'test.federation' - assert actor.type == 'Person' + assert actor.url == payload["id"] + assert actor.inbox_url == payload["inbox"] + assert actor.outbox_url == payload["outbox"] + assert actor.followers_url == payload["followers"] + assert actor.following_url == payload["following"] + assert actor.preferred_username == payload["preferredUsername"] + assert actor.domain == "test.federation" + assert actor.type == "Person" assert actor.manually_approves_followers is None def test_actor_serializer_to_ap(): expected = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", {}, ], - 'id': 'https://test.federation/user', - 'type': 'Person', - 'following': 'https://test.federation/user/following', - 'followers': 'https://test.federation/user/followers', - 'inbox': 'https://test.federation/user/inbox', - 'outbox': 'https://test.federation/user/outbox', - 'preferredUsername': 'user', - 'name': 'Real User', - 'summary': 'Hello world', - 'manuallyApprovesFollowers': False, - 'publicKey': { - 'id': 'https://test.federation/user#main-key', - 'owner': 'https://test.federation/user', - 'publicKeyPem': 'yolo' - }, - 'endpoints': { - 'sharedInbox': 'https://test.federation/inbox' + "id": "https://test.federation/user", + "type": "Person", + "following": "https://test.federation/user/following", + "followers": "https://test.federation/user/followers", + "inbox": "https://test.federation/user/inbox", + "outbox": "https://test.federation/user/outbox", + "preferredUsername": "user", + "name": "Real User", + "summary": "Hello world", + "manuallyApprovesFollowers": False, + "publicKey": { + "id": "https://test.federation/user#main-key", + "owner": "https://test.federation/user", + "publicKeyPem": "yolo", }, + "endpoints": {"sharedInbox": "https://test.federation/inbox"}, } ac = models.Actor( - url=expected['id'], - inbox_url=expected['inbox'], - outbox_url=expected['outbox'], - shared_inbox_url=expected['endpoints']['sharedInbox'], - followers_url=expected['followers'], - following_url=expected['following'], - public_key=expected['publicKey']['publicKeyPem'], - preferred_username=expected['preferredUsername'], - name=expected['name'], - domain='test.federation', - summary=expected['summary'], - type='Person', + url=expected["id"], + inbox_url=expected["inbox"], + outbox_url=expected["outbox"], + shared_inbox_url=expected["endpoints"]["sharedInbox"], + followers_url=expected["followers"], + following_url=expected["following"], + public_key=expected["publicKey"]["publicKeyPem"], + preferred_username=expected["preferredUsername"], + name=expected["name"], + domain="test.federation", + summary=expected["summary"], + type="Person", manually_approves_followers=False, - ) serializer = serializers.ActorSerializer(ac) @@ -130,22 +125,20 @@ def test_actor_serializer_to_ap(): def test_webfinger_serializer(): expected = { - 'subject': 'acct:service@test.federation', - 'links': [ + "subject": "acct:service@test.federation", + "links": [ { - 'rel': 'self', - 'href': 'https://test.federation/federation/instance/actor', - 'type': 'application/activity+json', + "rel": "self", + "href": "https://test.federation/federation/instance/actor", + "type": "application/activity+json", } ], - 'aliases': [ - 'https://test.federation/federation/instance/actor', - ] + "aliases": ["https://test.federation/federation/instance/actor"], } actor = models.Actor( - url=expected['links'][0]['href'], - preferred_username='service', - domain='test.federation', + url=expected["links"][0]["href"], + preferred_username="service", + domain="test.federation", ) serializer = serializers.ActorWebfingerSerializer(actor) @@ -153,33 +146,33 @@ def test_webfinger_serializer(): def test_follow_serializer_to_ap(factories): - follow = factories['federation.Follow'](local=True) + follow = factories["federation.Follow"](local=True) serializer = serializers.FollowSerializer(follow) expected = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", {}, ], - 'id': follow.get_federation_url(), - 'type': 'Follow', - 'actor': follow.actor.url, - 'object': follow.target.url, + "id": follow.get_federation_url(), + "type": "Follow", + "actor": follow.actor.url, + "object": follow.target.url, } assert serializer.data == expected def test_follow_serializer_save(factories): - actor = factories['federation.Actor']() - target = factories['federation.Actor']() + actor = factories["federation.Actor"]() + target = factories["federation.Actor"]() data = expected = { - 'id': 'https://test.follow', - 'type': 'Follow', - 'actor': actor.url, - 'object': target.url, + "id": "https://test.follow", + "type": "Follow", + "actor": actor.url, + "object": target.url, } serializer = serializers.FollowSerializer(data=data) @@ -194,39 +187,39 @@ def test_follow_serializer_save(factories): def test_follow_serializer_save_validates_on_context(factories): - actor = factories['federation.Actor']() - target = factories['federation.Actor']() - impostor = factories['federation.Actor']() + actor = factories["federation.Actor"]() + target = factories["federation.Actor"]() + impostor = factories["federation.Actor"]() data = expected = { - 'id': 'https://test.follow', - 'type': 'Follow', - 'actor': actor.url, - 'object': target.url, + "id": "https://test.follow", + "type": "Follow", + "actor": actor.url, + "object": target.url, } serializer = serializers.FollowSerializer( - data=data, - context={'follow_actor': impostor, 'follow_target': impostor}) + data=data, context={"follow_actor": impostor, "follow_target": impostor} + ) assert serializer.is_valid() is False - assert 'actor' in serializer.errors - assert 'object' in serializer.errors + assert "actor" in serializer.errors + assert "object" in serializer.errors def test_accept_follow_serializer_representation(factories): - follow = factories['federation.Follow'](approved=None) + follow = factories["federation.Follow"](approved=None) expected = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", {}, ], - 'id': follow.get_federation_url() + '/accept', - 'type': 'Accept', - 'actor': follow.target.url, - 'object': serializers.FollowSerializer(follow).data, + "id": follow.get_federation_url() + "/accept", + "type": "Accept", + "actor": follow.target.url, + "object": serializers.FollowSerializer(follow).data, } serializer = serializers.AcceptFollowSerializer(follow) @@ -235,18 +228,18 @@ def test_accept_follow_serializer_representation(factories): def test_accept_follow_serializer_save(factories): - follow = factories['federation.Follow'](approved=None) + follow = factories["federation.Follow"](approved=None) data = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", {}, ], - 'id': follow.get_federation_url() + '/accept', - 'type': 'Accept', - 'actor': follow.target.url, - 'object': serializers.FollowSerializer(follow).data, + "id": follow.get_federation_url() + "/accept", + "type": "Accept", + "actor": follow.target.url, + "object": serializers.FollowSerializer(follow).data, } serializer = serializers.AcceptFollowSerializer(data=data) @@ -259,42 +252,42 @@ def test_accept_follow_serializer_save(factories): def test_accept_follow_serializer_validates_on_context(factories): - follow = factories['federation.Follow'](approved=None) - impostor = factories['federation.Actor']() + follow = factories["federation.Follow"](approved=None) + impostor = factories["federation.Actor"]() data = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", {}, ], - 'id': follow.get_federation_url() + '/accept', - 'type': 'Accept', - 'actor': impostor.url, - 'object': serializers.FollowSerializer(follow).data, + "id": follow.get_federation_url() + "/accept", + "type": "Accept", + "actor": impostor.url, + "object": serializers.FollowSerializer(follow).data, } serializer = serializers.AcceptFollowSerializer( - data=data, - context={'follow_actor': impostor, 'follow_target': impostor}) + data=data, context={"follow_actor": impostor, "follow_target": impostor} + ) assert serializer.is_valid() is False - assert 'actor' in serializer.errors['object'] - assert 'object' in serializer.errors['object'] + assert "actor" in serializer.errors["object"] + assert "object" in serializer.errors["object"] def test_undo_follow_serializer_representation(factories): - follow = factories['federation.Follow'](approved=True) + follow = factories["federation.Follow"](approved=True) expected = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", {}, ], - 'id': follow.get_federation_url() + '/undo', - 'type': 'Undo', - 'actor': follow.actor.url, - 'object': serializers.FollowSerializer(follow).data, + "id": follow.get_federation_url() + "/undo", + "type": "Undo", + "actor": follow.actor.url, + "object": serializers.FollowSerializer(follow).data, } serializer = serializers.UndoFollowSerializer(follow) @@ -303,18 +296,18 @@ def test_undo_follow_serializer_representation(factories): def test_undo_follow_serializer_save(factories): - follow = factories['federation.Follow'](approved=True) + follow = factories["federation.Follow"](approved=True) data = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", {}, ], - 'id': follow.get_federation_url() + '/undo', - 'type': 'Undo', - 'actor': follow.actor.url, - 'object': serializers.FollowSerializer(follow).data, + "id": follow.get_federation_url() + "/undo", + "type": "Undo", + "actor": follow.actor.url, + "object": serializers.FollowSerializer(follow).data, } serializer = serializers.UndoFollowSerializer(data=data) @@ -326,53 +319,53 @@ def test_undo_follow_serializer_save(factories): def test_undo_follow_serializer_validates_on_context(factories): - follow = factories['federation.Follow'](approved=True) - impostor = factories['federation.Actor']() + follow = factories["federation.Follow"](approved=True) + impostor = factories["federation.Actor"]() data = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", {}, ], - 'id': follow.get_federation_url() + '/undo', - 'type': 'Undo', - 'actor': impostor.url, - 'object': serializers.FollowSerializer(follow).data, + "id": follow.get_federation_url() + "/undo", + "type": "Undo", + "actor": impostor.url, + "object": serializers.FollowSerializer(follow).data, } serializer = serializers.UndoFollowSerializer( - data=data, - context={'follow_actor': impostor, 'follow_target': impostor}) + data=data, context={"follow_actor": impostor, "follow_target": impostor} + ) assert serializer.is_valid() is False - assert 'actor' in serializer.errors['object'] - assert 'object' in serializer.errors['object'] + assert "actor" in serializer.errors["object"] + assert "object" in serializer.errors["object"] def test_paginated_collection_serializer(factories): - tfs = factories['music.TrackFile'].create_batch(size=5) - actor = factories['federation.Actor'](local=True) + tfs = factories["music.TrackFile"].create_batch(size=5) + actor = factories["federation.Actor"](local=True) conf = { - 'id': 'https://test.federation/test', - 'items': tfs, - 'item_serializer': serializers.AudioSerializer, - 'actor': actor, - 'page_size': 2, + "id": "https://test.federation/test", + "items": tfs, + "item_serializer": serializers.AudioSerializer, + "actor": actor, + "page_size": 2, } expected = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", {}, ], - 'type': 'Collection', - 'id': conf['id'], - 'actor': actor.url, - 'totalItems': len(tfs), - 'current': conf['id'] + '?page=1', - 'last': conf['id'] + '?page=3', - 'first': conf['id'] + '?page=1', + "type": "Collection", + "id": conf["id"], + "actor": actor.url, + "totalItems": len(tfs), + "current": conf["id"] + "?page=1", + "last": conf["id"] + "?page=3", + "first": conf["id"] + "?page=1", } serializer = serializers.PaginatedCollectionSerializer(conf) @@ -382,108 +375,102 @@ def test_paginated_collection_serializer(factories): def test_paginated_collection_serializer_validation(): data = { - 'type': 'Collection', - 'id': 'https://test.federation/test', - 'totalItems': 5, - 'actor': 'http://test.actor', - 'first': 'https://test.federation/test?page=1', - 'last': 'https://test.federation/test?page=1', - 'items': [] + "type": "Collection", + "id": "https://test.federation/test", + "totalItems": 5, + "actor": "http://test.actor", + "first": "https://test.federation/test?page=1", + "last": "https://test.federation/test?page=1", + "items": [], } - serializer = serializers.PaginatedCollectionSerializer( - data=data - ) + serializer = serializers.PaginatedCollectionSerializer(data=data) assert serializer.is_valid(raise_exception=True) is True - assert serializer.validated_data['totalItems'] == 5 - assert serializer.validated_data['id'] == data['id'] - assert serializer.validated_data['actor'] == data['actor'] + assert serializer.validated_data["totalItems"] == 5 + assert serializer.validated_data["id"] == data["id"] + assert serializer.validated_data["actor"] == data["actor"] def test_collection_page_serializer_validation(): - base = 'https://test.federation/test' + base = "https://test.federation/test" data = { - 'type': 'CollectionPage', - 'id': base + '?page=2', - 'totalItems': 5, - 'actor': 'https://test.actor', - 'items': [], - 'first': 'https://test.federation/test?page=1', - 'last': 'https://test.federation/test?page=3', - 'prev': base + '?page=1', - 'next': base + '?page=3', - 'partOf': base, + "type": "CollectionPage", + "id": base + "?page=2", + "totalItems": 5, + "actor": "https://test.actor", + "items": [], + "first": "https://test.federation/test?page=1", + "last": "https://test.federation/test?page=3", + "prev": base + "?page=1", + "next": base + "?page=3", + "partOf": base, } - serializer = serializers.CollectionPageSerializer( - data=data - ) + serializer = serializers.CollectionPageSerializer(data=data) assert serializer.is_valid(raise_exception=True) is True - assert serializer.validated_data['totalItems'] == 5 - assert serializer.validated_data['id'] == data['id'] - assert serializer.validated_data['actor'] == data['actor'] - assert serializer.validated_data['items'] == [] - assert serializer.validated_data['prev'] == data['prev'] - assert serializer.validated_data['next'] == data['next'] - assert serializer.validated_data['partOf'] == data['partOf'] + assert serializer.validated_data["totalItems"] == 5 + assert serializer.validated_data["id"] == data["id"] + assert serializer.validated_data["actor"] == data["actor"] + assert serializer.validated_data["items"] == [] + assert serializer.validated_data["prev"] == data["prev"] + assert serializer.validated_data["next"] == data["next"] + assert serializer.validated_data["partOf"] == data["partOf"] def test_collection_page_serializer_can_validate_child(): data = { - 'type': 'CollectionPage', - 'id': 'https://test.page?page=2', - 'actor': 'https://test.actor', - 'first': 'https://test.page?page=1', - 'last': 'https://test.page?page=3', - 'partOf': 'https://test.page', - 'totalItems': 1, - 'items': [{'in': 'valid'}], + "type": "CollectionPage", + "id": "https://test.page?page=2", + "actor": "https://test.actor", + "first": "https://test.page?page=1", + "last": "https://test.page?page=3", + "partOf": "https://test.page", + "totalItems": 1, + "items": [{"in": "valid"}], } serializer = serializers.CollectionPageSerializer( - data=data, - context={'item_serializer': serializers.AudioSerializer} + data=data, context={"item_serializer": serializers.AudioSerializer} ) # child are validated but not included in data if not valid assert serializer.is_valid(raise_exception=True) is True - assert len(serializer.validated_data['items']) == 0 + assert len(serializer.validated_data["items"]) == 0 def test_collection_page_serializer(factories): - tfs = factories['music.TrackFile'].create_batch(size=5) - actor = factories['federation.Actor'](local=True) + tfs = factories["music.TrackFile"].create_batch(size=5) + actor = factories["federation.Actor"](local=True) conf = { - 'id': 'https://test.federation/test', - 'item_serializer': serializers.AudioSerializer, - 'actor': actor, - 'page': Paginator(tfs, 2).page(2), + "id": "https://test.federation/test", + "item_serializer": serializers.AudioSerializer, + "actor": actor, + "page": Paginator(tfs, 2).page(2), } expected = { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", {}, ], - 'type': 'CollectionPage', - 'id': conf['id'] + '?page=2', - 'actor': actor.url, - 'totalItems': len(tfs), - 'partOf': conf['id'], - 'prev': conf['id'] + '?page=1', - 'next': conf['id'] + '?page=3', - 'first': conf['id'] + '?page=1', - 'last': conf['id'] + '?page=3', - 'items': [ - conf['item_serializer']( - i, - context={'actor': actor, 'include_ap_context': False} + "type": "CollectionPage", + "id": conf["id"] + "?page=2", + "actor": actor.url, + "totalItems": len(tfs), + "partOf": conf["id"], + "prev": conf["id"] + "?page=1", + "next": conf["id"] + "?page=3", + "first": conf["id"] + "?page=1", + "last": conf["id"] + "?page=3", + "items": [ + conf["item_serializer"]( + i, context={"actor": actor, "include_ap_context": False} ).data - for i in conf['page'].object_list - ] + for i in conf["page"].object_list + ], } serializer = serializers.CollectionPageSerializer(conf) @@ -492,35 +479,37 @@ def test_collection_page_serializer(factories): def test_activity_pub_audio_serializer_to_library_track(factories): - remote_library = factories['federation.Library']() - audio = factories['federation.Audio']() + remote_library = factories["federation.Library"]() + audio = factories["federation.Audio"]() serializer = serializers.AudioSerializer( - data=audio, context={'library': remote_library}) + data=audio, context={"library": remote_library} + ) assert serializer.is_valid(raise_exception=True) lt = serializer.save() assert lt.pk is not None - assert lt.url == audio['id'] + assert lt.url == audio["id"] assert lt.library == remote_library - assert lt.audio_url == audio['url']['href'] - assert lt.audio_mimetype == audio['url']['mediaType'] - assert lt.metadata == audio['metadata'] - assert lt.title == audio['metadata']['recording']['title'] - assert lt.artist_name == audio['metadata']['artist']['name'] - assert lt.album_title == audio['metadata']['release']['title'] - assert lt.published_date == arrow.get(audio['published']) + assert lt.audio_url == audio["url"]["href"] + assert lt.audio_mimetype == audio["url"]["mediaType"] + assert lt.metadata == audio["metadata"] + assert lt.title == audio["metadata"]["recording"]["title"] + assert lt.artist_name == audio["metadata"]["artist"]["name"] + assert lt.album_title == audio["metadata"]["release"]["title"] + assert lt.published_date == arrow.get(audio["published"]) -def test_activity_pub_audio_serializer_to_library_track_no_duplicate( - factories): - remote_library = factories['federation.Library']() - audio = factories['federation.Audio']() +def test_activity_pub_audio_serializer_to_library_track_no_duplicate(factories): + remote_library = factories["federation.Library"]() + audio = factories["federation.Audio"]() serializer1 = serializers.AudioSerializer( - data=audio, context={'library': remote_library}) + data=audio, context={"library": remote_library} + ) serializer2 = serializers.AudioSerializer( - data=audio, context={'library': remote_library}) + data=audio, context={"library": remote_library} + ) assert serializer1.is_valid() is True assert serializer2.is_valid() is True @@ -533,192 +522,168 @@ def test_activity_pub_audio_serializer_to_library_track_no_duplicate( def test_activity_pub_audio_serializer_to_ap(factories): - tf = factories['music.TrackFile']( - mimetype='audio/mp3', - bitrate=42, - duration=43, - size=44, + tf = factories["music.TrackFile"]( + mimetype="audio/mp3", bitrate=42, duration=43, size=44 ) - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + library = actors.SYSTEM_ACTORS["library"].get_actor_instance() expected = { - '@context': serializers.AP_CONTEXT, - 'type': 'Audio', - 'id': tf.get_federation_url(), - 'name': tf.track.full_name, - 'published': tf.creation_date.isoformat(), - 'updated': tf.modification_date.isoformat(), - 'metadata': { - 'artist': { - 'musicbrainz_id': tf.track.artist.mbid, - 'name': tf.track.artist.name, + "@context": serializers.AP_CONTEXT, + "type": "Audio", + "id": tf.get_federation_url(), + "name": tf.track.full_name, + "published": tf.creation_date.isoformat(), + "updated": tf.modification_date.isoformat(), + "metadata": { + "artist": { + "musicbrainz_id": tf.track.artist.mbid, + "name": tf.track.artist.name, }, - 'release': { - 'musicbrainz_id': tf.track.album.mbid, - 'title': tf.track.album.title, + "release": { + "musicbrainz_id": tf.track.album.mbid, + "title": tf.track.album.title, }, - 'recording': { - 'musicbrainz_id': tf.track.mbid, - 'title': tf.track.title, - }, - 'size': tf.size, - 'length': tf.duration, - 'bitrate': tf.bitrate, + "recording": {"musicbrainz_id": tf.track.mbid, "title": tf.track.title}, + "size": tf.size, + "length": tf.duration, + "bitrate": tf.bitrate, }, - 'url': { - 'href': utils.full_url(tf.path), - 'type': 'Link', - 'mediaType': 'audio/mp3' + "url": { + "href": utils.full_url(tf.path), + "type": "Link", + "mediaType": "audio/mp3", }, - 'attributedTo': [ - library.url - ] + "attributedTo": [library.url], } - serializer = serializers.AudioSerializer(tf, context={'actor': library}) + serializer = serializers.AudioSerializer(tf, context={"actor": library}) assert serializer.data == expected def test_activity_pub_audio_serializer_to_ap_no_mbid(factories): - tf = factories['music.TrackFile']( - mimetype='audio/mp3', + tf = factories["music.TrackFile"]( + mimetype="audio/mp3", track__mbid=None, track__album__mbid=None, track__album__artist__mbid=None, ) - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + library = actors.SYSTEM_ACTORS["library"].get_actor_instance() expected = { - '@context': serializers.AP_CONTEXT, - 'type': 'Audio', - 'id': tf.get_federation_url(), - 'name': tf.track.full_name, - 'published': tf.creation_date.isoformat(), - 'updated': tf.modification_date.isoformat(), - 'metadata': { - 'artist': { - 'name': tf.track.artist.name, - 'musicbrainz_id': None, - }, - 'release': { - 'title': tf.track.album.title, - 'musicbrainz_id': None, - }, - 'recording': { - 'title': tf.track.title, - 'musicbrainz_id': None, - }, - 'size': None, - 'length': None, - 'bitrate': None, + "@context": serializers.AP_CONTEXT, + "type": "Audio", + "id": tf.get_federation_url(), + "name": tf.track.full_name, + "published": tf.creation_date.isoformat(), + "updated": tf.modification_date.isoformat(), + "metadata": { + "artist": {"name": tf.track.artist.name, "musicbrainz_id": None}, + "release": {"title": tf.track.album.title, "musicbrainz_id": None}, + "recording": {"title": tf.track.title, "musicbrainz_id": None}, + "size": None, + "length": None, + "bitrate": None, }, - 'url': { - 'href': utils.full_url(tf.path), - 'type': 'Link', - 'mediaType': 'audio/mp3' + "url": { + "href": utils.full_url(tf.path), + "type": "Link", + "mediaType": "audio/mp3", }, - 'attributedTo': [ - library.url - ] + "attributedTo": [library.url], } - serializer = serializers.AudioSerializer(tf, context={'actor': library}) + serializer = serializers.AudioSerializer(tf, context={"actor": library}) assert serializer.data == expected def test_collection_serializer_to_ap(factories): - tf1 = factories['music.TrackFile'](mimetype='audio/mp3') - tf2 = factories['music.TrackFile'](mimetype='audio/ogg') - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() + tf1 = factories["music.TrackFile"](mimetype="audio/mp3") + tf2 = factories["music.TrackFile"](mimetype="audio/ogg") + library = actors.SYSTEM_ACTORS["library"].get_actor_instance() expected = { - '@context': serializers.AP_CONTEXT, - 'id': 'https://test.id', - 'actor': library.url, - 'totalItems': 2, - 'type': 'Collection', - 'items': [ + "@context": serializers.AP_CONTEXT, + "id": "https://test.id", + "actor": library.url, + "totalItems": 2, + "type": "Collection", + "items": [ serializers.AudioSerializer( - tf1, context={'actor': library, 'include_ap_context': False} + tf1, context={"actor": library, "include_ap_context": False} ).data, serializers.AudioSerializer( - tf2, context={'actor': library, 'include_ap_context': False} + tf2, context={"actor": library, "include_ap_context": False} ).data, - ] + ], } collection = { - 'id': expected['id'], - 'actor': library, - 'items': [tf1, tf2], - 'item_serializer': serializers.AudioSerializer + "id": expected["id"], + "actor": library, + "items": [tf1, tf2], + "item_serializer": serializers.AudioSerializer, } serializer = serializers.CollectionSerializer( - collection, context={'actor': library, 'id': 'https://test.id'}) + collection, context={"actor": library, "id": "https://test.id"} + ) assert serializer.data == expected def test_api_library_create_serializer_save(factories, r_mock): - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - actor = factories['federation.Actor']() - follow = factories['federation.Follow']( - target=actor, - actor=library_actor, - ) + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + actor = factories["federation.Actor"]() + follow = factories["federation.Follow"](target=actor, actor=library_actor) actor_data = serializers.ActorSerializer(actor).data - actor_data['url'] = [{ - 'href': 'https://test.library', - 'name': 'library', - 'type': 'Link', - }] + actor_data["url"] = [ + {"href": "https://test.library", "name": "library", "type": "Link"} + ] library_conf = { - 'id': 'https://test.library', - 'items': range(10), - 'actor': actor, - 'page_size': 5, + "id": "https://test.library", + "items": range(10), + "actor": actor, + "page_size": 5, } library_data = serializers.PaginatedCollectionSerializer(library_conf).data r_mock.get(actor.url, json=actor_data) - r_mock.get('https://test.library', json=library_data) + r_mock.get("https://test.library", json=library_data) data = { - 'actor': actor.url, - 'autoimport': False, - 'federation_enabled': True, - 'download_files': False, + "actor": actor.url, + "autoimport": False, + "federation_enabled": True, + "download_files": False, } serializer = serializers.APILibraryCreateSerializer(data=data) assert serializer.is_valid(raise_exception=True) is True library = serializer.save() - follow = models.Follow.objects.get( - target=actor, actor=library_actor, approved=None) + follow = models.Follow.objects.get(target=actor, actor=library_actor, approved=None) - assert library.autoimport is data['autoimport'] - assert library.federation_enabled is data['federation_enabled'] - assert library.download_files is data['download_files'] + assert library.autoimport is data["autoimport"] + assert library.federation_enabled is data["federation_enabled"] + assert library.download_files is data["download_files"] assert library.tracks_count == 10 assert library.actor == actor assert library.follow == follow def test_tapi_library_track_serializer_not_imported(factories): - lt = factories['federation.LibraryTrack']() + lt = factories["federation.LibraryTrack"]() serializer = serializers.APILibraryTrackSerializer(lt) - assert serializer.get_status(lt) == 'not_imported' + assert serializer.get_status(lt) == "not_imported" def test_tapi_library_track_serializer_imported(factories): - tf = factories['music.TrackFile'](federation=True) + tf = factories["music.TrackFile"](federation=True) lt = tf.library_track serializer = serializers.APILibraryTrackSerializer(lt) - assert serializer.get_status(lt) == 'imported' + assert serializer.get_status(lt) == "imported" def test_tapi_library_track_serializer_import_pending(factories): - job = factories['music.ImportJob'](federation=True, status='pending') + job = factories["music.ImportJob"](federation=True, status="pending") lt = job.library_track serializer = serializers.APILibraryTrackSerializer(lt) - assert serializer.get_status(lt) == 'import_pending' + assert serializer.get_status(lt) == "import_pending" diff --git a/api/tests/federation/test_signing.py b/api/tests/federation/test_signing.py index 0c1ec2e0b..e88425e8f 100644 --- a/api/tests/federation/test_signing.py +++ b/api/tests/federation/test_signing.py @@ -8,36 +8,31 @@ from funkwhale_api.federation import keys def test_can_sign_and_verify_request(nodb_factories): - private, public = nodb_factories['federation.KeyPair']() - auth = nodb_factories['federation.SignatureAuth'](key=private) - request = nodb_factories['federation.SignedRequest']( - auth=auth - ) + private, public = nodb_factories["federation.KeyPair"]() + auth = nodb_factories["federation.SignatureAuth"](key=private) + request = nodb_factories["federation.SignedRequest"](auth=auth) prepared_request = request.prepare() - assert 'date' in prepared_request.headers - assert 'signature' in prepared_request.headers - assert signing.verify( - prepared_request, public) is None + assert "date" in prepared_request.headers + assert "signature" in prepared_request.headers + assert signing.verify(prepared_request, public) is None def test_can_sign_and_verify_request_digest(nodb_factories): - private, public = nodb_factories['federation.KeyPair']() - auth = nodb_factories['federation.SignatureAuth'](key=private) - request = nodb_factories['federation.SignedRequest']( - auth=auth, - method='post', - data=b'hello=world' + private, public = nodb_factories["federation.KeyPair"]() + auth = nodb_factories["federation.SignatureAuth"](key=private) + request = nodb_factories["federation.SignedRequest"]( + auth=auth, method="post", data=b"hello=world" ) prepared_request = request.prepare() - assert 'date' in prepared_request.headers - assert 'digest' in prepared_request.headers - assert 'signature' in prepared_request.headers + assert "date" in prepared_request.headers + assert "digest" in prepared_request.headers + assert "signature" in prepared_request.headers assert signing.verify(prepared_request, public) is None def test_verify_fails_with_wrong_key(nodb_factories): - wrong_private, wrong_public = nodb_factories['federation.KeyPair']() - request = nodb_factories['federation.SignedRequest']() + wrong_private, wrong_public = nodb_factories["federation.KeyPair"]() + request = nodb_factories["federation.SignedRequest"]() prepared_request = request.prepare() with pytest.raises(cryptography.exceptions.InvalidSignature): @@ -46,18 +41,15 @@ def test_verify_fails_with_wrong_key(nodb_factories): def test_can_verify_django_request(factories, fake_request): private_key, public_key = keys.get_key_pair() - signed_request = factories['federation.SignedRequest']( - auth__key=private_key, - auth__headers=[ - 'date', - ] + signed_request = factories["federation.SignedRequest"]( + auth__key=private_key, auth__headers=["date"] ) prepared = signed_request.prepare() django_request = fake_request.get( - '/', + "/", **{ - 'HTTP_DATE': prepared.headers['date'], - 'HTTP_SIGNATURE': prepared.headers['signature'], + "HTTP_DATE": prepared.headers["date"], + "HTTP_SIGNATURE": prepared.headers["signature"], } ) assert signing.verify_django(django_request, public_key) is None @@ -65,22 +57,19 @@ def test_can_verify_django_request(factories, fake_request): def test_can_verify_django_request_digest(factories, fake_request): private_key, public_key = keys.get_key_pair() - signed_request = factories['federation.SignedRequest']( + signed_request = factories["federation.SignedRequest"]( auth__key=private_key, - method='post', - data=b'hello=world', - auth__headers=[ - 'date', - 'digest', - ] + method="post", + data=b"hello=world", + auth__headers=["date", "digest"], ) prepared = signed_request.prepare() django_request = fake_request.post( - '/', + "/", **{ - 'HTTP_DATE': prepared.headers['date'], - 'HTTP_DIGEST': prepared.headers['digest'], - 'HTTP_SIGNATURE': prepared.headers['signature'], + "HTTP_DATE": prepared.headers["date"], + "HTTP_DIGEST": prepared.headers["digest"], + "HTTP_SIGNATURE": prepared.headers["signature"], } ) @@ -89,22 +78,19 @@ def test_can_verify_django_request_digest(factories, fake_request): def test_can_verify_django_request_digest_failure(factories, fake_request): private_key, public_key = keys.get_key_pair() - signed_request = factories['federation.SignedRequest']( + signed_request = factories["federation.SignedRequest"]( auth__key=private_key, - method='post', - data=b'hello=world', - auth__headers=[ - 'date', - 'digest', - ] + method="post", + data=b"hello=world", + auth__headers=["date", "digest"], ) prepared = signed_request.prepare() django_request = fake_request.post( - '/', + "/", **{ - 'HTTP_DATE': prepared.headers['date'], - 'HTTP_DIGEST': prepared.headers['digest'] + 'noop', - 'HTTP_SIGNATURE': prepared.headers['signature'], + "HTTP_DATE": prepared.headers["date"], + "HTTP_DIGEST": prepared.headers["digest"] + "noop", + "HTTP_SIGNATURE": prepared.headers["signature"], } ) @@ -114,19 +100,12 @@ def test_can_verify_django_request_digest_failure(factories, fake_request): def test_can_verify_django_request_failure(factories, fake_request): private_key, public_key = keys.get_key_pair() - signed_request = factories['federation.SignedRequest']( - auth__key=private_key, - auth__headers=[ - 'date', - ] + signed_request = factories["federation.SignedRequest"]( + auth__key=private_key, auth__headers=["date"] ) prepared = signed_request.prepare() django_request = fake_request.get( - '/', - **{ - 'HTTP_DATE': 'Wrong', - 'HTTP_SIGNATURE': prepared.headers['signature'], - } + "/", **{"HTTP_DATE": "Wrong", "HTTP_SIGNATURE": prepared.headers["signature"]} ) with pytest.raises(cryptography.exceptions.InvalidSignature): signing.verify_django(django_request, public_key) diff --git a/api/tests/federation/test_tasks.py b/api/tests/federation/test_tasks.py index 3517e8feb..f6fafffd6 100644 --- a/api/tests/federation/test_tasks.py +++ b/api/tests/federation/test_tasks.py @@ -11,122 +11,111 @@ from funkwhale_api.federation import tasks def test_scan_library_does_nothing_if_federation_disabled(mocker, factories): - library = factories['federation.Library'](federation_enabled=False) + library = factories["federation.Library"](federation_enabled=False) tasks.scan_library(library_id=library.pk) assert library.tracks.count() == 0 -def test_scan_library_page_does_nothing_if_federation_disabled( - mocker, factories): - library = factories['federation.Library'](federation_enabled=False) +def test_scan_library_page_does_nothing_if_federation_disabled(mocker, factories): + library = factories["federation.Library"](federation_enabled=False) tasks.scan_library_page(library_id=library.pk, page_url=None) assert library.tracks.count() == 0 -def test_scan_library_fetches_page_and_calls_scan_page( - mocker, factories, r_mock): +def test_scan_library_fetches_page_and_calls_scan_page(mocker, factories, r_mock): now = timezone.now() - library = factories['federation.Library'](federation_enabled=True) + library = factories["federation.Library"](federation_enabled=True) collection_conf = { - 'actor': library.actor, - 'id': library.url, - 'page_size': 10, - 'items': range(10), + "actor": library.actor, + "id": library.url, + "page_size": 10, + "items": range(10), } collection = serializers.PaginatedCollectionSerializer(collection_conf) - scan_page = mocker.patch( - 'funkwhale_api.federation.tasks.scan_library_page.delay') - r_mock.get(collection_conf['id'], json=collection.data) + scan_page = mocker.patch("funkwhale_api.federation.tasks.scan_library_page.delay") + r_mock.get(collection_conf["id"], json=collection.data) tasks.scan_library(library_id=library.pk) scan_page.assert_called_once_with( - library_id=library.id, - page_url=collection.data['first'], - until=None, + library_id=library.id, page_url=collection.data["first"], until=None ) library.refresh_from_db() assert library.fetched_date > now -def test_scan_page_fetches_page_and_creates_tracks( - mocker, factories, r_mock): - library = factories['federation.Library'](federation_enabled=True) - tfs = factories['music.TrackFile'].create_batch(size=5) +def test_scan_page_fetches_page_and_creates_tracks(mocker, factories, r_mock): + library = factories["federation.Library"](federation_enabled=True) + tfs = factories["music.TrackFile"].create_batch(size=5) page_conf = { - 'actor': library.actor, - 'id': library.url, - 'page': Paginator(tfs, 5).page(1), - 'item_serializer': serializers.AudioSerializer, + "actor": library.actor, + "id": library.url, + "page": Paginator(tfs, 5).page(1), + "item_serializer": serializers.AudioSerializer, } page = serializers.CollectionPageSerializer(page_conf) - r_mock.get(page.data['id'], json=page.data) + r_mock.get(page.data["id"], json=page.data) - tasks.scan_library_page(library_id=library.pk, page_url=page.data['id']) + tasks.scan_library_page(library_id=library.pk, page_url=page.data["id"]) - lts = list(library.tracks.all().order_by('-published_date')) + lts = list(library.tracks.all().order_by("-published_date")) assert len(lts) == 5 -def test_scan_page_trigger_next_page_scan_skip_if_same( - mocker, factories, r_mock): +def test_scan_page_trigger_next_page_scan_skip_if_same(mocker, factories, r_mock): patched_scan = mocker.patch( - 'funkwhale_api.federation.tasks.scan_library_page.delay' + "funkwhale_api.federation.tasks.scan_library_page.delay" ) - library = factories['federation.Library'](federation_enabled=True) - tfs = factories['music.TrackFile'].create_batch(size=1) + library = factories["federation.Library"](federation_enabled=True) + tfs = factories["music.TrackFile"].create_batch(size=1) page_conf = { - 'actor': library.actor, - 'id': library.url, - 'page': Paginator(tfs, 3).page(1), - 'item_serializer': serializers.AudioSerializer, + "actor": library.actor, + "id": library.url, + "page": Paginator(tfs, 3).page(1), + "item_serializer": serializers.AudioSerializer, } page = serializers.CollectionPageSerializer(page_conf) data = page.data - data['next'] = data['id'] - r_mock.get(page.data['id'], json=data) + data["next"] = data["id"] + r_mock.get(page.data["id"], json=data) - tasks.scan_library_page(library_id=library.pk, page_url=data['id']) + tasks.scan_library_page(library_id=library.pk, page_url=data["id"]) patched_scan.assert_not_called() -def test_scan_page_stops_once_until_is_reached( - mocker, factories, r_mock): - library = factories['federation.Library'](federation_enabled=True) - tfs = list(reversed(factories['music.TrackFile'].create_batch(size=5))) +def test_scan_page_stops_once_until_is_reached(mocker, factories, r_mock): + library = factories["federation.Library"](federation_enabled=True) + tfs = list(reversed(factories["music.TrackFile"].create_batch(size=5))) page_conf = { - 'actor': library.actor, - 'id': library.url, - 'page': Paginator(tfs, 3).page(1), - 'item_serializer': serializers.AudioSerializer, + "actor": library.actor, + "id": library.url, + "page": Paginator(tfs, 3).page(1), + "item_serializer": serializers.AudioSerializer, } page = serializers.CollectionPageSerializer(page_conf) - r_mock.get(page.data['id'], json=page.data) + r_mock.get(page.data["id"], json=page.data) tasks.scan_library_page( - library_id=library.pk, - page_url=page.data['id'], - until=tfs[1].creation_date) + library_id=library.pk, page_url=page.data["id"], until=tfs[1].creation_date + ) - lts = list(library.tracks.all().order_by('-published_date')) + lts = list(library.tracks.all().order_by("-published_date")) assert len(lts) == 2 for i, tf in enumerate(tfs[:1]): assert tf.creation_date == lts[i].published_date def test_clean_federation_music_cache_if_no_listen(preferences, factories): - preferences['federation__music_cache_duration'] = 60 - lt1 = factories['federation.LibraryTrack'](with_audio_file=True) - lt2 = factories['federation.LibraryTrack'](with_audio_file=True) - lt3 = factories['federation.LibraryTrack'](with_audio_file=True) - tf1 = factories['music.TrackFile']( - accessed_date=timezone.now(), library_track=lt1) - tf2 = factories['music.TrackFile']( - accessed_date=timezone.now()-datetime.timedelta(minutes=61), - library_track=lt2) - tf3 = factories['music.TrackFile']( - accessed_date=None, library_track=lt3) + preferences["federation__music_cache_duration"] = 60 + lt1 = factories["federation.LibraryTrack"](with_audio_file=True) + lt2 = factories["federation.LibraryTrack"](with_audio_file=True) + lt3 = factories["federation.LibraryTrack"](with_audio_file=True) + tf1 = factories["music.TrackFile"](accessed_date=timezone.now(), library_track=lt1) + tf2 = factories["music.TrackFile"]( + accessed_date=timezone.now() - datetime.timedelta(minutes=61), library_track=lt2 + ) + tf3 = factories["music.TrackFile"](accessed_date=None, library_track=lt3) path1 = lt1.audio_file.path path2 = lt2.audio_file.path path3 = lt3.audio_file.path @@ -145,22 +134,19 @@ def test_clean_federation_music_cache_if_no_listen(preferences, factories): assert os.path.exists(path3) is False -def test_clean_federation_music_cache_orphaned( - settings, preferences, factories): - preferences['federation__music_cache_duration'] = 60 - path = os.path.join(settings.MEDIA_ROOT, 'federation_cache') - keep_path = os.path.join(os.path.join(path, '1a', 'b2'), 'keep.ogg') - remove_path = os.path.join(os.path.join(path, 'c3', 'd4'), 'remove.ogg') +def test_clean_federation_music_cache_orphaned(settings, preferences, factories): + preferences["federation__music_cache_duration"] = 60 + path = os.path.join(settings.MEDIA_ROOT, "federation_cache") + keep_path = os.path.join(os.path.join(path, "1a", "b2"), "keep.ogg") + remove_path = os.path.join(os.path.join(path, "c3", "d4"), "remove.ogg") os.makedirs(os.path.dirname(keep_path), exist_ok=True) os.makedirs(os.path.dirname(remove_path), exist_ok=True) pathlib.Path(keep_path).touch() pathlib.Path(remove_path).touch() - lt = factories['federation.LibraryTrack']( - with_audio_file=True, - audio_file__path=keep_path) - tf = factories['music.TrackFile']( - library_track=lt, - accessed_date=timezone.now()) + lt = factories["federation.LibraryTrack"]( + with_audio_file=True, audio_file__path=keep_path + ) + tf = factories["music.TrackFile"](library_track=lt, accessed_date=timezone.now()) tasks.clean_music_cache() diff --git a/api/tests/federation/test_utils.py b/api/tests/federation/test_utils.py index dc371ad9e..dbebe0fdc 100644 --- a/api/tests/federation/test_utils.py +++ b/api/tests/federation/test_utils.py @@ -3,12 +3,15 @@ import pytest from funkwhale_api.federation import utils -@pytest.mark.parametrize('url,path,expected', [ - ('http://test.com', '/hello', 'http://test.com/hello'), - ('http://test.com/', 'hello', 'http://test.com/hello'), - ('http://test.com/', '/hello', 'http://test.com/hello'), - ('http://test.com', 'hello', 'http://test.com/hello'), -]) +@pytest.mark.parametrize( + "url,path,expected", + [ + ("http://test.com", "/hello", "http://test.com/hello"), + ("http://test.com/", "hello", "http://test.com/hello"), + ("http://test.com/", "/hello", "http://test.com/hello"), + ("http://test.com", "hello", "http://test.com/hello"), + ], +) def test_full_url(settings, url, path, expected): settings.FUNKWHALE_URL = url assert utils.full_url(path) == expected @@ -16,33 +19,34 @@ def test_full_url(settings, url, path, expected): def test_extract_headers_from_meta(): wsgi_headers = { - 'HTTP_HOST': 'nginx', - 'HTTP_X_REAL_IP': '172.20.0.4', - 'HTTP_X_FORWARDED_FOR': '188.165.228.227, 172.20.0.4', - 'HTTP_X_FORWARDED_PROTO': 'http', - 'HTTP_X_FORWARDED_HOST': 'localhost:80', - 'HTTP_X_FORWARDED_PORT': '80', - 'HTTP_CONNECTION': 'close', - 'CONTENT_LENGTH': '1155', - 'CONTENT_TYPE': 'txt/application', - 'HTTP_SIGNATURE': 'Hello', - 'HTTP_DATE': 'Sat, 31 Mar 2018 13:53:55 GMT', - 'HTTP_USER_AGENT': 'http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)'} + "HTTP_HOST": "nginx", + "HTTP_X_REAL_IP": "172.20.0.4", + "HTTP_X_FORWARDED_FOR": "188.165.228.227, 172.20.0.4", + "HTTP_X_FORWARDED_PROTO": "http", + "HTTP_X_FORWARDED_HOST": "localhost:80", + "HTTP_X_FORWARDED_PORT": "80", + "HTTP_CONNECTION": "close", + "CONTENT_LENGTH": "1155", + "CONTENT_TYPE": "txt/application", + "HTTP_SIGNATURE": "Hello", + "HTTP_DATE": "Sat, 31 Mar 2018 13:53:55 GMT", + "HTTP_USER_AGENT": "http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)", + } cleaned_headers = utils.clean_wsgi_headers(wsgi_headers) expected = { - 'Host': 'nginx', - 'X-Real-Ip': '172.20.0.4', - 'X-Forwarded-For': '188.165.228.227, 172.20.0.4', - 'X-Forwarded-Proto': 'http', - 'X-Forwarded-Host': 'localhost:80', - 'X-Forwarded-Port': '80', - 'Connection': 'close', - 'Content-Length': '1155', - 'Content-Type': 'txt/application', - 'Signature': 'Hello', - 'Date': 'Sat, 31 Mar 2018 13:53:55 GMT', - 'User-Agent': 'http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)' + "Host": "nginx", + "X-Real-Ip": "172.20.0.4", + "X-Forwarded-For": "188.165.228.227, 172.20.0.4", + "X-Forwarded-Proto": "http", + "X-Forwarded-Host": "localhost:80", + "X-Forwarded-Port": "80", + "Connection": "close", + "Content-Length": "1155", + "Content-Type": "txt/application", + "Signature": "Hello", + "Date": "Sat, 31 Mar 2018 13:53:55 GMT", + "User-Agent": "http.rb/3.0.0 (Mastodon/2.2.0; +https://mastodon.eliotberriot.com/)", } assert cleaned_headers == expected diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py index 04a419aed..2b0e47cdb 100644 --- a/api/tests/federation/test_views.py +++ b/api/tests/federation/test_views.py @@ -13,315 +13,297 @@ from funkwhale_api.federation import views from funkwhale_api.federation import webfinger -@pytest.mark.parametrize('view,permissions', [ - (views.LibraryViewSet, ['federation']), - (views.LibraryTrackViewSet, ['federation']), -]) +@pytest.mark.parametrize( + "view,permissions", + [ + (views.LibraryViewSet, ["federation"]), + (views.LibraryTrackViewSet, ["federation"]), + ], +) def test_permissions(assert_user_permission, view, permissions): assert_user_permission(view, permissions) -@pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys()) +@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys()) def test_instance_actors(system_actor, db, api_client): actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance() - url = reverse( - 'federation:instance-actors-detail', - kwargs={'actor': system_actor}) + url = reverse("federation:instance-actors-detail", kwargs={"actor": system_actor}) response = api_client.get(url) serializer = serializers.ActorSerializer(actor) - if system_actor == 'library': - response.data.pop('url') + if system_actor == "library": + response.data.pop("url") assert response.status_code == 200 assert response.data == serializer.data -@pytest.mark.parametrize('route,kwargs', [ - ('instance-actors-outbox', {'actor': 'library'}), - ('instance-actors-inbox', {'actor': 'library'}), - ('instance-actors-detail', {'actor': 'library'}), - ('well-known-webfinger', {}), -]) +@pytest.mark.parametrize( + "route,kwargs", + [ + ("instance-actors-outbox", {"actor": "library"}), + ("instance-actors-inbox", {"actor": "library"}), + ("instance-actors-detail", {"actor": "library"}), + ("well-known-webfinger", {}), + ], +) def test_instance_endpoints_405_if_federation_disabled( - authenticated_actor, db, preferences, api_client, route, kwargs): - preferences['federation__enabled'] = False - url = reverse('federation:{}'.format(route), kwargs=kwargs) + authenticated_actor, db, preferences, api_client, route, kwargs +): + preferences["federation__enabled"] = False + url = reverse("federation:{}".format(route), kwargs=kwargs) response = api_client.get(url) assert response.status_code == 405 -def test_wellknown_webfinger_validates_resource( - db, api_client, settings, mocker): - clean = mocker.spy(webfinger, 'clean_resource') - url = reverse('federation:well-known-webfinger') - response = api_client.get(url, data={'resource': 'something'}) +def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker): + clean = mocker.spy(webfinger, "clean_resource") + url = reverse("federation:well-known-webfinger") + response = api_client.get(url, data={"resource": "something"}) - clean.assert_called_once_with('something') - assert url == '/.well-known/webfinger' + clean.assert_called_once_with("something") + assert url == "/.well-known/webfinger" assert response.status_code == 400 - assert response.data['errors']['resource'] == ( - 'Missing webfinger resource type' - ) + assert response.data["errors"]["resource"] == ("Missing webfinger resource type") -@pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys()) -def test_wellknown_webfinger_system( - system_actor, db, api_client, settings, mocker): +@pytest.mark.parametrize("system_actor", actors.SYSTEM_ACTORS.keys()) +def test_wellknown_webfinger_system(system_actor, db, api_client, settings, mocker): actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance() - url = reverse('federation:well-known-webfinger') + url = reverse("federation:well-known-webfinger") response = api_client.get( url, - data={'resource': 'acct:{}'.format(actor.webfinger_subject)}, - HTTP_ACCEPT='application/jrd+json', + data={"resource": "acct:{}".format(actor.webfinger_subject)}, + HTTP_ACCEPT="application/jrd+json", ) serializer = serializers.ActorWebfingerSerializer(actor) assert response.status_code == 200 - assert response['Content-Type'] == 'application/jrd+json' + assert response["Content-Type"] == "application/jrd+json" assert response.data == serializer.data def test_wellknown_nodeinfo(db, preferences, api_client, settings): expected = { - 'links': [ + "links": [ { - 'rel': 'http://nodeinfo.diaspora.software/ns/schema/2.0', - 'href': '{}{}'.format( - settings.FUNKWHALE_URL, - reverse('api:v1:instance:nodeinfo-2.0') - ) + "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", + "href": "{}{}".format( + settings.FUNKWHALE_URL, reverse("api:v1:instance:nodeinfo-2.0") + ), } ] } - url = reverse('federation:well-known-nodeinfo') - response = api_client.get(url, HTTP_ACCEPT='application/jrd+json') + url = reverse("federation:well-known-nodeinfo") + response = api_client.get(url, HTTP_ACCEPT="application/jrd+json") assert response.status_code == 200 - assert response['Content-Type'] == 'application/jrd+json' + assert response["Content-Type"] == "application/jrd+json" assert response.data == expected def test_wellknown_nodeinfo_disabled(db, preferences, api_client): - preferences['instance__nodeinfo_enabled'] = False - url = reverse('federation:well-known-nodeinfo') + preferences["instance__nodeinfo_enabled"] = False + url = reverse("federation:well-known-nodeinfo") response = api_client.get(url) assert response.status_code == 404 -def test_audio_file_list_requires_authenticated_actor( - db, preferences, api_client): - preferences['federation__music_needs_approval'] = True - url = reverse('federation:music:files-list') +def test_audio_file_list_requires_authenticated_actor(db, preferences, api_client): + preferences["federation__music_needs_approval"] = True + url = reverse("federation:music:files-list") response = api_client.get(url) assert response.status_code == 403 -def test_audio_file_list_actor_no_page( - db, preferences, api_client, factories): - preferences['federation__music_needs_approval'] = False - preferences['federation__collection_page_size'] = 2 - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() - tfs = factories['music.TrackFile'].create_batch(size=5) +def test_audio_file_list_actor_no_page(db, preferences, api_client, factories): + preferences["federation__music_needs_approval"] = False + preferences["federation__collection_page_size"] = 2 + library = actors.SYSTEM_ACTORS["library"].get_actor_instance() + tfs = factories["music.TrackFile"].create_batch(size=5) conf = { - 'id': utils.full_url(reverse('federation:music:files-list')), - 'page_size': 2, - 'items': list(reversed(tfs)), # we order by -creation_date - 'item_serializer': serializers.AudioSerializer, - 'actor': library + "id": utils.full_url(reverse("federation:music:files-list")), + "page_size": 2, + "items": list(reversed(tfs)), # we order by -creation_date + "item_serializer": serializers.AudioSerializer, + "actor": library, } expected = serializers.PaginatedCollectionSerializer(conf).data - url = reverse('federation:music:files-list') + url = reverse("federation:music:files-list") response = api_client.get(url) assert response.status_code == 200 assert response.data == expected -def test_audio_file_list_actor_page( - db, preferences, api_client, factories): - preferences['federation__music_needs_approval'] = False - preferences['federation__collection_page_size'] = 2 - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() - tfs = factories['music.TrackFile'].create_batch(size=5) +def test_audio_file_list_actor_page(db, preferences, api_client, factories): + preferences["federation__music_needs_approval"] = False + preferences["federation__collection_page_size"] = 2 + library = actors.SYSTEM_ACTORS["library"].get_actor_instance() + tfs = factories["music.TrackFile"].create_batch(size=5) conf = { - 'id': utils.full_url(reverse('federation:music:files-list')), - 'page': Paginator(list(reversed(tfs)), 2).page(2), - 'item_serializer': serializers.AudioSerializer, - 'actor': library + "id": utils.full_url(reverse("federation:music:files-list")), + "page": Paginator(list(reversed(tfs)), 2).page(2), + "item_serializer": serializers.AudioSerializer, + "actor": library, } expected = serializers.CollectionPageSerializer(conf).data - url = reverse('federation:music:files-list') - response = api_client.get(url, data={'page': 2}) + url = reverse("federation:music:files-list") + response = api_client.get(url, data={"page": 2}) assert response.status_code == 200 assert response.data == expected def test_audio_file_list_actor_page_exclude_federated_files( - db, preferences, api_client, factories): - preferences['federation__music_needs_approval'] = False - library = actors.SYSTEM_ACTORS['library'].get_actor_instance() - tfs = factories['music.TrackFile'].create_batch(size=5, federation=True) + db, preferences, api_client, factories +): + preferences["federation__music_needs_approval"] = False + library = actors.SYSTEM_ACTORS["library"].get_actor_instance() + tfs = factories["music.TrackFile"].create_batch(size=5, federation=True) - url = reverse('federation:music:files-list') + url = reverse("federation:music:files-list") response = api_client.get(url) assert response.status_code == 200 - assert response.data['totalItems'] == 0 + assert response.data["totalItems"] == 0 -def test_audio_file_list_actor_page_error( - db, preferences, api_client, factories): - preferences['federation__music_needs_approval'] = False - url = reverse('federation:music:files-list') - response = api_client.get(url, data={'page': 'nope'}) +def test_audio_file_list_actor_page_error(db, preferences, api_client, factories): + preferences["federation__music_needs_approval"] = False + url = reverse("federation:music:files-list") + response = api_client.get(url, data={"page": "nope"}) assert response.status_code == 400 def test_audio_file_list_actor_page_error_too_far( - db, preferences, api_client, factories): - preferences['federation__music_needs_approval'] = False - url = reverse('federation:music:files-list') - response = api_client.get(url, data={'page': 5000}) + db, preferences, api_client, factories +): + preferences["federation__music_needs_approval"] = False + url = reverse("federation:music:files-list") + response = api_client.get(url, data={"page": 5000}) assert response.status_code == 404 def test_library_actor_includes_library_link(db, preferences, api_client): - actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - url = reverse( - 'federation:instance-actors-detail', - kwargs={'actor': 'library'}) + actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + url = reverse("federation:instance-actors-detail", kwargs={"actor": "library"}) response = api_client.get(url) expected_links = [ { - 'type': 'Link', - 'name': 'library', - 'mediaType': 'application/activity+json', - 'href': utils.full_url(reverse('federation:music:files-list')) + "type": "Link", + "name": "library", + "mediaType": "application/activity+json", + "href": utils.full_url(reverse("federation:music:files-list")), } ] assert response.status_code == 200 - assert response.data['url'] == expected_links + assert response.data["url"] == expected_links def test_can_fetch_library(superuser_api_client, mocker): - result = {'test': 'test'} + result = {"test": "test"} scan = mocker.patch( - 'funkwhale_api.federation.library.scan_from_account_name', - return_value=result) + "funkwhale_api.federation.library.scan_from_account_name", return_value=result + ) - url = reverse('api:v1:federation:libraries-fetch') - response = superuser_api_client.get( - url, data={'account': 'test@test.library'}) + url = reverse("api:v1:federation:libraries-fetch") + response = superuser_api_client.get(url, data={"account": "test@test.library"}) assert response.status_code == 200 assert response.data == result - scan.assert_called_once_with('test@test.library') + scan.assert_called_once_with("test@test.library") def test_follow_library(superuser_api_client, mocker, factories, r_mock): - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - actor = factories['federation.Actor']() - follow = {'test': 'follow'} - on_commit = mocker.patch( - 'funkwhale_api.common.utils.on_commit') + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + actor = factories["federation.Actor"]() + follow = {"test": "follow"} + on_commit = mocker.patch("funkwhale_api.common.utils.on_commit") actor_data = serializers.ActorSerializer(actor).data - actor_data['url'] = [{ - 'href': 'https://test.library', - 'name': 'library', - 'type': 'Link', - }] + actor_data["url"] = [ + {"href": "https://test.library", "name": "library", "type": "Link"} + ] library_conf = { - 'id': 'https://test.library', - 'items': range(10), - 'actor': actor, - 'page_size': 5, + "id": "https://test.library", + "items": range(10), + "actor": actor, + "page_size": 5, } library_data = serializers.PaginatedCollectionSerializer(library_conf).data r_mock.get(actor.url, json=actor_data) - r_mock.get('https://test.library', json=library_data) + r_mock.get("https://test.library", json=library_data) data = { - 'actor': actor.url, - 'autoimport': False, - 'federation_enabled': True, - 'download_files': False, + "actor": actor.url, + "autoimport": False, + "federation_enabled": True, + "download_files": False, } - url = reverse('api:v1:federation:libraries-list') - response = superuser_api_client.post( - url, data) + url = reverse("api:v1:federation:libraries-list") + response = superuser_api_client.post(url, data) assert response.status_code == 201 - follow = models.Follow.objects.get( - actor=library_actor, - target=actor, - approved=None, - ) + follow = models.Follow.objects.get(actor=library_actor, target=actor, approved=None) library = follow.library - assert response.data == serializers.APILibraryCreateSerializer( - library).data + assert response.data == serializers.APILibraryCreateSerializer(library).data on_commit.assert_called_once_with( activity.deliver, serializers.FollowSerializer(follow).data, on_behalf_of=library_actor, - to=[actor.url] + to=[actor.url], ) def test_can_list_system_actor_following(factories, superuser_api_client): - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - follow1 = factories['federation.Follow'](actor=library_actor) - follow2 = factories['federation.Follow']() + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + follow1 = factories["federation.Follow"](actor=library_actor) + follow2 = factories["federation.Follow"]() - url = reverse('api:v1:federation:libraries-following') + url = reverse("api:v1:federation:libraries-following") response = superuser_api_client.get(url) assert response.status_code == 200 - assert response.data['results'] == [ - serializers.APIFollowSerializer(follow1).data - ] + assert response.data["results"] == [serializers.APIFollowSerializer(follow1).data] def test_can_list_system_actor_followers(factories, superuser_api_client): - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - follow1 = factories['federation.Follow'](actor=library_actor) - follow2 = factories['federation.Follow'](target=library_actor) + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + follow1 = factories["federation.Follow"](actor=library_actor) + follow2 = factories["federation.Follow"](target=library_actor) - url = reverse('api:v1:federation:libraries-followers') + url = reverse("api:v1:federation:libraries-followers") response = superuser_api_client.get(url) assert response.status_code == 200 - assert response.data['results'] == [ - serializers.APIFollowSerializer(follow2).data - ] + assert response.data["results"] == [serializers.APIFollowSerializer(follow2).data] def test_can_list_libraries(factories, superuser_api_client): - library1 = factories['federation.Library']() - library2 = factories['federation.Library']() + library1 = factories["federation.Library"]() + library2 = factories["federation.Library"]() - url = reverse('api:v1:federation:libraries-list') + url = reverse("api:v1:federation:libraries-list") response = superuser_api_client.get(url) assert response.status_code == 200 - assert response.data['results'] == [ + assert response.data["results"] == [ serializers.APILibrarySerializer(library1).data, serializers.APILibrarySerializer(library2).data, ] def test_can_detail_library(factories, superuser_api_client): - library = factories['federation.Library']() + library = factories["federation.Library"]() url = reverse( - 'api:v1:federation:libraries-detail', - kwargs={'uuid': str(library.uuid)}) + "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)} + ) response = superuser_api_client.get(url) assert response.status_code == 200 @@ -329,15 +311,15 @@ def test_can_detail_library(factories, superuser_api_client): def test_can_patch_library(factories, superuser_api_client): - library = factories['federation.Library']() + library = factories["federation.Library"]() data = { - 'federation_enabled': not library.federation_enabled, - 'download_files': not library.download_files, - 'autoimport': not library.autoimport, + "federation_enabled": not library.federation_enabled, + "download_files": not library.download_files, + "autoimport": not library.autoimport, } url = reverse( - 'api:v1:federation:libraries-detail', - kwargs={'uuid': str(library.uuid)}) + "api:v1:federation:libraries-detail", kwargs={"uuid": str(library.uuid)} + ) response = superuser_api_client.patch(url, data) assert response.status_code == 200 @@ -349,55 +331,49 @@ def test_can_patch_library(factories, superuser_api_client): def test_scan_library(factories, mocker, superuser_api_client): scan = mocker.patch( - 'funkwhale_api.federation.tasks.scan_library.delay', - return_value=mocker.Mock(id='id')) - library = factories['federation.Library']() + "funkwhale_api.federation.tasks.scan_library.delay", + return_value=mocker.Mock(id="id"), + ) + library = factories["federation.Library"]() now = timezone.now() - data = { - 'until': now, - } + data = {"until": now} url = reverse( - 'api:v1:federation:libraries-scan', - kwargs={'uuid': str(library.uuid)}) + "api:v1:federation:libraries-scan", kwargs={"uuid": str(library.uuid)} + ) response = superuser_api_client.post(url, data) assert response.status_code == 200 - assert response.data == {'task': 'id'} - scan.assert_called_once_with( - library_id=library.pk, - until=now - ) + assert response.data == {"task": "id"} + scan.assert_called_once_with(library_id=library.pk, until=now) def test_list_library_tracks(factories, superuser_api_client): - library = factories['federation.Library']() - lts = list(reversed(factories['federation.LibraryTrack'].create_batch( - size=5, library=library))) - factories['federation.LibraryTrack'].create_batch(size=5) - url = reverse('api:v1:federation:library-tracks-list') - response = superuser_api_client.get(url, {'library': library.uuid}) + library = factories["federation.Library"]() + lts = list( + reversed( + factories["federation.LibraryTrack"].create_batch(size=5, library=library) + ) + ) + factories["federation.LibraryTrack"].create_batch(size=5) + url = reverse("api:v1:federation:library-tracks-list") + response = superuser_api_client.get(url, {"library": library.uuid}) assert response.status_code == 200 assert response.data == { - 'results': serializers.APILibraryTrackSerializer(lts, many=True).data, - 'count': 5, - 'previous': None, - 'next': None, + "results": serializers.APILibraryTrackSerializer(lts, many=True).data, + "count": 5, + "previous": None, + "next": None, } def test_can_update_follow_status(factories, superuser_api_client, mocker): - patched_accept = mocker.patch( - 'funkwhale_api.federation.activity.accept_follow' - ) - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - follow = factories['federation.Follow'](target=library_actor) + patched_accept = mocker.patch("funkwhale_api.federation.activity.accept_follow") + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + follow = factories["federation.Follow"](target=library_actor) - payload = { - 'follow': follow.pk, - 'approved': True - } - url = reverse('api:v1:federation:libraries-followers') + payload = {"follow": follow.pk, "approved": True} + url = reverse("api:v1:federation:libraries-followers") response = superuser_api_client.patch(url, payload) follow.refresh_from_db() @@ -407,45 +383,33 @@ def test_can_update_follow_status(factories, superuser_api_client, mocker): def test_can_filter_pending_follows(factories, superuser_api_client): - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - follow = factories['federation.Follow']( - target=library_actor, - approved=True) + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + follow = factories["federation.Follow"](target=library_actor, approved=True) - params = {'pending': True} - url = reverse('api:v1:federation:libraries-followers') + params = {"pending": True} + url = reverse("api:v1:federation:libraries-followers") response = superuser_api_client.get(url, params) assert response.status_code == 200 - assert len(response.data['results']) == 0 + assert len(response.data["results"]) == 0 -def test_library_track_action_import( - factories, superuser_api_client, mocker): - lt1 = factories['federation.LibraryTrack']() - lt2 = factories['federation.LibraryTrack'](library=lt1.library) - lt3 = factories['federation.LibraryTrack']() - lt4 = factories['federation.LibraryTrack'](library=lt3.library) - mocked_run = mocker.patch( - 'funkwhale_api.music.tasks.import_batch_run.delay') +def test_library_track_action_import(factories, superuser_api_client, mocker): + lt1 = factories["federation.LibraryTrack"]() + lt2 = factories["federation.LibraryTrack"](library=lt1.library) + lt3 = factories["federation.LibraryTrack"]() + lt4 = factories["federation.LibraryTrack"](library=lt3.library) + mocked_run = mocker.patch("funkwhale_api.music.tasks.import_batch_run.delay") payload = { - 'objects': 'all', - 'action': 'import', - 'filters': { - 'library': lt1.library.uuid - } - } - url = reverse('api:v1:federation:library-tracks-action') - response = superuser_api_client.post(url, payload, format='json') - batch = superuser_api_client.user.imports.latest('id') - expected = { - 'updated': 2, - 'action': 'import', - 'result': { - 'batch': {'id': batch.pk} - } + "objects": "all", + "action": "import", + "filters": {"library": lt1.library.uuid}, } + url = reverse("api:v1:federation:library-tracks-action") + response = superuser_api_client.post(url, payload, format="json") + batch = superuser_api_client.user.imports.latest("id") + expected = {"updated": 2, "action": "import", "result": {"batch": {"id": batch.pk}}} imported_lts = [lt1, lt2] assert response.status_code == 200 diff --git a/api/tests/federation/test_webfinger.py b/api/tests/federation/test_webfinger.py index 4b8dca207..1b7db9278 100644 --- a/api/tests/federation/test_webfinger.py +++ b/api/tests/federation/test_webfinger.py @@ -7,16 +7,19 @@ from funkwhale_api.federation import webfinger def test_webfinger_clean_resource(): - t, r = webfinger.clean_resource('acct:service@test.federation') - assert t == 'acct' - assert r == 'service@test.federation' + t, r = webfinger.clean_resource("acct:service@test.federation") + assert t == "acct" + assert r == "service@test.federation" -@pytest.mark.parametrize('resource,message', [ - ('', 'Invalid resource string'), - ('service@test.com', 'Missing webfinger resource type'), - ('noop:service@test.com', 'Invalid webfinger resource type'), -]) +@pytest.mark.parametrize( + "resource,message", + [ + ("", "Invalid resource string"), + ("service@test.com", "Missing webfinger resource type"), + ("noop:service@test.com", "Invalid webfinger resource type"), + ], +) def test_webfinger_clean_resource_errors(resource, message): with pytest.raises(forms.ValidationError) as excinfo: webfinger.clean_resource(resource) @@ -25,16 +28,19 @@ def test_webfinger_clean_resource_errors(resource, message): def test_webfinger_clean_acct(settings): - username, hostname = webfinger.clean_acct('library@test.federation') - assert username == 'library' - assert hostname == 'test.federation' + username, hostname = webfinger.clean_acct("library@test.federation") + assert username == "library" + assert hostname == "test.federation" -@pytest.mark.parametrize('resource,message', [ - ('service', 'Invalid format'), - ('service@test.com', 'Invalid hostname test.com'), - ('noop@test.federation', 'Invalid account'), -]) +@pytest.mark.parametrize( + "resource,message", + [ + ("service", "Invalid format"), + ("service@test.com", "Invalid hostname test.com"), + ("noop@test.federation", "Invalid account"), + ], +) def test_webfinger_clean_acct_errors(resource, message, settings): with pytest.raises(forms.ValidationError) as excinfo: webfinger.clean_resource(resource) @@ -43,26 +49,24 @@ def test_webfinger_clean_acct_errors(resource, message, settings): def test_webfinger_get_resource(r_mock): - resource = 'acct:test@test.webfinger' + resource = "acct:test@test.webfinger" payload = { - 'subject': resource, - 'aliases': ['https://test.webfinger'], - 'links': [ + "subject": resource, + "aliases": ["https://test.webfinger"], + "links": [ { - 'rel': 'self', - 'type': 'application/activity+json', - 'href': 'https://test.webfinger/user/test' + "rel": "self", + "type": "application/activity+json", + "href": "https://test.webfinger/user/test", } - ] + ], } r_mock.get( - 'https://test.webfinger/.well-known/webfinger?resource={}'.format( - resource - ), - json=payload + "https://test.webfinger/.well-known/webfinger?resource={}".format(resource), + json=payload, ) - data = webfinger.get_resource('acct:test@test.webfinger') + data = webfinger.get_resource("acct:test@test.webfinger") - assert data['actor_url'] == 'https://test.webfinger/user/test' - assert data['subject'] == resource + assert data["actor_url"] == "https://test.webfinger/user/test" + assert data["subject"] == resource diff --git a/api/tests/history/test_activity.py b/api/tests/history/test_activity.py index 04000604b..c3cca5925 100644 --- a/api/tests/history/test_activity.py +++ b/api/tests/history/test_activity.py @@ -5,15 +5,14 @@ from funkwhale_api.history import activities def test_get_listening_activity_url(settings, factories): - listening = factories['history.Listening']() + listening = factories["history.Listening"]() user_url = listening.user.get_activity_url() - expected = '{}/listenings/tracks/{}'.format( - user_url, listening.pk) + expected = "{}/listenings/tracks/{}".format(user_url, listening.pk) assert listening.get_activity_url() == expected def test_activity_listening_serializer(factories): - listening = factories['history.Listening']() + listening = factories["history.Listening"]() actor = UserActivitySerializer(listening.user).data field = serializers.serializers.DateTimeField() @@ -32,44 +31,31 @@ def test_activity_listening_serializer(factories): def test_track_listening_serializer_is_connected(activity_registry): - conf = activity_registry['history.Listening'] - assert conf['serializer'] == serializers.ListeningActivitySerializer + conf = activity_registry["history.Listening"] + assert conf["serializer"] == serializers.ListeningActivitySerializer -def test_track_listening_serializer_instance_activity_consumer( - activity_registry): - conf = activity_registry['history.Listening'] +def test_track_listening_serializer_instance_activity_consumer(activity_registry): + conf = activity_registry["history.Listening"] consumer = activities.broadcast_listening_to_instance_activity - assert consumer in conf['consumers'] + assert consumer in conf["consumers"] -def test_broadcast_listening_to_instance_activity( - factories, mocker): - p = mocker.patch('funkwhale_api.common.channels.group_send') - listening = factories['history.Listening']() +def test_broadcast_listening_to_instance_activity(factories, mocker): + p = mocker.patch("funkwhale_api.common.channels.group_send") + listening = factories["history.Listening"]() data = serializers.ListeningActivitySerializer(listening).data consumer = activities.broadcast_listening_to_instance_activity - message = { - "type": 'event.send', - "text": '', - "data": data - } + message = {"type": "event.send", "text": "", "data": data} consumer(data=data, obj=listening) - p.assert_called_once_with('instance_activity', message) + p.assert_called_once_with("instance_activity", message) -def test_broadcast_listening_to_instance_activity_private( - factories, mocker): - p = mocker.patch('funkwhale_api.common.channels.group_send') - listening = factories['history.Listening']( - user__privacy_level='me' - ) +def test_broadcast_listening_to_instance_activity_private(factories, mocker): + p = mocker.patch("funkwhale_api.common.channels.group_send") + listening = factories["history.Listening"](user__privacy_level="me") data = serializers.ListeningActivitySerializer(listening).data consumer = activities.broadcast_listening_to_instance_activity - message = { - "type": 'event.send', - "text": '', - "data": data - } + message = {"type": "event.send", "text": "", "data": data} consumer(data=data, obj=listening) p.assert_not_called() diff --git a/api/tests/history/test_history.py b/api/tests/history/test_history.py index 202725596..016c2a987 100644 --- a/api/tests/history/test_history.py +++ b/api/tests/history/test_history.py @@ -8,36 +8,34 @@ from funkwhale_api.history import models def test_can_create_listening(factories): - track = factories['music.Track']() - user = factories['users.User']() + track = factories["music.Track"]() + user = factories["users.User"]() now = timezone.now() l = models.Listening.objects.create(user=user, track=track) def test_logged_in_user_can_create_listening_via_api( - logged_in_client, factories, activity_muted): - track = factories['music.Track']() + logged_in_client, factories, activity_muted +): + track = factories["music.Track"]() - url = reverse('api:v1:history:listenings-list') - response = logged_in_client.post(url, { - 'track': track.pk, - }) + url = reverse("api:v1:history:listenings-list") + response = logged_in_client.post(url, {"track": track.pk}) - listening = models.Listening.objects.latest('id') + listening = models.Listening.objects.latest("id") assert listening.track == track assert listening.user == logged_in_client.user def test_adding_listening_calls_activity_record( - factories, logged_in_client, activity_muted): - track = factories['music.Track']() + factories, logged_in_client, activity_muted +): + track = factories["music.Track"]() - url = reverse('api:v1:history:listenings-list') - response = logged_in_client.post(url, { - 'track': track.pk, - }) + url = reverse("api:v1:history:listenings-list") + response = logged_in_client.post(url, {"track": track.pk}) - listening = models.Listening.objects.latest('id') + listening = models.Listening.objects.latest("id") activity_muted.assert_called_once_with(listening) diff --git a/api/tests/instance/test_nodeinfo.py b/api/tests/instance/test_nodeinfo.py index 87b888288..3c2fa862e 100644 --- a/api/tests/instance/test_nodeinfo.py +++ b/api/tests/instance/test_nodeinfo.py @@ -6,102 +6,76 @@ from funkwhale_api.instance import nodeinfo def test_nodeinfo_dump(preferences, mocker): - preferences['instance__nodeinfo_stats_enabled'] = True + preferences["instance__nodeinfo_stats_enabled"] = True stats = { - 'users': 1, - 'tracks': 2, - 'albums': 3, - 'artists': 4, - 'track_favorites': 5, - 'music_duration': 6, - 'listenings': 7, + "users": 1, + "tracks": 2, + "albums": 3, + "artists": 4, + "track_favorites": 5, + "music_duration": 6, + "listenings": 7, } - mocker.patch('funkwhale_api.instance.stats.get', return_value=stats) + mocker.patch("funkwhale_api.instance.stats.get", return_value=stats) expected = { - 'version': '2.0', - 'software': { - 'name': 'funkwhale', - 'version': funkwhale_api.__version__ - }, - 'protocols': ['activitypub'], - 'services': { - 'inbound': [], - 'outbound': [] - }, - 'openRegistrations': preferences['users__registration_enabled'], - 'usage': { - 'users': { - 'total': stats['users'], - } - }, - 'metadata': { - 'private': preferences['instance__nodeinfo_private'], - 'shortDescription': preferences['instance__short_description'], - 'longDescription': preferences['instance__long_description'], - 'nodeName': preferences['instance__name'], - 'library': { - 'federationEnabled': preferences['federation__enabled'], - 'federationNeedsApproval': preferences['federation__music_needs_approval'], - 'anonymousCanListen': preferences['common__api_authentication_required'], - 'tracks': { - 'total': stats['tracks'], - }, - 'artists': { - 'total': stats['artists'], - }, - 'albums': { - 'total': stats['albums'], - }, - 'music': { - 'hours': stats['music_duration'] - }, + "version": "2.0", + "software": {"name": "funkwhale", "version": funkwhale_api.__version__}, + "protocols": ["activitypub"], + "services": {"inbound": [], "outbound": []}, + "openRegistrations": preferences["users__registration_enabled"], + "usage": {"users": {"total": stats["users"]}}, + "metadata": { + "private": preferences["instance__nodeinfo_private"], + "shortDescription": preferences["instance__short_description"], + "longDescription": preferences["instance__long_description"], + "nodeName": preferences["instance__name"], + "library": { + "federationEnabled": preferences["federation__enabled"], + "federationNeedsApproval": preferences[ + "federation__music_needs_approval" + ], + "anonymousCanListen": preferences[ + "common__api_authentication_required" + ], + "tracks": {"total": stats["tracks"]}, + "artists": {"total": stats["artists"]}, + "albums": {"total": stats["albums"]}, + "music": {"hours": stats["music_duration"]}, }, - 'usage': { - 'favorites': { - 'tracks': { - 'total': stats['track_favorites'], - } - }, - 'listenings': { - 'total': stats['listenings'] - } - } - } + "usage": { + "favorites": {"tracks": {"total": stats["track_favorites"]}}, + "listenings": {"total": stats["listenings"]}, + }, + }, } assert nodeinfo.get() == expected def test_nodeinfo_dump_stats_disabled(preferences, mocker): - preferences['instance__nodeinfo_stats_enabled'] = False + preferences["instance__nodeinfo_stats_enabled"] = False expected = { - 'version': '2.0', - 'software': { - 'name': 'funkwhale', - 'version': funkwhale_api.__version__ - }, - 'protocols': ['activitypub'], - 'services': { - 'inbound': [], - 'outbound': [] - }, - 'openRegistrations': preferences['users__registration_enabled'], - 'usage': { - 'users': { - 'total': 0, - } - }, - 'metadata': { - 'private': preferences['instance__nodeinfo_private'], - 'shortDescription': preferences['instance__short_description'], - 'longDescription': preferences['instance__long_description'], - 'nodeName': preferences['instance__name'], - 'library': { - 'federationEnabled': preferences['federation__enabled'], - 'federationNeedsApproval': preferences['federation__music_needs_approval'], - 'anonymousCanListen': preferences['common__api_authentication_required'], + "version": "2.0", + "software": {"name": "funkwhale", "version": funkwhale_api.__version__}, + "protocols": ["activitypub"], + "services": {"inbound": [], "outbound": []}, + "openRegistrations": preferences["users__registration_enabled"], + "usage": {"users": {"total": 0}}, + "metadata": { + "private": preferences["instance__nodeinfo_private"], + "shortDescription": preferences["instance__short_description"], + "longDescription": preferences["instance__long_description"], + "nodeName": preferences["instance__name"], + "library": { + "federationEnabled": preferences["federation__enabled"], + "federationNeedsApproval": preferences[ + "federation__music_needs_approval" + ], + "anonymousCanListen": preferences[ + "common__api_authentication_required" + ], }, - } + }, } assert nodeinfo.get() == expected diff --git a/api/tests/instance/test_preferences.py b/api/tests/instance/test_preferences.py index beb8e6d33..85932145b 100644 --- a/api/tests/instance/test_preferences.py +++ b/api/tests/instance/test_preferences.py @@ -6,12 +6,13 @@ from dynamic_preferences.api import serializers def test_can_list_settings_via_api(preferences, api_client): - url = reverse('api:v1:instance:settings') + url = reverse("api:v1:instance:settings") all_preferences = preferences.model.objects.all() expected_preferences = { p.preference.identifier(): p for p in all_preferences - if getattr(p.preference, 'show_in_api', False)} + if getattr(p.preference, "show_in_api", False) + } assert len(expected_preferences) > 0 @@ -20,15 +21,18 @@ def test_can_list_settings_via_api(preferences, api_client): assert len(response.data) == len(expected_preferences) for p in response.data: - i = '__'.join([p['section'], p['name']]) + i = "__".join([p["section"], p["name"]]) assert i in expected_preferences -@pytest.mark.parametrize('pref,value', [ - ('instance__name', 'My instance'), - ('instance__short_description', 'For music lovers'), - ('instance__long_description', 'For real music lovers'), -]) +@pytest.mark.parametrize( + "pref,value", + [ + ("instance__name", "My instance"), + ("instance__short_description", "For music lovers"), + ("instance__long_description", "For real music lovers"), + ], +) def test_instance_settings(pref, value, preferences): preferences[pref] = value diff --git a/api/tests/instance/test_stats.py b/api/tests/instance/test_stats.py index 6063e9300..9e60d1cdd 100644 --- a/api/tests/instance/test_stats.py +++ b/api/tests/instance/test_stats.py @@ -4,14 +4,13 @@ from funkwhale_api.instance import stats def test_get_users(mocker): - mocker.patch( - 'funkwhale_api.users.models.User.objects.count', return_value=42) + mocker.patch("funkwhale_api.users.models.User.objects.count", return_value=42) assert stats.get_users() == 42 def test_get_music_duration(factories): - factories['music.TrackFile'].create_batch(size=5, duration=360) + factories["music.TrackFile"].create_batch(size=5, duration=360) # duration is in hours assert stats.get_music_duration() == 0.5 @@ -19,56 +18,48 @@ def test_get_music_duration(factories): def test_get_listenings(mocker): mocker.patch( - 'funkwhale_api.history.models.Listening.objects.count', - return_value=42) + "funkwhale_api.history.models.Listening.objects.count", return_value=42 + ) assert stats.get_listenings() == 42 def test_get_track_favorites(mocker): mocker.patch( - 'funkwhale_api.favorites.models.TrackFavorite.objects.count', - return_value=42) + "funkwhale_api.favorites.models.TrackFavorite.objects.count", return_value=42 + ) assert stats.get_track_favorites() == 42 def test_get_tracks(mocker): - mocker.patch( - 'funkwhale_api.music.models.Track.objects.count', - return_value=42) + mocker.patch("funkwhale_api.music.models.Track.objects.count", return_value=42) assert stats.get_tracks() == 42 def test_get_albums(mocker): - mocker.patch( - 'funkwhale_api.music.models.Album.objects.count', - return_value=42) + mocker.patch("funkwhale_api.music.models.Album.objects.count", return_value=42) assert stats.get_albums() == 42 def test_get_artists(mocker): - mocker.patch( - 'funkwhale_api.music.models.Artist.objects.count', - return_value=42) + mocker.patch("funkwhale_api.music.models.Artist.objects.count", return_value=42) assert stats.get_artists() == 42 def test_get(mocker): keys = [ - 'users', - 'tracks', - 'albums', - 'artists', - 'track_favorites', - 'listenings', - 'music_duration', + "users", + "tracks", + "albums", + "artists", + "track_favorites", + "listenings", + "music_duration", ] mocks = [ - mocker.patch.object(stats, 'get_{}'.format(k), return_value=i) + mocker.patch.object(stats, "get_{}".format(k), return_value=i) for i, k in enumerate(keys) ] - expected = { - k: i for i, k in enumerate(keys) - } + expected = {k: i for i, k in enumerate(keys)} assert stats.get() == expected diff --git a/api/tests/instance/test_views.py b/api/tests/instance/test_views.py index daf54db51..dc9de2a8c 100644 --- a/api/tests/instance/test_views.py +++ b/api/tests/instance/test_views.py @@ -5,58 +5,53 @@ from django.urls import reverse from funkwhale_api.instance import views -@pytest.mark.parametrize('view,permissions', [ - (views.AdminSettings, ['settings']), -]) +@pytest.mark.parametrize("view,permissions", [(views.AdminSettings, ["settings"])]) def test_permissions(assert_user_permission, view, permissions): assert_user_permission(view, permissions) def test_nodeinfo_endpoint(db, api_client, mocker): - payload = { - 'test': 'test' - } + payload = {"test": "test"} mocked_nodeinfo = mocker.patch( - 'funkwhale_api.instance.nodeinfo.get', return_value=payload) - url = reverse('api:v1:instance:nodeinfo-2.0') + "funkwhale_api.instance.nodeinfo.get", return_value=payload + ) + url = reverse("api:v1:instance:nodeinfo-2.0") response = api_client.get(url) - ct = 'application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8' # noqa + ct = "application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8" # noqa assert response.status_code == 200 - assert response['Content-Type'] == ct + assert response["Content-Type"] == ct assert response.data == payload def test_nodeinfo_endpoint_disabled(db, api_client, preferences): - preferences['instance__nodeinfo_enabled'] = False - url = reverse('api:v1:instance:nodeinfo-2.0') + preferences["instance__nodeinfo_enabled"] = False + url = reverse("api:v1:instance:nodeinfo-2.0") response = api_client.get(url) assert response.status_code == 404 def test_settings_only_list_public_settings(db, api_client, preferences): - url = reverse('api:v1:instance:settings') + url = reverse("api:v1:instance:settings") response = api_client.get(url) for conf in response.data: - p = preferences.model.objects.get( - section=conf['section'], name=conf['name']) + p = preferences.model.objects.get(section=conf["section"], name=conf["name"]) assert p.preference.show_in_api is True def test_admin_settings_restrict_access(db, logged_in_api_client, preferences): - url = reverse('api:v1:instance:admin-settings-list') + url = reverse("api:v1:instance:admin-settings-list") response = logged_in_api_client.get(url) assert response.status_code == 403 -def test_admin_settings_correct_permission( - db, logged_in_api_client, preferences): +def test_admin_settings_correct_permission(db, logged_in_api_client, preferences): user = logged_in_api_client.user user.permission_settings = True user.save() - url = reverse('api:v1:instance:admin-settings-list') + url = reverse("api:v1:instance:admin-settings-list") response = logged_in_api_client.get(url) assert response.status_code == 200 diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py index 45167722c..893cfd86e 100644 --- a/api/tests/manage/test_serializers.py +++ b/api/tests/manage/test_serializers.py @@ -2,7 +2,7 @@ from funkwhale_api.manage import serializers def test_manage_track_file_action_delete(factories): - tfs = factories['music.TrackFile'](size=5) + tfs = factories["music.TrackFile"](size=5) s = serializers.ManageTrackFileActionSerializer(queryset=None) s.handle_delete(tfs.__class__.objects.all()) diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py index db2e0980a..aa8347f76 100644 --- a/api/tests/manage/test_views.py +++ b/api/tests/manage/test_views.py @@ -6,21 +6,22 @@ from funkwhale_api.manage import serializers from funkwhale_api.manage import views -@pytest.mark.parametrize('view,permissions,operator', [ - (views.ManageTrackFileViewSet, ['library'], 'and'), -]) +@pytest.mark.parametrize( + "view,permissions,operator", [(views.ManageTrackFileViewSet, ["library"], "and")] +) def test_permissions(assert_user_permission, view, permissions, operator): assert_user_permission(view, permissions, operator) def test_track_file_view(factories, superuser_api_client): - tfs = factories['music.TrackFile'].create_batch(size=5) - qs = tfs[0].__class__.objects.order_by('-creation_date') - url = reverse('api:v1:manage:library:track-files-list') + tfs = factories["music.TrackFile"].create_batch(size=5) + qs = tfs[0].__class__.objects.order_by("-creation_date") + url = reverse("api:v1:manage:library:track-files-list") - response = superuser_api_client.get(url, {'sort': '-creation_date'}) + response = superuser_api_client.get(url, {"sort": "-creation_date"}) expected = serializers.ManageTrackFileSerializer( - qs, many=True, context={'request': response.wsgi_request}).data + qs, many=True, context={"request": response.wsgi_request} + ).data - assert response.data['count'] == len(tfs) - assert response.data['results'] == expected + assert response.data["count"] == len(tfs) + assert response.data["results"] == expected diff --git a/api/tests/music/conftest.py b/api/tests/music/conftest.py index 4eea8effe..8108b1a49 100644 --- a/api/tests/music/conftest.py +++ b/api/tests/music/conftest.py @@ -1,66 +1,76 @@ import pytest -_artists = {'search': {}, 'get': {}} +_artists = {"search": {}, "get": {}} -_artists['search']['adhesive_wombat'] = { - 'artist-list': [ +_artists["search"]["adhesive_wombat"] = { + "artist-list": [ { - 'type': 'Person', - 'ext:score': '100', - 'id': '62c3befb-6366-4585-b256-809472333801', - 'disambiguation': 'George Shaw', - 'gender': 'male', - 'area': {'sort-name': 'Raleigh', 'id': '3f8828b9-ba93-4604-9b92-1f616fa1abd1', 'name': 'Raleigh'}, - 'sort-name': 'Wombat, Adhesive', - 'life-span': {'ended': 'false'}, - 'name': 'Adhesive Wombat' + "type": "Person", + "ext:score": "100", + "id": "62c3befb-6366-4585-b256-809472333801", + "disambiguation": "George Shaw", + "gender": "male", + "area": { + "sort-name": "Raleigh", + "id": "3f8828b9-ba93-4604-9b92-1f616fa1abd1", + "name": "Raleigh", + }, + "sort-name": "Wombat, Adhesive", + "life-span": {"ended": "false"}, + "name": "Adhesive Wombat", }, { - 'country': 'SE', - 'type': 'Group', - 'ext:score': '42', - 'id': '61b34e69-7573-4208-bc89-7061bca5a8fc', - 'area': {'sort-name': 'Sweden', 'id': '23d10872-f5ae-3f0c-bf55-332788a16ecb', 'name': 'Sweden'}, - 'sort-name': 'Adhesive', - 'life-span': {'end': '2002-07-12', 'begin': '1994', 'ended': 'true'}, - 'name': 'Adhesive', - 'begin-area': { - 'sort-name': 'Katrineholm', - 'id': '02390d96-b5a3-4282-a38f-e64a95d08b7f', - 'name': 'Katrineholm' + "country": "SE", + "type": "Group", + "ext:score": "42", + "id": "61b34e69-7573-4208-bc89-7061bca5a8fc", + "area": { + "sort-name": "Sweden", + "id": "23d10872-f5ae-3f0c-bf55-332788a16ecb", + "name": "Sweden", + }, + "sort-name": "Adhesive", + "life-span": {"end": "2002-07-12", "begin": "1994", "ended": "true"}, + "name": "Adhesive", + "begin-area": { + "sort-name": "Katrineholm", + "id": "02390d96-b5a3-4282-a38f-e64a95d08b7f", + "name": "Katrineholm", }, }, ] } -_artists['get']['adhesive_wombat'] = {'artist': _artists['search']['adhesive_wombat']['artist-list'][0]} +_artists["get"]["adhesive_wombat"] = { + "artist": _artists["search"]["adhesive_wombat"]["artist-list"][0] +} -_artists['get']['soad'] = { - 'artist': { - 'country': 'US', - 'isni-list': ['0000000121055332'], - 'type': 'Group', - 'area': { - 'iso-3166-1-code-list': ['US'], - 'sort-name': 'United States', - 'id': '489ce91b-6658-3307-9877-795b68554c98', - 'name': 'United States' +_artists["get"]["soad"] = { + "artist": { + "country": "US", + "isni-list": ["0000000121055332"], + "type": "Group", + "area": { + "iso-3166-1-code-list": ["US"], + "sort-name": "United States", + "id": "489ce91b-6658-3307-9877-795b68554c98", + "name": "United States", }, - 'begin-area': { - 'sort-name': 'Glendale', - 'id': '6db2e45d-d7f3-43da-ac0b-7ba5ca627373', - 'name': 'Glendale' + "begin-area": { + "sort-name": "Glendale", + "id": "6db2e45d-d7f3-43da-ac0b-7ba5ca627373", + "name": "Glendale", }, - 'id': 'cc0b7089-c08d-4c10-b6b0-873582c17fd6', - 'life-span': {'begin': '1994'}, - 'sort-name': 'System of a Down', - 'name': 'System of a Down' + "id": "cc0b7089-c08d-4c10-b6b0-873582c17fd6", + "life-span": {"begin": "1994"}, + "sort-name": "System of a Down", + "name": "System of a Down", } } -_albums = {'search': {}, 'get': {}, 'get_with_includes': {}} -_albums['search']['hypnotize'] = { - 'release-list': [ +_albums = {"search": {}, "get": {}, "get_with_includes": {}} +_albums["search"]["hypnotize"] = { + "release-list": [ { "artist-credit": [ { @@ -69,22 +79,22 @@ _albums['search']['hypnotize'] = { { "alias": "SoaD", "sort-name": "SoaD", - "type": "Search hint" + "type": "Search hint", }, { "alias": "S.O.A.D.", "sort-name": "S.O.A.D.", - "type": "Search hint" + "type": "Search hint", }, { "alias": "System Of Down", "sort-name": "System Of Down", - "type": "Search hint" - } + "type": "Search hint", + }, ], "id": "cc0b7089-c08d-4c10-b6b0-873582c17fd6", "name": "System of a Down", - "sort-name": "System of a Down" + "sort-name": "System of a Down", } } ], @@ -99,16 +109,16 @@ _albums['search']['hypnotize'] = { "catalog-number": "8-2796-93871-2", "label": { "id": "f5be9cfe-e1af-405c-a074-caeaed6797c0", - "name": "American Recordings" - } + "name": "American Recordings", + }, }, { "catalog-number": "D162990", "label": { "id": "9a7d39a4-a887-40f3-a645-a9a136d1f13f", - "name": "BMG Direct Marketing, Inc." - } - } + "name": "BMG Direct Marketing, Inc.", + }, + }, ], "medium-count": 1, "medium-list": [ @@ -117,7 +127,7 @@ _albums['search']['hypnotize'] = { "disc-list": [], "format": "CD", "track-count": 12, - "track-list": [] + "track-list": [], } ], "medium-track-count": 12, @@ -126,26 +136,21 @@ _albums['search']['hypnotize'] = { { "area": { "id": "489ce91b-6658-3307-9877-795b68554c98", - "iso-3166-1-code-list": [ - "US" - ], + "iso-3166-1-code-list": ["US"], "name": "United States", - "sort-name": "United States" + "sort-name": "United States", }, - "date": "2005" + "date": "2005", } ], "release-group": { "id": "72035143-d6ec-308b-8ee5-070b8703902a", "primary-type": "Album", - "type": "Album" + "type": "Album", }, "status": "Official", - "text-representation": { - "language": "eng", - "script": "Latn" - }, - "title": "Hypnotize" + "text-representation": {"language": "eng", "script": "Latn"}, + "title": "Hypnotize", }, { "artist-credit": [ @@ -155,22 +160,22 @@ _albums['search']['hypnotize'] = { { "alias": "SoaD", "sort-name": "SoaD", - "type": "Search hint" + "type": "Search hint", }, { "alias": "S.O.A.D.", "sort-name": "S.O.A.D.", - "type": "Search hint" + "type": "Search hint", }, { "alias": "System Of Down", "sort-name": "System Of Down", - "type": "Search hint" - } + "type": "Search hint", + }, ], "id": "cc0b7089-c08d-4c10-b6b0-873582c17fd6", "name": "System of a Down", - "sort-name": "System of a Down" + "sort-name": "System of a Down", } } ], @@ -188,7 +193,7 @@ _albums['search']['hypnotize'] = { "disc-list": [], "format": "Vinyl", "track-count": 12, - "track-list": [] + "track-list": [], } ], "medium-track-count": 12, @@ -196,167 +201,233 @@ _albums['search']['hypnotize'] = { { "area": { "id": "489ce91b-6658-3307-9877-795b68554c98", - "iso-3166-1-code-list": [ - "US" - ], + "iso-3166-1-code-list": ["US"], "name": "United States", - "sort-name": "United States" + "sort-name": "United States", }, - "date": "2005-12-20" + "date": "2005-12-20", } ], "release-group": { "id": "72035143-d6ec-308b-8ee5-070b8703902a", "primary-type": "Album", - "type": "Album" + "type": "Album", }, "status": "Official", - "text-representation": { - "language": "eng", - "script": "Latn" - }, - "title": "Hypnotize" + "text-representation": {"language": "eng", "script": "Latn"}, + "title": "Hypnotize", }, ] } -_albums['get']['hypnotize'] = {'release': _albums['search']['hypnotize']['release-list'][0]} -_albums['get_with_includes']['hypnotize'] = { - 'release': { - 'artist-credit': [ - {'artist': {'id': 'cc0b7089-c08d-4c10-b6b0-873582c17fd6', - 'name': 'System of a Down', - 'sort-name': 'System of a Down'}}], - 'artist-credit-phrase': 'System of a Down', - 'barcode': '', - 'country': 'US', - 'cover-art-archive': {'artwork': 'true', - 'back': 'false', - 'count': '1', - 'front': 'true'}, - 'date': '2005', - 'id': '47ae093f-1607-49a3-be11-a15d335ccc94', - 'medium-count': 1, - 'medium-list': [{'format': 'CD', - 'position': '1', - 'track-count': 12, - 'track-list': [{'id': '59f5cf9a-75b2-3aa3-abda-6807a87107b3', - 'length': '186000', - 'number': '1', - 'position': '1', - 'recording': {'id': '76d03fc5-758c-48d0-a354-a67de086cc68', - 'length': '186000', - 'title': 'Attack'}, - 'track_or_recording_length': '186000'}, - {'id': '3aaa28c1-12b1-3c2a-b90a-82e09e355608', - 'length': '239000', - 'number': '2', - 'position': '2', - 'recording': {'id': '327543b0-9193-48c5-83c9-01c7b36c8c0a', - 'length': '239000', - 'title': 'Dreaming'}, - 'track_or_recording_length': '239000'}, - {'id': 'a34fef19-e637-3436-b7eb-276ff2814d6f', - 'length': '147000', - 'number': '3', - 'position': '3', - 'recording': {'id': '6e27866c-07a1-425d-bb4f-9d9e728db344', - 'length': '147000', - 'title': 'Kill Rock ’n Roll'}, - 'track_or_recording_length': '147000'}, - {'id': '72a4e5c0-c150-3ba1-9ceb-3ab82648af25', - 'length': '189000', - 'number': '4', - 'position': '4', - 'recording': {'id': '7ff8a67d-c8e2-4b3a-a045-7ad3561d0605', - 'length': '189000', - 'title': 'Hypnotize'}, - 'track_or_recording_length': '189000'}, - {'id': 'a748fa6e-b3b7-3b22-89fb-a038ec92ac32', - 'length': '178000', - 'number': '5', - 'position': '5', - 'recording': {'id': '19b6eb6a-0e76-4ef7-b63f-959339dbd5d2', - 'length': '178000', - 'title': 'Stealing Society'}, - 'track_or_recording_length': '178000'}, - {'id': '5c5a8d4e-e21a-317e-a719-6e2dbdefa5d2', - 'length': '216000', - 'number': '6', - 'position': '6', - 'recording': {'id': 'c3c2afe1-ee9a-47cb-b3c6-ff8100bc19d5', - 'length': '216000', - 'title': 'Tentative'}, - 'track_or_recording_length': '216000'}, - {'id': '265718ba-787f-3193-947b-3b6fa69ffe96', - 'length': '175000', - 'number': '7', - 'position': '7', - 'recording': {'id': '96f804e1-f600-4faa-95a6-ce597e7db120', - 'length': '175000', - 'title': 'U‐Fig'}, - 'title': 'U-Fig', - 'track_or_recording_length': '175000'}, - {'id': 'cdcf8572-3060-31ca-a72c-1ded81ca1f7a', - 'length': '328000', - 'number': '8', - 'position': '8', - 'recording': {'id': '26ba38f0-b26b-48b7-8e77-226b22a55f79', - 'length': '328000', - 'title': 'Holy Mountains'}, - 'track_or_recording_length': '328000'}, - {'id': 'f9f00cb0-5635-3217-a2a0-bd61917eb0df', - 'length': '171000', - 'number': '9', - 'position': '9', - 'recording': {'id': '039f3379-3a69-4e75-a882-df1c4e1608aa', - 'length': '171000', - 'title': 'Vicinity of Obscenity'}, - 'track_or_recording_length': '171000'}, - {'id': 'cdd45914-6741-353e-bbb5-d281048ff24f', - 'length': '164000', - 'number': '10', - 'position': '10', - 'recording': {'id': 'c24d541a-a9a8-4a22-84c6-5e6419459cf8', - 'length': '164000', - 'title': 'She’s Like Heroin'}, - 'track_or_recording_length': '164000'}, - {'id': 'cfcf12ac-6831-3dd6-a2eb-9d0bfeee3f6d', - 'length': '167000', - 'number': '11', - 'position': '11', - 'recording': {'id': '0aff4799-849f-4f83-84f4-22cabbba2378', - 'length': '167000', - 'title': 'Lonely Day'}, - 'track_or_recording_length': '167000'}, - {'id': '7e38bb38-ff62-3e41-a670-b7d77f578a1f', - 'length': '220000', - 'number': '12', - 'position': '12', - 'recording': {'id': 'e1b4d90f-2f44-4fe6-a826-362d4e3d9b88', - 'length': '220000', - 'title': 'Soldier Side'}, - 'track_or_recording_length': '220000'}]}], - 'packaging': 'Digipak', - 'quality': 'normal', - 'release-event-count': 1, - 'release-event-list': [{'area': {'id': '489ce91b-6658-3307-9877-795b68554c98', - 'iso-3166-1-code-list': ['US'], - 'name': 'United States', - 'sort-name': 'United States'}, - 'date': '2005'}], - 'status': 'Official', - 'text-representation': {'language': 'eng', 'script': 'Latn'}, - 'title': 'Hypnotize'}} +_albums["get"]["hypnotize"] = { + "release": _albums["search"]["hypnotize"]["release-list"][0] +} +_albums["get_with_includes"]["hypnotize"] = { + "release": { + "artist-credit": [ + { + "artist": { + "id": "cc0b7089-c08d-4c10-b6b0-873582c17fd6", + "name": "System of a Down", + "sort-name": "System of a Down", + } + } + ], + "artist-credit-phrase": "System of a Down", + "barcode": "", + "country": "US", + "cover-art-archive": { + "artwork": "true", + "back": "false", + "count": "1", + "front": "true", + }, + "date": "2005", + "id": "47ae093f-1607-49a3-be11-a15d335ccc94", + "medium-count": 1, + "medium-list": [ + { + "format": "CD", + "position": "1", + "track-count": 12, + "track-list": [ + { + "id": "59f5cf9a-75b2-3aa3-abda-6807a87107b3", + "length": "186000", + "number": "1", + "position": "1", + "recording": { + "id": "76d03fc5-758c-48d0-a354-a67de086cc68", + "length": "186000", + "title": "Attack", + }, + "track_or_recording_length": "186000", + }, + { + "id": "3aaa28c1-12b1-3c2a-b90a-82e09e355608", + "length": "239000", + "number": "2", + "position": "2", + "recording": { + "id": "327543b0-9193-48c5-83c9-01c7b36c8c0a", + "length": "239000", + "title": "Dreaming", + }, + "track_or_recording_length": "239000", + }, + { + "id": "a34fef19-e637-3436-b7eb-276ff2814d6f", + "length": "147000", + "number": "3", + "position": "3", + "recording": { + "id": "6e27866c-07a1-425d-bb4f-9d9e728db344", + "length": "147000", + "title": "Kill Rock ’n Roll", + }, + "track_or_recording_length": "147000", + }, + { + "id": "72a4e5c0-c150-3ba1-9ceb-3ab82648af25", + "length": "189000", + "number": "4", + "position": "4", + "recording": { + "id": "7ff8a67d-c8e2-4b3a-a045-7ad3561d0605", + "length": "189000", + "title": "Hypnotize", + }, + "track_or_recording_length": "189000", + }, + { + "id": "a748fa6e-b3b7-3b22-89fb-a038ec92ac32", + "length": "178000", + "number": "5", + "position": "5", + "recording": { + "id": "19b6eb6a-0e76-4ef7-b63f-959339dbd5d2", + "length": "178000", + "title": "Stealing Society", + }, + "track_or_recording_length": "178000", + }, + { + "id": "5c5a8d4e-e21a-317e-a719-6e2dbdefa5d2", + "length": "216000", + "number": "6", + "position": "6", + "recording": { + "id": "c3c2afe1-ee9a-47cb-b3c6-ff8100bc19d5", + "length": "216000", + "title": "Tentative", + }, + "track_or_recording_length": "216000", + }, + { + "id": "265718ba-787f-3193-947b-3b6fa69ffe96", + "length": "175000", + "number": "7", + "position": "7", + "recording": { + "id": "96f804e1-f600-4faa-95a6-ce597e7db120", + "length": "175000", + "title": "U‐Fig", + }, + "title": "U-Fig", + "track_or_recording_length": "175000", + }, + { + "id": "cdcf8572-3060-31ca-a72c-1ded81ca1f7a", + "length": "328000", + "number": "8", + "position": "8", + "recording": { + "id": "26ba38f0-b26b-48b7-8e77-226b22a55f79", + "length": "328000", + "title": "Holy Mountains", + }, + "track_or_recording_length": "328000", + }, + { + "id": "f9f00cb0-5635-3217-a2a0-bd61917eb0df", + "length": "171000", + "number": "9", + "position": "9", + "recording": { + "id": "039f3379-3a69-4e75-a882-df1c4e1608aa", + "length": "171000", + "title": "Vicinity of Obscenity", + }, + "track_or_recording_length": "171000", + }, + { + "id": "cdd45914-6741-353e-bbb5-d281048ff24f", + "length": "164000", + "number": "10", + "position": "10", + "recording": { + "id": "c24d541a-a9a8-4a22-84c6-5e6419459cf8", + "length": "164000", + "title": "She’s Like Heroin", + }, + "track_or_recording_length": "164000", + }, + { + "id": "cfcf12ac-6831-3dd6-a2eb-9d0bfeee3f6d", + "length": "167000", + "number": "11", + "position": "11", + "recording": { + "id": "0aff4799-849f-4f83-84f4-22cabbba2378", + "length": "167000", + "title": "Lonely Day", + }, + "track_or_recording_length": "167000", + }, + { + "id": "7e38bb38-ff62-3e41-a670-b7d77f578a1f", + "length": "220000", + "number": "12", + "position": "12", + "recording": { + "id": "e1b4d90f-2f44-4fe6-a826-362d4e3d9b88", + "length": "220000", + "title": "Soldier Side", + }, + "track_or_recording_length": "220000", + }, + ], + } + ], + "packaging": "Digipak", + "quality": "normal", + "release-event-count": 1, + "release-event-list": [ + { + "area": { + "id": "489ce91b-6658-3307-9877-795b68554c98", + "iso-3166-1-code-list": ["US"], + "name": "United States", + "sort-name": "United States", + }, + "date": "2005", + } + ], + "status": "Official", + "text-representation": {"language": "eng", "script": "Latn"}, + "title": "Hypnotize", + } +} -_albums['get']['marsupial'] = { - 'release': { +_albums["get"]["marsupial"] = { + "release": { "artist-credit": [ { "artist": { "disambiguation": "George Shaw", "id": "62c3befb-6366-4585-b256-809472333801", "name": "Adhesive Wombat", - "sort-name": "Wombat, Adhesive" + "sort-name": "Wombat, Adhesive", } } ], @@ -366,7 +437,7 @@ _albums['get']['marsupial'] = { "artwork": "true", "back": "false", "count": "1", - "front": "true" + "front": "true", }, "date": "2013-06-05", "id": "a50d2a81-2a50-484d-9cb4-b9f6833f583e", @@ -377,28 +448,23 @@ _albums['get']['marsupial'] = { { "area": { "id": "525d4e18-3d00-31b9-a58b-a146a916de8f", - "iso-3166-1-code-list": [ - "XW" - ], + "iso-3166-1-code-list": ["XW"], "name": "[Worldwide]", - "sort-name": "[Worldwide]" + "sort-name": "[Worldwide]", }, - "date": "2013-06-05" + "date": "2013-06-05", } ], "status": "Official", - "text-representation": { - "language": "eng", - "script": "Latn" - }, - "title": "Marsupial Madness" + "text-representation": {"language": "eng", "script": "Latn"}, + "title": "Marsupial Madness", } } -_tracks = {'search': {}, 'get': {}} +_tracks = {"search": {}, "get": {}} -_tracks['search']['8bitadventures'] = { - 'recording-list': [ +_tracks["search"]["8bitadventures"] = { + "recording-list": [ { "artist-credit": [ { @@ -406,7 +472,7 @@ _tracks['search']['8bitadventures'] = { "disambiguation": "George Shaw", "id": "62c3befb-6366-4585-b256-809472333801", "name": "Adhesive Wombat", - "sort-name": "Wombat, Adhesive" + "sort-name": "Wombat, Adhesive", } } ], @@ -430,9 +496,9 @@ _tracks['search']['8bitadventures'] = { "length": "271000", "number": "1", "title": "8-Bit Adventure", - "track_or_recording_length": "271000" + "track_or_recording_length": "271000", } - ] + ], } ], "medium-track-count": 11, @@ -440,70 +506,85 @@ _tracks['search']['8bitadventures'] = { { "area": { "id": "525d4e18-3d00-31b9-a58b-a146a916de8f", - "iso-3166-1-code-list": [ - "XW" - ], + "iso-3166-1-code-list": ["XW"], "name": "[Worldwide]", - "sort-name": "[Worldwide]" + "sort-name": "[Worldwide]", }, - "date": "2013-06-05" + "date": "2013-06-05", } ], "release-group": { "id": "447b4979-2178-405c-bfe6-46bf0b09e6c7", "primary-type": "Album", - "type": "Album" + "type": "Album", }, "status": "Official", - "title": "Marsupial Madness" + "title": "Marsupial Madness", } ], "title": "8-Bit Adventure", "tag-list": [ - { - "count": "2", - "name": "techno" - }, - { - "count": "2", - "name": "good-music" - }, + {"count": "2", "name": "techno"}, + {"count": "2", "name": "good-music"}, ], - }, + } ] } -_tracks['get']['8bitadventures'] = {'recording': _tracks['search']['8bitadventures']['recording-list'][0]} -_tracks['get']['chop_suey'] = { - 'recording': { - 'id': '46c7368a-013a-47b6-97cc-e55e7ab25213', - 'length': '210240', - 'title': 'Chop Suey!', - 'work-relation-list': [{'target': 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5', - 'type': 'performance', - 'type-id': 'a3005666-a872-32c3-ad06-98af558e99b0', - 'work': {'id': 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5', - 'language': 'eng', - 'title': 'Chop Suey!'}}]}} +_tracks["get"]["8bitadventures"] = { + "recording": _tracks["search"]["8bitadventures"]["recording-list"][0] +} +_tracks["get"]["chop_suey"] = { + "recording": { + "id": "46c7368a-013a-47b6-97cc-e55e7ab25213", + "length": "210240", + "title": "Chop Suey!", + "work-relation-list": [ + { + "target": "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5", + "type": "performance", + "type-id": "a3005666-a872-32c3-ad06-98af558e99b0", + "work": { + "id": "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5", + "language": "eng", + "title": "Chop Suey!", + }, + } + ], + } +} -_works = {'search': {}, 'get': {}} -_works['get']['chop_suey'] = {'work': {'id': 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5', - 'language': 'eng', - 'recording-relation-list': [{'direction': 'backward', - 'recording': {'disambiguation': 'edit', - 'id': '07ca77cf-f513-4e9c-b190-d7e24bbad448', - 'length': '170893', - 'title': 'Chop Suey!'}, - 'target': '07ca77cf-f513-4e9c-b190-d7e24bbad448', - 'type': 'performance', - 'type-id': 'a3005666-a872-32c3-ad06-98af558e99b0'}, - ], - 'title': 'Chop Suey!', - 'type': 'Song', - 'url-relation-list': [{'direction': 'backward', - 'target': 'http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!', - 'type': 'lyrics', - 'type-id': 'e38e65aa-75e0-42ba-ace0-072aeb91a538'}]}} +_works = {"search": {}, "get": {}} +_works["get"]["chop_suey"] = { + "work": { + "id": "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5", + "language": "eng", + "recording-relation-list": [ + { + "direction": "backward", + "recording": { + "disambiguation": "edit", + "id": "07ca77cf-f513-4e9c-b190-d7e24bbad448", + "length": "170893", + "title": "Chop Suey!", + }, + "target": "07ca77cf-f513-4e9c-b190-d7e24bbad448", + "type": "performance", + "type-id": "a3005666-a872-32c3-ad06-98af558e99b0", + } + ], + "title": "Chop Suey!", + "type": "Song", + "url-relation-list": [ + { + "direction": "backward", + "target": "http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!", + "type": "lyrics", + "type-id": "e38e65aa-75e0-42ba-ace0-072aeb91a538", + } + ], + } +} @pytest.fixture() @@ -570,4 +651,4 @@ def binary_cover(): """ Return an album cover image in form of a binary string """ - return b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x02\x01\x00H\x00H\x00\x00\xff\xed\x08\xaePhotoshop 3.0\x008BIM\x03\xe9\x00\x00\x00\x00\x00x\x00\x03\x00\x00\x00H\x00H\x00\x00\x00\x00\x02\xd8\x02(\xff\xe1\xff\xe2\x02\xf9\x02F\x03G\x05(\x03\xfc\x00\x02\x00\x00\x00H\x00H\x00\x00\x00\x00\x02\xd8\x02(\x00\x01\x00\x00\x00d\x00\x00\x00\x01\x00\x03\x03\x03\x00\x00\x00\x01\'\x0f\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00`\x08\x00\x19\x01\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x008BIM\x03\xed\x00\x00\x00\x00\x00\x10\x00H\x00\x00\x00\x01\x00\x01\x00H\x00\x00\x00\x01\x00\x018BIM\x03\xf3\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x008BIM\x04\n\x00\x00\x00\x00\x00\x01\x00\x008BIM\'\x10\x00\x00\x00\x00\x00\n\x00\x01\x00\x00\x00\x00\x00\x00\x00\x028BIM\x03\xf5\x00\x00\x00\x00\x00H\x00/ff\x00\x01\x00lff\x00\x06\x00\x00\x00\x00\x00\x01\x00/ff\x00\x01\x00\xa1\x99\x9a\x00\x06\x00\x00\x00\x00\x00\x01\x002\x00\x00\x00\x01\x00Z\x00\x00\x00\x06\x00\x00\x00\x00\x00\x01\x005\x00\x00\x00\x01\x00-\x00\x00\x00\x06\x00\x00\x00\x00\x00\x018BIM\x03\xf8\x00\x00\x00\x00\x00p\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x03\xe8\x00\x008BIM\x04\x00\x00\x00\x00\x00\x00\x02\x00\x018BIM\x04\x02\x00\x00\x00\x00\x00\x04\x00\x00\x00\x008BIM\x04\x08\x00\x00\x00\x00\x00\x10\x00\x00\x00\x01\x00\x00\x02@\x00\x00\x02@\x00\x00\x00\x008BIM\x04\t\x00\x00\x00\x00\x06\x9b\x00\x00\x00\x01\x00\x00\x00\x80\x00\x00\x00\x80\x00\x00\x01\x80\x00\x00\xc0\x00\x00\x00\x06\x7f\x00\x18\x00\x01\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x02\x01\x00H\x00H\x00\x00\xff\xfe\x00\'File written by Adobe Photoshop\xa8 4.0\x00\xff\xee\x00\x0eAdobe\x00d\x80\x00\x00\x00\x01\xff\xdb\x00\x84\x00\x0c\x08\x08\x08\t\x08\x0c\t\t\x0c\x11\x0b\n\x0b\x11\x15\x0f\x0c\x0c\x0f\x15\x18\x13\x13\x15\x13\x13\x18\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x01\r\x0b\x0b\r\x0e\r\x10\x0e\x0e\x10\x14\x0e\x0e\x0e\x14\x14\x0e\x0e\x0e\x0e\x14\x11\x0c\x0c\x0c\x0c\x0c\x11\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\xff\xc0\x00\x11\x08\x00\x80\x00\x80\x03\x01"\x00\x02\x11\x01\x03\x11\x01\xff\xdd\x00\x04\x00\x08\xff\xc4\x01?\x00\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x03\x00\x01\x02\x04\x05\x06\x07\x08\t\n\x0b\x01\x00\x01\x05\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x10\x00\x01\x04\x01\x03\x02\x04\x02\x05\x07\x06\x08\x05\x03\x0c3\x01\x00\x02\x11\x03\x04!\x121\x05AQa\x13"q\x812\x06\x14\x91\xa1\xb1B#$\x15R\xc1b34r\x82\xd1C\x07%\x92S\xf0\xe1\xf1cs5\x16\xa2\xb2\x83&D\x93TdE\xc2\xa3t6\x17\xd2U\xe2e\xf2\xb3\x84\xc3\xd3u\xe3\xf3F\'\x94\xa4\x85\xb4\x95\xc4\xd4\xe4\xf4\xa5\xb5\xc5\xd5\xe5\xf5Vfv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf67GWgw\x87\x97\xa7\xb7\xc7\xd7\xe7\xf7\x11\x00\x02\x02\x01\x02\x04\x04\x03\x04\x05\x06\x07\x07\x06\x055\x01\x00\x02\x11\x03!1\x12\x04AQaq"\x13\x052\x81\x91\x14\xa1\xb1B#\xc1R\xd1\xf03$b\xe1r\x82\x92CS\x15cs4\xf1%\x06\x16\xa2\xb2\x83\x07&5\xc2\xd2D\x93T\xa3\x17dEU6te\xe2\xf2\xb3\x84\xc3\xd3u\xe3\xf3F\x94\xa4\x85\xb4\x95\xc4\xd4\xe4\xf4\xa5\xb5\xc5\xd5\xe5\xf5Vfv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf6\'7GWgw\x87\x97\xa7\xb7\xc7\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\xf5T\x92I%)$\x92IJI$\x92R\x92I$\x94\xa4\x92I%)$\x92IJI$\x92R\x92I$\x94\xff\x00\xff\xd0\xf5T\x92I%)$\x92IJI%\xe7\xff\x00Z\x7f\xc6\xbf\xfc\xde\xeb\xb9]\x1f\xf6_\xda~\xcd\xe9\xfe\x9b\xed\x1e\x9e\xefR\xba\xef\xfeo\xec\xf6\xed\xdb\xea\xec\xfeq%>\x80\x92\xf2\xaf\xfc}?\xf3I\xff\x00\xb3_\xfb\xe8\x97\xfe>\x9f\xf9\xa4\xff\x00\xd9\xaf\xfd\xf4IO\xaa\xa4\xbc\xab\xff\x00\x1fO\xfc\xd2\x7f\xec\xd7\xfe\xfa%\xff\x00\x8f\xa7\xfei?\xf6k\xff\x00}\x12S\xea\xa9.+\xeaW\xf8\xc8\xff\x00\x9d}V\xde\x9d\xfb;\xec~\x96;\xb2=O[\xd5\x9d\xaf\xaa\xad\x9b=\n\x7f\xd3}-\xeb\xb5IJI$\x92R\x92I$\x94\xff\x00\xff\xd1\xf5T\x92I%)$\x97\x9f\xff\x00\x8d\x7f\xad=w\xea\xf7\xec\xbf\xd8\xf9_f\xfbO\xda=o\xd1\xd7f\xefO\xec\xfe\x9f\xf3\xf5\xdb\xb7o\xabg\xd0IO\xa0/\x9f\xff\x00\xc6\x97\xfe.\xfa\x9f\xfdc\xff\x00m\xf1\xd2\xff\x00\xc7K\xeb\xdf\xfeY\xff\x00\xe0\x18\xff\x00\xfb\xce\xb9\xfe\xa9\xd53\xfa\xbe}\xbdG\xa8\xdb\xeb\xe5\xdf\xb7\xd4\xb3kY;\x1a\xda\x99\xec\xa9\xac\xaf\xf9\xb63\xf3\x12SU$\x92IJI$\x92S\xdf\xff\x00\x89O\xfcUe\x7f\xe1\x0b?\xf3\xf6*\xf6\xb5\xf3/D\xeb\xfd[\xa0\xe5?3\xa4\xdf\xf6l\x8b+59\xfb\x18\xf9a-\xb1\xcd\xdb{-g\xd3\xa9\x8bk\xff\x00\x1d/\xaf\x7f\xf9g\xff\x00\x80c\xff\x00\xef:J~\x80Iq\xff\x00\xe2\xbf\xaf\xf5n\xbd\xd023:\xb5\xff\x00i\xc8\xaf-\xf55\xfb\x18\xc8`\xae\x8b\x1a\xdd\xb42\xa6};^\xbb\x04\x94\xa4\x92I%?\xff\xd2\xf5T\x92I%)yW\xf8\xf4\xff\x00\xbcO\xfd\n\xff\x00\xddE\xea\xab\xca\xbf\xc7\xa7\xfd\xe2\x7f\xe8W\xfe\xea$\xa7\xca\x92I$\x94\xa4\x92I%)$\x92IJI$\x92S\xed_\xe2S\xff\x00\x12\xb9_\xf8~\xcf\xfc\xf3\x8a\xbd\x01y\xff\x00\xf8\x94\xff\x00\xc4\xaeW\xfe\x1f\xb3\xff\x00<\xe2\xaf@IJI$\x92S\xff\xd3\xf5T\x92I%)yW\xf8\xf4\xff\x00\xbcO\xfd\n\xff\x00\xddE\xea\xab\xca\xbf\xc7\xa7\xfd\xe2\x7f\xe8W\xfe\xea$\xa7\xca\x92I$\x94\xa4\x92I%)$\x92IJI$\x92S\xed_\xe2S\xff\x00\x12\xb9_\xf8~\xcf\xfc\xf3\x8a\xbd\x01y\xff\x00\xf8\x94\xff\x00\xc4\xaeW\xfe\x1f\xb3\xff\x00<\xe2\xaf@IJI$\x92S\xff\xd4\xf5T\x92I%)q_\xe3#\xeaWU\xfa\xd7\xfb;\xf6u\xb8\xf5}\x8f\xd6\xf5>\xd0\xe7\xb6}_Cf\xcfJ\xab\xbf\xd0\xbfr\xedRIO\x8a\x7f\xe3)\xf5\xab\xfe\xe5`\x7f\xdb\x97\x7f\xef*\xe4:\xff\x00D\xca\xe8=Z\xfe\x93\x98\xfa\xec\xc8\xc6\xd9\xbd\xd5\x12Xw\xb1\x97\xb7k\xacmO\xfa\x16\xfe\xe2\xfai|\xff\x00\xfe4\xbf\xf1w\xd4\xff\x00\xeb\x1f\xfbo\x8e\x92\x9eU$\x92IJI$\x92S\xb1\xf5_\xea\xbfP\xfa\xd1\xd4,\xc0\xc0\xb2\x9a\xad\xaa\x93{\x9dys[\xb5\xae\xae\xa2\x01\xaa\xbb\x9d\xbfu\xcd\xfc\xd5\xd3\xff\x00\xe3)\xf5\xab\xfe\xe5`\x7f\xdb\x97\x7f\xef*_\xe2S\xff\x00\x15Y_\xf8B\xcf\xfc\xfd\x8a\xbd\xad%<\xbf\xf8\xbc\xfa\xaf\xd4>\xab\xf4[\xb03\xec\xa6\xdbm\xc9u\xedu\x05\xcen\xd7WM@\x13mt\xbb~\xea]\xf9\xab\xa8I$\x94\xa4\x92I%?\xff\xd5\xf5T\x92I%)$\x92IJ\\\x7f_\xff\x00\x15\xfd\x03\xafuk\xfa\xb6fF]y\x19;7\xb6\xa7\xd6\x1861\x947kl\xa2\xd7\xfd\n\xbf}v\t$\xa7\xcf\xff\x00\xf1\x94\xfa\xab\xff\x00r\xb3\xff\x00\xed\xca\x7f\xf7\x95/\xfce>\xaa\xff\x00\xdc\xac\xff\x00\xfbr\x9f\xfd\xe5^\x80\x92J|\xff\x00\xff\x00\x19O\xaa\xbf\xf7+?\xfe\xdc\xa7\xff\x00yR\xff\x00\xc6S\xea\xaf\xfd\xca\xcf\xff\x00\xb7)\xff\x00\xdeU\xe8\t$\xa7\x97\xfa\xaf\xfe/:/\xd5~\xa1f~\x05\xd96\xdbm&\x876\xf7V\xe6\xeds\xab\xb4\x90*\xa6\x97o\xddK\x7f9u\t$\x92\x94\x92I$\xa5$\x92I)\xff\xd6\xf5T\x92I%)$\x92IJI$\x92R\x92I$\x94\xa4\x92I%)$\x92IJI$\x92R\x92I$\x94\xff\x00\xff\xd9\x008BIM\x04\x06\x00\x00\x00\x00\x00\x07\x00\x03\x00\x00\x00\x01\x01\x00\xff\xfe\x00\'File written by Adobe Photoshop\xa8 4.0\x00\xff\xee\x00\x0eAdobe\x00d\x00\x00\x00\x00\x01\xff\xdb\x00\x84\x00\n\x07\x07\x07\x08\x07\n\x08\x08\n\x0f\n\x08\n\x0f\x12\r\n\n\r\x12\x14\x10\x10\x12\x10\x10\x14\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x01\x0b\x0c\x0c\x15\x13\x15"\x18\x18"\x14\x0e\x0e\x0e\x14\x14\x0e\x0e\x0e\x0e\x14\x11\x0c\x0c\x0c\x0c\x0c\x11\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\xff\xc0\x00\x11\x08\x00\t\x00\t\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xdd\x00\x04\x00\x02\xff\xc4\x01\xa2\x00\x00\x00\x07\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x03\x02\x06\x01\x00\x07\x08\t\n\x0b\x01\x00\x02\x02\x03\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x10\x00\x02\x01\x03\x03\x02\x04\x02\x06\x07\x03\x04\x02\x06\x02s\x01\x02\x03\x11\x04\x00\x05!\x121AQ\x06\x13a"q\x81\x142\x91\xa1\x07\x15\xb1B#\xc1R\xd1\xe13\x16b\xf0$r\x82\xf1%C4S\x92\xa2\xb2cs\xc25D\'\x93\xa3\xb36\x17Tdt\xc3\xd2\xe2\x08&\x83\t\n\x18\x19\x84\x94EF\xa4\xb4V\xd3U(\x1a\xf2\xe3\xf3\xc4\xd4\xe4\xf4eu\x85\x95\xa5\xb5\xc5\xd5\xe5\xf5fv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf67GWgw\x87\x97\xa7\xb7\xc7\xd7\xe7\xf78HXhx\x88\x98\xa8\xb8\xc8\xd8\xe8\xf8)9IYiy\x89\x99\xa9\xb9\xc9\xd9\xe9\xf9*:JZjz\x8a\x9a\xaa\xba\xca\xda\xea\xfa\x11\x00\x02\x02\x01\x02\x03\x05\x05\x04\x05\x06\x04\x08\x03\x03m\x01\x00\x02\x11\x03\x04!\x121A\x05Q\x13a"\x06q\x81\x912\xa1\xb1\xf0\x14\xc1\xd1\xe1#B\x15Rbr\xf13$4C\x82\x16\x92S%\xa2c\xb2\xc2\x07s\xd25\xe2D\x83\x17T\x93\x08\t\n\x18\x19&6E\x1a\'dtU7\xf2\xa3\xb3\xc3()\xd3\xe3\xf3\x84\x94\xa4\xb4\xc4\xd4\xe4\xf4eu\x85\x95\xa5\xb5\xc5\xd5\xe5\xf5FVfv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf6GWgw\x87\x97\xa7\xb7\xc7\xd7\xe7\xf78HXhx\x88\x98\xa8\xb8\xc8\xd8\xe8\xf89IYiy\x89\x99\xa9\xb9\xc9\xd9\xe9\xf9*:JZjz\x8a\x9a\xaa\xba\xca\xda\xea\xfa\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\x91\xea\xfa\xbf\xe6D_\x99\x16\x96\x16\x16\x8c\xdeWf\x84;\x88U\xa1hY\x7f\xd3\'\x9e\xf3\xedCq\x0bz\xfe\x94^\xbc?\xdc\xdb\xff\x00\xa3\xcd\xeb\x7f\xa4\xaa\xf4\x80\x92\xf2\xaf\xfc}?\xf3I\xff\x00\xb3_\xfb\xe8\x97\xfe>\x9f\xf9\xa4\xff\x00\xd9\xaf\xfd\xf4IO\xaa\xa4\xbc\xab\xff\x00\x1fO\xfc\xd2\x7f\xec\xd7\xfe\xfa%\xff\x00\x8f\xa7\xfei?\xf6k\xff\x00}\x12S\xea\xa9.+\xeaW\xf8\xc8\xff\x00\x9d}V\xde\x9d\xfb;\xec~\x96;\xb2=O[\xd5\x9d\xaf\xaa\xad\x9b=\n\x7f\xd3}-\xeb\xb5IJI$\x92R\x92I$\x94\xff\x00\xff\xd1\xf5T\x92I%)$\x97\x9f\xff\x00\x8d\x7f\xad=w\xea\xf7\xec\xbf\xd8\xf9_f\xfbO\xda=o\xd1\xd7f\xefO\xec\xfe\x9f\xf3\xf5\xdb\xb7o\xabg\xd0IO\xa0/\x9f\xff\x00\xc6\x97\xfe.\xfa\x9f\xfdc\xff\x00m\xf1\xd2\xff\x00\xc7K\xeb\xdf\xfeY\xff\x00\xe0\x18\xff\x00\xfb\xce\xb9\xfe\xa9\xd53\xfa\xbe}\xbdG\xa8\xdb\xeb\xe5\xdf\xb7\xd4\xb3kY;\x1a\xda\x99\xec\xa9\xac\xaf\xf9\xb63\xf3\x12SU$\x92IJI$\x92S\xdf\xff\x00\x89O\xfcUe\x7f\xe1\x0b?\xf3\xf6*\xf6\xb5\xf3/D\xeb\xfd[\xa0\xe5?3\xa4\xdf\xf6l\x8b+59\xfb\x18\xf9a-\xb1\xcd\xdb{-g\xd3\xa9\x8bk\xff\x00\x1d/\xaf\x7f\xf9g\xff\x00\x80c\xff\x00\xef:J~\x80Iq\xff\x00\xe2\xbf\xaf\xf5n\xbd\xd023:\xb5\xff\x00i\xc8\xaf-\xf55\xfb\x18\xc8`\xae\x8b\x1a\xdd\xb42\xa6};^\xbb\x04\x94\xa4\x92I%?\xff\xd2\xf5T\x92I%)yW\xf8\xf4\xff\x00\xbcO\xfd\n\xff\x00\xddE\xea\xab\xca\xbf\xc7\xa7\xfd\xe2\x7f\xe8W\xfe\xea$\xa7\xca\x92I$\x94\xa4\x92I%)$\x92IJI$\x92S\xed_\xe2S\xff\x00\x12\xb9_\xf8~\xcf\xfc\xf3\x8a\xbd\x01y\xff\x00\xf8\x94\xff\x00\xc4\xaeW\xfe\x1f\xb3\xff\x00<\xe2\xaf@IJI$\x92S\xff\xd3\xf5T\x92I%)yW\xf8\xf4\xff\x00\xbcO\xfd\n\xff\x00\xddE\xea\xab\xca\xbf\xc7\xa7\xfd\xe2\x7f\xe8W\xfe\xea$\xa7\xca\x92I$\x94\xa4\x92I%)$\x92IJI$\x92S\xed_\xe2S\xff\x00\x12\xb9_\xf8~\xcf\xfc\xf3\x8a\xbd\x01y\xff\x00\xf8\x94\xff\x00\xc4\xaeW\xfe\x1f\xb3\xff\x00<\xe2\xaf@IJI$\x92S\xff\xd4\xf5T\x92I%)q_\xe3#\xeaWU\xfa\xd7\xfb;\xf6u\xb8\xf5}\x8f\xd6\xf5>\xd0\xe7\xb6}_Cf\xcfJ\xab\xbf\xd0\xbfr\xedRIO\x8a\x7f\xe3)\xf5\xab\xfe\xe5`\x7f\xdb\x97\x7f\xef*\xe4:\xff\x00D\xca\xe8=Z\xfe\x93\x98\xfa\xec\xc8\xc6\xd9\xbd\xd5\x12Xw\xb1\x97\xb7k\xacmO\xfa\x16\xfe\xe2\xfai|\xff\x00\xfe4\xbf\xf1w\xd4\xff\x00\xeb\x1f\xfbo\x8e\x92\x9eU$\x92IJI$\x92S\xb1\xf5_\xea\xbfP\xfa\xd1\xd4,\xc0\xc0\xb2\x9a\xad\xaa\x93{\x9dys[\xb5\xae\xae\xa2\x01\xaa\xbb\x9d\xbfu\xcd\xfc\xd5\xd3\xff\x00\xe3)\xf5\xab\xfe\xe5`\x7f\xdb\x97\x7f\xef*_\xe2S\xff\x00\x15Y_\xf8B\xcf\xfc\xfd\x8a\xbd\xad%<\xbf\xf8\xbc\xfa\xaf\xd4>\xab\xf4[\xb03\xec\xa6\xdbm\xc9u\xedu\x05\xcen\xd7WM@\x13mt\xbb~\xea]\xf9\xab\xa8I$\x94\xa4\x92I%?\xff\xd5\xf5T\x92I%)$\x92IJ\\\x7f_\xff\x00\x15\xfd\x03\xafuk\xfa\xb6fF]y\x19;7\xb6\xa7\xd6\x1861\x947kl\xa2\xd7\xfd\n\xbf}v\t$\xa7\xcf\xff\x00\xf1\x94\xfa\xab\xff\x00r\xb3\xff\x00\xed\xca\x7f\xf7\x95/\xfce>\xaa\xff\x00\xdc\xac\xff\x00\xfbr\x9f\xfd\xe5^\x80\x92J|\xff\x00\xff\x00\x19O\xaa\xbf\xf7+?\xfe\xdc\xa7\xff\x00yR\xff\x00\xc6S\xea\xaf\xfd\xca\xcf\xff\x00\xb7)\xff\x00\xdeU\xe8\t$\xa7\x97\xfa\xaf\xfe/:/\xd5~\xa1f~\x05\xd96\xdbm&\x876\xf7V\xe6\xeds\xab\xb4\x90*\xa6\x97o\xddK\x7f9u\t$\x92\x94\x92I$\xa5$\x92I)\xff\xd6\xf5T\x92I%)$\x92IJI$\x92R\x92I$\x94\xa4\x92I%)$\x92IJI$\x92R\x92I$\x94\xff\x00\xff\xd9\x008BIM\x04\x06\x00\x00\x00\x00\x00\x07\x00\x03\x00\x00\x00\x01\x01\x00\xff\xfe\x00'File written by Adobe Photoshop\xa8 4.0\x00\xff\xee\x00\x0eAdobe\x00d\x00\x00\x00\x00\x01\xff\xdb\x00\x84\x00\n\x07\x07\x07\x08\x07\n\x08\x08\n\x0f\n\x08\n\x0f\x12\r\n\n\r\x12\x14\x10\x10\x12\x10\x10\x14\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x01\x0b\x0c\x0c\x15\x13\x15\"\x18\x18\"\x14\x0e\x0e\x0e\x14\x14\x0e\x0e\x0e\x0e\x14\x11\x0c\x0c\x0c\x0c\x0c\x11\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\xff\xc0\x00\x11\x08\x00\t\x00\t\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xdd\x00\x04\x00\x02\xff\xc4\x01\xa2\x00\x00\x00\x07\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x03\x02\x06\x01\x00\x07\x08\t\n\x0b\x01\x00\x02\x02\x03\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x10\x00\x02\x01\x03\x03\x02\x04\x02\x06\x07\x03\x04\x02\x06\x02s\x01\x02\x03\x11\x04\x00\x05!\x121AQ\x06\x13a\"q\x81\x142\x91\xa1\x07\x15\xb1B#\xc1R\xd1\xe13\x16b\xf0$r\x82\xf1%C4S\x92\xa2\xb2cs\xc25D'\x93\xa3\xb36\x17Tdt\xc3\xd2\xe2\x08&\x83\t\n\x18\x19\x84\x94EF\xa4\xb4V\xd3U(\x1a\xf2\xe3\xf3\xc4\xd4\xe4\xf4eu\x85\x95\xa5\xb5\xc5\xd5\xe5\xf5fv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf67GWgw\x87\x97\xa7\xb7\xc7\xd7\xe7\xf78HXhx\x88\x98\xa8\xb8\xc8\xd8\xe8\xf8)9IYiy\x89\x99\xa9\xb9\xc9\xd9\xe9\xf9*:JZjz\x8a\x9a\xaa\xba\xca\xda\xea\xfa\x11\x00\x02\x02\x01\x02\x03\x05\x05\x04\x05\x06\x04\x08\x03\x03m\x01\x00\x02\x11\x03\x04!\x121A\x05Q\x13a\"\x06q\x81\x912\xa1\xb1\xf0\x14\xc1\xd1\xe1#B\x15Rbr\xf13$4C\x82\x16\x92S%\xa2c\xb2\xc2\x07s\xd25\xe2D\x83\x17T\x93\x08\t\n\x18\x19&6E\x1a'dtU7\xf2\xa3\xb3\xc3()\xd3\xe3\xf3\x84\x94\xa4\xb4\xc4\xd4\xe4\xf4eu\x85\x95\xa5\xb5\xc5\xd5\xe5\xf5FVfv\x86\x96\xa6\xb6\xc6\xd6\xe6\xf6GWgw\x87\x97\xa7\xb7\xc7\xd7\xe7\xf78HXhx\x88\x98\xa8\xb8\xc8\xd8\xe8\xf89IYiy\x89\x99\xa9\xb9\xc9\xd9\xe9\xf9*:JZjz\x8a\x9a\xaa\xba\xca\xda\xea\xfa\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00\x91\xea\xfa\xbf\xe6D_\x99\x16\x96\x16\x16\x8c\xdeWf\x84;\x88U\xa1hY\x7f\xd3'\x9e\xf3\xedCq\x0bz\xfe\x94^\xbc?\xdc\xdb\xff\x00\xa3\xcd\xeb\x7f\xa4\xaa\xf4 0 - assert type(cover_data['content']) == bytes - assert type(cover_data['description']) == str + cover_data = data.get_picture("cover_front") + assert cover_data["mimetype"].startswith("image/") + assert len(cover_data["content"]) > 0 + assert type(cover_data["content"]) == bytes + assert type(cover_data["description"]) == str -@pytest.mark.parametrize('field,value', [ - ('title', '999,999'), - ('artist', 'Nine Inch Nails'), - ('album', 'The Slip'), - ('date', datetime.date(2008, 5, 5)), - ('track_number', 1), - ('musicbrainz_albumid', uuid.UUID('12b57d46-a192-499e-a91f-7da66790a1c1')), - ('musicbrainz_recordingid', uuid.UUID('30f3f33e-8d0c-4e69-8539-cbd701d18f28')), - ('musicbrainz_artistid', uuid.UUID('b7ffd2af-418f-4be2-bdd1-22f8b48613da')), -]) +@pytest.mark.parametrize( + "field,value", + [ + ("title", "999,999"), + ("artist", "Nine Inch Nails"), + ("album", "The Slip"), + ("date", datetime.date(2008, 5, 5)), + ("track_number", 1), + ("musicbrainz_albumid", uuid.UUID("12b57d46-a192-499e-a91f-7da66790a1c1")), + ("musicbrainz_recordingid", uuid.UUID("30f3f33e-8d0c-4e69-8539-cbd701d18f28")), + ("musicbrainz_artistid", uuid.UUID("b7ffd2af-418f-4be2-bdd1-22f8b48613da")), + ], +) def test_can_get_metadata_from_flac_file(field, value): - path = os.path.join(DATA_DIR, 'sample.flac') + path = os.path.join(DATA_DIR, "sample.flac") data = metadata.Metadata(path) assert data.get(field) == value def test_can_get_metadata_from_flac_file_not_crash_if_empty(): - path = os.path.join(DATA_DIR, 'sample.flac') + path = os.path.join(DATA_DIR, "sample.flac") data = metadata.Metadata(path) with pytest.raises(metadata.TagNotFound): - data.get('test') + data.get("test") -@pytest.mark.parametrize('field_name', [ - 'musicbrainz_artistid', - 'musicbrainz_albumid', - 'musicbrainz_recordingid', -]) +@pytest.mark.parametrize( + "field_name", + ["musicbrainz_artistid", "musicbrainz_albumid", "musicbrainz_recordingid"], +) def test_mbid_clean_keeps_only_first(field_name): u1 = str(uuid.uuid4()) u2 = str(uuid.uuid4()) field = metadata.VALIDATION[field_name] - result = field.to_python('/'.join([u1, u2])) + result = field.to_python("/".join([u1, u2])) assert str(result) == u1 diff --git a/api/tests/music/test_models.py b/api/tests/music/test_models.py index 0ef54eb66..f81dd433f 100644 --- a/api/tests/music/test_models.py +++ b/api/tests/music/test_models.py @@ -9,7 +9,7 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__)) def test_can_store_release_group_id_on_album(factories): - album = factories['music.Album']() + album = factories["music.Album"]() assert album.release_group_id is not None @@ -21,7 +21,7 @@ def test_import_album_stores_release_group(factories): "disambiguation": "George Shaw", "id": "62c3befb-6366-4585-b256-809472333801", "name": "Adhesive Wombat", - "sort-name": "Wombat, Adhesive" + "sort-name": "Wombat, Adhesive", } } ], @@ -31,137 +31,134 @@ def test_import_album_stores_release_group(factories): "id": "a50d2a81-2a50-484d-9cb4-b9f6833f583e", "status": "Official", "title": "Marsupial Madness", - 'release-group': {'id': '447b4979-2178-405c-bfe6-46bf0b09e6c7'} + "release-group": {"id": "447b4979-2178-405c-bfe6-46bf0b09e6c7"}, } - artist = factories['music.Artist']( - mbid=album_data['artist-credit'][0]['artist']['id'] + artist = factories["music.Artist"]( + mbid=album_data["artist-credit"][0]["artist"]["id"] ) cleaned_data = models.Album.clean_musicbrainz_data(album_data) album = importers.load(models.Album, cleaned_data, album_data, import_hooks=[]) - assert album.release_group_id == album_data['release-group']['id'] + assert album.release_group_id == album_data["release-group"]["id"] assert album.artist == artist def test_import_track_from_release(factories, mocker): - album = factories['music.Album']( - mbid='430347cb-0879-3113-9fde-c75b658c298e') + album = factories["music.Album"](mbid="430347cb-0879-3113-9fde-c75b658c298e") album_data = { - 'release': { - 'id': album.mbid, - 'title': 'Daydream Nation', - 'status': 'Official', - 'medium-count': 1, - 'medium-list': [ + "release": { + "id": album.mbid, + "title": "Daydream Nation", + "status": "Official", + "medium-count": 1, + "medium-list": [ { - 'position': '1', - 'format': 'CD', - 'track-list': [ + "position": "1", + "format": "CD", + "track-list": [ { - 'id': '03baca8b-855a-3c05-8f3d-d3235287d84d', - 'position': '4', - 'number': '4', - 'length': '417973', - 'recording': { - 'id': '2109e376-132b-40ad-b993-2bb6812e19d4', - 'title': 'Teen Age Riot', - 'length': '417973'}, - 'track_or_recording_length': '417973' + "id": "03baca8b-855a-3c05-8f3d-d3235287d84d", + "position": "4", + "number": "4", + "length": "417973", + "recording": { + "id": "2109e376-132b-40ad-b993-2bb6812e19d4", + "title": "Teen Age Riot", + "length": "417973", + }, + "track_or_recording_length": "417973", } ], - 'track-count': 1 + "track-count": 1, } ], } } mocked_get = mocker.patch( - 'funkwhale_api.musicbrainz.api.releases.get', - return_value=album_data) - track_data = album_data['release']['medium-list'][0]['track-list'][0] + "funkwhale_api.musicbrainz.api.releases.get", return_value=album_data + ) + track_data = album_data["release"]["medium-list"][0]["track-list"][0] track = models.Track.get_or_create_from_release( - '430347cb-0879-3113-9fde-c75b658c298e', - track_data['recording']['id'], + "430347cb-0879-3113-9fde-c75b658c298e", track_data["recording"]["id"] )[0] - mocked_get.assert_called_once_with( - album.mbid, includes=models.Album.api_includes) - assert track.title == track_data['recording']['title'] - assert track.mbid == track_data['recording']['id'] + mocked_get.assert_called_once_with(album.mbid, includes=models.Album.api_includes) + assert track.title == track_data["recording"]["title"] + assert track.mbid == track_data["recording"]["id"] assert track.album == album assert track.artist == album.artist - assert track.position == int(track_data['position']) + assert track.position == int(track_data["position"]) + def test_import_job_is_bound_to_track_file(factories, mocker): - track = factories['music.Track']() - job = factories['music.ImportJob'](mbid=track.mbid) + track = factories["music.Track"]() + job = factories["music.ImportJob"](mbid=track.mbid) - mocker.patch('funkwhale_api.music.models.TrackFile.download_file') + mocker.patch("funkwhale_api.music.models.TrackFile.download_file") tasks.import_job_run(import_job_id=job.pk) job.refresh_from_db() assert job.track_file.track == track -@pytest.mark.parametrize('status', ['pending', 'errored', 'finished']) -def test_saving_job_updates_batch_status(status,factories, mocker): - batch = factories['music.ImportBatch']() +@pytest.mark.parametrize("status", ["pending", "errored", "finished"]) +def test_saving_job_updates_batch_status(status, factories, mocker): + batch = factories["music.ImportBatch"]() - assert batch.status == 'pending' + assert batch.status == "pending" - job = factories['music.ImportJob'](batch=batch, status=status) + job = factories["music.ImportJob"](batch=batch, status=status) batch.refresh_from_db() assert batch.status == status -@pytest.mark.parametrize('extention,mimetype', [ - ('ogg', 'audio/ogg'), - ('mp3', 'audio/mpeg'), -]) +@pytest.mark.parametrize( + "extention,mimetype", [("ogg", "audio/ogg"), ("mp3", "audio/mpeg")] +) def test_audio_track_mime_type(extention, mimetype, factories): - name = '.'.join(['test', extention]) + name = ".".join(["test", extention]) path = os.path.join(DATA_DIR, name) - tf = factories['music.TrackFile'](audio_file__from_path=path) + tf = factories["music.TrackFile"](audio_file__from_path=path) assert tf.mimetype == mimetype def test_track_file_file_name(factories): - name = 'test.mp3' + name = "test.mp3" path = os.path.join(DATA_DIR, name) - tf = factories['music.TrackFile'](audio_file__from_path=path) + tf = factories["music.TrackFile"](audio_file__from_path=path) - assert tf.filename == tf.track.full_name + '.mp3' + assert tf.filename == tf.track.full_name + ".mp3" def test_track_get_file_size(factories): - name = 'test.mp3' + name = "test.mp3" path = os.path.join(DATA_DIR, name) - tf = factories['music.TrackFile'](audio_file__from_path=path) + tf = factories["music.TrackFile"](audio_file__from_path=path) assert tf.get_file_size() == 297745 def test_track_get_file_size_federation(factories): - tf = factories['music.TrackFile']( - federation=True, - library_track__with_audio_file=True) + tf = factories["music.TrackFile"]( + federation=True, library_track__with_audio_file=True + ) assert tf.get_file_size() == tf.library_track.audio_file.size def test_track_get_file_size_in_place(factories): - name = 'test.mp3' + name = "test.mp3" path = os.path.join(DATA_DIR, name) - tf = factories['music.TrackFile']( - in_place=True, source='file://{}'.format(path)) + tf = factories["music.TrackFile"](in_place=True, source="file://{}".format(path)) assert tf.get_file_size() == 297745 def test_album_get_image_content(factories): - album = factories['music.Album']() - album.get_image(data={'content': b'test', 'mimetype':'image/jpeg'}) + album = factories["music.Album"]() + album.get_image(data={"content": b"test", "mimetype": "image/jpeg"}) album.refresh_from_db() - assert album.cover.read() == b'test' + assert album.cover.read() == b"test" diff --git a/api/tests/music/test_music.py b/api/tests/music/test_music.py index 4162912e4..391af26cb 100644 --- a/api/tests/music/test_music.py +++ b/api/tests/music/test_music.py @@ -5,121 +5,136 @@ import datetime def test_can_create_artist_from_api(artists, mocker, db): mocker.patch( - 'musicbrainzngs.search_artists', - return_value=artists['search']['adhesive_wombat']) + "musicbrainzngs.search_artists", + return_value=artists["search"]["adhesive_wombat"], + ) artist = models.Artist.create_from_api(query="Adhesive wombat") - data = models.Artist.api.search(query='Adhesive wombat')['artist-list'][0] + data = models.Artist.api.search(query="Adhesive wombat")["artist-list"][0] - assert int(data['ext:score']), 100 - assert data['id'], '62c3befb-6366-4585-b256-809472333801' - assert artist.mbid, data['id'] - assert artist.name, 'Adhesive Wombat' + assert int(data["ext:score"]), 100 + assert data["id"], "62c3befb-6366-4585-b256-809472333801" + assert artist.mbid, data["id"] + assert artist.name, "Adhesive Wombat" def test_can_create_album_from_api(artists, albums, mocker, db): mocker.patch( - 'funkwhale_api.musicbrainz.api.releases.search', - return_value=albums['search']['hypnotize']) + "funkwhale_api.musicbrainz.api.releases.search", + return_value=albums["search"]["hypnotize"], + ) mocker.patch( - 'funkwhale_api.musicbrainz.api.artists.get', - return_value=artists['get']['soad']) - album = models.Album.create_from_api(query="Hypnotize", artist='system of a down', type='album') - data = models.Album.api.search(query='Hypnotize', artist='system of a down', type='album')['release-list'][0] + "funkwhale_api.musicbrainz.api.artists.get", return_value=artists["get"]["soad"] + ) + album = models.Album.create_from_api( + query="Hypnotize", artist="system of a down", type="album" + ) + data = models.Album.api.search( + query="Hypnotize", artist="system of a down", type="album" + )["release-list"][0] - assert album.mbid, data['id'] - assert album.title, 'Hypnotize' + assert album.mbid, data["id"] + assert album.title, "Hypnotize" with pytest.raises(ValueError): assert album.cover.path is not None assert album.release_date, datetime.date(2005, 1, 1) - assert album.artist.name, 'System of a Down' - assert album.artist.mbid, data['artist-credit'][0]['artist']['id'] + assert album.artist.name, "System of a Down" + assert album.artist.mbid, data["artist-credit"][0]["artist"]["id"] def test_can_create_track_from_api(artists, albums, tracks, mocker, db): mocker.patch( - 'funkwhale_api.musicbrainz.api.artists.get', - return_value=artists['get']['adhesive_wombat']) + "funkwhale_api.musicbrainz.api.artists.get", + return_value=artists["get"]["adhesive_wombat"], + ) mocker.patch( - 'funkwhale_api.musicbrainz.api.releases.get', - return_value=albums['get']['marsupial']) + "funkwhale_api.musicbrainz.api.releases.get", + return_value=albums["get"]["marsupial"], + ) mocker.patch( - 'funkwhale_api.musicbrainz.api.recordings.search', - return_value=tracks['search']['8bitadventures']) + "funkwhale_api.musicbrainz.api.recordings.search", + return_value=tracks["search"]["8bitadventures"], + ) track = models.Track.create_from_api(query="8-bit adventure") - data = models.Track.api.search(query='8-bit adventure')['recording-list'][0] - assert int(data['ext:score']) == 100 - assert data['id'] == '9968a9d6-8d92-4051-8f76-674e157b6eed' - assert track.mbid == data['id'] + data = models.Track.api.search(query="8-bit adventure")["recording-list"][0] + assert int(data["ext:score"]) == 100 + assert data["id"] == "9968a9d6-8d92-4051-8f76-674e157b6eed" + assert track.mbid == data["id"] assert track.artist.pk is not None - assert str(track.artist.mbid) == '62c3befb-6366-4585-b256-809472333801' - assert track.artist.name == 'Adhesive Wombat' - assert str(track.album.mbid) == 'a50d2a81-2a50-484d-9cb4-b9f6833f583e' - assert track.album.title == 'Marsupial Madness' + assert str(track.artist.mbid) == "62c3befb-6366-4585-b256-809472333801" + assert track.artist.name == "Adhesive Wombat" + assert str(track.album.mbid) == "a50d2a81-2a50-484d-9cb4-b9f6833f583e" + assert track.album.title == "Marsupial Madness" def test_can_create_track_from_api_with_corresponding_tags( - artists, albums, tracks, mocker, db): + artists, albums, tracks, mocker, db +): mocker.patch( - 'funkwhale_api.musicbrainz.api.artists.get', - return_value=artists['get']['adhesive_wombat']) + "funkwhale_api.musicbrainz.api.artists.get", + return_value=artists["get"]["adhesive_wombat"], + ) mocker.patch( - 'funkwhale_api.musicbrainz.api.releases.get', - return_value=albums['get']['marsupial']) + "funkwhale_api.musicbrainz.api.releases.get", + return_value=albums["get"]["marsupial"], + ) mocker.patch( - 'funkwhale_api.musicbrainz.api.recordings.get', - return_value=tracks['get']['8bitadventures']) - track = models.Track.create_from_api(id='9968a9d6-8d92-4051-8f76-674e157b6eed') - expected_tags = ['techno', 'good-music'] + "funkwhale_api.musicbrainz.api.recordings.get", + return_value=tracks["get"]["8bitadventures"], + ) + track = models.Track.create_from_api(id="9968a9d6-8d92-4051-8f76-674e157b6eed") + expected_tags = ["techno", "good-music"] track_tags = [tag.slug for tag in track.tags.all()] for tag in expected_tags: assert tag in track_tags -def test_can_get_or_create_track_from_api( - artists, albums, tracks, mocker, db): +def test_can_get_or_create_track_from_api(artists, albums, tracks, mocker, db): mocker.patch( - 'funkwhale_api.musicbrainz.api.artists.get', - return_value=artists['get']['adhesive_wombat']) + "funkwhale_api.musicbrainz.api.artists.get", + return_value=artists["get"]["adhesive_wombat"], + ) mocker.patch( - 'funkwhale_api.musicbrainz.api.releases.get', - return_value=albums['get']['marsupial']) + "funkwhale_api.musicbrainz.api.releases.get", + return_value=albums["get"]["marsupial"], + ) mocker.patch( - 'funkwhale_api.musicbrainz.api.recordings.search', - return_value=tracks['search']['8bitadventures']) + "funkwhale_api.musicbrainz.api.recordings.search", + return_value=tracks["search"]["8bitadventures"], + ) track = models.Track.create_from_api(query="8-bit adventure") - data = models.Track.api.search(query='8-bit adventure')['recording-list'][0] - assert int(data['ext:score']) == 100 - assert data['id'] == '9968a9d6-8d92-4051-8f76-674e157b6eed' - assert track.mbid == data['id'] + data = models.Track.api.search(query="8-bit adventure")["recording-list"][0] + assert int(data["ext:score"]) == 100 + assert data["id"] == "9968a9d6-8d92-4051-8f76-674e157b6eed" + assert track.mbid == data["id"] assert track.artist.pk is not None - assert str(track.artist.mbid) == '62c3befb-6366-4585-b256-809472333801' - assert track.artist.name == 'Adhesive Wombat' + assert str(track.artist.mbid) == "62c3befb-6366-4585-b256-809472333801" + assert track.artist.name == "Adhesive Wombat" - track2, created = models.Track.get_or_create_from_api(mbid=data['id']) + track2, created = models.Track.get_or_create_from_api(mbid=data["id"]) assert not created assert track == track2 def test_album_tags_deduced_from_tracks_tags(factories, django_assert_num_queries): - tag = factories['taggit.Tag']() - album = factories['music.Album']() - tracks = factories['music.Track'].create_batch( - 5, album=album, tags=[tag]) + tag = factories["taggit.Tag"]() + album = factories["music.Album"]() + tracks = factories["music.Track"].create_batch(5, album=album, tags=[tag]) - album = models.Album.objects.prefetch_related('tracks__tags').get(pk=album.pk) + album = models.Album.objects.prefetch_related("tracks__tags").get(pk=album.pk) with django_assert_num_queries(0): assert tag in album.tags def test_artist_tags_deduced_from_album_tags(factories, django_assert_num_queries): - tag = factories['taggit.Tag']() - album = factories['music.Album']() + tag = factories["taggit.Tag"]() + album = factories["music.Album"]() artist = album.artist - tracks = factories['music.Track'].create_batch( - 5, album=album, tags=[tag]) + tracks = factories["music.Track"].create_batch(5, album=album, tags=[tag]) - artist = models.Artist.objects.prefetch_related('albums__tracks__tags').get(pk=artist.pk) + artist = models.Artist.objects.prefetch_related("albums__tracks__tags").get( + pk=artist.pk + ) with django_assert_num_queries(0): assert tag in artist.tags @@ -127,10 +142,10 @@ def test_artist_tags_deduced_from_album_tags(factories, django_assert_num_querie def test_can_download_image_file_for_album(binary_cover, mocker, factories): mocker.patch( - 'funkwhale_api.musicbrainz.api.images.get_front', - return_value=binary_cover) + "funkwhale_api.musicbrainz.api.images.get_front", return_value=binary_cover + ) # client._api.get_image_front('55ea4f82-b42b-423e-a0e5-290ccdf443ed') - album = factories['music.Album'](mbid='55ea4f82-b42b-423e-a0e5-290ccdf443ed') + album = factories["music.Album"](mbid="55ea4f82-b42b-423e-a0e5-290ccdf443ed") album.get_image() album.save() diff --git a/api/tests/music/test_permissions.py b/api/tests/music/test_permissions.py index 825d1731d..5f73a361e 100644 --- a/api/tests/music/test_permissions.py +++ b/api/tests/music/test_permissions.py @@ -5,58 +5,56 @@ from funkwhale_api.music import permissions def test_list_permission_no_protect(preferences, anonymous_user, api_request): - preferences['common__api_authentication_required'] = False + preferences["common__api_authentication_required"] = False view = APIView.as_view() permission = permissions.Listen() - request = api_request.get('/') + request = api_request.get("/") assert permission.has_permission(request, view) is True -def test_list_permission_protect_authenticated( - factories, api_request, preferences): - preferences['common__api_authentication_required'] = True - user = factories['users.User']() +def test_list_permission_protect_authenticated(factories, api_request, preferences): + preferences["common__api_authentication_required"] = True + user = factories["users.User"]() view = APIView.as_view() permission = permissions.Listen() - request = api_request.get('/') - setattr(request, 'user', user) + request = api_request.get("/") + setattr(request, "user", user) assert permission.has_permission(request, view) is True def test_list_permission_protect_not_following_actor( - factories, api_request, preferences): - preferences['common__api_authentication_required'] = True - actor = factories['federation.Actor']() + factories, api_request, preferences +): + preferences["common__api_authentication_required"] = True + actor = factories["federation.Actor"]() view = APIView.as_view() permission = permissions.Listen() - request = api_request.get('/') - setattr(request, 'actor', actor) + request = api_request.get("/") + setattr(request, "actor", actor) assert permission.has_permission(request, view) is False -def test_list_permission_protect_following_actor( - factories, api_request, preferences): - preferences['common__api_authentication_required'] = True - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - follow = factories['federation.Follow']( - approved=True, target=library_actor) +def test_list_permission_protect_following_actor(factories, api_request, preferences): + preferences["common__api_authentication_required"] = True + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + follow = factories["federation.Follow"](approved=True, target=library_actor) view = APIView.as_view() permission = permissions.Listen() - request = api_request.get('/') - setattr(request, 'actor', follow.actor) + request = api_request.get("/") + setattr(request, "actor", follow.actor) assert permission.has_permission(request, view) is True def test_list_permission_protect_following_actor_not_approved( - factories, api_request, preferences): - preferences['common__api_authentication_required'] = True - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - follow = factories['federation.Follow']( - approved=False, target=library_actor) + factories, api_request, preferences +): + preferences["common__api_authentication_required"] = True + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + follow = factories["federation.Follow"](approved=False, target=library_actor) view = APIView.as_view() permission = permissions.Listen() - request = api_request.get('/') - setattr(request, 'actor', follow.actor) + request = api_request.get("/") + setattr(request, "actor", follow.actor) assert permission.has_permission(request, view) is False diff --git a/api/tests/music/test_serializers.py b/api/tests/music/test_serializers.py index fa22cecee..9402e1459 100644 --- a/api/tests/music/test_serializers.py +++ b/api/tests/music/test_serializers.py @@ -2,18 +2,18 @@ from funkwhale_api.music import serializers def test_artist_album_serializer(factories, to_api_date): - track = factories['music.Track']() + track = factories["music.Track"]() album = track.album album = album.__class__.objects.with_tracks_count().get(pk=album.pk) expected = { - 'id': album.id, - 'mbid': str(album.mbid), - 'title': album.title, - 'artist': album.artist.id, - 'creation_date': to_api_date(album.creation_date), - 'tracks_count': 1, - 'cover': album.cover.url, - 'release_date': to_api_date(album.release_date), + "id": album.id, + "mbid": str(album.mbid), + "title": album.title, + "artist": album.artist.id, + "creation_date": to_api_date(album.creation_date), + "tracks_count": 1, + "cover": album.cover.url, + "release_date": to_api_date(album.release_date), } serializer = serializers.ArtistAlbumSerializer(album) @@ -21,79 +21,72 @@ def test_artist_album_serializer(factories, to_api_date): def test_artist_with_albums_serializer(factories, to_api_date): - track = factories['music.Track']() + track = factories["music.Track"]() artist = track.artist artist = artist.__class__.objects.with_albums().get(pk=artist.pk) album = list(artist.albums.all())[0] expected = { - 'id': artist.id, - 'mbid': str(artist.mbid), - 'name': artist.name, - 'creation_date': to_api_date(artist.creation_date), - 'albums': [ - serializers.ArtistAlbumSerializer(album).data - ] + "id": artist.id, + "mbid": str(artist.mbid), + "name": artist.name, + "creation_date": to_api_date(artist.creation_date), + "albums": [serializers.ArtistAlbumSerializer(album).data], } serializer = serializers.ArtistWithAlbumsSerializer(artist) assert serializer.data == expected def test_album_track_serializer(factories, to_api_date): - tf = factories['music.TrackFile']() + tf = factories["music.TrackFile"]() track = tf.track expected = { - 'id': track.id, - 'artist': track.artist.id, - 'album': track.album.id, - 'mbid': str(track.mbid), - 'title': track.title, - 'position': track.position, - 'creation_date': to_api_date(track.creation_date), - 'files': [ - serializers.TrackFileSerializer(tf).data - ] + "id": track.id, + "artist": track.artist.id, + "album": track.album.id, + "mbid": str(track.mbid), + "title": track.title, + "position": track.position, + "creation_date": to_api_date(track.creation_date), + "files": [serializers.TrackFileSerializer(tf).data], } serializer = serializers.AlbumTrackSerializer(track) assert serializer.data == expected def test_track_file_serializer(factories, to_api_date): - tf = factories['music.TrackFile']() + tf = factories["music.TrackFile"]() expected = { - 'id': tf.id, - 'path': tf.path, - 'source': tf.source, - 'filename': tf.filename, - 'mimetype': tf.mimetype, - 'track': tf.track.pk, - 'duration': tf.duration, - 'mimetype': tf.mimetype, - 'bitrate': tf.bitrate, - 'size': tf.size, + "id": tf.id, + "path": tf.path, + "source": tf.source, + "filename": tf.filename, + "mimetype": tf.mimetype, + "track": tf.track.pk, + "duration": tf.duration, + "mimetype": tf.mimetype, + "bitrate": tf.bitrate, + "size": tf.size, } serializer = serializers.TrackFileSerializer(tf) assert serializer.data == expected def test_album_serializer(factories, to_api_date): - track1 = factories['music.Track'](position=2) - track2 = factories['music.Track'](position=1, album=track1.album) + track1 = factories["music.Track"](position=2) + track2 = factories["music.Track"](position=1, album=track1.album) album = track1.album expected = { - 'id': album.id, - 'mbid': str(album.mbid), - 'title': album.title, - 'artist': serializers.ArtistSimpleSerializer(album.artist).data, - 'creation_date': to_api_date(album.creation_date), - 'cover': album.cover.url, - 'release_date': to_api_date(album.release_date), - 'tracks': serializers.AlbumTrackSerializer( - [track2, track1], - many=True - ).data + "id": album.id, + "mbid": str(album.mbid), + "title": album.title, + "artist": serializers.ArtistSimpleSerializer(album.artist).data, + "creation_date": to_api_date(album.creation_date), + "cover": album.cover.url, + "release_date": to_api_date(album.release_date), + "tracks": serializers.AlbumTrackSerializer([track2, track1], many=True).data, } serializer = serializers.AlbumSerializer(album) @@ -101,21 +94,19 @@ def test_album_serializer(factories, to_api_date): def test_track_serializer(factories, to_api_date): - tf = factories['music.TrackFile']() + tf = factories["music.TrackFile"]() track = tf.track expected = { - 'id': track.id, - 'artist': serializers.ArtistSimpleSerializer(track.artist).data, - 'album': serializers.TrackAlbumSerializer(track.album).data, - 'mbid': str(track.mbid), - 'title': track.title, - 'position': track.position, - 'creation_date': to_api_date(track.creation_date), - 'lyrics': track.get_lyrics_url(), - 'files': [ - serializers.TrackFileSerializer(tf).data - ] + "id": track.id, + "artist": serializers.ArtistSimpleSerializer(track.artist).data, + "album": serializers.TrackAlbumSerializer(track.album).data, + "mbid": str(track.mbid), + "title": track.title, + "position": track.position, + "creation_date": to_api_date(track.creation_date), + "lyrics": track.get_lyrics_url(), + "files": [serializers.TrackFileSerializer(tf).data], } serializer = serializers.TrackSerializer(track) assert serializer.data == expected diff --git a/api/tests/music/test_tasks.py b/api/tests/music/test_tasks.py index 77245e204..1d35e75e2 100644 --- a/api/tests/music/test_tasks.py +++ b/api/tests/music/test_tasks.py @@ -8,39 +8,45 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__)) def test_set_acoustid_on_track_file(factories, mocker, preferences): - preferences['providers_acoustid__api_key'] = 'test' - track_file = factories['music.TrackFile'](acoustid_track_id=None) - id = 'e475bf79-c1ce-4441-bed7-1e33f226c0a2' + preferences["providers_acoustid__api_key"] = "test" + track_file = factories["music.TrackFile"](acoustid_track_id=None) + id = "e475bf79-c1ce-4441-bed7-1e33f226c0a2" payload = { - 'results': [ - {'id': id, - 'recordings': [ - {'artists': [ - {'id': '9c6bddde-6228-4d9f-ad0d-03f6fcb19e13', - 'name': 'Binärpilot'}], - 'duration': 268, - 'id': 'f269d497-1cc0-4ae4-a0c4-157ec7d73fcb', - 'title': 'Bend'}], - 'score': 0.860825}], - 'status': 'ok' + "results": [ + { + "id": id, + "recordings": [ + { + "artists": [ + { + "id": "9c6bddde-6228-4d9f-ad0d-03f6fcb19e13", + "name": "Binärpilot", + } + ], + "duration": 268, + "id": "f269d497-1cc0-4ae4-a0c4-157ec7d73fcb", + "title": "Bend", + } + ], + "score": 0.860825, + } + ], + "status": "ok", } - m = mocker.patch('acoustid.match', return_value=payload) + m = mocker.patch("acoustid.match", return_value=payload) r = tasks.set_acoustid_on_track_file(track_file_id=track_file.pk) track_file.refresh_from_db() assert str(track_file.acoustid_track_id) == id assert r == id - m.assert_called_once_with('test', track_file.audio_file.path, parse=False) + m.assert_called_once_with("test", track_file.audio_file.path, parse=False) def test_set_acoustid_on_track_file_required_high_score(factories, mocker): - track_file = factories['music.TrackFile'](acoustid_track_id=None) - id = 'e475bf79-c1ce-4441-bed7-1e33f226c0a2' - payload = { - 'results': [{'score': 0.79}], - 'status': 'ok' - } - m = mocker.patch('acoustid.match', return_value=payload) + track_file = factories["music.TrackFile"](acoustid_track_id=None) + id = "e475bf79-c1ce-4441-bed7-1e33f226c0a2" + payload = {"results": [{"score": 0.79}], "status": "ok"} + m = mocker.patch("acoustid.match", return_value=payload) r = tasks.set_acoustid_on_track_file(track_file_id=track_file.pk) track_file.refresh_from_db() @@ -48,110 +54,111 @@ def test_set_acoustid_on_track_file_required_high_score(factories, mocker): def test_import_batch_run(factories, mocker): - job = factories['music.ImportJob']() - mocked_job_run = mocker.patch( - 'funkwhale_api.music.tasks.import_job_run.delay') + job = factories["music.ImportJob"]() + mocked_job_run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay") tasks.import_batch_run(import_batch_id=job.batch.pk) mocked_job_run.assert_called_once_with(import_job_id=job.pk) -@pytest.mark.skip('Acoustid is disabled') +@pytest.mark.skip("Acoustid is disabled") def test_import_job_can_run_with_file_and_acoustid( - artists, albums, tracks, preferences, factories, mocker): - preferences['providers_acoustid__api_key'] = 'test' - path = os.path.join(DATA_DIR, 'test.ogg') - mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed' + artists, albums, tracks, preferences, factories, mocker +): + preferences["providers_acoustid__api_key"] = "test" + path = os.path.join(DATA_DIR, "test.ogg") + mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed" acoustid_payload = { - 'results': [ - {'id': 'e475bf79-c1ce-4441-bed7-1e33f226c0a2', - 'recordings': [ - { - 'duration': 268, - 'id': mbid}], - 'score': 0.860825}], - 'status': 'ok' + "results": [ + { + "id": "e475bf79-c1ce-4441-bed7-1e33f226c0a2", + "recordings": [{"duration": 268, "id": mbid}], + "score": 0.860825, + } + ], + "status": "ok", } mocker.patch( - 'funkwhale_api.music.utils.get_audio_file_data', - return_value={'bitrate': 42, 'length': 43}) + "funkwhale_api.music.utils.get_audio_file_data", + return_value={"bitrate": 42, "length": 43}, + ) mocker.patch( - 'funkwhale_api.musicbrainz.api.artists.get', - return_value=artists['get']['adhesive_wombat']) + "funkwhale_api.musicbrainz.api.artists.get", + return_value=artists["get"]["adhesive_wombat"], + ) mocker.patch( - 'funkwhale_api.musicbrainz.api.releases.get', - return_value=albums['get']['marsupial']) + "funkwhale_api.musicbrainz.api.releases.get", + return_value=albums["get"]["marsupial"], + ) mocker.patch( - 'funkwhale_api.musicbrainz.api.recordings.search', - return_value=tracks['search']['8bitadventures']) - mocker.patch('acoustid.match', return_value=acoustid_payload) + "funkwhale_api.musicbrainz.api.recordings.search", + return_value=tracks["search"]["8bitadventures"], + ) + mocker.patch("acoustid.match", return_value=acoustid_payload) - job = factories['music.FileImportJob'](audio_file__path=path) + job = factories["music.FileImportJob"](audio_file__path=path) f = job.audio_file tasks.import_job_run(import_job_id=job.pk) job.refresh_from_db() track_file = job.track_file - with open(path, 'rb') as f: + with open(path, "rb") as f: assert track_file.audio_file.read() == f.read() assert track_file.bitrate == 42 assert track_file.duration == 43 assert track_file.size == os.path.getsize(path) # audio file is deleted from import job once persisted to audio file assert not job.audio_file - assert job.status == 'finished' - assert job.source == 'file://' + assert job.status == "finished" + assert job.source == "file://" def test_run_import_skipping_accoustid(factories, mocker): - m = mocker.patch('funkwhale_api.music.tasks._do_import') - path = os.path.join(DATA_DIR, 'test.ogg') - job = factories['music.FileImportJob'](audio_file__path=path) + m = mocker.patch("funkwhale_api.music.tasks._do_import") + path = os.path.join(DATA_DIR, "test.ogg") + job = factories["music.FileImportJob"](audio_file__path=path) tasks.import_job_run(import_job_id=job.pk, use_acoustid=False) m.assert_called_once_with(job, False, use_acoustid=False) def test__do_import_skipping_accoustid(factories, mocker): - t = factories['music.Track']() + t = factories["music.Track"]() m = mocker.patch( - 'funkwhale_api.providers.audiofile.tasks.import_track_data_from_path', - return_value=t) - path = os.path.join(DATA_DIR, 'test.ogg') - job = factories['music.FileImportJob']( - mbid=None, - audio_file__path=path) + "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path", + return_value=t, + ) + path = os.path.join(DATA_DIR, "test.ogg") + job = factories["music.FileImportJob"](mbid=None, audio_file__path=path) p = job.audio_file.path tasks._do_import(job, replace=False, use_acoustid=False) m.assert_called_once_with(p) -def test__do_import_skipping_accoustid_if_no_key( - factories, mocker, preferences): - preferences['providers_acoustid__api_key'] = '' - t = factories['music.Track']() +def test__do_import_skipping_accoustid_if_no_key(factories, mocker, preferences): + preferences["providers_acoustid__api_key"] = "" + t = factories["music.Track"]() m = mocker.patch( - 'funkwhale_api.providers.audiofile.tasks.import_track_data_from_path', - return_value=t) - path = os.path.join(DATA_DIR, 'test.ogg') - job = factories['music.FileImportJob']( - mbid=None, - audio_file__path=path) + "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path", + return_value=t, + ) + path = os.path.join(DATA_DIR, "test.ogg") + job = factories["music.FileImportJob"](mbid=None, audio_file__path=path) p = job.audio_file.path tasks._do_import(job, replace=False, use_acoustid=False) m.assert_called_once_with(p) -def test_import_job_skip_if_already_exists( - artists, albums, tracks, factories, mocker): - path = os.path.join(DATA_DIR, 'test.ogg') - mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed' - track_file = factories['music.TrackFile'](track__mbid=mbid) +def test_import_job_skip_if_already_exists(artists, albums, tracks, factories, mocker): + path = os.path.join(DATA_DIR, "test.ogg") + mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed" + track_file = factories["music.TrackFile"](track__mbid=mbid) mocker.patch( - 'funkwhale_api.providers.audiofile.tasks.import_track_data_from_path', - return_value=track_file.track) + "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path", + return_value=track_file.track, + ) - job = factories['music.FileImportJob'](audio_file__path=path) + job = factories["music.FileImportJob"](audio_file__path=path) f = job.audio_file tasks.import_job_run(import_job_id=job.pk) job.refresh_from_db() @@ -159,23 +166,20 @@ def test_import_job_skip_if_already_exists( assert job.track_file is None # audio file is deleted from import job once persisted to audio file assert not job.audio_file - assert job.status == 'skipped' + assert job.status == "skipped" def test_import_job_can_be_errored(factories, mocker, preferences): - path = os.path.join(DATA_DIR, 'test.ogg') - mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed' - track_file = factories['music.TrackFile'](track__mbid=mbid) + path = os.path.join(DATA_DIR, "test.ogg") + mbid = "9968a9d6-8d92-4051-8f76-674e157b6eed" + track_file = factories["music.TrackFile"](track__mbid=mbid) class MyException(Exception): pass - mocker.patch( - 'funkwhale_api.music.tasks._do_import', - side_effect=MyException()) + mocker.patch("funkwhale_api.music.tasks._do_import", side_effect=MyException()) - job = factories['music.FileImportJob']( - audio_file__path=path, track_file=None) + job = factories["music.FileImportJob"](audio_file__path=path, track_file=None) with pytest.raises(MyException): tasks.import_job_run(import_job_id=job.pk) @@ -183,23 +187,22 @@ def test_import_job_can_be_errored(factories, mocker, preferences): job.refresh_from_db() assert job.track_file is None - assert job.status == 'errored' + assert job.status == "errored" def test__do_import_calls_update_album_cover_if_no_cover(factories, mocker): - path = os.path.join(DATA_DIR, 'test.ogg') - album = factories['music.Album'](cover='') - track = factories['music.Track'](album=album) + path = os.path.join(DATA_DIR, "test.ogg") + album = factories["music.Album"](cover="") + track = factories["music.Track"](album=album) mocker.patch( - 'funkwhale_api.providers.audiofile.tasks.import_track_data_from_path', - return_value=track) + "funkwhale_api.providers.audiofile.tasks.import_track_data_from_path", + return_value=track, + ) - mocked_update = mocker.patch( - 'funkwhale_api.music.tasks.update_album_cover') + mocked_update = mocker.patch("funkwhale_api.music.tasks.update_album_cover") - job = factories['music.FileImportJob']( - audio_file__path=path, track_file=None) + job = factories["music.FileImportJob"](audio_file__path=path, track_file=None) tasks.import_job_run(import_job_id=job.pk) @@ -207,50 +210,43 @@ def test__do_import_calls_update_album_cover_if_no_cover(factories, mocker): def test_update_album_cover_mbid(factories, mocker): - album = factories['music.Album'](cover='') + album = factories["music.Album"](cover="") - mocked_get = mocker.patch('funkwhale_api.music.models.Album.get_image') + mocked_get = mocker.patch("funkwhale_api.music.models.Album.get_image") tasks.update_album_cover(album=album, track_file=None) mocked_get.assert_called_once_with() def test_update_album_cover_file_data(factories, mocker): - path = os.path.join(DATA_DIR, 'test.mp3') - album = factories['music.Album'](cover='', mbid=None) - tf = factories['music.TrackFile'](track__album=album) + path = os.path.join(DATA_DIR, "test.mp3") + album = factories["music.Album"](cover="", mbid=None) + tf = factories["music.TrackFile"](track__album=album) - mocked_get = mocker.patch('funkwhale_api.music.models.Album.get_image') + mocked_get = mocker.patch("funkwhale_api.music.models.Album.get_image") mocker.patch( - 'funkwhale_api.music.metadata.Metadata.get_picture', - return_value={'hello': 'world'}) + "funkwhale_api.music.metadata.Metadata.get_picture", + return_value={"hello": "world"}, + ) tasks.update_album_cover(album=album, track_file=tf) md = data = tf.get_metadata() - mocked_get.assert_called_once_with( - data={'hello': 'world'}) + mocked_get.assert_called_once_with(data={"hello": "world"}) -@pytest.mark.parametrize('ext,mimetype', [ - ('jpg', 'image/jpeg'), - ('png', 'image/png'), -]) -def test_update_album_cover_file_cover_separate_file( - ext, mimetype, factories, mocker): - mocker.patch('funkwhale_api.music.tasks.IMAGE_TYPES', [(ext, mimetype)]) - path = os.path.join(DATA_DIR, 'test.mp3') - image_path = os.path.join(DATA_DIR, 'cover.{}'.format(ext)) - with open(image_path, 'rb') as f: +@pytest.mark.parametrize("ext,mimetype", [("jpg", "image/jpeg"), ("png", "image/png")]) +def test_update_album_cover_file_cover_separate_file(ext, mimetype, factories, mocker): + mocker.patch("funkwhale_api.music.tasks.IMAGE_TYPES", [(ext, mimetype)]) + path = os.path.join(DATA_DIR, "test.mp3") + image_path = os.path.join(DATA_DIR, "cover.{}".format(ext)) + with open(image_path, "rb") as f: image_content = f.read() - album = factories['music.Album'](cover='', mbid=None) - tf = factories['music.TrackFile']( - track__album=album, - source='file://' + image_path) + album = factories["music.Album"](cover="", mbid=None) + tf = factories["music.TrackFile"](track__album=album, source="file://" + image_path) - mocked_get = mocker.patch('funkwhale_api.music.models.Album.get_image') - mocker.patch( - 'funkwhale_api.music.metadata.Metadata.get_picture', - return_value=None) + mocked_get = mocker.patch("funkwhale_api.music.models.Album.get_image") + mocker.patch("funkwhale_api.music.metadata.Metadata.get_picture", return_value=None) tasks.update_album_cover(album=album, track_file=tf) md = data = tf.get_metadata() mocked_get.assert_called_once_with( - data={'mimetype': mimetype, 'content': image_content}) + data={"mimetype": mimetype, "content": image_content} + ) diff --git a/api/tests/music/test_utils.py b/api/tests/music/test_utils.py index 7b967dbbc..276a450d4 100644 --- a/api/tests/music/test_utils.py +++ b/api/tests/music/test_utils.py @@ -7,35 +7,31 @@ DATA_DIR = os.path.dirname(os.path.abspath(__file__)) def test_guess_mimetype_try_using_extension(factories, mocker): - mocker.patch( - 'magic.from_buffer', return_value='audio/mpeg') - f = factories['music.TrackFile'].build( - audio_file__filename='test.ogg') + mocker.patch("magic.from_buffer", return_value="audio/mpeg") + f = factories["music.TrackFile"].build(audio_file__filename="test.ogg") - assert utils.guess_mimetype(f.audio_file) == 'audio/mpeg' + assert utils.guess_mimetype(f.audio_file) == "audio/mpeg" -@pytest.mark.parametrize('wrong', [ - 'application/octet-stream', - 'application/x-empty', -]) +@pytest.mark.parametrize("wrong", ["application/octet-stream", "application/x-empty"]) def test_guess_mimetype_try_using_extension_if_fail(wrong, factories, mocker): - mocker.patch( - 'magic.from_buffer', return_value=wrong) - f = factories['music.TrackFile'].build( - audio_file__filename='test.mp3') + mocker.patch("magic.from_buffer", return_value=wrong) + f = factories["music.TrackFile"].build(audio_file__filename="test.mp3") - assert utils.guess_mimetype(f.audio_file) == 'audio/mpeg' + assert utils.guess_mimetype(f.audio_file) == "audio/mpeg" -@pytest.mark.parametrize('name, expected', [ - ('sample.flac', {'bitrate': 1608000, 'length': 0.001}), - ('test.mp3', {'bitrate': 8000, 'length': 267.70285714285717}), - ('test.ogg', {'bitrate': 128000, 'length': 229.18304166666667}), -]) +@pytest.mark.parametrize( + "name, expected", + [ + ("sample.flac", {"bitrate": 1608000, "length": 0.001}), + ("test.mp3", {"bitrate": 8000, "length": 267.70285714285717}), + ("test.ogg", {"bitrate": 128000, "length": 229.18304166666667}), + ], +) def test_get_audio_file_data(name, expected): path = os.path.join(DATA_DIR, name) - with open(path, 'rb') as f: + with open(path, "rb") as f: result = utils.get_audio_file_data(f) assert result == expected diff --git a/api/tests/music/test_views.py b/api/tests/music/test_views.py index 91fef13f2..139a33cf5 100644 --- a/api/tests/music/test_views.py +++ b/api/tests/music/test_views.py @@ -9,28 +9,27 @@ from funkwhale_api.music import views from funkwhale_api.federation import actors -@pytest.mark.parametrize('view,permissions,operator', [ - (views.ImportBatchViewSet, ['library', 'upload'], 'or'), - (views.ImportJobViewSet, ['library', 'upload'], 'or'), -]) +@pytest.mark.parametrize( + "view,permissions,operator", + [ + (views.ImportBatchViewSet, ["library", "upload"], "or"), + (views.ImportJobViewSet, ["library", "upload"], "or"), + ], +) def test_permissions(assert_user_permission, view, permissions, operator): assert_user_permission(view, permissions, operator) def test_artist_list_serializer(api_request, factories, logged_in_api_client): - track = factories['music.Track']() + track = factories["music.Track"]() artist = track.artist - request = api_request.get('/') + request = api_request.get("/") qs = artist.__class__.objects.with_albums() serializer = serializers.ArtistWithAlbumsSerializer( - qs, many=True, context={'request': request}) - expected = { - 'count': 1, - 'next': None, - 'previous': None, - 'results': serializer.data - } - url = reverse('api:v1:artists-list') + qs, many=True, context={"request": request} + ) + expected = {"count": 1, "next": None, "previous": None, "results": serializer.data} + url = reverse("api:v1:artists-list") response = logged_in_api_client.get(url) assert response.status_code == 200 @@ -38,19 +37,15 @@ def test_artist_list_serializer(api_request, factories, logged_in_api_client): def test_album_list_serializer(api_request, factories, logged_in_api_client): - track = factories['music.Track']() + track = factories["music.Track"]() album = track.album - request = api_request.get('/') + request = api_request.get("/") qs = album.__class__.objects.all() serializer = serializers.AlbumSerializer( - qs, many=True, context={'request': request}) - expected = { - 'count': 1, - 'next': None, - 'previous': None, - 'results': serializer.data - } - url = reverse('api:v1:albums-list') + qs, many=True, context={"request": request} + ) + expected = {"count": 1, "next": None, "previous": None, "results": serializer.data} + url = reverse("api:v1:albums-list") response = logged_in_api_client.get(url) assert response.status_code == 200 @@ -58,38 +53,30 @@ def test_album_list_serializer(api_request, factories, logged_in_api_client): def test_track_list_serializer(api_request, factories, logged_in_api_client): - track = factories['music.Track']() - request = api_request.get('/') + track = factories["music.Track"]() + request = api_request.get("/") qs = track.__class__.objects.all() serializer = serializers.TrackSerializer( - qs, many=True, context={'request': request}) - expected = { - 'count': 1, - 'next': None, - 'previous': None, - 'results': serializer.data - } - url = reverse('api:v1:tracks-list') + qs, many=True, context={"request": request} + ) + expected = {"count": 1, "next": None, "previous": None, "results": serializer.data} + url = reverse("api:v1:tracks-list") response = logged_in_api_client.get(url) assert response.status_code == 200 assert response.data == expected -@pytest.mark.parametrize('param,expected', [ - ('true', 'full'), - ('false', 'empty'), -]) -def test_artist_view_filter_listenable( - param, expected, factories, api_request): +@pytest.mark.parametrize("param,expected", [("true", "full"), ("false", "empty")]) +def test_artist_view_filter_listenable(param, expected, factories, api_request): artists = { - 'empty': factories['music.Artist'](), - 'full': factories['music.TrackFile']().track.artist, + "empty": factories["music.Artist"](), + "full": factories["music.TrackFile"]().track.artist, } - request = api_request.get('/', {'listenable': param}) + request = api_request.get("/", {"listenable": param}) view = views.ArtistViewSet() - view.action_map = {'get': 'list'} + view.action_map = {"get": "list"} expected = [artists[expected]] view.request = view.initialize_request(request) queryset = view.filter_queryset(view.get_queryset()) @@ -97,20 +84,16 @@ def test_artist_view_filter_listenable( assert list(queryset) == expected -@pytest.mark.parametrize('param,expected', [ - ('true', 'full'), - ('false', 'empty'), -]) -def test_album_view_filter_listenable( - param, expected, factories, api_request): +@pytest.mark.parametrize("param,expected", [("true", "full"), ("false", "empty")]) +def test_album_view_filter_listenable(param, expected, factories, api_request): artists = { - 'empty': factories['music.Album'](), - 'full': factories['music.TrackFile']().track.album, + "empty": factories["music.Album"](), + "full": factories["music.TrackFile"]().track.album, } - request = api_request.get('/', {'listenable': param}) + request = api_request.get("/", {"listenable": param}) view = views.AlbumViewSet() - view.action_map = {'get': 'list'} + view.action_map = {"get": "list"} expected = [artists[expected]] view.request = view.initialize_request(request) queryset = view.filter_queryset(view.get_queryset()) @@ -119,58 +102,53 @@ def test_album_view_filter_listenable( def test_can_serve_track_file_as_remote_library( - factories, authenticated_actor, api_client, settings, preferences): - preferences['common__api_authentication_required'] = True - library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance() - follow = factories['federation.Follow']( - approved=True, - actor=authenticated_actor, - target=library_actor) + factories, authenticated_actor, api_client, settings, preferences +): + preferences["common__api_authentication_required"] = True + library_actor = actors.SYSTEM_ACTORS["library"].get_actor_instance() + follow = factories["federation.Follow"]( + approved=True, actor=authenticated_actor, target=library_actor + ) - track_file = factories['music.TrackFile']() + track_file = factories["music.TrackFile"]() response = api_client.get(track_file.path) assert response.status_code == 200 - assert response['X-Accel-Redirect'] == "{}{}".format( - settings.PROTECT_FILES_PATH, - track_file.audio_file.url) + assert response["X-Accel-Redirect"] == "{}{}".format( + settings.PROTECT_FILES_PATH, track_file.audio_file.url + ) def test_can_serve_track_file_as_remote_library_deny_not_following( - factories, authenticated_actor, settings, api_client, preferences): - preferences['common__api_authentication_required'] = True - track_file = factories['music.TrackFile']() + factories, authenticated_actor, settings, api_client, preferences +): + preferences["common__api_authentication_required"] = True + track_file = factories["music.TrackFile"]() response = api_client.get(track_file.path) assert response.status_code == 403 -@pytest.mark.parametrize('proxy,serve_path,expected', [ - ('apache2', '/host/music', '/host/music/hello/world.mp3'), - ('apache2', '/app/music', '/app/music/hello/world.mp3'), - ('nginx', '/host/music', '/_protected/music/hello/world.mp3'), - ('nginx', '/app/music', '/_protected/music/hello/world.mp3'), -]) +@pytest.mark.parametrize( + "proxy,serve_path,expected", + [ + ("apache2", "/host/music", "/host/music/hello/world.mp3"), + ("apache2", "/app/music", "/app/music/hello/world.mp3"), + ("nginx", "/host/music", "/_protected/music/hello/world.mp3"), + ("nginx", "/app/music", "/_protected/music/hello/world.mp3"), + ], +) def test_serve_file_in_place( - proxy, - serve_path, - expected, - factories, - api_client, - preferences, - settings): - headers = { - 'apache2': 'X-Sendfile', - 'nginx': 'X-Accel-Redirect', - } - preferences['common__api_authentication_required'] = False - settings.PROTECT_FILE_PATH = '/_protected/music' + proxy, serve_path, expected, factories, api_client, preferences, settings +): + headers = {"apache2": "X-Sendfile", "nginx": "X-Accel-Redirect"} + preferences["common__api_authentication_required"] = False + settings.PROTECT_FILE_PATH = "/_protected/music" settings.REVERSE_PROXY_TYPE = proxy - settings.MUSIC_DIRECTORY_PATH = '/app/music' + settings.MUSIC_DIRECTORY_PATH = "/app/music" settings.MUSIC_DIRECTORY_SERVE_PATH = serve_path - tf = factories['music.TrackFile']( - in_place=True, - source='file:///app/music/hello/world.mp3' + tf = factories["music.TrackFile"]( + in_place=True, source="file:///app/music/hello/world.mp3" ) response = api_client.get(tf.path) @@ -178,86 +156,76 @@ def test_serve_file_in_place( assert response[headers[proxy]] == expected -@pytest.mark.parametrize('proxy,serve_path,expected', [ - ('apache2', '/host/music', '/host/music/hello/worldéà.mp3'), - ('apache2', '/app/music', '/app/music/hello/worldéà.mp3'), - ('nginx', '/host/music', '/_protected/music/hello/worldéà.mp3'), - ('nginx', '/app/music', '/_protected/music/hello/worldéà.mp3'), -]) +@pytest.mark.parametrize( + "proxy,serve_path,expected", + [ + ("apache2", "/host/music", "/host/music/hello/worldéà.mp3"), + ("apache2", "/app/music", "/app/music/hello/worldéà.mp3"), + ("nginx", "/host/music", "/_protected/music/hello/worldéà.mp3"), + ("nginx", "/app/music", "/_protected/music/hello/worldéà.mp3"), + ], +) def test_serve_file_in_place_utf8( - proxy, - serve_path, - expected, - factories, - api_client, - settings, - preferences): - preferences['common__api_authentication_required'] = False - settings.PROTECT_FILE_PATH = '/_protected/music' + proxy, serve_path, expected, factories, api_client, settings, preferences +): + preferences["common__api_authentication_required"] = False + settings.PROTECT_FILE_PATH = "/_protected/music" settings.REVERSE_PROXY_TYPE = proxy - settings.MUSIC_DIRECTORY_PATH = '/app/music' + settings.MUSIC_DIRECTORY_PATH = "/app/music" settings.MUSIC_DIRECTORY_SERVE_PATH = serve_path - path = views.get_file_path('/app/music/hello/worldéà.mp3') + path = views.get_file_path("/app/music/hello/worldéà.mp3") - assert path == expected.encode('utf-8') + assert path == expected.encode("utf-8") -@pytest.mark.parametrize('proxy,serve_path,expected', [ - ('apache2', '/host/music', '/host/media/tracks/hello/world.mp3'), - # apache with container not supported yet - # ('apache2', '/app/music', '/app/music/tracks/hello/world.mp3'), - ('nginx', '/host/music', '/_protected/media/tracks/hello/world.mp3'), - ('nginx', '/app/music', '/_protected/media/tracks/hello/world.mp3'), -]) +@pytest.mark.parametrize( + "proxy,serve_path,expected", + [ + ("apache2", "/host/music", "/host/media/tracks/hello/world.mp3"), + # apache with container not supported yet + # ('apache2', '/app/music', '/app/music/tracks/hello/world.mp3'), + ("nginx", "/host/music", "/_protected/media/tracks/hello/world.mp3"), + ("nginx", "/app/music", "/_protected/media/tracks/hello/world.mp3"), + ], +) def test_serve_file_media( - proxy, - serve_path, - expected, - factories, - api_client, - settings, - preferences): - headers = { - 'apache2': 'X-Sendfile', - 'nginx': 'X-Accel-Redirect', - } - preferences['common__api_authentication_required'] = False - settings.MEDIA_ROOT = '/host/media' - settings.PROTECT_FILE_PATH = '/_protected/music' + proxy, serve_path, expected, factories, api_client, settings, preferences +): + headers = {"apache2": "X-Sendfile", "nginx": "X-Accel-Redirect"} + preferences["common__api_authentication_required"] = False + settings.MEDIA_ROOT = "/host/media" + settings.PROTECT_FILE_PATH = "/_protected/music" settings.REVERSE_PROXY_TYPE = proxy - settings.MUSIC_DIRECTORY_PATH = '/app/music' + settings.MUSIC_DIRECTORY_PATH = "/app/music" settings.MUSIC_DIRECTORY_SERVE_PATH = serve_path - tf = factories['music.TrackFile']() - tf.__class__.objects.filter(pk=tf.pk).update( - audio_file='tracks/hello/world.mp3') + tf = factories["music.TrackFile"]() + tf.__class__.objects.filter(pk=tf.pk).update(audio_file="tracks/hello/world.mp3") response = api_client.get(tf.path) assert response.status_code == 200 assert response[headers[proxy]] == expected -def test_can_proxy_remote_track( - factories, settings, api_client, r_mock, preferences): - preferences['common__api_authentication_required'] = False - track_file = factories['music.TrackFile'](federation=True) +def test_can_proxy_remote_track(factories, settings, api_client, r_mock, preferences): + preferences["common__api_authentication_required"] = False + track_file = factories["music.TrackFile"](federation=True) - r_mock.get(track_file.library_track.audio_url, body=io.BytesIO(b'test')) + r_mock.get(track_file.library_track.audio_url, body=io.BytesIO(b"test")) response = api_client.get(track_file.path) library_track = track_file.library_track library_track.refresh_from_db() assert response.status_code == 200 - assert response['X-Accel-Redirect'] == "{}{}".format( - settings.PROTECT_FILES_PATH, - library_track.audio_file.url) - assert library_track.audio_file.read() == b'test' + assert response["X-Accel-Redirect"] == "{}{}".format( + settings.PROTECT_FILES_PATH, library_track.audio_file.url + ) + assert library_track.audio_file.read() == b"test" -def test_serve_updates_access_date( - factories, settings, api_client, preferences): - preferences['common__api_authentication_required'] = False - track_file = factories['music.TrackFile']() +def test_serve_updates_access_date(factories, settings, api_client, preferences): + preferences["common__api_authentication_required"] = False + track_file = factories["music.TrackFile"]() now = timezone.now() assert track_file.accessed_date is None @@ -269,128 +237,118 @@ def test_serve_updates_access_date( def test_can_list_import_jobs(factories, superuser_api_client): - job = factories['music.ImportJob']() - url = reverse('api:v1:import-jobs-list') + job = factories["music.ImportJob"]() + url = reverse("api:v1:import-jobs-list") response = superuser_api_client.get(url) assert response.status_code == 200 - assert response.data['results'][0]['id'] == job.pk + assert response.data["results"][0]["id"] == job.pk def test_import_job_stats(factories, superuser_api_client): - job1 = factories['music.ImportJob'](status='pending') - job2 = factories['music.ImportJob'](status='errored') + job1 = factories["music.ImportJob"](status="pending") + job2 = factories["music.ImportJob"](status="errored") - url = reverse('api:v1:import-jobs-stats') + url = reverse("api:v1:import-jobs-stats") response = superuser_api_client.get(url) - expected = { - 'errored': 1, - 'pending': 1, - 'finished': 0, - 'skipped': 0, - 'count': 2, - } + expected = {"errored": 1, "pending": 1, "finished": 0, "skipped": 0, "count": 2} assert response.status_code == 200 assert response.data == expected def test_import_job_stats_filter(factories, superuser_api_client): - job1 = factories['music.ImportJob'](status='pending') - job2 = factories['music.ImportJob'](status='errored') + job1 = factories["music.ImportJob"](status="pending") + job2 = factories["music.ImportJob"](status="errored") - url = reverse('api:v1:import-jobs-stats') - response = superuser_api_client.get(url, {'batch': job1.batch.pk}) - expected = { - 'errored': 0, - 'pending': 1, - 'finished': 0, - 'skipped': 0, - 'count': 1, - } + url = reverse("api:v1:import-jobs-stats") + response = superuser_api_client.get(url, {"batch": job1.batch.pk}) + expected = {"errored": 0, "pending": 1, "finished": 0, "skipped": 0, "count": 1} assert response.status_code == 200 assert response.data == expected def test_import_job_run_via_api(factories, superuser_api_client, mocker): - run = mocker.patch('funkwhale_api.music.tasks.import_job_run.delay') - job1 = factories['music.ImportJob'](status='errored') - job2 = factories['music.ImportJob'](status='pending') + run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay") + job1 = factories["music.ImportJob"](status="errored") + job2 = factories["music.ImportJob"](status="pending") - url = reverse('api:v1:import-jobs-run') - response = superuser_api_client.post(url, {'jobs': [job2.pk, job1.pk]}) + url = reverse("api:v1:import-jobs-run") + response = superuser_api_client.post(url, {"jobs": [job2.pk, job1.pk]}) job1.refresh_from_db() job2.refresh_from_db() assert response.status_code == 200 - assert response.data == {'jobs': [job1.pk, job2.pk]} - assert job1.status == 'pending' - assert job2.status == 'pending' + assert response.data == {"jobs": [job1.pk, job2.pk]} + assert job1.status == "pending" + assert job2.status == "pending" run.assert_any_call(import_job_id=job1.pk) run.assert_any_call(import_job_id=job2.pk) def test_import_batch_run_via_api(factories, superuser_api_client, mocker): - run = mocker.patch('funkwhale_api.music.tasks.import_job_run.delay') + run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay") - batch = factories['music.ImportBatch']() - job1 = factories['music.ImportJob'](batch=batch, status='errored') - job2 = factories['music.ImportJob'](batch=batch, status='pending') + batch = factories["music.ImportBatch"]() + job1 = factories["music.ImportJob"](batch=batch, status="errored") + job2 = factories["music.ImportJob"](batch=batch, status="pending") - url = reverse('api:v1:import-jobs-run') - response = superuser_api_client.post(url, {'batches': [batch.pk]}) + url = reverse("api:v1:import-jobs-run") + response = superuser_api_client.post(url, {"batches": [batch.pk]}) job1.refresh_from_db() job2.refresh_from_db() assert response.status_code == 200 - assert job1.status == 'pending' - assert job2.status == 'pending' + assert job1.status == "pending" + assert job2.status == "pending" run.assert_any_call(import_job_id=job1.pk) run.assert_any_call(import_job_id=job2.pk) -def test_import_batch_and_job_run_via_api( - factories, superuser_api_client, mocker): - run = mocker.patch('funkwhale_api.music.tasks.import_job_run.delay') +def test_import_batch_and_job_run_via_api(factories, superuser_api_client, mocker): + run = mocker.patch("funkwhale_api.music.tasks.import_job_run.delay") - batch = factories['music.ImportBatch']() - job1 = factories['music.ImportJob'](batch=batch, status='errored') - job2 = factories['music.ImportJob'](status='pending') + batch = factories["music.ImportBatch"]() + job1 = factories["music.ImportJob"](batch=batch, status="errored") + job2 = factories["music.ImportJob"](status="pending") - url = reverse('api:v1:import-jobs-run') + url = reverse("api:v1:import-jobs-run") response = superuser_api_client.post( - url, {'batches': [batch.pk], 'jobs': [job2.pk]}) + url, {"batches": [batch.pk], "jobs": [job2.pk]} + ) job1.refresh_from_db() job2.refresh_from_db() assert response.status_code == 200 - assert job1.status == 'pending' - assert job2.status == 'pending' + assert job1.status == "pending" + assert job2.status == "pending" run.assert_any_call(import_job_id=job1.pk) run.assert_any_call(import_job_id=job2.pk) def test_import_job_viewset_get_queryset_upload_filters_user( - factories, logged_in_api_client): + factories, logged_in_api_client +): logged_in_api_client.user.permission_upload = True logged_in_api_client.user.save() - job = factories['music.ImportJob']() - url = reverse('api:v1:import-jobs-list') + job = factories["music.ImportJob"]() + url = reverse("api:v1:import-jobs-list") response = logged_in_api_client.get(url) - assert response.data['count'] == 0 + assert response.data["count"] == 0 def test_import_batch_viewset_get_queryset_upload_filters_user( - factories, logged_in_api_client): + factories, logged_in_api_client +): logged_in_api_client.user.permission_upload = True logged_in_api_client.user.save() - job = factories['music.ImportBatch']() - url = reverse('api:v1:import-batches-list') + job = factories["music.ImportBatch"]() + url = reverse("api:v1:import-batches-list") response = logged_in_api_client.get(url) - assert response.data['count'] == 0 + assert response.data["count"] == 0 diff --git a/api/tests/music/test_works.py b/api/tests/music/test_works.py index 13f6447be..6b2e57fdc 100644 --- a/api/tests/music/test_works.py +++ b/api/tests/music/test_works.py @@ -8,16 +8,16 @@ from funkwhale_api.music import serializers def test_can_import_work(factories, mocker, works): mocker.patch( - 'funkwhale_api.musicbrainz.api.works.get', - return_value=works['get']['chop_suey']) - recording = factories['music.Track']( - mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448') - mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5' + "funkwhale_api.musicbrainz.api.works.get", + return_value=works["get"]["chop_suey"], + ) + recording = factories["music.Track"](mbid="07ca77cf-f513-4e9c-b190-d7e24bbad448") + mbid = "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5" work = models.Work.create_from_api(id=mbid) - assert work.title == 'Chop Suey!' - assert work.nature == 'song' - assert work.language == 'eng' + assert work.title == "Chop Suey!" + assert work.nature == "song" + assert work.language == "eng" assert work.mbid == mbid # a imported work should also be linked to corresponding recordings @@ -28,23 +28,25 @@ def test_can_import_work(factories, mocker, works): def test_can_get_work_from_recording(factories, mocker, works, tracks): mocker.patch( - 'funkwhale_api.musicbrainz.api.works.get', - return_value=works['get']['chop_suey']) + "funkwhale_api.musicbrainz.api.works.get", + return_value=works["get"]["chop_suey"], + ) mocker.patch( - 'funkwhale_api.musicbrainz.api.recordings.get', - return_value=tracks['get']['chop_suey']) - recording = factories['music.Track']( - work=None, - mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448') - mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5' + "funkwhale_api.musicbrainz.api.recordings.get", + return_value=tracks["get"]["chop_suey"], + ) + recording = factories["music.Track"]( + work=None, mbid="07ca77cf-f513-4e9c-b190-d7e24bbad448" + ) + mbid = "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5" assert recording.work == None work = recording.get_work() - assert work.title == 'Chop Suey!' - assert work.nature == 'song' - assert work.language == 'eng' + assert work.title == "Chop Suey!" + assert work.nature == "song" + assert work.language == "eng" assert work.mbid == mbid recording.refresh_from_db() @@ -53,11 +55,12 @@ def test_can_get_work_from_recording(factories, mocker, works, tracks): def test_works_import_lyrics_if_any(db, mocker, works): mocker.patch( - 'funkwhale_api.musicbrainz.api.works.get', - return_value=works['get']['chop_suey']) - mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5' + "funkwhale_api.musicbrainz.api.works.get", + return_value=works["get"]["chop_suey"], + ) + mbid = "e2ecabc4-1b9d-30b2-8f30-3596ec423dc5" work = models.Work.create_from_api(id=mbid) - lyrics = models.Lyrics.objects.latest('id') + lyrics = models.Lyrics.objects.latest("id") assert lyrics.work == work - assert lyrics.url == 'http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!' + assert lyrics.url == "http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!" diff --git a/api/tests/musicbrainz/conftest.py b/api/tests/musicbrainz/conftest.py index 505d6e553..3e3ebfa48 100644 --- a/api/tests/musicbrainz/conftest.py +++ b/api/tests/musicbrainz/conftest.py @@ -1,33 +1,28 @@ import pytest -_artists = {'search': {}, 'get': {}} -_artists['search']['lost fingers'] = { - 'artist-count': 696, - 'artist-list': [ +_artists = {"search": {}, "get": {}} +_artists["search"]["lost fingers"] = { + "artist-count": 696, + "artist-list": [ { - 'country': 'CA', - 'sort-name': 'Lost Fingers, The', - 'id': 'ac16bbc0-aded-4477-a3c3-1d81693d58c9', - 'type': 'Group', - 'life-span': { - 'ended': 'false', - 'begin': '2008' + "country": "CA", + "sort-name": "Lost Fingers, The", + "id": "ac16bbc0-aded-4477-a3c3-1d81693d58c9", + "type": "Group", + "life-span": {"ended": "false", "begin": "2008"}, + "area": { + "sort-name": "Canada", + "id": "71bbafaa-e825-3e15-8ca9-017dcad1748b", + "name": "Canada", }, - 'area': { - 'sort-name': 'Canada', - 'id': '71bbafaa-e825-3e15-8ca9-017dcad1748b', - 'name': 'Canada' - }, - 'ext:score': '100', - 'name': 'The Lost Fingers' - }, - ] + "ext:score": "100", + "name": "The Lost Fingers", + } + ], } -_artists['get']['lost fingers'] = { +_artists["get"]["lost fingers"] = { "artist": { - "life-span": { - "begin": "2008" - }, + "life-span": {"begin": "2008"}, "type": "Group", "id": "ac16bbc0-aded-4477-a3c3-1d81693d58c9", "release-group-count": 8, @@ -38,137 +33,135 @@ _artists['get']['lost fingers'] = { "first-release-date": "2010", "type": "Album", "id": "03d3f1d4-e2b0-40d3-8314-05f1896e93a0", - "primary-type": "Album" + "primary-type": "Album", }, { "title": "Gitan Kameleon", "first-release-date": "2011-11-11", "type": "Album", "id": "243c0cd2-2492-4f5d-bf37-c7c76bed05b7", - "primary-type": "Album" + "primary-type": "Album", }, { "title": "Pump Up the Jam \u2013 Do Not Cover, Pt. 3", "first-release-date": "2014-03-17", "type": "Single", "id": "4429befd-ff45-48eb-a8f4-cdf7bf007f3f", - "primary-type": "Single" + "primary-type": "Single", }, { "title": "La Marquise", "first-release-date": "2012-03-27", "type": "Album", "id": "4dab4b96-0a6b-4507-a31e-2189e3e7bad1", - "primary-type": "Album" + "primary-type": "Album", }, { "title": "Christmas Caravan", "first-release-date": "2016-11-11", "type": "Album", "id": "ca0a506d-6ba9-47c3-a712-de5ce9ae6b1f", - "primary-type": "Album" + "primary-type": "Album", }, { "title": "Rendez-vous rose", "first-release-date": "2009-06-16", "type": "Album", "id": "d002f1a8-5890-4188-be58-1caadbbd767f", - "primary-type": "Album" + "primary-type": "Album", }, { "title": "Wonders of the World", "first-release-date": "2014-05-06", "type": "Album", "id": "eeb644c2-5000-42fb-b959-e5e9cc2901c5", - "primary-type": "Album" + "primary-type": "Album", }, { "title": "Lost in the 80s", "first-release-date": "2008-05-06", "type": "Album", "id": "f04ed607-11b7-3843-957e-503ecdd485d1", - "primary-type": "Album" - } + "primary-type": "Album", + }, ], "area": { - "iso-3166-1-code-list": [ - "CA" - ], + "iso-3166-1-code-list": ["CA"], "name": "Canada", "id": "71bbafaa-e825-3e15-8ca9-017dcad1748b", - "sort-name": "Canada" + "sort-name": "Canada", }, "sort-name": "Lost Fingers, The", - "country": "CA" + "country": "CA", } } -_release_groups = {'browse': {}} -_release_groups['browse']["lost fingers"] = { +_release_groups = {"browse": {}} +_release_groups["browse"]["lost fingers"] = { "release-group-list": [ { "first-release-date": "2010", "type": "Album", "primary-type": "Album", "title": "Gypsy Kameleon", - "id": "03d3f1d4-e2b0-40d3-8314-05f1896e93a0" + "id": "03d3f1d4-e2b0-40d3-8314-05f1896e93a0", }, { "first-release-date": "2011-11-11", "type": "Album", "primary-type": "Album", "title": "Gitan Kameleon", - "id": "243c0cd2-2492-4f5d-bf37-c7c76bed05b7" + "id": "243c0cd2-2492-4f5d-bf37-c7c76bed05b7", }, { "first-release-date": "2014-03-17", "type": "Single", "primary-type": "Single", "title": "Pump Up the Jam \u2013 Do Not Cover, Pt. 3", - "id": "4429befd-ff45-48eb-a8f4-cdf7bf007f3f" + "id": "4429befd-ff45-48eb-a8f4-cdf7bf007f3f", }, { "first-release-date": "2012-03-27", "type": "Album", "primary-type": "Album", "title": "La Marquise", - "id": "4dab4b96-0a6b-4507-a31e-2189e3e7bad1" + "id": "4dab4b96-0a6b-4507-a31e-2189e3e7bad1", }, { "first-release-date": "2016-11-11", "type": "Album", "primary-type": "Album", "title": "Christmas Caravan", - "id": "ca0a506d-6ba9-47c3-a712-de5ce9ae6b1f" + "id": "ca0a506d-6ba9-47c3-a712-de5ce9ae6b1f", }, { "first-release-date": "2009-06-16", "type": "Album", "primary-type": "Album", "title": "Rendez-vous rose", - "id": "d002f1a8-5890-4188-be58-1caadbbd767f" + "id": "d002f1a8-5890-4188-be58-1caadbbd767f", }, { "first-release-date": "2014-05-06", "type": "Album", "primary-type": "Album", "title": "Wonders of the World", - "id": "eeb644c2-5000-42fb-b959-e5e9cc2901c5" + "id": "eeb644c2-5000-42fb-b959-e5e9cc2901c5", }, { "first-release-date": "2008-05-06", "type": "Album", "primary-type": "Album", "title": "Lost in the 80s", - "id": "f04ed607-11b7-3843-957e-503ecdd485d1" - } + "id": "f04ed607-11b7-3843-957e-503ecdd485d1", + }, ], - "release-group-count": 8 + "release-group-count": 8, } -_recordings = {'search': {}, 'get': {}} -_recordings['search']['brontide matador'] = { +_recordings = {"search": {}, "get": {}} +_recordings["search"]["brontide matador"] = { "recording-count": 1044, "recording-list": [ { @@ -184,9 +177,9 @@ _recordings['search']['brontide matador'] = { "name": "United Kingdom", "sort-name": "United Kingdom", "id": "8a754a16-0027-3a29-b6d7-2b40ea0481ed", - "iso-3166-1-code-list": ["GB"] + "iso-3166-1-code-list": ["GB"], }, - "date": "2011-05-30" + "date": "2011-05-30", } ], "country": "GB", @@ -196,7 +189,7 @@ _recordings['search']['brontide matador'] = { "release-group": { "type": "Album", "id": "113ab958-cfb8-4782-99af-639d4d9eae8d", - "primary-type": "Album" + "primary-type": "Album", }, "medium-list": [ { @@ -206,22 +199,24 @@ _recordings['search']['brontide matador'] = { "track_or_recording_length": "366280", "id": "fe506782-a5cb-3d89-9b3e-86287be05768", "length": "366280", - "title": "Matador", "number": "1" + "title": "Matador", + "number": "1", } ], "position": "1", - "track-count": 8 + "track-count": 8, } - ] - }, - ] + ], + } + ], } - ] + ], } -_releases = {'search': {}, 'get': {}, 'browse': {}} -_releases['search']['brontide matador'] = { - "release-count": 116, "release-list": [ +_releases = {"search": {}, "get": {}, "browse": {}} +_releases["search"]["brontide matador"] = { + "release-count": 116, + "release-list": [ { "ext:score": "100", "date": "2009-04-02", @@ -231,16 +226,16 @@ _releases['search']['brontide matador'] = { "name": "[Worldwide]", "sort-name": "[Worldwide]", "id": "525d4e18-3d00-31b9-a58b-a146a916de8f", - "iso-3166-1-code-list": ["XW"] + "iso-3166-1-code-list": ["XW"], }, - "date": "2009-04-02" + "date": "2009-04-02", } ], "label-info-list": [ { "label": { "name": "Holy Roar", - "id": "6e940f35-961d-4ac3-bc2a-569fc211c2e3" + "id": "6e940f35-961d-4ac3-bc2a-569fc211c2e3", } } ], @@ -251,7 +246,7 @@ _releases['search']['brontide matador'] = { "artist": { "name": "Brontide", "sort-name": "Brontide", - "id": "2179fbd2-3c88-4b94-a778-eb3daf1e81a1" + "id": "2179fbd2-3c88-4b94-a778-eb3daf1e81a1", } } ], @@ -265,7 +260,7 @@ _releases['search']['brontide matador'] = { "type": "EP", "secondary-type-list": ["Demo"], "id": "b9207129-2d03-4a68-8a53-3c46fe7d2810", - "primary-type": "EP" + "primary-type": "EP", }, "medium-list": [ { @@ -273,28 +268,22 @@ _releases['search']['brontide matador'] = { "format": "Digital Media", "disc-count": 0, "track-count": 3, - "track-list": [] + "track-list": [], } ], "medium-count": 1, - "text-representation": { - "script": "Latn", - "language": "eng" - } - }, - ] + "text-representation": {"script": "Latn", "language": "eng"}, + } + ], } -_releases['browse']['Lost in the 80s'] = { +_releases["browse"]["Lost in the 80s"] = { "release-count": 3, "release-list": [ { "quality": "normal", "status": "Official", - "text-representation": { - "script": "Latn", - "language": "eng" - }, + "text-representation": {"script": "Latn", "language": "eng"}, "title": "Lost in the 80s", "date": "2008-05-06", "release-event-count": 1, @@ -304,14 +293,12 @@ _releases['browse']['Lost in the 80s'] = { "release-event-list": [ { "area": { - "iso-3166-1-code-list": [ - "CA" - ], + "iso-3166-1-code-list": ["CA"], "id": "71bbafaa-e825-3e15-8ca9-017dcad1748b", "name": "Canada", - "sort-name": "Canada" + "sort-name": "Canada", }, - "date": "2008-05-06" + "date": "2008-05-06", } ], "country": "CA", @@ -319,7 +306,7 @@ _releases['browse']['Lost in the 80s'] = { "back": "false", "artwork": "false", "front": "false", - "count": "0" + "count": "0", }, "medium-list": [ { @@ -333,11 +320,11 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "2e0dbf37-65af-4408-8def-7b0b3cb8426b", "length": "228000", - "title": "Pump Up the Jam" + "title": "Pump Up the Jam", }, "track_or_recording_length": "228000", "position": "1", - "number": "1" + "number": "1", }, { "id": "01a8cf99-2170-3d3f-96ef-5e4ef7a015a4", @@ -345,11 +332,11 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "57017e2e-625d-4e7b-a445-47cdb0224dd2", "length": "231000", - "title": "You Give Love a Bad Name" + "title": "You Give Love a Bad Name", }, "track_or_recording_length": "231000", "position": "2", - "number": "2" + "number": "2", }, { "id": "375a7ce7-5a41-3fbf-9809-96d491401034", @@ -357,11 +344,11 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "a948672b-b42d-44a5-89b0-7e9ab6a7e11d", "length": "189000", - "title": "You Shook Me All Night Long" + "title": "You Shook Me All Night Long", }, "track_or_recording_length": "189000", "position": "3", - "number": "3" + "number": "3", }, { "id": "ed7d823e-76da-31be-82a8-770288e27d32", @@ -369,11 +356,11 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "6e097e31-f37b-4fae-8ad0-ada57f3091a7", "length": "253000", - "title": "Incognito" + "title": "Incognito", }, "track_or_recording_length": "253000", "position": "4", - "number": "4" + "number": "4", }, { "id": "76ac8c77-6a99-34d9-ae4d-be8f056d50e0", @@ -381,11 +368,11 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "faa922e6-e834-44ee-8125-79e640a690e3", "length": "221000", - "title": "Touch Me" + "title": "Touch Me", }, "track_or_recording_length": "221000", "position": "5", - "number": "5" + "number": "5", }, { "id": "d0a87409-2be6-3ab7-8526-4313e7134be1", @@ -393,11 +380,11 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "02da8148-60d8-4c79-ab31-8d90d233d711", "length": "228000", - "title": "Part-Time Lover" + "title": "Part-Time Lover", }, "track_or_recording_length": "228000", "position": "6", - "number": "6" + "number": "6", }, { "id": "02c5384b-5ca9-38e9-8b7c-c08dce608deb", @@ -405,11 +392,11 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "40085704-d6ab-44f6-a4d8-b27c9ca25b31", "length": "248000", - "title": "Fresh" + "title": "Fresh", }, "track_or_recording_length": "248000", "position": "7", - "number": "7" + "number": "7", }, { "id": "ab389542-53d5-346a-b168-1d915ecf0ef6", @@ -417,11 +404,11 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "77edd338-eeaf-4157-9e2a-5cc3bcee8abd", "length": "257000", - "title": "Billie Jean" + "title": "Billie Jean", }, "track_or_recording_length": "257000", "position": "8", - "number": "8" + "number": "8", }, { "id": "6d9e722b-7408-350e-bb7c-2de1e329ae84", @@ -429,11 +416,11 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "040aaffa-7206-40ff-9930-469413fe2420", "length": "293000", - "title": "Careless Whisper" + "title": "Careless Whisper", }, "track_or_recording_length": "293000", "position": "9", - "number": "9" + "number": "9", }, { "id": "63b4e67c-7536-3cd0-8c47-0310c1e40866", @@ -441,11 +428,11 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "054942f0-4c0f-4e92-a606-d590976b1cff", "length": "211000", - "title": "Tainted Love" + "title": "Tainted Love", }, "track_or_recording_length": "211000", "position": "10", - "number": "10" + "number": "10", }, { "id": "a07f4ca3-dbf0-3337-a247-afcd0509334a", @@ -453,11 +440,11 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "8023b5ad-649a-4c67-b7a2-e12358606f6e", "length": "245000", - "title": "Straight Up" + "title": "Straight Up", }, "track_or_recording_length": "245000", "position": "11", - "number": "11" + "number": "11", }, { "id": "73d47f16-b18d-36ff-b0bb-1fa1fd32ebf7", @@ -465,18 +452,18 @@ _releases['browse']['Lost in the 80s'] = { "recording": { "id": "95a8c8a1-fcb6-4cbb-a853-be86d816b357", "length": "322000", - "title": "Black Velvet" + "title": "Black Velvet", }, "track_or_recording_length": "322000", "position": "12", - "number": "12" - } - ] + "number": "12", + }, + ], } ], - "asin": "B0017M8YTO" - }, - ] + "asin": "B0017M8YTO", + } + ], } diff --git a/api/tests/musicbrainz/test_api.py b/api/tests/musicbrainz/test_api.py index fdd1dbdb0..de84813f1 100644 --- a/api/tests/musicbrainz/test_api.py +++ b/api/tests/musicbrainz/test_api.py @@ -4,89 +4,95 @@ from django.urls import reverse from funkwhale_api.musicbrainz import api - def test_can_search_recording_in_musicbrainz_api( - recordings, db, mocker, logged_in_api_client): + recordings, db, mocker, logged_in_api_client +): mocker.patch( - 'funkwhale_api.musicbrainz.api.recordings.search', - return_value=recordings['search']['brontide matador']) - query = 'brontide matador' - url = reverse('api:v1:providers:musicbrainz:search-recordings') - expected = recordings['search']['brontide matador'] - response = logged_in_api_client.get(url, data={'query': query}) + "funkwhale_api.musicbrainz.api.recordings.search", + return_value=recordings["search"]["brontide matador"], + ) + query = "brontide matador" + url = reverse("api:v1:providers:musicbrainz:search-recordings") + expected = recordings["search"]["brontide matador"] + response = logged_in_api_client.get(url, data={"query": query}) assert expected == response.data -def test_can_search_release_in_musicbrainz_api(releases, db, mocker, logged_in_api_client): +def test_can_search_release_in_musicbrainz_api( + releases, db, mocker, logged_in_api_client +): mocker.patch( - 'funkwhale_api.musicbrainz.api.releases.search', - return_value=releases['search']['brontide matador']) - query = 'brontide matador' - url = reverse('api:v1:providers:musicbrainz:search-releases') - expected = releases['search']['brontide matador'] - response = logged_in_api_client.get(url, data={'query': query}) + "funkwhale_api.musicbrainz.api.releases.search", + return_value=releases["search"]["brontide matador"], + ) + query = "brontide matador" + url = reverse("api:v1:providers:musicbrainz:search-releases") + expected = releases["search"]["brontide matador"] + response = logged_in_api_client.get(url, data={"query": query}) assert expected == response.data -def test_can_search_artists_in_musicbrainz_api(artists, db, mocker, logged_in_api_client): +def test_can_search_artists_in_musicbrainz_api( + artists, db, mocker, logged_in_api_client +): mocker.patch( - 'funkwhale_api.musicbrainz.api.artists.search', - return_value=artists['search']['lost fingers']) - query = 'lost fingers' - url = reverse('api:v1:providers:musicbrainz:search-artists') - expected = artists['search']['lost fingers'] - response = logged_in_api_client.get(url, data={'query': query}) + "funkwhale_api.musicbrainz.api.artists.search", + return_value=artists["search"]["lost fingers"], + ) + query = "lost fingers" + url = reverse("api:v1:providers:musicbrainz:search-artists") + expected = artists["search"]["lost fingers"] + response = logged_in_api_client.get(url, data={"query": query}) assert expected == response.data def test_can_get_artist_in_musicbrainz_api(artists, db, mocker, logged_in_api_client): mocker.patch( - 'funkwhale_api.musicbrainz.api.artists.get', - return_value=artists['get']['lost fingers']) - uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9' - url = reverse('api:v1:providers:musicbrainz:artist-detail', kwargs={ - 'uuid': uuid, - }) + "funkwhale_api.musicbrainz.api.artists.get", + return_value=artists["get"]["lost fingers"], + ) + uuid = "ac16bbc0-aded-4477-a3c3-1d81693d58c9" + url = reverse("api:v1:providers:musicbrainz:artist-detail", kwargs={"uuid": uuid}) response = logged_in_api_client.get(url) - expected = artists['get']['lost fingers'] + expected = artists["get"]["lost fingers"] assert expected == response.data def test_can_broswe_release_group_using_musicbrainz_api( - release_groups, db, mocker, logged_in_api_client): + release_groups, db, mocker, logged_in_api_client +): mocker.patch( - 'funkwhale_api.musicbrainz.api.release_groups.browse', - return_value=release_groups['browse']['lost fingers']) - uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9' + "funkwhale_api.musicbrainz.api.release_groups.browse", + return_value=release_groups["browse"]["lost fingers"], + ) + uuid = "ac16bbc0-aded-4477-a3c3-1d81693d58c9" url = reverse( - 'api:v1:providers:musicbrainz:release-group-browse', - kwargs={ - 'artist_uuid': uuid, - } + "api:v1:providers:musicbrainz:release-group-browse", + kwargs={"artist_uuid": uuid}, ) response = logged_in_api_client.get(url) - expected = release_groups['browse']['lost fingers'] + expected = release_groups["browse"]["lost fingers"] assert expected == response.data def test_can_broswe_releases_using_musicbrainz_api( - releases, db, mocker, logged_in_api_client): + releases, db, mocker, logged_in_api_client +): mocker.patch( - 'funkwhale_api.musicbrainz.api.releases.browse', - return_value=releases['browse']['Lost in the 80s']) - uuid = 'f04ed607-11b7-3843-957e-503ecdd485d1' + "funkwhale_api.musicbrainz.api.releases.browse", + return_value=releases["browse"]["Lost in the 80s"], + ) + uuid = "f04ed607-11b7-3843-957e-503ecdd485d1" url = reverse( - 'api:v1:providers:musicbrainz:release-browse', - kwargs={ - 'release_group_uuid': uuid, - } + "api:v1:providers:musicbrainz:release-browse", + kwargs={"release_group_uuid": uuid}, ) response = logged_in_api_client.get(url) - expected = releases['browse']['Lost in the 80s'] + expected = releases["browse"]["Lost in the 80s"] assert expected == response.data diff --git a/api/tests/musicbrainz/test_cache.py b/api/tests/musicbrainz/test_cache.py index fe0d56773..3a326ff24 100644 --- a/api/tests/musicbrainz/test_cache.py +++ b/api/tests/musicbrainz/test_cache.py @@ -2,12 +2,12 @@ from funkwhale_api.musicbrainz import client def test_can_search_recording_in_musicbrainz_api(mocker): - r = {'hello': 'world'} + r = {"hello": "world"} m = mocker.patch( - 'funkwhale_api.musicbrainz.client._api.search_artists', - return_value=r) - assert client.api.artists.search('test') == r + "funkwhale_api.musicbrainz.client._api.search_artists", return_value=r + ) + assert client.api.artists.search("test") == r # now call from cache - assert client.api.artists.search('test') == r - assert client.api.artists.search('test') == r + assert client.api.artists.search("test") == r + assert client.api.artists.search("test") == r assert m.call_count == 1 diff --git a/api/tests/playlists/test_models.py b/api/tests/playlists/test_models.py index fe5dd40a8..3258e6d10 100644 --- a/api/tests/playlists/test_models.py +++ b/api/tests/playlists/test_models.py @@ -4,7 +4,7 @@ from rest_framework import exceptions def test_can_insert_plt(factories): - plt = factories['playlists.PlaylistTrack']() + plt = factories["playlists.PlaylistTrack"]() modification_date = plt.playlist.modification_date assert plt.index is None @@ -17,9 +17,8 @@ def test_can_insert_plt(factories): def test_insert_use_last_idx_by_default(factories): - playlist = factories['playlists.Playlist']() - plts = factories['playlists.PlaylistTrack'].create_batch( - size=3, playlist=playlist) + playlist = factories["playlists.Playlist"]() + plts = factories["playlists.PlaylistTrack"].create_batch(size=3, playlist=playlist) for i, plt in enumerate(plts): index = playlist.insert(plt) @@ -28,11 +27,12 @@ def test_insert_use_last_idx_by_default(factories): assert index == i assert plt.index == i + def test_can_insert_at_index(factories): - playlist = factories['playlists.Playlist']() - first = factories['playlists.PlaylistTrack'](playlist=playlist) + playlist = factories["playlists.Playlist"]() + first = factories["playlists.PlaylistTrack"](playlist=playlist) playlist.insert(first) - new_first = factories['playlists.PlaylistTrack'](playlist=playlist) + new_first = factories["playlists.PlaylistTrack"](playlist=playlist) index = playlist.insert(new_first, index=0) first.refresh_from_db() new_first.refresh_from_db() @@ -43,10 +43,10 @@ def test_can_insert_at_index(factories): def test_can_insert_and_move(factories): - playlist = factories['playlists.Playlist']() - first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0) - second = factories['playlists.PlaylistTrack'](playlist=playlist, index=1) - third = factories['playlists.PlaylistTrack'](playlist=playlist, index=2) + playlist = factories["playlists.Playlist"]() + first = factories["playlists.PlaylistTrack"](playlist=playlist, index=0) + second = factories["playlists.PlaylistTrack"](playlist=playlist, index=1) + third = factories["playlists.PlaylistTrack"](playlist=playlist, index=2) playlist.insert(second, index=0) @@ -60,10 +60,10 @@ def test_can_insert_and_move(factories): def test_can_insert_and_move_last_to_0(factories): - playlist = factories['playlists.Playlist']() - first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0) - second = factories['playlists.PlaylistTrack'](playlist=playlist, index=1) - third = factories['playlists.PlaylistTrack'](playlist=playlist, index=2) + playlist = factories["playlists.Playlist"]() + first = factories["playlists.PlaylistTrack"](playlist=playlist, index=0) + second = factories["playlists.PlaylistTrack"](playlist=playlist, index=1) + third = factories["playlists.PlaylistTrack"](playlist=playlist, index=2) playlist.insert(third, index=0) @@ -77,24 +77,24 @@ def test_can_insert_and_move_last_to_0(factories): def test_cannot_insert_at_wrong_index(factories): - plt = factories['playlists.PlaylistTrack']() - new = factories['playlists.PlaylistTrack'](playlist=plt.playlist) + plt = factories["playlists.PlaylistTrack"]() + new = factories["playlists.PlaylistTrack"](playlist=plt.playlist) with pytest.raises(exceptions.ValidationError): plt.playlist.insert(new, 2) def test_cannot_insert_at_negative_index(factories): - plt = factories['playlists.PlaylistTrack']() - new = factories['playlists.PlaylistTrack'](playlist=plt.playlist) + plt = factories["playlists.PlaylistTrack"]() + new = factories["playlists.PlaylistTrack"](playlist=plt.playlist) with pytest.raises(exceptions.ValidationError): plt.playlist.insert(new, -1) def test_remove_update_indexes(factories): - playlist = factories['playlists.Playlist']() - first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0) - second = factories['playlists.PlaylistTrack'](playlist=playlist, index=1) - third = factories['playlists.PlaylistTrack'](playlist=playlist, index=2) + playlist = factories["playlists.Playlist"]() + first = factories["playlists.PlaylistTrack"](playlist=playlist, index=0) + second = factories["playlists.PlaylistTrack"](playlist=playlist, index=1) + third = factories["playlists.PlaylistTrack"](playlist=playlist, index=2) second.delete(update_indexes=True) @@ -106,9 +106,9 @@ def test_remove_update_indexes(factories): def test_can_insert_many(factories): - playlist = factories['playlists.Playlist']() - existing = factories['playlists.PlaylistTrack'](playlist=playlist, index=0) - tracks = factories['music.Track'].create_batch(size=3) + playlist = factories["playlists.Playlist"]() + existing = factories["playlists.PlaylistTrack"](playlist=playlist, index=0) + tracks = factories["music.Track"].create_batch(size=3) plts = playlist.insert_many(tracks) for i, plt in enumerate(plts): assert plt.index == i + 1 @@ -117,10 +117,9 @@ def test_can_insert_many(factories): def test_insert_many_honor_max_tracks(preferences, factories): - preferences['playlists__max_tracks'] = 4 - playlist = factories['playlists.Playlist']() - plts = factories['playlists.PlaylistTrack'].create_batch( - size=2, playlist=playlist) - track = factories['music.Track']() + preferences["playlists__max_tracks"] = 4 + playlist = factories["playlists.Playlist"]() + plts = factories["playlists.PlaylistTrack"].create_batch(size=2, playlist=playlist) + track = factories["music.Track"]() with pytest.raises(exceptions.ValidationError): playlist.insert_many([track, track, track]) diff --git a/api/tests/playlists/test_serializers.py b/api/tests/playlists/test_serializers.py index 908c1c796..05e5bd7ca 100644 --- a/api/tests/playlists/test_serializers.py +++ b/api/tests/playlists/test_serializers.py @@ -3,29 +3,25 @@ from funkwhale_api.playlists import serializers def test_cannot_max_500_tracks_per_playlist(factories, preferences): - preferences['playlists__max_tracks'] = 2 - playlist = factories['playlists.Playlist']() - plts = factories['playlists.PlaylistTrack'].create_batch( - size=2, playlist=playlist) - track = factories['music.Track']() - serializer = serializers.PlaylistTrackWriteSerializer(data={ - 'playlist': playlist.pk, - 'track': track.pk, - }) + preferences["playlists__max_tracks"] = 2 + playlist = factories["playlists.Playlist"]() + plts = factories["playlists.PlaylistTrack"].create_batch(size=2, playlist=playlist) + track = factories["music.Track"]() + serializer = serializers.PlaylistTrackWriteSerializer( + data={"playlist": playlist.pk, "track": track.pk} + ) assert serializer.is_valid() is False - assert 'playlist' in serializer.errors + assert "playlist" in serializer.errors def test_create_insert_is_called_when_index_is_None(factories, mocker): - insert = mocker.spy(models.Playlist, 'insert') - playlist = factories['playlists.Playlist']() - track = factories['music.Track']() - serializer = serializers.PlaylistTrackWriteSerializer(data={ - 'playlist': playlist.pk, - 'track': track.pk, - 'index': None, - }) + insert = mocker.spy(models.Playlist, "insert") + playlist = factories["playlists.Playlist"]() + track = factories["music.Track"]() + serializer = serializers.PlaylistTrackWriteSerializer( + data={"playlist": playlist.pk, "track": track.pk, "index": None} + ) assert serializer.is_valid() is True plt = serializer.save() @@ -34,16 +30,14 @@ def test_create_insert_is_called_when_index_is_None(factories, mocker): def test_create_insert_is_called_when_index_is_provided(factories, mocker): - playlist = factories['playlists.Playlist']() - first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0) - insert = mocker.spy(models.Playlist, 'insert') - factories['playlists.Playlist']() - track = factories['music.Track']() - serializer = serializers.PlaylistTrackWriteSerializer(data={ - 'playlist': playlist.pk, - 'track': track.pk, - 'index': 0, - }) + playlist = factories["playlists.Playlist"]() + first = factories["playlists.PlaylistTrack"](playlist=playlist, index=0) + insert = mocker.spy(models.Playlist, "insert") + factories["playlists.Playlist"]() + track = factories["music.Track"]() + serializer = serializers.PlaylistTrackWriteSerializer( + data={"playlist": playlist.pk, "track": track.pk, "index": 0} + ) assert serializer.is_valid() is True plt = serializer.save() @@ -54,17 +48,15 @@ def test_create_insert_is_called_when_index_is_provided(factories, mocker): def test_update_insert_is_called_when_index_is_provided(factories, mocker): - playlist = factories['playlists.Playlist']() - first = factories['playlists.PlaylistTrack'](playlist=playlist, index=0) - second = factories['playlists.PlaylistTrack'](playlist=playlist, index=1) - insert = mocker.spy(models.Playlist, 'insert') - factories['playlists.Playlist']() - track = factories['music.Track']() - serializer = serializers.PlaylistTrackWriteSerializer(second, data={ - 'playlist': playlist.pk, - 'track': second.track.pk, - 'index': 0, - }) + playlist = factories["playlists.Playlist"]() + first = factories["playlists.PlaylistTrack"](playlist=playlist, index=0) + second = factories["playlists.PlaylistTrack"](playlist=playlist, index=1) + insert = mocker.spy(models.Playlist, "insert") + factories["playlists.Playlist"]() + track = factories["music.Track"]() + serializer = serializers.PlaylistTrackWriteSerializer( + second, data={"playlist": playlist.pk, "track": second.track.pk, "index": 0} + ) assert serializer.is_valid() is True plt = serializer.save() diff --git a/api/tests/playlists/test_views.py b/api/tests/playlists/test_views.py index 44d060821..7835a7258 100644 --- a/api/tests/playlists/test_views.py +++ b/api/tests/playlists/test_views.py @@ -10,63 +10,55 @@ from funkwhale_api.playlists import serializers def test_can_create_playlist_via_api(logged_in_api_client): - url = reverse('api:v1:playlists-list') - data = { - 'name': 'test', - 'privacy_level': 'everyone' - } + url = reverse("api:v1:playlists-list") + data = {"name": "test", "privacy_level": "everyone"} response = logged_in_api_client.post(url, data) - playlist = logged_in_api_client.user.playlists.latest('id') - assert playlist.name == 'test' - assert playlist.privacy_level == 'everyone' + playlist = logged_in_api_client.user.playlists.latest("id") + assert playlist.name == "test" + assert playlist.privacy_level == "everyone" def test_serializer_includes_tracks_count(factories, logged_in_api_client): - playlist = factories['playlists.Playlist']() - plt = factories['playlists.PlaylistTrack'](playlist=playlist) + playlist = factories["playlists.Playlist"]() + plt = factories["playlists.PlaylistTrack"](playlist=playlist) - url = reverse('api:v1:playlists-detail', kwargs={'pk': playlist.pk}) + url = reverse("api:v1:playlists-detail", kwargs={"pk": playlist.pk}) response = logged_in_api_client.get(url) - assert response.data['tracks_count'] == 1 + assert response.data["tracks_count"] == 1 def test_playlist_inherits_user_privacy(logged_in_api_client): - url = reverse('api:v1:playlists-list') + url = reverse("api:v1:playlists-list") user = logged_in_api_client.user - user.privacy_level = 'me' + user.privacy_level = "me" user.save() - data = { - 'name': 'test', - } + data = {"name": "test"} response = logged_in_api_client.post(url, data) - playlist = user.playlists.latest('id') + playlist = user.playlists.latest("id") assert playlist.privacy_level == user.privacy_level def test_can_add_playlist_track_via_api(factories, logged_in_api_client): - tracks = factories['music.Track'].create_batch(5) - playlist = factories['playlists.Playlist'](user=logged_in_api_client.user) - url = reverse('api:v1:playlist-tracks-list') - data = { - 'playlist': playlist.pk, - 'track': tracks[0].pk - } + tracks = factories["music.Track"].create_batch(5) + playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) + url = reverse("api:v1:playlist-tracks-list") + data = {"playlist": playlist.pk, "track": tracks[0].pk} response = logged_in_api_client.post(url, data) assert response.status_code == 201 - plts = logged_in_api_client.user.playlists.latest('id').playlist_tracks.all() + plts = logged_in_api_client.user.playlists.latest("id").playlist_tracks.all() assert plts.first().track == tracks[0] -@pytest.mark.parametrize('name,method', [ - ('api:v1:playlist-tracks-list', 'post'), - ('api:v1:playlists-list', 'post'), -]) +@pytest.mark.parametrize( + "name,method", + [("api:v1:playlist-tracks-list", "post"), ("api:v1:playlists-list", "post")], +) def test_url_requires_login(name, method, factories, api_client): url = reverse(name) @@ -75,29 +67,24 @@ def test_url_requires_login(name, method, factories, api_client): assert response.status_code == 401 -def test_only_can_add_track_on_own_playlist_via_api( - factories, logged_in_api_client): - track = factories['music.Track']() - playlist = factories['playlists.Playlist']() - url = reverse('api:v1:playlist-tracks-list') - data = { - 'playlist': playlist.pk, - 'track': track.pk - } +def test_only_can_add_track_on_own_playlist_via_api(factories, logged_in_api_client): + track = factories["music.Track"]() + playlist = factories["playlists.Playlist"]() + url = reverse("api:v1:playlist-tracks-list") + data = {"playlist": playlist.pk, "track": track.pk} response = logged_in_api_client.post(url, data) assert response.status_code == 400 assert playlist.playlist_tracks.count() == 0 -def test_deleting_plt_updates_indexes( - mocker, factories, logged_in_api_client): - remove = mocker.spy(models.Playlist, 'remove') - track = factories['music.Track']() - plt = factories['playlists.PlaylistTrack']( - index=0, - playlist__user=logged_in_api_client.user) - url = reverse('api:v1:playlist-tracks-detail', kwargs={'pk': plt.pk}) +def test_deleting_plt_updates_indexes(mocker, factories, logged_in_api_client): + remove = mocker.spy(models.Playlist, "remove") + track = factories["music.Track"]() + plt = factories["playlists.PlaylistTrack"]( + index=0, playlist__user=logged_in_api_client.user + ) + url = reverse("api:v1:playlist-tracks-detail", kwargs={"pk": plt.pk}) response = logged_in_api_client.delete(url) @@ -105,97 +92,93 @@ def test_deleting_plt_updates_indexes( remove.assert_called_once_with(plt.playlist, 0) -@pytest.mark.parametrize('level', ['instance', 'me', 'followers']) +@pytest.mark.parametrize("level", ["instance", "me", "followers"]) def test_playlist_privacy_respected_in_list_anon( - preferences, level, factories, api_client): - preferences['common__api_authentication_required'] = False - factories['playlists.Playlist'](privacy_level=level) - url = reverse('api:v1:playlists-list') + preferences, level, factories, api_client +): + preferences["common__api_authentication_required"] = False + factories["playlists.Playlist"](privacy_level=level) + url = reverse("api:v1:playlists-list") response = api_client.get(url) - assert response.data['count'] == 0 + assert response.data["count"] == 0 -@pytest.mark.parametrize('method', ['PUT', 'PATCH', 'DELETE']) +@pytest.mark.parametrize("method", ["PUT", "PATCH", "DELETE"]) def test_only_owner_can_edit_playlist(method, factories, logged_in_api_client): - playlist = factories['playlists.Playlist']() - url = reverse('api:v1:playlists-detail', kwargs={'pk': playlist.pk}) + playlist = factories["playlists.Playlist"]() + url = reverse("api:v1:playlists-detail", kwargs={"pk": playlist.pk}) response = getattr(logged_in_api_client, method.lower())(url) assert response.status_code == 404 -@pytest.mark.parametrize('method', ['PUT', 'PATCH', 'DELETE']) -def test_only_owner_can_edit_playlist_track( - method, factories, logged_in_api_client): - plt = factories['playlists.PlaylistTrack']() - url = reverse('api:v1:playlist-tracks-detail', kwargs={'pk': plt.pk}) +@pytest.mark.parametrize("method", ["PUT", "PATCH", "DELETE"]) +def test_only_owner_can_edit_playlist_track(method, factories, logged_in_api_client): + plt = factories["playlists.PlaylistTrack"]() + url = reverse("api:v1:playlist-tracks-detail", kwargs={"pk": plt.pk}) response = getattr(logged_in_api_client, method.lower())(url) assert response.status_code == 404 -@pytest.mark.parametrize('level', ['instance', 'me', 'followers']) +@pytest.mark.parametrize("level", ["instance", "me", "followers"]) def test_playlist_track_privacy_respected_in_list_anon( - level, factories, api_client, preferences): - preferences['common__api_authentication_required'] = False - factories['playlists.PlaylistTrack'](playlist__privacy_level=level) - url = reverse('api:v1:playlist-tracks-list') + level, factories, api_client, preferences +): + preferences["common__api_authentication_required"] = False + factories["playlists.PlaylistTrack"](playlist__privacy_level=level) + url = reverse("api:v1:playlist-tracks-list") response = api_client.get(url) - assert response.data['count'] == 0 + assert response.data["count"] == 0 -@pytest.mark.parametrize('level', ['instance', 'me', 'followers']) -def test_can_list_tracks_from_playlist( - level, factories, logged_in_api_client): - plt = factories['playlists.PlaylistTrack']( - playlist__user=logged_in_api_client.user) - url = reverse('api:v1:playlists-tracks', kwargs={'pk': plt.playlist.pk}) +@pytest.mark.parametrize("level", ["instance", "me", "followers"]) +def test_can_list_tracks_from_playlist(level, factories, logged_in_api_client): + plt = factories["playlists.PlaylistTrack"](playlist__user=logged_in_api_client.user) + url = reverse("api:v1:playlists-tracks", kwargs={"pk": plt.playlist.pk}) response = logged_in_api_client.get(url) serialized_plt = serializers.PlaylistTrackSerializer(plt).data - assert response.data['count'] == 1 - assert response.data['results'][0] == serialized_plt + assert response.data["count"] == 1 + assert response.data["results"][0] == serialized_plt def test_can_add_multiple_tracks_at_once_via_api( - factories, mocker, logged_in_api_client): - playlist = factories['playlists.Playlist'](user=logged_in_api_client.user) - tracks = factories['music.Track'].create_batch(size=5) + factories, mocker, logged_in_api_client +): + playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) + tracks = factories["music.Track"].create_batch(size=5) track_ids = [t.id for t in tracks] - mocker.spy(playlist, 'insert_many') - url = reverse('api:v1:playlists-add', kwargs={'pk': playlist.pk}) - response = logged_in_api_client.post(url, {'tracks': track_ids}) + mocker.spy(playlist, "insert_many") + url = reverse("api:v1:playlists-add", kwargs={"pk": playlist.pk}) + response = logged_in_api_client.post(url, {"tracks": track_ids}) assert response.status_code == 201 assert playlist.playlist_tracks.count() == len(track_ids) - for plt in playlist.playlist_tracks.order_by('index'): - assert response.data['results'][plt.index]['id'] == plt.id + for plt in playlist.playlist_tracks.order_by("index"): + assert response.data["results"][plt.index]["id"] == plt.id assert plt.track == tracks[plt.index] -def test_can_clear_playlist_from_api( - factories, mocker, logged_in_api_client): - playlist = factories['playlists.Playlist'](user=logged_in_api_client.user) - plts = factories['playlists.PlaylistTrack'].create_batch( - size=5, playlist=playlist) - url = reverse('api:v1:playlists-clear', kwargs={'pk': playlist.pk}) +def test_can_clear_playlist_from_api(factories, mocker, logged_in_api_client): + playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) + plts = factories["playlists.PlaylistTrack"].create_batch(size=5, playlist=playlist) + url = reverse("api:v1:playlists-clear", kwargs={"pk": playlist.pk}) response = logged_in_api_client.delete(url) assert response.status_code == 204 assert playlist.playlist_tracks.count() == 0 -def test_update_playlist_from_api( - factories, mocker, logged_in_api_client): - playlist = factories['playlists.Playlist'](user=logged_in_api_client.user) - plts = factories['playlists.PlaylistTrack'].create_batch( - size=5, playlist=playlist) - url = reverse('api:v1:playlists-detail', kwargs={'pk': playlist.pk}) - response = logged_in_api_client.patch(url, {'name': 'test'}) +def test_update_playlist_from_api(factories, mocker, logged_in_api_client): + playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) + plts = factories["playlists.PlaylistTrack"].create_batch(size=5, playlist=playlist) + url = reverse("api:v1:playlists-detail", kwargs={"pk": playlist.pk}) + response = logged_in_api_client.patch(url, {"name": "test"}) playlist.refresh_from_db() assert response.status_code == 200 - assert response.data['user']['username'] == playlist.user.username + assert response.data["user"]["username"] == playlist.user.username diff --git a/api/tests/radios/test_api.py b/api/tests/radios/test_api.py index 66bf6052d..4a72d81ba 100644 --- a/api/tests/radios/test_api.py +++ b/api/tests/radios/test_api.py @@ -9,151 +9,125 @@ from funkwhale_api.radios import serializers def test_can_list_config_options(logged_in_client): - url = reverse('api:v1:radios:radios-filters') + url = reverse("api:v1:radios:radios-filters") response = logged_in_client.get(url) assert response.status_code == 200 - payload = json.loads(response.content.decode('utf-8')) + payload = json.loads(response.content.decode("utf-8")) expected = [f for f in filters.registry.values() if f.expose_in_api] assert len(payload) == len(expected) def test_can_validate_config(logged_in_client, factories): - artist1 = factories['music.Artist']() - artist2 = factories['music.Artist']() - factories['music.Track'].create_batch(3, artist=artist1) - factories['music.Track'].create_batch(3, artist=artist2) - candidates = artist1.tracks.order_by('pk') - f = { - 'filters': [ - {'type': 'artist', 'ids': [artist1.pk]} - ] - } - url = reverse('api:v1:radios:radios-validate') + artist1 = factories["music.Artist"]() + artist2 = factories["music.Artist"]() + factories["music.Track"].create_batch(3, artist=artist1) + factories["music.Track"].create_batch(3, artist=artist2) + candidates = artist1.tracks.order_by("pk") + f = {"filters": [{"type": "artist", "ids": [artist1.pk]}]} + url = reverse("api:v1:radios:radios-validate") response = logged_in_client.post( - url, - json.dumps(f), - content_type="application/json") + url, json.dumps(f), content_type="application/json" + ) assert response.status_code == 200 - payload = json.loads(response.content.decode('utf-8')) + payload = json.loads(response.content.decode("utf-8")) expected = { - 'count': candidates.count(), - 'sample': TrackSerializer(candidates, many=True).data + "count": candidates.count(), + "sample": TrackSerializer(candidates, many=True).data, } - assert payload['filters'][0]['candidates'] == expected - assert payload['filters'][0]['errors'] == [] + assert payload["filters"][0]["candidates"] == expected + assert payload["filters"][0]["errors"] == [] def test_can_validate_config_with_wrong_config(logged_in_client, factories): - f = { - 'filters': [ - {'type': 'artist', 'ids': [999]} - ] - } - url = reverse('api:v1:radios:radios-validate') + f = {"filters": [{"type": "artist", "ids": [999]}]} + url = reverse("api:v1:radios:radios-validate") response = logged_in_client.post( - url, - json.dumps(f), - content_type="application/json") + url, json.dumps(f), content_type="application/json" + ) assert response.status_code == 200 - payload = json.loads(response.content.decode('utf-8')) + payload = json.loads(response.content.decode("utf-8")) - expected = { - 'count': None, - 'sample': None - } - assert payload['filters'][0]['candidates'] == expected - assert len(payload['filters'][0]['errors']) == 1 + expected = {"count": None, "sample": None} + assert payload["filters"][0]["candidates"] == expected + assert len(payload["filters"][0]["errors"]) == 1 def test_saving_radio_sets_user(logged_in_client, factories): - artist = factories['music.Artist']() - f = { - 'name': 'Test', - 'config': [ - {'type': 'artist', 'ids': [artist.pk]} - ] - } - url = reverse('api:v1:radios:radios-list') + artist = factories["music.Artist"]() + f = {"name": "Test", "config": [{"type": "artist", "ids": [artist.pk]}]} + url = reverse("api:v1:radios:radios-list") response = logged_in_client.post( - url, - json.dumps(f), - content_type="application/json") + url, json.dumps(f), content_type="application/json" + ) assert response.status_code == 201 - radio = logged_in_client.user.radios.latest('id') - assert radio.name == 'Test' + radio = logged_in_client.user.radios.latest("id") + assert radio.name == "Test" assert radio.user == logged_in_client.user def test_user_can_detail_his_radio(logged_in_client, factories): - radio = factories['radios.Radio'](user=logged_in_client.user) - url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk}) + radio = factories["radios.Radio"](user=logged_in_client.user) + url = reverse("api:v1:radios:radios-detail", kwargs={"pk": radio.pk}) response = logged_in_client.get(url) assert response.status_code == 200 def test_user_can_detail_public_radio(logged_in_client, factories): - radio = factories['radios.Radio'](is_public=True) - url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk}) + radio = factories["radios.Radio"](is_public=True) + url = reverse("api:v1:radios:radios-detail", kwargs={"pk": radio.pk}) response = logged_in_client.get(url) assert response.status_code == 200 def test_user_cannot_detail_someone_else_radio(logged_in_client, factories): - radio = factories['radios.Radio'](is_public=False) - url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk}) + radio = factories["radios.Radio"](is_public=False) + url = reverse("api:v1:radios:radios-detail", kwargs={"pk": radio.pk}) response = logged_in_client.get(url) assert response.status_code == 404 def test_user_can_edit_his_radio(logged_in_client, factories): - radio = factories['radios.Radio'](user=logged_in_client.user) - url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk}) + radio = factories["radios.Radio"](user=logged_in_client.user) + url = reverse("api:v1:radios:radios-detail", kwargs={"pk": radio.pk}) response = logged_in_client.put( - url, - json.dumps({'name': 'new', 'config': []}), - content_type="application/json") + url, json.dumps({"name": "new", "config": []}), content_type="application/json" + ) radio.refresh_from_db() assert response.status_code == 200 - assert radio.name == 'new' + assert radio.name == "new" def test_user_cannot_edit_someone_else_radio(logged_in_client, factories): - radio = factories['radios.Radio']() - url = reverse('api:v1:radios:radios-detail', kwargs={'pk': radio.pk}) + radio = factories["radios.Radio"]() + url = reverse("api:v1:radios:radios-detail", kwargs={"pk": radio.pk}) response = logged_in_client.put( - url, - json.dumps({'name': 'new', 'config': []}), - content_type="application/json") + url, json.dumps({"name": "new", "config": []}), content_type="application/json" + ) assert response.status_code == 404 def test_clean_config_is_called_on_serializer_save(mocker, factories): - user = factories['users.User']() - artist = factories['music.Artist']() - data= { - 'name': 'Test', - 'config': [ - {'type': 'artist', 'ids': [artist.pk]} - ] - } - spied = mocker.spy(filters.registry['artist'], 'clean_config') + user = factories["users.User"]() + artist = factories["music.Artist"]() + data = {"name": "Test", "config": [{"type": "artist", "ids": [artist.pk]}]} + spied = mocker.spy(filters.registry["artist"], "clean_config") serializer = serializers.RadioSerializer(data=data) assert serializer.is_valid() instance = serializer.save(user=user) - spied.assert_called_once_with(data['config'][0]) - assert instance.config[0]['names'] == [artist.name] + spied.assert_called_once_with(data["config"][0]) + assert instance.config[0]["names"] == [artist.name] diff --git a/api/tests/radios/test_filters.py b/api/tests/radios/test_filters.py index 27166b4ab..025fc7a63 100644 --- a/api/tests/radios/test_filters.py +++ b/api/tests/radios/test_filters.py @@ -8,154 +8,147 @@ from funkwhale_api.radios import filters @filters.registry.register class NoopFilter(filters.RadioFilter): - code = 'noop' + code = "noop" + def get_query(self, candidates, **kwargs): return def test_most_simple_radio_does_not_filter_anything(factories): - tracks = factories['music.Track'].create_batch(3) - radio = factories['radios.Radio'](config=[{'type': 'noop'}]) + tracks = factories["music.Track"].create_batch(3) + radio = factories["radios.Radio"](config=[{"type": "noop"}]) assert radio.version == 0 assert radio.get_candidates().count() == 3 - def test_filter_can_use_custom_queryset(factories): - tracks = factories['music.Track'].create_batch(3) + tracks = factories["music.Track"].create_batch(3) candidates = Track.objects.filter(pk=tracks[0].pk) - qs = filters.run([{'type': 'noop'}], candidates=candidates) + qs = filters.run([{"type": "noop"}], candidates=candidates) assert qs.count() == 1 assert qs.first() == tracks[0] def test_filter_on_tag(factories): - tracks = factories['music.Track'].create_batch(3, tags=['metal']) - factories['music.Track'].create_batch(3, tags=['pop']) + tracks = factories["music.Track"].create_batch(3, tags=["metal"]) + factories["music.Track"].create_batch(3, tags=["pop"]) expected = tracks - f = [ - {'type': 'tag', 'names': ['metal']} - ] + f = [{"type": "tag", "names": ["metal"]}] candidates = filters.run(f) - assert list(candidates.order_by('pk')) == expected + assert list(candidates.order_by("pk")) == expected def test_filter_on_artist(factories): - artist1 = factories['music.Artist']() - artist2 = factories['music.Artist']() - factories['music.Track'].create_batch(3, artist=artist1) - factories['music.Track'].create_batch(3, artist=artist2) - expected = list(artist1.tracks.order_by('pk')) - f = [ - {'type': 'artist', 'ids': [artist1.pk]} - ] + artist1 = factories["music.Artist"]() + artist2 = factories["music.Artist"]() + factories["music.Track"].create_batch(3, artist=artist1) + factories["music.Track"].create_batch(3, artist=artist2) + expected = list(artist1.tracks.order_by("pk")) + f = [{"type": "artist", "ids": [artist1.pk]}] candidates = filters.run(f) - assert list(candidates.order_by('pk')) == expected + assert list(candidates.order_by("pk")) == expected def test_can_combine_with_or(factories): - artist1 = factories['music.Artist']() - artist2 = factories['music.Artist']() - artist3 = factories['music.Artist']() - factories['music.Track'].create_batch(3, artist=artist1) - factories['music.Track'].create_batch(3, artist=artist2) - factories['music.Track'].create_batch(3, artist=artist3) - expected = Track.objects.exclude(artist=artist3).order_by('pk') + artist1 = factories["music.Artist"]() + artist2 = factories["music.Artist"]() + artist3 = factories["music.Artist"]() + factories["music.Track"].create_batch(3, artist=artist1) + factories["music.Track"].create_batch(3, artist=artist2) + factories["music.Track"].create_batch(3, artist=artist3) + expected = Track.objects.exclude(artist=artist3).order_by("pk") f = [ - {'type': 'artist', 'ids': [artist1.pk]}, - {'type': 'artist', 'ids': [artist2.pk], 'operator': 'or'}, + {"type": "artist", "ids": [artist1.pk]}, + {"type": "artist", "ids": [artist2.pk], "operator": "or"}, ] candidates = filters.run(f) - assert list(candidates.order_by('pk')) == list(expected) + assert list(candidates.order_by("pk")) == list(expected) def test_can_combine_with_and(factories): - artist1 = factories['music.Artist']() - artist2 = factories['music.Artist']() - metal_tracks = factories['music.Track'].create_batch( - 2, artist=artist1, tags=['metal']) - factories['music.Track'].create_batch(2, artist=artist1, tags=['pop']) - factories['music.Track'].create_batch(3, artist=artist2) + artist1 = factories["music.Artist"]() + artist2 = factories["music.Artist"]() + metal_tracks = factories["music.Track"].create_batch( + 2, artist=artist1, tags=["metal"] + ) + factories["music.Track"].create_batch(2, artist=artist1, tags=["pop"]) + factories["music.Track"].create_batch(3, artist=artist2) expected = metal_tracks f = [ - {'type': 'artist', 'ids': [artist1.pk]}, - {'type': 'tag', 'names': ['metal'], 'operator': 'and'}, + {"type": "artist", "ids": [artist1.pk]}, + {"type": "tag", "names": ["metal"], "operator": "and"}, ] candidates = filters.run(f) - assert list(candidates.order_by('pk')) == list(expected) + assert list(candidates.order_by("pk")) == list(expected) def test_can_negate(factories): - artist1 = factories['music.Artist']() - artist2 = factories['music.Artist']() - factories['music.Track'].create_batch(3, artist=artist1) - factories['music.Track'].create_batch(3, artist=artist2) - expected = artist2.tracks.order_by('pk') - f = [ - {'type': 'artist', 'ids': [artist1.pk], 'not': True}, - ] + artist1 = factories["music.Artist"]() + artist2 = factories["music.Artist"]() + factories["music.Track"].create_batch(3, artist=artist1) + factories["music.Track"].create_batch(3, artist=artist2) + expected = artist2.tracks.order_by("pk") + f = [{"type": "artist", "ids": [artist1.pk], "not": True}] candidates = filters.run(f) - assert list(candidates.order_by('pk')) == list(expected) + assert list(candidates.order_by("pk")) == list(expected) def test_can_group(factories): - artist1 = factories['music.Artist']() - artist2 = factories['music.Artist']() - factories['music.Track'].create_batch(2, artist=artist1) - t1 = factories['music.Track'].create_batch( - 2, artist=artist1, tags=['metal']) - factories['music.Track'].create_batch(2, artist=artist2) - t2 = factories['music.Track'].create_batch( - 2, artist=artist2, tags=['metal']) - factories['music.Track'].create_batch(2, tags=['metal']) + artist1 = factories["music.Artist"]() + artist2 = factories["music.Artist"]() + factories["music.Track"].create_batch(2, artist=artist1) + t1 = factories["music.Track"].create_batch(2, artist=artist1, tags=["metal"]) + factories["music.Track"].create_batch(2, artist=artist2) + t2 = factories["music.Track"].create_batch(2, artist=artist2, tags=["metal"]) + factories["music.Track"].create_batch(2, tags=["metal"]) expected = t1 + t2 f = [ - {'type': 'tag', 'names': ['metal']}, - {'type': 'group', 'operator': 'and', 'filters': [ - {'type': 'artist', 'ids': [artist1.pk], 'operator': 'or'}, - {'type': 'artist', 'ids': [artist2.pk], 'operator': 'or'}, - ]} + {"type": "tag", "names": ["metal"]}, + { + "type": "group", + "operator": "and", + "filters": [ + {"type": "artist", "ids": [artist1.pk], "operator": "or"}, + {"type": "artist", "ids": [artist2.pk], "operator": "or"}, + ], + }, ] candidates = filters.run(f) - assert list(candidates.order_by('pk')) == list(expected) + assert list(candidates.order_by("pk")) == list(expected) def test_artist_filter_clean_config(factories): - artist1 = factories['music.Artist']() - artist2 = factories['music.Artist']() + artist1 = factories["music.Artist"]() + artist2 = factories["music.Artist"]() - config = filters.clean_config( - {'type': 'artist', 'ids': [artist2.pk, artist1.pk]}) + config = filters.clean_config({"type": "artist", "ids": [artist2.pk, artist1.pk]}) expected = { - 'type': 'artist', - 'ids': [artist1.pk, artist2.pk], - 'names': [artist1.name, artist2.name] + "type": "artist", + "ids": [artist1.pk, artist2.pk], + "names": [artist1.name, artist2.name], } assert filters.clean_config(config) == expected def test_can_check_artist_filter(factories): - artist = factories['music.Artist']() + artist = factories["music.Artist"]() - assert filters.validate({'type': 'artist', 'ids': [artist.pk]}) + assert filters.validate({"type": "artist", "ids": [artist.pk]}) with pytest.raises(ValidationError): - filters.validate({'type': 'artist', 'ids': [artist.pk + 1]}) + filters.validate({"type": "artist", "ids": [artist.pk + 1]}) def test_can_check_operator(): - assert filters.validate( - {'type': 'group', 'operator': 'or', 'filters': []}) - assert filters.validate( - {'type': 'group', 'operator': 'and', 'filters': []}) + assert filters.validate({"type": "group", "operator": "or", "filters": []}) + assert filters.validate({"type": "group", "operator": "and", "filters": []}) with pytest.raises(ValidationError): - assert filters.validate( - {'type': 'group', 'operator': 'nope', 'filters': []}) + assert filters.validate({"type": "group", "operator": "nope", "filters": []}) diff --git a/api/tests/radios/test_radios.py b/api/tests/radios/test_radios.py index b166b648c..ebc45f1f9 100644 --- a/api/tests/radios/test_radios.py +++ b/api/tests/radios/test_radios.py @@ -51,9 +51,9 @@ def test_can_pick_by_weight(): def test_can_get_choices_for_favorites_radio(factories): - files = factories['music.TrackFile'].create_batch(10) + files = factories["music.TrackFile"].create_batch(10) tracks = [f.track for f in files] - user = factories['users.User']() + user = factories["users.User"]() for i in range(5): TrackFavorite.add(track=random.choice(tracks), user=user) @@ -71,56 +71,52 @@ def test_can_get_choices_for_favorites_radio(factories): def test_can_get_choices_for_custom_radio(factories): - artist = factories['music.Artist']() - files = factories['music.TrackFile'].create_batch( - 5, track__artist=artist) + artist = factories["music.Artist"]() + files = factories["music.TrackFile"].create_batch(5, track__artist=artist) tracks = [f.track for f in files] - wrong_files = factories['music.TrackFile'].create_batch(5) + wrong_files = factories["music.TrackFile"].create_batch(5) wrong_tracks = [f.track for f in wrong_files] - session = factories['radios.CustomRadioSession']( - custom_radio__config=[{'type': 'artist', 'ids': [artist.pk]}] + session = factories["radios.CustomRadioSession"]( + custom_radio__config=[{"type": "artist", "ids": [artist.pk]}] ) choices = session.radio.get_choices() expected = [t.pk for t in tracks] - assert list(choices.values_list('id', flat=True)) == expected + assert list(choices.values_list("id", flat=True)) == expected def test_cannot_start_custom_radio_if_not_owner_or_not_public(factories): - user = factories['users.User']() - artist = factories['music.Artist']() - radio = factories['radios.Radio']( - config=[{'type': 'artist', 'ids': [artist.pk]}] - ) + user = factories["users.User"]() + artist = factories["music.Artist"]() + radio = factories["radios.Radio"](config=[{"type": "artist", "ids": [artist.pk]}]) serializer = serializers.RadioSessionSerializer( - data={ - 'radio_type': 'custom', 'custom_radio': radio.pk, 'user': user.pk} + data={"radio_type": "custom", "custom_radio": radio.pk, "user": user.pk} ) message = "You don't have access to this radio" assert not serializer.is_valid() - assert message in serializer.errors['non_field_errors'] + assert message in serializer.errors["non_field_errors"] def test_can_start_custom_radio_from_api(logged_in_client, factories): - artist = factories['music.Artist']() - radio = factories['radios.Radio']( - config=[{'type': 'artist', 'ids': [artist.pk]}], - user=logged_in_client.user + artist = factories["music.Artist"]() + radio = factories["radios.Radio"]( + config=[{"type": "artist", "ids": [artist.pk]}], user=logged_in_client.user ) - url = reverse('api:v1:radios:sessions-list') + url = reverse("api:v1:radios:sessions-list") response = logged_in_client.post( - url, {'radio_type': 'custom', 'custom_radio': radio.pk}) + url, {"radio_type": "custom", "custom_radio": radio.pk} + ) assert response.status_code == 201 - session = radio.sessions.latest('id') - assert session.radio_type == 'custom' + session = radio.sessions.latest("id") + assert session.radio_type == "custom" assert session.user == logged_in_client.user def test_can_use_radio_session_to_filter_choices(factories): - files = factories['music.TrackFile'].create_batch(30) + files = factories["music.TrackFile"].create_batch(30) tracks = [f.track for f in files] - user = factories['users.User']() + user = factories["users.User"]() radio = radios.RandomRadio() session = radio.start_session(user) @@ -129,13 +125,13 @@ def test_can_use_radio_session_to_filter_choices(factories): # ensure 30 differents tracks have been suggested tracks_id = [ - session_track.track.pk - for session_track in session.session_tracks.all()] + session_track.track.pk for session_track in session.session_tracks.all() + ] assert len(set(tracks_id)) == 30 def test_can_restore_radio_from_previous_session(factories): - user = factories['users.User']() + user = factories["users.User"]() radio = radios.RandomRadio() session = radio.start_session(user) @@ -144,37 +140,37 @@ def test_can_restore_radio_from_previous_session(factories): def test_can_start_radio_for_logged_in_user(logged_in_client): - url = reverse('api:v1:radios:sessions-list') - response = logged_in_client.post(url, {'radio_type': 'random'}) - session = models.RadioSession.objects.latest('id') - assert session.radio_type == 'random' + url = reverse("api:v1:radios:sessions-list") + response = logged_in_client.post(url, {"radio_type": "random"}) + session = models.RadioSession.objects.latest("id") + assert session.radio_type == "random" assert session.user == logged_in_client.user def test_can_get_track_for_session_from_api(factories, logged_in_client): - files = factories['music.TrackFile'].create_batch(1) + files = factories["music.TrackFile"].create_batch(1) tracks = [f.track for f in files] - url = reverse('api:v1:radios:sessions-list') - response = logged_in_client.post(url, {'radio_type': 'random'}) - session = models.RadioSession.objects.latest('id') + url = reverse("api:v1:radios:sessions-list") + response = logged_in_client.post(url, {"radio_type": "random"}) + session = models.RadioSession.objects.latest("id") - url = reverse('api:v1:radios:tracks-list') - response = logged_in_client.post(url, {'session': session.pk}) - data = json.loads(response.content.decode('utf-8')) + url = reverse("api:v1:radios:tracks-list") + response = logged_in_client.post(url, {"session": session.pk}) + data = json.loads(response.content.decode("utf-8")) - assert data['track']['id'] == tracks[0].id - assert data['position'] == 1 + assert data["track"]["id"] == tracks[0].id + assert data["position"] == 1 - next_track = factories['music.TrackFile']().track - response = logged_in_client.post(url, {'session': session.pk}) - data = json.loads(response.content.decode('utf-8')) + next_track = factories["music.TrackFile"]().track + response = logged_in_client.post(url, {"session": session.pk}) + data = json.loads(response.content.decode("utf-8")) - assert data['track']['id'] == next_track.id - assert data['position'] == 2 + assert data["track"]["id"] == next_track.id + assert data["position"] == 2 def test_related_object_radio_validate_related_object(factories): - user = factories['users.User']() + user = factories["users.User"]() # cannot start without related object radio = radios.ArtistRadio() with pytest.raises(ValidationError): @@ -187,59 +183,57 @@ def test_related_object_radio_validate_related_object(factories): def test_can_start_artist_radio(factories): - user = factories['users.User']() - artist = factories['music.Artist']() - wrong_files = factories['music.TrackFile'].create_batch(5) + user = factories["users.User"]() + artist = factories["music.Artist"]() + wrong_files = factories["music.TrackFile"].create_batch(5) wrong_tracks = [f.track for f in wrong_files] - good_files = factories['music.TrackFile'].create_batch( - 5, track__artist=artist) + good_files = factories["music.TrackFile"].create_batch(5, track__artist=artist) good_tracks = [f.track for f in good_files] radio = radios.ArtistRadio() session = radio.start_session(user, related_object=artist) - assert session.radio_type == 'artist' + assert session.radio_type == "artist" for i in range(5): assert radio.pick() in good_tracks def test_can_start_tag_radio(factories): - user = factories['users.User']() - tag = factories['taggit.Tag']() - wrong_files = factories['music.TrackFile'].create_batch(5) + user = factories["users.User"]() + tag = factories["taggit.Tag"]() + wrong_files = factories["music.TrackFile"].create_batch(5) wrong_tracks = [f.track for f in wrong_files] - good_files = factories['music.TrackFile'].create_batch( - 5, track__tags=[tag]) + good_files = factories["music.TrackFile"].create_batch(5, track__tags=[tag]) good_tracks = [f.track for f in good_files] radio = radios.TagRadio() session = radio.start_session(user, related_object=tag) - assert session.radio_type == 'tag' + assert session.radio_type == "tag" for i in range(5): assert radio.pick() in good_tracks -def test_can_start_artist_radio_from_api( - logged_in_api_client, preferences, factories): - artist = factories['music.Artist']() - url = reverse('api:v1:radios:sessions-list') +def test_can_start_artist_radio_from_api(logged_in_api_client, preferences, factories): + artist = factories["music.Artist"]() + url = reverse("api:v1:radios:sessions-list") response = logged_in_api_client.post( - url, {'radio_type': 'artist', 'related_object_id': artist.id}) + url, {"radio_type": "artist", "related_object_id": artist.id} + ) assert response.status_code == 201 - session = models.RadioSession.objects.latest('id') + session = models.RadioSession.objects.latest("id") - assert session.radio_type == 'artist' + assert session.radio_type == "artist" assert session.related_object == artist def test_can_start_less_listened_radio(factories): - user = factories['users.User']() - wrong_files = factories['music.TrackFile'].create_batch(5) + user = factories["users.User"]() + wrong_files = factories["music.TrackFile"].create_batch(5) for f in wrong_files: - factories['history.Listening'](track=f.track, user=user) - good_files = factories['music.TrackFile'].create_batch(5) + factories["history.Listening"](track=f.track, user=user) + good_files = factories["music.TrackFile"].create_batch(5) good_tracks = [f.track for f in good_files] radio = radios.LessListenedRadio() session = radio.start_session(user) diff --git a/api/tests/requests/test_models.py b/api/tests/requests/test_models.py index 797656bd7..d6b71635a 100644 --- a/api/tests/requests/test_models.py +++ b/api/tests/requests/test_models.py @@ -4,20 +4,20 @@ from django.forms import ValidationError def test_can_bind_import_batch_to_request(factories): - request = factories['requests.ImportRequest']() + request = factories["requests.ImportRequest"]() - assert request.status == 'pending' + assert request.status == "pending" # when we create the import, we consider the request as accepted - batch = factories['music.ImportBatch'](import_request=request) + batch = factories["music.ImportBatch"](import_request=request) request.refresh_from_db() - assert request.status == 'accepted' + assert request.status == "accepted" # now, the batch is finished, therefore the request status should be # imported - batch.status = 'finished' - batch.save(update_fields=['status']) + batch.status = "finished" + batch.save(update_fields=["status"]) request.refresh_from_db() - assert request.status == 'imported' + assert request.status == "imported" diff --git a/api/tests/requests/test_views.py b/api/tests/requests/test_views.py index 6c34f9ad1..0d6433672 100644 --- a/api/tests/requests/test_views.py +++ b/api/tests/requests/test_views.py @@ -2,25 +2,25 @@ from django.urls import reverse def test_request_viewset_requires_auth(db, api_client): - url = reverse('api:v1:requests:import-requests-list') + url = reverse("api:v1:requests:import-requests-list") response = api_client.get(url) assert response.status_code == 401 def test_user_can_create_request(logged_in_api_client): - url = reverse('api:v1:requests:import-requests-list') + url = reverse("api:v1:requests:import-requests-list") user = logged_in_api_client.user data = { - 'artist_name': 'System of a Down', - 'albums': 'All please!', - 'comment': 'Please, they rock!', + "artist_name": "System of a Down", + "albums": "All please!", + "comment": "Please, they rock!", } response = logged_in_api_client.post(url, data) assert response.status_code == 201 - ir = user.import_requests.latest('id') - assert ir.status == 'pending' + ir = user.import_requests.latest("id") + assert ir.status == "pending" assert ir.creation_date is not None for field, value in data.items(): assert getattr(ir, field) == value diff --git a/api/tests/subsonic/test_authentication.py b/api/tests/subsonic/test_authentication.py index 656f8c44d..f404c8b0a 100644 --- a/api/tests/subsonic/test_authentication.py +++ b/api/tests/subsonic/test_authentication.py @@ -7,16 +7,12 @@ 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' + 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 - }) + token = authentication.get_token(salt, "password") + request = api_request.get("/", {"t": token, "s": salt, "u": user.username}) authenticator = authentication.SubsonicAuthentication() u, _ = authenticator.authenticate(request) @@ -25,16 +21,22 @@ def test_auth_with_salt(api_request, factories): def test_auth_with_password_hex(api_request, factories): - salt = 'salt' - user = factories['users.User']() - user.subsonic_api_token = 'password' + 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')) - }) + 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) @@ -43,15 +45,12 @@ def test_auth_with_password_hex(api_request, factories): def test_auth_with_password_cleartext(api_request, factories): - salt = 'salt' - user = factories['users.User']() - user.subsonic_api_token = 'password' + 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', - }) + token = authentication.get_token(salt, "password") + request = api_request.get("/", {"u": user.username, "p": "password"}) authenticator = authentication.SubsonicAuthentication() u, _ = authenticator.authenticate(request) @@ -60,15 +59,12 @@ def test_auth_with_password_cleartext(api_request, factories): def test_auth_with_inactive_users(api_request, factories): - salt = 'salt' - user = factories['users.User'](is_active=False) - user.subsonic_api_token = 'password' + salt = "salt" + user = factories["users.User"](is_active=False) + user.subsonic_api_token = "password" user.save() - token = authentication.get_token(salt, 'password') - request = api_request.get('/', { - 'u': user.username, - 'p': 'password', - }) + token = authentication.get_token(salt, "password") + request = api_request.get("/", {"u": user.username, "p": "password"}) authenticator = authentication.SubsonicAuthentication() with pytest.raises(exceptions.AuthenticationFailed): diff --git a/api/tests/subsonic/test_renderers.py b/api/tests/subsonic/test_renderers.py index 8e2ea3f85..65dae6f22 100644 --- a/api/tests/subsonic/test_renderers.py +++ b/api/tests/subsonic/test_renderers.py @@ -5,37 +5,25 @@ from funkwhale_api.subsonic import renderers def test_json_renderer(): - data = {'hello': 'world'} + data = {"hello": "world"} expected = { - 'subsonic-response': { - 'status': 'ok', - 'version': '1.16.0', - 'hello': 'world' - } + "subsonic-response": {"status": "ok", "version": "1.16.0", "hello": "world"} } renderer = renderers.SubsonicJSONRenderer() assert json.loads(renderer.render(data)) == expected def test_xml_renderer_dict_to_xml(): - payload = { - 'hello': 'world', - 'item': [ - {'this': 1}, - {'some': 'node'}, - ] - } + payload = {"hello": "world", "item": [{"this": 1}, {"some": "node"}]} expected = """ """ - result = renderers.dict_to_xml_tree('key', payload) + result = renderers.dict_to_xml_tree("key", payload) exp = ET.fromstring(expected) assert ET.tostring(result) == ET.tostring(exp) def test_xml_renderer(): - payload = { - 'hello': 'world', - } + payload = {"hello": "world"} expected = b'\n' renderer = renderers.SubsonicXMLRenderer() diff --git a/api/tests/subsonic/test_serializers.py b/api/tests/subsonic/test_serializers.py index 6b9ec232d..6fdf02e2d 100644 --- a/api/tests/subsonic/test_serializers.py +++ b/api/tests/subsonic/test_serializers.py @@ -3,136 +3,121 @@ from funkwhale_api.subsonic import serializers def test_get_artists_serializer(factories): - artist1 = factories['music.Artist'](name='eliot') - artist2 = factories['music.Artist'](name='Ellena') - artist3 = factories['music.Artist'](name='Rilay') + artist1 = factories["music.Artist"](name="eliot") + artist2 = factories["music.Artist"](name="Ellena") + artist3 = factories["music.Artist"](name="Rilay") - factories['music.Album'].create_batch(size=3, artist=artist1) - factories['music.Album'].create_batch(size=2, artist=artist2) + factories["music.Album"].create_batch(size=3, artist=artist1) + factories["music.Album"].create_batch(size=2, artist=artist2) expected = { - 'ignoredArticles': '', - 'index': [ + "ignoredArticles": "", + "index": [ { - 'name': 'E', - 'artist': [ - { - 'id': artist1.pk, - 'name': artist1.name, - 'albumCount': 3, - }, - { - 'id': artist2.pk, - 'name': artist2.name, - 'albumCount': 2, - }, - ] + "name": "E", + "artist": [ + {"id": artist1.pk, "name": artist1.name, "albumCount": 3}, + {"id": artist2.pk, "name": artist2.name, "albumCount": 2}, + ], }, { - 'name': 'R', - 'artist': [ - { - 'id': artist3.pk, - 'name': artist3.name, - 'albumCount': 0, - }, - ] + "name": "R", + "artist": [{"id": artist3.pk, "name": artist3.name, "albumCount": 0}], }, - ] + ], } - queryset = artist1.__class__.objects.filter(pk__in=[ - artist1.pk, artist2.pk, artist3.pk - ]) + queryset = artist1.__class__.objects.filter( + pk__in=[artist1.pk, artist2.pk, artist3.pk] + ) assert serializers.GetArtistsSerializer(queryset).data == expected def test_get_artist_serializer(factories): - artist = factories['music.Artist']() - album = factories['music.Album'](artist=artist) - tracks = factories['music.Track'].create_batch(size=3, album=album) + artist = factories["music.Artist"]() + album = factories["music.Album"](artist=artist) + tracks = factories["music.Track"].create_batch(size=3, album=album) expected = { - 'id': artist.pk, - 'name': artist.name, - 'albumCount': 1, - 'album': [ + "id": artist.pk, + "name": artist.name, + "albumCount": 1, + "album": [ { - 'id': album.pk, - 'coverArt': 'al-{}'.format(album.id), - 'artistId': artist.pk, - 'name': album.title, - 'artist': artist.name, - 'songCount': len(tracks), - 'created': album.creation_date, - 'year': album.release_date.year, + "id": album.pk, + "coverArt": "al-{}".format(album.id), + "artistId": artist.pk, + "name": album.title, + "artist": artist.name, + "songCount": len(tracks), + "created": album.creation_date, + "year": album.release_date.year, } - ] + ], } assert serializers.GetArtistSerializer(artist).data == expected def test_get_album_serializer(factories): - artist = factories['music.Artist']() - album = factories['music.Album'](artist=artist) - track = factories['music.Track'](album=album) - tf = factories['music.TrackFile']( - track=track, bitrate=42000, duration=43, size=44) + artist = factories["music.Artist"]() + album = factories["music.Album"](artist=artist) + track = factories["music.Track"](album=album) + tf = factories["music.TrackFile"](track=track, bitrate=42000, duration=43, size=44) expected = { - 'id': album.pk, - 'artistId': artist.pk, - 'name': album.title, - 'artist': artist.name, - 'songCount': 1, - 'created': album.creation_date, - 'year': album.release_date.year, - 'coverArt': 'al-{}'.format(album.id), - 'song': [ + "id": album.pk, + "artistId": artist.pk, + "name": album.title, + "artist": artist.name, + "songCount": 1, + "created": album.creation_date, + "year": album.release_date.year, + "coverArt": "al-{}".format(album.id), + "song": [ { - 'id': track.pk, - 'isDir': 'false', - 'title': track.title, - 'coverArt': 'al-{}'.format(album.id), - 'album': album.title, - 'artist': artist.name, - 'track': track.position, - 'year': track.album.release_date.year, - 'contentType': tf.mimetype, - 'suffix': tf.extension or '', - 'bitrate': 42, - 'duration': 43, - 'size': 44, - 'created': track.creation_date, - 'albumId': album.pk, - 'artistId': artist.pk, - 'type': 'music', + "id": track.pk, + "isDir": "false", + "title": track.title, + "coverArt": "al-{}".format(album.id), + "album": album.title, + "artist": artist.name, + "track": track.position, + "year": track.album.release_date.year, + "contentType": tf.mimetype, + "suffix": tf.extension or "", + "bitrate": 42, + "duration": 43, + "size": 44, + "created": track.creation_date, + "albumId": album.pk, + "artistId": artist.pk, + "type": "music", } - ] + ], } 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) + 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 + 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']() + album1 = factories["music.Album"]() + album2 = factories["music.Album"]() - qs = music_models.Album.objects.with_tracks_count().order_by('pk') + qs = music_models.Album.objects.with_tracks_count().order_by("pk") expected = [ serializers.get_album2_data(album1), serializers.get_album2_data(album2), @@ -142,17 +127,17 @@ def test_get_album_list2_serializer(factories): def test_playlist_serializer(factories): - plt = factories['playlists.PlaylistTrack']() + plt = factories["playlists.PlaylistTrack"]() playlist = plt.playlist - qs = music_models.Album.objects.with_tracks_count().order_by('pk') + qs = music_models.Album.objects.with_tracks_count().order_by("pk") expected = { - 'id': playlist.pk, - 'name': playlist.name, - 'owner': playlist.user.username, - 'public': 'false', - 'songCount': 1, - 'duration': 0, - 'created': playlist.creation_date, + "id": playlist.pk, + "name": playlist.name, + "owner": playlist.user.username, + "public": "false", + "songCount": 1, + "duration": 0, + "created": playlist.creation_date, } qs = playlist.__class__.objects.with_tracks_count() data = serializers.get_playlist_data(qs.first()) @@ -160,21 +145,19 @@ def test_playlist_serializer(factories): def test_playlist_detail_serializer(factories): - plt = factories['playlists.PlaylistTrack']() - tf = factories['music.TrackFile'](track=plt.track) + plt = factories["playlists.PlaylistTrack"]() + tf = factories["music.TrackFile"](track=plt.track) playlist = plt.playlist - qs = music_models.Album.objects.with_tracks_count().order_by('pk') + qs = music_models.Album.objects.with_tracks_count().order_by("pk") expected = { - 'id': playlist.pk, - 'name': playlist.name, - 'owner': playlist.user.username, - 'public': 'false', - 'songCount': 1, - 'duration': 0, - 'created': playlist.creation_date, - 'entry': [ - serializers.get_track_data(plt.track.album, plt.track, tf) - ] + "id": playlist.pk, + "name": playlist.name, + "owner": playlist.user.username, + "public": "false", + "songCount": 1, + "duration": 0, + "created": playlist.creation_date, + "entry": [serializers.get_track_data(plt.track.album, plt.track, tf)], } qs = playlist.__class__.objects.with_tracks_count() data = serializers.get_playlist_detail_data(qs.first()) @@ -182,50 +165,47 @@ def test_playlist_detail_serializer(factories): def test_directory_serializer_artist(factories): - track = factories['music.Track']() - tf = factories['music.TrackFile']( - track=track, bitrate=42000, duration=43, size=44) + track = factories["music.Track"]() + tf = factories["music.TrackFile"](track=track, bitrate=42000, duration=43, size=44) album = track.album artist = track.artist expected = { - 'id': artist.pk, - 'parent': 1, - 'name': artist.name, - 'child': [{ - 'id': track.pk, - '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 or '', - 'bitrate': 42, - 'duration': 43, - 'size': 44, - 'created': track.creation_date, - 'albumId': album.pk, - 'artistId': artist.pk, - 'parent': artist.pk, - 'type': 'music', - }] + "id": artist.pk, + "parent": 1, + "name": artist.name, + "child": [ + { + "id": track.pk, + "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 or "", + "bitrate": 42, + "duration": 43, + "size": 44, + "created": track.creation_date, + "albumId": album.pk, + "artistId": artist.pk, + "parent": artist.pk, + "type": "music", + } + ], } data = serializers.get_music_directory_data(artist) assert data == expected def test_scrobble_serializer(factories): - tf = factories['music.TrackFile']() + tf = factories["music.TrackFile"]() track = tf.track - user = factories['users.User']() - payload = { - 'id': track.pk, - 'submission': True, - } - serializer = serializers.ScrobbleSerializer( - data=payload, context={'user': user}) + user = factories["users.User"]() + payload = {"id": track.pk, "submission": True} + serializer = serializers.ScrobbleSerializer(data=payload, context={"user": user}) assert serializer.is_valid(raise_exception=True) diff --git a/api/tests/subsonic/test_views.py b/api/tests/subsonic/test_views.py index 52e410e52..d06bf5525 100644 --- a/api/tests/subsonic/test_views.py +++ b/api/tests/subsonic/test_views.py @@ -18,372 +18,341 @@ def render_json(data): def test_render_content_json(db, api_client): - url = reverse('api:subsonic-ping') - response = api_client.get(url, {'f': 'json'}) + url = reverse("api:subsonic-ping") + response = api_client.get(url, {"f": "json"}) - expected = { - 'status': 'ok', - 'version': '1.16.0' - } + expected = {"status": "ok", "version": "1.16.0"} assert response.status_code == 200 assert json.loads(response.content) == render_json(expected) -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_exception_wrong_credentials(f, db, api_client): - url = reverse('api:subsonic-ping') - response = api_client.get(url, {'f': f, 'u': 'yolo'}) + url = reverse("api:subsonic-ping") + response = api_client.get(url, {"f": f, "u": "yolo"}) expected = { - 'status': 'failed', - 'error': { - 'code': 40, - 'message': 'Wrong username or password.' - } + "status": "failed", + "error": {"code": 40, "message": "Wrong username or password."}, } assert response.status_code == 200 assert response.data == expected def test_disabled_subsonic(preferences, api_client): - preferences['subsonic__enabled'] = False - url = reverse('api:subsonic-ping') + preferences["subsonic__enabled"] = False + url = reverse("api:subsonic-ping") response = api_client.get(url) assert response.status_code == 405 -@pytest.mark.parametrize('f', ['xml', 'json']) +@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 + 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}) + 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) - } + "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']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_ping(f, db, api_client): - url = reverse('api:subsonic-ping') - response = api_client.get(url, {'f': f}) + url = reverse("api:subsonic-ping") + response = api_client.get(url, {"f": f}) - expected = { - 'status': 'ok', - 'version': '1.16.0', - } + expected = {"status": "ok", "version": "1.16.0"} assert response.status_code == 200 assert response.data == expected -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_get_artists(f, db, logged_in_api_client, factories): - url = reverse('api:subsonic-get-artists') - assert url.endswith('getArtists') is True - artists = factories['music.Artist'].create_batch(size=10) + url = reverse("api:subsonic-get-artists") + assert url.endswith("getArtists") is True + artists = factories["music.Artist"].create_batch(size=10) expected = { - 'artists': serializers.GetArtistsSerializer( + "artists": serializers.GetArtistsSerializer( music_models.Artist.objects.all() ).data } - response = logged_in_api_client.get(url, {'f': f}) + response = logged_in_api_client.get(url, {"f": f}) assert response.status_code == 200 assert response.data == expected -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_get_artist(f, db, logged_in_api_client, factories): - url = reverse('api:subsonic-get-artist') - assert url.endswith('getArtist') is True - artist = factories['music.Artist']() - albums = factories['music.Album'].create_batch(size=3, artist=artist) - expected = { - 'artist': serializers.GetArtistSerializer(artist).data - } - response = logged_in_api_client.get(url, {'id': artist.pk}) + url = reverse("api:subsonic-get-artist") + assert url.endswith("getArtist") is True + artist = factories["music.Artist"]() + albums = factories["music.Album"].create_batch(size=3, artist=artist) + expected = {"artist": serializers.GetArtistSerializer(artist).data} + 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']) +@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']() + 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}) + 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']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_get_album(f, db, logged_in_api_client, factories): - url = reverse('api:subsonic-get-album') - assert url.endswith('getAlbum') is True - artist = factories['music.Artist']() - album = factories['music.Album'](artist=artist) - tracks = factories['music.Track'].create_batch(size=3, album=album) - expected = { - 'album': serializers.GetAlbumSerializer(album).data - } - response = logged_in_api_client.get(url, {'f': f, 'id': album.pk}) + url = reverse("api:subsonic-get-album") + assert url.endswith("getAlbum") is True + artist = factories["music.Artist"]() + album = factories["music.Album"](artist=artist) + tracks = factories["music.Track"].create_batch(size=3, album=album) + expected = {"album": serializers.GetAlbumSerializer(album).data} + response = logged_in_api_client.get(url, {"f": f, "id": album.pk}) assert response.status_code == 200 assert response.data == expected -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_stream(f, db, logged_in_api_client, factories, mocker): - url = reverse('api:subsonic-stream') - mocked_serve = mocker.spy( - music_views, 'handle_serve') - assert url.endswith('stream') is True - artist = factories['music.Artist']() - album = factories['music.Album'](artist=artist) - track = factories['music.Track'](album=album) - tf = factories['music.TrackFile'](track=track) - response = logged_in_api_client.get(url, {'f': f, 'id': track.pk}) + url = reverse("api:subsonic-stream") + mocked_serve = mocker.spy(music_views, "handle_serve") + assert url.endswith("stream") is True + artist = factories["music.Artist"]() + album = factories["music.Album"](artist=artist) + track = factories["music.Track"](album=album) + tf = factories["music.TrackFile"](track=track) + response = logged_in_api_client.get(url, {"f": f, "id": track.pk}) - mocked_serve.assert_called_once_with( - track_file=tf - ) + mocked_serve.assert_called_once_with(track_file=tf) assert response.status_code == 200 -@pytest.mark.parametrize('f', ['xml', 'json']) +@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}) + 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'} + assert response.data == {"status": "ok"} - favorite = logged_in_api_client.user.track_favorites.latest('id') + favorite = logged_in_api_client.user.track_favorites.latest("id") assert favorite.track == track -@pytest.mark.parametrize('f', ['xml', 'json']) +@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}) + 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 response.data == {"status": "ok"} assert logged_in_api_client.user.track_favorites.count() == 0 -@pytest.mark.parametrize('f', ['xml', 'json']) +@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}) + 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 == { - 'starred2': { - 'song': serializers.get_starred_tracks_data([favorite]) - } + "starred2": {"song": serializers.get_starred_tracks_data([favorite])} } -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_get_starred(f, db, logged_in_api_client, factories): - url = reverse('api:subsonic-get-starred') - assert url.endswith('getStarred') 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}) + url = reverse("api:subsonic-get-starred") + assert url.endswith("getStarred") 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 == { - 'starred': { - 'song': serializers.get_starred_tracks_data([favorite]) - } + "starred": {"song": serializers.get_starred_tracks_data([favorite])} } -@pytest.mark.parametrize('f', ['xml', 'json']) +@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'}) + 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]) - } + "albumList2": {"album": serializers.get_album_list2_data([album2, album1])} } -@pytest.mark.parametrize('f', ['xml', 'json']) +@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') + 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'}) + 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') + 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]), + "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]), } } -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_get_playlists(f, db, logged_in_api_client, factories): - url = reverse('api:subsonic-get-playlists') - assert url.endswith('getPlaylists') is True - playlist = factories['playlists.Playlist']( - user=logged_in_api_client.user - ) - response = logged_in_api_client.get(url, {'f': f}) + url = reverse("api:subsonic-get-playlists") + assert url.endswith("getPlaylists") is True + playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) + response = logged_in_api_client.get(url, {"f": f}) qs = playlist.__class__.objects.with_tracks_count() assert response.status_code == 200 assert response.data == { - 'playlists': { - 'playlist': [serializers.get_playlist_data(qs.first())], - } + "playlists": {"playlist": [serializers.get_playlist_data(qs.first())]} } -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_get_playlist(f, db, logged_in_api_client, factories): - url = reverse('api:subsonic-get-playlist') - assert url.endswith('getPlaylist') is True - playlist = factories['playlists.Playlist']( - user=logged_in_api_client.user - ) - response = logged_in_api_client.get(url, {'f': f, 'id': playlist.pk}) + url = reverse("api:subsonic-get-playlist") + assert url.endswith("getPlaylist") is True + playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) + response = logged_in_api_client.get(url, {"f": f, "id": playlist.pk}) qs = playlist.__class__.objects.with_tracks_count() assert response.status_code == 200 assert response.data == { - 'playlist': serializers.get_playlist_detail_data(qs.first()) + "playlist": serializers.get_playlist_detail_data(qs.first()) } -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_update_playlist(f, db, logged_in_api_client, factories): - url = reverse('api:subsonic-update-playlist') - assert url.endswith('updatePlaylist') is True - playlist = factories['playlists.Playlist']( - user=logged_in_api_client.user - ) - plt = factories['playlists.PlaylistTrack']( - index=0, playlist=playlist) - new_track = factories['music.Track']() + url = reverse("api:subsonic-update-playlist") + assert url.endswith("updatePlaylist") is True + playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) + plt = factories["playlists.PlaylistTrack"](index=0, playlist=playlist) + new_track = factories["music.Track"]() response = logged_in_api_client.get( - url, { - 'f': f, - 'name': 'new_name', - 'playlistId': playlist.pk, - 'songIdToAdd': new_track.pk, - 'songIndexToRemove': 0}) + url, + { + "f": f, + "name": "new_name", + "playlistId": playlist.pk, + "songIdToAdd": new_track.pk, + "songIndexToRemove": 0, + }, + ) playlist.refresh_from_db() assert response.status_code == 200 - assert playlist.name == 'new_name' + assert playlist.name == "new_name" assert playlist.playlist_tracks.count() == 1 assert playlist.playlist_tracks.first().track_id == new_track.pk -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_delete_playlist(f, db, logged_in_api_client, factories): - url = reverse('api:subsonic-delete-playlist') - assert url.endswith('deletePlaylist') is True - playlist = factories['playlists.Playlist']( - user=logged_in_api_client.user - ) - response = logged_in_api_client.get( - url, {'f': f, 'id': playlist.pk}) + url = reverse("api:subsonic-delete-playlist") + assert url.endswith("deletePlaylist") is True + playlist = factories["playlists.Playlist"](user=logged_in_api_client.user) + response = logged_in_api_client.get(url, {"f": f, "id": playlist.pk}) assert response.status_code == 200 with pytest.raises(playlist.__class__.DoesNotExist): playlist.refresh_from_db() -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_create_playlist(f, db, logged_in_api_client, factories): - url = reverse('api:subsonic-create-playlist') - assert url.endswith('createPlaylist') is True - track1 = factories['music.Track']() - track2 = factories['music.Track']() + url = reverse("api:subsonic-create-playlist") + assert url.endswith("createPlaylist") is True + track1 = factories["music.Track"]() + track2 = factories["music.Track"]() response = logged_in_api_client.get( - url, {'f': f, 'name': 'hello', 'songId': [track1.pk, track2.pk]}) + url, {"f": f, "name": "hello", "songId": [track1.pk, track2.pk]} + ) assert response.status_code == 200 - playlist = logged_in_api_client.user.playlists.latest('id') + playlist = logged_in_api_client.user.playlists.latest("id") assert playlist.playlist_tracks.count() == 2 for i, t in enumerate([track1, track2]): plt = playlist.playlist_tracks.get(track=t) assert plt.index == i - assert playlist.name == 'hello' + assert playlist.name == "hello" qs = playlist.__class__.objects.with_tracks_count() assert response.data == { - 'playlist': serializers.get_playlist_detail_data(qs.first()) + "playlist": serializers.get_playlist_detail_data(qs.first()) } -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_get_music_folders(f, db, logged_in_api_client, factories): - url = reverse('api:subsonic-get-music-folders') - assert url.endswith('getMusicFolders') is True - response = logged_in_api_client.get(url, {'f': f}) + url = reverse("api:subsonic-get-music-folders") + assert url.endswith("getMusicFolders") is True + response = logged_in_api_client.get(url, {"f": f}) assert response.status_code == 200 assert response.data == { - 'musicFolders': { - 'musicFolder': [{ - 'id': 1, - 'name': 'Music' - }] - } + "musicFolders": {"musicFolder": [{"id": 1, "name": "Music"}]} } -@pytest.mark.parametrize('f', ['xml', 'json']) +@pytest.mark.parametrize("f", ["xml", "json"]) def test_get_indexes(f, db, logged_in_api_client, factories): - url = reverse('api:subsonic-get-indexes') - assert url.endswith('getIndexes') is True - artists = factories['music.Artist'].create_batch(size=10) + url = reverse("api:subsonic-get-indexes") + assert url.endswith("getIndexes") is True + artists = factories["music.Artist"].create_batch(size=10) expected = { - 'indexes': serializers.GetArtistsSerializer( + "indexes": serializers.GetArtistsSerializer( music_models.Artist.objects.all() ).data } @@ -394,27 +363,26 @@ def test_get_indexes(f, db, logged_in_api_client, factories): def test_get_cover_art_album(factories, logged_in_api_client): - url = reverse('api:subsonic-get-cover-art') - assert url.endswith('getCoverArt') is True - album = factories['music.Album']() - response = logged_in_api_client.get(url, {'id': 'al-{}'.format(album.pk)}) + url = reverse("api:subsonic-get-cover-art") + assert url.endswith("getCoverArt") is True + album = factories["music.Album"]() + response = logged_in_api_client.get(url, {"id": "al-{}".format(album.pk)}) assert response.status_code == 200 - assert response['Content-Type'] == '' - assert response['X-Accel-Redirect'] == music_views.get_file_path( + assert response["Content-Type"] == "" + assert response["X-Accel-Redirect"] == music_views.get_file_path( album.cover - ).decode('utf-8') + ).decode("utf-8") def test_scrobble(factories, logged_in_api_client): - tf = factories['music.TrackFile']() + tf = factories["music.TrackFile"]() track = tf.track - url = reverse('api:subsonic-scrobble') - assert url.endswith('scrobble') is True - response = logged_in_api_client.get( - url, {'id': track.pk, 'submission': True}) + url = reverse("api:subsonic-scrobble") + assert url.endswith("scrobble") is True + response = logged_in_api_client.get(url, {"id": track.pk, "submission": True}) assert response.status_code == 200 - l = logged_in_api_client.user.listenings.latest('id') + l = logged_in_api_client.user.listenings.latest("id") assert l.track == track diff --git a/api/tests/test_acoustid.py b/api/tests/test_acoustid.py index 1f7de9247..ab3dfd1d8 100644 --- a/api/tests/test_acoustid.py +++ b/api/tests/test_acoustid.py @@ -2,33 +2,42 @@ from funkwhale_api.providers.acoustid import get_acoustid_client def test_client_is_configured_with_correct_api_key(preferences): - api_key = 'hello world' - preferences['providers_acoustid__api_key'] = api_key + api_key = "hello world" + preferences["providers_acoustid__api_key"] = api_key client = get_acoustid_client() assert client.api_key == api_key def test_client_returns_raw_results(db, mocker, preferences): - api_key = 'test' - preferences['providers_acoustid__api_key'] = api_key + api_key = "test" + preferences["providers_acoustid__api_key"] = api_key payload = { - 'results': [ - {'id': 'e475bf79-c1ce-4441-bed7-1e33f226c0a2', - 'recordings': [ - {'artists': [ - {'id': '9c6bddde-6228-4d9f-ad0d-03f6fcb19e13', - 'name': 'Binärpilot'}], - 'duration': 268, - 'id': 'f269d497-1cc0-4ae4-a0c4-157ec7d73fcb', - 'title': 'Bend'}], - 'score': 0.860825}], - 'status': 'ok' + "results": [ + { + "id": "e475bf79-c1ce-4441-bed7-1e33f226c0a2", + "recordings": [ + { + "artists": [ + { + "id": "9c6bddde-6228-4d9f-ad0d-03f6fcb19e13", + "name": "Binärpilot", + } + ], + "duration": 268, + "id": "f269d497-1cc0-4ae4-a0c4-157ec7d73fcb", + "title": "Bend", + } + ], + "score": 0.860825, + } + ], + "status": "ok", } - m = mocker.patch('acoustid.match', return_value=payload) + m = mocker.patch("acoustid.match", return_value=payload) client = get_acoustid_client() - response = client.match('/tmp/noopfile.mp3') + response = client.match("/tmp/noopfile.mp3") assert response == payload - m.assert_called_once_with('test', '/tmp/noopfile.mp3', parse=False) + m.assert_called_once_with("test", "/tmp/noopfile.mp3", parse=False) diff --git a/api/tests/test_downloader.py b/api/tests/test_downloader.py index ede7bb16c..0a4134393 100644 --- a/api/tests/test_downloader.py +++ b/api/tests/test_downloader.py @@ -5,7 +5,7 @@ from funkwhale_api import downloader def test_can_download_audio_from_youtube_url_to_vorbis(tmpdir): data = downloader.download( - 'https://www.youtube.com/watch?v=tPEE9ZwTmy0', - target_directory=tmpdir) - assert data['audio_file_path'] == os.path.join(tmpdir, 'tPEE9ZwTmy0.ogg') - assert os.path.exists(data['audio_file_path']) + "https://www.youtube.com/watch?v=tPEE9ZwTmy0", target_directory=tmpdir + ) + assert data["audio_file_path"] == os.path.join(tmpdir, "tPEE9ZwTmy0.ogg") + assert os.path.exists(data["audio_file_path"]) diff --git a/api/tests/test_import_audio_file.py b/api/tests/test_import_audio_file.py index da3d1959c..4346a4777 100644 --- a/api/tests/test_import_audio_file.py +++ b/api/tests/test_import_audio_file.py @@ -9,193 +9,160 @@ from django.core.management.base import CommandError from funkwhale_api.providers.audiofile import tasks from funkwhale_api.music import tasks as music_tasks -DATA_DIR = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - 'files' -) +DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "files") def test_can_create_track_from_file_metadata_no_mbid(db, mocker): metadata = { - 'artist': ['Test artist'], - 'album': ['Test album'], - 'title': ['Test track'], - 'TRACKNUMBER': ['4'], - 'date': ['2012-08-15'], + "artist": ["Test artist"], + "album": ["Test album"], + "title": ["Test track"], + "TRACKNUMBER": ["4"], + "date": ["2012-08-15"], } - m1 = mocker.patch('mutagen.File', return_value=metadata) + m1 = mocker.patch("mutagen.File", return_value=metadata) m2 = mocker.patch( - 'funkwhale_api.music.metadata.Metadata.get_file_type', - return_value='OggVorbis', + "funkwhale_api.music.metadata.Metadata.get_file_type", return_value="OggVorbis" ) - track = tasks.import_track_data_from_path( - os.path.join(DATA_DIR, 'dummy_file.ogg')) + track = tasks.import_track_data_from_path(os.path.join(DATA_DIR, "dummy_file.ogg")) - assert track.title == metadata['title'][0] + assert track.title == metadata["title"][0] assert track.mbid is None assert track.position == 4 - assert track.album.title == metadata['album'][0] + assert track.album.title == metadata["album"][0] assert track.album.mbid is None assert track.album.release_date == datetime.date(2012, 8, 15) - assert track.artist.name == metadata['artist'][0] + assert track.artist.name == metadata["artist"][0] assert track.artist.mbid is None def test_can_create_track_from_file_metadata_mbid(factories, mocker): - album = factories['music.Album']() + album = factories["music.Album"]() mocker.patch( - 'funkwhale_api.music.models.Album.get_or_create_from_api', + "funkwhale_api.music.models.Album.get_or_create_from_api", return_value=(album, True), ) album_data = { - 'release': { - 'id': album.mbid, - 'medium-list': [ + "release": { + "id": album.mbid, + "medium-list": [ { - 'track-list': [ + "track-list": [ { - 'id': '03baca8b-855a-3c05-8f3d-d3235287d84d', - 'position': '4', - 'number': '4', - 'recording': { - 'id': '2109e376-132b-40ad-b993-2bb6812e19d4', - 'title': 'Teen Age Riot', + "id": "03baca8b-855a-3c05-8f3d-d3235287d84d", + "position": "4", + "number": "4", + "recording": { + "id": "2109e376-132b-40ad-b993-2bb6812e19d4", + "title": "Teen Age Riot", }, } ], - 'track-count': 1 + "track-count": 1, } ], } } - mocker.patch( - 'funkwhale_api.musicbrainz.api.releases.get', - return_value=album_data) - track_data = album_data['release']['medium-list'][0]['track-list'][0] + mocker.patch("funkwhale_api.musicbrainz.api.releases.get", return_value=album_data) + track_data = album_data["release"]["medium-list"][0]["track-list"][0] metadata = { - 'musicbrainz_albumid': [album.mbid], - 'musicbrainz_trackid': [track_data['recording']['id']], + "musicbrainz_albumid": [album.mbid], + "musicbrainz_trackid": [track_data["recording"]["id"]], } - m1 = mocker.patch('mutagen.File', return_value=metadata) + m1 = mocker.patch("mutagen.File", return_value=metadata) m2 = mocker.patch( - 'funkwhale_api.music.metadata.Metadata.get_file_type', - return_value='OggVorbis', + "funkwhale_api.music.metadata.Metadata.get_file_type", return_value="OggVorbis" ) - track = tasks.import_track_data_from_path( - os.path.join(DATA_DIR, 'dummy_file.ogg')) + track = tasks.import_track_data_from_path(os.path.join(DATA_DIR, "dummy_file.ogg")) - assert track.title == track_data['recording']['title'] - assert track.mbid == track_data['recording']['id'] + assert track.title == track_data["recording"]["title"] + assert track.mbid == track_data["recording"]["id"] assert track.position == 4 assert track.album == album assert track.artist == album.artist def test_management_command_requires_a_valid_username(factories, mocker): - path = os.path.join(DATA_DIR, 'dummy_file.ogg') - user = factories['users.User'](username='me') + path = os.path.join(DATA_DIR, "dummy_file.ogg") + user = factories["users.User"](username="me") mocker.patch( - 'funkwhale_api.providers.audiofile.management.commands.import_files.Command.do_import', # noqa - return_value=(mocker.MagicMock(), [])) + "funkwhale_api.providers.audiofile.management.commands.import_files.Command.do_import", # noqa + return_value=(mocker.MagicMock(), []), + ) with pytest.raises(CommandError): - call_command('import_files', path, username='not_me', interactive=False) - call_command('import_files', path, username='me', interactive=False) + call_command("import_files", path, username="not_me", interactive=False) + call_command("import_files", path, username="me", interactive=False) def test_in_place_import_only_from_music_dir(factories, settings): - user = factories['users.User'](username='me') - settings.MUSIC_DIRECTORY_PATH = '/nope' - path = os.path.join(DATA_DIR, 'dummy_file.ogg') + user = factories["users.User"](username="me") + settings.MUSIC_DIRECTORY_PATH = "/nope" + path = os.path.join(DATA_DIR, "dummy_file.ogg") with pytest.raises(CommandError): call_command( - 'import_files', - path, - in_place=True, - username='me', - interactive=False + "import_files", path, in_place=True, username="me", interactive=False ) def test_import_files_creates_a_batch_and_job(factories, mocker): - m = mocker.patch('funkwhale_api.music.tasks.import_job_run') - user = factories['users.User'](username='me') - path = os.path.join(DATA_DIR, 'dummy_file.ogg') - call_command( - 'import_files', - path, - username='me', - async=False, - interactive=False) + m = mocker.patch("funkwhale_api.music.tasks.import_job_run") + user = factories["users.User"](username="me") + path = os.path.join(DATA_DIR, "dummy_file.ogg") + call_command("import_files", path, username="me", async=False, interactive=False) - batch = user.imports.latest('id') - assert batch.source == 'shell' + batch = user.imports.latest("id") + assert batch.source == "shell" assert batch.jobs.count() == 1 job = batch.jobs.first() - assert job.status == 'pending' - with open(path, 'rb') as f: + assert job.status == "pending" + with open(path, "rb") as f: assert job.audio_file.read() == f.read() - assert job.source == 'file://' + path - m.assert_called_once_with( - import_job_id=job.pk, - use_acoustid=False) + assert job.source == "file://" + path + m.assert_called_once_with(import_job_id=job.pk, use_acoustid=False) def test_import_files_skip_if_path_already_imported(factories, mocker): - user = factories['users.User'](username='me') - path = os.path.join(DATA_DIR, 'dummy_file.ogg') - existing = factories['music.TrackFile']( - source='file://{}'.format(path)) + user = factories["users.User"](username="me") + path = os.path.join(DATA_DIR, "dummy_file.ogg") + existing = factories["music.TrackFile"](source="file://{}".format(path)) - call_command( - 'import_files', - path, - username='me', - async=False, - interactive=False) + call_command("import_files", path, username="me", async=False, interactive=False) assert user.imports.count() == 0 def test_import_files_works_with_utf8_file_name(factories, mocker): - m = mocker.patch('funkwhale_api.music.tasks.import_job_run') - user = factories['users.User'](username='me') - path = os.path.join(DATA_DIR, 'utf8-éà◌.ogg') - call_command( - 'import_files', - path, - username='me', - async=False, - interactive=False) - batch = user.imports.latest('id') + m = mocker.patch("funkwhale_api.music.tasks.import_job_run") + user = factories["users.User"](username="me") + path = os.path.join(DATA_DIR, "utf8-éà◌.ogg") + call_command("import_files", path, username="me", async=False, interactive=False) + batch = user.imports.latest("id") job = batch.jobs.first() - m.assert_called_once_with( - import_job_id=job.pk, - use_acoustid=False) + m.assert_called_once_with(import_job_id=job.pk, use_acoustid=False) def test_import_files_in_place(factories, mocker, settings): settings.MUSIC_DIRECTORY_PATH = DATA_DIR - m = mocker.patch('funkwhale_api.music.tasks.import_job_run') - user = factories['users.User'](username='me') - path = os.path.join(DATA_DIR, 'utf8-éà◌.ogg') + m = mocker.patch("funkwhale_api.music.tasks.import_job_run") + user = factories["users.User"](username="me") + path = os.path.join(DATA_DIR, "utf8-éà◌.ogg") call_command( - 'import_files', + "import_files", path, - username='me', + username="me", async=False, in_place=True, - interactive=False) - batch = user.imports.latest('id') + interactive=False, + ) + batch = user.imports.latest("id") job = batch.jobs.first() assert bool(job.audio_file) is False - m.assert_called_once_with( - import_job_id=job.pk, - use_acoustid=False) + m.assert_called_once_with(import_job_id=job.pk, use_acoustid=False) def test_storage_rename_utf_8_files(factories): - tf = factories['music.TrackFile'](audio_file__filename='été.ogg') - assert tf.audio_file.name.endswith('ete.ogg') + tf = factories["music.TrackFile"](audio_file__filename="été.ogg") + assert tf.audio_file.name.endswith("ete.ogg") diff --git a/api/tests/test_jwt_querystring.py b/api/tests/test_jwt_querystring.py index f18e6b729..18a673fb4 100644 --- a/api/tests/test_jwt_querystring.py +++ b/api/tests/test_jwt_querystring.py @@ -5,18 +5,15 @@ jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER -def test_can_authenticate_using_token_param_in_url( - factories, preferences, client): - user = factories['users.User']() - preferences['common__api_authentication_required'] = True - url = reverse('api:v1:tracks-list') +def test_can_authenticate_using_token_param_in_url(factories, preferences, client): + user = factories["users.User"]() + preferences["common__api_authentication_required"] = True + url = reverse("api:v1:tracks-list") response = client.get(url) assert response.status_code == 401 payload = jwt_payload_handler(user) token = jwt_encode_handler(payload) - response = client.get(url, data={ - 'jwt': token - }) + response = client.get(url, data={"jwt": token}) assert response.status_code == 200 diff --git a/api/tests/test_tasks.py b/api/tests/test_tasks.py index 16088d53f..d46c3a3fb 100644 --- a/api/tests/test_tasks.py +++ b/api/tests/test_tasks.py @@ -10,24 +10,25 @@ class Dummy: def test_require_instance_decorator(factories, mocker): - user = factories['users.User']() + user = factories["users.User"]() - @celery.require_instance(user.__class__, 'user') + @celery.require_instance(user.__class__, "user") def t(user): Dummy.noop(user) - m = mocker.patch.object(Dummy, 'noop') + m = mocker.patch.object(Dummy, "noop") t(user_id=user.pk) m.assert_called_once_with(user) def test_require_instance_decorator_accepts_qs(factories, mocker): - user = factories['users.User'](is_active=False) + user = factories["users.User"](is_active=False) qs = user.__class__.objects.filter(is_active=True) - @celery.require_instance(qs, 'user') + @celery.require_instance(qs, "user") def t(user): pass + with pytest.raises(user.__class__.DoesNotExist): t(user_id=user.pk) diff --git a/api/tests/test_youtube.py b/api/tests/test_youtube.py index 7ab6256da..629558cc3 100644 --- a/api/tests/test_youtube.py +++ b/api/tests/test_youtube.py @@ -8,24 +8,25 @@ from .data import youtube as api_data def test_can_get_search_results_from_youtube(mocker): mocker.patch( - 'funkwhale_api.providers.youtube.client._do_search', - return_value=api_data.search['8 bit adventure']) - query = '8 bit adventure' + "funkwhale_api.providers.youtube.client._do_search", + return_value=api_data.search["8 bit adventure"], + ) + query = "8 bit adventure" results = client.search(query) - assert results[0]['id']['videoId'] == '0HxZn6CzOIo' - assert results[0]['snippet']['title'] == 'AdhesiveWombat - 8 Bit Adventure' - assert results[0]['full_url'] == 'https://www.youtube.com/watch?v=0HxZn6CzOIo' + assert results[0]["id"]["videoId"] == "0HxZn6CzOIo" + assert results[0]["snippet"]["title"] == "AdhesiveWombat - 8 Bit Adventure" + assert results[0]["full_url"] == "https://www.youtube.com/watch?v=0HxZn6CzOIo" -def test_can_get_search_results_from_funkwhale( - preferences, mocker, api_client, db): - preferences['common__api_authentication_required'] = False +def test_can_get_search_results_from_funkwhale(preferences, mocker, api_client, db): + preferences["common__api_authentication_required"] = False mocker.patch( - 'funkwhale_api.providers.youtube.client._do_search', - return_value=api_data.search['8 bit adventure']) - query = '8 bit adventure' - url = reverse('api:v1:providers:youtube:search') - response = api_client.get(url, {'query': query}) + "funkwhale_api.providers.youtube.client._do_search", + return_value=api_data.search["8 bit adventure"], + ) + query = "8 bit adventure" + url = reverse("api:v1:providers:youtube:search") + response = api_client.get(url, {"query": query}) # we should cast the youtube result to something more generic expected = { "id": "0HxZn6CzOIo", @@ -36,7 +37,7 @@ def test_can_get_search_results_from_funkwhale( "title": "AdhesiveWombat - 8 Bit Adventure", "channelTitle": "AdhesiveWombat", "publishedAt": "2012-08-22T18:41:03.000Z", - "cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg" + "cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg", } assert response.data[0] == expected @@ -44,41 +45,37 @@ def test_can_get_search_results_from_funkwhale( def test_can_send_multiple_queries_at_once(mocker): mocker.patch( - 'funkwhale_api.providers.youtube.client._do_search', + "funkwhale_api.providers.youtube.client._do_search", side_effect=[ - api_data.search['8 bit adventure'], - api_data.search['system of a down toxicity'], - ] + api_data.search["8 bit adventure"], + api_data.search["system of a down toxicity"], + ], ) queries = OrderedDict() - queries['1'] = { - 'q': '8 bit adventure', - } - queries['2'] = { - 'q': 'system of a down toxicity', - } + queries["1"] = {"q": "8 bit adventure"} + queries["2"] = {"q": "system of a down toxicity"} results = client.search_multiple(queries) - assert results['1'][0]['id']['videoId'] == '0HxZn6CzOIo' - assert results['1'][0]['snippet']['title'] == 'AdhesiveWombat - 8 Bit Adventure' - assert results['1'][0]['full_url'] == 'https://www.youtube.com/watch?v=0HxZn6CzOIo' - assert results['2'][0]['id']['videoId'] == 'BorYwGi2SJc' - assert results['2'][0]['snippet']['title'] == 'System of a Down: Toxicity' - assert results['2'][0]['full_url'] == 'https://www.youtube.com/watch?v=BorYwGi2SJc' + assert results["1"][0]["id"]["videoId"] == "0HxZn6CzOIo" + assert results["1"][0]["snippet"]["title"] == "AdhesiveWombat - 8 Bit Adventure" + assert results["1"][0]["full_url"] == "https://www.youtube.com/watch?v=0HxZn6CzOIo" + assert results["2"][0]["id"]["videoId"] == "BorYwGi2SJc" + assert results["2"][0]["snippet"]["title"] == "System of a Down: Toxicity" + assert results["2"][0]["full_url"] == "https://www.youtube.com/watch?v=BorYwGi2SJc" def test_can_send_multiple_queries_at_once_from_funwkhale( - preferences, mocker, db, api_client): - preferences['common__api_authentication_required'] = False + preferences, mocker, db, api_client +): + preferences["common__api_authentication_required"] = False mocker.patch( - 'funkwhale_api.providers.youtube.client._do_search', - return_value=api_data.search['8 bit adventure']) + "funkwhale_api.providers.youtube.client._do_search", + return_value=api_data.search["8 bit adventure"], + ) queries = OrderedDict() - queries['1'] = { - 'q': '8 bit adventure', - } + queries["1"] = {"q": "8 bit adventure"} expected = { "id": "0HxZn6CzOIo", @@ -89,10 +86,10 @@ def test_can_send_multiple_queries_at_once_from_funwkhale( "title": "AdhesiveWombat - 8 Bit Adventure", "channelTitle": "AdhesiveWombat", "publishedAt": "2012-08-22T18:41:03.000Z", - "cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg" + "cover": "https://i.ytimg.com/vi/0HxZn6CzOIo/hqdefault.jpg", } - url = reverse('api:v1:providers:youtube:searchs') - response = api_client.post(url, queries, format='json') + url = reverse("api:v1:providers:youtube:searchs") + response = api_client.post(url, queries, format="json") - assert expected == response.data['1'][0] + assert expected == response.data["1"][0] diff --git a/api/tests/users/test_activity.py b/api/tests/users/test_activity.py index 26d0b11f8..cfacff997 100644 --- a/api/tests/users/test_activity.py +++ b/api/tests/users/test_activity.py @@ -2,13 +2,14 @@ from funkwhale_api.users import serializers def test_get_user_activity_url(settings, factories): - user = factories['users.User']() - assert user.get_activity_url() == '{}/@{}'.format( - settings.FUNKWHALE_URL, user.username) + user = factories["users.User"]() + assert user.get_activity_url() == "{}/@{}".format( + settings.FUNKWHALE_URL, user.username + ) def test_activity_user_serializer(factories): - user = factories['users.User']() + user = factories["users.User"]() expected = { "type": "Person", diff --git a/api/tests/users/test_admin.py b/api/tests/users/test_admin.py index 7645a0295..03b316eb0 100644 --- a/api/tests/users/test_admin.py +++ b/api/tests/users/test_admin.py @@ -3,28 +3,24 @@ from funkwhale_api.users.admin import MyUserCreationForm def test_clean_username_success(db): # Instantiate the form with a new username - form = MyUserCreationForm({ - 'username': 'alamode', - 'password1': '123456', - 'password2': '123456', - }) + form = MyUserCreationForm( + {"username": "alamode", "password1": "123456", "password2": "123456"} + ) # Run is_valid() to trigger the validation valid = form.is_valid() assert valid # Run the actual clean_username method username = form.clean_username() - assert 'alamode' == username + assert "alamode" == username def test_clean_username_false(factories): - user = factories['users.User']() + user = factories["users.User"]() # Instantiate the form with the same username as self.user - form = MyUserCreationForm({ - 'username': user.username, - 'password1': '123456', - 'password2': '123456', - }) + form = MyUserCreationForm( + {"username": user.username, "password1": "123456", "password2": "123456"} + ) # Run is_valid() to trigger the validation, which is going to fail # because the username is already taken valid = form.is_valid() @@ -32,4 +28,4 @@ def test_clean_username_false(factories): # The form.errors dict should contain a single error called 'username' assert len(form.errors) == 1 - assert 'username' in form.errors + assert "username" in form.errors diff --git a/api/tests/users/test_jwt.py b/api/tests/users/test_jwt.py index d264494e5..28ff09d74 100644 --- a/api/tests/users/test_jwt.py +++ b/api/tests/users/test_jwt.py @@ -6,8 +6,9 @@ from rest_framework_jwt.settings import api_settings from funkwhale_api.users.models import User + def test_can_invalidate_token_when_changing_user_secret_key(factories): - user = factories['users.User']() + user = factories["users.User"]() u1 = user.secret_key jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py index 42123b5e8..c73a4a1b1 100644 --- a/api/tests/users/test_models.py +++ b/api/tests/users/test_models.py @@ -4,26 +4,26 @@ from funkwhale_api.users import models def test__str__(factories): - user = factories['users.User'](username='hello') - assert user.__str__() == 'hello' + user = factories["users.User"](username="hello") + assert user.__str__() == "hello" def test_changing_password_updates_subsonic_api_token_no_token(factories): - user = factories['users.User'](subsonic_api_token=None) - user.set_password('new') + user = factories["users.User"](subsonic_api_token=None) + user.set_password("new") assert user.subsonic_api_token is None def test_changing_password_updates_subsonic_api_token(factories): - user = factories['users.User'](subsonic_api_token='test') - user.set_password('new') + user = factories["users.User"](subsonic_api_token="test") + user.set_password("new") assert user.subsonic_api_token is not None - assert user.subsonic_api_token != 'test' + assert user.subsonic_api_token != "test" def test_get_permissions_superuser(factories): - user = factories['users.User'](is_superuser=True) + user = factories["users.User"](is_superuser=True) perms = user.get_permissions() for p in models.PERMISSIONS: @@ -31,44 +31,50 @@ def test_get_permissions_superuser(factories): def test_get_permissions_regular(factories): - user = factories['users.User'](permission_library=True) + user = factories["users.User"](permission_library=True) perms = user.get_permissions() for p in models.PERMISSIONS: - if p == 'library': + if p == "library": assert perms[p] is True else: assert perms[p] is False def test_get_permissions_default(factories, preferences): - preferences['users__default_permissions'] = ['upload', 'federation'] - user = factories['users.User']() + preferences["users__default_permissions"] = ["upload", "federation"] + user = factories["users.User"]() perms = user.get_permissions() - assert perms['upload'] is True - assert perms['federation'] is True - assert perms['library'] is False - assert perms['settings'] is False + assert perms["upload"] is True + assert perms["federation"] is True + assert perms["library"] is False + assert perms["settings"] is False -@pytest.mark.parametrize('args,perms,expected', [ - ({'is_superuser': True}, ['federation', 'library'], True), - ({'is_superuser': False}, ['federation'], False), - ({'permission_library': True}, ['library'], True), - ({'permission_library': True}, ['library', 'federation'], False), -]) +@pytest.mark.parametrize( + "args,perms,expected", + [ + ({"is_superuser": True}, ["federation", "library"], True), + ({"is_superuser": False}, ["federation"], False), + ({"permission_library": True}, ["library"], True), + ({"permission_library": True}, ["library", "federation"], False), + ], +) def test_has_permissions_and(args, perms, expected, factories): - user = factories['users.User'](**args) - assert user.has_permissions(*perms, operator='and') is expected + user = factories["users.User"](**args) + assert user.has_permissions(*perms, operator="and") is expected -@pytest.mark.parametrize('args,perms,expected', [ - ({'is_superuser': True}, ['federation', 'library'], True), - ({'is_superuser': False}, ['federation'], False), - ({'permission_library': True}, ['library', 'federation'], True), - ({'permission_library': True}, ['federation'], False), -]) +@pytest.mark.parametrize( + "args,perms,expected", + [ + ({"is_superuser": True}, ["federation", "library"], True), + ({"is_superuser": False}, ["federation"], False), + ({"permission_library": True}, ["library", "federation"], True), + ({"permission_library": True}, ["federation"], False), + ], +) def test_has_permissions_or(args, perms, expected, factories): - user = factories['users.User'](**args) - assert user.has_permissions(*perms, operator='or') is expected + user = factories["users.User"](**args) + assert user.has_permissions(*perms, operator="or") is expected diff --git a/api/tests/users/test_permissions.py b/api/tests/users/test_permissions.py index 518ccd1c8..f4b501018 100644 --- a/api/tests/users/test_permissions.py +++ b/api/tests/users/test_permissions.py @@ -7,76 +7,88 @@ from funkwhale_api.users import permissions def test_has_user_permission_no_user(api_request): view = APIView.as_view() permission = permissions.HasUserPermission() - request = api_request.get('/') + request = api_request.get("/") assert permission.has_permission(request, view) is False def test_has_user_permission_anonymous(anonymous_user, api_request): view = APIView.as_view() permission = permissions.HasUserPermission() - request = api_request.get('/') - setattr(request, 'user', anonymous_user) + request = api_request.get("/") + setattr(request, "user", anonymous_user) assert permission.has_permission(request, view) is False -@pytest.mark.parametrize('value', [True, False]) +@pytest.mark.parametrize("value", [True, False]) def test_has_user_permission_logged_in_single(value, factories, api_request): - user = factories['users.User'](permission_federation=value) + user = factories["users.User"](permission_federation=value) class View(APIView): - required_permissions = ['federation'] + required_permissions = ["federation"] + view = View() permission = permissions.HasUserPermission() - request = api_request.get('/') - setattr(request, 'user', user) + request = api_request.get("/") + setattr(request, "user", user) result = permission.has_permission(request, view) - assert result == user.has_permissions('federation') == value + assert result == user.has_permissions("federation") == value -@pytest.mark.parametrize('federation,library,expected', [ - (True, False, False), - (False, True, False), - (False, False, False), - (True, True, True), -]) +@pytest.mark.parametrize( + "federation,library,expected", + [ + (True, False, False), + (False, True, False), + (False, False, False), + (True, True, True), + ], +) def test_has_user_permission_logged_in_multiple_and( - federation, library, expected, factories, api_request): - user = factories['users.User']( - permission_federation=federation, - permission_library=library, + federation, library, expected, factories, api_request +): + user = factories["users.User"]( + permission_federation=federation, permission_library=library ) class View(APIView): - required_permissions = ['federation', 'library'] - permission_operator = 'and' + required_permissions = ["federation", "library"] + permission_operator = "and" + view = View() permission = permissions.HasUserPermission() - request = api_request.get('/') - setattr(request, 'user', user) + request = api_request.get("/") + setattr(request, "user", user) result = permission.has_permission(request, view) - assert result == user.has_permissions('federation', 'library') == expected + assert result == user.has_permissions("federation", "library") == expected -@pytest.mark.parametrize('federation,library,expected', [ - (True, False, True), - (False, True, True), - (False, False, False), - (True, True, True), -]) +@pytest.mark.parametrize( + "federation,library,expected", + [ + (True, False, True), + (False, True, True), + (False, False, False), + (True, True, True), + ], +) def test_has_user_permission_logged_in_multiple_or( - federation, library, expected, factories, api_request): - user = factories['users.User']( - permission_federation=federation, - permission_library=library, + federation, library, expected, factories, api_request +): + user = factories["users.User"]( + permission_federation=federation, permission_library=library ) class View(APIView): - required_permissions = ['federation', 'library'] - permission_operator = 'or' + required_permissions = ["federation", "library"] + permission_operator = "or" + view = View() permission = permissions.HasUserPermission() - request = api_request.get('/') - setattr(request, 'user', user) + request = api_request.get("/") + setattr(request, "user", user) result = permission.has_permission(request, view) - assert result == user.has_permissions( - 'federation', 'library', operator='or') == expected + assert ( + result + == user.has_permissions("federation", "library", operator="or") + == expected + ) diff --git a/api/tests/users/test_views.py b/api/tests/users/test_views.py index 6418889ce..f7a11367e 100644 --- a/api/tests/users/test_views.py +++ b/api/tests/users/test_views.py @@ -8,91 +8,83 @@ from funkwhale_api.users.models import User def test_can_create_user_via_api(preferences, api_client, db): - url = reverse('rest_register') + url = reverse("rest_register") data = { - 'username': 'test1', - 'email': 'test1@test.com', - 'password1': 'testtest', - 'password2': 'testtest', + "username": "test1", + "email": "test1@test.com", + "password1": "testtest", + "password2": "testtest", } - preferences['users__registration_enabled'] = True + preferences["users__registration_enabled"] = True response = api_client.post(url, data) assert response.status_code == 201 - u = User.objects.get(email='test1@test.com') - assert u.username == 'test1' + u = User.objects.get(email="test1@test.com") + assert u.username == "test1" def test_can_restrict_usernames(settings, preferences, db, api_client): - url = reverse('rest_register') - preferences['users__registration_enabled'] = True - settings.USERNAME_BLACKLIST = ['funkwhale'] + url = reverse("rest_register") + preferences["users__registration_enabled"] = True + settings.USERNAME_BLACKLIST = ["funkwhale"] data = { - 'username': 'funkwhale', - 'email': 'contact@funkwhale.io', - 'password1': 'testtest', - 'password2': 'testtest', + "username": "funkwhale", + "email": "contact@funkwhale.io", + "password1": "testtest", + "password2": "testtest", } response = api_client.post(url, data) assert response.status_code == 400 - assert 'username' in response.data + assert "username" in response.data def test_can_disable_registration_view(preferences, api_client, db): - url = reverse('rest_register') + url = reverse("rest_register") data = { - 'username': 'test1', - 'email': 'test1@test.com', - 'password1': 'testtest', - 'password2': 'testtest', + "username": "test1", + "email": "test1@test.com", + "password1": "testtest", + "password2": "testtest", } - preferences['users__registration_enabled'] = False + preferences["users__registration_enabled"] = False response = api_client.post(url, data) assert response.status_code == 403 def test_can_fetch_data_from_api(api_client, factories): - url = reverse('api:v1:users:users-me') + url = reverse("api:v1:users:users-me") response = api_client.get(url) # login required assert response.status_code == 401 - user = factories['users.User']( - permission_library=True - ) - api_client.login(username=user.username, password='test') + user = factories["users.User"](permission_library=True) + api_client.login(username=user.username, password="test") response = api_client.get(url) assert response.status_code == 200 - assert response.data['username'] == user.username - assert response.data['is_staff'] == user.is_staff - assert response.data['is_superuser'] == user.is_superuser - assert response.data['email'] == user.email - assert response.data['name'] == user.name - assert response.data['permissions'] == user.get_permissions() + assert response.data["username"] == user.username + assert response.data["is_staff"] == user.is_staff + assert response.data["is_superuser"] == user.is_superuser + assert response.data["email"] == user.email + assert response.data["name"] == user.name + assert response.data["permissions"] == user.get_permissions() def test_can_get_token_via_api(api_client, factories): - user = factories['users.User']() - url = reverse('api:v1:token') - payload = { - 'username': user.username, - 'password': 'test' - } + user = factories["users.User"]() + url = reverse("api:v1:token") + payload = {"username": user.username, "password": "test"} response = api_client.post(url, payload) assert response.status_code == 200 - assert 'token' in response.data + assert "token" in response.data def test_can_get_token_via_api_inactive(api_client, factories): - user = factories['users.User'](is_active=False) - url = reverse('api:v1:token') - payload = { - 'username': user.username, - 'password': 'test' - } + user = factories["users.User"](is_active=False) + url = reverse("api:v1:token") + payload = {"username": user.username, "password": "test"} response = api_client.post(url, payload) assert response.status_code == 400 @@ -100,34 +92,27 @@ def test_can_get_token_via_api_inactive(api_client, factories): def test_can_refresh_token_via_api(api_client, factories, mocker): # first, we get a token - user = factories['users.User']() - url = reverse('api:v1:token') - payload = { - 'username': user.username, - 'password': 'test' - } + user = factories["users.User"]() + url = reverse("api:v1:token") + payload = {"username": user.username, "password": "test"} response = api_client.post(url, payload) assert response.status_code == 200 - token = response.data['token'] - url = reverse('api:v1:token_refresh') - response = api_client.post(url, {'token': token}) + token = response.data["token"] + url = reverse("api:v1:token_refresh") + response = api_client.post(url, {"token": token}) assert response.status_code == 200 - assert 'token' in response.data + assert "token" in response.data def test_changing_password_updates_secret_key(logged_in_api_client): user = logged_in_api_client.user password = user.password secret_key = user.secret_key - payload = { - 'old_password': 'test', - 'new_password1': 'new', - 'new_password2': 'new', - } - url = reverse('change_password') + payload = {"old_password": "test", "new_password1": "new", "new_password2": "new"} + url = reverse("change_password") response = logged_in_api_client.post(url, payload) @@ -137,14 +122,11 @@ def test_changing_password_updates_secret_key(logged_in_api_client): assert user.password != password -def test_can_request_password_reset( - factories, api_client, mailoutbox): - user = factories['users.User']() - payload = { - 'email': user.email, - } +def test_can_request_password_reset(factories, api_client, mailoutbox): + user = factories["users.User"]() + payload = {"email": user.email} emails = len(mailoutbox) - url = reverse('rest_password_reset') + url = reverse("rest_password_reset") response = api_client.post(url, payload) assert response.status_code == 200 @@ -153,86 +135,76 @@ def test_can_request_password_reset( def test_user_can_patch_his_own_settings(logged_in_api_client): user = logged_in_api_client.user - payload = { - 'privacy_level': 'me', - } - url = reverse( - 'api:v1:users:users-detail', - kwargs={'username': user.username}) + payload = {"privacy_level": "me"} + url = reverse("api:v1:users:users-detail", kwargs={"username": user.username}) response = logged_in_api_client.patch(url, payload) assert response.status_code == 200 user.refresh_from_db() - assert user.privacy_level == 'me' + assert user.privacy_level == "me" def test_user_can_request_new_subsonic_token(logged_in_api_client): user = logged_in_api_client.user - user.subsonic_api_token = 'test' + user.subsonic_api_token = "test" user.save() url = reverse( - 'api:v1:users:users-subsonic-token', - kwargs={'username': user.username}) + "api:v1:users:users-subsonic-token", kwargs={"username": user.username} + ) response = logged_in_api_client.post(url) assert response.status_code == 200 user.refresh_from_db() - assert user.subsonic_api_token != 'test' + assert user.subsonic_api_token != "test" assert user.subsonic_api_token is not None - assert response.data == { - 'subsonic_api_token': user.subsonic_api_token - } + assert response.data == {"subsonic_api_token": user.subsonic_api_token} def test_user_can_get_new_subsonic_token(logged_in_api_client): user = logged_in_api_client.user - user.subsonic_api_token = 'test' + user.subsonic_api_token = "test" user.save() url = reverse( - 'api:v1:users:users-subsonic-token', - kwargs={'username': user.username}) + "api:v1:users:users-subsonic-token", kwargs={"username": user.username} + ) response = logged_in_api_client.get(url) assert response.status_code == 200 - assert response.data == { - 'subsonic_api_token': 'test' - } + assert response.data == {"subsonic_api_token": "test"} def test_user_can_request_new_subsonic_token(logged_in_api_client): user = logged_in_api_client.user - user.subsonic_api_token = 'test' + user.subsonic_api_token = "test" user.save() url = reverse( - 'api:v1:users:users-subsonic-token', - kwargs={'username': user.username}) + "api:v1:users:users-subsonic-token", kwargs={"username": user.username} + ) response = logged_in_api_client.post(url) assert response.status_code == 200 user.refresh_from_db() - assert user.subsonic_api_token != 'test' + assert user.subsonic_api_token != "test" assert user.subsonic_api_token is not None - assert response.data == { - 'subsonic_api_token': user.subsonic_api_token - } + assert response.data == {"subsonic_api_token": user.subsonic_api_token} def test_user_can_delete_subsonic_token(logged_in_api_client): user = logged_in_api_client.user - user.subsonic_api_token = 'test' + user.subsonic_api_token = "test" user.save() url = reverse( - 'api:v1:users:users-subsonic-token', - kwargs={'username': user.username}) + "api:v1:users:users-subsonic-token", kwargs={"username": user.username} + ) response = logged_in_api_client.delete(url) @@ -241,16 +213,11 @@ def test_user_can_delete_subsonic_token(logged_in_api_client): assert user.subsonic_api_token is None -@pytest.mark.parametrize('method', ['put', 'patch']) -def test_user_cannot_patch_another_user( - method, logged_in_api_client, factories): - user = factories['users.User']() - payload = { - 'privacy_level': 'me', - } - url = reverse( - 'api:v1:users:users-detail', - kwargs={'username': user.username}) +@pytest.mark.parametrize("method", ["put", "patch"]) +def test_user_cannot_patch_another_user(method, logged_in_api_client, factories): + user = factories["users.User"]() + payload = {"privacy_level": "me"} + url = reverse("api:v1:users:users-detail", kwargs={"username": user.username}) handler = getattr(logged_in_api_client, method) response = handler(url, payload)