resolving real migration issues
This commit is contained in:
parent
29965702e4
commit
ef0ee1cbc5
|
@ -1,4 +1,5 @@
|
||||||
from funkwhale_api.common import admin
|
from funkwhale_api.common import admin
|
||||||
|
from funkwhale_api.playlists import models as playlist_models
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
@ -98,6 +99,14 @@ class UploadAdmin(admin.ModelAdmin):
|
||||||
]
|
]
|
||||||
list_filter = ["mimetype", "import_status", "library__privacy_level"]
|
list_filter = ["mimetype", "import_status", "library__privacy_level"]
|
||||||
|
|
||||||
|
def formfield_for_manytomany(self, db_field, request, **kwargs):
|
||||||
|
if db_field.name == "playlist_libraries":
|
||||||
|
object_id = request.resolver_match.kwargs.get("object_id")
|
||||||
|
kwargs["queryset"] = playlist_models.Playlist.objects.filter(
|
||||||
|
playlist_tracks__track__uploads=object_id
|
||||||
|
)
|
||||||
|
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.UploadVersion)
|
@admin.register(models.UploadVersion)
|
||||||
class UploadVersionAdmin(admin.ModelAdmin):
|
class UploadVersionAdmin(admin.ModelAdmin):
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# 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, transaction
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
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
|
||||||
|
@ -9,8 +10,9 @@ import uuid
|
||||||
|
|
||||||
def insert_tracks_to_playlist(apps, playlist, uploads):
|
def insert_tracks_to_playlist(apps, playlist, uploads):
|
||||||
PlaylistTrack = apps.get_model("playlists", "PlaylistTrack")
|
PlaylistTrack = apps.get_model("playlists", "PlaylistTrack")
|
||||||
plts = [
|
for i, upload in enumerate(uploads):
|
||||||
PlaylistTrack(
|
if upload.track:
|
||||||
|
PlaylistTrack.objects.create(
|
||||||
creation_date=playlist.creation_date,
|
creation_date=playlist.creation_date,
|
||||||
playlist=playlist,
|
playlist=playlist,
|
||||||
track=upload.track,
|
track=upload.track,
|
||||||
|
@ -23,21 +25,28 @@ def insert_tracks_to_playlist(apps, playlist, uploads):
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for i, upload in enumerate(uploads)
|
upload.library = None
|
||||||
if upload.track
|
upload.save()
|
||||||
]
|
|
||||||
|
|
||||||
return PlaylistTrack.objects.bulk_create(plts)
|
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
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")
|
||||||
Actor = apps.get_model("federation", "Actor")
|
Actor = apps.get_model("federation", "Actor")
|
||||||
|
Channel = apps.get_model("audio", "Channel")
|
||||||
|
|
||||||
for library in Library.objects.all():
|
for library in Library.objects.all():
|
||||||
|
if (
|
||||||
|
Channel.objects.filter(library=library).exists()
|
||||||
|
or library.name.startswith("playlist_")
|
||||||
|
or Playlist.objects.filter(library=library).exists()
|
||||||
|
or not federation_utils.is_local(library.fid)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
playlist = Playlist.objects.create(
|
playlist, created = Playlist.objects.get_or_create(
|
||||||
name=library.name,
|
name=library.name,
|
||||||
library=library,
|
library=library,
|
||||||
actor=library.actor,
|
actor=library.actor,
|
||||||
|
@ -56,17 +65,21 @@ def migrate_libraries_to_playlist(apps, schema_editor):
|
||||||
|
|
||||||
if library.uploads.all().exists():
|
if library.uploads.all().exists():
|
||||||
uploads = library.uploads.all()
|
uploads = library.uploads.all()
|
||||||
|
with transaction.atomic():
|
||||||
insert_tracks_to_playlist(apps, playlist, uploads)
|
insert_tracks_to_playlist(apps, playlist, uploads)
|
||||||
playlist.library.playlist_uploads.set(uploads)
|
playlist.library.playlist_uploads.set(uploads)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred during library.playlist creation {e}")
|
print(f"An error occurred during library.playlist creation : {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# migrate uploads to new built-in libraries
|
# migrate uploads to new built-in libraries
|
||||||
for actor in Actor.objects.all():
|
for actor in Actor.objects.all():
|
||||||
|
if not federation_utils.is_local(actor.fid):
|
||||||
|
continue
|
||||||
|
|
||||||
privacy_levels = ["me", "instance", "everyone"]
|
privacy_levels = ["me", "instance", "everyone"]
|
||||||
for privacy_level in privacy_levels:
|
for privacy_level in privacy_levels:
|
||||||
build_in_lib = Library.objects.create(
|
build_in_lib, created = Library.objects.get_or_create(
|
||||||
actor=actor,
|
actor=actor,
|
||||||
privacy_level=privacy_level,
|
privacy_level=privacy_level,
|
||||||
name=privacy_level,
|
name=privacy_level,
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.conf import settings
|
||||||
|
|
||||||
from funkwhale_api.factories import NoUpdateOnCreate, registry
|
from funkwhale_api.factories import NoUpdateOnCreate, registry
|
||||||
from funkwhale_api.federation import models
|
from funkwhale_api.federation import models
|
||||||
from funkwhale_api.federation.factories import ActorFactory
|
from funkwhale_api.federation.factories import ActorFactory, MusicLibraryFactory
|
||||||
from funkwhale_api.music.factories import TrackFactory
|
from funkwhale_api.music.factories import TrackFactory
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,9 +13,7 @@ class PlaylistFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
||||||
actor = factory.SubFactory(ActorFactory)
|
actor = factory.SubFactory(ActorFactory)
|
||||||
fid = factory.Faker("federation_url")
|
fid = factory.Faker("federation_url")
|
||||||
uuid = factory.Faker("uuid4")
|
uuid = factory.Faker("uuid4")
|
||||||
library = factory.SubFactory(
|
library = factory.SubFactory(MusicLibraryFactory)
|
||||||
"funkwhale_api.federation.factories.MusicLibraryFactory"
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = "playlists.Playlist"
|
model = "playlists.Playlist"
|
||||||
|
|
|
@ -1,32 +1,43 @@
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models, transaction
|
||||||
|
from funkwhale_api.federation import utils as federation_utils
|
||||||
|
|
||||||
|
|
||||||
# to do : test migration
|
# to do : test migration
|
||||||
def add_uploads_to_pl_library(playlist, library):
|
def add_uploads_to_pl_library(playlist, library):
|
||||||
for plt in playlist.playlist_tracks.all():
|
for plt in playlist.playlist_tracks.all():
|
||||||
for upload in plt.track.uploads.filter(library__actor=playlist.actor):
|
for upload in plt.track.uploads.filter(library__actor=playlist.actor):
|
||||||
library.uploads.add(upload)
|
library.playlist_uploads.add(upload)
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def create_playlist_libraries(apps, schema_editor):
|
def create_playlist_libraries(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")
|
||||||
|
|
||||||
for playlist in Playlist.objects.all():
|
for playlist in Playlist.objects.all():
|
||||||
|
if not federation_utils.is_local(playlist.fid):
|
||||||
|
continue
|
||||||
library = playlist.library
|
library = playlist.library
|
||||||
if library is None:
|
if not library:
|
||||||
try:
|
try:
|
||||||
|
# we don't want to get_or_create in case it's a channel lib
|
||||||
library = Library.objects.create(
|
library = Library.objects.create(
|
||||||
name=playlist.name, privacy_level="me", actor=playlist.actor
|
name="playlist_" + playlist.name,
|
||||||
|
privacy_level="me",
|
||||||
|
actor=playlist.actor,
|
||||||
)
|
)
|
||||||
library.save()
|
library.save()
|
||||||
playlist.library = library
|
playlist.library = library
|
||||||
playlist.save()
|
playlist.save()
|
||||||
|
with transaction.atomic():
|
||||||
add_uploads_to_pl_library(playlist, library)
|
add_uploads_to_pl_library(playlist, library)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred during playlist.library creation {e}")
|
print(
|
||||||
continue
|
f"An error occurred during playlist.library creation, raising since we want\
|
||||||
|
to enforce one lib per playlist"
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -49,13 +60,4 @@ class Migration(migrations.Migration):
|
||||||
migrations.RunPython(
|
migrations.RunPython(
|
||||||
create_playlist_libraries, reverse_code=migrations.RunPython.noop
|
create_playlist_libraries, reverse_code=migrations.RunPython.noop
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
|
||||||
model_name="playlist",
|
|
||||||
name="library",
|
|
||||||
field=models.OneToOneField(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="playlist",
|
|
||||||
to="music.library",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -90,6 +90,8 @@ class Playlist(federation_models.FederationMixin):
|
||||||
federation_namespace = "playlists"
|
federation_namespace = "playlists"
|
||||||
library = models.OneToOneField(
|
library = models.OneToOneField(
|
||||||
"music.Library",
|
"music.Library",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name="playlist",
|
related_name="playlist",
|
||||||
)
|
)
|
||||||
|
|
|
@ -122,6 +122,16 @@ def test_migrate_libraries_to_playlist(migrator):
|
||||||
description="This is a description",
|
description="This is a description",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
library_not_local = Library.objects.create(
|
||||||
|
name="This should becane playlist name",
|
||||||
|
fid="https://asupernotlocal.acab/federation/music/libraries/8505207e-45da-449a-9ec8-ed12a848fcea",
|
||||||
|
actor=target_actor,
|
||||||
|
creation_date=now(),
|
||||||
|
privacy_level="everyone",
|
||||||
|
uuid=uuid4(),
|
||||||
|
description="This is a description recalling to eat the rich",
|
||||||
|
)
|
||||||
|
|
||||||
Track.objects.create()
|
Track.objects.create()
|
||||||
Track.objects.create()
|
Track.objects.create()
|
||||||
track = Track.objects.create()
|
track = Track.objects.create()
|
||||||
|
@ -134,6 +144,10 @@ def test_migrate_libraries_to_playlist(migrator):
|
||||||
Upload.objects.create(library=library, track=track3),
|
Upload.objects.create(library=library, track=track3),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Upload.objects.create(library=library_not_local, track=track),
|
||||||
|
Upload.objects.create(library=library_not_local, track=track2),
|
||||||
|
Upload.objects.create(library=library_not_local, track=track3),
|
||||||
|
|
||||||
library_follow = LibraryFollow.objects.create(
|
library_follow = LibraryFollow.objects.create(
|
||||||
uuid=uuid4(),
|
uuid=uuid4(),
|
||||||
target=library,
|
target=library,
|
||||||
|
@ -151,6 +165,7 @@ def test_migrate_libraries_to_playlist(migrator):
|
||||||
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")
|
||||||
LibraryFollow = new_apps.get_model("federation", "LibraryFollow")
|
LibraryFollow = new_apps.get_model("federation", "LibraryFollow")
|
||||||
|
Library = new_apps.get_model("music", "Library")
|
||||||
|
|
||||||
# Assertions
|
# Assertions
|
||||||
|
|
||||||
|
@ -180,6 +195,10 @@ def test_migrate_libraries_to_playlist(migrator):
|
||||||
assert upload.pk not in [u.pk for u in playlist.library.uploads.all()]
|
assert upload.pk not in [u.pk for u in playlist.library.uploads.all()]
|
||||||
assert not playlist.library.uploads.all()
|
assert not playlist.library.uploads.all()
|
||||||
|
|
||||||
|
# Not local
|
||||||
|
library_not_local = Library.objects.get(fid=library_not_local.fid)
|
||||||
|
assert not library_not_local.playlist_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
|
||||||
# NewAppLibrary = new_apps.get_model("music", "Library")
|
# NewAppLibrary = new_apps.get_model("music", "Library")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
## Playlist Import export
|
## Playlist libraries to share audio files
|
||||||
|
|
||||||
### The Issue
|
### The Issue
|
||||||
|
|
||||||
|
@ -15,12 +15,28 @@ Users will be able to click on a "Request access to playlist audios files" butto
|
||||||
|
|
||||||
#### Backend
|
#### Backend
|
||||||
|
|
||||||
|
##### Data model
|
||||||
|
|
||||||
|
`Playlist` one_to_one with `Library` through `library` field
|
||||||
|
`Upload` many_to_one with `Library` through `library` (reverse is `library.uploads`)
|
||||||
|
`Upload` has also a many_to_many with `Library` through `playlist_libraries` (the same upload can be share various time in through various playlists). Reverse relation is `library.playlist_uploads`
|
||||||
|
|
||||||
|
##### Migrations
|
||||||
|
|
||||||
|
1. Remote library are not changed
|
||||||
|
2. Local lib are not deleted but are assigned to a playlist
|
||||||
|
3. Libraries Follows are not touched
|
||||||
|
4. Remote want fetch local libs as always but they will need to update the data or fail (migrating uploads from `library` to `playlist_library`)
|
||||||
|
|
||||||
|
##### Done
|
||||||
|
|
||||||
- [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
|
||||||
- [x] make sure only owned upload are added to the playlist.library
|
- [x] make sure only owned upload are added to the playlist.library
|
||||||
- [x] 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
|
||||||
|
- [ ] update the federation api to send the playlist_library info
|
||||||
|
|
||||||
### Follow up
|
### Follow up
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue