diff --git a/api/config/api_urls.py b/api/config/api_urls.py
index ff6db0d06..cab6805b6 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 325d1e820..fd9b185cf 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 000000000..46336930e
--- /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 000000000..e66de1ccf
--- /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 ef9f840dc..1a18b5f27 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 6d0480e73..5ddfb8998 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 000000000..d83dbb0a4
--- /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 56310ddc0..762d5bf7b 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 7a2280cea..8fe6fa6e0 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 000000000..e69de29bb
diff --git a/api/tests/activity/test_serializers.py b/api/tests/activity/test_serializers.py
new file mode 100644
index 000000000..792fa74b9
--- /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 000000000..43bb45df8
--- /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 000000000..bdc3c6339
--- /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 000000000..e69de29bb
diff --git a/api/tests/common/__init__.py b/api/tests/common/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/api/tests/common/test_fields.py b/api/tests/common/test_fields.py
index 7c63431a3..29a8fb05c 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 b5c5160f8..f04f12e0b 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 62bc5ada6..d2ff01bc5 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 000000000..e69de29bb
diff --git a/api/tests/history/test_activity.py b/api/tests/history/test_activity.py
index b5ab07b82..04000604b 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 000000000..e69de29bb
diff --git a/api/tests/music/conftest.py b/api/tests/music/conftest.py
new file mode 100644
index 000000000..1d0fa4e38
--- /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 """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+System Of A Down:Chop Suey! Lyrics - LyricWikia - Wikia
+
+
+
+We're rolling "Suicide". Wake up (wake up) Grab a brush and put on a little makeup Hide the scars to fade away the shakeup (hide the scars to fade away the) Why'd you leave the keys upon the table? Here you go, create another fable You wanted to Grab a brush and put a little makeup You wanted to Hide the scars to fade away the shakeup You wanted to Why'd you leave the keys upon the table? You wanted to I don't think you trust In my self-righteous suicide I cry when angels deserve to die Wake up (wake up) Grab a brush and put on a little makeup Hide the scars to fade away the (hide the scars to fade away the) Why'd you leave the keys upon the table? Here you go, create another fable You wanted to Grab a brush and put a little makeup You wanted to Hide the scars to fade away the shakeup You wanted to Why'd you leave the keys upon the table? You wanted to I don't think you trust In my self-righteous suicide I cry when angels deserve to die In my self-righteous suicide I cry when angels deserve to die Father (father) Father (father) Father (father) Father (father) Father, into your hands I commit my spirit Father, into your hands Why have you forsaken me? In your eyes forsaken me In your thoughts forsaken me In your heart forsaken me, oh Trust in my self-righteous suicide I cry when angels deserve to die In my self-righteous suicide I cry when angels deserve to die
+
+
+"""
+
+
+@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\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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-System Of A Down:Chop Suey! Lyrics - LyricWikia - Wikia
-
-
-
-We're rolling "Suicide". Wake up (wake up) Grab a brush and put on a little makeup Hide the scars to fade away the shakeup (hide the scars to fade away the) Why'd you leave the keys upon the table? Here you go, create another fable You wanted to Grab a brush and put a little makeup You wanted to Hide the scars to fade away the shakeup You wanted to Why'd you leave the keys upon the table? You wanted to I don't think you trust In my self-righteous suicide I cry when angels deserve to die Wake up (wake up) Grab a brush and put on a little makeup Hide the scars to fade away the (hide the scars to fade away the) Why'd you leave the keys upon the table? Here you go, create another fable You wanted to Grab a brush and put a little makeup You wanted to Hide the scars to fade away the shakeup You wanted to Why'd you leave the keys upon the table? You wanted to I don't think you trust In my self-righteous suicide I cry when angels deserve to die In my self-righteous suicide I cry when angels deserve to die Father (father) Father (father) Father (father) Father (father) Father, into your hands I commit my spirit Father, into your hands Why have you forsaken me? In your eyes forsaken me In your thoughts forsaken me In your heart forsaken me, oh Trust in my self-righteous suicide I cry when angels deserve to die In my self-righteous suicide I cry when angels deserve to die
-
-
-
-"""
diff --git a/api/tests/music/test_api.py b/api/tests/music/test_api.py
index 8196d3c09..625bf9d2b 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 f2ca1abbd..0f709e81f 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 d10d113d7..3aee368c0 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 076ad2bd0..4162912e4 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 5ecf9b9e4..ddbc4ba9a 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 9b72768ad..13f6447be 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 1d7b9a3de..505d6e553 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 bbade3400..fdd1dbdb0 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 000000000..e69de29bb
diff --git a/api/tests/radios/__init__.py b/api/tests/radios/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/changes/changelog.d/141.enhancement b/changes/changelog.d/141.enhancement
new file mode 100644
index 000000000..5bccbfee5
--- /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 8d2129bef..dd3a55ddc 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 bff52e97e..e8ab18694 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -36,9 +36,6 @@
diff --git a/front/src/store/instance.js b/front/src/store/instance.js
index 2436eab07..245acaf03 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 b959c25d6..8ffcd9758 100644
--- a/front/src/views/instance/Timeline.vue
+++ b/front/src/views/instance/Timeline.vue
@@ -1,7 +1,10 @@
-
+
+
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')
+ })
+ }
}
}