diff --git a/api/funkwhale_api/music/management/commands/prune_non_mbid_content.py b/api/funkwhale_api/music/management/commands/prune_non_mbid_content.py new file mode 100644 index 000000000..b60ed129e --- /dev/null +++ b/api/funkwhale_api/music/management/commands/prune_non_mbid_content.py @@ -0,0 +1,61 @@ +from django.core.management.base import BaseCommand +from django.db import transaction + +from funkwhale_api.music import models + + +class Command(BaseCommand): + help = """Deletes any tracks not tagged with a MusicBrainz ID from the database. By default, any tracks that + have been favorited by a user or added to a playlist are preserved.""" + + def add_arguments(self, parser): + parser.add_argument( + "--no-dry-run", + action="store_true", + dest="no_dry_run", + default=True, + help="Disable dry run mode and apply pruning for real on the database", + ) + + parser.add_argument( + "--include-playlist-content", + action="store_true", + dest="include_playlist_content", + default=False, + help="Allow tracks included in playlists to be pruned", + ) + + parser.add_argument( + "--include-favorites-content", + action="store_true", + dest="include_favorited_content", + default=False, + help="Allow favorited tracks to be pruned", + ) + + parser.add_argument( + "--include-listened-content", + action="store_true", + dest="include_listened_content", + default=False, + help="Allow tracks with listening history to be pruned", + ) + + @transaction.atomic + def handle(self, *args, **options): + tracks = models.Track.objects.filter(mbid__isnull=True) + if not options["include_favorited_content"]: + tracks = tracks.filter(track_favorites__isnull=True) + if not options["include_playlist_content"]: + tracks = tracks.filter(playlist_tracks__isnull=True) + if not options["include_listened_content"]: + tracks = tracks.filter(listenings__isnull=True) + + pruned_total = tracks.count() + total = models.Track.objects.count() + + if options["no_dry_run"]: + self.stdout.write(f"Deleting {pruned_total}/{total} tracks…") + tracks.delete() + else: + self.stdout.write(f"Would prune {pruned_total}/{total} tracks") diff --git a/api/tests/music/test_commands.py b/api/tests/music/test_commands.py index adee497b6..c46d5dad1 100644 --- a/api/tests/music/test_commands.py +++ b/api/tests/music/test_commands.py @@ -7,6 +7,7 @@ from funkwhale_api.music.management.commands import ( check_inplace_files, fix_uploads, prune_library, + prune_non_mbid_content, ) DATA_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -204,3 +205,45 @@ def test_check_inplace_files_no_dry_run(factories, tmpfile): for u in not_prunable: u.refresh_from_db() + + +def test_prune_non_mbid_content(factories): + prunable = factories["music.Track"](mbid=None) + + track = factories["music.Track"](mbid=None) + factories["playlists.PlaylistTrack"](track=track) + not_prunable = [factories["music.Track"](), track] + c = prune_non_mbid_content.Command() + options = { + "include_playlist_content": False, + "include_listened_content": False, + "include_favorited_content": True, + "no_dry_run": True, + } + c.handle(**options) + + with pytest.raises(prunable.DoesNotExist): + prunable.refresh_from_db() + + for t in not_prunable: + t.refresh_from_db() + + track = factories["music.Track"](mbid=None) + factories["playlists.PlaylistTrack"](track=track) + prunable = [factories["music.Track"](mbid=None), track] + + not_prunable = [factories["music.Track"]()] + options = { + "include_playlist_content": True, + "include_listened_content": False, + "include_favorited_content": False, + "no_dry_run": True, + } + c.handle(**options) + + for t in prunable: + with pytest.raises(t.DoesNotExist): + t.refresh_from_db() + + for t in not_prunable: + t.refresh_from_db() diff --git a/changes/changelog.d/2083.enhancement b/changes/changelog.d/2083.enhancement new file mode 100644 index 000000000..62a5c7482 --- /dev/null +++ b/changes/changelog.d/2083.enhancement @@ -0,0 +1 @@ +Add cli command to prune non mbid content from db (#2083)