442 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			442 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
| import json
 | |
| import random
 | |
| 
 | |
| import pytest
 | |
| from django.urls import reverse
 | |
| from rest_framework.exceptions import ValidationError
 | |
| 
 | |
| from funkwhale_api.favorites.models import TrackFavorite
 | |
| from funkwhale_api.radios import models, radios, serializers
 | |
| 
 | |
| 
 | |
| def test_can_pick_track_from_choices():
 | |
|     choices = [1, 2, 3, 4, 5]
 | |
| 
 | |
|     radio = radios.SimpleRadio()
 | |
| 
 | |
|     first_pick = radio.pick(choices=choices)
 | |
| 
 | |
|     assert first_pick in choices
 | |
| 
 | |
|     previous_choices = [first_pick]
 | |
|     for remaining_choice in choices:
 | |
|         pick = radio.pick(choices=choices, previous_choices=previous_choices)
 | |
|         assert pick in set(choices).difference(set(previous_choices))
 | |
| 
 | |
| 
 | |
| def test_can_pick_by_weight():
 | |
|     choices_with_weight = [
 | |
|         # choice, weight
 | |
|         (1, 1),
 | |
|         (2, 2),
 | |
|         (3, 3),
 | |
|         (4, 4),
 | |
|         (5, 5),
 | |
|     ]
 | |
| 
 | |
|     picks = {choice: 0 for choice, weight in choices_with_weight}
 | |
| 
 | |
|     for i in range(1000):
 | |
|         radio = radios.SimpleRadio()
 | |
|         pick = radio.weighted_pick(choices=choices_with_weight)
 | |
|         picks[pick] = picks[pick] + 1
 | |
| 
 | |
|     assert picks[5] > picks[4]
 | |
|     assert picks[4] > picks[3]
 | |
|     assert picks[3] > picks[2]
 | |
|     assert picks[2] > picks[1]
 | |
| 
 | |
| 
 | |
| def test_session_radio_excludes_previous_picks(factories):
 | |
|     tracks = factories["music.Track"].create_batch(5)
 | |
|     user = factories["users.User"](with_actor=True)
 | |
|     previous_choices = []
 | |
|     for i in range(5):
 | |
|         TrackFavorite.add(track=random.choice(tracks), actor=user.actor)
 | |
| 
 | |
|     radio = radios.SessionRadio()
 | |
|     radio.radio_type = "favorites"
 | |
|     radio.start_session(user)
 | |
| 
 | |
|     for i in range(5):
 | |
|         pick = radio.pick(user=user, filter_playable=False)
 | |
|         assert pick in tracks
 | |
|         assert pick not in previous_choices
 | |
|         previous_choices.append(pick)
 | |
| 
 | |
|     with pytest.raises(ValueError):
 | |
|         # no more picks available
 | |
|         radio.pick(user=user, filter_playable=False)
 | |
| 
 | |
| 
 | |
| def test_can_get_choices_for_favorites_radio(factories):
 | |
|     files = factories["music.Upload"].create_batch(10)
 | |
|     tracks = [f.track for f in files]
 | |
|     user = factories["users.User"](with_actor=True)
 | |
|     for i in range(5):
 | |
|         TrackFavorite.add(track=random.choice(tracks), actor=user.actor)
 | |
| 
 | |
|     radio = radios.FavoritesRadio()
 | |
|     choices = radio.get_choices(user=user)
 | |
| 
 | |
|     assert choices.count() == user.actor.track_favorites.all().count()
 | |
| 
 | |
|     for favorite in user.actor.track_favorites.all():
 | |
|         assert favorite.track in choices
 | |
| 
 | |
|     for i in range(5):
 | |
|         pick = radio.pick(user=user)
 | |
|         assert pick in choices
 | |
| 
 | |
| 
 | |
| def test_can_get_choices_for_custom_radio(factories):
 | |
|     artist = factories["music.Artist"]()
 | |
|     files = factories["music.Upload"].create_batch(
 | |
|         5, track__artist_credit__artist=artist
 | |
|     )
 | |
|     tracks = [f.track for f in files]
 | |
|     factories["music.Upload"].create_batch(5)
 | |
| 
 | |
|     session = factories["radios.CustomRadioSession"](
 | |
|         custom_radio__config=[{"type": "artist", "ids": [artist.pk]}]
 | |
|     )
 | |
|     choices = session.radio(api_version=1).get_choices(filter_playable=False)
 | |
| 
 | |
|     expected = [t.pk for t in tracks]
 | |
|     assert list(choices.values_list("id", flat=True)) == expected
 | |
| 
 | |
| 
 | |
| def test_cannot_start_custom_radio_if_not_owner_or_not_public(factories):
 | |
|     user = factories["users.User"]()
 | |
|     artist = factories["music.Artist"]()
 | |
|     radio = factories["radios.Radio"](config=[{"type": "artist", "ids": [artist.pk]}])
 | |
|     serializer = serializers.RadioSessionSerializer(
 | |
|         data={"radio_type": "custom", "custom_radio": radio.pk, "user": user.pk}
 | |
|     )
 | |
|     message = "You don't have access to this radio"
 | |
|     assert not serializer.is_valid()
 | |
|     assert message in serializer.errors["non_field_errors"]
 | |
| 
 | |
| 
 | |
| def test_can_start_custom_radio_from_api(logged_in_api_client, factories):
 | |
|     artist = factories["music.Artist"]()
 | |
|     radio = factories["radios.Radio"](
 | |
|         config=[{"type": "artist", "ids": [artist.pk]}], user=logged_in_api_client.user
 | |
|     )
 | |
|     url = reverse("api:v1:radios:sessions-list")
 | |
|     response = logged_in_api_client.post(
 | |
|         url, {"radio_type": "custom", "custom_radio": radio.pk}
 | |
|     )
 | |
|     assert response.status_code == 201
 | |
|     session = radio.sessions.latest("id")
 | |
|     assert session.radio_type == "custom"
 | |
|     assert session.user == logged_in_api_client.user
 | |
| 
 | |
| 
 | |
| def test_can_use_radio_session_to_filter_choices(factories):
 | |
|     factories["music.Upload"].create_batch(10)
 | |
|     user = factories["users.User"]()
 | |
|     radio = radios.RandomRadio()
 | |
|     session = radio.start_session(user)
 | |
| 
 | |
|     for i in range(10):
 | |
|         radio.pick(filter_playable=False)
 | |
| 
 | |
|     # ensure 10 different tracks have been suggested
 | |
|     tracks_id = [
 | |
|         session_track.track.pk for session_track in session.session_tracks.all()
 | |
|     ]
 | |
|     assert len(set(tracks_id)) == 10
 | |
| 
 | |
| 
 | |
| def test_can_restore_radio_from_previous_session(factories):
 | |
|     user = factories["users.User"]()
 | |
|     radio = radios.RandomRadio()
 | |
|     session = radio.start_session(user)
 | |
| 
 | |
|     restarted_radio = radios.RandomRadio(session)
 | |
|     assert radio.session == restarted_radio.session
 | |
| 
 | |
| 
 | |
| def test_can_start_radio_for_logged_in_user(logged_in_api_client):
 | |
|     url = reverse("api:v1:radios:sessions-list")
 | |
|     logged_in_api_client.post(url, {"radio_type": "random"})
 | |
|     session = models.RadioSession.objects.latest("id")
 | |
|     assert session.radio_type == "random"
 | |
|     assert session.user == logged_in_api_client.user
 | |
| 
 | |
| 
 | |
| def test_can_get_track_for_session_from_api(factories, logged_in_api_client):
 | |
|     actor = logged_in_api_client.user.create_actor()
 | |
|     track = factories["music.Upload"](
 | |
|         library__actor=actor, import_status="finished"
 | |
|     ).track
 | |
|     url = reverse("api:v1:radios:sessions-list")
 | |
|     response = logged_in_api_client.post(url, {"radio_type": "random"})
 | |
|     session = models.RadioSession.objects.latest("id")
 | |
| 
 | |
|     url = reverse("api:v1:radios:tracks-list")
 | |
|     response = logged_in_api_client.post(url, {"session": session.pk})
 | |
|     data = json.loads(response.content.decode("utf-8"))
 | |
| 
 | |
|     assert data["track"]["id"] == track.pk
 | |
|     assert data["position"] == 1
 | |
| 
 | |
|     next_track = factories["music.Upload"](
 | |
|         library__actor=actor, import_status="finished"
 | |
|     ).track
 | |
|     response = logged_in_api_client.post(url, {"session": session.pk})
 | |
|     data = json.loads(response.content.decode("utf-8"))
 | |
| 
 | |
|     assert data["track"]["id"] == next_track.id
 | |
|     assert data["position"] == 2
 | |
| 
 | |
| 
 | |
| def test_related_object_radio_validate_related_object(factories):
 | |
|     # cannot start without related object
 | |
|     radio = {"radio_type": "tag"}
 | |
|     serializer = serializers.RadioSessionSerializer()
 | |
|     with pytest.raises(ValidationError):
 | |
|         serializer.validate(data=radio)
 | |
| 
 | |
|     # cannot start with bad related object type
 | |
|     radio = {"radio_type": "tag", "related_object": "whatever"}
 | |
|     serializer = serializers.RadioSessionSerializer()
 | |
|     with pytest.raises(ValidationError):
 | |
|         serializer.validate(data=radio)
 | |
| 
 | |
| 
 | |
| def test_can_start_artist_radio(factories):
 | |
|     user = factories["users.User"]()
 | |
|     artist = factories["music.Artist"]()
 | |
|     factories["music.Upload"].create_batch(5)
 | |
|     good_files = factories["music.Upload"].create_batch(
 | |
|         5, track__artist_credit__artist=artist
 | |
|     )
 | |
|     good_tracks = [f.track for f in good_files]
 | |
| 
 | |
|     radio = radios.ArtistRadio()
 | |
|     session = radio.start_session(user, related_object=artist)
 | |
|     assert session.radio_type == "artist"
 | |
|     for i in range(5):
 | |
|         assert radio.pick(filter_playable=False) in good_tracks
 | |
| 
 | |
| 
 | |
| def test_can_start_tag_radio(factories):
 | |
|     user = factories["users.User"]()
 | |
|     tag = factories["tags.Tag"]()
 | |
|     good_tracks = [
 | |
|         factories["music.Track"](set_tags=[tag.name]),
 | |
|         factories["music.Track"](album__set_tags=[tag.name]),
 | |
|         factories["music.Track"](album__artist_credit__artist__set_tags=[tag.name]),
 | |
|     ]
 | |
|     factories["music.Track"].create_batch(3, set_tags=["notrock"])
 | |
| 
 | |
|     radio = radios.TagRadio()
 | |
|     session = radio.start_session(user, related_object=tag)
 | |
|     assert session.radio_type == "tag"
 | |
| 
 | |
|     for i in range(3):
 | |
|         assert radio.pick(filter_playable=False) in good_tracks
 | |
| 
 | |
| 
 | |
| def test_can_start_actor_content_radio(factories):
 | |
|     actor_library = factories["music.Library"](actor__local=True)
 | |
|     good_tracks = [
 | |
|         factories["music.Upload"](playable=True, library=actor_library).track,
 | |
|         factories["music.Upload"](playable=True, library=actor_library).track,
 | |
|         factories["music.Upload"](playable=True, library=actor_library).track,
 | |
|     ]
 | |
|     factories["music.Upload"].create_batch(3, playable=True)
 | |
| 
 | |
|     radio = radios.ActorContentRadio()
 | |
|     session = radio.start_session(
 | |
|         actor_library.actor.user, related_object=actor_library.actor
 | |
|     )
 | |
|     assert session.radio_type == "actor-content"
 | |
| 
 | |
|     for i in range(3):
 | |
|         assert radio.pick() in good_tracks
 | |
| 
 | |
| 
 | |
| def test_can_start_actor_content_radio_from_api(
 | |
|     logged_in_api_client, preferences, factories
 | |
| ):
 | |
|     actor = factories["federation.Actor"]()
 | |
|     url = reverse("api:v1:radios:sessions-list")
 | |
| 
 | |
|     response = logged_in_api_client.post(
 | |
|         url, {"radio_type": "actor-content", "related_object_id": actor.full_username}
 | |
|     )
 | |
| 
 | |
|     assert response.status_code == 201
 | |
| 
 | |
|     session = models.RadioSession.objects.latest("id")
 | |
| 
 | |
|     assert session.radio_type == "actor-content"
 | |
|     assert session.related_object == actor
 | |
| 
 | |
| 
 | |
| def test_can_start_library_radio(factories):
 | |
|     user = factories["users.User"]()
 | |
|     library = factories["music.Library"]()
 | |
|     good_tracks = [
 | |
|         factories["music.Upload"](library=library).track,
 | |
|         factories["music.Upload"](library=library).track,
 | |
|         factories["music.Upload"](library=library).track,
 | |
|     ]
 | |
|     factories["music.Upload"].create_batch(3)
 | |
| 
 | |
|     radio = radios.LibraryRadio()
 | |
|     session = radio.start_session(user, related_object=library)
 | |
|     assert session.radio_type == "library"
 | |
| 
 | |
|     for i in range(3):
 | |
|         assert radio.pick(filter_playable=False) in good_tracks
 | |
| 
 | |
| 
 | |
| def test_can_start_library_radio_from_api(logged_in_api_client, preferences, factories):
 | |
|     library = factories["music.Library"]()
 | |
|     url = reverse("api:v1:radios:sessions-list")
 | |
| 
 | |
|     response = logged_in_api_client.post(
 | |
|         url, {"radio_type": "library", "related_object_id": library.uuid}
 | |
|     )
 | |
| 
 | |
|     assert response.status_code == 201
 | |
| 
 | |
|     session = models.RadioSession.objects.latest("id")
 | |
| 
 | |
|     assert session.radio_type == "library"
 | |
|     assert session.related_object == library
 | |
| 
 | |
| 
 | |
| def test_can_start_artist_radio_from_api(logged_in_api_client, preferences, factories):
 | |
|     artist = factories["music.Artist"]()
 | |
|     url = reverse("api:v1:radios:sessions-list")
 | |
| 
 | |
|     response = logged_in_api_client.post(
 | |
|         url, {"radio_type": "artist", "related_object_id": artist.id}
 | |
|     )
 | |
| 
 | |
|     assert response.status_code == 201
 | |
| 
 | |
|     session = models.RadioSession.objects.latest("id")
 | |
| 
 | |
|     assert session.radio_type == "artist"
 | |
|     assert session.related_object == artist
 | |
| 
 | |
| 
 | |
| def test_can_start_less_listened_radio(factories):
 | |
|     user = factories["users.User"](with_actor=True)
 | |
|     wrong_files = factories["music.Upload"].create_batch(5)
 | |
|     for f in wrong_files:
 | |
|         factories["history.Listening"](track=f.track, actor=user.actor)
 | |
|     good_files = factories["music.Upload"].create_batch(5)
 | |
|     good_tracks = [f.track for f in good_files]
 | |
|     radio = radios.LessListenedRadio()
 | |
|     radio.start_session(user)
 | |
| 
 | |
|     for i in range(5):
 | |
|         assert radio.pick(filter_playable=False) in good_tracks
 | |
| 
 | |
| 
 | |
| def test_similar_radio_track(factories):
 | |
|     user = factories["users.User"]()
 | |
|     seed = factories["music.Track"]()
 | |
|     radio = radios.SimilarRadio()
 | |
|     radio.start_session(user, related_object=seed)
 | |
| 
 | |
|     factories["music.Track"].create_batch(5)
 | |
| 
 | |
|     # one user listened to this track
 | |
|     l1user = factories["users.User"](with_actor=True)
 | |
|     l1 = factories["history.Listening"](track=seed, actor=l1user.actor)
 | |
| 
 | |
|     expected_next = factories["music.Track"]()
 | |
|     factories["history.Listening"](track=expected_next, actor=l1.actor)
 | |
| 
 | |
|     assert radio.pick(filter_playable=False) == expected_next
 | |
| 
 | |
| 
 | |
| def test_session_radio_get_queryset_ignore_filtered_track_artist(
 | |
|     factories, queryset_equal_list
 | |
| ):
 | |
|     cf = factories["moderation.UserFilter"](for_artist=True)
 | |
|     factories["music.Track"](artist_credit__artist=cf.target_artist)
 | |
|     valid_track = factories["music.Track"]()
 | |
|     radio = radios.RandomRadio()
 | |
|     radio.start_session(user=cf.user)
 | |
| 
 | |
|     assert radio.get_queryset() == [valid_track]
 | |
| 
 | |
| 
 | |
| def test_session_radio_get_queryset_ignore_filtered_track_album_artist(
 | |
|     factories, queryset_equal_list
 | |
| ):
 | |
|     cf = factories["moderation.UserFilter"](for_artist=True)
 | |
|     factories["music.Track"](album__artist_credit__artist=cf.target_artist)
 | |
|     valid_track = factories["music.Track"]()
 | |
|     radio = radios.RandomRadio()
 | |
|     radio.start_session(user=cf.user)
 | |
| 
 | |
|     assert radio.get_queryset() == [valid_track]
 | |
| 
 | |
| 
 | |
| def test_get_choices_for_custom_radio_exclude_artist(factories):
 | |
|     included_artist = factories["music.Artist"]()
 | |
|     excluded_artist = factories["music.Artist"]()
 | |
|     included_uploads = factories["music.Upload"].create_batch(
 | |
|         5, track__artist_credit__artist=included_artist
 | |
|     )
 | |
|     factories["music.Upload"].create_batch(
 | |
|         5, track__artist_credit__artist=excluded_artist
 | |
|     )
 | |
| 
 | |
|     session = factories["radios.CustomRadioSession"](
 | |
|         custom_radio__config=[
 | |
|             {"type": "artist", "ids": [included_artist.pk]},
 | |
|             {"type": "artist", "ids": [excluded_artist.pk], "not": True},
 | |
|         ]
 | |
|     )
 | |
|     choices = session.radio(api_version=1).get_choices(filter_playable=False)
 | |
| 
 | |
|     expected = [u.track.pk for u in included_uploads]
 | |
|     assert list(choices.values_list("id", flat=True)) == expected
 | |
| 
 | |
| 
 | |
| def test_get_choices_for_custom_radio_exclude_tag(factories):
 | |
|     included_uploads = factories["music.Upload"].create_batch(
 | |
|         5, track__set_tags=["rap"]
 | |
|     )
 | |
|     factories["music.Upload"].create_batch(5, track__set_tags=["rock", "rap"])
 | |
| 
 | |
|     session = factories["radios.CustomRadioSession"](
 | |
|         custom_radio__config=[
 | |
|             {"type": "tag", "names": ["rap"]},
 | |
|             {"type": "tag", "names": ["rock"], "not": True},
 | |
|         ]
 | |
|     )
 | |
|     choices = session.radio(api_version=1).get_choices(filter_playable=False)
 | |
| 
 | |
|     expected = [u.track.pk for u in included_uploads]
 | |
|     assert list(choices.values_list("id", flat=True)) == expected
 | |
| 
 | |
| 
 | |
| def test_can_start_custom_multiple_radio_from_api(api_client, factories):
 | |
|     tracks = factories["music.Track"].create_batch(5)
 | |
|     url = reverse("api:v1:radios:sessions-list")
 | |
|     map_filters_to_type = {"tags": "names", "artists": "ids", "playlists": "names"}
 | |
|     for key, value in map_filters_to_type.items():
 | |
|         attr = value[:-1]
 | |
|         track_filter_key = [
 | |
|             getattr(a.artist_credit.all()[0].artist, attr) for a in tracks
 | |
|         ]
 | |
|         config = {"filters": [{"type": key, value: track_filter_key}]}
 | |
|         response = api_client.post(
 | |
|             url,
 | |
|             {"radio_type": "custom_multiple", "config": config},
 | |
|             format="json",
 | |
|         )
 | |
|         assert response.status_code == 201
 |