diff --git a/api/config/api_urls.py b/api/config/api_urls.py index ff6db0d069395c316d207a640e0187ccf92b12df..cab6805b67e394838ec942ecf8b162edb08a88cf 100644 --- a/api/config/api_urls.py +++ b/api/config/api_urls.py @@ -1,5 +1,6 @@ from rest_framework import routers from django.conf.urls import include, url +from funkwhale_api.activity import views as activity_views from funkwhale_api.instance import views as instance_views from funkwhale_api.music import views from funkwhale_api.playlists import views as playlists_views @@ -10,6 +11,7 @@ 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') diff --git a/api/funkwhale_api/activity/serializers.py b/api/funkwhale_api/activity/serializers.py index 325d1e820db5699abca69b57b3421b0e0ca1d68b..fd9b185cf9a6d3891f0356208f62b2bb54e8686f 100644 --- a/api/funkwhale_api/activity/serializers.py +++ b/api/funkwhale_api/activity/serializers.py @@ -1,5 +1,7 @@ from rest_framework import serializers +from funkwhale_api.activity import record + class ModelSerializer(serializers.ModelSerializer): id = serializers.CharField(source='get_activity_url') @@ -8,3 +10,15 @@ class ModelSerializer(serializers.ModelSerializer): def get_url(self, obj): return self.get_id(obj) + + +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 + ) + return serializer.data diff --git a/api/funkwhale_api/activity/utils.py b/api/funkwhale_api/activity/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..46336930ef693e29d9d5696ee3ccf12446c2bdad --- /dev/null +++ b/api/funkwhale_api/activity/utils.py @@ -0,0 +1,64 @@ +from django.db import models + +from funkwhale_api.common import fields +from funkwhale_api.favorites.models import TrackFavorite +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') + } + querysets = { + k: qs.annotate( + __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'] + }) + # Now we bulk-load each object type in turn + to_load = {} + for record in records: + to_load.setdefault(record['type'], []).append(record['pk']) + fetched = {} + + for key, pks in to_load.items(): + for item in source_querysets[key].filter(pk__in=pks): + fetched[(key, item.pk)] = item + + # Annotate 'records' with loaded objects + for record in records: + 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') + querysets = [ + Listening.objects.filter(query).select_related( + 'track', + 'user', + 'track__artist', + 'track__album__artist', + ), + TrackFavorite.objects.filter(query).select_related( + 'track', + 'user', + 'track__artist', + 'track__album__artist', + ), + ] + records = combined_recent(limit=limit, querysets=querysets) + return [r['object'] for r in records] diff --git a/api/funkwhale_api/activity/views.py b/api/funkwhale_api/activity/views.py new file mode 100644 index 0000000000000000000000000000000000000000..e66de1ccfdc94f51cd823fa5c6b104488a4aad7f --- /dev/null +++ b/api/funkwhale_api/activity/views.py @@ -0,0 +1,20 @@ +from rest_framework import viewsets +from rest_framework.response import Response + +from funkwhale_api.common.permissions import ConditionalAuthentication +from funkwhale_api.favorites.models import TrackFavorite + +from . import serializers +from . import utils + + +class ActivityViewSet(viewsets.GenericViewSet): + + serializer_class = serializers.AutoSerializer + permission_classes = [ConditionalAuthentication] + queryset = TrackFavorite.objects.none() + + 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) diff --git a/api/funkwhale_api/common/fields.py b/api/funkwhale_api/common/fields.py index ef9f840dc763409c8a1555d693d1939030877fd5..1a18b5f27d1e1839ef32722499de6b4365d6ac55 100644 --- a/api/funkwhale_api/common/fields.py +++ b/api/funkwhale_api/common/fields.py @@ -22,6 +22,6 @@ def privacy_level_query(user, lookup_field='privacy_level'): return models.Q(**{ '{}__in'.format(lookup_field): [ - 'me', 'followers', 'instance', 'everyone' + 'followers', 'instance', 'everyone' ] }) diff --git a/api/funkwhale_api/history/admin.py b/api/funkwhale_api/history/admin.py index 6d0480e73b4209629499c7ae26b8c5efa8348999..5ddfb899848f389d776632eaf9e3b6d389cd7f58 100644 --- a/api/funkwhale_api/history/admin.py +++ b/api/funkwhale_api/history/admin.py @@ -4,7 +4,7 @@ from . import models @admin.register(models.Listening) class ListeningAdmin(admin.ModelAdmin): - list_display = ['track', 'end_date', 'user', 'session_key'] + list_display = ['track', 'creation_date', 'user', 'session_key'] search_fields = ['track__name', 'user__username'] list_select_related = [ 'user', diff --git a/api/funkwhale_api/history/migrations/0002_auto_20180325_1433.py b/api/funkwhale_api/history/migrations/0002_auto_20180325_1433.py new file mode 100644 index 0000000000000000000000000000000000000000..d83dbb0a466b668279619e53406b8ae977ab5dc7 --- /dev/null +++ b/api/funkwhale_api/history/migrations/0002_auto_20180325_1433.py @@ -0,0 +1,22 @@ +# Generated by Django 2.0.3 on 2018-03-25 14:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('history', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='listening', + options={'ordering': ('-creation_date',)}, + ), + migrations.RenameField( + 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 56310ddc0d2546784bed02952fe6142a2d139858..762d5bf7b2cf66bdd9a96325c630db65a53ddaae 100644 --- a/api/funkwhale_api/history/models.py +++ b/api/funkwhale_api/history/models.py @@ -6,7 +6,8 @@ from funkwhale_api.music.models import Track class Listening(models.Model): - end_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) user = models.ForeignKey( @@ -18,7 +19,7 @@ class Listening(models.Model): session_key = models.CharField(max_length=100, null=True, blank=True) class Meta: - ordering = ('-end_date',) + ordering = ('-creation_date',) def save(self, **kwargs): if not self.user and not self.session_key: diff --git a/api/funkwhale_api/history/serializers.py b/api/funkwhale_api/history/serializers.py index 7a2280cea2a236357982f309b7218c3e0d073299..8fe6fa6e01f07a395f2c337ea45591bd315a03d3 100644 --- a/api/funkwhale_api/history/serializers.py +++ b/api/funkwhale_api/history/serializers.py @@ -12,7 +12,7 @@ class ListeningActivitySerializer(activity_serializers.ModelSerializer): type = serializers.SerializerMethodField() object = TrackActivitySerializer(source='track') actor = UserActivitySerializer(source='user') - published = serializers.DateTimeField(source='end_date') + published = serializers.DateTimeField(source='creation_date') class Meta: model = models.Listening @@ -36,7 +36,7 @@ class ListeningSerializer(serializers.ModelSerializer): class Meta: model = models.Listening - fields = ('id', 'user', 'session_key', 'track', 'end_date') + fields = ('id', 'user', 'session_key', 'track', 'creation_date') def create(self, validated_data): diff --git a/api/tests/activity/__init__.py b/api/tests/activity/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/tests/activity/test_serializers.py b/api/tests/activity/test_serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..792fa74b9cbb3ed778c5e84bd746fb210e738acf --- /dev/null +++ b/api/tests/activity/test_serializers.py @@ -0,0 +1,17 @@ +from funkwhale_api.activity import serializers +from funkwhale_api.favorites.serializers import TrackFavoriteActivitySerializer +from funkwhale_api.history.serializers import \ + ListeningActivitySerializer + + +def test_autoserializer(factories): + favorite = factories['favorites.TrackFavorite']() + listening = factories['history.Listening']() + objects = [favorite, listening] + serializer = serializers.AutoSerializer(objects, many=True) + expected = [ + TrackFavoriteActivitySerializer(favorite).data, + ListeningActivitySerializer(listening).data, + ] + + assert serializer.data == expected diff --git a/api/tests/activity/test_utils.py b/api/tests/activity/test_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..43bb45df84931ccd3ba2a56e555b991627c3a62c --- /dev/null +++ b/api/tests/activity/test_utils.py @@ -0,0 +1,21 @@ +from funkwhale_api.activity import utils + + +def test_get_activity(factories): + 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') + + 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 new file mode 100644 index 0000000000000000000000000000000000000000..bdc3c6339ffe91981621c8f8272788347a01cc8e --- /dev/null +++ b/api/tests/activity/test_views.py @@ -0,0 +1,18 @@ +from django.urls import reverse + +from funkwhale_api.activity import serializers +from funkwhale_api.activity import utils + + +def test_activity_view(factories, api_client, settings, anonymous_user): + settings.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 diff --git a/api/tests/channels/__init__.py b/api/tests/channels/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/tests/common/__init__.py b/api/tests/common/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/tests/common/test_fields.py b/api/tests/common/test_fields.py index 7c63431a38ae4e5eaf41c5285966afd80a2dd6e2..29a8fb05c4ead7d9be810de7b64ba430a17f7fee 100644 --- a/api/tests/common/test_fields.py +++ b/api/tests/common/test_fields.py @@ -10,7 +10,7 @@ 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=['me', 'followers', 'instance', 'everyone'])), + Q(privacy_level__in=['followers', 'instance', 'everyone'])), ]) def test_privacy_level_query(user,expected): query = fields.privacy_level_query(user) diff --git a/api/tests/common/test_permissions.py b/api/tests/common/test_permissions.py index b5c5160f8accdf6e0bbeb29f9ee4d464962dff5b..f04f12e0b0e19a75992ad364eee4f05219d0e3f7 100644 --- a/api/tests/common/test_permissions.py +++ b/api/tests/common/test_permissions.py @@ -2,7 +2,6 @@ import pytest from rest_framework.views import APIView -from django.contrib.auth.models import AnonymousUser from django.http import Http404 from funkwhale_api.common import permissions @@ -19,24 +18,26 @@ def test_owner_permission_owner_field_ok(nodb_factories, api_request): assert check is True -def test_owner_permission_owner_field_not_ok(nodb_factories, api_request): +def test_owner_permission_owner_field_not_ok( + 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', AnonymousUser()) + setattr(request, 'user', anonymous_user) with pytest.raises(Http404): permission.has_object_permission(request, view, playlist) -def test_owner_permission_read_only(nodb_factories, api_request): +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']) permission = permissions.OwnerPermission() request = api_request.get('/') - setattr(request, 'user', AnonymousUser()) + setattr(request, 'user', anonymous_user) check = permission.has_object_permission(request, view, playlist) assert check is True diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 62bc5ada676327aa1d5044c7bd31eaea45904dea..d2ff01bc571a99726846764fdc73dd229f4695c4 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -3,6 +3,7 @@ import tempfile import shutil import pytest +from django.contrib.auth.models import AnonymousUser from django.core.cache import cache as django_cache from dynamic_preferences.registries import global_preferences_registry @@ -66,6 +67,11 @@ def logged_in_client(db, factories, client): delattr(client, 'user') +@pytest.fixture +def anonymous_user(): + return AnonymousUser() + + @pytest.fixture def api_client(client): return APIClient() @@ -126,3 +132,11 @@ def activity_registry(): @pytest.fixture def activity_muted(activity_registry, mocker): yield mocker.patch.object(record, 'send') + + +@pytest.fixture(autouse=True) +def media_root(settings): + tmp_dir = tempfile.mkdtemp() + settings.MEDIA_ROOT = tmp_dir + yield settings.MEDIA_ROOT + shutil.rmtree(tmp_dir) diff --git a/api/tests/favorites/__init__.py b/api/tests/favorites/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/tests/history/test_activity.py b/api/tests/history/test_activity.py index b5ab07b8235f12045f5ee1b9fba3fce0c3da57d5..04000604b264394ab7c3c1425e49a1f2b59bff71 100644 --- a/api/tests/history/test_activity.py +++ b/api/tests/history/test_activity.py @@ -23,7 +23,7 @@ def test_activity_listening_serializer(factories): "id": listening.get_activity_url(), "actor": actor, "object": TrackActivitySerializer(listening.track).data, - "published": field.to_representation(listening.end_date), + "published": field.to_representation(listening.creation_date), } data = serializers.ListeningActivitySerializer(listening).data diff --git a/api/tests/instance/__init__.py b/api/tests/instance/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/tests/music/conftest.py b/api/tests/music/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..1d0fa4e38627ad7f0711082a0edd2c726f02b2e6 --- /dev/null +++ b/api/tests/music/conftest.py @@ -0,0 +1,566 @@ +import pytest + + +_artists = {'search': {}, 'get': {}} + +_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' + }, + { + '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']['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' + }, + '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': [ + { + "artist-credit": [ + { + "artist": { + "alias-list": [ + { + "alias": "SoaD", + "sort-name": "SoaD", + "type": "Search hint" + }, + { + "alias": "S.O.A.D.", + "sort-name": "S.O.A.D.", + "type": "Search hint" + }, + { + "alias": "System Of Down", + "sort-name": "System Of Down", + "type": "Search hint" + } + ], + "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", + "date": "2005", + "ext:score": "100", + "id": "47ae093f-1607-49a3-be11-a15d335ccc94", + "label-info-list": [ + { + "catalog-number": "8-2796-93871-2", + "label": { + "id": "f5be9cfe-e1af-405c-a074-caeaed6797c0", + "name": "American Recordings" + } + }, + { + "catalog-number": "D162990", + "label": { + "id": "9a7d39a4-a887-40f3-a645-a9a136d1f13f", + "name": "BMG Direct Marketing, Inc." + } + } + ], + "medium-count": 1, + "medium-list": [ + { + "disc-count": 1, + "disc-list": [], + "format": "CD", + "track-count": 12, + "track-list": [] + } + ], + "medium-track-count": 12, + "packaging": "Digipak", + "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" + } + ], + "release-group": { + "id": "72035143-d6ec-308b-8ee5-070b8703902a", + "primary-type": "Album", + "type": "Album" + }, + "status": "Official", + "text-representation": { + "language": "eng", + "script": "Latn" + }, + "title": "Hypnotize" + }, + { + "artist-credit": [ + { + "artist": { + "alias-list": [ + { + "alias": "SoaD", + "sort-name": "SoaD", + "type": "Search hint" + }, + { + "alias": "S.O.A.D.", + "sort-name": "S.O.A.D.", + "type": "Search hint" + }, + { + "alias": "System Of Down", + "sort-name": "System Of Down", + "type": "Search hint" + } + ], + "id": "cc0b7089-c08d-4c10-b6b0-873582c17fd6", + "name": "System of a Down", + "sort-name": "System of a Down" + } + } + ], + "artist-credit-phrase": "System of a Down", + "asin": "B000C6NRY8", + "barcode": "827969387115", + "country": "US", + "date": "2005-12-20", + "ext:score": "100", + "id": "8a4034a9-7834-3b7e-a6f0-d0791e3731fb", + "medium-count": 1, + "medium-list": [ + { + "disc-count": 0, + "disc-list": [], + "format": "Vinyl", + "track-count": 12, + "track-list": [] + } + ], + "medium-track-count": 12, + "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-12-20" + } + ], + "release-group": { + "id": "72035143-d6ec-308b-8ee5-070b8703902a", + "primary-type": "Album", + "type": "Album" + }, + "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': { + "artist-credit": [ + { + "artist": { + "disambiguation": "George Shaw", + "id": "62c3befb-6366-4585-b256-809472333801", + "name": "Adhesive Wombat", + "sort-name": "Wombat, Adhesive" + } + } + ], + "artist-credit-phrase": "Adhesive Wombat", + "country": "XW", + "cover-art-archive": { + "artwork": "true", + "back": "false", + "count": "1", + "front": "true" + }, + "date": "2013-06-05", + "id": "a50d2a81-2a50-484d-9cb4-b9f6833f583e", + "packaging": "None", + "quality": "normal", + "release-event-count": 1, + "release-event-list": [ + { + "area": { + "id": "525d4e18-3d00-31b9-a58b-a146a916de8f", + "iso-3166-1-code-list": [ + "XW" + ], + "name": "[Worldwide]", + "sort-name": "[Worldwide]" + }, + "date": "2013-06-05" + } + ], + "status": "Official", + "text-representation": { + "language": "eng", + "script": "Latn" + }, + "title": "Marsupial Madness" + } +} + +_tracks = {'search': {}, 'get': {}} + +_tracks['search']['8bitadventures'] = { + 'recording-list': [ + { + "artist-credit": [ + { + "artist": { + "disambiguation": "George Shaw", + "id": "62c3befb-6366-4585-b256-809472333801", + "name": "Adhesive Wombat", + "sort-name": "Wombat, Adhesive" + } + } + ], + "artist-credit-phrase": "Adhesive Wombat", + "ext:score": "100", + "id": "9968a9d6-8d92-4051-8f76-674e157b6eed", + "length": "271000", + "release-list": [ + { + "country": "XW", + "date": "2013-06-05", + "id": "a50d2a81-2a50-484d-9cb4-b9f6833f583e", + "medium-list": [ + { + "format": "Digital Media", + "position": "1", + "track-count": 11, + "track-list": [ + { + "id": "64d43604-c1ee-4f45-a02c-030672d2fe27", + "length": "271000", + "number": "1", + "title": "8-Bit Adventure", + "track_or_recording_length": "271000" + } + ] + } + ], + "medium-track-count": 11, + "release-event-list": [ + { + "area": { + "id": "525d4e18-3d00-31b9-a58b-a146a916de8f", + "iso-3166-1-code-list": [ + "XW" + ], + "name": "[Worldwide]", + "sort-name": "[Worldwide]" + }, + "date": "2013-06-05" + } + ], + "release-group": { + "id": "447b4979-2178-405c-bfe6-46bf0b09e6c7", + "primary-type": "Album", + "type": "Album" + }, + "status": "Official", + "title": "Marsupial Madness" + } + ], + "title": "8-Bit Adventure", + "tag-list": [ + { + "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!'}}]}} + +_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() +def artists(): + return _artists + + +@pytest.fixture() +def albums(): + return _albums + + +@pytest.fixture() +def tracks(): + return _tracks + + +@pytest.fixture() +def works(): + return _works + + +@pytest.fixture() +def lyricswiki_content(): + return """<!doctype html> +<html lang="en" dir="ltr"> +<head> + +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> +<meta name="generator" content="MediaWiki 1.19.24" /> +<meta name="keywords" content="Chop Suey! lyrics,System Of A Down Chop Suey! lyrics,Chop Suey! by System Of A Down lyrics,lyrics,LyricWiki,LyricWikia,lyricwiki,System Of A Down:Chop Suey!,System Of A Down,System Of A Down:Toxicity (2001),Enter Shikari,Enter Shikari:Chop Suey!,"Weird Al" Yankovic,"Weird Al" Yankovic:Angry White Boy Polka,Renard,Renard:Physicality,System Of A Down:Chop Suey!/pt,Daron Malakian" /> +<meta name="description" content="Chop Suey! This song is by System of a Down and appears on the album Toxicity (2001)." /> +<meta name="twitter:card" content="summary" /> +<meta name="twitter:site" content="@Wikia" /> +<meta name="twitter:url" content="http://lyrics.wikia.com/wiki/System_Of_A_Down:Chop_Suey!" /> +<meta name="twitter:title" content="System Of A Down:Chop Suey! Lyrics - LyricWikia - Wikia" /> +<meta name="twitter:description" content="Chop Suey! This song is by System of a Down and appears on the album Toxicity (2001)." /> +<link rel="canonical" href="http://lyrics.wikia.com/wiki/System_Of_A_Down:Chop_Suey!" /> +<link rel="alternate" type="application/x-wiki" title="Edit" href="/wiki/System_Of_A_Down:Chop_Suey!?action=edit" /> +<link rel="edit" title="Edit" href="/wiki/System_Of_A_Down:Chop_Suey!?action=edit" /> +<link rel="apple-touch-icon" href="http://img4.wikia.nocookie.net/__cb22/lyricwiki/images/b/bc/Wiki.png" /> +<link rel="shortcut icon" href="http://slot1.images.wikia.nocookie.net/__cb1474018633/common/skins/common/images/favicon.ico" /> +<link rel="search" type="application/opensearchdescription+xml" href="/opensearch_desc.php" title="LyricWikia (en)" /> +<link rel="EditURI" type="application/rsd+xml" href="http://lyrics.wikia.com/api.php?action=rsd" /> +<link rel="copyright" href="/wiki/LyricWiki:Copyrights" /> +<link rel="alternate" type="application/atom+xml" title="LyricWikia Atom feed" href="/wiki/Special:RecentChanges?feed=atom" /> +<title>System Of A Down:Chop Suey! Lyrics - LyricWikia - Wikia</title> + +<body> +<div class='lyricbox'> +<i>We're rolling "Suicide".</i><br /><br />Wake up <i>(wake up)</i><br />Grab a brush and put on a little makeup<br />Hide the scars to fade away the shakeup <i>(hide the scars to fade away the)</i><br />Why'd you leave the keys upon the table?<br />Here you go, create another fable<br /><br />You wanted to<br />Grab a brush and put a little makeup<br />You wanted to<br />Hide the scars to fade away the shakeup<br />You wanted to<br />Why'd you leave the keys upon the table?<br />You wanted to<br /><br />I don't think you trust<br />In my self-righteous suicide<br />I cry when angels deserve to die<br /><br />Wake up <i>(wake up)</i><br />Grab a brush and put on a little makeup<br />Hide the scars to fade away the <i>(hide the scars to fade away the)</i><br />Why'd you leave the keys upon the table?<br />Here you go, create another fable<br /><br />You wanted to<br />Grab a brush and put a little makeup<br />You wanted to<br />Hide the scars to fade away the shakeup<br />You wanted to<br />Why'd you leave the keys upon the table?<br />You wanted to<br /><br />I don't think you trust<br />In my self-righteous suicide<br />I cry when angels deserve to die<br />In my self-righteous suicide<br />I cry when angels deserve to die<br /><br />Father <i>(father)</i><br />Father <i>(father)</i><br />Father <i>(father)</i><br />Father <i>(father)</i><br />Father, into your hands I commit my spirit<br />Father, into your hands<br /><br />Why have you forsaken me?<br />In your eyes forsaken me<br />In your thoughts forsaken me<br />In your heart forsaken me, oh<br /><br />Trust in my self-righteous suicide<br />I cry when angels deserve to die<br />In my self-righteous suicide<br />I cry when angels deserve to die +</div> +</body> +</html>""" + + +@pytest.fixture() +def binary_cover(): + 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<U\xff\xd0\xec\xd8\xab\xb1W\xff\xd9' diff --git a/api/tests/music/cover.py b/api/tests/music/cover.py deleted file mode 100644 index 401bc105227acc0fd2f2265ef899604b138475a3..0000000000000000000000000000000000000000 --- a/api/tests/music/cover.py +++ /dev/null @@ -1 +0,0 @@ -binary_data = 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<U\xff\xd0\xec\xd8\xab\xb1W\xff\xd9' diff --git a/api/tests/music/data.py b/api/tests/music/data.py deleted file mode 100644 index 54da6bc846190992dd2bd24fde94aaeced0b1a63..0000000000000000000000000000000000000000 --- a/api/tests/music/data.py +++ /dev/null @@ -1,502 +0,0 @@ -artists = {'search': {}, 'get': {}} -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' - }, - { - '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']['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' - }, - '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': [ - { - "artist-credit": [ - { - "artist": { - "alias-list": [ - { - "alias": "SoaD", - "sort-name": "SoaD", - "type": "Search hint" - }, - { - "alias": "S.O.A.D.", - "sort-name": "S.O.A.D.", - "type": "Search hint" - }, - { - "alias": "System Of Down", - "sort-name": "System Of Down", - "type": "Search hint" - } - ], - "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", - "date": "2005", - "ext:score": "100", - "id": "47ae093f-1607-49a3-be11-a15d335ccc94", - "label-info-list": [ - { - "catalog-number": "8-2796-93871-2", - "label": { - "id": "f5be9cfe-e1af-405c-a074-caeaed6797c0", - "name": "American Recordings" - } - }, - { - "catalog-number": "D162990", - "label": { - "id": "9a7d39a4-a887-40f3-a645-a9a136d1f13f", - "name": "BMG Direct Marketing, Inc." - } - } - ], - "medium-count": 1, - "medium-list": [ - { - "disc-count": 1, - "disc-list": [], - "format": "CD", - "track-count": 12, - "track-list": [] - } - ], - "medium-track-count": 12, - "packaging": "Digipak", - "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" - } - ], - "release-group": { - "id": "72035143-d6ec-308b-8ee5-070b8703902a", - "primary-type": "Album", - "type": "Album" - }, - "status": "Official", - "text-representation": { - "language": "eng", - "script": "Latn" - }, - "title": "Hypnotize" - }, - { - "artist-credit": [ - { - "artist": { - "alias-list": [ - { - "alias": "SoaD", - "sort-name": "SoaD", - "type": "Search hint" - }, - { - "alias": "S.O.A.D.", - "sort-name": "S.O.A.D.", - "type": "Search hint" - }, - { - "alias": "System Of Down", - "sort-name": "System Of Down", - "type": "Search hint" - } - ], - "id": "cc0b7089-c08d-4c10-b6b0-873582c17fd6", - "name": "System of a Down", - "sort-name": "System of a Down" - } - } - ], - "artist-credit-phrase": "System of a Down", - "asin": "B000C6NRY8", - "barcode": "827969387115", - "country": "US", - "date": "2005-12-20", - "ext:score": "100", - "id": "8a4034a9-7834-3b7e-a6f0-d0791e3731fb", - "medium-count": 1, - "medium-list": [ - { - "disc-count": 0, - "disc-list": [], - "format": "Vinyl", - "track-count": 12, - "track-list": [] - } - ], - "medium-track-count": 12, - "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-12-20" - } - ], - "release-group": { - "id": "72035143-d6ec-308b-8ee5-070b8703902a", - "primary-type": "Album", - "type": "Album" - }, - "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': { - "artist-credit": [ - { - "artist": { - "disambiguation": "George Shaw", - "id": "62c3befb-6366-4585-b256-809472333801", - "name": "Adhesive Wombat", - "sort-name": "Wombat, Adhesive" - } - } - ], - "artist-credit-phrase": "Adhesive Wombat", - "country": "XW", - "cover-art-archive": { - "artwork": "true", - "back": "false", - "count": "1", - "front": "true" - }, - "date": "2013-06-05", - "id": "a50d2a81-2a50-484d-9cb4-b9f6833f583e", - "packaging": "None", - "quality": "normal", - "release-event-count": 1, - "release-event-list": [ - { - "area": { - "id": "525d4e18-3d00-31b9-a58b-a146a916de8f", - "iso-3166-1-code-list": [ - "XW" - ], - "name": "[Worldwide]", - "sort-name": "[Worldwide]" - }, - "date": "2013-06-05" - } - ], - "status": "Official", - "text-representation": { - "language": "eng", - "script": "Latn" - }, - "title": "Marsupial Madness" - } -} - -tracks = {'search': {}, 'get': {}} - -tracks['search']['8bitadventures'] = { - 'recording-list': [ - { - "artist-credit": [ - { - "artist": { - "disambiguation": "George Shaw", - "id": "62c3befb-6366-4585-b256-809472333801", - "name": "Adhesive Wombat", - "sort-name": "Wombat, Adhesive" - } - } - ], - "artist-credit-phrase": "Adhesive Wombat", - "ext:score": "100", - "id": "9968a9d6-8d92-4051-8f76-674e157b6eed", - "length": "271000", - "release-list": [ - { - "country": "XW", - "date": "2013-06-05", - "id": "a50d2a81-2a50-484d-9cb4-b9f6833f583e", - "medium-list": [ - { - "format": "Digital Media", - "position": "1", - "track-count": 11, - "track-list": [ - { - "id": "64d43604-c1ee-4f45-a02c-030672d2fe27", - "length": "271000", - "number": "1", - "title": "8-Bit Adventure", - "track_or_recording_length": "271000" - } - ] - } - ], - "medium-track-count": 11, - "release-event-list": [ - { - "area": { - "id": "525d4e18-3d00-31b9-a58b-a146a916de8f", - "iso-3166-1-code-list": [ - "XW" - ], - "name": "[Worldwide]", - "sort-name": "[Worldwide]" - }, - "date": "2013-06-05" - } - ], - "release-group": { - "id": "447b4979-2178-405c-bfe6-46bf0b09e6c7", - "primary-type": "Album", - "type": "Album" - }, - "status": "Official", - "title": "Marsupial Madness" - } - ], - "title": "8-Bit Adventure", - "tag-list": [ - { - "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!'}}]}} - -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'}]}} diff --git a/api/tests/music/mocking/lyricswiki.py b/api/tests/music/mocking/lyricswiki.py deleted file mode 100644 index 360a7174f0740ff33ab9aa85dedc622e1dd176ef..0000000000000000000000000000000000000000 --- a/api/tests/music/mocking/lyricswiki.py +++ /dev/null @@ -1,32 +0,0 @@ -content = """<!doctype html> -<html lang="en" dir="ltr"> -<head> - -<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> -<meta name="generator" content="MediaWiki 1.19.24" /> -<meta name="keywords" content="Chop Suey! lyrics,System Of A Down Chop Suey! lyrics,Chop Suey! by System Of A Down lyrics,lyrics,LyricWiki,LyricWikia,lyricwiki,System Of A Down:Chop Suey!,System Of A Down,System Of A Down:Toxicity (2001),Enter Shikari,Enter Shikari:Chop Suey!,"Weird Al" Yankovic,"Weird Al" Yankovic:Angry White Boy Polka,Renard,Renard:Physicality,System Of A Down:Chop Suey!/pt,Daron Malakian" /> -<meta name="description" content="Chop Suey! This song is by System of a Down and appears on the album Toxicity (2001)." /> -<meta name="twitter:card" content="summary" /> -<meta name="twitter:site" content="@Wikia" /> -<meta name="twitter:url" content="http://lyrics.wikia.com/wiki/System_Of_A_Down:Chop_Suey!" /> -<meta name="twitter:title" content="System Of A Down:Chop Suey! Lyrics - LyricWikia - Wikia" /> -<meta name="twitter:description" content="Chop Suey! This song is by System of a Down and appears on the album Toxicity (2001)." /> -<link rel="canonical" href="http://lyrics.wikia.com/wiki/System_Of_A_Down:Chop_Suey!" /> -<link rel="alternate" type="application/x-wiki" title="Edit" href="/wiki/System_Of_A_Down:Chop_Suey!?action=edit" /> -<link rel="edit" title="Edit" href="/wiki/System_Of_A_Down:Chop_Suey!?action=edit" /> -<link rel="apple-touch-icon" href="http://img4.wikia.nocookie.net/__cb22/lyricwiki/images/b/bc/Wiki.png" /> -<link rel="shortcut icon" href="http://slot1.images.wikia.nocookie.net/__cb1474018633/common/skins/common/images/favicon.ico" /> -<link rel="search" type="application/opensearchdescription+xml" href="/opensearch_desc.php" title="LyricWikia (en)" /> -<link rel="EditURI" type="application/rsd+xml" href="http://lyrics.wikia.com/api.php?action=rsd" /> -<link rel="copyright" href="/wiki/LyricWiki:Copyrights" /> -<link rel="alternate" type="application/atom+xml" title="LyricWikia Atom feed" href="/wiki/Special:RecentChanges?feed=atom" /> -<title>System Of A Down:Chop Suey! Lyrics - LyricWikia - Wikia</title> - -<body> -<div class='lyricbox'> -<i>We're rolling "Suicide".</i><br /><br />Wake up <i>(wake up)</i><br />Grab a brush and put on a little makeup<br />Hide the scars to fade away the shakeup <i>(hide the scars to fade away the)</i><br />Why'd you leave the keys upon the table?<br />Here you go, create another fable<br /><br />You wanted to<br />Grab a brush and put a little makeup<br />You wanted to<br />Hide the scars to fade away the shakeup<br />You wanted to<br />Why'd you leave the keys upon the table?<br />You wanted to<br /><br />I don't think you trust<br />In my self-righteous suicide<br />I cry when angels deserve to die<br /><br />Wake up <i>(wake up)</i><br />Grab a brush and put on a little makeup<br />Hide the scars to fade away the <i>(hide the scars to fade away the)</i><br />Why'd you leave the keys upon the table?<br />Here you go, create another fable<br /><br />You wanted to<br />Grab a brush and put a little makeup<br />You wanted to<br />Hide the scars to fade away the shakeup<br />You wanted to<br />Why'd you leave the keys upon the table?<br />You wanted to<br /><br />I don't think you trust<br />In my self-righteous suicide<br />I cry when angels deserve to die<br />In my self-righteous suicide<br />I cry when angels deserve to die<br /><br />Father <i>(father)</i><br />Father <i>(father)</i><br />Father <i>(father)</i><br />Father <i>(father)</i><br />Father, into your hands I commit my spirit<br />Father, into your hands<br /><br />Why have you forsaken me?<br />In your eyes forsaken me<br />In your thoughts forsaken me<br />In your heart forsaken me, oh<br /><br />Trust in my self-righteous suicide<br />I cry when angels deserve to die<br />In my self-righteous suicide<br />I cry when angels deserve to die -</div> -</body> -</html> -""" diff --git a/api/tests/music/test_api.py b/api/tests/music/test_api.py index 8196d3c092e62b4d85f5da5c1b7490edc6f2def2..625bf9d2be0af241097af9262c6de7492cfd1db9 100644 --- a/api/tests/music/test_api.py +++ b/api/tests/music/test_api.py @@ -8,21 +8,21 @@ from funkwhale_api.musicbrainz import api from funkwhale_api.music import serializers from funkwhale_api.music import tasks -from . import data as api_data DATA_DIR = os.path.dirname(os.path.abspath(__file__)) -def test_can_submit_youtube_url_for_track_import(mocker, superuser_client): +def test_can_submit_youtube_url_for_track_import( + artists, albums, tracks, mocker, superuser_client): mocker.patch( 'funkwhale_api.musicbrainz.api.artists.get', - return_value=api_data.artists['get']['adhesive_wombat']) + return_value=artists['get']['adhesive_wombat']) mocker.patch( 'funkwhale_api.musicbrainz.api.releases.get', - return_value=api_data.albums['get']['marsupial']) + return_value=albums['get']['marsupial']) mocker.patch( 'funkwhale_api.musicbrainz.api.recordings.get', - return_value=api_data.tracks['get']['8bitadventures']) + return_value=tracks['get']['8bitadventures']) mocker.patch( 'funkwhale_api.music.models.TrackFile.download_file', return_value=None) @@ -58,17 +58,18 @@ def test_import_creates_an_import_with_correct_data(mocker, superuser_client): assert job.source == 'https://www.youtube.com/watch?v={0}'.format(video_id) -def test_can_import_whole_album(mocker, superuser_client): +def test_can_import_whole_album( + artists, albums, mocker, superuser_client): mocker.patch('funkwhale_api.music.tasks.import_job_run') mocker.patch( 'funkwhale_api.musicbrainz.api.artists.get', - return_value=api_data.artists['get']['soad']) + return_value=artists['get']['soad']) mocker.patch( 'funkwhale_api.musicbrainz.api.images.get_front', return_value=b'') mocker.patch( 'funkwhale_api.musicbrainz.api.releases.get', - return_value=api_data.albums['get_with_includes']['hypnotize']) + return_value=albums['get_with_includes']['hypnotize']) payload = { 'releaseId': '47ae093f-1607-49a3-be11-a15d335ccc94', 'tracks': [ @@ -97,7 +98,7 @@ def test_can_import_whole_album(mocker, superuser_client): album = models.Album.objects.latest('id') assert str(album.mbid) == '47ae093f-1607-49a3-be11-a15d335ccc94' - medium_data = api_data.albums['get_with_includes']['hypnotize']['release']['medium-list'][0] + medium_data = albums['get_with_includes']['hypnotize']['release']['medium-list'][0] assert int(medium_data['track-count']) == album.tracks.all().count() for track in medium_data['track-list']: @@ -113,17 +114,18 @@ def test_can_import_whole_album(mocker, superuser_client): assert job.source == row['source'] -def test_can_import_whole_artist(mocker, superuser_client): +def test_can_import_whole_artist( + artists, albums, mocker, superuser_client): mocker.patch('funkwhale_api.music.tasks.import_job_run') mocker.patch( 'funkwhale_api.musicbrainz.api.artists.get', - return_value=api_data.artists['get']['soad']) + return_value=artists['get']['soad']) mocker.patch( 'funkwhale_api.musicbrainz.api.images.get_front', return_value=b'') mocker.patch( 'funkwhale_api.musicbrainz.api.releases.get', - return_value=api_data.albums['get_with_includes']['hypnotize']) + return_value=albums['get_with_includes']['hypnotize']) payload = { 'artistId': 'mbid', 'albums': [ @@ -157,7 +159,7 @@ def test_can_import_whole_artist(mocker, superuser_client): album = models.Album.objects.latest('id') assert str(album.mbid) == '47ae093f-1607-49a3-be11-a15d335ccc94' - medium_data = api_data.albums['get_with_includes']['hypnotize']['release']['medium-list'][0] + medium_data = albums['get_with_includes']['hypnotize']['release']['medium-list'][0] assert int(medium_data['track-count']) == album.tracks.all().count() for track in medium_data['track-list']: @@ -173,55 +175,57 @@ def test_can_import_whole_artist(mocker, superuser_client): assert job.source == row['source'] -def test_user_can_query_api_for_his_own_batches(client, factories): - user1 = factories['users.SuperUser']() - user2 = factories['users.SuperUser']() - - job = factories['music.ImportJob'](batch__submitted_by=user1) +def test_user_can_query_api_for_his_own_batches( + superuser_api_client, factories): + factories['music.ImportJob']() + job = factories['music.ImportJob']( + batch__submitted_by=superuser_api_client.user) url = reverse('api:v1:import-batches-list') - client.login(username=user2.username, password='test') - response2 = client.get(url) - results = json.loads(response2.content.decode('utf-8')) - assert results['count'] == 0 - client.logout() - - client.login(username=user1.username, password='test') - response1 = client.get(url) - results = json.loads(response1.content.decode('utf-8')) + response = superuser_api_client.get(url) + results = response.data assert results['count'] == 1 assert results['results'][0]['jobs'][0]['mbid'] == job.mbid -def test_user_can_create_an_empty_batch(client, factories): - user = factories['users.SuperUser']() +def test_user_cannnot_access_other_batches( + superuser_api_client, factories): + factories['music.ImportJob']() + job = factories['music.ImportJob']() url = reverse('api:v1:import-batches-list') - client.login(username=user.username, password='test') - response = client.post(url) + + response = superuser_api_client.get(url) + results = response.data + assert results['count'] == 0 + + +def test_user_can_create_an_empty_batch(superuser_api_client, factories): + url = reverse('api:v1:import-batches-list') + response = superuser_api_client.post(url) assert response.status_code == 201 - batch = user.imports.latest('id') + batch = superuser_api_client.user.imports.latest('id') - assert batch.submitted_by == user + assert batch.submitted_by == superuser_api_client.user assert batch.source == 'api' -def test_user_can_create_import_job_with_file(client, factories, mocker): +def test_user_can_create_import_job_with_file( + superuser_api_client, factories, mocker): path = os.path.join(DATA_DIR, 'test.ogg') m = mocker.patch('funkwhale_api.common.utils.on_commit') - user = factories['users.SuperUser']() - batch = factories['music.ImportBatch'](submitted_by=user) + batch = factories['music.ImportBatch']( + submitted_by=superuser_api_client.user) url = reverse('api:v1:import-jobs-list') - client.login(username=user.username, password='test') with open(path, 'rb') as f: content = f.read() f.seek(0) - response = client.post(url, { + response = superuser_api_client.post(url, { 'batch': batch.pk, 'audio_file': f, 'source': 'file://' - }, format='multipart') + }) assert response.status_code == 201 @@ -237,16 +241,16 @@ def test_user_can_create_import_job_with_file(client, factories, mocker): import_job_id=job.pk) -def test_can_search_artist(factories, client): +def test_can_search_artist(factories, logged_in_client): artist1 = factories['music.Artist']() artist2 = factories['music.Artist']() expected = [serializers.ArtistSerializerNested(artist1).data] url = reverse('api:v1:artists-search') - response = client.get(url, {'query': artist1.name}) - assert json.loads(response.content.decode('utf-8')) == expected + response = logged_in_client.get(url, {'query': artist1.name}) + assert response.data == expected -def test_can_search_artist_by_name_start(factories, client): +def test_can_search_artist_by_name_start(factories, logged_in_client): artist1 = factories['music.Artist'](name='alpha') artist2 = factories['music.Artist'](name='beta') expected = { @@ -256,20 +260,20 @@ def test_can_search_artist_by_name_start(factories, client): 'results': [serializers.ArtistSerializerNested(artist1).data] } url = reverse('api:v1:artists-list') - response = client.get(url, {'name__startswith': 'a'}) + response = logged_in_client.get(url, {'name__startswith': 'a'}) - assert expected == json.loads(response.content.decode('utf-8')) + assert expected == response.data -def test_can_search_tracks(factories, client): +def test_can_search_tracks(factories, logged_in_client): track1 = factories['music.Track'](title="test track 1") track2 = factories['music.Track']() query = 'test track 1' expected = [serializers.TrackSerializerNested(track1).data] url = reverse('api:v1:tracks-search') - response = client.get(url, {'query': query}) + response = logged_in_client.get(url, {'query': query}) - assert expected == json.loads(response.content.decode('utf-8')) + assert expected == response.data @pytest.mark.parametrize('route,method', [ @@ -278,24 +282,31 @@ def test_can_search_tracks(factories, client): ('api:v1:artists-list', 'get'), ('api:v1:albums-list', 'get'), ]) -def test_can_restrict_api_views_to_authenticated_users(db, route, method, settings, client): +def test_can_restrict_api_views_to_authenticated_users( + db, route, method, settings, client): url = reverse(route) settings.API_AUTHENTICATION_REQUIRED = True response = getattr(client, method)(url) assert response.status_code == 401 -def test_track_file_url_is_restricted_to_authenticated_users(client, factories, settings): +def test_track_file_url_is_restricted_to_authenticated_users( + api_client, factories, settings): settings.API_AUTHENTICATION_REQUIRED = True f = factories['music.TrackFile']() assert f.audio_file is not None url = f.path - response = client.get(url) + response = api_client.get(url) assert response.status_code == 401 - user = factories['users.SuperUser']() - client.login(username=user.username, password='test') - response = client.get(url) + +def test_track_file_url_is_accessible_to_authenticated_users( + logged_in_api_client, factories, settings): + settings.API_AUTHENTICATION_REQUIRED = True + f = factories['music.TrackFile']() + assert f.audio_file is not None + url = f.path + response = logged_in_api_client.get(url) assert response.status_code == 200 assert response['X-Accel-Redirect'] == '/_protected{}'.format(f.audio_file.url) diff --git a/api/tests/music/test_import.py b/api/tests/music/test_import.py index f2ca1abbd04a562764194f09653e17c4724f3cc4..0f709e81f508fcb0e4e2ee06e92991a2f907cdff 100644 --- a/api/tests/music/test_import.py +++ b/api/tests/music/test_import.py @@ -2,23 +2,21 @@ import json from django.urls import reverse -from . import data as api_data - def test_create_import_can_bind_to_request( - mocker, factories, superuser_api_client): + artists, albums, mocker, factories, superuser_api_client): request = factories['requests.ImportRequest']() mocker.patch('funkwhale_api.music.tasks.import_job_run') mocker.patch( 'funkwhale_api.musicbrainz.api.artists.get', - return_value=api_data.artists['get']['soad']) + return_value=artists['get']['soad']) mocker.patch( 'funkwhale_api.musicbrainz.api.images.get_front', return_value=b'') mocker.patch( 'funkwhale_api.musicbrainz.api.releases.get', - return_value=api_data.albums['get_with_includes']['hypnotize']) + return_value=albums['get_with_includes']['hypnotize']) payload = { 'releaseId': '47ae093f-1607-49a3-be11-a15d335ccc94', 'importRequest': request.pk, diff --git a/api/tests/music/test_lyrics.py b/api/tests/music/test_lyrics.py index d10d113d7741d1e53e64d9af856a4d466bab6d35..3aee368c0e9c98a0da626b05f427191b4f8372d4 100644 --- a/api/tests/music/test_lyrics.py +++ b/api/tests/music/test_lyrics.py @@ -7,15 +7,12 @@ from funkwhale_api.music import serializers from funkwhale_api.music import tasks from funkwhale_api.music import lyrics as lyrics_utils -from .mocking import lyricswiki -from . import data as api_data - - -def test_works_import_lyrics_if_any(mocker, factories): +def test_works_import_lyrics_if_any( + lyricswiki_content, mocker, factories): mocker.patch( 'funkwhale_api.music.lyrics._get_html', - return_value=lyricswiki.content) + return_value=lyricswiki_content) lyrics = factories['music.Lyrics']( url='http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!') @@ -48,16 +45,22 @@ Is it me you're looking for?""" assert expected == l.content_rendered -def test_works_import_lyrics_if_any(mocker, factories, logged_in_client): +def test_works_import_lyrics_if_any( + lyricswiki_content, + works, + tracks, + mocker, + factories, + logged_in_client): mocker.patch( 'funkwhale_api.musicbrainz.api.works.get', - return_value=api_data.works['get']['chop_suey']) + return_value=works['get']['chop_suey']) mocker.patch( 'funkwhale_api.musicbrainz.api.recordings.get', - return_value=api_data.tracks['get']['chop_suey']) + return_value=tracks['get']['chop_suey']) mocker.patch( 'funkwhale_api.music.lyrics._get_html', - return_value=lyricswiki.content) + return_value=lyricswiki_content) track = factories['music.Track']( work=None, mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448') diff --git a/api/tests/music/test_music.py b/api/tests/music/test_music.py index 076ad2bd05cb714c6436592666dffeeae61396ba..4162912e4fdee2e13d192ce14ed2a1fc4dcd2a23 100644 --- a/api/tests/music/test_music.py +++ b/api/tests/music/test_music.py @@ -2,14 +2,11 @@ import pytest from funkwhale_api.music import models import datetime -from . import data as api_data -from .cover import binary_data - -def test_can_create_artist_from_api(mocker, db): +def test_can_create_artist_from_api(artists, mocker, db): mocker.patch( 'musicbrainzngs.search_artists', - return_value=api_data.artists['search']['adhesive_wombat']) + 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] @@ -19,13 +16,13 @@ def test_can_create_artist_from_api(mocker, db): assert artist.name, 'Adhesive Wombat' -def test_can_create_album_from_api(mocker, db): +def test_can_create_album_from_api(artists, albums, mocker, db): mocker.patch( 'funkwhale_api.musicbrainz.api.releases.search', - return_value=api_data.albums['search']['hypnotize']) + return_value=albums['search']['hypnotize']) mocker.patch( 'funkwhale_api.musicbrainz.api.artists.get', - return_value=api_data.artists['get']['soad']) + 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] @@ -38,16 +35,16 @@ def test_can_create_album_from_api(mocker, db): assert album.artist.mbid, data['artist-credit'][0]['artist']['id'] -def test_can_create_track_from_api(mocker, db): +def test_can_create_track_from_api(artists, albums, tracks, mocker, db): mocker.patch( 'funkwhale_api.musicbrainz.api.artists.get', - return_value=api_data.artists['get']['adhesive_wombat']) + return_value=artists['get']['adhesive_wombat']) mocker.patch( 'funkwhale_api.musicbrainz.api.releases.get', - return_value=api_data.albums['get']['marsupial']) + return_value=albums['get']['marsupial']) mocker.patch( 'funkwhale_api.musicbrainz.api.recordings.search', - return_value=api_data.tracks['search']['8bitadventures']) + 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 @@ -60,16 +57,17 @@ def test_can_create_track_from_api(mocker, db): assert track.album.title == 'Marsupial Madness' -def test_can_create_track_from_api_with_corresponding_tags(mocker, db): +def test_can_create_track_from_api_with_corresponding_tags( + artists, albums, tracks, mocker, db): mocker.patch( 'funkwhale_api.musicbrainz.api.artists.get', - return_value=api_data.artists['get']['adhesive_wombat']) + return_value=artists['get']['adhesive_wombat']) mocker.patch( 'funkwhale_api.musicbrainz.api.releases.get', - return_value=api_data.albums['get']['marsupial']) + return_value=albums['get']['marsupial']) mocker.patch( 'funkwhale_api.musicbrainz.api.recordings.get', - return_value=api_data.tracks['get']['8bitadventures']) + 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()] @@ -77,16 +75,17 @@ def test_can_create_track_from_api_with_corresponding_tags(mocker, db): assert tag in track_tags -def test_can_get_or_create_track_from_api(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=api_data.artists['get']['adhesive_wombat']) + return_value=artists['get']['adhesive_wombat']) mocker.patch( 'funkwhale_api.musicbrainz.api.releases.get', - return_value=api_data.albums['get']['marsupial']) + return_value=albums['get']['marsupial']) mocker.patch( 'funkwhale_api.musicbrainz.api.recordings.search', - return_value=api_data.tracks['search']['8bitadventures']) + 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 @@ -126,13 +125,13 @@ def test_artist_tags_deduced_from_album_tags(factories, django_assert_num_querie assert tag in artist.tags -def test_can_download_image_file_for_album(mocker, factories): +def test_can_download_image_file_for_album(binary_cover, mocker, factories): mocker.patch( 'funkwhale_api.musicbrainz.api.images.get_front', - return_value=binary_data) + 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.get_image() album.save() - assert album.cover.file.read() == binary_data + assert album.cover.file.read() == binary_cover diff --git a/api/tests/music/test_tasks.py b/api/tests/music/test_tasks.py index 5ecf9b9e46310c5f3779fa4ced90daa0809c5aff..ddbc4ba9a2c7407bd067dc2799f499654cbb004c 100644 --- a/api/tests/music/test_tasks.py +++ b/api/tests/music/test_tasks.py @@ -4,8 +4,6 @@ import pytest from funkwhale_api.providers.acoustid import get_acoustid_client from funkwhale_api.music import tasks -from . import data as api_data - DATA_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -50,7 +48,7 @@ def test_set_acoustid_on_track_file_required_high_score(factories, mocker): def test_import_job_can_run_with_file_and_acoustid( - preferences, factories, mocker): + 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' @@ -66,13 +64,13 @@ def test_import_job_can_run_with_file_and_acoustid( } mocker.patch( 'funkwhale_api.musicbrainz.api.artists.get', - return_value=api_data.artists['get']['adhesive_wombat']) + return_value=artists['get']['adhesive_wombat']) mocker.patch( 'funkwhale_api.musicbrainz.api.releases.get', - return_value=api_data.albums['get']['marsupial']) + return_value=albums['get']['marsupial']) mocker.patch( 'funkwhale_api.musicbrainz.api.recordings.search', - return_value=api_data.tracks['search']['8bitadventures']) + return_value=tracks['search']['8bitadventures']) mocker.patch('acoustid.match', return_value=acoustid_payload) job = factories['music.FileImportJob'](audio_file__path=path) @@ -129,7 +127,8 @@ def test__do_import_skipping_accoustid_if_no_key( m.assert_called_once_with(p) -def test_import_job_can_be_skipped(factories, mocker, preferences): +def test_import_job_can_be_skipped( + artists, albums, tracks, factories, mocker, preferences): preferences['providers_acoustid__api_key'] = 'test' path = os.path.join(DATA_DIR, 'test.ogg') mbid = '9968a9d6-8d92-4051-8f76-674e157b6eed' @@ -146,13 +145,13 @@ def test_import_job_can_be_skipped(factories, mocker, preferences): } mocker.patch( 'funkwhale_api.musicbrainz.api.artists.get', - return_value=api_data.artists['get']['adhesive_wombat']) + return_value=artists['get']['adhesive_wombat']) mocker.patch( 'funkwhale_api.musicbrainz.api.releases.get', - return_value=api_data.albums['get']['marsupial']) + return_value=albums['get']['marsupial']) mocker.patch( 'funkwhale_api.musicbrainz.api.recordings.search', - return_value=api_data.tracks['search']['8bitadventures']) + return_value=tracks['search']['8bitadventures']) mocker.patch('acoustid.match', return_value=acoustid_payload) job = factories['music.FileImportJob'](audio_file__path=path) diff --git a/api/tests/music/test_works.py b/api/tests/music/test_works.py index 9b72768ad07bf05adae848eef73784866de83c7d..13f6447bec60f54797f890f441412494f557b7c7 100644 --- a/api/tests/music/test_works.py +++ b/api/tests/music/test_works.py @@ -5,13 +5,11 @@ from funkwhale_api.music import models from funkwhale_api.musicbrainz import api from funkwhale_api.music import serializers -from . import data as api_data - -def test_can_import_work(factories, mocker): +def test_can_import_work(factories, mocker, works): mocker.patch( 'funkwhale_api.musicbrainz.api.works.get', - return_value=api_data.works['get']['chop_suey']) + return_value=works['get']['chop_suey']) recording = factories['music.Track']( mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448') mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5' @@ -28,13 +26,13 @@ def test_can_import_work(factories, mocker): assert recording.work == work -def test_can_get_work_from_recording(factories, mocker): +def test_can_get_work_from_recording(factories, mocker, works, tracks): mocker.patch( 'funkwhale_api.musicbrainz.api.works.get', - return_value=api_data.works['get']['chop_suey']) + return_value=works['get']['chop_suey']) mocker.patch( 'funkwhale_api.musicbrainz.api.recordings.get', - return_value=api_data.tracks['get']['chop_suey']) + return_value=tracks['get']['chop_suey']) recording = factories['music.Track']( work=None, mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448') @@ -53,10 +51,10 @@ def test_can_get_work_from_recording(factories, mocker): assert recording.work == work -def test_works_import_lyrics_if_any(db, mocker): +def test_works_import_lyrics_if_any(db, mocker, works): mocker.patch( 'funkwhale_api.musicbrainz.api.works.get', - return_value=api_data.works['get']['chop_suey']) + return_value=works['get']['chop_suey']) mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5' work = models.Work.create_from_api(id=mbid) diff --git a/api/tests/musicbrainz/data.py b/api/tests/musicbrainz/conftest.py similarity index 96% rename from api/tests/musicbrainz/data.py rename to api/tests/musicbrainz/conftest.py index 1d7b9a3defa3c9927a649485e89b487eed7b9123..505d6e5537ab367090a4ead483e56105f561080c 100644 --- a/api/tests/musicbrainz/data.py +++ b/api/tests/musicbrainz/conftest.py @@ -1,5 +1,7 @@ -artists = {'search': {}, 'get': {}} -artists['search']['lost fingers'] = { +import pytest + +_artists = {'search': {}, 'get': {}} +_artists['search']['lost fingers'] = { 'artist-count': 696, 'artist-list': [ { @@ -21,7 +23,7 @@ artists['search']['lost fingers'] = { }, ] } -artists['get']['lost fingers'] = { +_artists['get']['lost fingers'] = { "artist": { "life-span": { "begin": "2008" @@ -102,8 +104,8 @@ artists['get']['lost fingers'] = { } -release_groups = {'browse': {}} -release_groups['browse']["lost fingers"] = { +_release_groups = {'browse': {}} +_release_groups['browse']["lost fingers"] = { "release-group-list": [ { "first-release-date": "2010", @@ -165,8 +167,8 @@ release_groups['browse']["lost fingers"] = { "release-group-count": 8 } -recordings = {'search': {}, 'get': {}} -recordings['search']['brontide matador'] = { +_recordings = {'search': {}, 'get': {}} +_recordings['search']['brontide matador'] = { "recording-count": 1044, "recording-list": [ { @@ -217,8 +219,8 @@ recordings['search']['brontide matador'] = { ] } -releases = {'search': {}, 'get': {}, 'browse': {}} -releases['search']['brontide matador'] = { +_releases = {'search': {}, 'get': {}, 'browse': {}} +_releases['search']['brontide matador'] = { "release-count": 116, "release-list": [ { "ext:score": "100", @@ -283,7 +285,7 @@ releases['search']['brontide matador'] = { ] } -releases['browse']['Lost in the 80s'] = { +_releases['browse']['Lost in the 80s'] = { "release-count": 3, "release-list": [ { @@ -476,3 +478,23 @@ releases['browse']['Lost in the 80s'] = { }, ] } + + +@pytest.fixture() +def releases(): + return _releases + + +@pytest.fixture() +def release_groups(): + return _release_groups + + +@pytest.fixture() +def artists(): + return _artists + + +@pytest.fixture() +def recordings(): + return _recordings diff --git a/api/tests/musicbrainz/test_api.py b/api/tests/musicbrainz/test_api.py index bbade340060dae4cbc4f2a64d296eb86e6c59e79..fdd1dbdb03b74769588ef6c66c23a49cc1053b29 100644 --- a/api/tests/musicbrainz/test_api.py +++ b/api/tests/musicbrainz/test_api.py @@ -2,64 +2,65 @@ import json from django.urls import reverse from funkwhale_api.musicbrainz import api -from . import data as api_data -def test_can_search_recording_in_musicbrainz_api(db, mocker, client): +def test_can_search_recording_in_musicbrainz_api( + recordings, db, mocker, logged_in_api_client): mocker.patch( 'funkwhale_api.musicbrainz.api.recordings.search', - return_value=api_data.recordings['search']['brontide matador']) + return_value=recordings['search']['brontide matador']) query = 'brontide matador' url = reverse('api:v1:providers:musicbrainz:search-recordings') - expected = api_data.recordings['search']['brontide matador'] - response = client.get(url, data={'query': query}) + expected = recordings['search']['brontide matador'] + response = logged_in_api_client.get(url, data={'query': query}) - assert expected == json.loads(response.content.decode('utf-8')) + assert expected == response.data -def test_can_search_release_in_musicbrainz_api(db, mocker, 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=api_data.releases['search']['brontide matador']) + return_value=releases['search']['brontide matador']) query = 'brontide matador' url = reverse('api:v1:providers:musicbrainz:search-releases') - expected = api_data.releases['search']['brontide matador'] - response = client.get(url, data={'query': query}) + expected = releases['search']['brontide matador'] + response = logged_in_api_client.get(url, data={'query': query}) - assert expected == json.loads(response.content.decode('utf-8')) + assert expected == response.data -def test_can_search_artists_in_musicbrainz_api(db, mocker, 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=api_data.artists['search']['lost fingers']) + return_value=artists['search']['lost fingers']) query = 'lost fingers' url = reverse('api:v1:providers:musicbrainz:search-artists') - expected = api_data.artists['search']['lost fingers'] - response = client.get(url, data={'query': query}) + expected = artists['search']['lost fingers'] + response = logged_in_api_client.get(url, data={'query': query}) - assert expected == json.loads(response.content.decode('utf-8')) + assert expected == response.data -def test_can_get_artist_in_musicbrainz_api(db, mocker, client): +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=api_data.artists['get']['lost fingers']) + return_value=artists['get']['lost fingers']) uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9' url = reverse('api:v1:providers:musicbrainz:artist-detail', kwargs={ 'uuid': uuid, }) - response = client.get(url) - expected = api_data.artists['get']['lost fingers'] + response = logged_in_api_client.get(url) + expected = artists['get']['lost fingers'] - assert expected == json.loads(response.content.decode('utf-8')) + assert expected == response.data -def test_can_broswe_release_group_using_musicbrainz_api(db, mocker, client): +def test_can_broswe_release_group_using_musicbrainz_api( + release_groups, db, mocker, logged_in_api_client): mocker.patch( 'funkwhale_api.musicbrainz.api.release_groups.browse', - return_value=api_data.release_groups['browse']['lost fingers']) + return_value=release_groups['browse']['lost fingers']) uuid = 'ac16bbc0-aded-4477-a3c3-1d81693d58c9' url = reverse( 'api:v1:providers:musicbrainz:release-group-browse', @@ -67,16 +68,17 @@ def test_can_broswe_release_group_using_musicbrainz_api(db, mocker, client): 'artist_uuid': uuid, } ) - response = client.get(url) - expected = api_data.release_groups['browse']['lost fingers'] + response = logged_in_api_client.get(url) + expected = release_groups['browse']['lost fingers'] - assert expected == json.loads(response.content.decode('utf-8')) + assert expected == response.data -def test_can_broswe_releases_using_musicbrainz_api(db, mocker, client): +def test_can_broswe_releases_using_musicbrainz_api( + releases, db, mocker, logged_in_api_client): mocker.patch( 'funkwhale_api.musicbrainz.api.releases.browse', - return_value=api_data.releases['browse']['Lost in the 80s']) + return_value=releases['browse']['Lost in the 80s']) uuid = 'f04ed607-11b7-3843-957e-503ecdd485d1' url = reverse( 'api:v1:providers:musicbrainz:release-browse', @@ -84,7 +86,7 @@ def test_can_broswe_releases_using_musicbrainz_api(db, mocker, client): 'release_group_uuid': uuid, } ) - response = client.get(url) - expected = api_data.releases['browse']['Lost in the 80s'] + response = logged_in_api_client.get(url) + expected = releases['browse']['Lost in the 80s'] - assert expected == json.loads(response.content.decode('utf-8')) + assert expected == response.data diff --git a/api/tests/playlists/__init__.py b/api/tests/playlists/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/tests/radios/__init__.py b/api/tests/radios/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/changes/changelog.d/141.enhancement b/changes/changelog.d/141.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..5bccbfee5d45d5b4ffa772fb94023274ac07e826 --- /dev/null +++ b/changes/changelog.d/141.enhancement @@ -0,0 +1,2 @@ +API endpoint for fetching instance activity and updated timeline to use this new +endpoint (#141) diff --git a/dev.yml b/dev.yml index 8d2129bef978e78bc8c1a874ae19692d8ee62997..dd3a55ddcb2eba19927a13ab1ae4b3e307f907f2 100644 --- a/dev.yml +++ b/dev.yml @@ -50,6 +50,7 @@ services: - ./api:/app - ./data/music:/music environment: + - "PYTHONDONTWRITEBYTECODE=true" - "DJANGO_ALLOWED_HOSTS=localhost,nginx" - "DJANGO_SETTINGS_MODULE=config.settings.local" - "DJANGO_SECRET_KEY=dev" diff --git a/front/src/App.vue b/front/src/App.vue index bff52e97e399cfe545bb913c9f1ba1cbaf943a6f..e8ab18694ae4c44a5c6995d263b194dffe598dc2 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -36,9 +36,6 @@ </template> <script> -import { WebSocketBridge } from 'django-channels' - -import logger from '@/logging' import Sidebar from '@/components/Sidebar' import Raven from '@/components/Raven' @@ -53,34 +50,11 @@ export default { }, created () { this.$store.dispatch('instance/fetchSettings') - this.openWebsocket() let self = this setInterval(() => { // used to redraw ago dates every minute self.$store.commit('ui/computeLastDate') }, 1000 * 60) - }, - methods: { - openWebsocket () { - if (!this.$store.state.auth.authenticated) { - return - } - let self = this - let token = this.$store.state.auth.token - // let token = 'test' - const bridge = new WebSocketBridge() - bridge.connect( - `/api/v1/instance/activity?token=${token}`, - null, - {reconnectInterval: 5000}) - bridge.listen(function (event) { - logger.default.info('Received timeline update', event) - self.$store.commit('instance/event', event) - }) - bridge.socket.addEventListener('open', function () { - console.log('Connected to WebSocket') - }) - } } } </script> diff --git a/front/src/store/instance.js b/front/src/store/instance.js index 2436eab079cd72f11fe48ecf2f64e857bc4f1e58..245acaf039adb4cc92e5df0760c3b38733e6b241 100644 --- a/front/src/store/instance.js +++ b/front/src/store/instance.js @@ -43,6 +43,9 @@ export default { if (state.events.length > state.maxEvents) { state.events = state.events.slice(0, state.maxEvents) } + }, + events: (state, value) => { + state.events = value } }, actions: { diff --git a/front/src/views/instance/Timeline.vue b/front/src/views/instance/Timeline.vue index b959c25d66d397fc24c3c4b9d2ab62f26cdfb9fb..8ffcd9758f63a6727999a8000659b6a36c99b651 100644 --- a/front/src/views/instance/Timeline.vue +++ b/front/src/views/instance/Timeline.vue @@ -1,7 +1,10 @@ <template> <div class="main pusher"> <div class="ui vertical center aligned stripe segment"> - <div class="ui text container"> + <div v-if="isLoading" :class="['ui', {'active': isLoading}, 'inverted', 'dimmer']"> + <div class="ui text loader">Loading timeline...</div> + </div> + <div v-else class="ui text container"> <h1 class="ui header">Recent activity on this instance</h1> <div class="ui feed"> <component @@ -26,6 +29,9 @@ <script> import {mapState} from 'vuex' +import { WebSocketBridge } from 'django-channels' +import axios from 'axios' +import logger from '@/logging' import Like from '@/components/activity/Like' import Listen from '@/components/activity/Listen' @@ -33,16 +39,51 @@ import Listen from '@/components/activity/Listen' export default { data () { return { + isLoading: false, components: { 'Like': Like, 'Listen': Listen } } }, + created () { + this.openWebsocket() + this.fetchEvents() + }, computed: { ...mapState({ events: state => state.instance.events }) + }, + methods: { + fetchEvents () { + this.isLoading = true + let self = this + axios.get('/activity/').then((response) => { + self.isLoading = false + self.$store.commit('instance/events', response.data.results) + }) + }, + openWebsocket () { + if (!this.$store.state.auth.authenticated) { + return + } + let self = this + let token = this.$store.state.auth.token + // let token = 'test' + const bridge = new WebSocketBridge() + bridge.connect( + `/api/v1/instance/activity?token=${token}`, + null, + {reconnectInterval: 5000}) + bridge.listen(function (event) { + logger.default.info('Received timeline update', event) + self.$store.commit('instance/event', event) + }) + bridge.socket.addEventListener('open', function () { + console.log('Connected to WebSocket') + }) + } } } </script>