Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 1.4.1-upgrade-release
  • 1121-download
  • 1218-smartplaylist_backend
  • 1373-login-form-move-reset-your-password-link
  • 1381-progress-bars
  • 1481
  • 1518-update-django-allauth
  • 1645
  • 1675-widget-improperly-configured-missing-resource-id
  • 1675-widget-improperly-configured-missing-resource-id-2
  • 1704-required-props-are-not-always-passed
  • 1716-add-frontend-tests-again
  • 1749-smtp-uri-configuration
  • 1930-first-upload-in-a-batch-always-fails
  • 1976-update-documentation-links-in-readme-files
  • 2054-player-layout
  • 2063-funkwhale-connection-interrupted-every-so-often-requires-network-reset-page-refresh
  • 2091-iii-6-improve-visuals-layout
  • 2151-refused-to-load-spa-manifest-json-2
  • 2154-add-to-playlist-pop-up-hidden-by-now-playing-screen
  • 2155-can-t-see-the-episode-list-of-a-podcast-as-an-anonymous-user-with-anonymous-access-enabled
  • 2156-add-management-command-to-change-file-ref-for-in-place-imported-files-to-s3
  • 2192-clear-queue-bug-when-random-shuffle-is-enabled
  • 2205-channel-page-pagination-link-dont-working
  • 2215-custom-logger-does-not-work-at-all-with-webkit-and-blink-based-browsers
  • 2228-troi-real-world-review
  • 2274-implement-new-upload-api
  • 2303-allow-users-to-own-tagged-items
  • 2395-far-right-filter
  • 2405-front-buttont-trigger-third-party-hook
  • 2408-troi-create-missing-tracks
  • 2416-revert-library-drop
  • 2429-fix-popover-auto-close
  • 2448-complete-tags
  • 2451-delete-no-user-query
  • 2452-fetch-third-party-metadata
  • 2469-Fix-search-bar-in-ManageUploads
  • 2490-fix-search-modal
  • 2490-search-modal
  • 2492-only-deliver-to-reachable-domains
  • 2494-check-instance-availability-exponentially
  • 2501-fix-compatibility-with-older-browsers
  • 2502-drop-uno-and-jquery
  • 2503-apiactoserializer-icon
  • 2506-fix-frontend-regressions
  • 623-test
  • 653-enable-starting-embedded-player-at-a-specific-position-in-track
  • activitypub-overview
  • album-sliders
  • arne/2091-improve-visuals
  • back-option-for-edits
  • chore/2406-compose-modularity-scope
  • develop
  • develop-password-reset
  • env-file-cleanup
  • feat/2091-improve-visuals
  • feature/2481-vui-translations
  • fix-amd64-docker-build-gfortran
  • fix-front-node-version
  • fix-gitpod
  • fix-plugins-dev-setup
  • fix-rate-limit-serializer
  • fix-schema-channel-metadata-choices
  • flupsi/2803-improve-visuals
  • flupsi/2804-new-upload-process
  • funkwhale-fix_pwa_manifest
  • funkwhale-petitminion-2136-bug-fix-prune-skipped-upload
  • funkwhale-ui-buttons
  • georg/add-typescript
  • gitpod/test-1866
  • global-button-experiment
  • global-buttons
  • juniorjpdj/pkg-repo
  • manage-py-reference
  • merge-review
  • minimal-python-version
  • petitminion-develop-patch-84496
  • pin-mutagen-to-1.46
  • pipenv
  • plugins
  • plugins-v2
  • plugins-v3
  • pre-release/1.3.0
  • prune_skipped_uploads_docs
  • refactor/homepage
  • renovate/front-all-dependencies
  • renovate/front-major-all-dependencies
  • schema-updates
  • small-gitpod-improvements
  • spectacular_schema
  • stable
  • tempArne
  • ui-buttons
  • update-frontend-dependencies
  • upload-process-spec
  • user-concept-docs
  • v2-artists
  • vite-ws-ssl-compatible
  • wip/2091-improve-visuals
  • wvffle/dependency-maintenance
  • 0.1
  • 0.10
  • 0.11
  • 0.12
  • 0.13
  • 0.14
  • 0.14.1
  • 0.14.2
  • 0.15
  • 0.16
  • 0.16.1
  • 0.16.2
  • 0.16.3
  • 0.17
  • 0.18
  • 0.18.1
  • 0.18.2
  • 0.18.3
  • 0.19.0
  • 0.19.0-rc1
  • 0.19.0-rc2
  • 0.19.1
  • 0.2
  • 0.2.1
  • 0.2.2
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.20.0
  • 0.20.0-rc1
  • 0.20.1
  • 0.21
  • 0.21-rc1
  • 0.21-rc2
  • 0.21.1
  • 0.21.2
  • 0.3
  • 0.3.1
  • 0.3.2
  • 0.3.3
  • 0.3.4
  • 0.3.5
  • 0.4
  • 0.5
  • 0.5.1
  • 0.5.2
  • 0.5.3
  • 0.5.4
  • 0.6
  • 0.6.1
  • 0.7
  • 0.8
  • 0.9
  • 0.9.1
  • 1.0
  • 1.0-rc1
  • 1.0.1
  • 1.1
  • 1.1-rc1
  • 1.1-rc2
  • 1.1.1
  • 1.1.2
  • 1.1.3
  • 1.1.4
  • 1.2.0
  • 1.2.0-rc1
  • 1.2.0-rc2
  • 1.2.0-testing
  • 1.2.0-testing2
  • 1.2.0-testing3
  • 1.2.0-testing4
  • 1.2.1
  • 1.2.10
  • 1.2.2
  • 1.2.3
  • 1.2.4
  • 1.2.5
  • 1.2.6
  • 1.2.6-1
  • 1.2.7
  • 1.2.8
  • 1.2.9
  • 1.3.0
  • 1.3.0-rc1
  • 1.3.0-rc2
  • 1.3.0-rc3
  • 1.3.0-rc4
  • 1.3.0-rc5
  • 1.3.0-rc6
  • 1.3.1
  • 1.3.2
  • 1.3.3
  • 1.3.4
  • 1.4.0
  • 1.4.0-rc1
  • 1.4.0-rc2
  • 1.4.1
  • 2.0.0-alpha.1
  • 2.0.0-alpha.2
200 results

Target

Select target project
  • funkwhale/funkwhale
  • Luclu7/funkwhale
  • mbothorel/funkwhale
  • EorlBruder/funkwhale
  • tcit/funkwhale
  • JocelynDelalande/funkwhale
  • eneiluj/funkwhale
  • reg/funkwhale
  • ButterflyOfFire/funkwhale
  • m4sk1n/funkwhale
  • wxcafe/funkwhale
  • andybalaam/funkwhale
  • jcgruenhage/funkwhale
  • pblayo/funkwhale
  • joshuaboniface/funkwhale
  • n3ddy/funkwhale
  • gegeweb/funkwhale
  • tohojo/funkwhale
  • emillumine/funkwhale
  • Te-k/funkwhale
  • asaintgenis/funkwhale
  • anoadragon453/funkwhale
  • Sakada/funkwhale
  • ilianaw/funkwhale
  • l4p1n/funkwhale
  • pnizet/funkwhale
  • dante383/funkwhale
  • interfect/funkwhale
  • akhardya/funkwhale
  • svfusion/funkwhale
  • noplanman/funkwhale
  • nykopol/funkwhale
  • roipoussiere/funkwhale
  • Von/funkwhale
  • aurieh/funkwhale
  • icaria36/funkwhale
  • floreal/funkwhale
  • paulwalko/funkwhale
  • comradekingu/funkwhale
  • FurryJulie/funkwhale
  • Legolars99/funkwhale
  • Vierkantor/funkwhale
  • zachhats/funkwhale
  • heyjake/funkwhale
  • sn0w/funkwhale
  • jvoisin/funkwhale
  • gordon/funkwhale
  • Alexander/funkwhale
  • bignose/funkwhale
  • qasim.ali/funkwhale
  • fakegit/funkwhale
  • Kxze/funkwhale
  • stenstad/funkwhale
  • creak/funkwhale
  • Kaze/funkwhale
  • Tixie/funkwhale
  • IISergII/funkwhale
  • lfuelling/funkwhale
  • nhaddag/funkwhale
  • yoasif/funkwhale
  • ifischer/funkwhale
  • keslerm/funkwhale
  • flupe/funkwhale
  • petitminion/funkwhale
  • ariasuni/funkwhale
  • ollie/funkwhale
  • ngaumont/funkwhale
  • techknowlogick/funkwhale
  • Shleeble/funkwhale
  • theflyingfrog/funkwhale
  • jonatron/funkwhale
  • neobrain/funkwhale
  • eorn/funkwhale
  • KokaKiwi/funkwhale
  • u1-liquid/funkwhale
  • marzzzello/funkwhale
  • sirenwatcher/funkwhale
  • newer027/funkwhale
  • codl/funkwhale
  • Zwordi/funkwhale
  • gisforgabriel/funkwhale
  • iuriatan/funkwhale
  • simon/funkwhale
  • bheesham/funkwhale
  • zeoses/funkwhale
  • accraze/funkwhale
  • meliurwen/funkwhale
  • divadsn/funkwhale
  • Etua/funkwhale
  • sdrik/funkwhale
  • Soran/funkwhale
  • kuba-orlik/funkwhale
  • cristianvogel/funkwhale
  • Forceu/funkwhale
  • jeff/funkwhale
  • der_scheibenhacker/funkwhale
  • owlnical/funkwhale
  • jovuit/funkwhale
  • SilverFox15/funkwhale
  • phw/funkwhale
  • mayhem/funkwhale
  • sridhar/funkwhale
  • stromlin/funkwhale
  • rrrnld/funkwhale
  • nitaibezerra/funkwhale
  • jaller94/funkwhale
  • pcouy/funkwhale
  • eduxstad/funkwhale
  • codingHahn/funkwhale
  • captain/funkwhale
  • polyedre/funkwhale
  • leishenailong/funkwhale
  • ccritter/funkwhale
  • lnceballosz/funkwhale
  • fpiesche/funkwhale
  • Fanyx/funkwhale
  • markusblogde/funkwhale
  • Firobe/funkwhale
  • devilcius/funkwhale
  • freaktechnik/funkwhale
  • blopware/funkwhale
  • cone/funkwhale
  • thanksd/funkwhale
  • vachan-maker/funkwhale
  • bbenti/funkwhale
  • tarator/funkwhale
  • prplecake/funkwhale
  • DMarzal/funkwhale
  • lullis/funkwhale
  • hanacgr/funkwhale
  • albjeremias/funkwhale
  • xeruf/funkwhale
  • llelite/funkwhale
  • RoiArthurB/funkwhale
  • cloo/funkwhale
  • nztvar/funkwhale
  • Keunes/funkwhale
  • petitminion/funkwhale-petitminion
  • m-idler/funkwhale
  • SkyLeite/funkwhale
140 results
Select Git revision
  • 876-http-signature
  • develop
  • master
  • plugins
  • plugins-v2
  • themev2
  • 0.1
  • 0.10
  • 0.11
  • 0.12
  • 0.13
  • 0.14
  • 0.14.1
  • 0.14.2
  • 0.15
  • 0.16
  • 0.16.1
  • 0.16.2
  • 0.16.3
  • 0.17
  • 0.18
  • 0.18.1
  • 0.18.2
  • 0.18.3
  • 0.19.0
  • 0.19.0-rc1
  • 0.19.0-rc2
  • 0.19.1
  • 0.2
  • 0.2.1
  • 0.2.2
  • 0.2.3
  • 0.2.4
  • 0.2.5
  • 0.2.6
  • 0.20.0
  • 0.20.0-rc1
  • 0.20.1
  • 0.21
  • 0.21-rc1
  • 0.21-rc2
  • 0.3
  • 0.3.1
  • 0.3.2
  • 0.3.3
  • 0.3.4
  • 0.3.5
  • 0.4
  • 0.5
  • 0.5.1
  • 0.5.2
  • 0.5.3
  • 0.5.4
  • 0.6
  • 0.6.1
  • 0.7
  • 0.8
  • 0.9
  • 0.9.1
59 results
Show changes
Showing
with 878 additions and 195 deletions
from funkwhale_api.radios import filters
def test_clean_config_artist_name_sorting(factories):
artist3 = factories["music.Artist"](name="The Green Eyes")
artist2 = factories["music.Artist"](name="The Green Eyed Machine")
artist1 = factories["music.Artist"](name="The Green Seed")
factories["music.Artist"]()
filter_config = {"type": "artist", "ids": [artist3.pk, artist1.pk, artist2.pk]}
artist_filter = filters.ArtistFilter()
config = artist_filter.clean_config(filter_config)
# list of names whose artists have been sorted by name then by id
sorted_names = [
a.name
for a in list(
sorted([artist2, artist1, artist3], key=lambda x: (len(x.name), x.id))
)
]
assert config["names"] == sorted_names
def test_clean_config_tag_name_sorting(factories):
tag3 = factories["tags.Tag"](name="Rock")
tag2 = factories["tags.Tag"](name="Classic")
tag1 = factories["tags.Tag"](name="Punk")
factories["tags.Tag"]()
filter_config = {"type": "tag", "names": [tag3.name, tag1.name, tag2.name]}
tag_filter = filters.TagFilter()
config = tag_filter.clean_config(filter_config)
# list of names whose tags have been sorted by name then by id
sorted_names = [
a.name
for a in list(sorted([tag2, tag1, tag3], key=lambda x: (len(x.name), x.id)))
]
assert config["names"] == sorted_names
import pytest
import troi.core
from django.core.cache import cache
from django.db.models import Q
from requests.exceptions import ConnectTimeout
from funkwhale_api.music.models import Track
from funkwhale_api.radios import lb_recommendations
from funkwhale_api.typesense import factories as custom_factories
from funkwhale_api.typesense import utils
def test_can_build_radio_queryset_with_fw_db(factories, mocker):
factories["music.Track"](
title="I Want It That Way", mbid="87dfa566-21c3-45ed-bc42-1d345b8563fa"
)
factories["music.Track"](
title="The Perfect Kiss", mbid="ec0da94e-fbfe-4eb0-968e-024d4c32d1d0"
)
factories["music.Track"]()
qs = Track.objects.all()
mocker.patch("funkwhale_api.typesense.utils.resolve_recordings_to_fw_track")
radio_qs = lb_recommendations.build_radio_queryset(
custom_factories.DummyPatch({"min_recordings": 1}), qs
)
recommended_recording_mbids = [
"87dfa566-21c3-45ed-bc42-1d345b8563fa",
"ec0da94e-fbfe-4eb0-968e-024d4c32d1d0",
]
assert list(
Track.objects.all().filter(Q(mbid__in=recommended_recording_mbids))
) == list(radio_qs)
def test_build_radio_queryset_without_fw_db(mocker):
resolve_recordings_to_fw_track = mocker.patch.object(
utils, "resolve_recordings_to_fw_track", return_value=None
)
# mocker.patch.object(cache, "get_many", return_value=None)
qs = Track.objects.all()
with pytest.raises(ValueError):
lb_recommendations.build_radio_queryset(
custom_factories.DummyPatch({"min_recordings": 1}), qs
)
assert resolve_recordings_to_fw_track.called_once_with(
custom_factories.recommended_recording_mbids
)
def test_build_radio_queryset_with_redis_and_fw_db(factories, mocker):
factories["music.Track"](
pk="1", title="I Want It That Way", mbid="87dfa566-21c3-45ed-bc42-1d345b8563fa"
)
mocker.patch.object(utils, "resolve_recordings_to_fw_track", return_value=None)
redis_cache = {}
redis_cache["ec0da94e-fbfe-4eb0-968e-024d4c32d1d0"] = 2
mocker.patch.object(cache, "get_many", return_value=redis_cache)
qs = Track.objects.all()
assert list(
lb_recommendations.build_radio_queryset(
custom_factories.DummyPatch({"min_recordings": 1}), qs
)
) == list(Track.objects.all().filter(pk__in=[1, 2]))
def test_build_radio_queryset_with_redis_and_without_fw_db(factories, mocker):
factories["music.Track"](
pk="1", title="Super title", mbid="87dfaaaa-2aaa-45ed-bc42-1d34aaaaaaaa"
)
mocker.patch.object(utils, "resolve_recordings_to_fw_track", return_value=None)
redis_cache = {}
redis_cache["87dfa566-21c3-45ed-bc42-1d345b8563fa"] = 1
mocker.patch.object(cache, "get_many", return_value=redis_cache)
qs = Track.objects.all()
assert list(
lb_recommendations.build_radio_queryset(
custom_factories.DummyPatch({"min_recordings": 1}), qs
)
) == list(Track.objects.all().filter(pk=1))
def test_build_radio_queryset_catch_troi_ConnectTimeout(mocker):
mocker.patch.object(
troi.patch.Patch,
"generate_playlist",
side_effect=ConnectTimeout,
)
qs = Track.objects.all()
with pytest.raises(ValueError):
lb_recommendations.build_radio_queryset(
custom_factories.DummyPatch({"min_recordings": 1}), qs
)
def test_build_radio_queryset_catch_troi_no_candidates(mocker):
mocker.patch.object(
troi.patch.Patch,
"generate_playlist",
)
qs = Track.objects.all()
with pytest.raises(ValueError):
lb_recommendations.build_radio_queryset(
custom_factories.DummyPatch({"min_recordings": 1}), qs
)
......@@ -2,8 +2,8 @@ import json
import random
import pytest
from django.core.exceptions import ValidationError
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
......@@ -21,7 +21,7 @@ def test_can_pick_track_from_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(previous_choices)
assert pick in set(choices).difference(set(previous_choices))
def test_can_pick_by_weight():
......@@ -49,10 +49,10 @@ def test_can_pick_by_weight():
def test_session_radio_excludes_previous_picks(factories):
tracks = factories["music.Track"].create_batch(5)
user = factories["users.User"]()
user = factories["users.User"](with_actor=True)
previous_choices = []
for i in range(5):
TrackFavorite.add(track=random.choice(tracks), user=user)
TrackFavorite.add(track=random.choice(tracks), actor=user.actor)
radio = radios.SessionRadio()
radio.radio_type = "favorites"
......@@ -72,16 +72,16 @@ def test_session_radio_excludes_previous_picks(factories):
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"]()
user = factories["users.User"](with_actor=True)
for i in range(5):
TrackFavorite.add(track=random.choice(tracks), user=user)
TrackFavorite.add(track=random.choice(tracks), actor=user.actor)
radio = radios.FavoritesRadio()
choices = radio.get_choices(user=user)
assert choices.count() == user.track_favorites.all().count()
assert choices.count() == user.actor.track_favorites.all().count()
for favorite in user.track_favorites.all():
for favorite in user.actor.track_favorites.all():
assert favorite.track in choices
for i in range(5):
......@@ -91,14 +91,16 @@ def test_can_get_choices_for_favorites_radio(factories):
def test_can_get_choices_for_custom_radio(factories):
artist = factories["music.Artist"]()
files = factories["music.Upload"].create_batch(5, track__artist=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.get_choices(filter_playable=False)
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
......@@ -140,7 +142,7 @@ def test_can_use_radio_session_to_filter_choices(factories):
for i in range(10):
radio.pick(filter_playable=False)
# ensure 10 differents tracks have been suggested
# ensure 10 different tracks have been suggested
tracks_id = [
session_track.track.pk for session_track in session.session_tracks.all()
]
......@@ -191,23 +193,26 @@ def test_can_get_track_for_session_from_api(factories, logged_in_api_client):
def test_related_object_radio_validate_related_object(factories):
user = factories["users.User"]()
# cannot start without related object
radio = radios.ArtistRadio()
radio = {"radio_type": "tag"}
serializer = serializers.RadioSessionSerializer()
with pytest.raises(ValidationError):
radio.start_session(user)
serializer.validate(data=radio)
# cannot start with bad related object type
radio = radios.ArtistRadio()
radio = {"radio_type": "tag", "related_object": "whatever"}
serializer = serializers.RadioSessionSerializer()
with pytest.raises(ValidationError):
radio.start_session(user, related_object=user)
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=artist)
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()
......@@ -223,7 +228,7 @@ def test_can_start_tag_radio(factories):
good_tracks = [
factories["music.Track"](set_tags=[tag.name]),
factories["music.Track"](album__set_tags=[tag.name]),
factories["music.Track"](album__artist__set_tags=[tag.name]),
factories["music.Track"](album__artist_credit__artist__set_tags=[tag.name]),
]
factories["music.Track"].create_batch(3, set_tags=["notrock"])
......@@ -248,7 +253,7 @@ def test_can_start_actor_content_radio(factories):
session = radio.start_session(
actor_library.actor.user, related_object=actor_library.actor
)
assert session.radio_type == "actor_content"
assert session.radio_type == "actor-content"
for i in range(3):
assert radio.pick() in good_tracks
......@@ -261,14 +266,14 @@ def test_can_start_actor_content_radio_from_api(
url = reverse("api:v1:radios:sessions-list")
response = logged_in_api_client.post(
url, {"radio_type": "actor_content", "related_object_id": actor.full_username}
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.radio_type == "actor-content"
assert session.related_object == actor
......@@ -323,10 +328,10 @@ def test_can_start_artist_radio_from_api(logged_in_api_client, preferences, fact
def test_can_start_less_listened_radio(factories):
user = factories["users.User"]()
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, user=user)
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()
......@@ -345,10 +350,11 @@ def test_similar_radio_track(factories):
factories["music.Track"].create_batch(5)
# one user listened to this track
l1 = factories["history.Listening"](track=seed)
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, user=l1.user)
factories["history.Listening"](track=expected_next, actor=l1.actor)
assert radio.pick(filter_playable=False) == expected_next
......@@ -357,7 +363,7 @@ 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=cf.target_artist)
factories["music.Track"](artist_credit__artist=cf.target_artist)
valid_track = factories["music.Track"]()
radio = radios.RandomRadio()
radio.start_session(user=cf.user)
......@@ -369,7 +375,7 @@ 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=cf.target_artist)
factories["music.Track"](album__artist_credit__artist=cf.target_artist)
valid_track = factories["music.Track"]()
radio = radios.RandomRadio()
radio.start_session(user=cf.user)
......@@ -381,9 +387,11 @@ 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=included_artist
5, track__artist_credit__artist=included_artist
)
factories["music.Upload"].create_batch(
5, track__artist_credit__artist=excluded_artist
)
factories["music.Upload"].create_batch(5, track__artist=excluded_artist)
session = factories["radios.CustomRadioSession"](
custom_radio__config=[
......@@ -391,7 +399,7 @@ def test_get_choices_for_custom_radio_exclude_artist(factories):
{"type": "artist", "ids": [excluded_artist.pk], "not": True},
]
)
choices = session.radio.get_choices(filter_playable=False)
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
......@@ -409,7 +417,25 @@ def test_get_choices_for_custom_radio_exclude_tag(factories):
{"type": "tag", "names": ["rock"], "not": True},
]
)
choices = session.radio.get_choices(filter_playable=False)
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
import json
import logging
import pickle
import random
from django.core.cache import cache
from django.urls import reverse
from funkwhale_api.favorites.models import TrackFavorite
from funkwhale_api.radios import models, radios_v2
def test_can_get_track_for_session_from_api_v2(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:v2:radios:sessions-list")
response = logged_in_api_client.post(url, {"radio_type": "random"})
session = models.RadioSession.objects.latest("id")
url = reverse("api:v2:radios:sessions-tracks", kwargs={"pk": session.pk})
response = logged_in_api_client.get(url, {"session": session.pk})
data = json.loads(response.content.decode("utf-8"))
assert data[0]["id"] == track.pk
next_track = factories["music.Upload"](
library__actor=actor, import_status="finished"
).track
response = logged_in_api_client.get(url, {"session": session.pk})
data = json.loads(response.content.decode("utf-8"))
assert data[0]["id"] == next_track.id
def test_can_use_radio_session_to_filter_choices_v2(factories):
factories["music.Upload"].create_batch(10)
user = factories["users.User"]()
radio = radios_v2.RandomRadio()
session = radio.start_session(user)
radio.pick_many(quantity=10, 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_session_radio_excludes_previous_picks_v2(factories, logged_in_api_client):
tracks = factories["music.Track"].create_batch(5)
url = reverse("api:v2:radios:sessions-list")
response = logged_in_api_client.post(url, {"radio_type": "random"})
session = models.RadioSession.objects.latest("id")
url = reverse("api:v2:radios:sessions-tracks", kwargs={"pk": session.pk})
previous_choices = []
for i in range(5):
response = logged_in_api_client.get(
url, {"session": session.pk, "filter_playable": False}
)
pick = json.loads(response.content.decode("utf-8"))
assert pick[0]["title"] not in previous_choices
assert pick[0]["title"] in [t.title for t in tracks]
previous_choices.append(pick[0]["title"])
response = logged_in_api_client.get(url, {"session": session.pk})
assert (
json.loads(response.content.decode("utf-8"))
== "Radio doesn't have more candidates"
)
def test_can_get_choices_for_favorites_radio_v2(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_v2.FavoritesRadio()
session = radio.start_session(user=user)
choices = session.radio(api_version=2).get_choices(
quantity=100, filter_playable=False
)
assert len(choices) == user.actor.track_favorites.all().count()
for favorite in user.actor.track_favorites.all():
assert favorite.track in choices
def test_can_get_choices_for_custom_radio_v2(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=2).get_choices(
quantity=1, filter_playable=False
)
expected = [t.pk for t in tracks]
for t in choices:
assert t.id in expected
def test_can_cache_radio_track(factories):
uploads = factories["music.Track"].create_batch(10)
user = factories["users.User"]()
radio = radios_v2.RandomRadio()
session = radio.start_session(user)
picked = session.radio(api_version=2).pick_many(quantity=1, filter_playable=False)
assert len(picked) == 1
for t in pickle.loads(cache.get(f"radiotracks{session.id}")):
assert t in uploads
def test_regenerate_cache_if_not_enought_tracks_in_it(
factories, caplog, logged_in_api_client
):
logger = logging.getLogger("funkwhale_api.radios.radios_v2")
caplog.set_level(logging.INFO)
logger.addHandler(caplog.handler)
factories["music.Track"].create_batch(10)
factories["users.User"]()
url = reverse("api:v2:radios:sessions-list")
response = logged_in_api_client.post(url, {"radio_type": "random"})
session = models.RadioSession.objects.latest("id")
url = reverse("api:v2:radios:sessions-tracks", kwargs={"pk": session.pk})
logged_in_api_client.get(url, {"count": 9, "filter_playable": False})
response = logged_in_api_client.get(url, {"count": 10, "filter_playable": False})
pick = json.loads(response.content.decode("utf-8"))
assert (
"Not enough radio tracks in cache. Trying to generate new cache" in caplog.text
)
assert len(pick) == 1
......@@ -40,5 +40,6 @@ def test_tag_radio_repr(factories, to_api_date):
"user": session.user.pk,
"related_object_id": tag.name,
"creation_date": to_api_date(session.creation_date),
"config": None,
}
assert serializers.RadioSessionSerializer(session).data == expected
import json
import pytest
import xml.etree.ElementTree as ET
import funkwhale_api
import pytest
import funkwhale_api
from funkwhale_api.subsonic import renderers
......@@ -17,6 +17,8 @@ from funkwhale_api.subsonic import renderers
"version": "1.16.0",
"type": "funkwhale",
"funkwhaleVersion": funkwhale_api.__version__,
"serverVersion": funkwhale_api.__version__,
"openSubsonic": True,
"hello": "world",
},
),
......@@ -30,6 +32,8 @@ from funkwhale_api.subsonic import renderers
"version": "1.16.0",
"type": "funkwhale",
"funkwhaleVersion": funkwhale_api.__version__,
"serverVersion": funkwhale_api.__version__,
"openSubsonic": True,
"hello": "world",
"error": {"code": 10, "message": "something went wrong"},
},
......@@ -41,6 +45,8 @@ from funkwhale_api.subsonic import renderers
"version": "1.16.0",
"type": "funkwhale",
"funkwhaleVersion": funkwhale_api.__version__,
"serverVersion": funkwhale_api.__version__,
"openSubsonic": True,
"hello": "world",
"error": {"code": 0, "message": "something went wrong"},
},
......@@ -59,6 +65,8 @@ def test_json_renderer():
"version": "1.16.0",
"type": "funkwhale",
"funkwhaleVersion": funkwhale_api.__version__,
"serverVersion": funkwhale_api.__version__,
"openSubsonic": True,
"hello": "world",
}
}
......@@ -70,9 +78,11 @@ def test_xml_renderer_dict_to_xml():
payload = {
"hello": "world",
"item": [{"this": 1, "value": "text"}, {"some": "node"}],
"list": [1, 2],
"some-tag": renderers.TagValue("foo"),
}
expected = """<?xml version="1.0" encoding="UTF-8"?>
<key hello="world"><item this="1">text</item><item some="node" /></key>"""
<key hello="world"><item this="1">text</item><item some="node" /><list>1</list><list>2</list><some-tag>foo</some-tag></key>""" # noqa
result = renderers.dict_to_xml_tree("key", payload)
exp = ET.fromstring(expected)
assert ET.tostring(result) == ET.tostring(exp)
......@@ -80,8 +90,9 @@ def test_xml_renderer_dict_to_xml():
def test_xml_renderer():
payload = {"hello": "world"}
expected = '<?xml version="1.0" encoding="UTF-8"?>\n<subsonic-response funkwhaleVersion="{}" hello="world" status="ok" type="funkwhale" version="1.16.0" xmlns="http://subsonic.org/restapi" />' # noqa
expected = expected.format(funkwhale_api.__version__).encode()
expected = '<?xml version="1.0" encoding="UTF-8"?>\n<subsonic-response funkwhaleVersion="{}" hello="world" openSubsonic="true" serverVersion="{}" status="ok" type="funkwhale" version="1.16.0" xmlns="http://subsonic.org/restapi" />' # noqa
version = funkwhale_api.__version__
expected = expected.format(version, version).encode()
renderer = renderers.SubsonicXMLRenderer()
rendered = renderer.render(payload)
......
import datetime
import pytest
from django.db.models.aggregates import Count
from funkwhale_api.music import models as music_models
from funkwhale_api.subsonic import serializers
from funkwhale_api.subsonic import renderers, serializers
@pytest.mark.parametrize(
......@@ -35,7 +37,7 @@ def test_get_valid_filepart(input, expected):
[
(
{
"artist__name": "Hello",
"artist_credit__artist__name": "Hello",
"album__title": "World",
"title": "foo",
"position": None,
......@@ -45,7 +47,7 @@ def test_get_valid_filepart(input, expected):
),
(
{
"artist__name": "AC/DC",
"artist_credit__artist__name": "AC/DC",
"album__title": "escape/my",
"title": "sla/sh",
"position": 12,
......@@ -67,13 +69,15 @@ def test_get_track_path(factory_kwargs, suffix, expected, factories):
def test_get_artists_serializer(factories):
artist1 = factories["music.Artist"](name="eliot")
artist2 = factories["music.Artist"](name="Ellena")
artist3 = factories["music.Artist"](name="Rilay")
artist4 = factories["music.Artist"](name="") # Shouldn't be serialised
artist1 = factories["music.Artist"](name="eliot", with_cover=True)
artist2 = factories["music.Artist"](name="Ellena", with_cover=True)
artist3 = factories["music.Artist"](name="Rilay", with_cover=True)
artist4 = factories["music.Artist"](
name="", with_cover=False
) # Shouldn't be serialised
factories["music.Album"].create_batch(size=3, artist=artist1)
factories["music.Album"].create_batch(size=2, artist=artist2)
factories["music.Album"].create_batch(size=3, artist_credit__artist=artist1)
factories["music.Album"].create_batch(size=2, artist_credit__artist=artist2)
expected = {
"ignoredArticles": "",
......@@ -81,13 +85,33 @@ def test_get_artists_serializer(factories):
{
"name": "E",
"artist": [
{"id": artist1.pk, "name": artist1.name, "albumCount": 3},
{"id": artist2.pk, "name": artist2.name, "albumCount": 2},
{
"id": artist1.pk,
"name": artist1.name,
"albumCount": 3,
"coverArt": f"ar-{artist1.id}",
"musicBrainzId": artist1.mbid,
},
{
"id": artist2.pk,
"name": artist2.name,
"albumCount": 2,
"coverArt": f"ar-{artist2.id}",
"musicBrainzId": artist2.mbid,
},
],
},
{
"name": "R",
"artist": [{"id": artist3.pk, "name": artist3.name, "albumCount": 0}],
"artist": [
{
"id": artist3.pk,
"name": artist3.name,
"albumCount": 0,
"coverArt": f"ar-{artist3.id}",
"musicBrainzId": artist3.mbid,
}
],
},
],
}
......@@ -100,18 +124,19 @@ def test_get_artists_serializer(factories):
def test_get_artist_serializer(factories):
artist = factories["music.Artist"]()
album = factories["music.Album"](artist=artist, with_cover=True)
artist = factories["music.Artist"](with_cover=True)
album = factories["music.Album"](artist_credit__artist=artist, with_cover=True)
tracks = factories["music.Track"].create_batch(size=3, album=album)
expected = {
"id": artist.pk,
"name": artist.name,
"albumCount": 1,
"coverArt": f"ar-{artist.id}",
"album": [
{
"id": album.pk,
"coverArt": "al-{}".format(album.id),
"coverArt": f"al-{album.id}",
"artistId": artist.pk,
"name": album.title,
"artist": artist.name,
......@@ -125,6 +150,27 @@ def test_get_artist_serializer(factories):
assert serializers.GetArtistSerializer(artist).data == expected
def test_get_artist_info_2_serializer(factories):
content = factories["common.Content"]()
artist = factories["music.Artist"](with_cover=True, description=content)
expected = {
"musicBrainzId": artist.mbid,
"smallImageUrl": renderers.TagValue(
artist.attachment_cover.download_url_small_square_crop
),
"mediumImageUrl": renderers.TagValue(
artist.attachment_cover.download_url_medium_square_crop
),
"largeImageUrl": renderers.TagValue(
artist.attachment_cover.download_url_large_square_crop
),
"biography": renderers.TagValue(artist.description.rendered),
}
assert serializers.GetArtistInfo2Serializer(artist).data == expected
@pytest.mark.parametrize(
"mimetype, extension, expected",
[
......@@ -137,7 +183,7 @@ def test_get_artist_serializer(factories):
def test_get_track_data_content_type(mimetype, extension, expected, factories):
upload = factories["music.Upload"]()
upload.mimetype = mimetype
upload.audio_file = "test.{}".format(extension)
upload.audio_file = f"test.{extension}"
data = serializers.get_track_data(
album=upload.track.album, track=upload.track, upload=upload
......@@ -148,10 +194,10 @@ def test_get_track_data_content_type(mimetype, extension, expected, factories):
def test_get_album_serializer(factories):
artist = factories["music.Artist"]()
album = factories["music.Album"](artist=artist, with_cover=True)
album = factories["music.Album"](artist_credit__artist=artist, with_cover=True)
track = factories["music.Track"](album=album, disc_number=42)
upload = factories["music.Upload"](track=track, bitrate=42000, duration=43, size=44)
tagged_item = factories["tags.TaggedItem"](content_object=album, tag__name="foo")
expected = {
"id": album.pk,
"artistId": artist.pk,
......@@ -160,13 +206,19 @@ def test_get_album_serializer(factories):
"songCount": 1,
"created": serializers.to_subsonic_date(album.creation_date),
"year": album.release_date.year,
"coverArt": "al-{}".format(album.id),
"coverArt": f"al-{album.id}",
"genre": tagged_item.tag.name,
"genres": [{"name": tagged_item.tag.name}],
"mediaType": "album",
"musicBrainzId": album.mbid,
"duration": 43,
"playCount": album.tracks.aggregate(l=Count("listenings"))["l"] or 0,
"song": [
{
"id": track.pk,
"isDir": "false",
"title": track.title,
"coverArt": "al-{}".format(album.id),
"coverArt": f"al-{album.id}",
"album": album.title,
"artist": artist.name,
"track": track.position,
......@@ -175,23 +227,27 @@ def test_get_album_serializer(factories):
"contentType": upload.mimetype,
"suffix": upload.extension or "",
"path": serializers.get_track_path(track, upload.extension),
"bitrate": 42,
"bitRate": 42,
"duration": 43,
"size": 44,
"created": serializers.to_subsonic_date(track.creation_date),
"albumId": album.pk,
"artistId": artist.pk,
"type": "music",
"mediaType": "song",
"musicBrainzId": track.mbid,
}
],
}
assert serializers.GetAlbumSerializer(album).data == expected
qs = album.__class__.objects.with_duration()
assert serializers.GetAlbumSerializer(qs.first()).data == expected
def test_starred_tracks2_serializer(factories):
artist = factories["music.Artist"]()
album = factories["music.Album"](artist=artist)
artist_credit = factories["music.ArtistCredit"]()
album = factories["music.Album"](artist_credit=artist_credit)
track = factories["music.Track"](album=album)
upload = factories["music.Upload"](track=track)
favorite = factories["favorites.TrackFavorite"](track=track)
......@@ -202,10 +258,10 @@ def test_starred_tracks2_serializer(factories):
def test_get_album_list2_serializer(factories):
album1 = factories["music.Album"]()
album2 = factories["music.Album"]()
album1 = factories["music.Album"]().__class__.objects.with_duration().first()
album2 = factories["music.Album"]().__class__.objects.with_duration().last()
qs = music_models.Album.objects.with_tracks_count().order_by("pk")
qs = music_models.Album.objects.with_tracks_count().with_duration().order_by("pk")
expected = [
serializers.get_album2_data(album1),
serializers.get_album2_data(album2),
......@@ -217,11 +273,12 @@ def test_get_album_list2_serializer(factories):
def test_playlist_serializer(factories):
plt = factories["playlists.PlaylistTrack"]()
playlist = plt.playlist
factories["users.User"](actor=playlist.actor)
qs = music_models.Album.objects.with_tracks_count().order_by("pk")
expected = {
"id": playlist.pk,
"name": playlist.name,
"owner": playlist.user.username,
"owner": playlist.actor.user.username,
"public": "false",
"songCount": 1,
"duration": 0,
......@@ -236,11 +293,12 @@ def test_playlist_detail_serializer(factories):
plt = factories["playlists.PlaylistTrack"]()
upload = factories["music.Upload"](track=plt.track)
playlist = plt.playlist
factories["users.User"](actor=playlist.actor)
qs = music_models.Album.objects.with_tracks_count().order_by("pk")
expected = {
"id": playlist.pk,
"name": playlist.name,
"owner": playlist.user.username,
"owner": playlist.actor.user.username,
"public": "false",
"songCount": 1,
"duration": 0,
......@@ -255,7 +313,7 @@ def test_playlist_detail_serializer(factories):
def test_scrobble_serializer(factories):
upload = factories["music.Upload"]()
track = upload.track
user = factories["users.User"]()
user = factories["users.User"](with_actor=True)
payload = {"id": track.pk, "submission": True}
serializer = serializers.ScrobbleSerializer(data=payload, context={"user": user})
......@@ -263,7 +321,7 @@ def test_scrobble_serializer(factories):
listening = serializer.save()
assert listening.user == user
assert listening.actor.user == user
assert listening.track == track
......@@ -281,7 +339,7 @@ def test_channel_serializer(factories):
"url": channel.rss_url,
"title": channel.artist.name,
"description": description.as_plain_text,
"coverArt": "at-{}".format(channel.artist.attachment_cover.uuid),
"coverArt": f"at-{channel.artist.attachment_cover.uuid}",
"originalImageUrl": channel.artist.attachment_cover.url,
"status": "completed",
"episode": [serializers.get_channel_episode_data(upload, channel.uuid)],
......@@ -294,7 +352,7 @@ def test_channel_episode_serializer(factories):
description = factories["common.Content"]()
channel = factories["audio.Channel"]()
track = factories["music.Track"](
description=description, artist=channel.artist, with_cover=True
description=description, artist_credit__artist=channel.artist, with_cover=True
)
upload = factories["music.Upload"](
playable=True, track=track, bitrate=128000, duration=42
......@@ -306,7 +364,7 @@ def test_channel_episode_serializer(factories):
"streamId": upload.track.id,
"title": track.title,
"description": description.as_plain_text,
"coverArt": "at-{}".format(track.attachment_cover.uuid),
"coverArt": f"at-{track.attachment_cover.uuid}",
"isDir": "false",
"year": track.creation_date.year,
"created": track.creation_date.isoformat(),
......@@ -314,7 +372,7 @@ def test_channel_episode_serializer(factories):
"genre": "Podcast",
"size": upload.size,
"duration": upload.duration,
"bitrate": upload.bitrate / 1000,
"bitRate": upload.bitrate / 1000,
"contentType": upload.mimetype,
"suffix": upload.extension,
"status": "completed",
......
......@@ -10,6 +10,7 @@ import funkwhale_api
from funkwhale_api.moderation import filters as moderation_filters
from funkwhale_api.music import models as music_models
from funkwhale_api.music import views as music_views
from funkwhale_api.playlists import models
from funkwhale_api.subsonic import renderers, serializers
......@@ -18,7 +19,7 @@ def render_json(data):
def test_render_content_json(db, api_client):
url = reverse("api:subsonic-ping")
url = reverse("api:subsonic:subsonic-ping")
response = api_client.get(url, {"f": "json"})
expected = {
......@@ -33,7 +34,7 @@ def test_render_content_json(db, api_client):
@pytest.mark.parametrize("f", ["xml", "json"])
def test_exception_wrong_credentials(f, db, api_client):
url = reverse("api:subsonic-ping")
url = reverse("api:subsonic:subsonic-ping")
response = api_client.get(url, {"f": f, "u": "yolo"})
expected = {
......@@ -46,7 +47,7 @@ def test_exception_wrong_credentials(f, db, api_client):
@pytest.mark.parametrize("f", ["json"])
def test_exception_missing_credentials(f, db, api_client):
url = reverse("api:subsonic-get_artists")
url = reverse("api:subsonic:subsonic-get_artists")
response = api_client.get(url)
expected = {
......@@ -60,14 +61,14 @@ def test_exception_missing_credentials(f, db, api_client):
def test_disabled_subsonic(preferences, api_client):
preferences["subsonic__enabled"] = False
url = reverse("api:subsonic-ping")
url = reverse("api:subsonic:subsonic-ping")
response = api_client.get(url)
assert response.status_code == 405
@pytest.mark.parametrize("f", ["xml", "json"])
def test_get_license(f, db, logged_in_api_client, mocker):
url = reverse("api:subsonic-get_license")
url = reverse("api:subsonic:subsonic-get_license")
assert url.endswith("getLicense") is True
now = timezone.now()
mocker.patch("django.utils.timezone.now", return_value=now)
......@@ -89,7 +90,7 @@ def test_get_license(f, db, logged_in_api_client, mocker):
@pytest.mark.parametrize("f", ["xml", "json"])
def test_ping(f, db, api_client):
url = reverse("api:subsonic-ping")
url = reverse("api:subsonic:subsonic-ping")
response = api_client.get(url, {"f": f})
expected = {"status": "ok", "version": "1.16.0"}
......@@ -97,6 +98,23 @@ def test_ping(f, db, api_client):
assert response.data == expected
@pytest.mark.parametrize("f", ["xml", "json"])
def test_get_open_subsonic_extensions(f, db, api_client):
url = reverse("api:subsonic:subsonic-get_open_subsonic_extensions")
response = api_client.get(url, {"f": f})
expected = {
"openSubsonicExtensions": [
{
"name": "formPost",
"versions": [1],
}
],
}
assert response.status_code == 200
assert response.data == expected
@pytest.mark.parametrize("f", ["json"])
def test_get_artists(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
......@@ -105,7 +123,7 @@ def test_get_artists(
user=logged_in_api_client.user,
target_artist=factories["music.Artist"](playable=True),
)
url = reverse("api:subsonic-get_artists")
url = reverse("api:subsonic:subsonic-get_artists")
assert url.endswith("getArtists") is True
factories["music.Artist"].create_batch(size=3, playable=True)
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
......@@ -132,10 +150,12 @@ def test_get_artists(
def test_get_artist(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get_artist")
url = reverse("api:subsonic:subsonic-get_artist")
assert url.endswith("getArtist") is True
artist = factories["music.Artist"](playable=True)
factories["music.Album"].create_batch(size=3, artist=artist, playable=True)
factories["music.Album"].create_batch(
size=3, artist_credit__artist=artist, playable=True
)
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
expected = {"artist": serializers.GetArtistSerializer(artist).data}
......@@ -148,7 +168,7 @@ def test_get_artist(
@pytest.mark.parametrize("f", ["json"])
def test_get_invalid_artist(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_artist")
url = reverse("api:subsonic:subsonic-get_artist")
assert url.endswith("getArtist") is True
expected = {"error": {"code": 0, "message": 'For input string "asdf"'}}
response = logged_in_api_client.get(url, {"id": "asdf"})
......@@ -161,12 +181,16 @@ def test_get_invalid_artist(f, db, logged_in_api_client, factories):
def test_get_artist_info2(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get_artist_info2")
url = reverse("api:subsonic:subsonic-get_artist_info2")
assert url.endswith("getArtistInfo2") is True
artist = factories["music.Artist"](playable=True)
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
expected = {"artist-info2": {}}
expected = {
"artistInfo2": {
"musicBrainzId": artist.mbid,
}
}
response = logged_in_api_client.get(url, {"id": artist.pk})
assert response.status_code == 200
......@@ -179,10 +203,14 @@ def test_get_artist_info2(
def test_get_album(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get_album")
url = reverse("api:subsonic:subsonic-get_album")
assert url.endswith("getAlbum") is True
artist = factories["music.Artist"]()
album = factories["music.Album"](artist=artist)
artist_credit = factories["music.ArtistCredit"]()
album = (
factories["music.Album"](artist_credit=artist_credit)
.__class__.objects.with_duration()
.first()
)
factories["music.Track"].create_batch(size=3, album=album, playable=True)
playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
expected = {"album": serializers.GetAlbumSerializer(album).data}
......@@ -192,7 +220,66 @@ def test_get_album(
assert response.data == expected
playable_by.assert_called_once_with(
music_models.Album.objects.select_related("artist"), None
music_models.Album.objects.with_duration().prefetch_related(
"artist_credit__artist"
),
None,
)
@pytest.mark.parametrize("f", ["json"])
def test_get_album_info_2(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic:subsonic-get_album_info_2")
assert url.endswith("getAlbumInfo2") is True
artist_credit = factories["music.ArtistCredit"]()
album = (
factories["music.Album"](artist_credit=artist_credit)
.__class__.objects.with_duration()
.first()
)
factories["music.Track"].create_batch(size=3, album=album, playable=True)
playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
expected = {"albumInfo": serializers.GetAlbumSerializer(album).data}
response = logged_in_api_client.get(url, {"f": f, "id": album.pk})
assert response.status_code == 200
assert response.data == expected
playable_by.assert_called_once_with(
music_models.Album.objects.with_duration().prefetch_related(
"artist_credit__artist"
),
None,
)
@pytest.mark.parametrize("f", ["json"])
def test_get_album_info(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic:subsonic-get_album_info")
assert url.endswith("getAlbumInfo") is True
artist_credit = factories["music.ArtistCredit"]()
album = (
factories["music.Album"](artist_credit=artist_credit)
.__class__.objects.with_duration()
.first()
)
factories["music.Track"].create_batch(size=3, album=album, playable=True)
playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
expected = {"albumInfo": serializers.GetAlbumSerializer(album).data}
response = logged_in_api_client.get(url, {"f": f, "id": album.pk})
assert response.status_code == 200
assert response.data == expected
playable_by.assert_called_once_with(
music_models.Album.objects.with_duration().prefetch_related(
"artist_credit__artist"
),
None,
)
......@@ -200,10 +287,10 @@ def test_get_album(
def test_get_song(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get_song")
url = reverse("api:subsonic:subsonic-get_song")
assert url.endswith("getSong") is True
artist = factories["music.Artist"]()
album = factories["music.Album"](artist=artist)
artist_credit = factories["music.ArtistCredit"]()
album = factories["music.Album"](artist_credit=artist_credit)
track = factories["music.Track"](album=album, playable=True)
upload = factories["music.Upload"](track=track)
playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by")
......@@ -216,6 +303,32 @@ def test_get_song(
playable_by.assert_called_once_with(music_models.Track.objects.all(), None)
@pytest.mark.parametrize("f", ["json"])
def test_get_top_songs(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic:subsonic-get_top_songs")
assert url.endswith("getTopSongs") is True
artist_credit = factories["music.ArtistCredit"]()
album = factories["music.Album"](artist_credit=artist_credit)
track = factories["music.Track"](album=album, playable=True)
tracks = factories["music.Track"].create_batch(20, album=album, playable=True)
factories["music.Upload"](track=track)
factories["history.Listening"].create_batch(20, track=track)
factories["history.Listening"].create_batch(2, track=tracks[2])
playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by")
response = logged_in_api_client.get(
url, {"f": f, "artist": artist_credit.artist.name, "count": 2}
)
assert response.status_code == 200
assert response.data["topSongs"][0] == serializers.get_track_data(
track.album, track, track.uploads.all()[0]
)
playable_by.assert_called_once_with(music_models.Track.objects.all(), None)
@pytest.mark.parametrize("f", ["json"])
def test_stream(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries, settings
......@@ -223,7 +336,7 @@ def test_stream(
# Even with this settings set to false, we proxy media in the subsonic API
# Because clients don't expect a 302 redirect
settings.PROXY_MEDIA = False
url = reverse("api:subsonic-stream")
url = reverse("api:subsonic:subsonic-stream")
mocked_serve = mocker.spy(music_views, "handle_serve")
assert url.endswith("stream") is True
upload = factories["music.Upload"](playable=True)
......@@ -244,7 +357,7 @@ def test_stream(
@pytest.mark.parametrize("format,expected", [("mp3", "mp3"), ("raw", None)])
def test_stream_format(format, expected, logged_in_api_client, factories, mocker):
url = reverse("api:subsonic-stream")
url = reverse("api:subsonic:subsonic-stream")
mocked_serve = mocker.patch.object(
music_views, "handle_serve", return_value=Response()
)
......@@ -295,7 +408,7 @@ def test_stream_transcode(
if format:
params["format"] = format
settings.SUBSONIC_DEFAULT_TRANSCODING_FORMAT = default_transcoding_format
url = reverse("api:subsonic-stream")
url = reverse("api:subsonic:subsonic-stream")
mocked_serve = mocker.patch.object(
music_views, "handle_serve", return_value=Response()
)
......@@ -314,7 +427,8 @@ def test_stream_transcode(
@pytest.mark.parametrize("f", ["json"])
def test_star(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-star")
logged_in_api_client.user.create_actor()
url = reverse("api:subsonic:subsonic-star")
assert url.endswith("star") is True
track = factories["music.Track"]()
response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
......@@ -322,30 +436,34 @@ def test_star(f, db, logged_in_api_client, factories):
assert response.status_code == 200
assert response.data == {"status": "ok"}
favorite = logged_in_api_client.user.track_favorites.latest("id")
favorite = logged_in_api_client.user.actor.track_favorites.latest("id")
assert favorite.track == track
@pytest.mark.parametrize("f", ["json"])
def test_unstar(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-unstar")
logged_in_api_client.user.create_actor()
url = reverse("api:subsonic:subsonic-unstar")
assert url.endswith("unstar") is True
track = factories["music.Track"]()
factories["favorites.TrackFavorite"](track=track, user=logged_in_api_client.user)
factories["favorites.TrackFavorite"](
track=track, actor=logged_in_api_client.user.actor
)
response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
assert response.status_code == 200
assert response.data == {"status": "ok"}
assert logged_in_api_client.user.track_favorites.count() == 0
assert logged_in_api_client.user.actor.track_favorites.count() == 0
@pytest.mark.parametrize("f", ["json"])
def test_get_starred2(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_starred2")
logged_in_api_client.user.create_actor()
url = reverse("api:subsonic:subsonic-get_starred2")
assert url.endswith("getStarred2") is True
track = factories["music.Track"]()
favorite = factories["favorites.TrackFavorite"](
track=track, user=logged_in_api_client.user
track=track, actor=logged_in_api_client.user.actor
)
response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
......@@ -357,7 +475,7 @@ def test_get_starred2(f, db, logged_in_api_client, factories):
@pytest.mark.parametrize("f", ["json"])
def test_get_random_songs(f, db, logged_in_api_client, factories, mocker):
url = reverse("api:subsonic-get_random_songs")
url = reverse("api:subsonic:subsonic-get_random_songs")
assert url.endswith("getRandomSongs") is True
track1 = factories["music.Track"]()
track2 = factories["music.Track"]()
......@@ -380,7 +498,7 @@ def test_get_random_songs(f, db, logged_in_api_client, factories, mocker):
@pytest.mark.parametrize("f", ["json"])
def test_get_genres(f, db, logged_in_api_client, factories, mocker):
url = reverse("api:subsonic-get_genres")
url = reverse("api:subsonic:subsonic-get_genres")
assert url.endswith("getGenres") is True
tag1 = factories["tags.Tag"](name="Pop")
tag2 = factories["tags.Tag"](name="Rock")
......@@ -402,11 +520,12 @@ def test_get_genres(f, db, logged_in_api_client, factories, mocker):
@pytest.mark.parametrize("f", ["json"])
def test_get_starred(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_starred")
logged_in_api_client.user.create_actor()
url = reverse("api:subsonic:subsonic-get_starred")
assert url.endswith("getStarred") is True
track = factories["music.Track"]()
favorite = factories["favorites.TrackFavorite"](
track=track, user=logged_in_api_client.user
track=track, actor=logged_in_api_client.user.actor
)
response = logged_in_api_client.get(url, {"f": f, "id": track.pk})
......@@ -420,10 +539,14 @@ def test_get_starred(f, db, logged_in_api_client, factories):
def test_get_album_list2(
f, db, logged_in_api_client, factories, mocker, queryset_equal_queries
):
url = reverse("api:subsonic-get_album_list2")
url = reverse("api:subsonic:subsonic-get_album_list2")
assert url.endswith("getAlbumList2") is True
album1 = factories["music.Album"](playable=True)
album2 = factories["music.Album"](playable=True)
album1 = factories["music.Album"](playable=True).__class__.objects.with_duration()[
0
]
album2 = factories["music.Album"](playable=True).__class__.objects.with_duration()[
1
]
factories["music.Album"]()
playable_by = mocker.spy(music_models.AlbumQuerySet, "playable_by")
response = logged_in_api_client.get(url, {"f": f, "type": "newest"})
......@@ -435,11 +558,34 @@ def test_get_album_list2(
playable_by.assert_called_once()
def test_get_album_list2_recent(db, logged_in_api_client, factories):
url = reverse("api:subsonic:subsonic-get_album_list2")
assert url.endswith("getAlbumList2") is True
factories["music.Album"](playable=True, release_date=None)
album2 = factories["music.Album"](playable=True).__class__.objects.with_duration()[
1
]
album3 = factories["music.Album"](playable=True).__class__.objects.with_duration()[
2
]
response = logged_in_api_client.get(url, {"f": "json", "type": "recent"})
assert response.status_code == 200
expected_albums = reversed(sorted([album3, album2], key=lambda a: a.release_date))
assert response.data == {
"albumList2": {"album": serializers.get_album_list2_data(expected_albums)}
}
@pytest.mark.parametrize("f", ["json"])
def test_get_album_list2_pagination(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_album_list2")
url = reverse("api:subsonic:subsonic-get_album_list2")
assert url.endswith("getAlbumList2") is True
album1 = factories["music.Album"](playable=True)
album1 = (
factories["music.Album"](playable=True)
.__class__.objects.with_duration()
.first()
)
factories["music.Album"](playable=True)
response = logged_in_api_client.get(
url, {"f": f, "type": "newest", "size": 1, "offset": 1}
......@@ -453,14 +599,16 @@ def test_get_album_list2_pagination(f, db, logged_in_api_client, factories):
@pytest.mark.parametrize("f", ["json"])
def test_get_album_list2_by_genre(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_album_list2")
url = reverse("api:subsonic:subsonic-get_album_list2")
assert url.endswith("getAlbumList2") is True
album1 = factories["music.Album"](
artist__name="Artist1", playable=True, set_tags=["Rock"]
)
artist_credit__artist__name="Artist1", playable=True, set_tags=["Rock"]
).__class__.objects.with_duration()[0]
album2 = factories["music.Album"](
artist__name="Artist2", playable=True, artist__set_tags=["Rock"]
)
artist_credit__artist__name="Artist2",
playable=True,
artist_credit__artist__set_tags=["Rock"],
).__class__.objects.with_duration()[1]
factories["music.Album"](playable=True, set_tags=["Pop"])
response = logged_in_api_client.get(
url, {"f": f, "type": "byGenre", "size": 5, "offset": 0, "genre": "rock"}
......@@ -485,10 +633,10 @@ def test_get_album_list2_by_year(params, expected, db, logged_in_api_client, fac
albums = [
factories["music.Album"](
playable=True, release_date=datetime.date(1900 + i, 1, 1)
)
).__class__.objects.with_duration()[i]
for i in range(5)
]
url = reverse("api:subsonic-get_album_list2")
url = reverse("api:subsonic:subsonic-get_album_list2")
base_params = {"f": "json"}
base_params.update(params)
response = logged_in_api_client.get(url, base_params)
......@@ -504,10 +652,15 @@ def test_get_album_list2_by_year(params, expected, db, logged_in_api_client, fac
@pytest.mark.parametrize("f", ["json"])
@pytest.mark.parametrize(
"tags_field",
["set_tags", "artist__set_tags", "album__set_tags", "album__artist__set_tags"],
[
"set_tags",
"artist_credit__artist__set_tags",
"album__set_tags",
"album__artist_credit__artist__set_tags",
],
)
def test_get_songs_by_genre(f, tags_field, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_songs_by_genre")
url = reverse("api:subsonic:subsonic-get_songs_by_genre")
assert url.endswith("getSongsByGenre") is True
track1 = factories["music.Track"](playable=True, **{tags_field: ["Rock"]})
track2 = factories["music.Track"](playable=True, **{tags_field: ["Rock"]})
......@@ -524,7 +677,7 @@ def test_get_songs_by_genre(f, tags_field, db, logged_in_api_client, factories):
def test_get_songs_by_genre_offset(logged_in_api_client, factories):
url = reverse("api:subsonic-get_songs_by_genre")
url = reverse("api:subsonic:subsonic-get_songs_by_genre")
assert url.endswith("getSongsByGenre") is True
track1 = factories["music.Track"](playable=True, set_tags=["Rock"])
factories["music.Track"](playable=True, set_tags=["Rock"])
......@@ -542,12 +695,14 @@ def test_get_songs_by_genre_offset(logged_in_api_client, factories):
@pytest.mark.parametrize("f", ["json"])
def test_search3(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-search3")
url = reverse("api:subsonic:subsonic-search3")
assert url.endswith("search3") is True
artist = factories["music.Artist"](name="testvalue", playable=True)
factories["music.Artist"](name="nope")
factories["music.Artist"](name="nope2", playable=True)
album = factories["music.Album"](title="testvalue", playable=True)
album = factories["music.Album"](
title="testvalue", playable=True
).__class__.objects.with_duration()[2]
factories["music.Album"](title="nope")
factories["music.Album"](title="nope2", playable=True)
track = factories["music.Track"](title="testvalue", playable=True)
......@@ -559,7 +714,7 @@ def test_search3(f, db, logged_in_api_client, factories):
artist_qs = (
music_models.Artist.objects.with_albums_count()
.filter(pk=artist.pk)
.values("_albums_count", "id", "name")
.values("_albums_count", "id", "name", "mbid")
)
assert response.status_code == 200
assert response.data == {
......@@ -573,10 +728,11 @@ def test_search3(f, db, logged_in_api_client, factories):
@pytest.mark.parametrize("f", ["json"])
def test_get_playlists(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_playlists")
url = reverse("api:subsonic:subsonic-get_playlists")
assert url.endswith("getPlaylists") is True
playlist1 = factories["playlists.PlaylistTrack"](
playlist__user=logged_in_api_client.user
playlist__actor=logged_in_api_client.user.create_actor()
).playlist
playlist2 = factories["playlists.PlaylistTrack"](
playlist__privacy_level="everyone"
......@@ -585,9 +741,15 @@ def test_get_playlists(f, db, logged_in_api_client, factories):
playlist__privacy_level="instance"
).playlist
# private
factories["playlists.PlaylistTrack"](playlist__privacy_level="me")
plt = factories["playlists.PlaylistTrack"](playlist__privacy_level="me")
# no track
factories["playlists.Playlist"](privacy_level="everyone")
playlist4 = factories["playlists.Playlist"](privacy_level="everyone")
factories["users.User"](actor=playlist2.actor)
factories["users.User"](actor=playlist3.actor)
factories["users.User"](actor=playlist4.actor)
factories["users.User"](actor=plt.playlist.actor)
response = logged_in_api_client.get(url, {"f": f})
qs = (
......@@ -604,11 +766,13 @@ def test_get_playlists(f, db, logged_in_api_client, factories):
@pytest.mark.parametrize("f", ["json"])
def test_get_playlist(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_playlist")
logged_in_api_client.user.create_actor()
url = reverse("api:subsonic:subsonic-get_playlist")
assert url.endswith("getPlaylist") is True
playlist = factories["playlists.PlaylistTrack"](
playlist__user=logged_in_api_client.user
playlist__actor__user=logged_in_api_client.user
).playlist
response = logged_in_api_client.get(url, {"f": f, "id": playlist.pk})
qs = playlist.__class__.objects.with_tracks_count()
......@@ -620,9 +784,10 @@ def test_get_playlist(f, db, logged_in_api_client, factories):
@pytest.mark.parametrize("f", ["json"])
def test_update_playlist(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-update_playlist")
url = reverse("api:subsonic:subsonic-update_playlist")
assert url.endswith("updatePlaylist") is True
playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
actor = logged_in_api_client.user.create_actor()
playlist = factories["playlists.Playlist"](actor=actor)
factories["playlists.PlaylistTrack"](index=0, playlist=playlist)
new_track = factories["music.Track"]()
response = logged_in_api_client.get(
......@@ -644,9 +809,10 @@ def test_update_playlist(f, db, logged_in_api_client, factories):
@pytest.mark.parametrize("f", ["json"])
def test_delete_playlist(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-delete_playlist")
url = reverse("api:subsonic:subsonic-delete_playlist")
assert url.endswith("deletePlaylist") is True
playlist = factories["playlists.Playlist"](user=logged_in_api_client.user)
actor = logged_in_api_client.user.create_actor()
playlist = factories["playlists.Playlist"](actor=actor)
response = logged_in_api_client.get(url, {"f": f, "id": playlist.pk})
assert response.status_code == 200
with pytest.raises(playlist.__class__.DoesNotExist):
......@@ -655,15 +821,16 @@ def test_delete_playlist(f, db, logged_in_api_client, factories):
@pytest.mark.parametrize("f", ["json"])
def test_create_playlist(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-create_playlist")
url = reverse("api:subsonic:subsonic-create_playlist")
assert url.endswith("createPlaylist") is True
track1 = factories["music.Track"]()
track2 = factories["music.Track"]()
actor = logged_in_api_client.user.create_actor()
response = logged_in_api_client.get(
url, {"f": f, "name": "hello", "songId": [track1.pk, track2.pk]}
)
assert response.status_code == 200
playlist = logged_in_api_client.user.playlists.latest("id")
playlist = models.Playlist.objects.filter(actor=actor).latest("id")
assert playlist.playlist_tracks.count() == 2
for i, t in enumerate([track1, track2]):
plt = playlist.playlist_tracks.get(track=t)
......@@ -675,9 +842,30 @@ def test_create_playlist(f, db, logged_in_api_client, factories):
}
@pytest.mark.parametrize("f", ["json"])
def test_create_playlist_with_update(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic:subsonic-create_playlist")
assert url.endswith("createPlaylist") is True
actor = logged_in_api_client.user.create_actor()
playlist = factories["playlists.Playlist"](actor=actor)
factories["playlists.PlaylistTrack"](index=0, playlist=playlist)
track1 = factories["music.Track"]()
track2 = factories["music.Track"]()
response = logged_in_api_client.get(
url, {"f": f, "playlistId": playlist.pk, "songId": [track1.pk, track2.pk]}
)
playlist.refresh_from_db()
assert response.status_code == 200
assert playlist.playlist_tracks.count() == 3
qs = playlist.__class__.objects.with_tracks_count()
assert response.data == {
"playlist": serializers.get_playlist_detail_data(qs.first())
}
@pytest.mark.parametrize("f", ["json"])
def test_get_music_folders(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_music_folders")
url = reverse("api:subsonic:subsonic-get_music_folders")
assert url.endswith("getMusicFolders") is True
response = logged_in_api_client.get(url, {"f": f})
assert response.status_code == 200
......@@ -698,7 +886,7 @@ def test_get_indexes(
moderation_filters.USER_FILTER_CONFIG["ARTIST"], logged_in_api_client.user
)
url = reverse("api:subsonic-get_indexes")
url = reverse("api:subsonic:subsonic-get_indexes")
assert url.endswith("getIndexes") is True
factories["music.Artist"].create_batch(size=3, playable=True)
expected = {
......@@ -719,10 +907,10 @@ def test_get_indexes(
def test_get_cover_art_album(factories, logged_in_api_client):
url = reverse("api:subsonic-get_cover_art")
url = reverse("api:subsonic:subsonic-get_cover_art")
assert url.endswith("getCoverArt") is True
album = factories["music.Album"](with_cover=True)
response = logged_in_api_client.get(url, {"id": "al-{}".format(album.pk)})
response = logged_in_api_client.get(url, {"id": f"al-{album.pk}"})
assert response.status_code == 200
assert response["Content-Type"] == ""
......@@ -733,9 +921,9 @@ def test_get_cover_art_album(factories, logged_in_api_client):
def test_get_cover_art_attachment(factories, logged_in_api_client):
attachment = factories["common.Attachment"]()
url = reverse("api:subsonic-get_cover_art")
url = reverse("api:subsonic:subsonic-get_cover_art")
assert url.endswith("getCoverArt") is True
response = logged_in_api_client.get(url, {"id": "at-{}".format(attachment.uuid)})
response = logged_in_api_client.get(url, {"id": f"at-{attachment.uuid}"})
assert response.status_code == 200
assert response["Content-Type"] == ""
......@@ -746,7 +934,7 @@ def test_get_cover_art_attachment(factories, logged_in_api_client):
def test_get_avatar(factories, logged_in_api_client):
user = factories["users.User"]()
url = reverse("api:subsonic-get_avatar")
url = reverse("api:subsonic:subsonic-get_avatar")
assert url.endswith("getAvatar") is True
response = logged_in_api_client.get(url, {"username": user.username})
......@@ -758,21 +946,22 @@ def test_get_avatar(factories, logged_in_api_client):
def test_scrobble(factories, logged_in_api_client):
logged_in_api_client.user.create_actor()
upload = factories["music.Upload"]()
track = upload.track
url = reverse("api:subsonic-scrobble")
url = reverse("api:subsonic:subsonic-scrobble")
assert url.endswith("scrobble") is True
response = logged_in_api_client.get(url, {"id": track.pk, "submission": True})
assert response.status_code == 200
listening = logged_in_api_client.user.listenings.latest("id")
listening = logged_in_api_client.user.actor.listenings.latest("id")
assert listening.track == track
@pytest.mark.parametrize("f", ["json"])
def test_get_user(f, db, logged_in_api_client, factories):
url = reverse("api:subsonic-get_user")
url = reverse("api:subsonic:subsonic-get_user")
assert url.endswith("getUser") is True
response = logged_in_api_client.get(
url, {"f": f, "username": logged_in_api_client.user.username}
......@@ -795,8 +984,7 @@ def test_get_user(f, db, logged_in_api_client, factories):
"coverArtRole": "false",
"shareRole": "false",
"folder": [
{"value": f["id"]}
for f in serializers.get_folders(logged_in_api_client.user)
f["id"] for f in serializers.get_folders(logged_in_api_client.user)
],
}
}
......@@ -810,7 +998,7 @@ def test_create_podcast_channel(logged_in_api_client, factories, mocker):
return_value=(channel, []),
)
actor = logged_in_api_client.user.create_actor()
url = reverse("api:subsonic-create_podcast_channel")
url = reverse("api:subsonic:subsonic-create_podcast_channel")
assert url.endswith("createPodcastChannel") is True
response = logged_in_api_client.get(url, {"f": "json", "url": rss_url})
assert response.status_code == 200
......@@ -826,7 +1014,7 @@ def test_delete_podcast_channel(logged_in_api_client, factories, mocker):
channel = factories["audio.Channel"](external=True)
subscription = factories["federation.Follow"](actor=actor, target=channel.actor)
other_subscription = factories["federation.Follow"](target=channel.actor)
url = reverse("api:subsonic-delete_podcast_channel")
url = reverse("api:subsonic:subsonic-delete_podcast_channel")
assert url.endswith("deletePodcastChannel") is True
response = logged_in_api_client.get(url, {"f": "json", "id": channel.uuid})
assert response.status_code == 200
......@@ -843,23 +1031,25 @@ def test_get_podcasts(logged_in_api_client, factories, mocker):
)
upload1 = factories["music.Upload"](
playable=True,
track__artist=channel.artist,
track__artist_credit__artist=channel.artist,
library=channel.library,
bitrate=128000,
duration=42,
)
upload2 = factories["music.Upload"](
playable=True,
track__artist=channel.artist,
track__artist_credit__artist=channel.artist,
library=channel.library,
bitrate=256000,
duration=43,
)
factories["federation.Follow"](actor=actor, target=channel.actor, approved=True)
factories["music.Upload"](import_status="pending", track__artist=channel.artist)
factories["music.Upload"](
import_status="pending", track__artist_credit__artist=channel.artist
)
factories["audio.Channel"](external=True)
factories["federation.Follow"]()
url = reverse("api:subsonic-get_podcasts")
url = reverse("api:subsonic:subsonic-get_podcasts")
assert url.endswith("getPodcasts") is True
response = logged_in_api_client.get(url, {"f": "json"})
assert response.status_code == 200
......@@ -880,21 +1070,21 @@ def test_get_podcasts_by_id(logged_in_api_client, factories, mocker):
)
upload1 = factories["music.Upload"](
playable=True,
track__artist=channel1.artist,
track__artist_credit__artist=channel1.artist,
library=channel1.library,
bitrate=128000,
duration=42,
)
factories["music.Upload"](
playable=True,
track__artist=channel2.artist,
track__artist_credit__artist=channel2.artist,
library=channel2.library,
bitrate=256000,
duration=43,
)
factories["federation.Follow"](actor=actor, target=channel1.actor, approved=True)
factories["federation.Follow"](actor=actor, target=channel2.actor, approved=True)
url = reverse("api:subsonic-get_podcasts")
url = reverse("api:subsonic:subsonic-get_podcasts")
assert url.endswith("getPodcasts") is True
response = logged_in_api_client.get(url, {"f": "json", "id": channel1.uuid})
assert response.status_code == 200
......@@ -910,20 +1100,20 @@ def test_get_newest_podcasts(logged_in_api_client, factories, mocker):
)
upload1 = factories["music.Upload"](
playable=True,
track__artist=channel.artist,
track__artist_credit__artist=channel.artist,
library=channel.library,
bitrate=128000,
duration=42,
)
upload2 = factories["music.Upload"](
playable=True,
track__artist=channel.artist,
track__artist_credit__artist=channel.artist,
library=channel.library,
bitrate=256000,
duration=43,
)
factories["federation.Follow"](actor=actor, target=channel.actor, approved=True)
url = reverse("api:subsonic-get_newest_podcasts")
url = reverse("api:subsonic:subsonic-get_newest_podcasts")
assert url.endswith("getNewestPodcasts") is True
response = logged_in_api_client.get(url, {"f": "json"})
assert response.status_code == 200
......
from funkwhale_api.tags import filters
from funkwhale_api.tags import models
from django.db.models.functions import Collate
from funkwhale_api.tags import filters, models
def test_filter_search_tag(factories, queryset_equal_list):
......@@ -10,7 +11,11 @@ def test_filter_search_tag(factories, queryset_equal_list):
]
factories["tags.Tag"](name="TestTag")
factories["tags.Tag"](name="TestTag2")
qs = models.Tag.objects.all().order_by("name")
qs = (
models.Tag.objects.all()
.annotate(tag_deterministic=Collate("name", "und-x-icu"))
.order_by("name")
)
filterset = filters.TagFilter({"q": "tag1"}, queryset=qs)
assert filterset.qs == matches
from funkwhale_api.music import models as music_models
from funkwhale_api.tags import tasks
from funkwhale_api.tags import models, tasks
def test_get_tags_from_foreign_key(factories):
rock_tag = factories["tags.Tag"](name="Rock")
rap_tag = factories["tags.Tag"](name="Rap")
rock_tag = factories["tags.Tag"](name="rock")
rap_tag = factories["tags.Tag"](name="rap")
artist = factories["music.Artist"]()
factories["music.Track"].create_batch(3, artist=artist, set_tags=["rock", "rap"])
factories["music.Track"].create_batch(
3, artist=artist, set_tags=["rock", "rap", "techno"]
3, artist_credit__artist=artist, set_tags=["rock", "rap"]
)
factories["music.Track"].create_batch(
3, artist_credit__artist=artist, set_tags=["rock", "rap", "techno"]
)
result = tasks.get_tags_from_foreign_key(
......@@ -29,7 +31,58 @@ def test_add_tags_batch(factories):
data = {artist.pk: [rock_tag.pk, rap_tag.pk]}
tasks.add_tags_batch(
data, model=artist.__class__,
data,
model=artist.__class__,
)
assert artist.get_tags() == ["Rap", "Rock"]
def test_update_musicbrainz_genre(factories, mocker):
tag1 = factories["tags.Tag"](mbid="2628c282-9075-4736-b1f9-7012404d09e8")
tag2 = factories["tags.Tag"](mbid=None)
factories["tags.Tag"]()
factories["tags.Tag"]()
mb_genre = [
{"name": "dnb", "id": "aaaac282-9075-4736-b1f9-7012404daaaa"},
{"name": tag1.name, "id": "2628c282-9075-4736-b1f9-7012404d09e8"},
{"name": tag2.name, "id": "2628c282-9075-4736-b1f9-7012404daaaa"},
]
mocker.patch(
"funkwhale_api.tags.tasks.fetch_musicbrainz_genre", return_value=mb_genre
)
tasks.update_musicbrainz_genre()
assert (
str(models.Tag.objects.get(name="dnb").mbid)
== "aaaac282-9075-4736-b1f9-7012404daaaa"
)
assert (
str(models.Tag.objects.get(name=tag2.name).mbid)
== "2628c282-9075-4736-b1f9-7012404daaaa"
)
assert (
str(models.Tag.objects.get(name=tag1.name).mbid)
== "2628c282-9075-4736-b1f9-7012404d09e8"
)
def test_sync_musicbrainz_tags(factories, mocker):
objs = [
factories["music.Artist"](mbid="2628c282-9075-4736-b1f9-7012404daaaa"),
factories["music.Track"](mbid="2628c282-9075-4736-b1f9-7012404daaaa"),
factories["music.Album"](mbid="2628c282-9075-4736-b1f9-7012404dacab"),
]
obj_map = {"Artist": "artists", "Track": "recordings", "Album": "releases"}
for obj in objs:
obj_type = obj.__class__.__name__
mocker.patch(
f"funkwhale_api.tags.tasks.musicbrainz.api.{obj_map[obj_type]}.get",
return_value={
obj_map[obj_type][:-1]: {"tag-list": [{"name": "Amazing Tag"}]}
},
)
tasks.sync_fw_item_tag_with_musicbrainz_tags(obj)
obj.refresh_from_db()
assert obj.tagged_items.all()[0].tag.name == "Amazing Tag"
......@@ -19,6 +19,22 @@ def test_tags_list(factories, logged_in_api_client):
assert response.data == expected
def test_tags_list_filter(factories, logged_in_api_client):
url = reverse("api:v1:tags-list") + "?name_icontains=fz"
tag = factories["tags.Tag"](name="fzl")
expected = {
"count": 1,
"next": None,
"previous": None,
"results": [serializers.TagSerializer(tag).data],
}
response = logged_in_api_client.get(url)
assert response.data == expected
def test_tags_list_ordering_length(factories, logged_in_api_client):
url = reverse("api:v1:tags-list")
tags = [
......
from django.urls import reverse
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
def test_can_authenticate_using_jwt_token_param_in_url(factories, preferences, client):
user = factories["users.User"]()
preferences["common__api_authentication_required"] = True
url = reverse("api:v1:tracks-list")
response = client.get(url)
assert response.status_code == 401
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
response = client.get(url, data={"jwt": token})
assert response.status_code == 200
def test_can_authenticate_using_oauth_token_param_in_url(
......
......@@ -52,9 +52,9 @@ def test_import_files_stores_proper_data(factories, mocker, now, path):
"import_files", str(library.uuid), path, async_=False, interactive=False
)
upload = library.uploads.last()
assert upload.import_reference == "cli-{}".format(now.isoformat())
assert upload.import_reference == f"cli-{now.isoformat()}"
assert upload.import_status == "pending"
assert upload.source == "file://{}".format(path)
assert upload.source == f"file://{path}"
assert upload.import_metadata == {
"funkwhale": {
"config": {"replace": False, "dispatch_outbox": False, "broadcast": False}
......@@ -131,7 +131,7 @@ def test_import_files_skip_if_path_already_imported(factories, mocker):
# existing one with same source
factories["music.Upload"](
library=library, import_status="finished", source="file://{}".format(path)
library=library, import_status="finished", source=f"file://{path}"
)
call_command(
......@@ -154,7 +154,7 @@ def test_import_files_in_place(factories, mocker, settings):
interactive=False,
)
upload = library.uploads.last()
assert bool(upload.audio_file) is False
assert not upload.audio_file
mocked_process.assert_called_once_with(upload_id=upload.pk)
......@@ -165,7 +165,7 @@ def test_storage_rename_utf_8_files(factories):
@pytest.mark.parametrize("name", ["modified", "moved", "created", "deleted"])
def test_handle_event(name, mocker):
handler = mocker.patch.object(import_files, "handle_{}".format(name))
handler = mocker.patch.object(import_files, f"handle_{name}")
event = {"type": name}
stdout = mocker.Mock()
......@@ -297,7 +297,10 @@ def test_handle_modified_skips_existing_checksum(tmpfile, factories, mocker):
import_status="finished",
)
import_files.handle_modified(
event=event, stdout=stdout, library=library, in_place=True,
event=event,
stdout=stdout,
library=library,
in_place=True,
)
assert library.uploads.count() == 1
......@@ -322,10 +325,14 @@ def test_handle_modified_update_existing_path_if_found(tmpfile, factories, mocke
audio_file=None,
)
import_files.handle_modified(
event=event, stdout=stdout, library=library, in_place=True,
event=event,
stdout=stdout,
library=library,
in_place=True,
)
update_track_metadata.assert_called_once_with(
get_metadata.return_value, upload.track,
get_metadata.return_value,
upload.track,
)
......@@ -349,6 +356,23 @@ def test_handle_modified_update_existing_path_if_found_and_attributed_to(
audio_file=None,
)
import_files.handle_modified(
event=event, stdout=stdout, library=library, in_place=True,
event=event,
stdout=stdout,
library=library,
in_place=True,
)
update_track_metadata.assert_not_called()
def test_import_files(factories, capsys):
# smoke test to ensure the command run properly
library = factories["music.Library"](actor__local=True)
call_command(
"import_files", str(library.uuid), DATA_DIR, interactive=False, recursive=True
)
captured = capsys.readouterr()
imported = library.uploads.filter(import_status="finished").count()
assert imported > 0
assert f"Successfully imported {imported} new tracks" in captured.out
assert "For details, please refer to import reference" in captured.out
def test_version():
from funkwhale_api import __version__, version
assert isinstance(version, str)
assert version == __version__
......@@ -2,5 +2,4 @@ from . import utils as test_utils
def test_to_api_date(now):
assert test_utils.to_api_date(now) == now.isoformat().split("+")[0] + "Z"
from django.urls import reverse
def test_can_resolve_v1():
path = reverse("api:v1:instance:nodeinfo-2.0")
assert path == "/api/v1/instance/nodeinfo/2.0"
def test_can_resolve_subsonic():
path = reverse("api:subsonic:subsonic-ping")
assert path == "/api/subsonic/rest/ping"
def test_can_resolve_v2():
path = reverse("api:v2:instance:nodeinfo-2.1")
assert path == "/api/v2/instance/nodeinfo/2.1"