Make library drop migration use playlist.library instead of user follow and resolve tests

This commit is contained in:
Petitminion 2025-03-11 17:32:39 +01:00
parent e8f30f6fbb
commit c33550d3d4
9 changed files with 65 additions and 91 deletions

View File

@ -106,7 +106,7 @@ class PrivacyLevelPermission(BasePermission):
elif privacy_level == "me" and obj_actor == request_actor: elif privacy_level == "me" and obj_actor == request_actor:
return True return True
elif request_actor in obj.user.actor.get_approved_followers(): elif request_actor in obj_actor.get_approved_followers():
return True return True
else: else:
return False return False

View File

@ -1,7 +1,6 @@
# Generated by Django 4.2.9 on 2025-01-03 16:12 # Generated by Django 4.2.9 on 2025-01-03 16:12
from django.db import migrations, models from django.db import migrations, models
from django.db import IntegrityError
from funkwhale_api.federation import utils as federation_utils from funkwhale_api.federation import utils as federation_utils
from django.urls import reverse from django.urls import reverse
@ -19,7 +18,7 @@ def insert_tracks_to_playlist(apps, playlist, uploads):
uuid=(new_uuid := uuid.uuid4()), uuid=(new_uuid := uuid.uuid4()),
fid=federation_utils.full_url( fid=federation_utils.full_url(
reverse( reverse(
f"federation:music:playlist-tracks-detail", "federation:music:playlist-tracks-detail",
kwargs={"uuid": new_uuid}, kwargs={"uuid": new_uuid},
) )
), ),
@ -34,49 +33,36 @@ def insert_tracks_to_playlist(apps, playlist, uploads):
def migrate_libraries_to_playlist(apps, schema_editor): def migrate_libraries_to_playlist(apps, schema_editor):
Playlist = apps.get_model("playlists", "Playlist") Playlist = apps.get_model("playlists", "Playlist")
Library = apps.get_model("music", "Library") Library = apps.get_model("music", "Library")
LibraryFollow = apps.get_model("federation", "LibraryFollow")
Follow = apps.get_model("federation", "Follow")
User = apps.get_model("users", "User")
Actor = apps.get_model("federation", "Actor") Actor = apps.get_model("federation", "Actor")
# library to playlist
for library in Library.objects.all(): for library in Library.objects.all():
playlist = Playlist.objects.create( try:
name=library.name, playlist = Playlist.objects.create(
actor=library.actor, name=library.name,
creation_date=library.creation_date, library=library,
privacy_level=library.privacy_level, actor=library.actor,
description=library.description, creation_date=library.creation_date,
uuid=(new_uuid := uuid.uuid4()), privacy_level=library.privacy_level,
fid=federation_utils.full_url( description=library.description,
reverse( uuid=(new_uuid := uuid.uuid4()),
f"federation:music:playlists-detail", fid=federation_utils.full_url(
kwargs={"uuid": new_uuid}, reverse(
) "federation:music:playlists-detail",
), kwargs={"uuid": new_uuid},
) )
playlist.save() ),
)
playlist.save()
if library.uploads.all().exists(): if library.uploads.all().exists():
insert_tracks_to_playlist(apps, playlist, library.uploads.all()) uploads = library.uploads.all()
insert_tracks_to_playlist(apps, playlist, uploads)
playlist.library.playlist_uploads.set(uploads)
except Exception as e:
print(f"An error occurred during library.playlist creation {e}")
continue
# library follows to user follow # migrate uploads to new built-in libraries
for lib_follow in LibraryFollow.objects.filter(target=library):
try:
Follow.objects.create(
uuid=lib_follow.uuid,
target=library.actor,
actor=lib_follow.actor,
approved=lib_follow.approved,
creation_date=lib_follow.creation_date,
modification_date=lib_follow.modification_date,
)
except IntegrityError:
pass
LibraryFollow.objects.all().delete()
# migrate uploads to new library
for actor in Actor.objects.all(): for actor in Actor.objects.all():
privacy_levels = ["me", "instance", "everyone"] privacy_levels = ["me", "instance", "everyone"]
for privacy_level in privacy_levels: for privacy_level in privacy_levels:
@ -87,23 +73,30 @@ def migrate_libraries_to_playlist(apps, schema_editor):
uuid=(new_uuid := uuid.uuid4()), uuid=(new_uuid := uuid.uuid4()),
fid=federation_utils.full_url( fid=federation_utils.full_url(
reverse( reverse(
f"federation:music:libraries-detail", "federation:music:libraries-detail",
kwargs={"uuid": new_uuid}, kwargs={"uuid": new_uuid},
) )
), ),
) )
for library in actor.libraries.filter(privacy_level=privacy_level): for library in actor.libraries.filter(privacy_level=privacy_level):
library.uploads.all().update(library=build_in_lib) library.uploads.all().update(library=build_in_lib)
if library.pk is not build_in_lib.pk:
library.delete()
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("music", "0060_empty_for_test"), ("music", "0060_empty_for_test"),
("playlists", "0008_playlist_library_drop"), ("playlists", "0009_playlist_library"),
] ]
operations = [ operations = [
migrations.AddField(
model_name="upload",
name="playlist_libraries",
field=models.ManyToManyField(
blank=True,
related_name="playlist_uploads",
to="music.library",
),
),
migrations.RunPython( migrations.RunPython(
migrate_libraries_to_playlist, reverse_code=migrations.RunPython.noop migrate_libraries_to_playlist, reverse_code=migrations.RunPython.noop
), ),

View File

@ -1,23 +0,0 @@
# Generated by Django 5.1.6 on 2025-03-09 21:58
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("music", "0063_upload_third_party_provider"),
]
operations = [
migrations.AddField(
model_name="upload",
name="playlist_libraries",
field=models.ManyToManyField(
blank=True,
null=True,
related_name="playlist_uploads",
to="music.library",
),
),
]

View File

@ -830,7 +830,6 @@ class Upload(models.Model):
playlist_libraries = models.ManyToManyField( playlist_libraries = models.ManyToManyField(
"library", "library",
null=True, null=True,
blank=True,
related_name="playlist_uploads", related_name="playlist_uploads",
) )

View File

@ -16,18 +16,21 @@ def create_playlist_libraries(apps, schema_editor):
for playlist in Playlist.objects.all(): for playlist in Playlist.objects.all():
library = playlist.library library = playlist.library
if library is None: if library is None:
library = Library.objects.create( try:
name=playlist.name, privacy_level="me", actor=playlist.actor library = Library.objects.create(
) name=playlist.name, privacy_level="me", actor=playlist.actor
library.save() )
playlist.library = library library.save()
playlist.save() playlist.library = library
add_uploads_to_pl_library(playlist, library) playlist.save()
add_uploads_to_pl_library(playlist, library)
except Exception as e:
print(f"An error occurred during playlist.library creation {e}")
continue
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("music", "0063_upload_third_party_provider"),
("playlists", "0008_playlist_library_drop"), ("playlists", "0008_playlist_library_drop"),
] ]

View File

@ -98,9 +98,10 @@ def test_privacylevel_permission_me(
assert check is expected assert check is expected
# "me" expects true since the object can be private but share with followers
@pytest.mark.parametrize( @pytest.mark.parametrize(
"privacy_level,expected", "privacy_level,expected",
[("me", False), ("followers", True), ("instance", False), ("everyone", True)], [("me", True), ("followers", True), ("instance", False), ("everyone", True)],
) )
def test_privacylevel_permission_followers( def test_privacylevel_permission_followers(
factories, api_request, anonymous_user, privacy_level, expected, mocker factories, api_request, anonymous_user, privacy_level, expected, mocker

View File

@ -107,8 +107,6 @@ def test_migrate_libraries_to_playlist(migrator):
domain = Domain.objects.create() domain = Domain.objects.create()
domain2 = Domain.objects.create(pk=2) domain2 = Domain.objects.create(pk=2)
actor = Actor.objects.create(name="Test Actor", domain=domain) actor = Actor.objects.create(name="Test Actor", domain=domain)
existing_urls = Actor.objects.values_list("fid", flat=True)
print(existing_urls)
target_actor = Actor.objects.create( target_actor = Actor.objects.create(
name="Test Actor 2", name="Test Actor 2",
domain=domain2, domain=domain2,
@ -152,9 +150,7 @@ def test_migrate_libraries_to_playlist(migrator):
new_apps = migrator.loader.project_state([music_final_migration]).apps new_apps = migrator.loader.project_state([music_final_migration]).apps
Playlist = new_apps.get_model("playlists", "Playlist") Playlist = new_apps.get_model("playlists", "Playlist")
PlaylistTrack = new_apps.get_model("playlists", "PlaylistTrack") PlaylistTrack = new_apps.get_model("playlists", "PlaylistTrack")
Follow = new_apps.get_model("federation", "Follow")
LibraryFollow = new_apps.get_model("federation", "LibraryFollow") LibraryFollow = new_apps.get_model("federation", "LibraryFollow")
Follow = new_apps.get_model("federation", "Follow")
# Assertions # Assertions
@ -172,13 +168,17 @@ def test_migrate_libraries_to_playlist(migrator):
for i, playlist_track in enumerate(playlist_tracks): for i, playlist_track in enumerate(playlist_tracks):
assert playlist_track.track.pk == uploads[i].track.pk assert playlist_track.track.pk == uploads[i].track.pk
# Verify User Follow creation # Verify playlist.library Follow creation
follow = Follow.objects.get(target__pk=target_actor.pk) follow = LibraryFollow.objects.get(target__pk=playlist.library.pk)
assert follow.actor.pk == actor.pk assert follow.actor.pk == actor.pk
assert follow.approved == library_follow.approved assert follow.approved == library_follow.approved
assert follow.target == playlist.library
# Verify LibraryFollow deletion and library creation # Verify uploads are migrated in lib.playlist_uploads
assert LibraryFollow.objects.count() == 0 for upload in uploads:
assert upload.pk in [u.pk for u in playlist.library.playlist_uploads.all()]
assert upload.pk not in [u.pk for u in playlist.library.uploads.all()]
assert not playlist.library.uploads.all()
# Test fail but works on real db I don't get why # Test fail but works on real db I don't get why
# no library are found in the new app # no library are found in the new app

View File

@ -15,14 +15,15 @@ Users will be able to click on a "Request access to playlist audios files" butto
#### Backend #### Backend
- [x] ``PlaylistViewSet` `add` `clear` `remove` update the uploads.playlist_libraries relationships - [x] `PlaylistViewSet` `add` `clear` `remove` update the uploads.playlist_libraries relationships
- [x] ``PlaylistViewSet` `add` `clear` `remove` -> `schedule_scan` -> Update activity to remote -> playlist.library scan on remote - [x] `PlaylistViewSet` `add` `clear` `remove` -> `schedule_scan` -> Update activity to remote -> playlist.library scan on remote
- [x] library and playlist scan delay are long (24h), force on ap update - [x] library and playlist scan delay are long (24h), force on ap update
- [ ] make sure only owned upload are added to the playlist.library - [x] make sure only owned upload are added to the playlist.library
- [ ] update the "drop library" migrations to use the playlist.library instead of user follow - [x] update the "drop library" migrations to use the playlist.library instead of user follow
- [ ] make sure user get the new libraries created after library drop - [ ] make sure user get the new libraries created after library drop
### Follow up ### Follow up
- [ ] Finish library drop (delete libraries endpoints)
- [ ] Playlist discovery : fetch federation endpoint for playlists - [ ] Playlist discovery : fetch federation endpoint for playlists
- [ ] Playlist discovery : add the playlist to my playlist collection = follow request to playlist - [ ] Playlist discovery : add the playlist to my playlist collection = follow request to playlist

View File

@ -3216,7 +3216,7 @@
}, },
"useErrorHandler": { "useErrorHandler": {
"errorReportMessage": "To help us understand why it happened, please attach a detailed description of what you did that has triggered the error.", "errorReportMessage": "To help us understand why it happened, please attach a detailed description of what you did that has triggered the error.",
"errorReportTitle": "An unexpected error occured.", "errorReportTitle": "An unexpected error occurred.",
"leaveFeedback": "Leave feedback", "leaveFeedback": "Leave feedback",
"unexpectedError": "An unexpected error occurred." "unexpectedError": "An unexpected error occurred."
}, },